feat(i18n): localize loose components directly under components/ (#2317)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import { useState } from "react";
|
||||
import { IconRefresh } from "@tabler/icons-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
type BranchRefreshButtonProps = {
|
||||
onRefresh: () => void;
|
||||
@@ -23,18 +25,19 @@ export function BranchRefreshButton({
|
||||
testId = "branch-refresh-button",
|
||||
touchTarget = false,
|
||||
}: BranchRefreshButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
// Controlled open so the tooltip only reacts to hover, not focus.
|
||||
// Radix Popover auto-focuses the first focusable child when it opens, which
|
||||
// would otherwise trigger this tooltip the moment the dropdown is opened.
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasError = Boolean(fetchError);
|
||||
const tooltip = formatRefreshTooltip(label, fetchedAt, refreshing, fetchError);
|
||||
const tooltip = formatRefreshTooltip(t, label, fetchedAt, refreshing, fetchError);
|
||||
return (
|
||||
<Tooltip open={open} onOpenChange={setOpen}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Refresh ${label}`}
|
||||
aria-label={t("common:refreshAriaLabel", { label })}
|
||||
data-testid={testId}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -58,20 +61,26 @@ export function BranchRefreshButton({
|
||||
);
|
||||
}
|
||||
|
||||
// `label` stays untranslated: it is compared with `===` below, and callers pass
|
||||
// it as a free-form noun ("branches", "repositories"). It travels through
|
||||
// interpolation instead.
|
||||
function formatRefreshTooltip(
|
||||
t: TFunction,
|
||||
label: string,
|
||||
fetchedAt: string | undefined,
|
||||
refreshing: boolean | undefined,
|
||||
fetchError: string | undefined,
|
||||
) {
|
||||
if (refreshing) return `Refreshing ${label}...`;
|
||||
if (fetchError) return `Last refresh failed: ${fetchError}`;
|
||||
if (!fetchedAt) return initialRefreshTooltip(label);
|
||||
if (refreshing) return t("common:refreshingLabel", { label });
|
||||
if (fetchError) return t("common:lastRefreshFailed", { error: fetchError });
|
||||
if (!fetchedAt) return initialRefreshTooltip(t, label);
|
||||
const date = new Date(fetchedAt);
|
||||
if (Number.isNaN(date.getTime())) return initialRefreshTooltip(label);
|
||||
return `Refresh ${label} (last fetched ${date.toLocaleTimeString()})`;
|
||||
if (Number.isNaN(date.getTime())) return initialRefreshTooltip(t, label);
|
||||
return t("common:refreshLabelLastFetched", { label, time: date.toLocaleTimeString() });
|
||||
}
|
||||
|
||||
function initialRefreshTooltip(label: string) {
|
||||
return label === "branches" ? "Refresh branches (git fetch)" : `Refresh ${label}`;
|
||||
function initialRefreshTooltip(t: TFunction, label: string) {
|
||||
return label === "branches"
|
||||
? t("common:refreshBranchesGitFetch")
|
||||
: t("common:refreshAriaLabel", { label });
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { IconTerminal2 } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Marks an agent profile that runs in CLI passthrough mode — the prompt is
|
||||
// auto-injected into a raw terminal rather than sent as a structured request.
|
||||
// Shown next to passthrough profiles in the manual create dialog and the
|
||||
// Linear/Jira/GitHub watcher dialogs so the two paths stay visually consistent.
|
||||
export function CliModeIcon({ className }: { className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<IconTerminal2
|
||||
className={className ?? "size-3.5 text-muted-foreground"}
|
||||
title="CLI mode — your prompt will be auto-injected into the terminal"
|
||||
title={t("common:cliModeYourPromptWillBe")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, type Dispatch, type SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { IconArchive, IconArrowRight, IconHammer, IconLoader2 } from "@tabler/icons-react";
|
||||
import {
|
||||
@@ -225,19 +226,25 @@ function CommandsListContent({
|
||||
repoMap,
|
||||
onTaskSelect,
|
||||
}: CommandsListContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const hasInlineResults = taskResults.length > 0 || isSearching;
|
||||
return (
|
||||
<>
|
||||
{!hasInlineResults && !isSearching && <CommandEmpty>No commands found.</CommandEmpty>}
|
||||
{!hasInlineResults && !isSearching && (
|
||||
<CommandEmpty>{t("common:noCommandsFound")}</CommandEmpty>
|
||||
)}
|
||||
{isSearching && taskResults.length === 0 && (
|
||||
<CommandGroup heading="Active Tasks" forceMount>
|
||||
<CommandGroup heading={t("common:activeTasks")} forceMount>
|
||||
<div className="flex items-center justify-center py-3">
|
||||
<IconLoader2 className="size-3.5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</CommandGroup>
|
||||
)}
|
||||
{taskResults.length > 0 && (
|
||||
<CommandGroup heading={search.trim() ? "Tasks" : "Active Tasks"} forceMount>
|
||||
<CommandGroup
|
||||
heading={search.trim() ? t("common:tasks") : t("common:activeTasks")}
|
||||
forceMount
|
||||
>
|
||||
{taskResults.map((task) => (
|
||||
<TaskResultItem
|
||||
key={task.id}
|
||||
@@ -283,6 +290,7 @@ function FileSearchContent({
|
||||
sessionId,
|
||||
onSelect,
|
||||
}: FileSearchContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const getRepoDisplayName = useRepoDisplayName(sessionId);
|
||||
if (isSearching && files.length === 0) {
|
||||
return (
|
||||
@@ -291,8 +299,9 @@ function FileSearchContent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (search.trim() && files.length === 0) return <CommandEmpty>No files found.</CommandEmpty>;
|
||||
if (!search.trim()) return <CommandEmpty>Type to search files...</CommandEmpty>;
|
||||
if (search.trim() && files.length === 0)
|
||||
return <CommandEmpty>{t("common:noFilesFound")}</CommandEmpty>;
|
||||
if (!search.trim()) return <CommandEmpty>{t("common:typeToSearchFiles")}</CommandEmpty>;
|
||||
|
||||
const groups = groupByRepositoryName(files, (file) => file.repository_name);
|
||||
const singleRepo = isSingleRepoGroup(groups);
|
||||
@@ -301,8 +310,10 @@ function FileSearchContent({
|
||||
key={group.repositoryName || "workspace"}
|
||||
heading={
|
||||
singleRepo
|
||||
? "Files"
|
||||
: (getRepoDisplayName(group.repositoryName) ?? group.repositoryName ?? "Workspace")
|
||||
? t("common:files")
|
||||
: (getRepoDisplayName(group.repositoryName) ??
|
||||
group.repositoryName ??
|
||||
t("common:workspace"))
|
||||
}
|
||||
forceMount
|
||||
data-testid="file-search-repo-group"
|
||||
@@ -333,27 +344,33 @@ function FileSearchContent({
|
||||
));
|
||||
}
|
||||
|
||||
function getInputPlaceholder(mode: CommandPanelMode, inputCommand: CommandItemType | null) {
|
||||
if (mode === "input") return inputCommand?.inputPlaceholder ?? "Enter value...";
|
||||
if (mode === "search-tasks") return "Search for tasks...";
|
||||
if (mode === MODE_SEARCH_FILES) return "Search for files...";
|
||||
if (mode === MODE_SEARCH_CONTENT) return "Search task contents…";
|
||||
return "Type a command...";
|
||||
// `mode` stays an untranslated discriminant — it is compared with `===`
|
||||
// throughout. Only the labels it selects are copy.
|
||||
function getInputPlaceholder(
|
||||
t: TFunction,
|
||||
mode: CommandPanelMode,
|
||||
inputCommand: CommandItemType | null,
|
||||
) {
|
||||
if (mode === "input") return inputCommand?.inputPlaceholder ?? t("common:enterValue");
|
||||
if (mode === "search-tasks") return t("common:searchForTasks");
|
||||
if (mode === MODE_SEARCH_FILES) return t("common:searchForFiles");
|
||||
if (mode === MODE_SEARCH_CONTENT) return t("common:searchTaskContents");
|
||||
return t("common:typeACommand");
|
||||
}
|
||||
|
||||
function getEnterLabel(mode: CommandPanelMode) {
|
||||
if (mode === "input") return "Confirm";
|
||||
function getEnterLabel(t: TFunction, mode: CommandPanelMode) {
|
||||
if (mode === "input") return t("common:confirm");
|
||||
if (mode === "search-tasks" || mode === MODE_SEARCH_FILES || mode === MODE_SEARCH_CONTENT) {
|
||||
return "Open";
|
||||
return t("common:open");
|
||||
}
|
||||
return "Select";
|
||||
return t("common:select");
|
||||
}
|
||||
|
||||
function getModeLabel(mode: CommandPanelMode, inputCommand: CommandItemType | null) {
|
||||
function getModeLabel(t: TFunction, mode: CommandPanelMode, inputCommand: CommandItemType | null) {
|
||||
if (mode === "input") return inputCommand?.label;
|
||||
if (mode === "search-tasks") return "Tasks";
|
||||
if (mode === MODE_SEARCH_FILES) return "Files";
|
||||
if (mode === MODE_SEARCH_CONTENT) return "Contents";
|
||||
if (mode === "search-tasks") return t("common:tasks");
|
||||
if (mode === MODE_SEARCH_FILES) return t("common:files");
|
||||
if (mode === MODE_SEARCH_CONTENT) return t("common:contents");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -364,6 +381,7 @@ function CommandPanelFooter({
|
||||
mode: CommandPanelMode;
|
||||
workspaceSearchAvailable: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isScopeMode = isCommandPanelScopeMode(mode);
|
||||
const canSwitchScope = workspaceSearchAvailable && isScopeMode;
|
||||
return (
|
||||
@@ -373,29 +391,30 @@ function CommandPanelFooter({
|
||||
<KbdGroup>
|
||||
<Kbd>↑</Kbd>
|
||||
<Kbd>↓</Kbd>
|
||||
<span>Navigate</span>
|
||||
<span>{t("common:navigate")}</span>
|
||||
</KbdGroup>
|
||||
{canSwitchScope && (
|
||||
<KbdGroup>
|
||||
<Kbd>Tab</Kbd>
|
||||
<span>Switch mode</span>
|
||||
<span>{t("common:switchMode")}</span>
|
||||
</KbdGroup>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<KbdGroup>
|
||||
<Kbd>↵</Kbd>
|
||||
<span>{getEnterLabel(mode)}</span>
|
||||
<span>{getEnterLabel(t, mode)}</span>
|
||||
</KbdGroup>
|
||||
{!isScopeMode && (
|
||||
<KbdGroup>
|
||||
<Kbd>⌫</Kbd>
|
||||
<span>Back</span>
|
||||
<span>{t("common:back")}</span>
|
||||
</KbdGroup>
|
||||
)}
|
||||
<KbdGroup>
|
||||
{/* A key name, not copy — it labels the physical key. */}
|
||||
<Kbd>esc</Kbd>
|
||||
<span>Close</span>
|
||||
<span>{t("common:close")}</span>
|
||||
</KbdGroup>
|
||||
</div>
|
||||
);
|
||||
@@ -442,9 +461,10 @@ function CommandPanelInputHeader({
|
||||
goBack,
|
||||
workspaceSearchAvailable,
|
||||
}: CommandPanelViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const isTopLevelMode = isCommandPanelScopeMode(mode);
|
||||
const canSwitchScope = workspaceSearchAvailable && isTopLevelMode;
|
||||
const modeLabel = getModeLabel(mode, inputCommand);
|
||||
const modeLabel = getModeLabel(t, mode, inputCommand);
|
||||
const onInputKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (
|
||||
canSwitchScope &&
|
||||
@@ -473,7 +493,7 @@ function CommandPanelInputHeader({
|
||||
</button>
|
||||
)}
|
||||
<CommandInput
|
||||
placeholder={getInputPlaceholder(mode, inputCommand)}
|
||||
placeholder={getInputPlaceholder(t, mode, inputCommand)}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
onKeyDown={onInputKeyDown}
|
||||
@@ -484,6 +504,7 @@ function CommandPanelInputHeader({
|
||||
}
|
||||
|
||||
function CommandPanelResultList(props: CommandPanelViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
mode,
|
||||
inputCommand,
|
||||
@@ -541,9 +562,9 @@ function CommandPanelResultList(props: CommandPanelViewProps) {
|
||||
)}
|
||||
{mode === "input" &&
|
||||
(!search.trim() ? (
|
||||
<CommandEmpty>{inputCommand?.inputPlaceholder ?? "Enter a value..."}</CommandEmpty>
|
||||
<CommandEmpty>{inputCommand?.inputPlaceholder ?? t("common:enterAValue")}</CommandEmpty>
|
||||
) : (
|
||||
<CommandEmpty>Press Enter to confirm</CommandEmpty>
|
||||
<CommandEmpty>{t("common:pressEnterToConfirm")}</CommandEmpty>
|
||||
))}
|
||||
</CommandList>
|
||||
);
|
||||
|
||||
@@ -6,21 +6,27 @@ import type { ConfigurableShortcutId } from "@/lib/keyboard/shortcut-overrides";
|
||||
import { getShortcut } from "@/lib/keyboard/shortcut-overrides";
|
||||
import { formatShortcut } from "@/lib/keyboard/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type CommandPanelScopeMode = "commands" | "search-files" | "search-content";
|
||||
|
||||
type ScopeOption = {
|
||||
mode: CommandPanelScopeMode;
|
||||
label: string;
|
||||
/**
|
||||
* Catalog key, resolved at render. `mode` stays the untranslated
|
||||
* discriminant — it is compared with `===` in `isCommandPanelScopeMode` and
|
||||
* threaded through the command-panel state.
|
||||
*/
|
||||
labelKey: string;
|
||||
shortcutId: ConfigurableShortcutId;
|
||||
};
|
||||
|
||||
const SCOPE_OPTIONS: ScopeOption[] = [
|
||||
{ mode: "commands", label: "Commands", shortcutId: "SEARCH" },
|
||||
{ mode: "search-files", label: "Files", shortcutId: "FILE_SEARCH" },
|
||||
{ mode: "commands", labelKey: "common:scopeCommands", shortcutId: "SEARCH" },
|
||||
{ mode: "search-files", labelKey: "common:scopeFiles", shortcutId: "FILE_SEARCH" },
|
||||
{
|
||||
mode: "search-content",
|
||||
label: "Contents",
|
||||
labelKey: "common:scopeContents",
|
||||
shortcutId: "CONTENT_SEARCH",
|
||||
},
|
||||
];
|
||||
@@ -46,26 +52,28 @@ export function CommandPanelScopeSwitcher({
|
||||
mode: CommandPanelScopeMode;
|
||||
onScopeChange: (mode: CommandPanelScopeMode) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const keyboardShortcuts = useAppStore((state) => state.userSettings.keyboardShortcuts);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Command palette mode"
|
||||
aria-label={t("common:commandPaletteMode")}
|
||||
className="mr-1 flex h-10 shrink-0 items-stretch gap-0.5"
|
||||
>
|
||||
{SCOPE_OPTIONS.map((scope) => {
|
||||
const active = mode === scope.mode;
|
||||
const shortcut = formatShortcut(getShortcut(scope.shortcutId, keyboardShortcuts));
|
||||
const label = t(scope.labelKey);
|
||||
return (
|
||||
<button
|
||||
key={scope.mode}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-label={scope.label}
|
||||
aria-label={label}
|
||||
aria-selected={active}
|
||||
tabIndex={-1}
|
||||
title={`${scope.label} (${shortcut})`}
|
||||
title={t("common:scopeTitleWithShortcut", { label, shortcut })}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => onScopeChange(scope.mode)}
|
||||
className={cn(
|
||||
@@ -75,7 +83,7 @@ export function CommandPanelScopeSwitcher({
|
||||
: "after:scale-x-75 after:opacity-0",
|
||||
)}
|
||||
>
|
||||
<span>{scope.label}</span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -30,7 +30,12 @@ import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { initializeLocalRepository } from "@/lib/api/domains/workspace-api";
|
||||
import { createDirectory } from "@/lib/api/domains/fs-api";
|
||||
import type { Repository } from "@/lib/types/http";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
// The returned strings are never rendered: the only caller uses this as a
|
||||
// boolean gate (`!nameError`), so they stay English rather than becoming dead
|
||||
// catalog entries.
|
||||
export function validateLocalRepositoryName(name: string): string | null {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return "Enter a repository name.";
|
||||
@@ -57,20 +62,27 @@ export type CreateLocalRepositorySurfaceProps = {
|
||||
onCreated: (repository: Repository) => void;
|
||||
};
|
||||
|
||||
// `context` and `requiresSwitch` stay logic; only the notices they select are
|
||||
// copy. The profile name is user data, interpolated rather than translated.
|
||||
function executorNotice(
|
||||
t: TFunction,
|
||||
selection: DirectLocalExecutorSelection | null,
|
||||
context: NonNullable<CreateLocalRepositorySurfaceProps["context"]>,
|
||||
): string {
|
||||
if (context === "workspace") {
|
||||
return "Creates an empty Git repository and registers it in this workspace.";
|
||||
return t("common:createsAnEmptyGitRepository");
|
||||
}
|
||||
if (!selection) {
|
||||
return "A direct local executor profile is required to create and use an empty repository.";
|
||||
return t("common:aDirectLocalExecutorProfileIsRequired");
|
||||
}
|
||||
if (selection.requiresSwitch) {
|
||||
return `Empty repositories run directly on this machine. This task will switch to “${selection.executorProfileName}”.`;
|
||||
return t("common:emptyRepositoriesRunDirectlySwitch", {
|
||||
profile: selection.executorProfileName,
|
||||
});
|
||||
}
|
||||
return `This empty repository will run with “${selection.executorProfileName}” on this machine.`;
|
||||
return t("common:thisEmptyRepositoryWillRunWith", {
|
||||
profile: selection.executorProfileName,
|
||||
});
|
||||
}
|
||||
|
||||
type RepositoryLocationFieldsProps = {
|
||||
@@ -90,10 +102,11 @@ function RepositoryLocationFields({
|
||||
onParentPathChange,
|
||||
onLoadTypedDirectory,
|
||||
}: RepositoryLocationFieldsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<label className="block space-y-1.5 text-xs font-medium">
|
||||
<span>Repository name</span>
|
||||
<span>{t("common:repositoryName")}</span>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(event) => onNameChange(event.target.value)}
|
||||
@@ -104,7 +117,7 @@ function RepositoryLocationFields({
|
||||
</label>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="local-repository-parent" className="text-xs font-medium">
|
||||
Parent directory
|
||||
{t("common:parentDirectory")}
|
||||
</label>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Input
|
||||
@@ -126,8 +139,8 @@ function RepositoryLocationFields({
|
||||
className="size-11 sm:size-8"
|
||||
onClick={onLoadTypedDirectory}
|
||||
disabled={!parentPath}
|
||||
aria-label="Browse parent directory"
|
||||
title="Browse parent directory"
|
||||
aria-label={t("common:browseParentDirectory")}
|
||||
title={t("common:browseParentDirectory")}
|
||||
>
|
||||
<IconFolderOpen />
|
||||
</Button>
|
||||
@@ -135,9 +148,9 @@ function RepositoryLocationFields({
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2 border-t border-border/70 pt-2 sm:col-span-2">
|
||||
<IconFolder className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="shrink-0 text-xs text-muted-foreground">Destination</span>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">{t("common:destination")}</span>
|
||||
<span className="truncate font-mono text-xs" title={targetPath || parentPath}>
|
||||
{targetPath || parentPath || "Loading folder…"}
|
||||
{targetPath || parentPath || t("common:loadingFolder")}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -156,6 +169,7 @@ function RepositoryFormDetails({
|
||||
submitError,
|
||||
...locationFields
|
||||
}: RepositoryFormDetailsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="shrink-0 space-y-3 px-4 py-4">
|
||||
<div className="grid gap-3 sm:grid-cols-[minmax(0,0.8fr)_minmax(0,1.6fr)]">
|
||||
@@ -169,7 +183,7 @@ function RepositoryFormDetails({
|
||||
}
|
||||
>
|
||||
<IconInfoCircle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<p>{executorNotice(executorSelection, context)}</p>
|
||||
<p>{executorNotice(t, executorSelection, context)}</p>
|
||||
</div>
|
||||
{submitError ? (
|
||||
<div
|
||||
@@ -191,6 +205,7 @@ function CreateRepositoryFooter({
|
||||
canSubmit: boolean;
|
||||
submitting: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex shrink-0 justify-end border-t border-border px-4 pt-3 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
<Button
|
||||
@@ -199,7 +214,7 @@ function CreateRepositoryFooter({
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
<IconFolderPlus className="h-4 w-4" />
|
||||
{submitting ? "Creating…" : "Create repository"}
|
||||
{submitting ? t("common:creating") : t("common:createRepository")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -213,6 +228,7 @@ function CreateRepositoryForm({
|
||||
onCreated,
|
||||
onDismiss,
|
||||
}: CreateLocalRepositorySurfaceProps & { onDismiss: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState("");
|
||||
const [parentPath, setParentPath] = useState("");
|
||||
const [editingParentPath, setEditingParentPath] = useState(false);
|
||||
@@ -250,7 +266,7 @@ function CreateRepositoryForm({
|
||||
setName("");
|
||||
onDismiss();
|
||||
} catch (err) {
|
||||
setSubmitError(err instanceof Error ? err.message : "Failed to create repository");
|
||||
setSubmitError(err instanceof Error ? err.message : t("common:failedToCreateRepository"));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -313,6 +329,7 @@ function CreateRepositoryForm({
|
||||
}
|
||||
|
||||
export function CreateLocalRepositorySurface(props: CreateLocalRepositorySurfaceProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useResponsiveBreakpoint();
|
||||
const handleOpenChange = (open: boolean) => props.onOpenChange(open);
|
||||
const form = <CreateRepositoryForm {...props} onDismiss={() => handleOpenChange(false)} />;
|
||||
@@ -325,10 +342,8 @@ export function CreateLocalRepositorySurface(props: CreateLocalRepositorySurface
|
||||
className="h-[88dvh] max-h-[88dvh] min-w-0 overflow-hidden pb-0"
|
||||
>
|
||||
<DrawerHeader className="shrink-0 border-b border-border text-left">
|
||||
<DrawerTitle>Create new repository</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
Choose a folder or enter a new path on this machine.
|
||||
</DrawerDescription>
|
||||
<DrawerTitle>{t("common:createNewRepository")}</DrawerTitle>
|
||||
<DrawerDescription>{t("common:chooseAFolderOrEnterA")}</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{form}
|
||||
</DrawerContent>
|
||||
@@ -343,10 +358,8 @@ export function CreateLocalRepositorySurface(props: CreateLocalRepositorySurface
|
||||
className="flex h-[min(640px,85dvh)] max-w-xl min-w-0 flex-col overflow-hidden p-0"
|
||||
>
|
||||
<DialogHeader className="shrink-0 border-b border-border px-4 py-3 pr-12">
|
||||
<DialogTitle>Create new repository</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose a folder or enter a new path on this machine.
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("common:createNewRepository")}</DialogTitle>
|
||||
<DialogDescription>{t("common:chooseAFolderOrEnterA")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{form}
|
||||
</DialogContent>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@kandev/ui/alert-dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const MAX_VISIBLE_FILES = 20;
|
||||
|
||||
@@ -30,9 +31,15 @@ export function DiscardLocalChangesDialog({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: DiscardLocalChangesDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const visible = dirtyFiles.slice(0, MAX_VISIBLE_FILES);
|
||||
const overflow = dirtyFiles.length - visible.length;
|
||||
const target = repoPath ? ` in your local clone at ${repoPath}` : " in your local clone";
|
||||
// One complete sentence per branch rather than a stem plus a " in your local
|
||||
// clone at X" tail: the tail's position in the sentence is the translator's
|
||||
// call, and a concatenated fragment freezes it here.
|
||||
const description = repoPath
|
||||
? t("common:discardLocalChangesAtPathDescription", { repoPath })
|
||||
: t("common:discardLocalChangesDescription");
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -44,11 +51,8 @@ export function DiscardLocalChangesDialog({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Discard local changes?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Starting this task will permanently discard the uncommitted changes
|
||||
{target}. Back up anything you want to keep before continuing.
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogTitle>{t("common:discardLocalChanges")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<ul
|
||||
className="max-h-48 overflow-auto rounded-md border border-border bg-muted/40 p-2 text-xs font-mono text-muted-foreground space-y-0.5"
|
||||
@@ -64,7 +68,7 @@ export function DiscardLocalChangesDialog({
|
||||
className="pt-1 text-[11px] italic text-muted-foreground/80"
|
||||
data-testid="discard-local-changes-overflow"
|
||||
>
|
||||
+{overflow} more
|
||||
{t("common:andMoreFiles", { count: overflow })}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
@@ -74,14 +78,14 @@ export function DiscardLocalChangesDialog({
|
||||
data-testid="discard-local-changes-cancel"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
{t("common:cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
data-testid="discard-local-changes-confirm"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Discard and continue
|
||||
{t("common:discardAndContinue")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@kandev/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { GridSpinner } from "@/components/grid-spinner";
|
||||
import { useTooltipMountGate } from "@/hooks/use-tooltip-mount-gate";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type EnhancePromptButtonProps = {
|
||||
onClick: () => void;
|
||||
@@ -17,11 +18,12 @@ export function EnhancePromptButton({
|
||||
isLoading,
|
||||
isConfigured = true,
|
||||
}: EnhancePromptButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { tooltipOpenState, handleTooltipOpenChange } = useTooltipMountGate();
|
||||
const isDisabled = !isConfigured || isLoading;
|
||||
const tooltipText = isConfigured
|
||||
? "Enhance prompt with AI"
|
||||
: "Configure a utility agent in settings to enable AI enhancement";
|
||||
? t("common:enhancePromptWithAi")
|
||||
: t("common:configureAUtilityAgentToEnhance");
|
||||
|
||||
return (
|
||||
<Tooltip open={tooltipOpenState} onOpenChange={handleTooltipOpenChange}>
|
||||
@@ -40,7 +42,7 @@ export function EnhancePromptButton({
|
||||
className="h-7 w-7 cursor-pointer hover:bg-muted/40 text-slate-400"
|
||||
onClick={isConfigured ? onClick : undefined}
|
||||
disabled={isDisabled}
|
||||
aria-label="Enhance prompt with AI"
|
||||
aria-label={t("common:enhancePromptWithAi")}
|
||||
aria-busy={isLoading}
|
||||
data-testid="enhance-prompt-button"
|
||||
>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { listDirectory, type DirectoryListing } from "@/lib/api/domains/fs-api";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type FolderPickerProps = {
|
||||
/** Currently chosen absolute path; empty when nothing is picked. */
|
||||
@@ -30,10 +31,11 @@ type FolderPickerProps = {
|
||||
* trigger lives in the chip row and the popover handles browse + commit.
|
||||
*/
|
||||
export function FolderPicker({ value, onChange, placeholder }: FolderPickerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { listing, loading, error, load } = useDirectoryListing(open, value);
|
||||
const leaf = leafName(value);
|
||||
const triggerLabel = leaf || placeholder || "scratch workspace";
|
||||
const triggerLabel = leaf || placeholder || t("common:scratchWorkspace");
|
||||
const hasValue = !!value;
|
||||
|
||||
const triggerClass = cn(
|
||||
@@ -106,27 +108,31 @@ function leafName(path: string): string {
|
||||
}
|
||||
|
||||
export function useDirectoryListing(open: boolean, value: string) {
|
||||
const { t } = useTranslation();
|
||||
const [listing, setListing] = useState<DirectoryListing | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const requestGeneration = useRef(0);
|
||||
|
||||
const load = useCallback(async (path: string) => {
|
||||
const generation = ++requestGeneration.current;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setListing(null);
|
||||
try {
|
||||
const nextListing = await listDirectory(path);
|
||||
if (generation !== requestGeneration.current) return;
|
||||
setListing(nextListing);
|
||||
} catch (err) {
|
||||
if (generation !== requestGeneration.current) return;
|
||||
setError(err instanceof Error ? err.message : "Failed to load directory");
|
||||
} finally {
|
||||
if (generation === requestGeneration.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
const load = useCallback(
|
||||
async (path: string) => {
|
||||
const generation = ++requestGeneration.current;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setListing(null);
|
||||
try {
|
||||
const nextListing = await listDirectory(path);
|
||||
if (generation !== requestGeneration.current) return;
|
||||
setListing(nextListing);
|
||||
} catch (err) {
|
||||
if (generation !== requestGeneration.current) return;
|
||||
setError(err instanceof Error ? err.message : t("common:failedToLoadDirectory"));
|
||||
} finally {
|
||||
if (generation === requestGeneration.current) setLoading(false);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset cached listing when the popover closes so the next open reloads
|
||||
@@ -197,11 +203,12 @@ function Breadcrumb({
|
||||
onNavigate: (p: string) => void;
|
||||
touchRows?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const segs = pathSegments(path);
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 overflow-x-auto overflow-y-hidden border-b border-border bg-muted/30 px-2 py-1.5">
|
||||
{segs.length === 0 && (
|
||||
<span className="text-[11px] text-muted-foreground italic">Loading…</span>
|
||||
<span className="text-[11px] text-muted-foreground italic">{t("common:loading")}</span>
|
||||
)}
|
||||
{segs.map((seg, i) => {
|
||||
const last = i === segs.length - 1;
|
||||
@@ -280,6 +287,7 @@ function DirectoryBrowserToolbar({
|
||||
onCreateDirectory: (name: string) => Promise<void>;
|
||||
touchRows: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
@@ -294,7 +302,7 @@ function DirectoryBrowserToolbar({
|
||||
await onCreateDirectory(trimmedName);
|
||||
setEditing(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to create folder");
|
||||
setError(err instanceof Error ? err.message : t("common:failedToCreateFolder"));
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
@@ -303,60 +311,21 @@ function DirectoryBrowserToolbar({
|
||||
return (
|
||||
<div className="shrink-0 border-b border-border bg-muted/10 px-3 py-2">
|
||||
{editing ? (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<IconFolderPlus className="size-4 shrink-0 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label="New folder name"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void createFolder();
|
||||
} else if (event.key === "Escape") {
|
||||
setEditing(false);
|
||||
}
|
||||
}}
|
||||
placeholder="Folder name"
|
||||
autoFocus
|
||||
className={cn("min-w-0 flex-1", touchRows && "h-10")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size={touchRows ? "icon-lg" : "icon"}
|
||||
className={touchRows ? "size-11" : undefined}
|
||||
onClick={() => void createFolder()}
|
||||
disabled={!name.trim() || creating}
|
||||
aria-label="Create folder"
|
||||
title="Create folder"
|
||||
>
|
||||
<IconCheck />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size={touchRows ? "icon-lg" : "icon"}
|
||||
className={touchRows ? "size-11" : undefined}
|
||||
onClick={() => setEditing(false)}
|
||||
aria-label="Cancel new folder"
|
||||
title="Cancel"
|
||||
>
|
||||
<IconX />
|
||||
</Button>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="pl-6 text-xs text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<NewFolderForm
|
||||
name={name}
|
||||
error={error}
|
||||
creating={creating}
|
||||
touchRows={touchRows}
|
||||
onNameChange={(next) => {
|
||||
setName(next);
|
||||
setError(null);
|
||||
}}
|
||||
onSubmit={() => void createFolder()}
|
||||
onCancel={() => setEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">Folders</span>
|
||||
<span className="text-xs font-medium text-muted-foreground">{t("common:folders")}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -366,7 +335,7 @@ function DirectoryBrowserToolbar({
|
||||
disabled={disabled}
|
||||
>
|
||||
<IconFolderPlus />
|
||||
New folder
|
||||
{t("common:newFolder")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -374,6 +343,78 @@ function DirectoryBrowserToolbar({
|
||||
);
|
||||
}
|
||||
|
||||
/** The inline "new folder" row, extracted to keep the toolbar under the
|
||||
* max-lines-per-function cap. */
|
||||
function NewFolderForm({
|
||||
name,
|
||||
error,
|
||||
creating,
|
||||
touchRows,
|
||||
onNameChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: {
|
||||
name: string;
|
||||
error: string | null;
|
||||
creating: boolean;
|
||||
touchRows: boolean;
|
||||
onNameChange: (next: string) => void;
|
||||
onSubmit: () => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<IconFolderPlus className="size-4 shrink-0 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label={t("common:newFolderName")}
|
||||
value={name}
|
||||
onChange={(event) => onNameChange(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
} else if (event.key === "Escape") {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
placeholder={t("common:folderName")}
|
||||
autoFocus
|
||||
className={cn("min-w-0 flex-1", touchRows && "h-10")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size={touchRows ? "icon-lg" : "icon"}
|
||||
className={touchRows ? "size-11" : undefined}
|
||||
onClick={onSubmit}
|
||||
disabled={!name.trim() || creating}
|
||||
aria-label={t("common:createFolder")}
|
||||
title={t("common:createFolder")}
|
||||
>
|
||||
<IconCheck />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size={touchRows ? "icon-lg" : "icon"}
|
||||
className={touchRows ? "size-11" : undefined}
|
||||
onClick={onCancel}
|
||||
aria-label={t("common:cancelNewFolder")}
|
||||
title={t("common:cancel")}
|
||||
>
|
||||
<IconX />
|
||||
</Button>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="pl-6 text-xs text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Entries({
|
||||
listing,
|
||||
loading,
|
||||
@@ -389,10 +430,11 @@ function Entries({
|
||||
touchRows?: boolean;
|
||||
fillAvailableHeight?: boolean;
|
||||
}) {
|
||||
if (loading) return <EmptyRow text="Loading…" />;
|
||||
const { t } = useTranslation();
|
||||
if (loading) return <EmptyRow text={t("common:loading")} />;
|
||||
if (error) return <EmptyRow text={error} variant="error" testId="folder-picker-error" />;
|
||||
if (!listing || listing.entries.length === 0) {
|
||||
return <EmptyRow text="No folders here — pick this one or go up" />;
|
||||
return <EmptyRow text={t("common:noFoldersHerePickThisOne")} />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
@@ -445,6 +487,7 @@ function Footer({
|
||||
onUseScratch: () => void;
|
||||
onChoose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 border-t border-border bg-muted/20 px-2 py-1.5">
|
||||
<button
|
||||
@@ -454,7 +497,7 @@ function Footer({
|
||||
className="inline-flex items-center gap-1.5 rounded px-2 py-1 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground cursor-pointer"
|
||||
>
|
||||
<IconBox className="h-3 w-3" />
|
||||
Use scratch instead
|
||||
{t("common:useScratchInstead")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -468,7 +511,7 @@ function Footer({
|
||||
)}
|
||||
>
|
||||
<IconFolder className="h-3 w-3" />
|
||||
Use this folder
|
||||
{t("common:useThisFolder")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
"use client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type GridSpinnerProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function GridSpinner({ className }: GridSpinnerProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className={`spinner-grid ${className ?? ""}`} role="status" aria-label="Loading">
|
||||
<span
|
||||
className={`spinner-grid ${className ?? ""}`}
|
||||
role="status"
|
||||
aria-label={t("common:loadingIndicatorLabel")}
|
||||
>
|
||||
<span className="spinner-grid-cube" />
|
||||
<span className="spinner-grid-cube" />
|
||||
<span className="spinner-grid-cube" />
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
resolveImproveKandevWorkflow,
|
||||
type ImproveKandevKind,
|
||||
} from "./improve-kandev-dialog-model";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
export type BootstrapState =
|
||||
| { kind: "idle" }
|
||||
@@ -43,22 +45,22 @@ type ImproveKind = ImproveKandevKind;
|
||||
// Surfaced in the dialog's submit-button tooltip while the bootstrap probe is
|
||||
// in flight, so the disabled state has an explanation the user can act on
|
||||
// instead of the usual missing-field reasons.
|
||||
function bootstrapBlockedReason(bootstrap: BootstrapState): string | null {
|
||||
function bootstrapBlockedReason(t: TFunction, bootstrap: BootstrapState): string | null {
|
||||
if (bootstrap.kind === "loading" || bootstrap.kind === "idle") {
|
||||
return "Preparing kandev repository…";
|
||||
return t("common:preparingKandevRepository");
|
||||
}
|
||||
if (bootstrap.kind === "error") {
|
||||
return "Bootstrap failed — close and reopen to retry.";
|
||||
return t("common:bootstrapFailedCloseAndReopen");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PLACEHOLDERS: Record<ImproveKind, string> = {
|
||||
bug: "E.g. When I open the kanban board, the column header text overlaps the task count badge on screens narrower than 1200px...",
|
||||
feature:
|
||||
"E.g. Add a keyboard shortcut to mark a task as Done from the task detail view, and show the shortcut in the task action menu...",
|
||||
issue:
|
||||
"Describe the problem or request you want published. The agent will ask for any details required by the matching GitHub issue template...",
|
||||
// Keys, not resolved copy: a `t()` at module scope would freeze at the boot
|
||||
// locale and the pseudo-locale could not see it.
|
||||
const PLACEHOLDER_KEYS: Record<ImproveKind, string> = {
|
||||
bug: "common:improveKandevBugPlaceholder",
|
||||
feature: "common:improveKandevFeaturePlaceholder",
|
||||
issue: "common:improveKandevIssuePlaceholder",
|
||||
};
|
||||
|
||||
type CreateModeViewProps = {
|
||||
@@ -74,6 +76,7 @@ type CreateModeViewProps = {
|
||||
};
|
||||
|
||||
export function CreateModeView(props: CreateModeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { bootstrap, captureLogs, setCaptureLogs } = props;
|
||||
const [kind, setKind] = useState<ImproveKind>("bug");
|
||||
const ready = bootstrap.kind === "ready" ? bootstrap : null;
|
||||
@@ -104,9 +107,9 @@ export function CreateModeView(props: CreateModeViewProps) {
|
||||
onSuccess={props.onTaskCreated}
|
||||
lockedFields={{ repository: true, branch: true, workflow: true }}
|
||||
submitBlockedReason={
|
||||
props.externalBlockedReason ?? forkBlockedReason ?? bootstrapBlockedReason(bootstrap)
|
||||
props.externalBlockedReason ?? forkBlockedReason ?? bootstrapBlockedReason(t, bootstrap)
|
||||
}
|
||||
descriptionPlaceholder={PLACEHOLDERS[kind]}
|
||||
descriptionPlaceholder={t(PLACEHOLDER_KEYS[kind])}
|
||||
aboveDescriptionSlot={<KindTabs kind={kind} onChange={handleKindChange} />}
|
||||
extraFormSlot={
|
||||
<BootstrapStatusSlot
|
||||
@@ -138,12 +141,13 @@ function CreateModeBottomSlot({ kind, steps }: { kind: ImproveKind; steps: Workf
|
||||
}
|
||||
|
||||
function WorkflowStepsPreview({ steps }: { steps: WorkflowStep[] }) {
|
||||
const { t } = useTranslation();
|
||||
if (steps.length === 0) return null;
|
||||
const ordered = [...steps].sort((a, b) => a.position - b.position);
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-muted/30 px-3 py-2">
|
||||
<p className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/80">
|
||||
Workflow
|
||||
{t("common:workflow")}
|
||||
</p>
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
|
||||
@@ -175,6 +179,7 @@ function WorkflowStepsPreview({ steps }: { steps: WorkflowStep[] }) {
|
||||
}
|
||||
|
||||
function UsefulInfoCollapsible() {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
@@ -184,7 +189,7 @@ function UsefulInfoCollapsible() {
|
||||
className="group flex w-full cursor-pointer items-center justify-between rounded-md border border-border bg-muted/30 px-3 py-2 text-left text-xs hover:bg-muted/50"
|
||||
>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Useful commands & agent skills
|
||||
{t("common:usefulCommandsAgentSkills")}
|
||||
</span>
|
||||
<IconChevronDown className="h-3.5 w-3.5 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</button>
|
||||
@@ -192,32 +197,33 @@ function UsefulInfoCollapsible() {
|
||||
<CollapsibleContent>
|
||||
<div className="mt-2 space-y-3 rounded-md border border-border bg-muted/20 px-3 py-2 text-xs leading-relaxed text-muted-foreground">
|
||||
<p>
|
||||
Shell command you can run in the secondary instance, plus slash-command <em>skills</em>{" "}
|
||||
you can ask the agent to run during the workflow.
|
||||
<Trans i18nKey="common:shellCommandPlusSlashCommandSkills">
|
||||
Shell command you can run in the secondary instance, plus slash-command{" "}
|
||||
<em>skills</em> you can ask the agent to run during the workflow.
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">
|
||||
Shell
|
||||
{t("common:shell")}
|
||||
</p>
|
||||
<UsefulInfoItem cmd="make install && make dev">
|
||||
Boots a secondary kandev dev instance with a clean DB so you can verify the change
|
||||
without touching your main one.
|
||||
{t("common:bootsASecondaryKandevDevInstance")}
|
||||
</UsefulInfoItem>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70">
|
||||
Agent skills (ask the agent to run)
|
||||
{t("common:agentSkillsAskTheAgentTo")}
|
||||
</p>
|
||||
<UsefulInfoItem cmd="/commit">
|
||||
Stages and commits changes using Conventional Commits.
|
||||
{t("common:stagesAndCommitsChangesUsingConventional")}
|
||||
</UsefulInfoItem>
|
||||
<UsefulInfoItem cmd="/push">Commits and pushes to the current branch.</UsefulInfoItem>
|
||||
<UsefulInfoItem cmd="/push">{t("common:commitsAndPushesToTheCurrent")}</UsefulInfoItem>
|
||||
<UsefulInfoItem cmd="/verify">
|
||||
Runs format, typecheck, test, and lint across the monorepo.
|
||||
{t("common:runsFormatTypecheckTestAndLint")}
|
||||
</UsefulInfoItem>
|
||||
<UsefulInfoItem cmd="/pr">Commits, pushes, and opens a pull request.</UsefulInfoItem>
|
||||
<UsefulInfoItem cmd="/pr">{t("common:commitsPushesAndOpensAPull")}</UsefulInfoItem>
|
||||
<UsefulInfoItem cmd="/pr-fixup">
|
||||
Waits for CI and automated reviews, then fixes failures and addresses comments.
|
||||
{t("common:waitsForCiAndAutomatedReviews")}
|
||||
</UsefulInfoItem>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,17 +248,18 @@ function KindTabs({
|
||||
kind: ImproveKind;
|
||||
onChange: (next: ImproveKind) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tabs value={kind} onValueChange={(v) => onChange(v as ImproveKind)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="bug" className="cursor-pointer">
|
||||
Bug fix
|
||||
{t("common:bugFix")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="feature" className="cursor-pointer">
|
||||
Feature request
|
||||
{t("common:featureRequest")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="issue" className="cursor-pointer">
|
||||
Open issue
|
||||
{t("common:openIssue")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -270,6 +277,7 @@ function BootstrapStatusSlot({
|
||||
captureLogs: boolean;
|
||||
setCaptureLogs: (v: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<BootstrapBanner bootstrap={bootstrap} kind={kind} />
|
||||
@@ -278,32 +286,27 @@ function BootstrapStatusSlot({
|
||||
className="rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-muted-foreground"
|
||||
data-testid="improve-kandev-issue-only-notice"
|
||||
>
|
||||
This workflow will only create a GitHub issue from the repository template. The agent will
|
||||
ask for missing details and publication approval; it will not implement the change or open
|
||||
a pull request.
|
||||
{t("common:thisWorkflowWillOnlyCreateA")}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<Checkbox checked={captureLogs} onCheckedChange={(v) => setCaptureLogs(v === true)} />
|
||||
Include recent backend & browser logs as context for the agent
|
||||
{t("common:includeRecentBackendAndBrowserLogs")}
|
||||
</label>
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="How log context works"
|
||||
aria-label={t("common:howLogContextWorks")}
|
||||
className="cursor-help text-muted-foreground/70 hover:text-muted-foreground"
|
||||
>
|
||||
<IconInfoCircle className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs text-xs leading-relaxed">
|
||||
Kandev keeps a small in-memory ring buffer of the most recent backend logs and browser
|
||||
console events. When enabled, those logs are written to a temporary folder on your
|
||||
machine, and the file paths are appended to the task description so the agent can read
|
||||
them while investigating.
|
||||
{t("common:kandevKeepsASmallInMemory")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -313,11 +316,12 @@ function BootstrapStatusSlot({
|
||||
}
|
||||
|
||||
function BootstrapBanner({ bootstrap, kind }: { bootstrap: BootstrapState; kind: ImproveKind }) {
|
||||
const { t } = useTranslation();
|
||||
if (bootstrap.kind === "loading" || bootstrap.kind === "idle") {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
<IconLoader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
Preparing kandev repository in background. Fill in the details, submit when ready.
|
||||
{t("common:preparingKandevRepositoryInBackground")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -339,12 +343,13 @@ function ContributorBanner({
|
||||
data: ImproveKandevBootstrapResponse;
|
||||
kind: ImproveKind;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { github_login: login, has_write_access: hasWrite } = data;
|
||||
if (data.fork_status === "blocked_emu" && kind !== "issue") {
|
||||
return (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
<IconAlertTriangle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
||||
<span>{data.fork_message ?? "This GitHub account cannot fork kdlbs/kandev."}</span>
|
||||
<span>{data.fork_message ?? t("common:thisGithubAccountCannotForkKdlbs")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -356,14 +361,18 @@ function ContributorBanner({
|
||||
) : (
|
||||
<IconGitFork className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
||||
)}
|
||||
{/* One complete sentence per branch. Splitting this into a "Contributing
|
||||
as" stem plus a tail would freeze the word order in JSX and leave the
|
||||
tail untranslatable. */}
|
||||
<span>
|
||||
Contributing as{" "}
|
||||
{login ? (
|
||||
<code className="font-mono text-foreground">@{login}</code>
|
||||
<Trans i18nKey="common:contributingAsLogin" values={{ login, access: accessMessage }}>
|
||||
Contributing as <code className="font-mono text-foreground">@{login}</code>.{" "}
|
||||
{accessMessage}
|
||||
</Trans>
|
||||
) : (
|
||||
<span>your GitHub account</span>
|
||||
t("common:contributingAsYourGithubAccount", { access: accessMessage })
|
||||
)}
|
||||
. {accessMessage}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render } from "@testing-library/react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
/**
|
||||
* These sentences interleave markup with a value, so the codemod half-converted
|
||||
* them: it keyed the `<code>` contents (`gh`, `kdlbs/kandev`) and left the prose
|
||||
* around them as a JSX literal. That renders correctly in English and passes
|
||||
* lint, `i18n:check`, `i18n:ratchet` and `i18n:removed` — the word order is just
|
||||
* frozen in JSX where no translator can reach it.
|
||||
*
|
||||
* Rebuilt as one `<Trans>` each. Two things are easy to get wrong and invisible
|
||||
* afterwards, so they are pinned here:
|
||||
*
|
||||
* 1. The `<n>` index must land on the element's position among ALL children,
|
||||
* counting text and `{" "}` expression containers. Prettier reflowing a
|
||||
* line inserts a `{" "}` and silently shifts every index after it.
|
||||
* 2. `gh` and `kdlbs/kandev` travel through `values`, not as tag content, so
|
||||
* a translator cannot reword a binary name or a repo slug. They cannot
|
||||
* instead be left as bare children under an empty `<1></1>` tag — that
|
||||
* form drops plain text children (it only preserves element children),
|
||||
* which silently renders "needs the CLI".
|
||||
*/
|
||||
describe("improve-kandev dialog <Trans> copy", () => {
|
||||
it("renders the gh-auth notice byte-identically to the old literal", () => {
|
||||
const { container } = render(
|
||||
<Trans
|
||||
i18nKey="common:theFinalStepOpensAPullRequest"
|
||||
values={{ binary: "gh", message: "Run gh auth login." }}
|
||||
>
|
||||
The final step of this workflow opens a pull request, which needs the <code>gh</code> CLI to
|
||||
be authenticated. {"Run gh auth login."}
|
||||
</Trans>,
|
||||
);
|
||||
|
||||
expect(container.textContent).toBe(
|
||||
"The final step of this workflow opens a pull request, which needs the gh CLI to be " +
|
||||
"authenticated. Run gh auth login.",
|
||||
);
|
||||
expect(container.querySelector("code")?.textContent).toBe("gh");
|
||||
});
|
||||
|
||||
it("renders the fork bullet with the repo slug intact", () => {
|
||||
const { container } = render(
|
||||
<Trans i18nKey="common:theAgentForksKandevToYourAccount" values={{ repo: "kdlbs/kandev" }}>
|
||||
The agent forks <code>kdlbs/kandev</code> to your GitHub account and opens a PR from your
|
||||
fork, credited to you
|
||||
</Trans>,
|
||||
);
|
||||
|
||||
expect(container.textContent).toBe(
|
||||
"The agent forks kdlbs/kandev to your GitHub account and opens a PR from your fork, " +
|
||||
"credited to you",
|
||||
);
|
||||
expect(container.querySelector("code")?.textContent).toBe("kdlbs/kandev");
|
||||
});
|
||||
|
||||
it("keeps the contributor line one sentence per branch", () => {
|
||||
const { container } = render(
|
||||
<Trans
|
||||
i18nKey="common:contributingAsLogin"
|
||||
values={{ login: "octocat", access: "You have write access." }}
|
||||
>
|
||||
Contributing as <code>@octocat</code>. {"You have write access."}
|
||||
</Trans>,
|
||||
);
|
||||
|
||||
expect(container.textContent).toBe("Contributing as @octocat. You have write access.");
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,14 @@ import {
|
||||
readImproveKandevSkipIntro,
|
||||
writeImproveKandevSkipIntro,
|
||||
} from "./improve-kandev-dialog-model";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
// The module-level `t` — resolved when called, not at import — for the two
|
||||
// error-only strings below. The hook-provided `t` changes identity on a locale
|
||||
// switch, and putting it in these effects' dependency arrays would re-POST
|
||||
// `bootstrapImproveKandev` (which forks a repository) and reset auth state
|
||||
// whenever the user changes language. Render-time copy still uses the reactive
|
||||
// `t` from `useTranslation()`.
|
||||
import { t as tStatic } from "@/lib/i18n";
|
||||
|
||||
type ImproveKandevDialogProps = {
|
||||
open: boolean;
|
||||
@@ -38,6 +46,7 @@ type AuthState =
|
||||
| { kind: "missing"; message: string; fixUrl: string; fixLabel: string };
|
||||
|
||||
export function ImproveKandevDialog(props: ImproveKandevDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { open, onOpenChange, workspaceId, onSuccess } = props;
|
||||
const [mode, setMode] = useState<Mode>(() => initialImproveKandevMode(readSkipIntro()));
|
||||
const [skipIntro, setSkipIntro] = useState(() => readSkipIntro());
|
||||
@@ -95,7 +104,9 @@ export function ImproveKandevDialog(props: ImproveKandevDialogProps) {
|
||||
setCaptureLogs={setCaptureLogs}
|
||||
transformDescription={transformDescription}
|
||||
onTaskCreated={handleSuccess}
|
||||
externalBlockedReason={auth.kind === "checking" ? "Checking GitHub authentication…" : null}
|
||||
externalBlockedReason={
|
||||
auth.kind === "checking" ? t("common:checkingGithubAuthentication") : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -106,7 +117,7 @@ export function ImproveKandevDialog(props: ImproveKandevDialogProps) {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<IconStethoscope className="h-5 w-5" />
|
||||
Improve Kandev
|
||||
{t("common:improveKandev")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<IntroBody
|
||||
@@ -145,7 +156,7 @@ function useGitHubAuthCheck(
|
||||
kind: "missing",
|
||||
message: ghIssue.message,
|
||||
fixUrl: ghIssue.fix_url.replace("{workspaceId}", workspaceId),
|
||||
fixLabel: ghIssue.fix_label || "Configure GitHub",
|
||||
fixLabel: ghIssue.fix_label || tStatic("common:configureGithub"),
|
||||
});
|
||||
} catch {
|
||||
if (!cancelled) setAuth({ kind: "ok" }); // Fail open — bootstrap will surface real errors.
|
||||
@@ -190,10 +201,10 @@ function useBootstrapKandev(
|
||||
});
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
const message = err instanceof Error ? err.message : "Bootstrap failed";
|
||||
const message = err instanceof Error ? err.message : tStatic("common:bootstrapFailed");
|
||||
setBootstrap({ kind: "error", message });
|
||||
toast({
|
||||
title: "Could not prepare Improve Kandev",
|
||||
title: tStatic("common:couldNotPrepareImproveKandev"),
|
||||
description: message,
|
||||
variant: "error",
|
||||
});
|
||||
@@ -239,21 +250,29 @@ function GhAuthMissing({
|
||||
auth: Extract<AuthState, { kind: "missing" }>;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="flex items-start gap-3 rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
|
||||
<IconAlertTriangle className="h-4 w-4 shrink-0 text-amber-500" />
|
||||
<div>
|
||||
<p className="font-medium text-foreground">GitHub CLI not authenticated</p>
|
||||
<p className="font-medium text-foreground">{t("common:githubCliNotAuthenticated")}</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
The final step of this workflow opens a pull request, which needs the <code>gh</code>{" "}
|
||||
CLI to be authenticated. {auth.message}
|
||||
{/* `gh` is the CLI's binary name, so it stays literal in the children
|
||||
and never becomes a catalog key. */}
|
||||
<Trans
|
||||
i18nKey="common:theFinalStepOpensAPullRequest"
|
||||
values={{ message: auth.message }}
|
||||
>
|
||||
The final step of this workflow opens a pull request, which needs the <code>gh</code>{" "}
|
||||
CLI to be authenticated. {auth.message}
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={onCancel} className="cursor-pointer">
|
||||
Cancel
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
<Button asChild className="cursor-pointer">
|
||||
<Link href={auth.fixUrl} onClick={onCancel}>
|
||||
@@ -278,30 +297,37 @@ function IntroExplanation({
|
||||
onCancel: () => void;
|
||||
onProceed: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-5 py-2">
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
Kandev is open source, and you can help make it better.
|
||||
{t("common:kandevIsOpenSourceAndYou")}
|
||||
</p>
|
||||
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
Describe a bug you hit or a feature you'd like, and we'll create a task on your
|
||||
own agent to implement it in the kandev codebase.
|
||||
{t("common:describeABugYouHitOr")}
|
||||
</p>
|
||||
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
When it's done, the agent opens a pull request to{" "}
|
||||
<code className="font-mono text-xs">kdlbs/kandev</code> for the maintainers to review,
|
||||
saving them time and shipping the improvement to everyone.
|
||||
<Trans i18nKey="common:whenItsDoneTheAgentOpensAPr" values={{ repo: "kdlbs/kandev" }}>
|
||||
When it's done, the agent opens a pull request to{" "}
|
||||
<code className="font-mono text-xs">kdlbs/kandev</code> for the maintainers to review,
|
||||
saving them time and shipping the improvement to everyone.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<IntroBullet>Create a task describing your bug or feature request</IntroBullet>
|
||||
<IntroBullet>Your agent implements it in the kandev repository, with tests</IntroBullet>
|
||||
<IntroBullet>You verify and test the change in a second kandev instance</IntroBullet>
|
||||
<IntroBullet>{t("common:createATaskDescribingYourBug")}</IntroBullet>
|
||||
<IntroBullet>{t("common:yourAgentImplementsItInThe")}</IntroBullet>
|
||||
<IntroBullet>{t("common:youVerifyAndTestTheChange")}</IntroBullet>
|
||||
<IntroBullet>
|
||||
The agent forks <code className="font-mono text-xs">kdlbs/kandev</code> to your GitHub
|
||||
account and opens a PR from your fork, credited to you
|
||||
<Trans
|
||||
i18nKey="common:theAgentForksKandevToYourAccount"
|
||||
values={{ repo: "kdlbs/kandev" }}
|
||||
>
|
||||
The agent forks <code className="font-mono text-xs">kdlbs/kandev</code> to your GitHub
|
||||
account and opens a PR from your fork, credited to you
|
||||
</Trans>
|
||||
</IntroBullet>
|
||||
</ul>
|
||||
<label
|
||||
@@ -312,11 +338,11 @@ function IntroExplanation({
|
||||
checked={skipIntro}
|
||||
onCheckedChange={(checked) => onSkipIntroChange(checked === true)}
|
||||
/>
|
||||
Do not show this again
|
||||
{t("common:doNotShowThisAgain")}
|
||||
</label>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={onCancel} className="cursor-pointer">
|
||||
Cancel
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onProceed}
|
||||
@@ -324,7 +350,7 @@ function IntroExplanation({
|
||||
className="cursor-pointer"
|
||||
data-testid="improve-kandev-proceed"
|
||||
>
|
||||
Contribute
|
||||
{t("common:contribute")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@ import { listAvailableAgents, listWorkflowTemplates } from "@/lib/api";
|
||||
import { listAgentsAction, updateAgentProfileAction } from "@/app/actions/agents";
|
||||
import { StepAgents, type AgentSetting } from "@/components/onboarding/step-agents";
|
||||
import type { AvailableAgent, ToolStatus, WorkflowTemplate, AgentProfile } from "@/lib/types/http";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
interface OnboardingDialogProps {
|
||||
open: boolean;
|
||||
@@ -42,36 +43,47 @@ interface OnboardingDialogProps {
|
||||
|
||||
const TOTAL_STEPS = 4;
|
||||
|
||||
// Catalog keys, not resolved copy: `t()` at module scope would freeze at the
|
||||
// boot locale. `id` stays untranslated so the React key is locale-independent.
|
||||
const RUNTIMES = [
|
||||
{
|
||||
name: "Local",
|
||||
description: "Run agents directly on your machine with full access to your local filesystem.",
|
||||
id: "local",
|
||||
nameKey: "common:runtimeLocal",
|
||||
descriptionKey: "common:runtimeLocalDescription",
|
||||
icon: IconFolder,
|
||||
},
|
||||
{
|
||||
name: "Git Worktree",
|
||||
description: "Isolated branch environment under a worktree root for parallel work.",
|
||||
id: "worktree",
|
||||
nameKey: "common:runtimeGitWorktree",
|
||||
descriptionKey: "common:runtimeGitWorktreeDescription",
|
||||
icon: IconFolders,
|
||||
},
|
||||
{
|
||||
name: "Docker",
|
||||
description: "Containerized execution for full isolation and reproducibility.",
|
||||
id: "docker",
|
||||
nameKey: "common:runtimeDocker",
|
||||
descriptionKey: "common:runtimeDockerDescription",
|
||||
icon: IconBrandDocker,
|
||||
},
|
||||
{
|
||||
name: "Sprites (Remote sprites.dev)",
|
||||
description: "Hardware-isolated execution environment for arbitrary code.",
|
||||
id: "sprites",
|
||||
nameKey: "common:runtimeSprites",
|
||||
descriptionKey: "common:runtimeSpritesDescription",
|
||||
icon: IconCloud,
|
||||
href: "https://sprites.dev",
|
||||
},
|
||||
];
|
||||
|
||||
const STEP_TITLES = ["AI Agents", "Executors", "Agentic Workflows", "Command Panel"];
|
||||
const STEP_DESCRIPTIONS = [
|
||||
"Manage discovered agents and install new ones.",
|
||||
"Agents can run in different executor environments — local, containerized, or remote.",
|
||||
"Workflows define the steps and automation for your tasks.",
|
||||
"Quick access to actions from anywhere with a keyboard shortcut.",
|
||||
const STEP_TITLE_KEYS = [
|
||||
"common:onboardingStepAgentsTitle",
|
||||
"common:onboardingStepExecutorsTitle",
|
||||
"common:onboardingStepWorkflowsTitle",
|
||||
"common:onboardingStepCommandPanelTitle",
|
||||
];
|
||||
const STEP_DESCRIPTION_KEYS = [
|
||||
"common:onboardingStepAgentsDescription",
|
||||
"common:onboardingStepExecutorsDescription",
|
||||
"common:onboardingStepWorkflowsDescription",
|
||||
"common:onboardingStepCommandPanelDescription",
|
||||
];
|
||||
|
||||
function buildAgentSettings(
|
||||
@@ -215,29 +227,30 @@ function useOnboardingResources(open: boolean) {
|
||||
}
|
||||
|
||||
function OnboardingFooter({ step, onSkip, onBack, onNext, onGetStarted }: OnboardingFooterProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<DialogFooter>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Button variant="ghost" size="sm" onClick={onSkip} className="cursor-pointer">
|
||||
<IconX className="mr-1.5 h-3.5 w-3.5" />
|
||||
Skip
|
||||
{t("common:skip")}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
{step > 0 && (
|
||||
<Button variant="outline" onClick={onBack} className="cursor-pointer">
|
||||
<IconArrowLeft className="mr-1.5 h-4 w-4" />
|
||||
Back
|
||||
{t("common:back")}
|
||||
</Button>
|
||||
)}
|
||||
{step < TOTAL_STEPS - 1 ? (
|
||||
<Button onClick={onNext} className="cursor-pointer">
|
||||
Next
|
||||
{t("common:next")}
|
||||
<IconArrowRight className="ml-1.5 h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={onGetStarted} className="cursor-pointer">
|
||||
<IconCheck className="mr-1.5 h-4 w-4" />
|
||||
Get Started
|
||||
{t("common:getStarted")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -247,6 +260,7 @@ function OnboardingFooter({ step, onSkip, onBack, onNext, onGetStarted }: Onboar
|
||||
}
|
||||
|
||||
export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [step, setStep] = useState(0);
|
||||
const {
|
||||
availableAgents,
|
||||
@@ -305,8 +319,10 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
<Dialog open={open} onOpenChange={() => {}}>
|
||||
<DialogContent className="sm:max-w-3xl" showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center text-2xl">{STEP_TITLES[step]}</DialogTitle>
|
||||
<DialogDescription className="text-center">{STEP_DESCRIPTIONS[step]}</DialogDescription>
|
||||
<DialogTitle className="text-center text-2xl">{t(STEP_TITLE_KEYS[step])}</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
{t(STEP_DESCRIPTION_KEYS[step])}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4 min-h-[220px]">
|
||||
{step === 0 && (
|
||||
@@ -336,6 +352,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
}
|
||||
|
||||
function StepEnvironments() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-2">
|
||||
@@ -348,26 +365,26 @@ function StepEnvironments() {
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium hover:underline cursor-pointer"
|
||||
>
|
||||
{runtime.name}
|
||||
{t(runtime.nameKey)}
|
||||
</a>
|
||||
) : (
|
||||
<p className="text-sm font-medium">{runtime.name}</p>
|
||||
<p className="text-sm font-medium">{t(runtime.nameKey)}</p>
|
||||
);
|
||||
return (
|
||||
<div key={runtime.name} className="flex items-start gap-3 rounded-lg border p-3">
|
||||
<div key={runtime.id} className="flex items-start gap-3 rounded-lg border p-3">
|
||||
<div className="h-8 w-8 rounded-md bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<Icon className="h-4.5 w-4.5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
{nameEl}
|
||||
<p className="text-xs text-muted-foreground">{runtime.description}</p>
|
||||
<p className="text-xs text-muted-foreground">{t(runtime.descriptionKey)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure executors in Settings to control where agents execute.
|
||||
{t("common:configureExecutorsInSettingsToControl")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -380,11 +397,12 @@ function StepWorkflows({
|
||||
templates: WorkflowTemplate[];
|
||||
loading: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-sm text-muted-foreground">
|
||||
<IconLoader2 className="h-6 w-6 animate-spin" />
|
||||
Loading workflow templates...
|
||||
{t("common:loadingWorkflowTemplates")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -398,7 +416,7 @@ function StepWorkflows({
|
||||
{defaultTemplate && <TemplateCard template={defaultTemplate} isDefault />}
|
||||
{otherTemplates.length > 0 && (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground mt-1">Available templates</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t("common:availableTemplates")}</p>
|
||||
{otherTemplates.map((template) => (
|
||||
<TemplateCard key={template.id} template={template} />
|
||||
))}
|
||||
@@ -406,22 +424,23 @@ function StepWorkflows({
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Workflows control the steps, automation, and agent behavior for your tasks. You can add more
|
||||
workflows from Settings.
|
||||
{t("common:workflowsControlTheStepsAutomationAnd")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// `id` keeps the React key locale-independent; `labelKey` resolves at render.
|
||||
const COMMAND_PANEL_PREVIEW_ITEMS = [
|
||||
{ icon: IconSearch, label: "Search Tasks", trailing: "→" },
|
||||
{ icon: IconHome, label: "Go to Home" },
|
||||
{ icon: IconGitCommit, label: "Commit Changes" },
|
||||
{ icon: IconArrowDown, label: "Pull" },
|
||||
{ icon: IconTerminal2, label: "Add Terminal Panel" },
|
||||
{ id: "search", icon: IconSearch, labelKey: "common:commandPreviewSearchTasks", trailing: "→" },
|
||||
{ id: "home", icon: IconHome, labelKey: "common:commandPreviewGoToHome" },
|
||||
{ id: "commit", icon: IconGitCommit, labelKey: "common:commandPreviewCommitChanges" },
|
||||
{ id: "pull", icon: IconArrowDown, labelKey: "common:commandPreviewPull" },
|
||||
{ id: "terminal", icon: IconTerminal2, labelKey: "common:commandPreviewAddTerminalPanel" },
|
||||
];
|
||||
|
||||
function StepCommandPanel() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Mock command panel preview */}
|
||||
@@ -429,7 +448,7 @@ function StepCommandPanel() {
|
||||
{/* Search input */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b">
|
||||
<IconSearch className="h-3.5 w-3.5 text-muted-foreground/50" />
|
||||
<span className="text-xs text-muted-foreground/50">Type a command...</span>
|
||||
<span className="text-xs text-muted-foreground/50">{t("common:typeACommand")}</span>
|
||||
</div>
|
||||
{/* Sample commands */}
|
||||
<div className="py-1">
|
||||
@@ -437,11 +456,11 @@ function StepCommandPanel() {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={item.label}
|
||||
key={item.id}
|
||||
className="flex items-center gap-3 px-3 py-1.5 text-sm first:bg-muted/50"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="flex-1 text-xs">{item.label}</span>
|
||||
<span className="flex-1 text-xs">{t(item.labelKey)}</span>
|
||||
{item.trailing && (
|
||||
<span className="text-xs text-muted-foreground">{item.trailing}</span>
|
||||
)}
|
||||
@@ -454,34 +473,39 @@ function StepCommandPanel() {
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Kbd>↑</Kbd>
|
||||
<Kbd>↓</Kbd>
|
||||
<span className="text-[0.6rem]">Navigate</span>
|
||||
<span className="text-[0.6rem]">{t("common:navigate")}</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Kbd>↵</Kbd>
|
||||
<span className="text-[0.6rem]">Select</span>
|
||||
<span className="text-[0.6rem]">{t("common:select")}</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{/* A key name, not copy — it labels the physical key. */}
|
||||
<Kbd>esc</Kbd>
|
||||
<span className="text-[0.6rem]">Close</span>
|
||||
<span className="text-[0.6rem]">{t("common:close")}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shortcut hint */}
|
||||
{/* One message with indexed tags rather than a "Press" stem and a tail:
|
||||
the flex children keep their gap-2 spacing, and a translator can still
|
||||
move the key chord to wherever the sentence needs it. */}
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<span>Press</span>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<Kbd>
|
||||
<IconCommand className="size-3" />
|
||||
</Kbd>
|
||||
<Kbd>K</Kbd>
|
||||
</span>
|
||||
<span>to open it anytime</span>
|
||||
<Trans i18nKey="common:pressCmdKToOpenItAnytime">
|
||||
<span>Press</span>
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
<Kbd>
|
||||
<IconCommand className="size-3" />
|
||||
</Kbd>
|
||||
<Kbd>K</Kbd>
|
||||
</span>
|
||||
<span>to open it anytime</span>
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Navigate between pages, search tasks, trigger git operations, and manage panels — all
|
||||
without leaving the keyboard. Context-aware commands appear based on the active page.
|
||||
{t("common:navigateBetweenPagesSearchTasksTrigger")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -494,6 +518,7 @@ function TemplateCard({
|
||||
template: WorkflowTemplate;
|
||||
isDefault?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const steps = (template.default_steps ?? []).slice().sort((a, b) => a.position - b.position);
|
||||
|
||||
return (
|
||||
@@ -505,7 +530,7 @@ function TemplateCard({
|
||||
{isDefault && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
||||
<IconCheck className="h-3.5 w-3.5" />
|
||||
Default
|
||||
{t("common:defaultBadge")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Button } from "@kandev/ui/button";
|
||||
|
||||
import type { UtilityGenerationResult } from "@/hooks/use-utility-agent-generator";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PromptResultRecoveryProps = {
|
||||
pendingResult: UtilityGenerationResult | null;
|
||||
@@ -15,6 +16,7 @@ export function PromptResultRecovery({
|
||||
onApply,
|
||||
onCopy,
|
||||
}: PromptResultRecoveryProps) {
|
||||
const { t } = useTranslation();
|
||||
if (!pendingResult) {
|
||||
return null;
|
||||
}
|
||||
@@ -25,7 +27,7 @@ export function PromptResultRecovery({
|
||||
data-testid="prompt-result-recovery"
|
||||
className="flex flex-col gap-2 rounded-md border border-border/60 bg-muted/30 p-3"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">An enhanced prompt is available.</p>
|
||||
<p className="text-sm text-muted-foreground">{t("common:anEnhancedPromptIsAvailable")}</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -33,10 +35,10 @@ export function PromptResultRecovery({
|
||||
className="cursor-pointer"
|
||||
onClick={() => void onCopy()}
|
||||
>
|
||||
Copy
|
||||
{t("common:copy")}
|
||||
</Button>
|
||||
<Button type="button" className="cursor-pointer" onClick={onApply}>
|
||||
Apply
|
||||
{t("common:apply")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
IconGitPullRequest,
|
||||
IconLoader2,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -51,6 +52,21 @@ export type PrimaryButtonConfig = {
|
||||
tooltip: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* One complete sentence per action rather than a single frame with the action
|
||||
* name interpolated. `primaryButtonConfig.label` is itself already translated,
|
||||
* and a translated value dropped into a translated frame cannot be declined to
|
||||
* agree with the surrounding grammar (genitive in German/Russian, and so on) —
|
||||
* nor can the frame be reordered around it. Keyed off `primaryAction`, which is
|
||||
* an untranslated discriminant.
|
||||
*/
|
||||
const PICK_REPOSITORY_KEYS: Record<"commit" | "push" | "pr" | "rebase", string> = {
|
||||
commit: "integrations:pickARepositoryForCommit",
|
||||
push: "integrations:pickARepositoryForPush",
|
||||
pr: "integrations:pickARepositoryForCreatePr",
|
||||
rebase: "integrations:pickARepositoryForRebase",
|
||||
};
|
||||
|
||||
const ITEM_CLASS = "cursor-pointer gap-3";
|
||||
const ICON_CLASS = "h-4 w-4 text-muted-foreground";
|
||||
|
||||
@@ -58,7 +74,8 @@ type ActionKey = "commit" | "push" | "pr" | "pull" | "rebase" | "merge" | "force
|
||||
|
||||
type ActionDef = {
|
||||
key: ActionKey;
|
||||
label: string;
|
||||
/** Catalog key, resolved at render: `t()` here would freeze at the boot locale. */
|
||||
labelKey: string;
|
||||
icon: ReactNode;
|
||||
/** Returns true when this action should be disabled for a given repo. */
|
||||
disabledFor: (status: PerRepoStatus | undefined) => boolean;
|
||||
@@ -68,49 +85,49 @@ type ActionDef = {
|
||||
const ACTION_DEFS: ActionDef[] = [
|
||||
{
|
||||
key: "commit",
|
||||
label: "Commit",
|
||||
labelKey: "integrations:commit",
|
||||
icon: <IconGitCommit className={ICON_CLASS} />,
|
||||
disabledFor: (s) => !((s?.hasStaged ?? false) || (s?.hasUnstaged ?? false)),
|
||||
invoke: (repo, cb) => cb.onCommit(repo),
|
||||
},
|
||||
{
|
||||
key: "push",
|
||||
label: "Push",
|
||||
labelKey: "integrations:push",
|
||||
icon: <IconCloudUpload className={ICON_CLASS} />,
|
||||
disabledFor: (s) => (s?.ahead ?? 0) === 0,
|
||||
invoke: (repo, cb) => cb.onPush(false, repo),
|
||||
},
|
||||
{
|
||||
key: "pr",
|
||||
label: "Create PR",
|
||||
labelKey: "integrations:createPr",
|
||||
icon: <IconGitPullRequest className={ICON_CLASS} />,
|
||||
disabledFor: () => false,
|
||||
invoke: (repo, cb) => cb.onPR(repo),
|
||||
},
|
||||
{
|
||||
key: "pull",
|
||||
label: "Pull",
|
||||
labelKey: "integrations:pull",
|
||||
icon: <IconCloudDownload className={ICON_CLASS} />,
|
||||
disabledFor: () => false,
|
||||
invoke: (repo, cb) => cb.onPull(repo),
|
||||
},
|
||||
{
|
||||
key: "rebase",
|
||||
label: "Rebase",
|
||||
labelKey: "integrations:rebase",
|
||||
icon: <IconGitCherryPick className={ICON_CLASS} />,
|
||||
disabledFor: () => false,
|
||||
invoke: (repo, cb) => cb.onRebase(repo),
|
||||
},
|
||||
{
|
||||
key: "merge",
|
||||
label: "Merge",
|
||||
labelKey: "integrations:merge",
|
||||
icon: <IconGitMerge className={ICON_CLASS} />,
|
||||
disabledFor: () => false,
|
||||
invoke: (repo, cb) => cb.onMerge(repo),
|
||||
},
|
||||
{
|
||||
key: "force-push",
|
||||
label: "Force Push",
|
||||
labelKey: "integrations:forcePush",
|
||||
icon: <IconAlertTriangle className={ICON_CLASS} />,
|
||||
disabledFor: (s) => (s?.ahead ?? 0) === 0,
|
||||
invoke: (repo, cb) => cb.onPush(true, repo),
|
||||
@@ -138,19 +155,20 @@ function PerRepoActionSub({
|
||||
repoDisplayName: (repositoryName: string) => string | undefined;
|
||||
callbacks: PerRepoCallbacks;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const statusByName = new Map(perRepoStatus.map((s) => [s.repository_name, s]));
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className={ITEM_CLASS} disabled={disabled}>
|
||||
{action.icon}
|
||||
<span className="flex-1">{action.label}</span>
|
||||
<span className="flex-1">{t(action.labelKey)}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-52">
|
||||
{repoNames.map((repo) => {
|
||||
const status = statusByName.get(repo);
|
||||
const ahead = status?.ahead ?? 0;
|
||||
const behind = status?.behind ?? 0;
|
||||
const label = repoDisplayName(repo) || repo || "Repository";
|
||||
const label = repoDisplayName(repo) || repo || t("integrations:repository");
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={repo || "__no_repo__"}
|
||||
@@ -194,10 +212,11 @@ function MultiRepoVcsDropdown({
|
||||
repoDisplayName: (repositoryName: string) => string | undefined;
|
||||
callbacks: PerRepoCallbacks;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel className="text-[10px] text-muted-foreground/70 uppercase tracking-wide">
|
||||
Pick action, then repo
|
||||
{t("integrations:pickActionThenRepo")}
|
||||
</DropdownMenuLabel>
|
||||
{ACTION_DEFS.map((action, idx) => (
|
||||
<div key={action.key}>
|
||||
@@ -221,10 +240,16 @@ function MultiRepoVcsDropdown({
|
||||
}
|
||||
|
||||
/** Inline footnote under Rebase/Merge showing the target branch. */
|
||||
// `type` stays a discriminant and never reaches the screen: rendering the union
|
||||
// member directly would make "onto"/"from" untranslatable and couple the copy to
|
||||
// the logic. Each branch resolves a whole message with the ref interpolated.
|
||||
function RebaseMergeFootnote({ target, type }: { target: string; type: "onto" | "from" }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="px-3 py-0.5 text-[10px] text-muted-foreground/60">
|
||||
{type} {target}
|
||||
{type === "onto"
|
||||
? t("integrations:ontoBranch", { branch: target })
|
||||
: t("integrations:fromBranch", { branch: target })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -256,6 +281,7 @@ export function MultiRepoVcsButton({
|
||||
repoDisplayName: (repositoryName: string) => string | undefined;
|
||||
callbacks: PerRepoCallbacks;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -294,7 +320,7 @@ export function MultiRepoVcsButton({
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Pick a repository for {primaryButtonConfig.label}</TooltipContent>
|
||||
<TooltipContent>{t(PICK_REPOSITORY_KEYS[primaryAction])}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
/**
|
||||
* The VCS tooltips used to build their plural by hand:
|
||||
*
|
||||
* `Create PR (${aheadCount} commit${aheadCount !== 1 ? "s" : ""} ahead)`
|
||||
*
|
||||
* That puts the plural rule at the call site where no other locale can reach
|
||||
* it, so the copy moved to `count` + `_one`/`_other`. These assertions pin the
|
||||
* English to exactly what the concatenation emitted — E2E specs and the
|
||||
* accessible-name queries depend on it, and a plural key is the easiest place
|
||||
* to silently reword a string while every gate stays green.
|
||||
*/
|
||||
describe("vcs split-button count-bearing copy", () => {
|
||||
const legacyCommit = (n: number) => `Commit ${n} changed file${n !== 1 ? "s" : ""}`;
|
||||
const legacyPr = (n: number) => `Create PR (${n} commit${n !== 1 ? "s" : ""} ahead)`;
|
||||
const legacyPush = (n: number) => `Push ${n} commit${n !== 1 ? "s" : ""} to remote`;
|
||||
|
||||
// 1 is the only value the old ternary treated specially; 0 and 2 both take
|
||||
// the "s" branch, and 0 is the one a naive `n > 1` port would get wrong.
|
||||
for (const n of [0, 1, 2, 5]) {
|
||||
it(`matches the pre-migration English for ${n} commit(s)`, () => {
|
||||
expect(t("integrations:commitChangedFiles", { count: n })).toBe(legacyCommit(n));
|
||||
expect(t("integrations:createPrCommitsAhead", { count: n })).toBe(legacyPr(n));
|
||||
expect(t("integrations:pushCommitsToRemote", { count: n })).toBe(legacyPush(n));
|
||||
});
|
||||
}
|
||||
|
||||
it("interpolates the git ref rather than translating it", () => {
|
||||
expect(t("integrations:ontoBranch", { branch: "origin/main" })).toBe("onto origin/main");
|
||||
expect(t("integrations:fromBranch", { branch: "release/1.2" })).toBe("from release/1.2");
|
||||
expect(t("integrations:rebaseOntoBranchBehind", { branch: "origin/main", behind: 3 })).toBe(
|
||||
"Rebase onto origin/main (3 behind)",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the divergence aria-labels count-invariant, as the shipped English was", () => {
|
||||
expect(t("integrations:commitsAheadAriaLabel", { value: 1 })).toBe("1 commits ahead");
|
||||
expect(t("integrations:commitsBehindAriaLabel", { value: 2 })).toBe("2 commits behind");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The discard dialog used to concatenate a " in your local clone at X" tail
|
||||
* onto a shared stem, which froze the tail's position in the sentence. Each
|
||||
* branch is now a whole message; these pin both to the previous rendering.
|
||||
*/
|
||||
describe("discard-local-changes description", () => {
|
||||
it("reads identically to the old stem + tail concatenation", () => {
|
||||
const stem = "Starting this task will permanently discard the uncommitted changes";
|
||||
const tail = "Back up anything you want to keep before continuing.";
|
||||
|
||||
expect(t("common:discardLocalChangesDescription")).toBe(`${stem} in your local clone. ${tail}`);
|
||||
expect(t("common:discardLocalChangesAtPathDescription", { repoPath: "/srv/repo" })).toBe(
|
||||
`${stem} in your local clone at /srv/repo. ${tail}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, type ComponentProps, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import {
|
||||
IconGitCommit,
|
||||
IconGitPullRequest,
|
||||
@@ -55,57 +57,75 @@ type PrimaryButtonConfig = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
// The count-bearing tooltips go through `count` + `_one`/`_other` rather than a
|
||||
// concatenated `commit${n !== 1 ? "s" : ""}`: an English morpheme appended here
|
||||
// puts the plural rule at the call site, where no other locale can reach it.
|
||||
function buildCommitConfig(
|
||||
t: TFunction,
|
||||
uncommittedFileCount: number,
|
||||
openCommitDialog: () => void,
|
||||
): PrimaryButtonConfig {
|
||||
return {
|
||||
icon: <IconGitCommit className="h-4 w-4" />,
|
||||
label: "Commit",
|
||||
label: t("integrations:commit"),
|
||||
badge: uncommittedFileCount > 0 ? uncommittedFileCount : null,
|
||||
tooltip:
|
||||
uncommittedFileCount > 0
|
||||
? `Commit ${uncommittedFileCount} changed file${uncommittedFileCount !== 1 ? "s" : ""}`
|
||||
: "No changes to commit",
|
||||
? t("integrations:commitChangedFiles", { count: uncommittedFileCount })
|
||||
: t("integrations:noChangesToCommit"),
|
||||
onClick: openCommitDialog,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPrConfig(aheadCount: number, openPRDialog: () => void): PrimaryButtonConfig {
|
||||
function buildPrConfig(
|
||||
t: TFunction,
|
||||
aheadCount: number,
|
||||
openPRDialog: () => void,
|
||||
): PrimaryButtonConfig {
|
||||
return {
|
||||
icon: <IconGitPullRequest className="h-4 w-4" />,
|
||||
label: "Create PR",
|
||||
label: t("integrations:createPr"),
|
||||
badge: null,
|
||||
tooltip: `Create PR (${aheadCount} commit${aheadCount !== 1 ? "s" : ""} ahead)`,
|
||||
tooltip: t("integrations:createPrCommitsAhead", { count: aheadCount }),
|
||||
onClick: openPRDialog,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPushConfig(aheadCount: number, handlePush: () => void): PrimaryButtonConfig {
|
||||
function buildPushConfig(
|
||||
t: TFunction,
|
||||
aheadCount: number,
|
||||
handlePush: () => void,
|
||||
): PrimaryButtonConfig {
|
||||
return {
|
||||
icon: <IconCloudUpload className="h-4 w-4" />,
|
||||
label: "Push",
|
||||
label: t("integrations:push"),
|
||||
badge: null,
|
||||
tooltip: `Push ${aheadCount} commit${aheadCount !== 1 ? "s" : ""} to remote`,
|
||||
tooltip: t("integrations:pushCommitsToRemote", { count: aheadCount }),
|
||||
onClick: handlePush,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRebaseConfig(
|
||||
t: TFunction,
|
||||
behindCount: number,
|
||||
baseBranch: string | undefined,
|
||||
handleRebase: () => void,
|
||||
): PrimaryButtonConfig {
|
||||
return {
|
||||
icon: <IconGitCherryPick className="h-4 w-4" />,
|
||||
label: "Rebase",
|
||||
label: t("integrations:rebase"),
|
||||
badge: behindCount > 0 ? behindCount : null,
|
||||
tooltip: `Rebase onto ${baseBranch || DEFAULT_BASE_BRANCH} (${behindCount} behind)`,
|
||||
// `branch` is a git ref — data, interpolated rather than translated.
|
||||
tooltip: t("integrations:rebaseOntoBranchBehind", {
|
||||
branch: baseBranch || DEFAULT_BASE_BRANCH,
|
||||
behind: behindCount,
|
||||
}),
|
||||
onClick: handleRebase,
|
||||
};
|
||||
}
|
||||
|
||||
type PrimaryConfigArgs = {
|
||||
t: TFunction;
|
||||
primaryAction: "commit" | "push" | "pr" | "rebase";
|
||||
uncommittedFileCount: number;
|
||||
aheadCount: number;
|
||||
@@ -118,6 +138,7 @@ type PrimaryConfigArgs = {
|
||||
};
|
||||
|
||||
function buildPrimaryButtonConfig({
|
||||
t,
|
||||
primaryAction,
|
||||
uncommittedFileCount,
|
||||
aheadCount,
|
||||
@@ -128,10 +149,11 @@ function buildPrimaryButtonConfig({
|
||||
handlePush,
|
||||
handleRebase,
|
||||
}: PrimaryConfigArgs): PrimaryButtonConfig {
|
||||
if (primaryAction === "push") return buildPushConfig(aheadCount, handlePush);
|
||||
if (primaryAction === "pr") return buildPrConfig(aheadCount, openPRDialog);
|
||||
if (primaryAction === "rebase") return buildRebaseConfig(behindCount, baseBranch, handleRebase);
|
||||
return buildCommitConfig(uncommittedFileCount, openCommitDialog);
|
||||
if (primaryAction === "push") return buildPushConfig(t, aheadCount, handlePush);
|
||||
if (primaryAction === "pr") return buildPrConfig(t, aheadCount, openPRDialog);
|
||||
if (primaryAction === "rebase")
|
||||
return buildRebaseConfig(t, behindCount, baseBranch, handleRebase);
|
||||
return buildCommitConfig(t, uncommittedFileCount, openCommitDialog);
|
||||
}
|
||||
|
||||
type DivergenceTone = "ahead" | "behind";
|
||||
@@ -144,17 +166,17 @@ const divergenceToneClass: Record<DivergenceTone, string> = {
|
||||
function DivergencePill({
|
||||
tone,
|
||||
value,
|
||||
label,
|
||||
ariaLabel,
|
||||
}: {
|
||||
tone: DivergenceTone;
|
||||
value: number;
|
||||
label: string;
|
||||
ariaLabel: string;
|
||||
}) {
|
||||
if (value <= 0) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-label={`${value} ${label}`}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
"inline-flex h-5 items-center rounded-md border px-1.5 text-[11px] font-semibold leading-none tabular-nums",
|
||||
divergenceToneClass[tone],
|
||||
@@ -167,12 +189,24 @@ function DivergencePill({
|
||||
}
|
||||
|
||||
function GitDivergencePills({ ahead, behind }: { ahead: number; behind: number }) {
|
||||
const { t } = useTranslation();
|
||||
if (ahead <= 0 && behind <= 0) return null;
|
||||
|
||||
// `{{value}}`, not `{{count}}`: the shipped English is count-invariant
|
||||
// ("1 commits ahead"), and switching to a plural here would change the
|
||||
// accessible name that E2E specs query by.
|
||||
return (
|
||||
<span className="ml-1 inline-flex items-center gap-1">
|
||||
<DivergencePill tone="ahead" value={ahead} label="commits ahead" />
|
||||
<DivergencePill tone="behind" value={behind} label="commits behind" />
|
||||
<DivergencePill
|
||||
tone="ahead"
|
||||
value={ahead}
|
||||
ariaLabel={t("integrations:commitsAheadAriaLabel", { value: ahead })}
|
||||
/>
|
||||
<DivergencePill
|
||||
tone="behind"
|
||||
value={behind}
|
||||
ariaLabel={t("integrations:commitsBehindAriaLabel", { value: behind })}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -202,16 +236,17 @@ function VcsDropdownItems({
|
||||
onRebase,
|
||||
onMerge,
|
||||
}: VcsDropdownItemsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onPR} disabled={disabled}>
|
||||
<IconGitPullRequest className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1">Create PR</span>
|
||||
<span className="flex-1">{t("integrations:createPr")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onPull} disabled={disabled}>
|
||||
<IconCloudDownload className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1">Pull</span>
|
||||
<span className="flex-1">{t("integrations:pull")}</span>
|
||||
{hasMatchingUpstream && behindCount > 0 && (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||||
↓{behindCount}
|
||||
@@ -221,7 +256,7 @@ function VcsDropdownItems({
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-pointer gap-3" disabled={disabled}>
|
||||
<IconCloudUpload className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1">Push</span>
|
||||
<span className="flex-1">{t("integrations:push")}</span>
|
||||
{hasMatchingUpstream && aheadCount > 0 && (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||||
↑{aheadCount}
|
||||
@@ -235,7 +270,7 @@ function VcsDropdownItems({
|
||||
disabled={disabled}
|
||||
>
|
||||
<IconCloudUpload className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Push</span>
|
||||
<span>{t("integrations:push")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer gap-3"
|
||||
@@ -243,23 +278,25 @@ function VcsDropdownItems({
|
||||
disabled={disabled}
|
||||
>
|
||||
<IconAlertTriangle className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Force Push</span>
|
||||
<span>{t("integrations:forcePush")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onRebase} disabled={disabled}>
|
||||
<IconGitCherryPick className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1">Rebase</span>
|
||||
<span className="flex-1">{t("integrations:rebase")}</span>
|
||||
{/* The branch is a git ref — data. One message with the ref
|
||||
interpolated, not an "onto" stem concatenated onto it. */}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
onto {baseBranch || DEFAULT_BASE_BRANCH}
|
||||
{t("integrations:ontoBranch", { branch: baseBranch || DEFAULT_BASE_BRANCH })}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer gap-3" onClick={onMerge} disabled={disabled}>
|
||||
<IconGitMerge className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex-1">Merge</span>
|
||||
<span className="flex-1">{t("integrations:merge")}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
from {baseBranch || DEFAULT_BASE_BRANCH}
|
||||
{t("integrations:fromBranch", { branch: baseBranch || DEFAULT_BASE_BRANCH })}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -273,6 +310,11 @@ type VcsSplitButtonProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// The `label` values below stay English on purpose. They are operation *names*
|
||||
// consumed by `useGitWithFeedback`, which concatenates them into its own English
|
||||
// toast titles (`${operationName} failed`). `hooks/use-git-with-feedback.ts` is
|
||||
// not migrated yet, so translating only this half would ship mixed-language
|
||||
// toasts. They move when that hook's messages do.
|
||||
function useGitActions(git: ReturnType<typeof useSessionGit>, baseBranch?: string) {
|
||||
const gitWithFeedback = useGitWithFeedback();
|
||||
|
||||
@@ -320,6 +362,7 @@ const VcsSplitButton = memo(function VcsSplitButton({
|
||||
buttonSize = "sm",
|
||||
className,
|
||||
}: VcsSplitButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const git = useSessionGit(sessionId);
|
||||
const { openCommitDialog, openPRDialog } = useVcsDialogs();
|
||||
const activePR = useActiveTaskPR();
|
||||
@@ -347,6 +390,7 @@ const VcsSplitButton = memo(function VcsSplitButton({
|
||||
hasOpenPR,
|
||||
);
|
||||
const primaryButtonConfig = buildPrimaryButtonConfig({
|
||||
t,
|
||||
primaryAction,
|
||||
uncommittedFileCount,
|
||||
aheadCount,
|
||||
@@ -439,6 +483,7 @@ function SingleRepoVcsButton({
|
||||
buttonSize?: ComponentProps<typeof Button>["size"];
|
||||
showDivergencePills?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={cn("inline-flex", className)}>
|
||||
<Tooltip>
|
||||
@@ -469,7 +514,7 @@ function SingleRepoVcsButton({
|
||||
size={buttonSize}
|
||||
variant="outline"
|
||||
className="-ml-px rounded-l-none px-2 cursor-pointer"
|
||||
aria-label="Open VCS options"
|
||||
aria-label={t("integrations:openVcsOptions")}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isGitLoading ? (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
resolveBaseBranch,
|
||||
resolveRepositoryId,
|
||||
} from "@/lib/watcher-repository-default";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PickItem = { id: string; label: string };
|
||||
|
||||
@@ -74,6 +75,7 @@ export function WatcherRepositoryFields({
|
||||
onRepositoryChange: (repositoryId: string) => void;
|
||||
onBaseBranchChange: (baseBranch: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// forceRefresh: a repo created in settings doesn't update the shared store
|
||||
// slice, and the lazy fetch is gated by isLoaded — so without this the picker
|
||||
// could miss repositories created since the slice first loaded. The hook owns
|
||||
@@ -89,8 +91,8 @@ export function WatcherRepositoryFields({
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<PickSelect
|
||||
label="Repository"
|
||||
description="Optional — the repository the agent works in."
|
||||
label={t("common:repository")}
|
||||
description={t("common:optionalTheRepositoryTheAgentWorks")}
|
||||
value={repositoryId || NO_REPOSITORY}
|
||||
onChange={(v) => onRepositoryChange(resolveRepositoryId(v))}
|
||||
placeholder={NO_REPOSITORY_LABEL}
|
||||
@@ -100,8 +102,8 @@ export function WatcherRepositoryFields({
|
||||
]}
|
||||
/>
|
||||
<PickSelect
|
||||
label="Base Branch"
|
||||
description="The base branch the agent starts from."
|
||||
label={t("common:baseBranch")}
|
||||
description={t("common:theBaseBranchTheAgentStarts")}
|
||||
value={baseBranch || DEFAULT_BRANCH}
|
||||
onChange={(v) => onBaseBranchChange(resolveBaseBranch(v))}
|
||||
placeholder={branchPlaceholder(repositoryId, branchesLoading)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, memo, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconCheck, IconChevronDown, IconLogicBuffer } from "@tabler/icons-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover";
|
||||
@@ -25,6 +26,7 @@ function InlineSteps({
|
||||
steps: StepItem[];
|
||||
agentProfiles: AgentProfileOption[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (steps.length === 0) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground whitespace-nowrap">
|
||||
@@ -47,7 +49,7 @@ function InlineSteps({
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-[10px] text-muted-foreground/60 leading-none">*</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Start step</TooltipContent>
|
||||
<TooltipContent>{t("workflows:startStep")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
@@ -97,6 +99,7 @@ export const WorkflowSelectorRow = memo(function WorkflowSelectorRow({
|
||||
lastUsedWorkflowId,
|
||||
agentProfiles,
|
||||
}: WorkflowSelectorRowProps) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const selectedWorkflow = useMemo(
|
||||
@@ -124,12 +127,16 @@ export const WorkflowSelectorRow = memo(function WorkflowSelectorRow({
|
||||
data-testid="workflow-selector-trigger"
|
||||
>
|
||||
<IconLogicBuffer className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate">{selectedWorkflow?.name ?? "Select workflow"}</span>
|
||||
<span className="truncate">
|
||||
{selectedWorkflow?.name ?? t("workflows:selectWorkflow")}
|
||||
</span>
|
||||
<IconChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto min-w-[300px] max-w-none p-1" align="start">
|
||||
<div className="text-muted-foreground px-2 py-1.5 text-xs border-b">Workflow</div>
|
||||
<div className="text-muted-foreground px-2 py-1.5 text-xs border-b">
|
||||
{t("workflows:workflow")}
|
||||
</div>
|
||||
{sortedWorkflows.map((wf) => {
|
||||
const isSelected = wf.id === selectedWorkflowId;
|
||||
const snapshot = snapshots[wf.id];
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRepoDisplayName } from "@/hooks/domains/session/use-repo-display-nam
|
||||
import type { WorkspaceContentSearchError } from "@/hooks/domains/session/use-workspace-content-search";
|
||||
import { groupByRepositoryName, isSingleRepoGroup } from "@/lib/group-by-repo";
|
||||
import type { ContentSearchMatchRange, WorkspaceContentSearchResult } from "@/lib/types/backend";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PreviewPart = { text: string; matched: boolean };
|
||||
|
||||
@@ -137,33 +138,38 @@ export function WorkspaceContentSearch({
|
||||
sessionId,
|
||||
onSelect,
|
||||
}: WorkspaceContentSearchProps) {
|
||||
const { t } = useTranslation();
|
||||
const getRepoDisplayName = useRepoDisplayName(sessionId);
|
||||
if (error === "session-unavailable") {
|
||||
return <CommandEmpty>Content search needs an active task session.</CommandEmpty>;
|
||||
return <CommandEmpty>{t("common:contentSearchNeedsAnActiveTask")}</CommandEmpty>;
|
||||
}
|
||||
if (error === "query-too-long") {
|
||||
return <CommandEmpty>Search queries are limited to 200 characters.</CommandEmpty>;
|
||||
return <CommandEmpty>{t("common:searchQueriesAreLimitedTo200")}</CommandEmpty>;
|
||||
}
|
||||
if (error === "transport-error") {
|
||||
return <CommandEmpty>Search failed. Edit the query or reopen search to retry.</CommandEmpty>;
|
||||
return <CommandEmpty>{t("common:searchFailedEditTheQueryOr")}</CommandEmpty>;
|
||||
}
|
||||
if (isSearching && results.length === 0) {
|
||||
return (
|
||||
<CommandEmpty>
|
||||
<IconLoader2 className="mr-2 inline size-3.5 animate-spin text-muted-foreground" />
|
||||
Searching task workspace…
|
||||
{t("common:searchingTaskWorkspace")}
|
||||
</CommandEmpty>
|
||||
);
|
||||
}
|
||||
if (!search.trim()) return <CommandEmpty>Type to search task contents…</CommandEmpty>;
|
||||
if (results.length === 0) return <CommandEmpty>No content matches found.</CommandEmpty>;
|
||||
if (!search.trim()) return <CommandEmpty>{t("common:typeToSearchTaskContents")}</CommandEmpty>;
|
||||
if (results.length === 0) return <CommandEmpty>{t("common:noContentMatchesFound")}</CommandEmpty>;
|
||||
|
||||
const groups = groupByRepositoryName(results, (result) => result.repository_name);
|
||||
const singleRepo = isSingleRepoGroup(groups);
|
||||
return groups.map((group) => (
|
||||
<CommandGroup
|
||||
key={group.repositoryName}
|
||||
heading={singleRepo ? "Results" : (getRepoDisplayName(group.repositoryName) ?? "Workspace")}
|
||||
heading={
|
||||
singleRepo
|
||||
? t("common:results")
|
||||
: (getRepoDisplayName(group.repositoryName) ?? t("common:workspace"))
|
||||
}
|
||||
forceMount
|
||||
data-testid="content-search-repo-group"
|
||||
data-repository={group.repositoryName}
|
||||
|
||||
@@ -1677,4 +1677,38 @@ export const i18nGuardFiles = [
|
||||
"components/task/share/**/*.{ts,tsx}",
|
||||
"components/task/inspector/**/*.{ts,tsx}",
|
||||
"components/task/document/**/*.{ts,tsx}",
|
||||
// Loose components directly under `components/`. Individual file entries
|
||||
// rather than a `components/*.tsx` glob: the sibling `components/task-*.tsx`
|
||||
// files match the `components/(task\/|task-)` namespace rule and belong with
|
||||
// the `components/task` batches, so a glob here would falsely claim them.
|
||||
//
|
||||
// These have no namespace rule of their own, so they are on `common` — except
|
||||
// the two `vcs-*` menus and `workflow-selector-row`, which were placed on
|
||||
// `integrations` / `workflows` by hand to sit with the copy they share.
|
||||
//
|
||||
// Deliberately left in English inside these files, because they are protocol
|
||||
// or data rather than copy: the `gh` CLI binary name and the `kdlbs/kandev`
|
||||
// repository slug in the Improve Kandev dialogs, the `esc` key name in the
|
||||
// onboarding preview and the command-panel footer, the `make install`/`/commit`
|
||||
// shell and slash commands, git refs reaching `ontoBranch`/`fromBranch`, and
|
||||
// the `ActionDef.key` / `RUNTIMES[].id` discriminants that keep React keys
|
||||
// locale-independent.
|
||||
"components/branch-refresh-button.tsx",
|
||||
"components/cli-mode-icon.tsx",
|
||||
"components/command-panel-footer.tsx",
|
||||
"components/command-panel-scope-switcher.tsx",
|
||||
"components/create-local-repository-surface.tsx",
|
||||
"components/discard-local-changes-dialog.tsx",
|
||||
"components/enhance-prompt-button.tsx",
|
||||
"components/folder-picker.tsx",
|
||||
"components/grid-spinner.tsx",
|
||||
"components/improve-kandev-dialog-create.tsx",
|
||||
"components/improve-kandev-dialog.tsx",
|
||||
"components/onboarding-dialog.tsx",
|
||||
"components/prompt-result-recovery.tsx",
|
||||
"components/vcs-multi-repo-menu.tsx",
|
||||
"components/vcs-split-button.tsx",
|
||||
"components/watcher-repository-fields.tsx",
|
||||
"components/workflow-selector-row.tsx",
|
||||
"components/workspace-content-search.tsx",
|
||||
];
|
||||
|
||||
@@ -2,19 +2,39 @@
|
||||
"aboutContextCompactionCount": "About inferred compaction count",
|
||||
"activeSubagents_one": "{{count}} subagent running",
|
||||
"activeSubagents_other": "{{count}} subagents running",
|
||||
"activeTasks": "Active Tasks",
|
||||
"aDirectLocalExecutorProfileIsRequired": "A direct local executor profile is required to create and use an empty repository.",
|
||||
"agentDashboard": "agent dashboard",
|
||||
"agentRun": "agent run",
|
||||
"agentRuns": "agent runs",
|
||||
"agents": "Agents",
|
||||
"agentSkillsAskTheAgentTo": "Agent skills (ask the agent to run)",
|
||||
"allowIndexing": "Allow indexing",
|
||||
"andMoreFiles_one": "+{{count}} more",
|
||||
"andMoreFiles_other": "+{{count}} more",
|
||||
"anEnhancedPromptIsAvailable": "An enhanced prompt is available.",
|
||||
"apply": "Apply",
|
||||
"autoApproveAllPermissions": "Auto-approve all permissions",
|
||||
"automations": "Automations",
|
||||
"availableTemplates": "Available templates",
|
||||
"back": "Back",
|
||||
"backToTaskOverview": "Back to task overview",
|
||||
"baseBranch": "Base Branch",
|
||||
"bootsASecondaryKandevDevInstance": "Boots a secondary kandev dev instance with a clean DB so you can verify the change without touching your main one.",
|
||||
"bootstrapFailed": "Bootstrap failed",
|
||||
"bootstrapFailedCloseAndReopen": "Bootstrap failed — close and reopen to retry.",
|
||||
"breadcrumb": "breadcrumb",
|
||||
"browseParentDirectory": "Browse parent directory",
|
||||
"bugFix": "Bug fix",
|
||||
"cancel": "Cancel",
|
||||
"cancelNewFolder": "Cancel new folder",
|
||||
"changelog": "Changelog",
|
||||
"checkingGithubAuthentication": "Checking GitHub authentication…",
|
||||
"chooseAFolderOrEnterA": "Choose a folder or enter a new path on this machine.",
|
||||
"cliClient": "CLI client",
|
||||
"cliModeYourPromptWillBe": "CLI mode — your prompt will be auto-injected into the terminal",
|
||||
"cliPassthrough": "CLI passthrough",
|
||||
"close": "Close",
|
||||
"commandAgentsSettingsKeywords": "agents, agent settings, agent profiles, installed agents, claude",
|
||||
"commandConfigChatKeywords": "config chat, config mode, configure kandev, workflow configuration, mcp configuration",
|
||||
"commandExecutorsSettingsKeywords": "executors, executor profiles, execution environment, environment variables, runtime, compute",
|
||||
@@ -33,6 +53,12 @@
|
||||
"commandGroupNavigation": "Navigation",
|
||||
"commandGroupPreferences": "Preferences",
|
||||
"commandGroupSettings": "Settings",
|
||||
"commandPaletteMode": "Command palette mode",
|
||||
"commandPreviewAddTerminalPanel": "Add Terminal Panel",
|
||||
"commandPreviewCommitChanges": "Commit Changes",
|
||||
"commandPreviewGoToHome": "Go to Home",
|
||||
"commandPreviewPull": "Pull",
|
||||
"commandPreviewSearchTasks": "Search Tasks",
|
||||
"commandPromptsSettingsKeywords": "prompts, prompt settings, custom prompts, prompt snippets, prompt templates",
|
||||
"commandQuickChat": "Quick Chat",
|
||||
"commandQuickChatKeywords": "quick chat, new quick chat, quick question, ask agent",
|
||||
@@ -40,44 +66,118 @@
|
||||
"commandSwitchToLightMode": "Switch to Light Mode",
|
||||
"commandThemeKeywords": "theme, color theme, appearance",
|
||||
"commandWorkspaceSettingsKeywords": "workspace, workspaces",
|
||||
"commitsAndPushesToTheCurrent": "Commits and pushes to the current branch.",
|
||||
"commitsPushesAndOpensAPull": "Commits, pushes, and opens a pull request.",
|
||||
"configurationChat": "Configuration Chat",
|
||||
"configureAUtilityAgentToEnhance": "Configure a utility agent in settings to enable AI enhancement",
|
||||
"configureExecutorsInSettingsToControl": "Configure executors in Settings to control where agents execute.",
|
||||
"configureGithub": "Configure GitHub",
|
||||
"confirm": "Confirm",
|
||||
"contents": "Contents",
|
||||
"contentSearchNeedsAnActiveTask": "Content search needs an active task session.",
|
||||
"contextCompactionCountHelp": "Kandev infers this count from observed context usage drops. Missing samples or provider resets can make it approximate.",
|
||||
"contextCompactions": "Compactions",
|
||||
"contribute": "Contribute",
|
||||
"contributingAsLogin": "Contributing as <1>@{{login}}</1>. {{access}}",
|
||||
"contributingAsYourGithubAccount": "Contributing as your GitHub account. {{access}}",
|
||||
"copy": "Copy",
|
||||
"couldNotPrepareImproveKandev": "Could not prepare Improve Kandev",
|
||||
"createATaskDescribingYourBug": "Create a task describing your bug or feature request",
|
||||
"createAWorkspaceToConfigureThisIntegration": "Create a workspace to configure this integration.",
|
||||
"createFolder": "Create folder",
|
||||
"createNewRepository": "Create new repository",
|
||||
"createRepository": "Create repository",
|
||||
"createsAnEmptyGitRepository": "Creates an empty Git repository and registers it in this workspace.",
|
||||
"creating": "Creating…",
|
||||
"dAgo": "{{diffDay}}d ago",
|
||||
"default": "default",
|
||||
"defaultBadge": "Default",
|
||||
"deleteSession": "Delete session",
|
||||
"describeABugYouHitOr": "Describe a bug you hit or a feature you'd like, and we'll create a task on your own agent to implement it in the kandev codebase.",
|
||||
"destination": "Destination",
|
||||
"discardAndContinue": "Discard and continue",
|
||||
"discardLocalChanges": "Discard local changes?",
|
||||
"discardLocalChangesAtPathDescription": "Starting this task will permanently discard the uncommitted changes in your local clone at {{repoPath}}. Back up anything you want to keep before continuing.",
|
||||
"discardLocalChangesDescription": "Starting this task will permanently discard the uncommitted changes in your local clone. Back up anything you want to keep before continuing.",
|
||||
"doNotShowThisAgain": "Do not show this again",
|
||||
"edit": "Edit",
|
||||
"editTask": "Edit task",
|
||||
"gitScopeSkipped": "Skipped {{scope}} because a child repository operation failed ({{failedScopes}}).",
|
||||
"emptyRepositoriesRunDirectlySwitch": "Empty repositories run directly on this machine. This task will switch to “{{profile}}”.",
|
||||
"enhancePromptWithAi": "Enhance prompt with AI",
|
||||
"enterAValue": "Enter a value...",
|
||||
"enterValue": "Enter value...",
|
||||
"executors": "Executors",
|
||||
"externalMcp": "External MCP",
|
||||
"failedToCreateFolder": "Failed to create folder",
|
||||
"failedToCreateRepository": "Failed to create repository",
|
||||
"failedToLoadDirectory": "Failed to load directory",
|
||||
"failedToLoadRoutine": "Failed to load routine",
|
||||
"failedToLoadSetup": "Failed to load setup",
|
||||
"featureRequest": "Feature request",
|
||||
"files": "Files",
|
||||
"folderName": "Folder name",
|
||||
"folders": "Folders",
|
||||
"forwardStdinStdoutStraightToThe": "Forward stdin/stdout straight to the CLI subprocess. Disables ACP.",
|
||||
"getStarted": "Get Started",
|
||||
"githubCliNotAuthenticated": "GitHub CLI not authenticated",
|
||||
"gitScopeSkipped": "Skipped {{scope}} because a child repository operation failed ({{failedScopes}}).",
|
||||
"hAgo": "{{diffHr}}h ago",
|
||||
"hideAdvancedOptions": "Hide advanced options",
|
||||
"howLogContextWorks": "How log context works",
|
||||
"improveKandev": "Improve Kandev",
|
||||
"improveKandevBugPlaceholder": "E.g. When I open the kanban board, the column header text overlaps the task count badge on screens narrower than 1200px...",
|
||||
"improveKandevFeaturePlaceholder": "E.g. Add a keyboard shortcut to mark a task as Done from the task detail view, and show the shortcut in the task action menu...",
|
||||
"improveKandevIssuePlaceholder": "Describe the problem or request you want published. The agent will ask for any details required by the matching GitHub issue template...",
|
||||
"includeRecentBackendAndBrowserLogs": "Include recent backend & browser logs as context for the agent",
|
||||
"integrations": "Integrations",
|
||||
"justNow": "just now",
|
||||
"kandevAllowsEveryAgentPermissionRequest": "Kandev allows every agent permission request without prompting you.",
|
||||
"kandevCouldNotLoad": "Kandev couldn’t load.",
|
||||
"kandevIsOpenSourceAndYou": "Kandev is open source, and you can help make it better.",
|
||||
"kandevKeepsASmallInMemory": "Kandev keeps a small in-memory ring buffer of the most recent backend logs and browser console events. When enabled, those logs are written to a temporary folder on your machine, and the file paths are appended to the task description so the agent can read them while investigating.",
|
||||
"lastRefreshFailed": "Last refresh failed: {{error}}",
|
||||
"loading": "Loading…",
|
||||
"loadingEllipsis": "Loading...",
|
||||
"loadingFolder": "Loading folder…",
|
||||
"loadingIndicatorLabel": "Loading",
|
||||
"loadingLabel": "Loading {{label}}...",
|
||||
"loadingRoute": "Loading {{routeName}}…",
|
||||
"loadingRoutine": "Loading routine...",
|
||||
"loadingTask": "Loading task…",
|
||||
"loadingWorkflowTemplates": "Loading workflow templates...",
|
||||
"mAgo": "{{diffMin}}m ago",
|
||||
"mode": "Mode",
|
||||
"model": "Model",
|
||||
"multiRepoUnavailableLocal": "Multi-repo tasks are unavailable on Local until its initial launch path can project sibling repositories.",
|
||||
"multiRepoUnavailableRemoteDocker": "Multi-repo tasks are unavailable on Remote Docker until it supports creating task instances.",
|
||||
"multiRepoUnsupportedExecutor": "Multi-repo tasks are not supported by this executor.",
|
||||
"navigate": "Navigate",
|
||||
"navigateBetweenPagesSearchTasksTrigger": "Navigate between pages, search tasks, trigger git operations, and manage panels — all without leaving the keyboard. Context-aware commands appear based on the active page.",
|
||||
"newFolder": "New folder",
|
||||
"newFolderName": "New folder name",
|
||||
"next": "Next",
|
||||
"noCliClientsInstalledYetInstall": "No CLI clients installed yet. Install Claude / Codex / OpenCode / Amp to enable this picker.",
|
||||
"noCommandsFound": "No commands found.",
|
||||
"noContentMatchesFound": "No content matches found.",
|
||||
"noFilesFound": "No files found.",
|
||||
"noFoldersHerePickThisOne": "No folders here — pick this one or go up",
|
||||
"officeIsNotEnabledForThis": "Office is not enabled for this runtime.",
|
||||
"officeRouteNotPortedYet": "This Office route is handled by the SPA shell, but its dedicated client page is still being ported: <1>{{pathname}}</1>",
|
||||
"onboardingStepAgentsDescription": "Manage discovered agents and install new ones.",
|
||||
"onboardingStepAgentsTitle": "AI Agents",
|
||||
"onboardingStepCommandPanelDescription": "Quick access to actions from anywhere with a keyboard shortcut.",
|
||||
"onboardingStepCommandPanelTitle": "Command Panel",
|
||||
"onboardingStepExecutorsDescription": "Agents can run in different executor environments — local, containerized, or remote.",
|
||||
"onboardingStepExecutorsTitle": "Executors",
|
||||
"onboardingStepWorkflowsDescription": "Workflows define the steps and automation for your tasks.",
|
||||
"onboardingStepWorkflowsTitle": "Agentic Workflows",
|
||||
"open": "Open",
|
||||
"openFullPage": "Open full page",
|
||||
"openingLastTask": "Opening last task…",
|
||||
"openIssue": "Open issue",
|
||||
"optionalTheRepositoryTheAgentWorks": "Optional — the repository the agent works in.",
|
||||
"panels": "Panels",
|
||||
"parentDirectory": "Parent directory",
|
||||
"permitTheCliToUploadCode": "Permit the CLI to upload code for cloud indexing (auggie / similar).",
|
||||
"pickACliClient": "Pick a CLI client",
|
||||
"pickACliClientToLoad": "Pick a CLI client to load its available models and modes.",
|
||||
@@ -85,6 +185,8 @@
|
||||
"pluginPanelUnavailable": "This panel is no longer available.",
|
||||
"plugins": "Plugins",
|
||||
"prDetails": "PR Details",
|
||||
"preparingKandevRepository": "Preparing kandev repository…",
|
||||
"preparingKandevRepositoryInBackground": "Preparing kandev repository in background. Fill in the details, submit when ready.",
|
||||
"presetIconBug": "Bug",
|
||||
"presetIconCheck": "Check",
|
||||
"presetIconCode": "Code",
|
||||
@@ -93,15 +195,22 @@
|
||||
"presetIconSearch": "Search",
|
||||
"presetIconSparkle": "Sparkle",
|
||||
"presetIconTool": "Tool",
|
||||
"pressCmdKToOpenItAnytime": "<0>Press</0><1></1><2>to open it anytime</2>",
|
||||
"pressEnterToConfirm": "Press Enter to confirm",
|
||||
"profileName": "Profile name",
|
||||
"prompts": "Prompts",
|
||||
"refreshAriaLabel": "Refresh {{label}}",
|
||||
"refreshBranchesGitFetch": "Refresh branches (git fetch)",
|
||||
"refreshingLabel": "Refreshing {{label}}...",
|
||||
"refreshLabelLastFetched": "Refresh {{label}} (last fetched {{time}})",
|
||||
"reload": "Reload",
|
||||
"reloadTheApplicationToTryAgain": "Reload the application to try again.",
|
||||
"requestFailed": "Request failed",
|
||||
"rename": "Rename",
|
||||
"reviewSubmoduleBoundary": "Submodule: {{scope}}",
|
||||
"repository": "Repository",
|
||||
"repositoryName": "Repository name",
|
||||
"requestFailed": "Request failed",
|
||||
"reset": "Reset",
|
||||
"resizeTableColumns": "Resize table columns {{left}} and {{right}}",
|
||||
"resetting": "Resetting…",
|
||||
"resetWatchChecking": "Checking how many tasks would be deleted…",
|
||||
"resetWatchDeleteAllTasks": "This will delete every task previously created by the watch, including archived tasks. The watch's polling cursor is also cleared so the next check re-imports every currently-matching item. This cannot be undone.",
|
||||
"resetWatchDeleteTasks_one": "This will delete {{count}} task previously created by the watch, including archived tasks. The watch's polling cursor is also cleared so the next check re-imports every currently-matching item. This cannot be undone.",
|
||||
@@ -109,24 +218,70 @@
|
||||
"resetWatchNoTasks": "No tasks were created by this watch yet — only the polling state will be cleared. The watch's polling cursor is also cleared so the next check re-imports every currently-matching item. This cannot be undone.",
|
||||
"resetWatchPreviewFailedRequired": "Could not load the affected task count. Retry the preview before resetting this watch.",
|
||||
"resetWatchTitle": "Reset {{label}}?",
|
||||
"resetting": "Resetting…",
|
||||
"resizeTableColumns": "Resize table columns {{left}} and {{right}}",
|
||||
"results": "Results",
|
||||
"retryPreview": "Retry preview",
|
||||
"reviewSubmoduleBoundary": "Submodule: {{scope}}",
|
||||
"runsFormatTypecheckTestAndLint": "Runs format, typecheck, test, and lint across the monorepo.",
|
||||
"runtimeDocker": "Docker",
|
||||
"runtimeDockerDescription": "Containerized execution for full isolation and reproducibility.",
|
||||
"runtimeGitWorktree": "Git Worktree",
|
||||
"runtimeGitWorktreeDescription": "Isolated branch environment under a worktree root for parallel work.",
|
||||
"runtimeLocal": "Local",
|
||||
"runtimeLocalDescription": "Run agents directly on your machine with full access to your local filesystem.",
|
||||
"runtimeSprites": "Sprites (Remote sprites.dev)",
|
||||
"runtimeSpritesDescription": "Hardware-isolated execution environment for arbitrary code.",
|
||||
"save": "Save",
|
||||
"scopeCommands": "Commands",
|
||||
"scopeContents": "Contents",
|
||||
"scopeFiles": "Files",
|
||||
"scopeTitleWithShortcut": "{{label}} ({{shortcut}})",
|
||||
"scratchWorkspace": "scratch workspace",
|
||||
"searchFailedEditTheQueryOr": "Search failed. Edit the query or reopen search to retry.",
|
||||
"searchForFiles": "Search for files...",
|
||||
"searchForTasks": "Search for tasks...",
|
||||
"searchingTaskWorkspace": "Searching task workspace…",
|
||||
"searchQueriesAreLimitedTo200": "Search queries are limited to 200 characters.",
|
||||
"searchTaskContents": "Search task contents…",
|
||||
"select": "Select",
|
||||
"settings": "Settings",
|
||||
"shell": "Shell",
|
||||
"shellCommandPlusSlashCommandSkills": "Shell command you can run in the secondary instance, plus slash-command <2>skills</2> you can ask the agent to run during the workflow.",
|
||||
"showAdvancedOptions": "Show advanced options",
|
||||
"skip": "Skip",
|
||||
"stagesAndCommitsChangesUsingConventional": "Stages and commits changes using Conventional Commits.",
|
||||
"status": "Status",
|
||||
"subtaskTitle": "Subtask title",
|
||||
"switchMode": "Switch mode",
|
||||
"system": "System",
|
||||
"taskLoading": "Loading task...",
|
||||
"tasks": "Tasks",
|
||||
"taskUnavailable": "Task unavailable",
|
||||
"taskUnavailableDescription": "We could not load this task. It may have been deleted or you may not have access.",
|
||||
"theAgentForksKandevToYourAccount": "The agent forks <1>{{repo}}</1> to your GitHub account and opens a PR from your fork, credited to you",
|
||||
"theBaseBranchTheAgentStarts": "The base branch the agent starts from.",
|
||||
"theFinalStepOpensAPullRequest": "The final step of this workflow opens a pull request, which needs the <1>{{binary}}</1> CLI to be authenticated. {{message}}",
|
||||
"thisEmptyRepositoryWillRunWith": "This empty repository will run with “{{profile}}” on this machine.",
|
||||
"thisGithubAccountCannotForkKdlbs": "This GitHub account cannot fork kdlbs/kandev.",
|
||||
"thisPageCouldNotLoad": "This page couldn’t load.",
|
||||
"thisWorkflowWillOnlyCreateA": "This workflow will only create a GitHub issue from the repository template. The agent will ask for missing details and publication approval; it will not implement the change or open a pull request.",
|
||||
"title": "Title",
|
||||
"toastRegionLabel": "Notifications",
|
||||
"typeACommand": "Type a command...",
|
||||
"typeToSearchFiles": "Type to search files...",
|
||||
"typeToSearchTaskContents": "Type to search task contents…",
|
||||
"unknownPanel": "Unknown panel: {{component}}",
|
||||
"unsavedChanges": "Unsaved changes",
|
||||
"usefulCommandsAgentSkills": "Useful commands & agent skills",
|
||||
"useScratchInstead": "Use scratch instead",
|
||||
"useThisFolder": "Use this folder",
|
||||
"waitsForCiAndAutomatedReviews": "Waits for CI and automated reviews, then fixes failures and addresses comments.",
|
||||
"whenItsDoneTheAgentOpensAPr": "When it's done, the agent opens a pull request to <2>{{repo}}</2> for the maintainers to review, saving them time and shipping the improvement to everyone.",
|
||||
"workflow": "Workflow",
|
||||
"workflowsControlTheStepsAutomationAnd": "Workflows control the steps, automation, and agent behavior for your tasks. You can add more workflows from Settings.",
|
||||
"workspace": "Workspace",
|
||||
"workspaces": "Workspaces",
|
||||
"youCanUseTheNavigationOr": "You can use the navigation or reload to try again."
|
||||
"youCanUseTheNavigationOr": "You can use the navigation or reload to try again.",
|
||||
"yourAgentImplementsItInThe": "Your agent implements it in the kandev repository, with tests",
|
||||
"youVerifyAndTestTheChange": "You verify and test the change in a second kandev instance"
|
||||
}
|
||||
|
||||
@@ -2,31 +2,38 @@
|
||||
"addDetailsAboutThisChange": "Add details about this change...",
|
||||
"allRepos": "All repos",
|
||||
"anErrorOccurred": "An error occurred",
|
||||
"authRequiredHeading": "{{name}} authentication required",
|
||||
"authRequiredInline": "{{name}} authentication required.",
|
||||
"authenticated": "Authenticated",
|
||||
"authenticationFailed": "Authentication failed: {{error}}",
|
||||
"authRequiredHeading": "{{name}} authentication required",
|
||||
"authRequiredInline": "{{name}} authentication required.",
|
||||
"branchPushedNotCreated": "Branch pushed; {{shortName}} not created",
|
||||
"branchWasPushedRetryCreation": "Branch was pushed; retry {{longName}} creation.",
|
||||
"checkedRelative": "· checked {{relative}}",
|
||||
"commit": "Commit",
|
||||
"commitChangedFiles_one": "Commit {{count}} changed file",
|
||||
"commitChangedFiles_other": "Commit {{count}} changed files",
|
||||
"commitChanges": "Commit Changes",
|
||||
"commitChangesScoped": "Commit Changes — {{scopedRepo}}",
|
||||
"commitsAheadAriaLabel": "{{value}} commits ahead",
|
||||
"commitsBehindAriaLabel": "{{value}} commits behind",
|
||||
"committingEllipsis": "Committing...",
|
||||
"configureAUtilityAgentInSettings": "Configure a utility agent in settings to enable AI generation",
|
||||
"copiedConfigTo": "Copied {{label}} config to {{targetName}}.",
|
||||
"copyConfig": "Copy config",
|
||||
"copyConfigSettingsOnly": "Copy the {{label}} settings from <4>{{sourceName}}</4> into another workspace. This overwrites the target workspace's current {{label}} configuration. Watchers and automations are not copied.",
|
||||
"copyConfigToAnotherWorkspace": "Copy config to another workspace",
|
||||
"copyConfigWithCredentials": "Copy the {{label}} settings and credentials from <4>{{sourceName}}</4> into another workspace. This overwrites the target workspace's current {{label}} configuration. Watchers and automations are not copied.",
|
||||
"copyConfiguration": "Copy {{label}} configuration",
|
||||
"copyConfigWithCredentials": "Copy the {{label}} settings and credentials from <4>{{sourceName}}</4> into another workspace. This overwrites the target workspace's current {{label}} configuration. Watchers and automations are not copied.",
|
||||
"copyingConfigTo": "Copying {{label}} config to {{targetName}}…",
|
||||
"createAsDraft": "Create as draft",
|
||||
"createdSuccessfully": "{{longName}} created successfully",
|
||||
"createFailed": "Create {{shortName}} failed",
|
||||
"createLongName": "Create {{longName}}",
|
||||
"createPr": "Create PR",
|
||||
"createPrCommitsAhead_one": "Create PR ({{count}} commit ahead)",
|
||||
"createPrCommitsAhead_other": "Create PR ({{count}} commits ahead)",
|
||||
"createScoped": "Create {{longName}} — {{scopedRepo}}",
|
||||
"createShortName": "Create {{shortName}}",
|
||||
"createdSuccessfully": "{{longName}} created successfully",
|
||||
"creatingEllipsis": "Creating...",
|
||||
"creatingFrom": "Creating {{shortName}} from <4>{{displayBranch}}</4>",
|
||||
"creatingFromInto": "Creating {{shortName}} from <4>{{displayBranch}}</4> → <6>{{baseBranch}}</6>",
|
||||
@@ -40,6 +47,8 @@
|
||||
"failedToCopyConfig": "Failed to copy config: {{err}}",
|
||||
"filesChanged_one": "<0>{{count}}</0> file changed",
|
||||
"filesChanged_other": "<0>{{count}}</0> files changed",
|
||||
"forcePush": "Force Push",
|
||||
"fromBranch": "from {{branch}}",
|
||||
"generateCommitDescriptionWithAi": "Generate commit description with AI",
|
||||
"generateCommitMessageWithAi": "Generate commit message with AI",
|
||||
"generateDescriptionWithAi": "Generate {{shortName}} description with AI",
|
||||
@@ -47,8 +56,22 @@
|
||||
"loading": "Loading…",
|
||||
"longNameTitle": "{{longName}} title",
|
||||
"longNameTitlePlaceholder": "{{longName}} title...",
|
||||
"merge": "Merge",
|
||||
"noChangesToCommit": "No changes to commit",
|
||||
"noSavedQueriesYet": "No saved queries yet",
|
||||
"ontoBranch": "onto {{branch}}",
|
||||
"openVcsOptions": "Open VCS options",
|
||||
"pickActionThenRepo": "Pick action, then repo",
|
||||
"pickARepositoryForCommit": "Pick a repository for Commit",
|
||||
"pickARepositoryForCreatePr": "Pick a repository for Create PR",
|
||||
"pickARepositoryForPush": "Pick a repository for Push",
|
||||
"pickARepositoryForRebase": "Pick a repository for Rebase",
|
||||
"pull": "Pull",
|
||||
"push": "Push",
|
||||
"pushCommitsToRemote_one": "Push {{count}} commit to remote",
|
||||
"pushCommitsToRemote_other": "Push {{count}} commits to remote",
|
||||
"rebase": "Rebase",
|
||||
"rebaseOntoBranchBehind": "Rebase onto {{branch}} ({{behind}} behind)",
|
||||
"reconnectProvider": "Reconnect {{name}}",
|
||||
"repository": "Repository",
|
||||
"retryShortName": "Retry {{shortName}}",
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"wipLimit": "WIP limit",
|
||||
"wipLimitHelp": "Maximum admitted (active) tasks in this step. Overflow stays visible in a queue. Use 0 for unlimited.",
|
||||
"wipLimitsNote": "WIP limits active work, not visibility. Overflow remains on the board until capacity opens; if a selected feeder is also full, new task creation is rejected.",
|
||||
"workflow": "Workflow",
|
||||
"workflowCycleBlocked": "Workflow cycle blocked",
|
||||
"workflowCycleBlockedDescription": "Change the workflow steps to remove the automatic cycle before continuing.",
|
||||
"workflowCycleBlockingBody": "This path re-enters \"{{stepName}}\" and can start the agent again without another user action.",
|
||||
|
||||
@@ -2,19 +2,39 @@
|
||||
"aboutContextCompactionCount": "Àƀōũţ ĩńƒēŕŕēď ćōḿƥàćţĩōń ćōũńţ",
|
||||
"activeSubagents_one": "{{count}} śũƀàĝēńţ ŕũńńĩńĝ",
|
||||
"activeSubagents_other": "{{count}} śũƀàĝēńţś ŕũńńĩńĝ",
|
||||
"activeTasks": "Àćţĩvē Ţàśķś",
|
||||
"aDirectLocalExecutorProfileIsRequired": "À ďĩŕēćţ ĺōćàĺ ēxēćũţōŕ ƥŕōƒĩĺē ĩś ŕēqũĩŕēď ţō ćŕēàţē àńď ũśē àń ēḿƥţŷ ŕēƥōśĩţōŕŷ.",
|
||||
"agentDashboard": "àĝēńţ ďàśĥƀōàŕď",
|
||||
"agentRun": "àĝēńţ ŕũń",
|
||||
"agentRuns": "àĝēńţ ŕũńś",
|
||||
"agents": "Àĝēńţś",
|
||||
"agentSkillsAskTheAgentTo": "Àĝēńţ śķĩĺĺś (àśķ ţĥē àĝēńţ ţō ŕũń)",
|
||||
"allowIndexing": "Àĺĺōŵ ĩńďēxĩńĝ",
|
||||
"andMoreFiles_one": "+{{count}} ḿōŕē",
|
||||
"andMoreFiles_other": "+{{count}} ḿōŕē",
|
||||
"anEnhancedPromptIsAvailable": "Àń ēńĥàńćēď ƥŕōḿƥţ ĩś àvàĩĺàƀĺē.",
|
||||
"apply": "Àƥƥĺŷ",
|
||||
"autoApproveAllPermissions": "Àũţō-àƥƥŕōvē àĺĺ ƥēŕḿĩśśĩōńś",
|
||||
"automations": "Àũţōḿàţĩōńś",
|
||||
"availableTemplates": "Àvàĩĺàƀĺē ţēḿƥĺàţēś",
|
||||
"back": "Ɓàćķ",
|
||||
"backToTaskOverview": "Ɓàćķ ţō ţàśķ ōvēŕvĩēŵ",
|
||||
"baseBranch": "Ɓàśē Ɓŕàńćĥ",
|
||||
"bootsASecondaryKandevDevInstance": "Ɓōōţś à śēćōńďàŕŷ ķàńďēv ďēv ĩńśţàńćē ŵĩţĥ à ćĺēàń ĎƁ śō ŷōũ ćàń vēŕĩƒŷ ţĥē ćĥàńĝē ŵĩţĥōũţ ţōũćĥĩńĝ ŷōũŕ ḿàĩń ōńē.",
|
||||
"bootstrapFailed": "Ɓōōţśţŕàƥ ƒàĩĺēď",
|
||||
"bootstrapFailedCloseAndReopen": "Ɓōōţśţŕàƥ ƒàĩĺēď — ćĺōśē àńď ŕēōƥēń ţō ŕēţŕŷ.",
|
||||
"breadcrumb": "ƀŕēàďćŕũḿƀ",
|
||||
"browseParentDirectory": "Ɓŕōŵśē ƥàŕēńţ ďĩŕēćţōŕŷ",
|
||||
"bugFix": "Ɓũĝ ƒĩx",
|
||||
"cancel": "Ćàńćēĺ",
|
||||
"cancelNewFolder": "Ćàńćēĺ ńēŵ ƒōĺďēŕ",
|
||||
"changelog": "Ćĥàńĝēĺōĝ",
|
||||
"checkingGithubAuthentication": "Ćĥēćķĩńĝ ĜĩţĤũƀ àũţĥēńţĩćàţĩōń…",
|
||||
"chooseAFolderOrEnterA": "Ćĥōōśē à ƒōĺďēŕ ōŕ ēńţēŕ à ńēŵ ƥàţĥ ōń ţĥĩś ḿàćĥĩńē.",
|
||||
"cliClient": "ĆĹĨ ćĺĩēńţ",
|
||||
"cliModeYourPromptWillBe": "ĆĹĨ ḿōďē — ŷōũŕ ƥŕōḿƥţ ŵĩĺĺ ƀē àũţō-ĩńĵēćţēď ĩńţō ţĥē ţēŕḿĩńàĺ",
|
||||
"cliPassthrough": "ĆĹĨ ƥàśśţĥŕōũĝĥ",
|
||||
"close": "Ćĺōśē",
|
||||
"commandAgentsSettingsKeywords": "àĝēńţś, àĝēńţ śēţţĩńĝś, àĝēńţ ƥŕōƒĩĺēś, ĩńśţàĺĺēď àĝēńţś, ćĺàũďē",
|
||||
"commandConfigChatKeywords": "ćōńƒĩĝ ćĥàţ, ćōńƒĩĝ ḿōďē, ćōńƒĩĝũŕē ķàńďēv, ŵōŕķƒĺōŵ ćōńƒĩĝũŕàţĩōń, ḿćƥ ćōńƒĩĝũŕàţĩōń",
|
||||
"commandExecutorsSettingsKeywords": "ēxēćũţōŕś, ēxēćũţōŕ ƥŕōƒĩĺēś, ēxēćũţĩōń ēńvĩŕōńḿēńţ, ēńvĩŕōńḿēńţ vàŕĩàƀĺēś, ŕũńţĩḿē, ćōḿƥũţē",
|
||||
@@ -33,6 +53,12 @@
|
||||
"commandGroupNavigation": "Ńàvĩĝàţĩōń",
|
||||
"commandGroupPreferences": "Ƥŕēƒēŕēńćēś",
|
||||
"commandGroupSettings": "Śēţţĩńĝś",
|
||||
"commandPaletteMode": "Ćōḿḿàńď ƥàĺēţţē ḿōďē",
|
||||
"commandPreviewAddTerminalPanel": "Àďď Ţēŕḿĩńàĺ Ƥàńēĺ",
|
||||
"commandPreviewCommitChanges": "Ćōḿḿĩţ Ćĥàńĝēś",
|
||||
"commandPreviewGoToHome": "Ĝō ţō Ĥōḿē",
|
||||
"commandPreviewPull": "Ƥũĺĺ",
|
||||
"commandPreviewSearchTasks": "Śēàŕćĥ Ţàśķś",
|
||||
"commandPromptsSettingsKeywords": "ƥŕōḿƥţś, ƥŕōḿƥţ śēţţĩńĝś, ćũśţōḿ ƥŕōḿƥţś, ƥŕōḿƥţ śńĩƥƥēţś, ƥŕōḿƥţ ţēḿƥĺàţēś",
|
||||
"commandQuickChat": "Qũĩćķ Ćĥàţ",
|
||||
"commandQuickChatKeywords": "qũĩćķ ćĥàţ, ńēŵ qũĩćķ ćĥàţ, qũĩćķ qũēśţĩōń, àśķ àĝēńţ",
|
||||
@@ -40,44 +66,118 @@
|
||||
"commandSwitchToLightMode": "Śŵĩţćĥ ţō Ĺĩĝĥţ Ḿōďē",
|
||||
"commandThemeKeywords": "ţĥēḿē, ćōĺōŕ ţĥēḿē, àƥƥēàŕàńćē",
|
||||
"commandWorkspaceSettingsKeywords": "ŵōŕķśƥàćē, ŵōŕķśƥàćēś",
|
||||
"commitsAndPushesToTheCurrent": "Ćōḿḿĩţś àńď ƥũśĥēś ţō ţĥē ćũŕŕēńţ ƀŕàńćĥ.",
|
||||
"commitsPushesAndOpensAPull": "Ćōḿḿĩţś, ƥũśĥēś, àńď ōƥēńś à ƥũĺĺ ŕēqũēśţ.",
|
||||
"configurationChat": "Ćōńƒĩĝũŕàţĩōń Ćĥàţ",
|
||||
"configureAUtilityAgentToEnhance": "Ćōńƒĩĝũŕē à ũţĩĺĩţŷ àĝēńţ ĩń śēţţĩńĝś ţō ēńàƀĺē ÀĨ ēńĥàńćēḿēńţ",
|
||||
"configureExecutorsInSettingsToControl": "Ćōńƒĩĝũŕē ēxēćũţōŕś ĩń Śēţţĩńĝś ţō ćōńţŕōĺ ŵĥēŕē àĝēńţś ēxēćũţē.",
|
||||
"configureGithub": "Ćōńƒĩĝũŕē ĜĩţĤũƀ",
|
||||
"confirm": "Ćōńƒĩŕḿ",
|
||||
"contents": "Ćōńţēńţś",
|
||||
"contentSearchNeedsAnActiveTask": "Ćōńţēńţ śēàŕćĥ ńēēďś àń àćţĩvē ţàśķ śēśśĩōń.",
|
||||
"contextCompactionCountHelp": "Ķàńďēv ĩńƒēŕś ţĥĩś ćōũńţ ƒŕōḿ ōƀśēŕvēď ćōńţēxţ ũśàĝē ďŕōƥś. Ḿĩśśĩńĝ śàḿƥĺēś ōŕ ƥŕōvĩďēŕ ŕēśēţś ćàń ḿàķē ĩţ àƥƥŕōxĩḿàţē.",
|
||||
"contextCompactions": "Ćōḿƥàćţĩōńś",
|
||||
"contribute": "Ćōńţŕĩƀũţē",
|
||||
"contributingAsLogin": "Ćōńţŕĩƀũţĩńĝ àś <1>@{{login}}</1>. {{access}}",
|
||||
"contributingAsYourGithubAccount": "Ćōńţŕĩƀũţĩńĝ àś ŷōũŕ ĜĩţĤũƀ àććōũńţ. {{access}}",
|
||||
"copy": "Ćōƥŷ",
|
||||
"couldNotPrepareImproveKandev": "Ćōũĺď ńōţ ƥŕēƥàŕē Ĩḿƥŕōvē Ķàńďēv",
|
||||
"createATaskDescribingYourBug": "Ćŕēàţē à ţàśķ ďēśćŕĩƀĩńĝ ŷōũŕ ƀũĝ ōŕ ƒēàţũŕē ŕēqũēśţ",
|
||||
"createAWorkspaceToConfigureThisIntegration": "Ćŕēàţē à ŵōŕķśƥàćē ţō ćōńƒĩĝũŕē ţĥĩś ĩńţēĝŕàţĩōń.",
|
||||
"createFolder": "Ćŕēàţē ƒōĺďēŕ",
|
||||
"createNewRepository": "Ćŕēàţē ńēŵ ŕēƥōśĩţōŕŷ",
|
||||
"createRepository": "Ćŕēàţē ŕēƥōśĩţōŕŷ",
|
||||
"createsAnEmptyGitRepository": "Ćŕēàţēś àń ēḿƥţŷ Ĝĩţ ŕēƥōśĩţōŕŷ àńď ŕēĝĩśţēŕś ĩţ ĩń ţĥĩś ŵōŕķśƥàćē.",
|
||||
"creating": "Ćŕēàţĩńĝ…",
|
||||
"dAgo": "{{diffDay}}ď àĝō",
|
||||
"default": "ďēƒàũĺţ",
|
||||
"defaultBadge": "Ďēƒàũĺţ",
|
||||
"deleteSession": "Ďēĺēţē śēśśĩōń",
|
||||
"describeABugYouHitOr": "Ďēśćŕĩƀē à ƀũĝ ŷōũ ĥĩţ ōŕ à ƒēàţũŕē ŷōũ'ď ĺĩķē, àńď ŵē'ĺĺ ćŕēàţē à ţàśķ ōń ŷōũŕ ōŵń àĝēńţ ţō ĩḿƥĺēḿēńţ ĩţ ĩń ţĥē ķàńďēv ćōďēƀàśē.",
|
||||
"destination": "Ďēśţĩńàţĩōń",
|
||||
"discardAndContinue": "Ďĩśćàŕď àńď ćōńţĩńũē",
|
||||
"discardLocalChanges": "Ďĩśćàŕď ĺōćàĺ ćĥàńĝēś?",
|
||||
"discardLocalChangesAtPathDescription": "Śţàŕţĩńĝ ţĥĩś ţàśķ ŵĩĺĺ ƥēŕḿàńēńţĺŷ ďĩśćàŕď ţĥē ũńćōḿḿĩţţēď ćĥàńĝēś ĩń ŷōũŕ ĺōćàĺ ćĺōńē àţ {{repoPath}}. Ɓàćķ ũƥ àńŷţĥĩńĝ ŷōũ ŵàńţ ţō ķēēƥ ƀēƒōŕē ćōńţĩńũĩńĝ.",
|
||||
"discardLocalChangesDescription": "Śţàŕţĩńĝ ţĥĩś ţàśķ ŵĩĺĺ ƥēŕḿàńēńţĺŷ ďĩśćàŕď ţĥē ũńćōḿḿĩţţēď ćĥàńĝēś ĩń ŷōũŕ ĺōćàĺ ćĺōńē. Ɓàćķ ũƥ àńŷţĥĩńĝ ŷōũ ŵàńţ ţō ķēēƥ ƀēƒōŕē ćōńţĩńũĩńĝ.",
|
||||
"doNotShowThisAgain": "Ďō ńōţ śĥōŵ ţĥĩś àĝàĩń",
|
||||
"edit": "Ēďĩţ",
|
||||
"editTask": "Ēďĩţ ţàśķ",
|
||||
"gitScopeSkipped": "Śķĩƥƥēď {{scope}} ƀēćàũśē à ćĥĩĺď ŕēƥōśĩţōŕŷ ōƥēŕàţĩōń ƒàĩĺēď ({{failedScopes}}).",
|
||||
"emptyRepositoriesRunDirectlySwitch": "Ēḿƥţŷ ŕēƥōśĩţōŕĩēś ŕũń ďĩŕēćţĺŷ ōń ţĥĩś ḿàćĥĩńē. Ţĥĩś ţàśķ ŵĩĺĺ śŵĩţćĥ ţō “{{profile}}”.",
|
||||
"enhancePromptWithAi": "Ēńĥàńćē ƥŕōḿƥţ ŵĩţĥ ÀĨ",
|
||||
"enterAValue": "Ēńţēŕ à vàĺũē...",
|
||||
"enterValue": "Ēńţēŕ vàĺũē...",
|
||||
"executors": "Ēxēćũţōŕś",
|
||||
"externalMcp": "Ēxţēŕńàĺ ḾĆƤ",
|
||||
"failedToCreateFolder": "Ƒàĩĺēď ţō ćŕēàţē ƒōĺďēŕ",
|
||||
"failedToCreateRepository": "Ƒàĩĺēď ţō ćŕēàţē ŕēƥōśĩţōŕŷ",
|
||||
"failedToLoadDirectory": "Ƒàĩĺēď ţō ĺōàď ďĩŕēćţōŕŷ",
|
||||
"failedToLoadRoutine": "Ƒàĩĺēď ţō ĺōàď ŕōũţĩńē",
|
||||
"failedToLoadSetup": "Ƒàĩĺēď ţō ĺōàď śēţũƥ",
|
||||
"featureRequest": "Ƒēàţũŕē ŕēqũēśţ",
|
||||
"files": "Ƒĩĺēś",
|
||||
"folderName": "Ƒōĺďēŕ ńàḿē",
|
||||
"folders": "Ƒōĺďēŕś",
|
||||
"forwardStdinStdoutStraightToThe": "Ƒōŕŵàŕď śţďĩń/śţďōũţ śţŕàĩĝĥţ ţō ţĥē ĆĹĨ śũƀƥŕōćēśś. Ďĩśàƀĺēś ÀĆƤ.",
|
||||
"getStarted": "Ĝēţ Śţàŕţēď",
|
||||
"githubCliNotAuthenticated": "ĜĩţĤũƀ ĆĹĨ ńōţ àũţĥēńţĩćàţēď",
|
||||
"gitScopeSkipped": "Śķĩƥƥēď {{scope}} ƀēćàũśē à ćĥĩĺď ŕēƥōśĩţōŕŷ ōƥēŕàţĩōń ƒàĩĺēď ({{failedScopes}}).",
|
||||
"hAgo": "{{diffHr}}ĥ àĝō",
|
||||
"hideAdvancedOptions": "Ĥĩďē àďvàńćēď ōƥţĩōńś",
|
||||
"howLogContextWorks": "Ĥōŵ ĺōĝ ćōńţēxţ ŵōŕķś",
|
||||
"improveKandev": "Ĩḿƥŕōvē Ķàńďēv",
|
||||
"improveKandevBugPlaceholder": "Ē.ĝ. Ŵĥēń Ĩ ōƥēń ţĥē ķàńƀàń ƀōàŕď, ţĥē ćōĺũḿń ĥēàďēŕ ţēxţ ōvēŕĺàƥś ţĥē ţàśķ ćōũńţ ƀàďĝē ōń śćŕēēńś ńàŕŕōŵēŕ ţĥàń 1200ƥx...",
|
||||
"improveKandevFeaturePlaceholder": "Ē.ĝ. Àďď à ķēŷƀōàŕď śĥōŕţćũţ ţō ḿàŕķ à ţàśķ àś Ďōńē ƒŕōḿ ţĥē ţàśķ ďēţàĩĺ vĩēŵ, àńď śĥōŵ ţĥē śĥōŕţćũţ ĩń ţĥē ţàśķ àćţĩōń ḿēńũ...",
|
||||
"improveKandevIssuePlaceholder": "Ďēśćŕĩƀē ţĥē ƥŕōƀĺēḿ ōŕ ŕēqũēśţ ŷōũ ŵàńţ ƥũƀĺĩśĥēď. Ţĥē àĝēńţ ŵĩĺĺ àśķ ƒōŕ àńŷ ďēţàĩĺś ŕēqũĩŕēď ƀŷ ţĥē ḿàţćĥĩńĝ ĜĩţĤũƀ ĩśśũē ţēḿƥĺàţē...",
|
||||
"includeRecentBackendAndBrowserLogs": "Ĩńćĺũďē ŕēćēńţ ƀàćķēńď & ƀŕōŵśēŕ ĺōĝś àś ćōńţēxţ ƒōŕ ţĥē àĝēńţ",
|
||||
"integrations": "Ĩńţēĝŕàţĩōńś",
|
||||
"justNow": "ĵũśţ ńōŵ",
|
||||
"kandevAllowsEveryAgentPermissionRequest": "Ķàńďēv àĺĺōŵś ēvēŕŷ àĝēńţ ƥēŕḿĩśśĩōń ŕēqũēśţ ŵĩţĥōũţ ƥŕōḿƥţĩńĝ ŷōũ.",
|
||||
"kandevCouldNotLoad": "Ķàńďēv ćōũĺďń’ţ ĺōàď.",
|
||||
"kandevIsOpenSourceAndYou": "Ķàńďēv ĩś ōƥēń śōũŕćē, àńď ŷōũ ćàń ĥēĺƥ ḿàķē ĩţ ƀēţţēŕ.",
|
||||
"kandevKeepsASmallInMemory": "Ķàńďēv ķēēƥś à śḿàĺĺ ĩń-ḿēḿōŕŷ ŕĩńĝ ƀũƒƒēŕ ōƒ ţĥē ḿōśţ ŕēćēńţ ƀàćķēńď ĺōĝś àńď ƀŕōŵśēŕ ćōńśōĺē ēvēńţś. Ŵĥēń ēńàƀĺēď, ţĥōśē ĺōĝś àŕē ŵŕĩţţēń ţō à ţēḿƥōŕàŕŷ ƒōĺďēŕ ōń ŷōũŕ ḿàćĥĩńē, àńď ţĥē ƒĩĺē ƥàţĥś àŕē àƥƥēńďēď ţō ţĥē ţàśķ ďēśćŕĩƥţĩōń śō ţĥē àĝēńţ ćàń ŕēàď ţĥēḿ ŵĥĩĺē ĩńvēśţĩĝàţĩńĝ.",
|
||||
"lastRefreshFailed": "Ĺàśţ ŕēƒŕēśĥ ƒàĩĺēď: {{error}}",
|
||||
"loading": "Ĺōàďĩńĝ…",
|
||||
"loadingEllipsis": "Ĺōàďĩńĝ...",
|
||||
"loadingFolder": "Ĺōàďĩńĝ ƒōĺďēŕ…",
|
||||
"loadingIndicatorLabel": "Ĺōàďĩńĝ",
|
||||
"loadingLabel": "Ĺōàďĩńĝ {{label}}...",
|
||||
"loadingRoute": "Ĺōàďĩńĝ {{routeName}}…",
|
||||
"loadingRoutine": "Ĺōàďĩńĝ ŕōũţĩńē...",
|
||||
"loadingTask": "Ĺōàďĩńĝ ţàśķ…",
|
||||
"loadingWorkflowTemplates": "Ĺōàďĩńĝ ŵōŕķƒĺōŵ ţēḿƥĺàţēś...",
|
||||
"mAgo": "{{diffMin}}ḿ àĝō",
|
||||
"mode": "Ḿōďē",
|
||||
"model": "Ḿōďēĺ",
|
||||
"multiRepoUnavailableLocal": "Ḿũĺţĩ-ŕēƥō ţàśķś àŕē ũńàvàĩĺàƀĺē ōń Ĺōćàĺ ũńţĩĺ ĩţś ĩńĩţĩàĺ ĺàũńćĥ ƥàţĥ ćàń ƥŕōĵēćţ śĩƀĺĩńĝ ŕēƥōśĩţōŕĩēś.",
|
||||
"multiRepoUnavailableRemoteDocker": "Ḿũĺţĩ-ŕēƥō ţàśķś àŕē ũńàvàĩĺàƀĺē ōń Ŕēḿōţē Ďōćķēŕ ũńţĩĺ ĩţ śũƥƥōŕţś ćŕēàţĩńĝ ţàśķ ĩńśţàńćēś.",
|
||||
"multiRepoUnsupportedExecutor": "Ḿũĺţĩ-ŕēƥō ţàśķś àŕē ńōţ śũƥƥōŕţēď ƀŷ ţĥĩś ēxēćũţōŕ.",
|
||||
"navigate": "Ńàvĩĝàţē",
|
||||
"navigateBetweenPagesSearchTasksTrigger": "Ńàvĩĝàţē ƀēţŵēēń ƥàĝēś, śēàŕćĥ ţàśķś, ţŕĩĝĝēŕ ĝĩţ ōƥēŕàţĩōńś, àńď ḿàńàĝē ƥàńēĺś — àĺĺ ŵĩţĥōũţ ĺēàvĩńĝ ţĥē ķēŷƀōàŕď. Ćōńţēxţ-àŵàŕē ćōḿḿàńďś àƥƥēàŕ ƀàśēď ōń ţĥē àćţĩvē ƥàĝē.",
|
||||
"newFolder": "Ńēŵ ƒōĺďēŕ",
|
||||
"newFolderName": "Ńēŵ ƒōĺďēŕ ńàḿē",
|
||||
"next": "Ńēxţ",
|
||||
"noCliClientsInstalledYetInstall": "Ńō ĆĹĨ ćĺĩēńţś ĩńśţàĺĺēď ŷēţ. Ĩńśţàĺĺ Ćĺàũďē / Ćōďēx / ŌƥēńĆōďē / Àḿƥ ţō ēńàƀĺē ţĥĩś ƥĩćķēŕ.",
|
||||
"noCommandsFound": "Ńō ćōḿḿàńďś ƒōũńď.",
|
||||
"noContentMatchesFound": "Ńō ćōńţēńţ ḿàţćĥēś ƒōũńď.",
|
||||
"noFilesFound": "Ńō ƒĩĺēś ƒōũńď.",
|
||||
"noFoldersHerePickThisOne": "Ńō ƒōĺďēŕś ĥēŕē — ƥĩćķ ţĥĩś ōńē ōŕ ĝō ũƥ",
|
||||
"officeIsNotEnabledForThis": "Ōƒƒĩćē ĩś ńōţ ēńàƀĺēď ƒōŕ ţĥĩś ŕũńţĩḿē.",
|
||||
"officeRouteNotPortedYet": "Ţĥĩś Ōƒƒĩćē ŕōũţē ĩś ĥàńďĺēď ƀŷ ţĥē ŚƤÀ śĥēĺĺ, ƀũţ ĩţś ďēďĩćàţēď ćĺĩēńţ ƥàĝē ĩś śţĩĺĺ ƀēĩńĝ ƥōŕţēď: <1>{{pathname}}</1>",
|
||||
"onboardingStepAgentsDescription": "Ḿàńàĝē ďĩśćōvēŕēď àĝēńţś àńď ĩńśţàĺĺ ńēŵ ōńēś.",
|
||||
"onboardingStepAgentsTitle": "ÀĨ Àĝēńţś",
|
||||
"onboardingStepCommandPanelDescription": "Qũĩćķ àććēśś ţō àćţĩōńś ƒŕōḿ àńŷŵĥēŕē ŵĩţĥ à ķēŷƀōàŕď śĥōŕţćũţ.",
|
||||
"onboardingStepCommandPanelTitle": "Ćōḿḿàńď Ƥàńēĺ",
|
||||
"onboardingStepExecutorsDescription": "Àĝēńţś ćàń ŕũń ĩń ďĩƒƒēŕēńţ ēxēćũţōŕ ēńvĩŕōńḿēńţś — ĺōćàĺ, ćōńţàĩńēŕĩźēď, ōŕ ŕēḿōţē.",
|
||||
"onboardingStepExecutorsTitle": "Ēxēćũţōŕś",
|
||||
"onboardingStepWorkflowsDescription": "Ŵōŕķƒĺōŵś ďēƒĩńē ţĥē śţēƥś àńď àũţōḿàţĩōń ƒōŕ ŷōũŕ ţàśķś.",
|
||||
"onboardingStepWorkflowsTitle": "Àĝēńţĩć Ŵōŕķƒĺōŵś",
|
||||
"open": "Ōƥēń",
|
||||
"openFullPage": "Ōƥēń ƒũĺĺ ƥàĝē",
|
||||
"openingLastTask": "Ōƥēńĩńĝ ĺàśţ ţàśķ…",
|
||||
"openIssue": "Ōƥēń ĩśśũē",
|
||||
"optionalTheRepositoryTheAgentWorks": "Ōƥţĩōńàĺ — ţĥē ŕēƥōśĩţōŕŷ ţĥē àĝēńţ ŵōŕķś ĩń.",
|
||||
"panels": "Ƥàńēĺś",
|
||||
"parentDirectory": "Ƥàŕēńţ ďĩŕēćţōŕŷ",
|
||||
"permitTheCliToUploadCode": "Ƥēŕḿĩţ ţĥē ĆĹĨ ţō ũƥĺōàď ćōďē ƒōŕ ćĺōũď ĩńďēxĩńĝ (àũĝĝĩē / śĩḿĩĺàŕ).",
|
||||
"pickACliClient": "Ƥĩćķ à ĆĹĨ ćĺĩēńţ",
|
||||
"pickACliClientToLoad": "Ƥĩćķ à ĆĹĨ ćĺĩēńţ ţō ĺōàď ĩţś àvàĩĺàƀĺē ḿōďēĺś àńď ḿōďēś.",
|
||||
@@ -85,6 +185,8 @@
|
||||
"pluginPanelUnavailable": "Ţĥĩś ƥàńēĺ ĩś ńō ĺōńĝēŕ àvàĩĺàƀĺē.",
|
||||
"plugins": "Ƥĺũĝĩńś",
|
||||
"prDetails": "ƤŔ Ďēţàĩĺś",
|
||||
"preparingKandevRepository": "Ƥŕēƥàŕĩńĝ ķàńďēv ŕēƥōśĩţōŕŷ…",
|
||||
"preparingKandevRepositoryInBackground": "Ƥŕēƥàŕĩńĝ ķàńďēv ŕēƥōśĩţōŕŷ ĩń ƀàćķĝŕōũńď. Ƒĩĺĺ ĩń ţĥē ďēţàĩĺś, śũƀḿĩţ ŵĥēń ŕēàďŷ.",
|
||||
"presetIconBug": "Ɓũĝ",
|
||||
"presetIconCheck": "Ćĥēćķ",
|
||||
"presetIconCode": "Ćōďē",
|
||||
@@ -93,15 +195,22 @@
|
||||
"presetIconSearch": "Śēàŕćĥ",
|
||||
"presetIconSparkle": "Śƥàŕķĺē",
|
||||
"presetIconTool": "Ţōōĺ",
|
||||
"pressCmdKToOpenItAnytime": "<0>Ƥŕēśś</0><1></1><2>ţō ōƥēń ĩţ àńŷţĩḿē</2>",
|
||||
"pressEnterToConfirm": "Ƥŕēśś Ēńţēŕ ţō ćōńƒĩŕḿ",
|
||||
"profileName": "Ƥŕōƒĩĺē ńàḿē",
|
||||
"prompts": "Ƥŕōḿƥţś",
|
||||
"refreshAriaLabel": "Ŕēƒŕēśĥ {{label}}",
|
||||
"refreshBranchesGitFetch": "Ŕēƒŕēśĥ ƀŕàńćĥēś (ĝĩţ ƒēţćĥ)",
|
||||
"refreshingLabel": "Ŕēƒŕēśĥĩńĝ {{label}}...",
|
||||
"refreshLabelLastFetched": "Ŕēƒŕēśĥ {{label}} (ĺàśţ ƒēţćĥēď {{time}})",
|
||||
"reload": "Ŕēĺōàď",
|
||||
"reloadTheApplicationToTryAgain": "Ŕēĺōàď ţĥē àƥƥĺĩćàţĩōń ţō ţŕŷ àĝàĩń.",
|
||||
"requestFailed": "Ŕēqũēśţ ƒàĩĺēď",
|
||||
"rename": "Ŕēńàḿē",
|
||||
"reviewSubmoduleBoundary": "Śũƀḿōďũĺē: {{scope}}",
|
||||
"repository": "Ŕēƥōśĩţōŕŷ",
|
||||
"repositoryName": "Ŕēƥōśĩţōŕŷ ńàḿē",
|
||||
"requestFailed": "Ŕēqũēśţ ƒàĩĺēď",
|
||||
"reset": "Ŕēśēţ",
|
||||
"resizeTableColumns": "Ŕēśĩźē ţàƀĺē ćōĺũḿńś {{left}} àńď {{right}}",
|
||||
"resetting": "Ŕēśēţţĩńĝ…",
|
||||
"resetWatchChecking": "Ćĥēćķĩńĝ ĥōŵ ḿàńŷ ţàśķś ŵōũĺď ƀē ďēĺēţēď…",
|
||||
"resetWatchDeleteAllTasks": "Ţĥĩś ŵĩĺĺ ďēĺēţē ēvēŕŷ ţàśķ ƥŕēvĩōũśĺŷ ćŕēàţēď ƀŷ ţĥē ŵàţćĥ, ĩńćĺũďĩńĝ àŕćĥĩvēď ţàśķś. Ţĥē ŵàţćĥ'ś ƥōĺĺĩńĝ ćũŕśōŕ ĩś àĺśō ćĺēàŕēď śō ţĥē ńēxţ ćĥēćķ ŕē-ĩḿƥōŕţś ēvēŕŷ ćũŕŕēńţĺŷ-ḿàţćĥĩńĝ ĩţēḿ. Ţĥĩś ćàńńōţ ƀē ũńďōńē.",
|
||||
"resetWatchDeleteTasks_one": "Ţĥĩś ŵĩĺĺ ďēĺēţē {{count}} ţàśķ ƥŕēvĩōũśĺŷ ćŕēàţēď ƀŷ ţĥē ŵàţćĥ, ĩńćĺũďĩńĝ àŕćĥĩvēď ţàśķś. Ţĥē ŵàţćĥ'ś ƥōĺĺĩńĝ ćũŕśōŕ ĩś àĺśō ćĺēàŕēď śō ţĥē ńēxţ ćĥēćķ ŕē-ĩḿƥōŕţś ēvēŕŷ ćũŕŕēńţĺŷ-ḿàţćĥĩńĝ ĩţēḿ. Ţĥĩś ćàńńōţ ƀē ũńďōńē.",
|
||||
@@ -109,24 +218,70 @@
|
||||
"resetWatchNoTasks": "Ńō ţàśķś ŵēŕē ćŕēàţēď ƀŷ ţĥĩś ŵàţćĥ ŷēţ — ōńĺŷ ţĥē ƥōĺĺĩńĝ śţàţē ŵĩĺĺ ƀē ćĺēàŕēď. Ţĥē ŵàţćĥ'ś ƥōĺĺĩńĝ ćũŕśōŕ ĩś àĺśō ćĺēàŕēď śō ţĥē ńēxţ ćĥēćķ ŕē-ĩḿƥōŕţś ēvēŕŷ ćũŕŕēńţĺŷ-ḿàţćĥĩńĝ ĩţēḿ. Ţĥĩś ćàńńōţ ƀē ũńďōńē.",
|
||||
"resetWatchPreviewFailedRequired": "Ćōũĺď ńōţ ĺōàď ţĥē àƒƒēćţēď ţàśķ ćōũńţ. Ŕēţŕŷ ţĥē ƥŕēvĩēŵ ƀēƒōŕē ŕēśēţţĩńĝ ţĥĩś ŵàţćĥ.",
|
||||
"resetWatchTitle": "Ŕēśēţ {{label}}?",
|
||||
"resetting": "Ŕēśēţţĩńĝ…",
|
||||
"resizeTableColumns": "Ŕēśĩźē ţàƀĺē ćōĺũḿńś {{left}} àńď {{right}}",
|
||||
"results": "Ŕēśũĺţś",
|
||||
"retryPreview": "Ŕēţŕŷ ƥŕēvĩēŵ",
|
||||
"reviewSubmoduleBoundary": "Śũƀḿōďũĺē: {{scope}}",
|
||||
"runsFormatTypecheckTestAndLint": "Ŕũńś ƒōŕḿàţ, ţŷƥēćĥēćķ, ţēśţ, àńď ĺĩńţ àćŕōśś ţĥē ḿōńōŕēƥō.",
|
||||
"runtimeDocker": "Ďōćķēŕ",
|
||||
"runtimeDockerDescription": "Ćōńţàĩńēŕĩźēď ēxēćũţĩōń ƒōŕ ƒũĺĺ ĩśōĺàţĩōń àńď ŕēƥŕōďũćĩƀĩĺĩţŷ.",
|
||||
"runtimeGitWorktree": "Ĝĩţ Ŵōŕķţŕēē",
|
||||
"runtimeGitWorktreeDescription": "Ĩśōĺàţēď ƀŕàńćĥ ēńvĩŕōńḿēńţ ũńďēŕ à ŵōŕķţŕēē ŕōōţ ƒōŕ ƥàŕàĺĺēĺ ŵōŕķ.",
|
||||
"runtimeLocal": "Ĺōćàĺ",
|
||||
"runtimeLocalDescription": "Ŕũń àĝēńţś ďĩŕēćţĺŷ ōń ŷōũŕ ḿàćĥĩńē ŵĩţĥ ƒũĺĺ àććēśś ţō ŷōũŕ ĺōćàĺ ƒĩĺēśŷśţēḿ.",
|
||||
"runtimeSprites": "Śƥŕĩţēś (Ŕēḿōţē śƥŕĩţēś.ďēv)",
|
||||
"runtimeSpritesDescription": "Ĥàŕďŵàŕē-ĩśōĺàţēď ēxēćũţĩōń ēńvĩŕōńḿēńţ ƒōŕ àŕƀĩţŕàŕŷ ćōďē.",
|
||||
"save": "Śàvē",
|
||||
"scopeCommands": "Ćōḿḿàńďś",
|
||||
"scopeContents": "Ćōńţēńţś",
|
||||
"scopeFiles": "Ƒĩĺēś",
|
||||
"scopeTitleWithShortcut": "{{label}} ({{shortcut}})",
|
||||
"scratchWorkspace": "śćŕàţćĥ ŵōŕķśƥàćē",
|
||||
"searchFailedEditTheQueryOr": "Śēàŕćĥ ƒàĩĺēď. Ēďĩţ ţĥē qũēŕŷ ōŕ ŕēōƥēń śēàŕćĥ ţō ŕēţŕŷ.",
|
||||
"searchForFiles": "Śēàŕćĥ ƒōŕ ƒĩĺēś...",
|
||||
"searchForTasks": "Śēàŕćĥ ƒōŕ ţàśķś...",
|
||||
"searchingTaskWorkspace": "Śēàŕćĥĩńĝ ţàśķ ŵōŕķśƥàćē…",
|
||||
"searchQueriesAreLimitedTo200": "Śēàŕćĥ qũēŕĩēś àŕē ĺĩḿĩţēď ţō 200 ćĥàŕàćţēŕś.",
|
||||
"searchTaskContents": "Śēàŕćĥ ţàśķ ćōńţēńţś…",
|
||||
"select": "Śēĺēćţ",
|
||||
"settings": "Śēţţĩńĝś",
|
||||
"shell": "Śĥēĺĺ",
|
||||
"shellCommandPlusSlashCommandSkills": "Śĥēĺĺ ćōḿḿàńď ŷōũ ćàń ŕũń ĩń ţĥē śēćōńďàŕŷ ĩńśţàńćē, ƥĺũś śĺàśĥ-ćōḿḿàńď <2>śķĩĺĺś</2> ŷōũ ćàń àśķ ţĥē àĝēńţ ţō ŕũń ďũŕĩńĝ ţĥē ŵōŕķƒĺōŵ.",
|
||||
"showAdvancedOptions": "Śĥōŵ àďvàńćēď ōƥţĩōńś",
|
||||
"skip": "Śķĩƥ",
|
||||
"stagesAndCommitsChangesUsingConventional": "Śţàĝēś àńď ćōḿḿĩţś ćĥàńĝēś ũśĩńĝ Ćōńvēńţĩōńàĺ Ćōḿḿĩţś.",
|
||||
"status": "Śţàţũś",
|
||||
"subtaskTitle": "Śũƀţàśķ ţĩţĺē",
|
||||
"switchMode": "Śŵĩţćĥ ḿōďē",
|
||||
"system": "Śŷśţēḿ",
|
||||
"taskLoading": "Ĺōàďĩńĝ ţàśķ...",
|
||||
"tasks": "Ţàśķś",
|
||||
"taskUnavailable": "Ţàśķ ũńàvàĩĺàƀĺē",
|
||||
"taskUnavailableDescription": "Ŵē ćōũĺď ńōţ ĺōàď ţĥĩś ţàśķ. Ĩţ ḿàŷ ĥàvē ƀēēń ďēĺēţēď ōŕ ŷōũ ḿàŷ ńōţ ĥàvē àććēśś.",
|
||||
"theAgentForksKandevToYourAccount": "Ţĥē àĝēńţ ƒōŕķś <1>{{repo}}</1> ţō ŷōũŕ ĜĩţĤũƀ àććōũńţ àńď ōƥēńś à ƤŔ ƒŕōḿ ŷōũŕ ƒōŕķ, ćŕēďĩţēď ţō ŷōũ",
|
||||
"theBaseBranchTheAgentStarts": "Ţĥē ƀàśē ƀŕàńćĥ ţĥē àĝēńţ śţàŕţś ƒŕōḿ.",
|
||||
"theFinalStepOpensAPullRequest": "Ţĥē ƒĩńàĺ śţēƥ ōƒ ţĥĩś ŵōŕķƒĺōŵ ōƥēńś à ƥũĺĺ ŕēqũēśţ, ŵĥĩćĥ ńēēďś ţĥē <1>{{binary}}</1> ĆĹĨ ţō ƀē àũţĥēńţĩćàţēď. {{message}}",
|
||||
"thisEmptyRepositoryWillRunWith": "Ţĥĩś ēḿƥţŷ ŕēƥōśĩţōŕŷ ŵĩĺĺ ŕũń ŵĩţĥ “{{profile}}” ōń ţĥĩś ḿàćĥĩńē.",
|
||||
"thisGithubAccountCannotForkKdlbs": "Ţĥĩś ĜĩţĤũƀ àććōũńţ ćàńńōţ ƒōŕķ ķďĺƀś/ķàńďēv.",
|
||||
"thisPageCouldNotLoad": "Ţĥĩś ƥàĝē ćōũĺďń’ţ ĺōàď.",
|
||||
"thisWorkflowWillOnlyCreateA": "Ţĥĩś ŵōŕķƒĺōŵ ŵĩĺĺ ōńĺŷ ćŕēàţē à ĜĩţĤũƀ ĩśśũē ƒŕōḿ ţĥē ŕēƥōśĩţōŕŷ ţēḿƥĺàţē. Ţĥē àĝēńţ ŵĩĺĺ àśķ ƒōŕ ḿĩśśĩńĝ ďēţàĩĺś àńď ƥũƀĺĩćàţĩōń àƥƥŕōvàĺ; ĩţ ŵĩĺĺ ńōţ ĩḿƥĺēḿēńţ ţĥē ćĥàńĝē ōŕ ōƥēń à ƥũĺĺ ŕēqũēśţ.",
|
||||
"title": "Ţĩţĺē",
|
||||
"toastRegionLabel": "Ńōţĩƒĩćàţĩōńś",
|
||||
"typeACommand": "Ţŷƥē à ćōḿḿàńď...",
|
||||
"typeToSearchFiles": "Ţŷƥē ţō śēàŕćĥ ƒĩĺēś...",
|
||||
"typeToSearchTaskContents": "Ţŷƥē ţō śēàŕćĥ ţàśķ ćōńţēńţś…",
|
||||
"unknownPanel": "Ũńķńōŵń ƥàńēĺ: {{component}}",
|
||||
"unsavedChanges": "Ũńśàvēď ćĥàńĝēś",
|
||||
"usefulCommandsAgentSkills": "Ũśēƒũĺ ćōḿḿàńďś & àĝēńţ śķĩĺĺś",
|
||||
"useScratchInstead": "Ũśē śćŕàţćĥ ĩńśţēàď",
|
||||
"useThisFolder": "Ũśē ţĥĩś ƒōĺďēŕ",
|
||||
"waitsForCiAndAutomatedReviews": "Ŵàĩţś ƒōŕ ĆĨ àńď àũţōḿàţēď ŕēvĩēŵś, ţĥēń ƒĩxēś ƒàĩĺũŕēś àńď àďďŕēśśēś ćōḿḿēńţś.",
|
||||
"whenItsDoneTheAgentOpensAPr": "Ŵĥēń ĩţ'ś ďōńē, ţĥē àĝēńţ ōƥēńś à ƥũĺĺ ŕēqũēśţ ţō <2>{{repo}}</2> ƒōŕ ţĥē ḿàĩńţàĩńēŕś ţō ŕēvĩēŵ, śàvĩńĝ ţĥēḿ ţĩḿē àńď śĥĩƥƥĩńĝ ţĥē ĩḿƥŕōvēḿēńţ ţō ēvēŕŷōńē.",
|
||||
"workflow": "Ŵōŕķƒĺōŵ",
|
||||
"workflowsControlTheStepsAutomationAnd": "Ŵōŕķƒĺōŵś ćōńţŕōĺ ţĥē śţēƥś, àũţōḿàţĩōń, àńď àĝēńţ ƀēĥàvĩōŕ ƒōŕ ŷōũŕ ţàśķś. Ŷōũ ćàń àďď ḿōŕē ŵōŕķƒĺōŵś ƒŕōḿ Śēţţĩńĝś.",
|
||||
"workspace": "Ŵōŕķśƥàćē",
|
||||
"workspaces": "Ŵōŕķśƥàćēś",
|
||||
"youCanUseTheNavigationOr": "Ŷōũ ćàń ũśē ţĥē ńàvĩĝàţĩōń ōŕ ŕēĺōàď ţō ţŕŷ àĝàĩń."
|
||||
"youCanUseTheNavigationOr": "Ŷōũ ćàń ũśē ţĥē ńàvĩĝàţĩōń ōŕ ŕēĺōàď ţō ţŕŷ àĝàĩń.",
|
||||
"yourAgentImplementsItInThe": "Ŷōũŕ àĝēńţ ĩḿƥĺēḿēńţś ĩţ ĩń ţĥē ķàńďēv ŕēƥōśĩţōŕŷ, ŵĩţĥ ţēśţś",
|
||||
"youVerifyAndTestTheChange": "Ŷōũ vēŕĩƒŷ àńď ţēśţ ţĥē ćĥàńĝē ĩń à śēćōńď ķàńďēv ĩńśţàńćē"
|
||||
}
|
||||
|
||||
@@ -2,31 +2,38 @@
|
||||
"addDetailsAboutThisChange": "Àďď ďēţàĩĺś àƀōũţ ţĥĩś ćĥàńĝē...",
|
||||
"allRepos": "Àĺĺ ŕēƥōś",
|
||||
"anErrorOccurred": "Àń ēŕŕōŕ ōććũŕŕēď",
|
||||
"authRequiredHeading": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď",
|
||||
"authRequiredInline": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď.",
|
||||
"authenticated": "Àũţĥēńţĩćàţēď",
|
||||
"authenticationFailed": "Àũţĥēńţĩćàţĩōń ƒàĩĺēď: {{error}}",
|
||||
"authRequiredHeading": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď",
|
||||
"authRequiredInline": "{{name}} àũţĥēńţĩćàţĩōń ŕēqũĩŕēď.",
|
||||
"branchPushedNotCreated": "Ɓŕàńćĥ ƥũśĥēď; {{shortName}} ńōţ ćŕēàţēď",
|
||||
"branchWasPushedRetryCreation": "Ɓŕàńćĥ ŵàś ƥũśĥēď; ŕēţŕŷ {{longName}} ćŕēàţĩōń.",
|
||||
"checkedRelative": "· ćĥēćķēď {{relative}}",
|
||||
"commit": "Ćōḿḿĩţ",
|
||||
"commitChangedFiles_one": "Ćōḿḿĩţ {{count}} ćĥàńĝēď ƒĩĺē",
|
||||
"commitChangedFiles_other": "Ćōḿḿĩţ {{count}} ćĥàńĝēď ƒĩĺēś",
|
||||
"commitChanges": "Ćōḿḿĩţ Ćĥàńĝēś",
|
||||
"commitChangesScoped": "Ćōḿḿĩţ Ćĥàńĝēś — {{scopedRepo}}",
|
||||
"commitsAheadAriaLabel": "{{value}} ćōḿḿĩţś àĥēàď",
|
||||
"commitsBehindAriaLabel": "{{value}} ćōḿḿĩţś ƀēĥĩńď",
|
||||
"committingEllipsis": "Ćōḿḿĩţţĩńĝ...",
|
||||
"configureAUtilityAgentInSettings": "Ćōńƒĩĝũŕē à ũţĩĺĩţŷ àĝēńţ ĩń śēţţĩńĝś ţō ēńàƀĺē ÀĨ ĝēńēŕàţĩōń",
|
||||
"copiedConfigTo": "Ćōƥĩēď {{label}} ćōńƒĩĝ ţō {{targetName}}.",
|
||||
"copyConfig": "Ćōƥŷ ćōńƒĩĝ",
|
||||
"copyConfigSettingsOnly": "Ćōƥŷ ţĥē {{label}} śēţţĩńĝś ƒŕōḿ <4>{{sourceName}}</4> ĩńţō àńōţĥēŕ ŵōŕķśƥàćē. Ţĥĩś ōvēŕŵŕĩţēś ţĥē ţàŕĝēţ ŵōŕķśƥàćē'ś ćũŕŕēńţ {{label}} ćōńƒĩĝũŕàţĩōń. Ŵàţćĥēŕś àńď àũţōḿàţĩōńś àŕē ńōţ ćōƥĩēď.",
|
||||
"copyConfigToAnotherWorkspace": "Ćōƥŷ ćōńƒĩĝ ţō àńōţĥēŕ ŵōŕķśƥàćē",
|
||||
"copyConfigWithCredentials": "Ćōƥŷ ţĥē {{label}} śēţţĩńĝś àńď ćŕēďēńţĩàĺś ƒŕōḿ <4>{{sourceName}}</4> ĩńţō àńōţĥēŕ ŵōŕķśƥàćē. Ţĥĩś ōvēŕŵŕĩţēś ţĥē ţàŕĝēţ ŵōŕķśƥàćē'ś ćũŕŕēńţ {{label}} ćōńƒĩĝũŕàţĩōń. Ŵàţćĥēŕś àńď àũţōḿàţĩōńś àŕē ńōţ ćōƥĩēď.",
|
||||
"copyConfiguration": "Ćōƥŷ {{label}} ćōńƒĩĝũŕàţĩōń",
|
||||
"copyConfigWithCredentials": "Ćōƥŷ ţĥē {{label}} śēţţĩńĝś àńď ćŕēďēńţĩàĺś ƒŕōḿ <4>{{sourceName}}</4> ĩńţō àńōţĥēŕ ŵōŕķśƥàćē. Ţĥĩś ōvēŕŵŕĩţēś ţĥē ţàŕĝēţ ŵōŕķśƥàćē'ś ćũŕŕēńţ {{label}} ćōńƒĩĝũŕàţĩōń. Ŵàţćĥēŕś àńď àũţōḿàţĩōńś àŕē ńōţ ćōƥĩēď.",
|
||||
"copyingConfigTo": "Ćōƥŷĩńĝ {{label}} ćōńƒĩĝ ţō {{targetName}}…",
|
||||
"createAsDraft": "Ćŕēàţē àś ďŕàƒţ",
|
||||
"createdSuccessfully": "{{longName}} ćŕēàţēď śũććēśśƒũĺĺŷ",
|
||||
"createFailed": "Ćŕēàţē {{shortName}} ƒàĩĺēď",
|
||||
"createLongName": "Ćŕēàţē {{longName}}",
|
||||
"createPr": "Ćŕēàţē ƤŔ",
|
||||
"createPrCommitsAhead_one": "Ćŕēàţē ƤŔ ({{count}} ćōḿḿĩţ àĥēàď)",
|
||||
"createPrCommitsAhead_other": "Ćŕēàţē ƤŔ ({{count}} ćōḿḿĩţś àĥēàď)",
|
||||
"createScoped": "Ćŕēàţē {{longName}} — {{scopedRepo}}",
|
||||
"createShortName": "Ćŕēàţē {{shortName}}",
|
||||
"createdSuccessfully": "{{longName}} ćŕēàţēď śũććēśśƒũĺĺŷ",
|
||||
"creatingEllipsis": "Ćŕēàţĩńĝ...",
|
||||
"creatingFrom": "Ćŕēàţĩńĝ {{shortName}} ƒŕōḿ <4>{{displayBranch}}</4>",
|
||||
"creatingFromInto": "Ćŕēàţĩńĝ {{shortName}} ƒŕōḿ <4>{{displayBranch}}</4> → <6>{{baseBranch}}</6>",
|
||||
@@ -40,6 +47,8 @@
|
||||
"failedToCopyConfig": "Ƒàĩĺēď ţō ćōƥŷ ćōńƒĩĝ: {{err}}",
|
||||
"filesChanged_one": "<0>{{count}}</0> ƒĩĺē ćĥàńĝēď",
|
||||
"filesChanged_other": "<0>{{count}}</0> ƒĩĺēś ćĥàńĝēď",
|
||||
"forcePush": "Ƒōŕćē Ƥũśĥ",
|
||||
"fromBranch": "ƒŕōḿ {{branch}}",
|
||||
"generateCommitDescriptionWithAi": "Ĝēńēŕàţē ćōḿḿĩţ ďēśćŕĩƥţĩōń ŵĩţĥ ÀĨ",
|
||||
"generateCommitMessageWithAi": "Ĝēńēŕàţē ćōḿḿĩţ ḿēśśàĝē ŵĩţĥ ÀĨ",
|
||||
"generateDescriptionWithAi": "Ĝēńēŕàţē {{shortName}} ďēśćŕĩƥţĩōń ŵĩţĥ ÀĨ",
|
||||
@@ -47,8 +56,22 @@
|
||||
"loading": "Ĺōàďĩńĝ…",
|
||||
"longNameTitle": "{{longName}} ţĩţĺē",
|
||||
"longNameTitlePlaceholder": "{{longName}} ţĩţĺē...",
|
||||
"merge": "Ḿēŕĝē",
|
||||
"noChangesToCommit": "Ńō ćĥàńĝēś ţō ćōḿḿĩţ",
|
||||
"noSavedQueriesYet": "Ńō śàvēď qũēŕĩēś ŷēţ",
|
||||
"ontoBranch": "ōńţō {{branch}}",
|
||||
"openVcsOptions": "Ōƥēń VĆŚ ōƥţĩōńś",
|
||||
"pickActionThenRepo": "Ƥĩćķ àćţĩōń, ţĥēń ŕēƥō",
|
||||
"pickARepositoryForCommit": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ćōḿḿĩţ",
|
||||
"pickARepositoryForCreatePr": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ćŕēàţē ƤŔ",
|
||||
"pickARepositoryForPush": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ƥũśĥ",
|
||||
"pickARepositoryForRebase": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒōŕ Ŕēƀàśē",
|
||||
"pull": "Ƥũĺĺ",
|
||||
"push": "Ƥũśĥ",
|
||||
"pushCommitsToRemote_one": "Ƥũśĥ {{count}} ćōḿḿĩţ ţō ŕēḿōţē",
|
||||
"pushCommitsToRemote_other": "Ƥũśĥ {{count}} ćōḿḿĩţś ţō ŕēḿōţē",
|
||||
"rebase": "Ŕēƀàśē",
|
||||
"rebaseOntoBranchBehind": "Ŕēƀàśē ōńţō {{branch}} ({{behind}} ƀēĥĩńď)",
|
||||
"reconnectProvider": "Ŕēćōńńēćţ {{name}}",
|
||||
"repository": "Ŕēƥōśĩţōŕŷ",
|
||||
"retryShortName": "Ŕēţŕŷ {{shortName}}",
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"wipLimit": "ŴĨƤ ĺĩḿĩţ",
|
||||
"wipLimitHelp": "Ḿàxĩḿũḿ àďḿĩţţēď (àćţĩvē) ţàśķś ĩń ţĥĩś śţēƥ. Ōvēŕƒĺōŵ śţàŷś vĩśĩƀĺē ĩń à qũēũē. Ũśē 0 ƒōŕ ũńĺĩḿĩţēď.",
|
||||
"wipLimitsNote": "ŴĨƤ ĺĩḿĩţś àćţĩvē ŵōŕķ, ńōţ vĩśĩƀĩĺĩţŷ. Ōvēŕƒĺōŵ ŕēḿàĩńś ōń ţĥē ƀōàŕď ũńţĩĺ ćàƥàćĩţŷ ōƥēńś; ĩƒ à śēĺēćţēď ƒēēďēŕ ĩś àĺśō ƒũĺĺ, ńēŵ ţàśķ ćŕēàţĩōń ĩś ŕēĵēćţēď.",
|
||||
"workflow": "Ŵōŕķƒĺōŵ",
|
||||
"workflowCycleBlocked": "Ŵōŕķƒĺōŵ ćŷćĺē ƀĺōćķēď",
|
||||
"workflowCycleBlockedDescription": "Ćĥàńĝē ţĥē ŵōŕķƒĺōŵ śţēƥś ţō ŕēḿōvē ţĥē àũţōḿàţĩć ćŷćĺē ƀēƒōŕē ćōńţĩńũĩńĝ.",
|
||||
"workflowCycleBlockingBody": "Ţĥĩś ƥàţĥ ŕē-ēńţēŕś \"{{stepName}}\" àńď ćàń śţàŕţ ţĥē àĝēńţ àĝàĩń ŵĩţĥōũţ àńōţĥēŕ ũśēŕ àćţĩōń.",
|
||||
|
||||
Reference in New Issue
Block a user