fix(i18n): close the lint-invisible gaps the guard structurally cannot see (#2470)
Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
@@ -7,6 +7,8 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { getTask } from "@/lib/api/domains/office-extended-api";
|
||||
import type { OfficeTask } from "@/lib/state/slices/office/types";
|
||||
import { StatusIcon } from "@/app/office/tasks/status-icon";
|
||||
import { normalizeTaskStatus } from "@/app/office/tasks/normalize-status";
|
||||
import { PRIORITY_LABEL_KEYS, STATUS_LABEL_KEYS } from "@/app/office/lib/label-keys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type TasksTouchedProps = {
|
||||
@@ -136,10 +138,16 @@ function TaskTableRow({ row }: { row: RowState }) {
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center gap-1.5 text-xs">
|
||||
<StatusIcon status={task.status} className="h-3.5 w-3.5" />
|
||||
{formatStatus(task.status)}
|
||||
{/* `status.replace(/_/g, " ")` used to render the wire identifier as
|
||||
pseudo-English ("in progress"), which no catalog can fix.
|
||||
`STATUS_LABEL_KEYS` is the same map the board, filters and status
|
||||
icon already resolve through. */}
|
||||
{t(STATUS_LABEL_KEYS[normalizeTaskStatus(task.status)])}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs capitalize">{task.priority || "—"}</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{task.priority ? t(PRIORITY_LABEL_KEYS[task.priority] ?? "office:none") : "—"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -148,10 +156,6 @@ function shortId(id: string): string {
|
||||
return id.length > 8 ? id.slice(0, 8) : id;
|
||||
}
|
||||
|
||||
function formatStatus(status: string): string {
|
||||
return status.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads each task row in parallel via the per-task API. Kept inline in
|
||||
* this file because it's only used here; the office store is task-tree
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
import { BUDGET_ACTION_LABEL_KEYS, BUDGET_PERIOD_LABEL_KEYS } from "./label-keys";
|
||||
|
||||
/**
|
||||
* Both maps back a `?? raw wire value` fallback at the call site rather than a
|
||||
* `?? someOtherKey` one. An earlier version defaulted an unmapped
|
||||
* `actionOnExceed` to "notify only", which reports the mildest possible
|
||||
* behaviour for an action the server may treat as blocking — a wrong label is
|
||||
* worse here than an unfamiliar identifier.
|
||||
*/
|
||||
describe("budget label maps", () => {
|
||||
it("covers every wire value the BudgetPolicy type allows", () => {
|
||||
expect(Object.keys(BUDGET_ACTION_LABEL_KEYS).sort()).toEqual([
|
||||
"block_new_tasks",
|
||||
"notify_only",
|
||||
"pause_agent",
|
||||
]);
|
||||
expect(Object.keys(BUDGET_PERIOD_LABEL_KEYS).sort()).toEqual(["monthly", "total"]);
|
||||
});
|
||||
|
||||
it("resolves every mapped key to real copy", () => {
|
||||
for (const key of [
|
||||
...Object.values(BUDGET_ACTION_LABEL_KEYS),
|
||||
...Object.values(BUDGET_PERIOD_LABEL_KEYS),
|
||||
]) {
|
||||
const value = t(key);
|
||||
expect(value).not.toBe(key);
|
||||
expect(value).not.toContain(":");
|
||||
}
|
||||
});
|
||||
|
||||
it("has no entry for an unknown action, so the caller falls back to the raw value", () => {
|
||||
expect(BUDGET_ACTION_LABEL_KEYS["freeze_workspace"]).toBeUndefined();
|
||||
expect(BUDGET_PERIOD_LABEL_KEYS["weekly"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -68,3 +68,20 @@ export const CONCURRENCY_POLICY_LABEL_KEYS: Record<string, string> = {
|
||||
coalesce_if_active: "office:coalesceIfActive",
|
||||
always_create: "office:alwaysCreate",
|
||||
};
|
||||
|
||||
/**
|
||||
* Budget overrun actions. The costs card rendered the wire value with
|
||||
* `actionOnExceed.replace(/_/g, " ")`, which is pseudo-English: a translator
|
||||
* with a complete catalog still saw "block new tasks" spelled out of an
|
||||
* identifier.
|
||||
*/
|
||||
export const BUDGET_PERIOD_LABEL_KEYS: Record<string, string> = {
|
||||
monthly: "office:budgetPeriodMonthly",
|
||||
total: "office:budgetPeriodTotal",
|
||||
};
|
||||
|
||||
export const BUDGET_ACTION_LABEL_KEYS: Record<string, string> = {
|
||||
notify_only: "office:budgetActionNotifyOnly",
|
||||
pause_agent: "office:budgetActionPauseAgent",
|
||||
block_new_tasks: "office:budgetActionBlockNewTasks",
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@kandev/ui/button";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import type { BudgetPolicy } from "@/lib/state/slices/office/types";
|
||||
import { BUDGET_ACTION_LABEL_KEYS, BUDGET_PERIOD_LABEL_KEYS } from "@/app/office/lib/label-keys";
|
||||
import { cn, formatDollars } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// Module-level `t`, resolved at call time: `getBudgetStatus` is a plain helper
|
||||
@@ -95,12 +96,24 @@ export function BudgetPolicyCard({ policy, spentSubcents = 0, onDelete }: Props)
|
||||
<span>{t("office:remainingAmount", { amount: formatDollars(remaining) })}</span>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs text-muted-foreground mt-1">
|
||||
{/* `period` and `actionOnExceed` are wire enums; only the field
|
||||
labels are copy, so the values are interpolated as-is. */}
|
||||
<span>{t("office:periodValue", { period: policy.period })}</span>
|
||||
{/* Both enums resolve through the shared label maps. An UNMAPPED
|
||||
value falls back to the raw wire value rather than to a mapped
|
||||
label: showing "notify only" for an action the server does not
|
||||
actually treat that way is worse than showing its identifier. */}
|
||||
<span>
|
||||
{t("office:periodValue", {
|
||||
period: BUDGET_PERIOD_LABEL_KEYS[policy.period]
|
||||
? t(BUDGET_PERIOD_LABEL_KEYS[policy.period])
|
||||
: policy.period,
|
||||
})}
|
||||
</span>
|
||||
<span>{t("office:alertPercent", { percent: policy.alertThresholdPct })}</span>
|
||||
<span>
|
||||
{t("office:actionValue", { action: policy.actionOnExceed.replace(/_/g, " ") })}
|
||||
{t("office:actionValue", {
|
||||
action: BUDGET_ACTION_LABEL_KEYS[policy.actionOnExceed]
|
||||
? t(BUDGET_ACTION_LABEL_KEYS[policy.actionOnExceed])
|
||||
: policy.actionOnExceed,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "@/components/routing/app-link";
|
||||
import { getExecutorLabel } from "@/lib/executor-icons";
|
||||
import { AgentStatusDot } from "../../agents/components/agent-status-dot";
|
||||
import type { OrgTreeNode } from "./org-tree-layout";
|
||||
import { CARD_W } from "./org-tree-layout";
|
||||
@@ -66,7 +67,7 @@ export function OrgNodeCard({ node }: OrgNodeCardProps) {
|
||||
)}
|
||||
{agent.executorPreference?.type && (
|
||||
<p className="text-[10px] uppercase tracking-wide text-muted-foreground/70 truncate mt-1">
|
||||
{agent.executorPreference.type.replace(/_/g, " ")}
|
||||
{getExecutorLabel(agent.executorPreference.type)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,10 @@ import { MobileSearchBar } from "@/components/kanban/mobile-search-bar";
|
||||
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
||||
import type { Task, Workspace, Workflow, Repository } from "@/lib/types/http";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
// Module-level `t`: every use below is inside a callback or a plain helper
|
||||
// (`errorDescription`), so it resolves at invocation rather than at import —
|
||||
// the case `apps/web/CLAUDE.md` sanctions. None of it renders as JSX.
|
||||
import { t } from "@/lib/i18n";
|
||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||
import { useKanbanDisplaySettings } from "@/hooks/use-kanban-display-settings";
|
||||
import { useDebounce } from "@/hooks/use-debounce";
|
||||
@@ -131,7 +135,7 @@ function useTaskOperations({
|
||||
if (!shouldCommit()) return;
|
||||
if (!silent) {
|
||||
toast({
|
||||
title: "Failed to load tasks",
|
||||
title: t("tasks:failedToLoadTasks"),
|
||||
description: errorDescription(err),
|
||||
variant: "error",
|
||||
});
|
||||
@@ -164,7 +168,7 @@ function useTaskOperations({
|
||||
}
|
||||
|
||||
function errorDescription(err: unknown): string {
|
||||
return err instanceof Error ? err.message : "Unknown error";
|
||||
return err instanceof Error ? err.message : t("common:unknownError");
|
||||
}
|
||||
|
||||
function useTaskMutations(fetchTasks: () => void) {
|
||||
@@ -175,11 +179,14 @@ function useTaskMutations(fetchTasks: () => void) {
|
||||
async (taskId: string, opts?: { cascade?: boolean }) => {
|
||||
try {
|
||||
await archiveTask(taskId, opts);
|
||||
toast({ title: "Task archived", description: "The task has been archived successfully." });
|
||||
toast({
|
||||
title: t("tasks:taskArchived"),
|
||||
description: t("tasks:taskArchivedDescription"),
|
||||
});
|
||||
fetchTasks();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to archive task",
|
||||
title: t("tasks:failedToArchiveTask"),
|
||||
description: errorDescription(err),
|
||||
variant: "error",
|
||||
});
|
||||
@@ -196,7 +203,7 @@ function useTaskMutations(fetchTasks: () => void) {
|
||||
fetchTasks();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to unarchive task",
|
||||
title: t("tasks:failedToUnarchiveTask"),
|
||||
description: errorDescription(err),
|
||||
variant: "error",
|
||||
});
|
||||
@@ -213,7 +220,7 @@ function useTaskMutations(fetchTasks: () => void) {
|
||||
fetchTasks();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to delete task",
|
||||
title: t("tasks:failedToDeleteTask"),
|
||||
description: errorDescription(err),
|
||||
variant: "error",
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MAX_SIDEBAR_VIEWS } from "@/lib/state/slices/ui/sidebar-view-builtins";
|
||||
import { SidebarFilterPopover } from "@/components/task/sidebar-filter/sidebar-filter-popover";
|
||||
import { useSidebarViewPopover } from "@/components/task/sidebar-filter/use-sidebar-view-popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { sidebarViewName } from "@/lib/state/slices/ui/sidebar-view-builtins";
|
||||
|
||||
const TRIGGER_BUTTON_CLASS = cn(
|
||||
"flex h-5 items-center justify-center rounded-sm px-1.5 cursor-pointer",
|
||||
@@ -42,7 +43,7 @@ export function TasksViewPicker() {
|
||||
[views, activeViewId],
|
||||
);
|
||||
const hasDraft = !!draft && draft.baseViewId === activeViewId;
|
||||
const activeLabel = activeView?.name ?? t("sidebar:viewAll");
|
||||
const activeLabel = activeView ? sidebarViewName(activeView, t) : t("sidebar:viewAll");
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
@@ -81,7 +82,7 @@ export function TasksViewPicker() {
|
||||
className="cursor-pointer gap-2 text-xs"
|
||||
>
|
||||
<IconCheck className={cn("h-3.5 w-3.5", isActive ? "opacity-100" : "opacity-0")} />
|
||||
<span className="truncate">{view.name}</span>
|
||||
<span className="truncate">{sidebarViewName(view, t)}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "@/lib/toast/sonner";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||
import { createQueuedUserSettingsSync } from "@/lib/user-settings-sync";
|
||||
import type { AppStatusBarOrderState } from "@/lib/state/slices/settings/types";
|
||||
@@ -49,7 +50,7 @@ export function useAppStatusBarOrder<T extends AppStatusItemDescriptor>(activeIt
|
||||
.catch(() => {
|
||||
if (latestRevision.current !== revision) return;
|
||||
setOptimisticOrder(null);
|
||||
toast.error("Could not save status bar order");
|
||||
toast.error(t("task:couldNotSaveStatusBarOrder"));
|
||||
});
|
||||
},
|
||||
[activeItems, order, store],
|
||||
|
||||
@@ -141,7 +141,7 @@ export function AzureDevOpsTaskLauncher({
|
||||
const onSuccess = async (task: Task) => {
|
||||
if (payload?.kind === "work-item" && workspaceId) {
|
||||
if (!payload.item.project) {
|
||||
toast.error("Failed to link Azure DevOps work item: project is missing.");
|
||||
toast.error(t("azuredevops:failedToLinkWorkItemNoProject"));
|
||||
} else {
|
||||
try {
|
||||
const linked = await associateAzureDevOpsWorkItem(workspaceId, task.id, {
|
||||
@@ -152,7 +152,7 @@ export function AzureDevOpsTaskLauncher({
|
||||
setTaskWorkItem(task.id, linked);
|
||||
} catch (error: unknown) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to link Azure DevOps work item.",
|
||||
error instanceof Error ? error.message : t("azuredevops:failedToLinkWorkItem"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ export function AzureDevOpsTaskLauncher({
|
||||
setTaskPullRequest(task.id, linked);
|
||||
} catch (error: unknown) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to link Azure DevOps pull request.",
|
||||
error instanceof Error ? error.message : t("azuredevops:failedToLinkPullRequest"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { requestPRReviewers } from "@/lib/api/domains/github-review-api";
|
||||
import type { PRReview, RequestedReviewer, TaskPR } from "@/lib/types/github";
|
||||
|
||||
@@ -208,8 +209,8 @@ export function usePRScopedReviewRequest(
|
||||
async (reviewer: string) => {
|
||||
if (!workspaceId || !identity) {
|
||||
toast({
|
||||
title: "Failed to re-request review",
|
||||
description: "Select a workspace before requesting review.",
|
||||
title: t("github:failedToReRequestReview"),
|
||||
description: t("github:selectWorkspaceBeforeRequestingReview"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
@@ -228,14 +229,17 @@ export function usePRScopedReviewRequest(
|
||||
} catch (error) {
|
||||
finishRequest(identity, reviewer, operationId, false);
|
||||
toast({
|
||||
title: "Failed to re-request review",
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
title: t("github:failedToReRequestReview"),
|
||||
description: error instanceof Error ? error.message : t("common:anErrorOccurred"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
finishRequest(identity, reviewer, operationId, true);
|
||||
toast({ description: `Review re-requested from ${reviewer}`, variant: "success" });
|
||||
toast({
|
||||
description: t("github:reviewReRequestedFrom", { reviewer }),
|
||||
variant: "success",
|
||||
});
|
||||
try {
|
||||
refresh();
|
||||
} catch {
|
||||
|
||||
@@ -4,11 +4,24 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
routerPush: vi.fn(),
|
||||
onViewModeChange: vi.fn(),
|
||||
commands: [] as Array<{ id: string; action?: () => void }>,
|
||||
commands: [] as Array<{
|
||||
id: string;
|
||||
label?: string;
|
||||
group?: string;
|
||||
keywords?: string[];
|
||||
action?: () => void;
|
||||
}>,
|
||||
activeWorkspaceId: "workspace-1" as string | null,
|
||||
activeWorkflowId: "workflow-1" as string | null,
|
||||
}));
|
||||
|
||||
// `t` echoes the key it is given, so copy that never reached the catalog comes
|
||||
// back as plain English and the copy test below names it. Asserting the English
|
||||
// itself cannot distinguish a catalog hit from an inlined literal.
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => `«${key}»` }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/state-provider", () => ({
|
||||
useAppStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
@@ -72,4 +85,25 @@ describe("HomepageCommands", () => {
|
||||
commandAction("view-list")();
|
||||
expect(mocks.routerPush).toHaveBeenCalledWith("/tasks?workspace=workspace-1");
|
||||
});
|
||||
|
||||
it("resolves every registered label and group through the catalog", () => {
|
||||
render(<HomepageCommands onCreateTask={vi.fn()} />);
|
||||
|
||||
const inlined = mocks.commands.flatMap((command) =>
|
||||
[
|
||||
["label", command.label],
|
||||
["group", command.group],
|
||||
// Search keywords are matched, never displayed, but a non-English user
|
||||
// types in their own language — leaving them English makes the command
|
||||
// unfindable. They arrive as one comma-separated catalog value.
|
||||
["keywords", command.keywords?.join(",")],
|
||||
]
|
||||
.filter(([, value]) => typeof value === "string" && !value.startsWith("«"))
|
||||
.map(([field, value]) => `${command.id}.${field} = ${value}`),
|
||||
);
|
||||
|
||||
expect(inlined).toEqual([]);
|
||||
expect(mocks.commands.length).toBeGreaterThan(0);
|
||||
expect(mocks.commands.every((command) => (command.keywords?.length ?? 0) > 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "@/lib/routing/client-router";
|
||||
import { IconLayoutKanban, IconGitBranch, IconList, IconPlus } from "@tabler/icons-react";
|
||||
import { searchKeywords } from "@/lib/commands/search-keywords";
|
||||
import { useRegisterCommands } from "@/hooks/use-register-commands";
|
||||
import { useKanbanDisplaySettings } from "@/hooks/use-kanban-display-settings";
|
||||
import { linkToTaskOverview, linkToTasks } from "@/lib/links";
|
||||
@@ -16,6 +18,7 @@ type HomepageCommandsProps = {
|
||||
};
|
||||
|
||||
export function HomepageCommands({ onCreateTask }: HomepageCommandsProps) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { onViewModeChange } = useKanbanDisplaySettings();
|
||||
const { isMobile } = useResponsiveBreakpoint();
|
||||
@@ -33,20 +36,20 @@ export function HomepageCommands({ onCreateTask }: HomepageCommandsProps) {
|
||||
const items: CommandItem[] = [
|
||||
{
|
||||
id: "task-create",
|
||||
label: "Create New Task",
|
||||
group: "Tasks",
|
||||
label: t("common:commandCreateNewTask"),
|
||||
group: t("common:commandGroupTasks"),
|
||||
icon: <IconPlus className="size-3.5" />,
|
||||
shortcut: newTaskShortcut,
|
||||
keywords: ["new", "create", "task", "add"],
|
||||
keywords: searchKeywords(t, "common:commandCreateNewTaskKeywords"),
|
||||
action: onCreateTask,
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
id: "view-kanban",
|
||||
label: "Switch to Kanban View",
|
||||
group: "View",
|
||||
label: t("common:commandSwitchToKanbanView"),
|
||||
group: t("common:commandGroupView"),
|
||||
icon: <IconLayoutKanban className="size-3.5" />,
|
||||
keywords: ["kanban", "board", "view"],
|
||||
keywords: searchKeywords(t, "common:commandSwitchToKanbanViewKeywords"),
|
||||
priority: 0,
|
||||
action: () => {
|
||||
router.push(taskOverviewHref);
|
||||
@@ -58,10 +61,10 @@ export function HomepageCommands({ onCreateTask }: HomepageCommandsProps) {
|
||||
if (!isMobile) {
|
||||
items.push({
|
||||
id: "view-pipeline",
|
||||
label: "Switch to Pipeline View",
|
||||
group: "View",
|
||||
label: t("common:commandSwitchToPipelineView"),
|
||||
group: t("common:commandGroupView"),
|
||||
icon: <IconGitBranch className="size-3.5" />,
|
||||
keywords: ["pipeline", "graph", "view"],
|
||||
keywords: searchKeywords(t, "common:commandSwitchToPipelineViewKeywords"),
|
||||
priority: 0,
|
||||
action: () => {
|
||||
router.push(taskOverviewHref);
|
||||
@@ -72,10 +75,10 @@ export function HomepageCommands({ onCreateTask }: HomepageCommandsProps) {
|
||||
|
||||
items.push({
|
||||
id: "view-list",
|
||||
label: "Switch to List View",
|
||||
group: "View",
|
||||
label: t("common:commandSwitchToListView"),
|
||||
group: t("common:commandGroupView"),
|
||||
icon: <IconList className="size-3.5" />,
|
||||
keywords: ["list", "table", "view"],
|
||||
keywords: searchKeywords(t, "common:commandSwitchToListViewKeywords"),
|
||||
priority: 0,
|
||||
action: () => router.push(tasksHref),
|
||||
});
|
||||
@@ -89,6 +92,7 @@ export function HomepageCommands({ onCreateTask }: HomepageCommandsProps) {
|
||||
isMobile,
|
||||
taskOverviewHref,
|
||||
tasksHref,
|
||||
t,
|
||||
]);
|
||||
|
||||
useRegisterCommands(commands);
|
||||
|
||||
@@ -61,11 +61,22 @@ afterEach(() => {
|
||||
statusDrawerState.issueSeverity = "none";
|
||||
});
|
||||
|
||||
function renderHeader(title: string, workspaceId?: string, onSearchChange?: () => void) {
|
||||
/**
|
||||
* `currentPage` — not the title text — decides whether this is the Home header.
|
||||
* The component used to derive that from `title === "Home"`, which was true only
|
||||
* in English, so these tests pass the discriminant explicitly now.
|
||||
*/
|
||||
function renderHeader(
|
||||
title: string,
|
||||
workspaceId?: string,
|
||||
onSearchChange?: () => void,
|
||||
currentPage: "kanban" | "tasks" = "kanban",
|
||||
) {
|
||||
return render(
|
||||
<StateProvider>
|
||||
<KanbanHeaderMobile
|
||||
title={title}
|
||||
currentPage={currentPage}
|
||||
workspaceId={workspaceId}
|
||||
onSearchChange={onSearchChange}
|
||||
workspaceLabel="/root/kandev"
|
||||
@@ -87,7 +98,7 @@ describe("KanbanHeaderMobile", () => {
|
||||
});
|
||||
|
||||
it("renders page title and workspace label for non-Home pages", () => {
|
||||
renderHeader("Tasks");
|
||||
renderHeader("Tasks", undefined, undefined, "tasks");
|
||||
|
||||
const leftActions = screen.getByTestId(LEFT_ACTIONS_TEST_ID);
|
||||
expect(leftActions.textContent).toContain("Tasks");
|
||||
|
||||
@@ -164,7 +164,7 @@ export function KanbanHeaderMobile({
|
||||
const setSearchOpen = useAppStore((state) => state.setMobileKanbanSearchOpen);
|
||||
const handleOpenQuickChat = useQuickChatLauncher(workspaceId);
|
||||
const handleOpenQuickTerminal = useQuickTerminalLauncher(workspaceId);
|
||||
const isHome = title === "Home";
|
||||
const isHome = currentPage !== "tasks";
|
||||
|
||||
const toggleSearch = () => {
|
||||
const next = !isSearchOpen;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
IconTimeline,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { PageTopbar } from "@/components/page-topbar";
|
||||
import { KanbanDisplayDropdown } from "../kanban-display-dropdown";
|
||||
import { usePluginTaskFilters } from "@/hooks/use-plugin-task-filters";
|
||||
@@ -64,13 +65,27 @@ const DESKTOP_HEADER_NARROW_PX = 800;
|
||||
function getWorkspaceLabel(
|
||||
workspaces: Array<{ id: string; name: string }>,
|
||||
activeWorkspaceId: string | null,
|
||||
t: TFunction,
|
||||
): string {
|
||||
if (!activeWorkspaceId) return "All workspaces";
|
||||
return workspaces.find((workspace) => workspace.id === activeWorkspaceId)?.name ?? "Workspace";
|
||||
if (!activeWorkspaceId) return t("kanban:allWorkspaces");
|
||||
return (
|
||||
workspaces.find((workspace) => workspace.id === activeWorkspaceId)?.name ??
|
||||
t("common:workspace")
|
||||
);
|
||||
}
|
||||
|
||||
function getHeaderTitle(currentPage: string): string {
|
||||
return currentPage === "tasks" ? "Tasks" : "Home";
|
||||
/**
|
||||
* The header title is display copy, so it must not double as the "is this the
|
||||
* home page?" flag — `title === "Home"` was true only in English and silently
|
||||
* picked the wrong branch in every other locale. `currentPage` is the
|
||||
* discriminant; the title is derived from it.
|
||||
*/
|
||||
function isHomePage(currentPage: string): boolean {
|
||||
return currentPage !== "tasks";
|
||||
}
|
||||
|
||||
function getHeaderTitle(currentPage: string, t: TFunction): string {
|
||||
return isHomePage(currentPage) ? t("sidebar:home") : t("sidebar:tasks");
|
||||
}
|
||||
|
||||
function toHeaderHealthProps(health: ReturnType<typeof useSystemHealthIndicator>) {
|
||||
@@ -206,7 +221,7 @@ function TabletHeader({
|
||||
hideTitle?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isHome = title === "Home";
|
||||
const isHome = isHomePage(currentPage);
|
||||
const pluginTaskFilters = usePluginTaskFilters();
|
||||
|
||||
return (
|
||||
@@ -305,7 +320,7 @@ function DesktopHeader({
|
||||
className={`${isNarrow ? "w-44" : "w-72 xl:w-80"} [&_input]:h-8`}
|
||||
/>
|
||||
) : null;
|
||||
const isHome = title === "Home";
|
||||
const isHome = isHomePage(currentPage);
|
||||
const centerSearch =
|
||||
searchInput && !isNarrow ? <div data-testid="kanban-header-search">{searchInput}</div> : null;
|
||||
const actionsSearch = isNarrow ? searchInput : null;
|
||||
@@ -381,6 +396,7 @@ export function KanbanHeader({
|
||||
isSearchLoading = false,
|
||||
tasksListOptions,
|
||||
}: KanbanHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isTablet } = useResponsiveBreakpoint();
|
||||
const isMenuOpen = useAppStore((state) => state.mobileKanban.isMenuOpen);
|
||||
const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen);
|
||||
@@ -391,8 +407,8 @@ export function KanbanHeader({
|
||||
const healthIndicator = useSystemHealthIndicator();
|
||||
const toggleValue = currentPage === "tasks" ? "list" : effectiveTaskListingView;
|
||||
const handleViewChange = useHeaderView(currentPage, workspaceId, workflowId, onViewModeChange);
|
||||
const title = getHeaderTitle(currentPage);
|
||||
const workspaceLabel = getWorkspaceLabel(workspaces, activeWorkspaceId);
|
||||
const title = getHeaderTitle(currentPage, t);
|
||||
const workspaceLabel = getWorkspaceLabel(workspaces, activeWorkspaceId, t);
|
||||
|
||||
const healthProps = toHeaderHealthProps(healthIndicator);
|
||||
const sharedSearch = { searchQuery, onSearchChange, isSearchLoading };
|
||||
|
||||
@@ -212,7 +212,7 @@ export function MobileColumnTabs({
|
||||
workflowNavigation?.workflows.find(
|
||||
(workflow) => workflow.id === workflowNavigation.activeWorkflowId,
|
||||
) ?? workflowNavigation?.workflows[0];
|
||||
const stepLabel = activeStep?.title ?? "No steps configured";
|
||||
const stepLabel = activeStep?.title ?? t("task:noStepsConfigured");
|
||||
|
||||
const selectStep = (index: number) => {
|
||||
onColumnChange(index);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { IconCheck, IconChevronDown, IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -155,6 +156,7 @@ function triggerDetails(
|
||||
currentModel: string | null,
|
||||
modelConfig: SelectConfigOption | undefined,
|
||||
extraConfigOptions: SelectConfigOption[],
|
||||
t: TFunction,
|
||||
): TriggerDetail[] {
|
||||
let modelValue = "";
|
||||
if (modelConfig) {
|
||||
@@ -166,7 +168,7 @@ function triggerDetails(
|
||||
? [
|
||||
{
|
||||
id: modelConfig?.id || MODEL_CONFIG_CATEGORY,
|
||||
name: modelConfig?.name || "Model",
|
||||
name: modelConfig?.name || t("common:model"),
|
||||
value: modelValue,
|
||||
},
|
||||
]
|
||||
@@ -531,8 +533,8 @@ export const ModelConfigSelector = memo(function ModelConfigSelector({
|
||||
onModelChange,
|
||||
onConfigChange,
|
||||
disabled,
|
||||
placeholder = "Select model...",
|
||||
ariaLabel = "Model settings",
|
||||
placeholder,
|
||||
ariaLabel,
|
||||
variant = "field",
|
||||
popoverSide = "bottom",
|
||||
triggerClassName: customTriggerClassName,
|
||||
@@ -557,7 +559,7 @@ export const ModelConfigSelector = memo(function ModelConfigSelector({
|
||||
);
|
||||
const details =
|
||||
triggerSummary === "changed"
|
||||
? triggerDetails(modelOptions, currentModel, modelConfig, extraConfigOptions)
|
||||
? triggerDetails(modelOptions, currentModel, modelConfig, extraConfigOptions, t)
|
||||
: undefined;
|
||||
// The (fallback) marker is a live WS signal, not replayed on reload. When
|
||||
// it is active, explain on the trigger that the note is transient so users
|
||||
@@ -583,11 +585,11 @@ export const ModelConfigSelector = memo(function ModelConfigSelector({
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<ModelConfigSelectorTrigger
|
||||
ariaLabel={ariaLabel}
|
||||
ariaLabel={ariaLabel ?? t("agents:modelSettings")}
|
||||
details={details}
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder ?? t("agents:selectModel")}
|
||||
triggerClassName={customTriggerClassName}
|
||||
triggerTitle={triggerTitle}
|
||||
variant={variant}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
splitReviewFileKey as splitFileKey,
|
||||
suppressAvailableGitlinkFiles,
|
||||
} from "./types";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
/**
|
||||
* Multi-repo dedup: keying ReviewFile entries by `path` only collapses
|
||||
@@ -312,7 +313,7 @@ function useReviewDialogHandlers(opts: ReviewDialogHandlerOptions) {
|
||||
else
|
||||
toast({
|
||||
title: "Discard failed",
|
||||
description: result.error || "An error occurred",
|
||||
description: result.error || t("common:anErrorOccurred"),
|
||||
variant: "error",
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function RepoGroupHeader({
|
||||
const { t } = useTranslation();
|
||||
// The empty-name group ("uncategorised" — no repository_name on its files)
|
||||
// gets a generic label so the user understands what they're looking at.
|
||||
const label = name || "Other changes";
|
||||
const label = name || t("task:otherChanges");
|
||||
return (
|
||||
<div
|
||||
className="sticky top-0 z-20 flex items-center gap-2 px-4 py-1.5 bg-muted/40 backdrop-blur-sm border-y border-border/60 text-xs font-medium text-foreground"
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { TFunction } from "i18next";
|
||||
import { t as translate } from "@/lib/i18n";
|
||||
import type { CommandItem } from "@/lib/commands/types";
|
||||
|
||||
import { buildTaskCommands } from "./session-commands";
|
||||
import {
|
||||
buildGitCommands,
|
||||
buildPanelCommands,
|
||||
buildSessionCommands,
|
||||
buildTaskCommands,
|
||||
buildWorkspaceCommands,
|
||||
} from "./session-commands";
|
||||
|
||||
const t = translate as unknown as TFunction;
|
||||
const ARCHIVE_COMMAND_ID = "task-archive";
|
||||
|
||||
/**
|
||||
* A `t` that echoes the key it was asked for. Any label or group that did NOT
|
||||
* go through the catalog comes back as plain English instead, which is what the
|
||||
* "every … resolves through the catalog" tests below detect. A palette entry
|
||||
* whose copy is inlined renders correct English and passes lint, so an assertion
|
||||
* on the English itself cannot tell the two apart.
|
||||
*/
|
||||
const markerT = ((key: string) => `«${key}»`) as unknown as TFunction;
|
||||
|
||||
const unresolved = (commands: CommandItem[]) =>
|
||||
commands.flatMap((cmd) =>
|
||||
[
|
||||
["label", cmd.label],
|
||||
["group", cmd.group],
|
||||
["inputPlaceholder", cmd.inputPlaceholder],
|
||||
]
|
||||
.filter(([, value]) => typeof value === "string" && !value.startsWith("«"))
|
||||
.map(([field, value]) => `${cmd.id}.${field} = ${value}`),
|
||||
);
|
||||
|
||||
function build(overrides: Partial<Parameters<typeof buildTaskCommands>[0]> = {}) {
|
||||
return buildTaskCommands({
|
||||
activeTaskId: "task-A",
|
||||
@@ -58,3 +85,73 @@ describe("buildTaskCommands", () => {
|
||||
expect(build({ activeTaskId: null })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("session palette copy", () => {
|
||||
const gitOptions = {
|
||||
git: { push: vi.fn(), pull: vi.fn(), rebase: vi.fn(), merge: vi.fn() },
|
||||
baseBranch: "origin/main",
|
||||
openCommitDialog: vi.fn(),
|
||||
openPRDialog: vi.fn(),
|
||||
runGitWithFeedback: vi.fn(),
|
||||
};
|
||||
|
||||
it("resolves every session, git, workspace and panel entry through the catalog", () => {
|
||||
const commands = [
|
||||
...buildSessionCommands(true, vi.fn(), markerT),
|
||||
...buildGitCommands({
|
||||
...gitOptions,
|
||||
git: gitOptions.git as unknown as Parameters<typeof buildGitCommands>[0]["git"],
|
||||
t: markerT,
|
||||
}),
|
||||
...buildWorkspaceCommands("session-A", markerT),
|
||||
...buildPanelCommands(
|
||||
{
|
||||
addBrowser: vi.fn(),
|
||||
addTerminal: vi.fn(),
|
||||
addPlan: vi.fn(),
|
||||
addChanges: vi.fn(),
|
||||
} as unknown as Parameters<typeof buildPanelCommands>[0],
|
||||
false,
|
||||
markerT,
|
||||
),
|
||||
...buildTaskCommands({
|
||||
activeTaskId: "task-A",
|
||||
isTaskArchived: false,
|
||||
t: markerT,
|
||||
openNewAgent: vi.fn(),
|
||||
openSubtask: vi.fn(),
|
||||
requestArchive: vi.fn(),
|
||||
}),
|
||||
];
|
||||
|
||||
expect(unresolved(commands)).toEqual([]);
|
||||
});
|
||||
|
||||
it("labels the git operation toast with the same catalog entry as the palette row", () => {
|
||||
const runGitWithFeedback = vi.fn();
|
||||
const push = buildGitCommands({
|
||||
...gitOptions,
|
||||
git: gitOptions.git as unknown as Parameters<typeof buildGitCommands>[0]["git"],
|
||||
runGitWithFeedback,
|
||||
t: markerT,
|
||||
}).find((cmd) => cmd.id === "git-push");
|
||||
|
||||
push?.action?.();
|
||||
|
||||
// The second argument is the operation name `useGitWithFeedback` composes
|
||||
// its toast titles around; it used to be a hardcoded "Push".
|
||||
expect(runGitWithFeedback).toHaveBeenCalledWith(expect.any(Function), push?.label);
|
||||
});
|
||||
|
||||
it("keeps every group of a builder identical so the palette does not split it", () => {
|
||||
const groups = new Set(
|
||||
buildGitCommands({
|
||||
...gitOptions,
|
||||
git: gitOptions.git as unknown as Parameters<typeof buildGitCommands>[0]["git"],
|
||||
t: markerT,
|
||||
}).map((cmd) => cmd.group),
|
||||
);
|
||||
|
||||
expect(groups.size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
import { useRegisterCommands } from "@/hooks/use-register-commands";
|
||||
import { useGitOperations } from "@/hooks/use-git-operations";
|
||||
import { useGitWithFeedback } from "@/hooks/use-git-with-feedback";
|
||||
import { gitOperationLabel, useGitWithFeedback } from "@/hooks/use-git-with-feedback";
|
||||
import { usePanelActions } from "@/hooks/use-panel-actions";
|
||||
import { useVcsDialogs } from "@/components/vcs/vcs-dialogs";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
@@ -53,98 +53,124 @@ type GitRunFn = (
|
||||
type GitOps = ReturnType<typeof useGitOperations>;
|
||||
type PanelActions = ReturnType<typeof usePanelActions>;
|
||||
|
||||
function buildSessionCommands(
|
||||
// Catalog keys, not copy — safe at module scope because nothing calls `t()`
|
||||
// here. The palette groups by the RESOLVED value, so every producer of a group
|
||||
// must go through these constants; `global-commands.tsx` does the same.
|
||||
const GROUP_AGENT = "common:commandGroupAgent";
|
||||
const GROUP_GIT = "common:commandGroupGit";
|
||||
const GROUP_WORKSPACE = "common:commandGroupWorkspace";
|
||||
const GROUP_PANELS = "common:commandGroupPanels";
|
||||
|
||||
export function buildSessionCommands(
|
||||
isAgentRunning: boolean | undefined,
|
||||
cancelTurn: () => void,
|
||||
t: TFunction,
|
||||
): CommandItem[] {
|
||||
const items: CommandItem[] = [];
|
||||
if (isAgentRunning)
|
||||
items.push({
|
||||
id: "session-cancel",
|
||||
label: "Cancel Turn",
|
||||
group: "Agent",
|
||||
label: t("common:commandCancelTurn"),
|
||||
group: t(GROUP_AGENT),
|
||||
icon: <IconPlayerStop className="size-3.5" />,
|
||||
keywords: ["cancel", "stop", "turn", "cancel agent", "stop agent", "interrupt agent"],
|
||||
keywords: searchKeywords(t, "common:commandCancelTurnKeywords"),
|
||||
action: cancelTurn,
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function buildGitCommands(
|
||||
git: GitOps,
|
||||
baseBranch: string | undefined,
|
||||
openCommitDialog: () => void,
|
||||
openPRDialog: () => void,
|
||||
runGitWithFeedback: GitRunFn,
|
||||
): CommandItem[] {
|
||||
type GitCommandOptions = {
|
||||
git: GitOps;
|
||||
baseBranch: string | undefined;
|
||||
openCommitDialog: () => void;
|
||||
openPRDialog: () => void;
|
||||
runGitWithFeedback: GitRunFn;
|
||||
t: TFunction;
|
||||
};
|
||||
|
||||
export function buildGitCommands({
|
||||
git,
|
||||
baseBranch,
|
||||
openCommitDialog,
|
||||
openPRDialog,
|
||||
runGitWithFeedback,
|
||||
t,
|
||||
}: GitCommandOptions): CommandItem[] {
|
||||
const group = t(GROUP_GIT);
|
||||
return [
|
||||
{
|
||||
id: "git-commit",
|
||||
label: "Commit Changes",
|
||||
group: "Git",
|
||||
label: t("common:commandCommitChanges"),
|
||||
group,
|
||||
icon: <IconGitCommit className="size-3.5" />,
|
||||
keywords: ["commit", "git", "save changes", "git commit"],
|
||||
keywords: searchKeywords(t, "common:commandCommitChangesKeywords"),
|
||||
action: openCommitDialog,
|
||||
},
|
||||
{
|
||||
id: "git-push",
|
||||
label: "Push",
|
||||
group: "Git",
|
||||
label: t("common:gitOpPush"),
|
||||
group,
|
||||
icon: <IconArrowUp className="size-3.5" />,
|
||||
keywords: ["push", "git", "push changes", "push to remote", "upload changes"],
|
||||
action: () => runGitWithFeedback(() => git.push(), "Push"),
|
||||
keywords: searchKeywords(t, "common:commandPushKeywords"),
|
||||
action: () => runGitWithFeedback(() => git.push(), gitOperationLabel(t, "common:gitOpPush")),
|
||||
},
|
||||
{
|
||||
id: "git-pull",
|
||||
label: "Pull",
|
||||
group: "Git",
|
||||
label: t("common:gitOpPull"),
|
||||
group,
|
||||
icon: <IconArrowDown className="size-3.5" />,
|
||||
keywords: ["pull", "git", "pull changes", "download changes"],
|
||||
action: () => runGitWithFeedback(() => git.pull(), "Pull"),
|
||||
keywords: searchKeywords(t, "common:commandPullKeywords"),
|
||||
action: () => runGitWithFeedback(() => git.pull(), gitOperationLabel(t, "common:gitOpPull")),
|
||||
},
|
||||
{
|
||||
id: "git-create-pr",
|
||||
label: "Create PR",
|
||||
group: "Git",
|
||||
label: t("common:commandCreatePr"),
|
||||
group,
|
||||
icon: <IconGitPullRequest className="size-3.5" />,
|
||||
keywords: ["pull request", "pr", "open pull request", "submit pull request", "git"],
|
||||
keywords: searchKeywords(t, "common:commandCreatePrKeywords"),
|
||||
action: openPRDialog,
|
||||
},
|
||||
{
|
||||
id: "git-rebase",
|
||||
label: "Rebase",
|
||||
group: "Git",
|
||||
label: t("common:gitOpRebase"),
|
||||
group,
|
||||
icon: <IconGitBranch className="size-3.5" />,
|
||||
keywords: ["rebase", "git", "branch"],
|
||||
keywords: searchKeywords(t, "common:commandRebaseKeywords"),
|
||||
action: () => {
|
||||
const t = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
return runGitWithFeedback(() => git.rebase(t), "Rebase");
|
||||
const target = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
return runGitWithFeedback(
|
||||
() => git.rebase(target),
|
||||
gitOperationLabel(t, "common:gitOpRebase"),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "git-merge",
|
||||
label: "Merge",
|
||||
group: "Git",
|
||||
label: t("common:gitOpMerge"),
|
||||
group,
|
||||
icon: <IconGitMerge className="size-3.5" />,
|
||||
keywords: ["merge", "git", "branch"],
|
||||
keywords: searchKeywords(t, "common:commandMergeKeywords"),
|
||||
action: () => {
|
||||
const t = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
return runGitWithFeedback(() => git.merge(t), "Merge");
|
||||
const target = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
return runGitWithFeedback(
|
||||
() => git.merge(target),
|
||||
gitOperationLabel(t, "common:gitOpMerge"),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildWorkspaceCommands(sessionId: string): CommandItem[] {
|
||||
export function buildWorkspaceCommands(sessionId: string, t: TFunction): CommandItem[] {
|
||||
return [
|
||||
{
|
||||
id: "workspace-create-file",
|
||||
label: "Create File",
|
||||
group: "Workspace",
|
||||
label: t("common:commandCreateFile"),
|
||||
group: t(GROUP_WORKSPACE),
|
||||
icon: <IconFilePlus className="size-3.5" />,
|
||||
keywords: ["create", "new", "file", "add"],
|
||||
keywords: searchKeywords(t, "common:commandCreateFileKeywords"),
|
||||
enterMode: "input",
|
||||
inputPlaceholder: "File path relative to workspace root...",
|
||||
inputPlaceholder: t("common:commandCreateFilePlaceholder"),
|
||||
onInputSubmit: async (path) => {
|
||||
const client = getWebSocketClient();
|
||||
if (!client) return;
|
||||
@@ -183,18 +209,18 @@ export function buildTaskCommands({
|
||||
const items: CommandItem[] = [
|
||||
{
|
||||
id: "agent-new",
|
||||
label: "New Agent",
|
||||
group: "Agent",
|
||||
label: t("common:commandNewAgent"),
|
||||
group: t(GROUP_AGENT),
|
||||
icon: <IconMessagePlus className="size-3.5" />,
|
||||
keywords: ["new", "agent", "session", "start agent", "new session"],
|
||||
keywords: searchKeywords(t, "common:commandNewAgentKeywords"),
|
||||
action: openNewAgent,
|
||||
},
|
||||
{
|
||||
id: "subtask-create",
|
||||
label: "Create Subtask",
|
||||
label: t("common:commandCreateSubtask"),
|
||||
group: t("common:commandGroupTasks"),
|
||||
icon: <IconSubtask className="size-3.5" />,
|
||||
keywords: ["subtask", "create", "new subtask", "new sub-task", "child task"],
|
||||
keywords: searchKeywords(t, "common:commandCreateSubtaskKeywords"),
|
||||
action: openSubtask,
|
||||
},
|
||||
];
|
||||
@@ -213,51 +239,45 @@ export function buildTaskCommands({
|
||||
return items;
|
||||
}
|
||||
|
||||
function buildPanelCommands(
|
||||
export function buildPanelCommands(
|
||||
panels: PanelActions,
|
||||
isPassthrough: boolean | undefined,
|
||||
t: TFunction,
|
||||
): CommandItem[] {
|
||||
const group = t(GROUP_PANELS);
|
||||
const items: CommandItem[] = [
|
||||
{
|
||||
id: "panel-browser",
|
||||
label: "Add Browser Panel",
|
||||
group: "Panels",
|
||||
label: t("common:commandAddBrowserPanel"),
|
||||
group,
|
||||
icon: <IconBrowser className="size-3.5" />,
|
||||
keywords: ["browser", "preview", "web", "open browser preview", "web preview", "app preview"],
|
||||
keywords: searchKeywords(t, "common:commandAddBrowserPanelKeywords"),
|
||||
action: () => panels.addBrowser(),
|
||||
},
|
||||
{
|
||||
id: "panel-terminal",
|
||||
label: "Add Terminal Panel",
|
||||
group: "Panels",
|
||||
label: t("common:commandAddTerminalPanel"),
|
||||
group,
|
||||
icon: <IconTerminal2 className="size-3.5" />,
|
||||
keywords: ["terminal", "shell", "console", "new terminal", "open terminal", "command line"],
|
||||
keywords: searchKeywords(t, "common:commandAddTerminalPanelKeywords"),
|
||||
action: () => panels.addTerminal(),
|
||||
},
|
||||
];
|
||||
if (!isPassthrough)
|
||||
items.push({
|
||||
id: "panel-plan",
|
||||
label: "Add Plan Panel",
|
||||
group: "Panels",
|
||||
label: t("common:commandAddPlanPanel"),
|
||||
group,
|
||||
icon: <IconFileText className="size-3.5" />,
|
||||
keywords: ["plan", "document", "task plan", "implementation plan", "plan details"],
|
||||
keywords: searchKeywords(t, "common:commandAddPlanPanelKeywords"),
|
||||
action: () => panels.addPlan(),
|
||||
});
|
||||
items.push({
|
||||
id: "panel-changes",
|
||||
label: "Add Changes Panel",
|
||||
group: "Panels",
|
||||
label: t("common:commandAddChangesPanel"),
|
||||
group,
|
||||
icon: <IconFileDiff className="size-3.5" />,
|
||||
keywords: [
|
||||
"changes",
|
||||
"diff",
|
||||
"git changes",
|
||||
"changed files",
|
||||
"source control",
|
||||
"git diff",
|
||||
"review changes",
|
||||
],
|
||||
keywords: searchKeywords(t, "common:commandAddChangesPanelKeywords"),
|
||||
action: () => panels.addChanges(),
|
||||
});
|
||||
return items;
|
||||
@@ -364,12 +384,19 @@ export function SessionCommands({
|
||||
const commands = useMemo<CommandItem[]>(() => {
|
||||
const sessionScoped = sessionId
|
||||
? [
|
||||
...buildSessionCommands(isAgentRunning, cancelTurn),
|
||||
...buildSessionCommands(isAgentRunning, cancelTurn, t),
|
||||
...(hasWorktree
|
||||
? buildGitCommands(git, baseBranch, openCommitDialog, openPRDialog, runGitWithFeedback)
|
||||
? buildGitCommands({
|
||||
git,
|
||||
baseBranch,
|
||||
openCommitDialog,
|
||||
openPRDialog,
|
||||
runGitWithFeedback,
|
||||
t,
|
||||
})
|
||||
: []),
|
||||
...(hasWorktree ? buildWorkspaceCommands(sessionId) : []),
|
||||
...buildPanelCommands(panels, isPassthrough),
|
||||
...(hasWorktree ? buildWorkspaceCommands(sessionId, t) : []),
|
||||
...buildPanelCommands(panels, isPassthrough, t),
|
||||
]
|
||||
: [];
|
||||
const items = [
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@kandev/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
builtInLayoutProfileName,
|
||||
BUILT_IN_LAYOUT_PROFILES,
|
||||
getBuiltInLayoutOverride,
|
||||
getBuiltInLayoutOverrideSourceId,
|
||||
@@ -109,7 +110,7 @@ function BuiltInProfileList({
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<span className="flex min-w-0 flex-wrap items-center justify-between gap-1">
|
||||
<span className="text-sm font-medium">{profile.name}</span>
|
||||
<span className="text-sm font-medium">{builtInLayoutProfileName(profile, t)}</span>
|
||||
<span className="flex flex-wrap justify-end gap-1">
|
||||
<Badge variant="outline">{t("settings:builtIn")}</Badge>
|
||||
{override && <Badge variant="secondary">{t("settings:customized")}</Badge>}
|
||||
@@ -118,7 +119,9 @@ function BuiltInProfileList({
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{profile.description}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{profile.descriptionKey ? t(profile.descriptionKey) : profile.description}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resetBuiltInLayoutOverride,
|
||||
resolveEffectiveDefaultLayout,
|
||||
setDefaultLayoutProfile,
|
||||
builtInLayoutProfileName,
|
||||
upsertBuiltInLayoutOverride,
|
||||
validateReusableLayout,
|
||||
} from "@/lib/layout/layout-profiles";
|
||||
@@ -151,8 +152,9 @@ function updateBuiltInDefault(drafts: Drafts, selected: SelectedState) {
|
||||
}
|
||||
|
||||
function useProfileActions(drafts: Drafts, selected: SelectedState) {
|
||||
const selectedName =
|
||||
selected.selectedBuiltIn?.name ?? selected.selectedCustom?.name ?? t("settings:layout");
|
||||
const selectedName = selected.selectedBuiltIn
|
||||
? builtInLayoutProfileName(selected.selectedBuiltIn, t)
|
||||
: (selected.selectedCustom?.name ?? t("settings:layout"));
|
||||
const duplicate = () =>
|
||||
attempt(drafts.setError, () => {
|
||||
const id = createLayoutProfileId();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from "@/lib/i18n";
|
||||
export type GitCredentialDisplay = {
|
||||
source: string;
|
||||
detail: string;
|
||||
@@ -57,7 +58,7 @@ export function getGitCredentialDisplay(
|
||||
case "executor":
|
||||
return {
|
||||
source: "Executor credentials",
|
||||
detail: actor || "Credentials available in the selected executor",
|
||||
detail: actor || t("task:credentialsAvailableInExecutor"),
|
||||
transport: transportLabels[transport] ?? "executor Git and gh credentials",
|
||||
};
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import i18n from "i18next";
|
||||
import type { PerRepoOperationResult } from "@/hooks/domains/session/use-session-git";
|
||||
|
||||
import { describePerRepo } from "./changes-panel-hooks";
|
||||
|
||||
/**
|
||||
* The multi-repo git toast. Its three branches were English template literals
|
||||
* (`${operationName} partially succeeded`, `Failed in N repos — …`) and are now
|
||||
* catalog messages with five interpolation variables between them.
|
||||
*
|
||||
* A misspelled variable — `repo` for `repos`, `count` for `succeeded` — makes
|
||||
* i18next leave the placeholder in the rendered string rather than throw, so it
|
||||
* ships a toast reading "succeeded in {{count}} repos" and nothing else catches
|
||||
* it. Every assertion below therefore checks for a leftover `{{`.
|
||||
*/
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
const repo = (name: string, success: boolean, error?: string): PerRepoOperationResult =>
|
||||
({ repository_name: name, success, error }) as PerRepoOperationResult;
|
||||
|
||||
const noPlaceholders = (result: { title: string; description: string }) => {
|
||||
expect(result.title).not.toContain("{{");
|
||||
expect(result.description).not.toContain("{{");
|
||||
};
|
||||
|
||||
describe("describePerRepo", () => {
|
||||
it("reports every repo succeeding", () => {
|
||||
const result = describePerRepo([repo("api", true), repo("web", true)], "Push");
|
||||
|
||||
expect(result.variant).toBe("success");
|
||||
expect(result.title).toBe("Push successful");
|
||||
expect(result.description).toBe("Push succeeded in 2 repos: api, web");
|
||||
noPlaceholders(result);
|
||||
});
|
||||
|
||||
it("reports every repo failing, with each repo's error", () => {
|
||||
const result = describePerRepo(
|
||||
[repo("api", false, "no upstream"), repo("web", false, "conflict")],
|
||||
"Rebase",
|
||||
);
|
||||
|
||||
expect(result.variant).toBe("error");
|
||||
expect(result.title).toBe("Rebase failed");
|
||||
expect(result.description).toBe("Failed in 2 repos — api: no upstream; web: conflict");
|
||||
noPlaceholders(result);
|
||||
});
|
||||
|
||||
it("surfaces a partial success as an error but names what did succeed", () => {
|
||||
const result = describePerRepo([repo("api", true), repo("web", false, "conflict")], "Merge");
|
||||
|
||||
expect(result.variant).toBe("error");
|
||||
expect(result.title).toBe("Merge partially succeeded");
|
||||
expect(result.description).toBe(
|
||||
"Merge succeeded in 1 of 2 repos (api); failed in web: conflict",
|
||||
);
|
||||
noPlaceholders(result);
|
||||
});
|
||||
|
||||
it("falls back to a generic error when a repo reports no message", () => {
|
||||
const result = describePerRepo([repo("api", false)], "Pull");
|
||||
|
||||
expect(result.description).toBe("Failed in 1 repo — api: Unknown error");
|
||||
noPlaceholders(result);
|
||||
});
|
||||
|
||||
it("uses the singular form for a single failing repo", () => {
|
||||
// `_one` vs `_other` is the reason `count` is passed at all; English differs
|
||||
// only in "repo"/"repos", but a locale with more forms needs the number.
|
||||
const one = describePerRepo([repo("api", false, "boom")], "Push");
|
||||
const two = describePerRepo([repo("api", false, "boom"), repo("web", false, "boom")], "Push");
|
||||
|
||||
expect(one.description).toContain("1 repo —");
|
||||
expect(two.description).toContain("2 repos —");
|
||||
});
|
||||
|
||||
it("keeps every branch resolving after a locale switch", async () => {
|
||||
await i18n.changeLanguage("pseudo");
|
||||
|
||||
for (const result of [
|
||||
describePerRepo([repo("api", true)], "Push"),
|
||||
describePerRepo([repo("api", false, "boom")], "Push"),
|
||||
describePerRepo([repo("api", true), repo("web", false, "boom")], "Push"),
|
||||
]) {
|
||||
noPlaceholders(result);
|
||||
// The repo names are data and stay ASCII; the frame around them must not.
|
||||
expect(result.title).toMatch(/[^\x20-\x7E]/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { gitOperationLabel } from "@/hooks/use-git-with-feedback";
|
||||
import type { useToast } from "@/components/toast-provider";
|
||||
import type { SessionGit, PerRepoOperationResult } from "@/hooks/domains/session/use-session-git";
|
||||
|
||||
@@ -37,43 +39,59 @@ type GitOperationFn = (op: () => Promise<GitOperationResultLike>, name: string)
|
||||
* present, summarise per-repo successes/failures instead of returning the
|
||||
* raw output (which was just the last repo's text and hid partial-success).
|
||||
*/
|
||||
function describePerRepo(
|
||||
export function describePerRepo(
|
||||
perRepo: PerRepoOperationResult[],
|
||||
operationName: string,
|
||||
): { title: string; description: string; variant: "success" | "error" } {
|
||||
const succeeded = perRepo.filter((r) => r.success);
|
||||
const failed = perRepo.filter((r) => !r.success);
|
||||
const succeededNames = succeeded.map((r) => r.repository_name).join(", ");
|
||||
const failedSummary = failed
|
||||
.map((r) => `${r.repository_name}: ${r.error || "unknown error"}`)
|
||||
const repos = succeeded.map((r) => r.repository_name).join(", ");
|
||||
const summary = failed
|
||||
.map((r) =>
|
||||
t("common:gitOperationRepoFailure", {
|
||||
repo: r.repository_name,
|
||||
error: r.error || t("common:unknownError"),
|
||||
}),
|
||||
)
|
||||
.join("; ");
|
||||
if (failed.length === 0) {
|
||||
return {
|
||||
title: `${operationName} successful`,
|
||||
description: `${operationName} succeeded in ${succeeded.length} repos: ${succeededNames}`,
|
||||
title: t("common:gitOperationSucceeded", { operation: operationName }),
|
||||
description: t("common:gitOperationSucceededInRepos", {
|
||||
count: succeeded.length,
|
||||
operation: operationName,
|
||||
repos,
|
||||
}),
|
||||
variant: "success",
|
||||
};
|
||||
}
|
||||
if (succeeded.length === 0) {
|
||||
return {
|
||||
title: `${operationName} failed`,
|
||||
description: `Failed in ${failed.length} repos — ${failedSummary}`,
|
||||
title: t("common:gitOperationFailed", { operation: operationName }),
|
||||
description: t("common:gitOperationFailedInRepos", { count: failed.length, summary }),
|
||||
variant: "error",
|
||||
};
|
||||
}
|
||||
// Partial success: surface as error so the user notices, but include the
|
||||
// succeeded list in the description so they don't retry the whole op.
|
||||
//
|
||||
// `gitOperationPartialDescription` carries `_one` as well as `_other` because
|
||||
// i18next requires both, but `_one` is unreachable here by construction: this
|
||||
// branch needs at least one success AND one failure, so `count` is never 1.
|
||||
// Translators can treat the singular as a formality.
|
||||
return {
|
||||
title: `${operationName} partially succeeded`,
|
||||
description: `${operationName} succeeded in ${succeeded.length} of ${perRepo.length} repos (${succeededNames}); failed in ${failedSummary}`,
|
||||
title: t("common:gitOperationPartiallySucceeded", { operation: operationName }),
|
||||
description: t("common:gitOperationPartialDescription", {
|
||||
count: perRepo.length,
|
||||
operation: operationName,
|
||||
succeeded: succeeded.length,
|
||||
repos,
|
||||
summary,
|
||||
}),
|
||||
variant: "error",
|
||||
};
|
||||
}
|
||||
|
||||
function labelWithRepo(label: string, repo: string | undefined): string {
|
||||
return repo ? `${label} (${repo})` : label;
|
||||
}
|
||||
|
||||
export function useChangesGitHandlers(
|
||||
gitOps: GitOps,
|
||||
toast: Toast,
|
||||
@@ -92,15 +110,18 @@ export function useChangesGitHandlers(
|
||||
return;
|
||||
}
|
||||
const variant = result.success ? "success" : "error";
|
||||
const title = result.success ? `${operationName} successful` : `${operationName} failed`;
|
||||
const title = result.success
|
||||
? t("common:gitOperationSucceeded", { operation: operationName })
|
||||
: t("common:gitOperationFailed", { operation: operationName });
|
||||
const description = result.success
|
||||
? result.output.slice(0, 200) || `${operationName} completed`
|
||||
: result.error || "An error occurred";
|
||||
? result.output.slice(0, 200) ||
|
||||
t("common:gitOperationCompleted", { operation: operationName })
|
||||
: result.error || t("common:anErrorOccurred");
|
||||
toast({ title, description, variant });
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: `${operationName} failed`,
|
||||
description: e instanceof Error ? e.message : "An unexpected error occurred",
|
||||
title: t("common:gitOperationFailed", { operation: operationName }),
|
||||
description: e instanceof Error ? e.message : t("common:anUnexpectedErrorOccurred"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
@@ -110,27 +131,39 @@ export function useChangesGitHandlers(
|
||||
|
||||
const handlePull = useCallback(
|
||||
(repo?: string) => {
|
||||
handleGitOperation(() => gitOps.pull(false, repo), labelWithRepo("Pull", repo));
|
||||
handleGitOperation(
|
||||
() => gitOps.pull(false, repo),
|
||||
gitOperationLabel(t, "common:gitOpPull", repo),
|
||||
);
|
||||
},
|
||||
[handleGitOperation, gitOps],
|
||||
);
|
||||
const handleRebase = useCallback(
|
||||
(repo?: string) => {
|
||||
const targetBranch = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
handleGitOperation(() => gitOps.rebase(targetBranch, repo), labelWithRepo("Rebase", repo));
|
||||
handleGitOperation(
|
||||
() => gitOps.rebase(targetBranch, repo),
|
||||
gitOperationLabel(t, "common:gitOpRebase", repo),
|
||||
);
|
||||
},
|
||||
[handleGitOperation, gitOps, baseBranch],
|
||||
);
|
||||
const handleMerge = useCallback(
|
||||
(repo?: string) => {
|
||||
const targetBranch = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
handleGitOperation(() => gitOps.merge(targetBranch, repo), labelWithRepo("Merge", repo));
|
||||
handleGitOperation(
|
||||
() => gitOps.merge(targetBranch, repo),
|
||||
gitOperationLabel(t, "common:gitOpMerge", repo),
|
||||
);
|
||||
},
|
||||
[handleGitOperation, gitOps, baseBranch],
|
||||
);
|
||||
const handlePush = useCallback(
|
||||
(repo?: string) => {
|
||||
handleGitOperation(() => gitOps.push(undefined, repo), labelWithRepo("Push", repo));
|
||||
handleGitOperation(
|
||||
() => gitOps.push(undefined, repo),
|
||||
gitOperationLabel(t, "common:gitOpPush", repo),
|
||||
);
|
||||
},
|
||||
[handleGitOperation, gitOps],
|
||||
);
|
||||
@@ -138,14 +171,14 @@ export function useChangesGitHandlers(
|
||||
(repo?: string) => {
|
||||
handleGitOperation(
|
||||
() => gitOps.push({ force: true }, repo),
|
||||
labelWithRepo("Force push", repo),
|
||||
gitOperationLabel(t, "common:gitOpForcePush", repo),
|
||||
);
|
||||
},
|
||||
[handleGitOperation, gitOps],
|
||||
);
|
||||
const handleRevertCommit = useCallback(
|
||||
(sha: string, repo?: string) => {
|
||||
handleGitOperation(() => gitOps.revertCommit(sha, repo), "Revert commit");
|
||||
handleGitOperation(() => gitOps.revertCommit(sha, repo), t("common:gitOpRevertCommit"));
|
||||
},
|
||||
[handleGitOperation, gitOps],
|
||||
);
|
||||
@@ -192,14 +225,14 @@ function useChangesDiscardAmendHandlers(
|
||||
const result = await gitOps.discard(paths, repoToDiscard);
|
||||
if (!result.success)
|
||||
toast({
|
||||
title: "Failed to discard changes",
|
||||
description: result.error || "An unknown error occurred",
|
||||
title: t("task:failedToDiscardChanges"),
|
||||
description: result.error || t("common:anUnknownErrorOccurred"),
|
||||
variant: "error",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Failed to discard changes",
|
||||
description: error instanceof Error ? error.message : "An unknown error occurred",
|
||||
title: t("task:failedToDiscardChanges"),
|
||||
description: error instanceof Error ? error.message : t("common:anUnknownErrorOccurred"),
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
@@ -229,7 +262,7 @@ function useChangesDiscardAmendHandlers(
|
||||
setAmendDialogOpen(false);
|
||||
await handleGitOperation(
|
||||
() => gitOps.commit(amendMessage.trim(), false, true, amendRepo),
|
||||
"Amend commit",
|
||||
t("common:gitOpAmendCommit"),
|
||||
);
|
||||
setAmendMessage("");
|
||||
setAmendRepo(undefined);
|
||||
@@ -270,7 +303,8 @@ function useChangesResetHandlers(gitOps: GitOps, handleGitOperation: GitOperatio
|
||||
async (mode: "soft" | "hard") => {
|
||||
if (!resetCommitSha) return;
|
||||
setResetDialogOpen(false);
|
||||
const operationName = mode === "hard" ? "Hard reset" : "Soft reset";
|
||||
const operationName =
|
||||
mode === "hard" ? t("common:gitOpHardReset") : t("common:gitOpSoftReset");
|
||||
await handleGitOperation(() => gitOps.reset(resetCommitSha, mode, resetRepo), operationName);
|
||||
setResetCommitSha(null);
|
||||
setResetRepo(undefined);
|
||||
|
||||
@@ -47,8 +47,9 @@ export function RepoGroupItem({
|
||||
displayName?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const stop = (e: React.MouseEvent) => e.stopPropagation();
|
||||
const label = displayName || group.repositoryName || "Repository";
|
||||
const label = displayName || group.repositoryName || t("common:repository");
|
||||
return (
|
||||
<li data-testid="changes-repo-group" data-repository-name={group.repositoryName || ""}>
|
||||
<div className="flex items-center justify-between gap-2 px-1 py-0.5">
|
||||
@@ -246,8 +247,9 @@ export function CommitsRepoGroup({
|
||||
// Each repo has its own "latest unpushed commit" — revert/amend in this
|
||||
// group must target THIS repo's newest, not the merged-list newest.
|
||||
const firstUnpushedInGroup = groupCommits.findIndex((c) => c.pushed !== true);
|
||||
const { t } = useTranslation();
|
||||
const stop = (e: React.MouseEvent) => e.stopPropagation();
|
||||
const label = displayName || repositoryName || "Repository";
|
||||
const label = displayName || repositoryName || t("common:repository");
|
||||
// Bug 10 acknowledged trade-off: `existingPrUrl` is sourced from a
|
||||
// workspace-scoped `prByRepo` map keyed only by "" today — the kandev task
|
||||
// model has one PR per task, not one PR per repo. As a result every per-
|
||||
|
||||
@@ -54,7 +54,7 @@ export function PermissionRequestMessage({ comment }: PermissionRequestMessagePr
|
||||
});
|
||||
|
||||
const statusBadge = getPermissionStatusBadge(permissionStatus);
|
||||
const titleText = comment.content || "Permission Required";
|
||||
const titleText = comment.content || t("task:permissionRequired");
|
||||
const detailSummary = summarizePermissionAction(permissionMetadata?.action_details, titleText);
|
||||
|
||||
return (
|
||||
|
||||
@@ -134,7 +134,7 @@ function parseStatusMetadata(comment: Message) {
|
||||
const progress =
|
||||
typeof metadata?.progress === "number" ? Math.min(Math.max(metadata.progress, 0), 100) : null;
|
||||
const statusLine = metadata?.stage || metadata?.status;
|
||||
const message = metadata?.message || comment.content || statusLine || "Status update";
|
||||
const message = metadata?.message || comment.content || statusLine || t("task:statusUpdate");
|
||||
const isError = comment.type === "error" || metadata?.variant === "error";
|
||||
const isWarning = metadata?.variant === "warning" || metadata?.cancelled === true;
|
||||
return { metadata, progress, statusLine, message, isError, isWarning };
|
||||
|
||||
@@ -20,6 +20,7 @@ import { PermissionActionRow } from "./permission-action-row";
|
||||
import { ExpandableRow } from "./expandable-row";
|
||||
import { useExpandState } from "./use-expand-state";
|
||||
import { parsePermission, usePermissionResponseHandlers } from "./use-permission-handlers";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
const TOOL_ICON_RULES: Array<{ keywords: string[]; Icon: typeof IconCode }> = [
|
||||
{ keywords: ["edit", "replace", "write", "save"], Icon: IconEdit },
|
||||
@@ -191,7 +192,7 @@ export const ToolCallMessage = memo(function ToolCallMessage({
|
||||
const { isExpanded, handleToggle } = useExpandState(status, autoExpanded);
|
||||
|
||||
const metadata = comment.metadata as ToolCallMetadata | undefined;
|
||||
const rawTitle = metadata?.title ?? comment.content ?? "Tool call";
|
||||
const rawTitle = metadata?.title ?? comment.content ?? t("task:toolCall");
|
||||
const title = transformPathsInText(prettifyToolTitle(rawTitle), worktreePath);
|
||||
|
||||
const formattedOutput = hasOutput && !inlineOutput ? formatToolOutput(output) : null;
|
||||
|
||||
@@ -277,13 +277,14 @@ export const ToolSubagentMessage = memo(function ToolSubagentMessage({
|
||||
onOpenFile,
|
||||
renderChild,
|
||||
}: ToolSubagentMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const metadata = comment.metadata as ToolCallMetadata | undefined;
|
||||
const childCount = childMessages.length;
|
||||
const { subagentTask, isActive, resultText, prompt, hasExpandableContent } =
|
||||
deriveSubagentDisplay(metadata, childCount, isContainingTurnActive);
|
||||
const resultSummary = firstResultLine(resultText);
|
||||
const description = subagentTask?.description || comment.content || "Subagent";
|
||||
const subagentType = subagentTask?.subagent_type || "Task";
|
||||
const description = subagentTask?.description || comment.content || t("task:subagent");
|
||||
const subagentType = subagentTask?.subagent_type || t("common:task");
|
||||
|
||||
// Track manual override state - null means "use auto behavior"
|
||||
const [manualExpandState, setManualExpandState] = useState<boolean | null>(null);
|
||||
|
||||
@@ -89,7 +89,7 @@ function HitAuthor({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const label = agentLabel?.trim() || "Agent";
|
||||
const label = agentLabel?.trim() || t("common:agent");
|
||||
return (
|
||||
<span className="text-[0.6875rem] uppercase tracking-wide font-medium text-muted-foreground inline-flex items-center gap-1.5 min-w-0">
|
||||
{agentName && <AgentLogo agentName={agentName} size={12} className="shrink-0" />}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { MentionItem } from "@/hooks/use-inline-mention";
|
||||
import type { AppState } from "@/lib/state/store";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
type TaskLike = AppState["kanban"]["tasks"][number];
|
||||
|
||||
@@ -57,8 +58,8 @@ export function buildTaskMentionItems(
|
||||
const addTask = (task: TaskLike, workflowId: string) => {
|
||||
if (task.id === currentTaskId || seen.has(task.id)) return;
|
||||
seen.add(task.id);
|
||||
const workflowName = workflowNameById.get(workflowId) ?? "Workflow";
|
||||
const stepTitle = stepTitleById.get(task.workflowStepId) ?? "Step";
|
||||
const workflowName = workflowNameById.get(workflowId) ?? t("common:workflow");
|
||||
const stepTitle = stepTitleById.get(task.workflowStepId) ?? t("common:step");
|
||||
items.push(toMentionItem(task, workflowId, workflowName, stepTitle));
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { NodeViewWrapper, ReactNodeViewRenderer, type ReactNodeViewProps } from "@tiptap/react";
|
||||
import { IconHash } from "@tabler/icons-react";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export const EntityReferenceNode = Node.create({
|
||||
name: "entityReference",
|
||||
@@ -54,7 +55,7 @@ function EntityReferenceChipView({ node }: ReactNodeViewProps) {
|
||||
kind?: string | null;
|
||||
id?: string | null;
|
||||
};
|
||||
const label = key || title || id || "Reference";
|
||||
const label = key || title || id || t("common:reference");
|
||||
const source = [provider, kind].filter(Boolean).join(" ");
|
||||
const description = source ? `${source}: ${title || label}` : title || label;
|
||||
return (
|
||||
|
||||
@@ -156,7 +156,7 @@ export function LeftHeaderActions(props: IDockviewHeaderActionsProps) {
|
||||
if (!environmentId) return;
|
||||
try {
|
||||
const result = await createUserShell(environmentId, { scriptId });
|
||||
const title = result.label ?? "Script";
|
||||
const title = result.label ?? t("common:script");
|
||||
addTerminalPanel(result.terminalId, group.id, environmentId, undefined, title);
|
||||
} catch (error) {
|
||||
console.error("Failed to run script:", error);
|
||||
@@ -491,7 +491,7 @@ function TerminalScriptsDropdown({
|
||||
rightBottomGroupId ?? undefined,
|
||||
environmentId,
|
||||
undefined,
|
||||
result.label ?? "Script",
|
||||
result.label ?? t("common:script"),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to run script:", error);
|
||||
@@ -574,7 +574,7 @@ function TerminalDevPreviewButton({
|
||||
}
|
||||
try {
|
||||
const shell = await createUserShell(environmentId, { taskId: taskID ?? undefined });
|
||||
const title = shell.displayName ?? shell.label ?? "Terminal";
|
||||
const title = shell.displayName ?? shell.label ?? t("common:terminal");
|
||||
addTerminalPanel(
|
||||
shell.terminalId,
|
||||
rightBottomGroupId ?? undefined,
|
||||
|
||||
@@ -155,7 +155,7 @@ function useWatermarkHandlers(
|
||||
groupId,
|
||||
environmentId,
|
||||
undefined,
|
||||
result.label ?? "Script",
|
||||
result.label ?? t("common:script"),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to run script:", error);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useOpenSessionFolder } from "@/hooks/use-open-session-folder";
|
||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useMultiSelect } from "@/hooks/use-multi-select";
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { useContextFilesStore } from "@/lib/state/context-files-store";
|
||||
@@ -156,7 +157,11 @@ type MoveFilesParams = {
|
||||
onRenameFile: (oldPath: string, newPath: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
function executeMoveFiles(params: MoveFilesParams, toast: ReturnType<typeof useToast>["toast"]) {
|
||||
function executeMoveFiles(
|
||||
params: MoveFilesParams,
|
||||
toast: ReturnType<typeof useToast>["toast"],
|
||||
t: TFunction,
|
||||
) {
|
||||
const { sources, targetPath, treeState, setSelectedPaths, onRenameFile } = params;
|
||||
const snapshot = treeState.tree;
|
||||
|
||||
@@ -172,8 +177,8 @@ function executeMoveFiles(params: MoveFilesParams, toast: ReturnType<typeof useT
|
||||
if (results.some((ok) => !ok)) {
|
||||
treeState.setTree(snapshot);
|
||||
toast({
|
||||
title: "Move failed",
|
||||
description: "Some files could not be moved",
|
||||
title: t("task:moveFailed"),
|
||||
description: t("task:moveFailedFiles"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
@@ -181,8 +186,8 @@ function executeMoveFiles(params: MoveFilesParams, toast: ReturnType<typeof useT
|
||||
.catch(() => {
|
||||
treeState.setTree(snapshot);
|
||||
toast({
|
||||
title: "Move failed",
|
||||
description: "An error occurred while moving files",
|
||||
title: t("task:moveFailed"),
|
||||
description: t("task:moveFailedUnexpected"),
|
||||
variant: "error",
|
||||
});
|
||||
});
|
||||
@@ -194,6 +199,7 @@ function useDragAndDrop(
|
||||
setSelectedPaths: (paths: Set<string>) => void,
|
||||
onRenameFile?: (oldPath: string, newPath: string) => Promise<boolean>,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragOverPath, setDragOverPath] = useState<string | null>(null);
|
||||
@@ -257,9 +263,13 @@ function useDragAndDrop(
|
||||
if (!onRenameFile) return;
|
||||
const sources = dragPathsRef.current;
|
||||
if (sources.length === 0 || isDropInvalid(sources, targetPath)) return;
|
||||
executeMoveFiles({ sources, targetPath, treeState, setSelectedPaths, onRenameFile }, toast);
|
||||
executeMoveFiles(
|
||||
{ sources, targetPath, treeState, setSelectedPaths, onRenameFile },
|
||||
toast,
|
||||
t,
|
||||
);
|
||||
},
|
||||
[onRenameFile, treeState, setSelectedPaths, toast],
|
||||
[onRenameFile, treeState, setSelectedPaths, t, toast],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -170,7 +170,7 @@ export const ModeSelector = memo(function ModeSelector({
|
||||
}
|
||||
|
||||
const currentMode = modeState.availableModes.find((m) => m.id === modeState.currentModeId);
|
||||
const displayName = currentMode?.name || modeState.currentModeId || "Mode";
|
||||
const displayName = currentMode?.name || modeState.currentModeId || t("common:mode");
|
||||
|
||||
return (
|
||||
<DropdownMenu open={dropdownOpen} onOpenChange={handleDropdownOpenChange}>
|
||||
|
||||
@@ -49,6 +49,7 @@ function agentProfileDisplayLabel(profile: AgentProfileOption): string {
|
||||
}
|
||||
|
||||
function useNewSessionDialogState(taskId: string) {
|
||||
const { t } = useTranslation();
|
||||
const resolvedWorkspaceId = useAppStore((state) =>
|
||||
resolveComposerWorkspaceId({
|
||||
sessionId: null,
|
||||
@@ -61,8 +62,8 @@ function useNewSessionDialogState(taskId: string) {
|
||||
}),
|
||||
);
|
||||
const taskTitle = useAppStore((state) => {
|
||||
const task = state.kanban.tasks.find((t: { id: string }) => t.id === taskId);
|
||||
return task?.title ?? "Task";
|
||||
const task = state.kanban.tasks.find((entry: { id: string }) => entry.id === taskId);
|
||||
return task?.title ?? t("common:task");
|
||||
});
|
||||
const agentProfiles = useAppStore((state) => state.agentProfiles.items);
|
||||
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type MutableRefObject,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { searchKeywords } from "@/lib/commands/search-keywords";
|
||||
import { useRouter } from "@/lib/routing/client-router";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { useCommandPanelOpen } from "@/lib/commands/command-registry";
|
||||
@@ -419,25 +420,16 @@ function useSwitcherActions({
|
||||
function useRecentTaskSwitcherCommand(shortcut: KeyboardShortcut, openSwitcher: () => void) {
|
||||
// `group` is the command palette's Map key AND its rendered heading, so it has
|
||||
// to be translated in lockstep with every other producer or the heading splits
|
||||
// in two under a non-English locale. `label`/`keywords` below stay English:
|
||||
// they belong to the components/task migration, not this one.
|
||||
// in two under a non-English locale.
|
||||
const { t } = useTranslation();
|
||||
const commands = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "open-recent-task-switcher",
|
||||
label: "Open Recent Task Switcher",
|
||||
label: t("common:commandOpenRecentTaskSwitcher"),
|
||||
group: t("common:commandGroupNavigation"),
|
||||
shortcut,
|
||||
keywords: [
|
||||
"recent",
|
||||
"task",
|
||||
"switcher",
|
||||
"history",
|
||||
"recent tasks",
|
||||
"switch task",
|
||||
"switch recent tasks",
|
||||
],
|
||||
keywords: searchKeywords(t, "common:commandOpenRecentTaskSwitcherKeywords"),
|
||||
action: openSwitcher,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SidebarViewChips } from "./sidebar-view-chips";
|
||||
import { SidebarFilterPopover } from "./sidebar-filter-popover";
|
||||
import { useSidebarViewPopover } from "./use-sidebar-view-popover";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { sidebarViewName } from "@/lib/state/slices/ui/sidebar-view-builtins";
|
||||
|
||||
// `group` is the command-palette bucket shared with `components/session-commands.tsx`;
|
||||
// it is grouping taxonomy rather than this surface's copy, so it migrates with
|
||||
@@ -36,9 +37,9 @@ function useSidebarCommands(
|
||||
for (const view of views) {
|
||||
list.push({
|
||||
id: `sidebar-switch-view-${view.id}`,
|
||||
label: t("task:switchSidebarView", { name: view.name }),
|
||||
label: t("task:switchSidebarView", { name: sidebarViewName(view, t) }),
|
||||
group: "Sidebar",
|
||||
keywords: ["view", "switch", "sidebar", view.name.toLowerCase()],
|
||||
keywords: ["view", "switch", "sidebar", sidebarViewName(view, t).toLowerCase()],
|
||||
action: () => setActiveView(view.id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import { CSS } from "@dnd-kit/utilities";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import type { SidebarView } from "@/lib/state/slices/ui/sidebar-view-types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { sidebarViewName } from "@/lib/state/slices/ui/sidebar-view-builtins";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DRAG_ACTIVATION_DISTANCE = 8;
|
||||
const TOUCH_DRAG_DELAY_MS = 250;
|
||||
@@ -80,6 +82,7 @@ function SidebarViewChip({
|
||||
active: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: view.id,
|
||||
});
|
||||
@@ -150,10 +153,10 @@ function SidebarViewChip({
|
||||
: "border-transparent text-muted-foreground hover:text-foreground",
|
||||
isDragging && "z-50 cursor-grabbing",
|
||||
)}
|
||||
title={view.name}
|
||||
title={sidebarViewName(view, t)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="block max-w-[120px] truncate">{view.name}</span>
|
||||
<span className="block max-w-[120px] truncate">{sidebarViewName(view, t)}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Input } from "@kandev/ui/input";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import type { SidebarView } from "@/lib/state/slices/ui/sidebar-view-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { sidebarViewName } from "@/lib/state/slices/ui/sidebar-view-builtins";
|
||||
|
||||
type HeaderMode = "view" | "rename" | "saveAs";
|
||||
|
||||
@@ -115,7 +116,7 @@ function NameDisplay({
|
||||
return (
|
||||
<>
|
||||
<span className="font-medium" data-testid="sidebar-filter-active-view-name">
|
||||
{activeView?.name ?? "—"}
|
||||
{activeView ? sidebarViewName(activeView, t) : "—"}
|
||||
</span>
|
||||
{hasDraft && (
|
||||
<span
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { calculateHash, generateUnifiedDiff } from "@/lib/utils/file-diff";
|
||||
import { requestFileContent, updateFileContent, deleteFile } from "@/lib/ws/workspace-files";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { getFileTabKey } from "./task-center-panel-file-tabs";
|
||||
import { lspClientManager } from "@/lib/lsp/lsp-client-manager";
|
||||
|
||||
@@ -235,16 +236,15 @@ export function useFileSaveDelete({
|
||||
);
|
||||
} else {
|
||||
toast({
|
||||
title: "Save failed",
|
||||
description: response.error || "Failed to save file",
|
||||
title: t("editors:saveFailed"),
|
||||
description: response.error || t("editors:failedToSaveFile"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Save failed",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred while saving the file",
|
||||
title: t("editors:saveFailed"),
|
||||
description: error instanceof Error ? error.message : t("editors:errorWhileSavingFile"),
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
@@ -268,16 +268,15 @@ export function useFileSaveDelete({
|
||||
handleCloseFileTab(getFileTabKey({ path, repo }));
|
||||
} else {
|
||||
toast({
|
||||
title: "Delete failed",
|
||||
description: response.error || "Failed to delete file",
|
||||
title: t("editors:deleteFailed"),
|
||||
description: response.error || t("editors:failedToDeleteFile"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Delete failed",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred while deleting the file",
|
||||
title: t("editors:deleteFailed"),
|
||||
description: error instanceof Error ? error.message : t("editors:errorWhileDeletingFile"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useAppStore } from "@/components/state-provider";
|
||||
import { PlanRevisionPreviewDialog } from "./task-plan-preview-dialog";
|
||||
import { PlanRevisionDiffDialog } from "./task-plan-diff-dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
type ComparePair = [string | null, string | null];
|
||||
|
||||
@@ -46,7 +47,8 @@ type TaskPlanRevisionsProps = {
|
||||
|
||||
/** Label rendered in the UI for any user-authored revision. Display-only —
|
||||
* the backend still stamps its own author_name on the row. */
|
||||
const USER_DISPLAY_NAME = "You";
|
||||
/** Catalog key, not copy — resolving at module scope freezes the locale. */
|
||||
const USER_DISPLAY_NAME_KEY = "common:you";
|
||||
|
||||
export function TaskPlanRevisions(props: TaskPlanRevisionsProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -267,7 +269,7 @@ function RevisionsDialogStack({
|
||||
// Remount on revision change so loader state resets without setState-in-effect.
|
||||
key={`preview-${previewRevision?.id ?? "none"}`}
|
||||
revision={previewRevision}
|
||||
authorLabel={previewRevision ? authorLabel(previewRevision) : ""}
|
||||
authorLabel={previewRevision ? authorLabel(previewRevision, t) : ""}
|
||||
loadContent={loadRevisionContent}
|
||||
onClose={() => setPreviewRevision(null)}
|
||||
onRestore={() => {
|
||||
@@ -307,9 +309,9 @@ function RevisionsDialogStack({
|
||||
|
||||
/** Display label for an author. User revisions always render as "You";
|
||||
* agent revisions use the stored author_name (the agent profile display name). */
|
||||
function authorLabel(revision: TaskPlanRevision): string {
|
||||
if (revision.author_kind === "user") return USER_DISPLAY_NAME;
|
||||
return revision.author_name || "Agent";
|
||||
function authorLabel(revision: TaskPlanRevision, t: TFunction): string {
|
||||
if (revision.author_kind === "user") return t(USER_DISPLAY_NAME_KEY);
|
||||
return revision.author_name || t("common:agent");
|
||||
}
|
||||
|
||||
/** Resolve the active session's agent backend name (e.g. "claude", "codex") so
|
||||
@@ -370,6 +372,7 @@ function RevisionAuthor({
|
||||
revision: TaskPlanRevision;
|
||||
agentName: string | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (revision.author_kind === "agent") {
|
||||
return (
|
||||
<>
|
||||
@@ -384,7 +387,7 @@ function RevisionAuthor({
|
||||
<>
|
||||
<IconUser className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="text-xs text-foreground truncate" data-testid="plan-revision-author">
|
||||
{USER_DISPLAY_NAME}
|
||||
{t(USER_DISPLAY_NAME_KEY)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { TaskSwitcherItem } from "./task-switcher";
|
||||
import type { useArchivedTaskState } from "./task-archived-context";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export function buildArchivedSidebarItem(
|
||||
s: ReturnType<typeof useArchivedTaskState>,
|
||||
): TaskSwitcherItem {
|
||||
return {
|
||||
id: s.archivedTaskId!,
|
||||
title: s.archivedTaskTitle ?? "Archived task",
|
||||
title: s.archivedTaskTitle ?? t("task:archivedTask"),
|
||||
state: undefined,
|
||||
sessionState: undefined,
|
||||
description: undefined,
|
||||
|
||||
@@ -138,13 +138,14 @@ function useTerminalTabClose({
|
||||
}
|
||||
|
||||
export function TerminalTab(props: IDockviewPanelHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
const { terminalId, taskID: stampedTaskID, environmentId: stampedEnv } = extractParams(props);
|
||||
const activeTaskID = useAppStore((s) => s.tasks?.activeTaskId ?? null);
|
||||
const taskID = stampedTaskID ?? activeTaskID ?? null;
|
||||
const { shell, isOrdinary, seq, showBadge, displayName, closable } = useTerminalTabState(
|
||||
stampedEnv,
|
||||
terminalId,
|
||||
props.api.title ?? "Terminal",
|
||||
props.api.title ?? t("common:terminal"),
|
||||
);
|
||||
|
||||
// DockviewDefaultTab reads the title from `api.title` directly and
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { startTunnel, stopTunnel } from "@/lib/api/domains/port-api";
|
||||
import { toast } from "@/lib/toast/sonner";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export function useTunnelActions(
|
||||
sessionId: string,
|
||||
@@ -16,10 +17,12 @@ export function useTunnelActions(
|
||||
try {
|
||||
const tunnelPort = await startTunnel(sessionId, port, requestedPort);
|
||||
setActiveTunnels((prev) => new Map(prev).set(port, tunnelPort));
|
||||
toast.success(`Tunnel started on port ${tunnelPort}`);
|
||||
toast.success(t("task:tunnelStartedOnPort", { port: tunnelPort }));
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
`Failed to start tunnel: ${err instanceof Error ? err.message : "unknown error"}`,
|
||||
t("task:failedToStartTunnel", {
|
||||
error: err instanceof Error ? err.message : t("task:unknownError"),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setPendingTunnels((prev) => {
|
||||
@@ -42,10 +45,12 @@ export function useTunnelActions(
|
||||
next.delete(port);
|
||||
return next;
|
||||
});
|
||||
toast.success("Tunnel stopped");
|
||||
toast.success(t("task:tunnelStopped"));
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
`Failed to stop tunnel: ${err instanceof Error ? err.message : "unknown error"}`,
|
||||
t("task:failedToStopTunnel", {
|
||||
error: err instanceof Error ? err.message : t("task:unknownError"),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setPendingTunnels((prev) => {
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { cn } from "@kandev/ui/lib/utils";
|
||||
import { useSessionGit } from "@/hooks/domains/session/use-session-git";
|
||||
import { useGitWithFeedback } from "@/hooks/use-git-with-feedback";
|
||||
import { gitOperationLabel, useGitWithFeedback } from "@/hooks/use-git-with-feedback";
|
||||
import { useVcsDialogs } from "@/components/vcs/vcs-dialogs";
|
||||
import { useActiveTaskPR } from "@/hooks/domains/github/use-task-pr";
|
||||
import { useRepoDisplayName } from "@/hooks/domains/session/use-repo-display-name";
|
||||
@@ -310,47 +310,45 @@ 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 { t } = useTranslation();
|
||||
const gitWithFeedback = useGitWithFeedback();
|
||||
|
||||
const handlePull = useCallback(
|
||||
(repo?: string) => {
|
||||
const label = repo ? `Pull (${repo})` : "Pull";
|
||||
gitWithFeedback(() => git.pull(false, repo), label);
|
||||
gitWithFeedback(() => git.pull(false, repo), gitOperationLabel(t, "common:gitOpPull", repo));
|
||||
},
|
||||
[gitWithFeedback, git],
|
||||
[gitWithFeedback, git, t],
|
||||
);
|
||||
|
||||
const handlePush = useCallback(
|
||||
(force = false, repo?: string) => {
|
||||
const baseLabel = force ? "Force Push" : "Push";
|
||||
const label = repo ? `${baseLabel} (${repo})` : baseLabel;
|
||||
gitWithFeedback(() => git.push({ force }, repo), label);
|
||||
const operationKey = force ? "common:gitOpForcePush" : "common:gitOpPush";
|
||||
gitWithFeedback(() => git.push({ force }, repo), gitOperationLabel(t, operationKey, repo));
|
||||
},
|
||||
[gitWithFeedback, git],
|
||||
[gitWithFeedback, git, t],
|
||||
);
|
||||
|
||||
const handleRebase = useCallback(
|
||||
(repo?: string) => {
|
||||
const targetBranch = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
const label = repo ? `Rebase (${repo})` : "Rebase";
|
||||
gitWithFeedback(() => git.rebase(targetBranch, repo), label);
|
||||
gitWithFeedback(
|
||||
() => git.rebase(targetBranch, repo),
|
||||
gitOperationLabel(t, "common:gitOpRebase", repo),
|
||||
);
|
||||
},
|
||||
[gitWithFeedback, git, baseBranch],
|
||||
[gitWithFeedback, git, baseBranch, t],
|
||||
);
|
||||
|
||||
const handleMerge = useCallback(
|
||||
(repo?: string) => {
|
||||
const targetBranch = baseBranch?.replace(/^origin\//, "") || "main";
|
||||
const label = repo ? `Merge (${repo})` : "Merge";
|
||||
gitWithFeedback(() => git.merge(targetBranch, repo), label);
|
||||
gitWithFeedback(
|
||||
() => git.merge(targetBranch, repo),
|
||||
gitOperationLabel(t, "common:gitOpMerge", repo),
|
||||
);
|
||||
},
|
||||
[gitWithFeedback, git, baseBranch],
|
||||
[gitWithFeedback, git, baseBranch, t],
|
||||
);
|
||||
|
||||
return { handlePull, handlePush, handleRebase, handleMerge };
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
useGitOperations,
|
||||
} from "@/hooks/use-git-operations";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { useGitWithFeedback } from "@/hooks/use-git-with-feedback";
|
||||
import { gitOperationLabel, useGitWithFeedback } from "@/hooks/use-git-with-feedback";
|
||||
import { useUtilityAgentGenerator } from "@/hooks/use-utility-agent-generator";
|
||||
import { useIsUtilityConfigured } from "@/hooks/use-is-utility-configured";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
@@ -451,6 +451,7 @@ function useVcsDialogsState(
|
||||
taskTitle: string | undefined,
|
||||
baseBranch: string | undefined,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const cs = useCommitDialogState();
|
||||
const ps = usePRDialogState();
|
||||
const { toast } = useToast();
|
||||
@@ -477,16 +478,12 @@ function useVcsDialogsState(
|
||||
const title = cs.message.trim();
|
||||
const body = cs.body.trim();
|
||||
const fullMessage = body ? `${title}\n\n${body}` : title;
|
||||
// Deliberately English: `useGitWithFeedback` composes this into
|
||||
// "<label>...", "<label> successful" and "<label> failed" itself, and that
|
||||
// hook is not migrated. Translating only the label would produce a
|
||||
// half-translated toast.
|
||||
const label = cs.repo ? `Commit (${cs.repo})` : "Commit";
|
||||
const label = gitOperationLabel(t, "common:gitOpCommit", cs.repo);
|
||||
await gitWithFeedback(() => commit(fullMessage, cs.stageAll, false, cs.repo), label);
|
||||
cs.setMessage("");
|
||||
cs.setBody("");
|
||||
cs.setRepo(undefined);
|
||||
}, [cs, gitWithFeedback, commit]);
|
||||
}, [cs, gitWithFeedback, commit, t]);
|
||||
const handleCreatePR = useCreatePRHandler(
|
||||
ps,
|
||||
baseBranch,
|
||||
|
||||
@@ -195,34 +195,18 @@ const SCREENS: Screen[] = [
|
||||
// run between `cc6eb4dd5` and this commit. Re-check these against
|
||||
// `BUILT_IN_LAYOUT_PROFILES` whenever a built-in name or description moves.
|
||||
//
|
||||
// Both groups are display strings that are also PERSISTED, so translating
|
||||
// them in place would write locale-dependent values into a user's saved
|
||||
// layouts and leave them there after a locale switch:
|
||||
// - Built-in profile names/descriptions (lib/layout/layout-profiles.ts).
|
||||
// `upsertBuiltInLayoutOverride` copies `builtIn.name` into the saved
|
||||
// record the first time a built-in is customized.
|
||||
// - Dockview panel titles (lib/state/layout-manager/constants.ts), which
|
||||
// `toSerializedDockview` writes into the stored layout JSON. That path
|
||||
// is already in `EXCLUDED` in scripts/externalize-strings.mjs.
|
||||
// Localizing either needs a key/persisted-value split in those modules.
|
||||
allow: [
|
||||
"Default",
|
||||
"Plan Mode",
|
||||
"Preview Mode",
|
||||
"VS Code",
|
||||
"Agent with Files, Changes, and Terminal",
|
||||
"Agent and Plan side by side",
|
||||
"Agent and Browser side by side",
|
||||
"Agent and VS Code side by side",
|
||||
"Agent",
|
||||
"Plan",
|
||||
"Changes",
|
||||
"Files",
|
||||
"Browser",
|
||||
"Terminal",
|
||||
"PR Details",
|
||||
"Merge Request",
|
||||
],
|
||||
// Both groups WERE display strings that are also persisted, and both have
|
||||
// had the key/persisted-value split this note used to ask for:
|
||||
// - Built-in profile names/descriptions (lib/layout/layout-profiles.ts)
|
||||
// carry `nameKey`/`descriptionKey`; `name` stays canonical English
|
||||
// because `upsertBuiltInLayoutOverride` copies it into the saved record.
|
||||
// - Dockview panel titles (lib/state/layout-manager/constants.ts) carry
|
||||
// `titleKey`; `toSerializedDockview` writes the canonical English
|
||||
// `title` into the stored layout JSON, and `normalizePanel` re-resolves
|
||||
// the display title on restore. `panel-titles.test.ts` covers both
|
||||
// directions.
|
||||
// Only the two product names are still expected to render as English.
|
||||
allow: ["VS Code", "Agent and VS Code side by side"],
|
||||
},
|
||||
{
|
||||
name: "settings — keyboard shortcuts",
|
||||
@@ -298,6 +282,23 @@ const SCREENS: Screen[] = [
|
||||
// `test-base` state and belongs with whoever owns that fixture — not an
|
||||
// allowlist entry and not a renamed screen, either of which would keep the
|
||||
// false claim alive in a new form.
|
||||
// NOT YET: "kanban board" (`/`, anchor `[data-testid=kanban-board]`). Probed
|
||||
// under pseudo against this fixture. The board's own chrome is clean, and the
|
||||
// one real finding it surfaced — the built-in sidebar view's "All tasks",
|
||||
// which `sidebar-view-builtins.ts` persists as a user-editable name — is fixed
|
||||
// and now resolves through `sidebar:viewAllTasks`.
|
||||
//
|
||||
// What is left is entirely the fixture's own records: the workflow name
|
||||
// ("E2E Workflow") and its four step names ("Backlog", "In Progress",
|
||||
// "Review", "Done"). Those are user data on exactly the footing as the
|
||||
// workspace and executor-profile names described above, so this entry needs
|
||||
// the same change — `findUnlocalizedText` learning to skip user data — and not
|
||||
// an `allow` list of fixture values, which would fix this fixture and leave
|
||||
// every developer instance red under different workflow names.
|
||||
//
|
||||
// `/tasks` was probed with it and cannot be added for a different reason:
|
||||
// `[data-testid=tasks-list]` never renders in this fixture, so there is no
|
||||
// anchor that proves the route mounted. It needs a seeded task list first.
|
||||
{
|
||||
name: "office — inbox",
|
||||
url: "/office/inbox",
|
||||
|
||||
@@ -1824,4 +1824,94 @@ export const i18nGuardFiles = [
|
||||
// chips, routing cards and activity rows all resolve through, so the same
|
||||
// vocabulary cannot drift apart across surfaces.
|
||||
"app/office/**/*.{ts,tsx}",
|
||||
// The command palette's two un-migrated producers. `global-commands.tsx` was
|
||||
// already listed and already resolved everything through the catalog, so ⌘K
|
||||
// rendered a translated Navigation group above English Git/Panels rows; these
|
||||
// two are what made the same list half-English. Their labels are `label:`
|
||||
// object properties rather than JSX, which is why the guard reported them
|
||||
// clean the whole time — see the note on `mode: "jsx-only"` above.
|
||||
"components/homepage-commands.tsx",
|
||||
"components/session-commands.tsx",
|
||||
// `use-git-with-feedback.ts` composed its own English around the operation
|
||||
// name ("Push failed"), which is why `vcs-split-button.tsx`, `vcs-dialogs.tsx`
|
||||
// and the palette all passed it a deliberately English label. It interpolates
|
||||
// catalog messages now, so those three no longer have to.
|
||||
"hooks/use-git-with-feedback.ts",
|
||||
// Toast and feedback copy. None of it is JSX — it is `title:`/`description:`
|
||||
// object properties and `toast.error("…")` arguments — so `mode: "jsx-only"`
|
||||
// never inspected any of these files and they read as clean for the whole
|
||||
// migration. Listing them records that they are done; only the pseudo-locale
|
||||
// can prove they stay that way.
|
||||
//
|
||||
// `changes-panel-hooks.ts` carried the multi-repo half of the same
|
||||
// concatenated-English problem as `use-git-with-feedback.ts`
|
||||
// (`${operationName} partially succeeded`), including two per-repo counts that
|
||||
// now use `count` with `_one`/`_other` instead of a bare "repos".
|
||||
//
|
||||
// `unarchive-feedback.ts` inflected its own sentence
|
||||
// (`${plural ? "Branches" : "Branch"} … no longer ${plural ? "exist" : "exists"}`),
|
||||
// which is the inline-plural shape docs/i18n.md rules out; the plural rule
|
||||
// lives in the catalog now. `check-inline-plurals.mjs` does not see this
|
||||
// form — it is two independent ternaries, not a `+ "s"`.
|
||||
"app/tasks/tasks-page-client.tsx",
|
||||
"components/azure-devops/azure-devops-task-launcher.tsx",
|
||||
"components/github/use-pr-scoped-review-request.ts",
|
||||
"components/task/changes-panel-hooks.ts",
|
||||
"components/task/task-center-panel-restoration.ts",
|
||||
"components/task/use-tunnel-actions.ts",
|
||||
"hooks/domains/comments/use-markdown-preview-comments.ts",
|
||||
"hooks/domains/session/use-task-environment.ts",
|
||||
"hooks/use-file-save-delete.ts",
|
||||
"hooks/use-utility-agent-generator.ts",
|
||||
"lib/tasks/unarchive-feedback.ts",
|
||||
// Dockview panel titles. These are display copy that is ALSO persisted —
|
||||
// `toSerializedDockview` writes the title into the stored layout JSON — so
|
||||
// they now carry a `titleKey` beside a canonical English `title`, the split
|
||||
// the note on the layouts screen in `e2e/tests/i18n/pseudo-coverage.spec.ts`
|
||||
// said they needed. `constants.ts` deliberately still holds English `title:`
|
||||
// values; they are storage, not copy, and `panel-titles.test.ts` asserts the
|
||||
// two stay apart.
|
||||
"lib/state/dockview-panel-actions.ts",
|
||||
"lib/state/layout-manager/constants.ts",
|
||||
"lib/state/layout-manager/serializer.ts",
|
||||
// Built-in layout profile names and descriptions, the other half of the same
|
||||
// problem: `upsertBuiltInLayoutOverride` copies `name` into a saved override,
|
||||
// so `name`/`description` stay canonical English and the settings list renders
|
||||
// `nameKey`/`descriptionKey`.
|
||||
"lib/layout/layout-profiles.ts",
|
||||
// English `??` / `||` fallbacks — "Agent", "Terminal", "Repository",
|
||||
// "Untitled task", "An error occurred". They render whenever the real value
|
||||
// is absent, which is a normal path rather than an edge case, and none of
|
||||
// them is a JSX literal, so the guard never saw them. Values that are
|
||||
// PERSISTED or sent to an agent verbatim ("New Repository", the quick-chat
|
||||
// session name, `use-plan-actions`' default prompt) are deliberately still
|
||||
// English and stay off this list.
|
||||
"components/review/review-diff-list-groups.tsx",
|
||||
"components/review/review-dialog.tsx",
|
||||
"components/task/changes-git-credential-display.ts",
|
||||
"components/task/task-session-sidebar-archived-item.ts",
|
||||
"hooks/domains/session/use-session-resumption.ts",
|
||||
"hooks/domains/session/use-terminals.ts",
|
||||
"hooks/domains/session/use-user-shells.ts",
|
||||
"hooks/use-editor-keybinds.ts",
|
||||
"hooks/use-summarize-session.ts",
|
||||
"hooks/use-update-available-toast.ts",
|
||||
"lib/agent-runtime-update.ts",
|
||||
"lib/api/domains/plan-api.ts",
|
||||
"lib/capability-warning.ts",
|
||||
"lib/github-auth.ts",
|
||||
"lib/recent-tasks.ts",
|
||||
"lib/state/slices/comments/format.ts",
|
||||
"lib/utils/file-diff.ts",
|
||||
// The built-in sidebar view's name, the third persisted-and-displayed string
|
||||
// in this change. `SidebarView.name` is user-editable and synced, so the
|
||||
// built-in keeps a canonical English `name` and every surface resolves
|
||||
// `sidebarViewName()` instead. Found by the pseudo oracle, not by lint: it is
|
||||
// a `.ts` constant, which `mode: "jsx-only"` never inspects.
|
||||
"lib/state/slices/ui/sidebar-view-builtins.ts",
|
||||
// The last English row in the palette. Its `group` was already translated —
|
||||
// it has to be, since the palette groups by the resolved value — and a
|
||||
// comment deferred the `label`/`keywords` to "the components/task migration".
|
||||
// Both are palette copy and belong with the rest of it.
|
||||
"components/task/recent-task-switcher-hooks.ts",
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { useCommentsStore, type DiffComment } from "@/lib/state/slices/comments";
|
||||
import {
|
||||
@@ -169,6 +170,7 @@ function useMarkdownCommentSubmitters({
|
||||
textSelection: MarkdownPreviewSelection | null;
|
||||
clearTextSelection: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const addComment = useCommentsStore((s) => s.addComment);
|
||||
const { runComment } = useRunComment({ sessionId: sessionId ?? null, taskId: taskId ?? null });
|
||||
const { toast } = useToast();
|
||||
@@ -193,24 +195,27 @@ function useMarkdownCommentSubmitters({
|
||||
[addComment, clearTextSelection, path, repositoryId, sessionId, textSelection],
|
||||
);
|
||||
|
||||
// Same `editors:` copy as the CodeMirror and Monaco comment flows in
|
||||
// `components/editors/**`, which say exactly this. The markdown preview is
|
||||
// the third surface of the same feature and was the one left in English.
|
||||
const submitComment = useCallback(
|
||||
(text: string) => {
|
||||
const comment = createComment(text);
|
||||
if (!comment) return;
|
||||
toast({
|
||||
title: "Comment added",
|
||||
description: "Your comment will be sent with your next message.",
|
||||
title: t("editors:commentAdded"),
|
||||
description: t("editors:commentWillBeSentWithNextMessage"),
|
||||
});
|
||||
},
|
||||
[createComment, toast],
|
||||
[createComment, t, toast],
|
||||
);
|
||||
|
||||
const submitAndRunComment = useCallback(
|
||||
async (text: string) => {
|
||||
if (!canRunComment) {
|
||||
toast({
|
||||
title: "Failed to send comment",
|
||||
description: "Open a task session before sending to the agent.",
|
||||
title: t("editors:failedToSendComment"),
|
||||
description: t("editors:openTaskSessionBeforeSending"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
@@ -220,18 +225,18 @@ function useMarkdownCommentSubmitters({
|
||||
try {
|
||||
const { queued } = await runComment(comment);
|
||||
toast({
|
||||
title: "Comment sent",
|
||||
description: queued ? "Queued for the agent." : "Sent to the agent.",
|
||||
title: t("editors:commentSent"),
|
||||
description: queued ? t("editors:queuedForTheAgent") : t("editors:sentToTheAgent"),
|
||||
});
|
||||
} catch {
|
||||
toast({
|
||||
title: "Failed to send comment",
|
||||
description: "Please try again.",
|
||||
title: t("editors:failedToSendComment"),
|
||||
description: t("editors:pleaseTryAgain"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
[canRunComment, createComment, runComment, toast],
|
||||
[canRunComment, createComment, runComment, t, toast],
|
||||
);
|
||||
|
||||
return { submitComment, submitAndRunComment: canRunComment ? submitAndRunComment : undefined };
|
||||
@@ -278,6 +283,7 @@ function useVisibleMarkdownCommentActions({
|
||||
}: {
|
||||
setCommentView: Dispatch<SetStateAction<MarkdownCommentView>>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const removeComment = useCommentsStore((s) => s.removeComment);
|
||||
const updateComment = useCommentsStore((s) => s.updateComment);
|
||||
const { toast } = useToast();
|
||||
@@ -289,9 +295,9 @@ function useVisibleMarkdownCommentActions({
|
||||
const nextComments = view.comments.filter((comment) => comment.id !== commentId);
|
||||
return nextComments.length > 0 ? { ...view, comments: nextComments } : null;
|
||||
});
|
||||
toast({ title: "Comment deleted" });
|
||||
toast({ title: t("editors:commentDeleted") });
|
||||
},
|
||||
[removeComment, setCommentView, toast],
|
||||
[removeComment, setCommentView, t, toast],
|
||||
);
|
||||
const updateVisibleComment = useCallback(
|
||||
(commentId: string, text: string) => {
|
||||
@@ -305,9 +311,9 @@ function useVisibleMarkdownCommentActions({
|
||||
),
|
||||
};
|
||||
});
|
||||
toast({ title: "Comment updated" });
|
||||
toast({ title: t("editors:commentUpdated") });
|
||||
},
|
||||
[setCommentView, toast, updateComment],
|
||||
[setCommentView, t, toast, updateComment],
|
||||
);
|
||||
|
||||
return { removeVisibleComment, updateVisibleComment };
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type TaskId,
|
||||
type TaskSessionState,
|
||||
} from "@/lib/types/http";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type SessionStatus = {
|
||||
session_id: string;
|
||||
@@ -95,7 +96,7 @@ function applyResumeResponse(
|
||||
return true;
|
||||
}
|
||||
setters.setResumptionState("error");
|
||||
setters.setError(resp.error ?? "Failed to resume session");
|
||||
setters.setError(resp.error ?? t("task:failedToResumeSession"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "@/lib/toast/sonner";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
import {
|
||||
fetchTaskEnvironmentLive,
|
||||
@@ -101,15 +102,15 @@ export function useTaskEnvironment(taskId: string | null | undefined, active: bo
|
||||
setIsResetting(true);
|
||||
try {
|
||||
await resetTaskEnvironment(taskId, { push_branch: pushBranch });
|
||||
toast.success("Environment reset");
|
||||
toast.success(t("task:environmentReset"));
|
||||
lastStatusRef.current = getEnvironmentStatusSnapshot(null, null);
|
||||
setEnv(null);
|
||||
setContainer(null);
|
||||
setSsh(null);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Unknown error";
|
||||
toast.error(`Reset failed: ${msg}`);
|
||||
const msg = err instanceof Error ? err.message : t("task:unknownError");
|
||||
toast.error(t("task:environmentResetFailed", { error: msg }));
|
||||
return false;
|
||||
} finally {
|
||||
setIsResetting(false);
|
||||
@@ -132,10 +133,10 @@ function maybeNotifyEnvironmentStatus(
|
||||
) {
|
||||
if (!prev || prev.key === next.key) return;
|
||||
if (prev.tone === "running" && next.tone !== "running") {
|
||||
toast.error("Executor environment stopped", {
|
||||
description: `Current state: ${next.label}`,
|
||||
toast.error(t("task:executorEnvironmentStopped"), {
|
||||
description: t("task:environmentStateCurrent", { state: next.label }),
|
||||
});
|
||||
} else if (prev.tone !== "running" && next.tone === "running") {
|
||||
toast.success("Executor environment running");
|
||||
toast.success(t("task:executorEnvironmentRunning"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { UserShellInfo } from "@/lib/state/slices/session-runtime/types";
|
||||
import type { RepositoryScript } from "@/lib/types/http";
|
||||
import type { Dispatch, SetStateAction, MouseEvent } from "react";
|
||||
import type { PreviewStage } from "@/lib/state/slices";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type { Terminal, TerminalType };
|
||||
|
||||
@@ -168,7 +169,7 @@ function useAddTerminal({
|
||||
const newTerm: Terminal = {
|
||||
id: result.terminalId,
|
||||
type: "shell",
|
||||
label: result.displayName ?? result.label ?? "Terminal",
|
||||
label: result.displayName ?? result.label ?? t("common:terminal"),
|
||||
closable: true,
|
||||
kind: ordinary ? "ordinary" : result.kind,
|
||||
seq: result.seq,
|
||||
@@ -440,7 +441,7 @@ function useTerminalActions({
|
||||
const newTerm: Terminal = {
|
||||
id: result.terminalId,
|
||||
type: "script",
|
||||
label: result.label ?? script.name ?? "Script",
|
||||
label: result.label ?? script.name ?? t("common:script"),
|
||||
closable: true,
|
||||
kind: "script",
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
UserShellState,
|
||||
UserShellPTYStatus,
|
||||
} from "@/lib/state/slices";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
interface UseUserShellsReturn {
|
||||
shells: UserShellInfo[];
|
||||
@@ -179,7 +180,7 @@ function mapListItemToShell(s: ListResponseItem): UserShellInfo {
|
||||
ptyStatus: s.pty_status,
|
||||
processId: s.process_id,
|
||||
running: s.running ?? s.pty_status === "running",
|
||||
label: s.label || s.display_name || "Terminal",
|
||||
label: s.label || s.display_name || t("common:terminal"),
|
||||
closable: s.closable ?? true,
|
||||
initialCommand: s.initial_command,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createUserShell } from "@/lib/api/domains/user-shell-api";
|
||||
import { matchesShortcut } from "@/lib/keyboard/utils";
|
||||
import { getShortcut, type StoredShortcutOverrides } from "@/lib/keyboard/shortcut-overrides";
|
||||
import type { DockviewApi } from "dockview-react";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
function handleTabNavigation(e: KeyboardEvent, api: DockviewApi) {
|
||||
const activePanel = api.activePanel;
|
||||
@@ -66,7 +67,7 @@ function handleTerminalToggle(
|
||||
|
||||
createUserShell(environmentId, { taskId: taskID ?? undefined })
|
||||
.then((result) => {
|
||||
const title = result.displayName ?? result.label ?? "Terminal";
|
||||
const title = result.displayName ?? result.label ?? t("common:terminal");
|
||||
useDockviewStore
|
||||
.getState()
|
||||
.addTerminalPanel(result.terminalId, undefined, environmentId, taskID ?? undefined, title);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateUnifiedDiff, calculateHash } from "@/lib/utils/file-diff";
|
||||
import type { useToast } from "@/components/toast-provider";
|
||||
import { buildRepoScopedItemId, PREVIEW_FILE_EDITOR_ID } from "@/lib/state/dockview-panel-actions";
|
||||
import { lspClientManager } from "@/lib/lsp/lsp-client-manager";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
/** Read openFiles from the store without subscribing to changes. */
|
||||
function getOpenFiles() {
|
||||
@@ -96,23 +97,22 @@ async function performSaveFile(path: string, repo: string | undefined, params: S
|
||||
if (stillClean) updatePanelAfterSave(file.path, file.name, file.repo);
|
||||
if (response.resolution === "overwritten") {
|
||||
params.toast({
|
||||
title: "File saved (overwritten)",
|
||||
description: "The file was modified externally. Your version was saved.",
|
||||
title: t("editors:fileSavedOverwritten"),
|
||||
description: t("editors:fileSavedOverwrittenDescription"),
|
||||
variant: "default",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
params.toast({
|
||||
title: "Save failed",
|
||||
description: response.error || "Failed to save file",
|
||||
title: t("editors:saveFailed"),
|
||||
description: response.error || t("editors:failedToSaveFile"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
params.toast({
|
||||
title: "Save failed",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred while saving the file",
|
||||
title: t("editors:saveFailed"),
|
||||
description: error instanceof Error ? error.message : t("editors:errorWhileSavingFile"),
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
@@ -143,17 +143,16 @@ export function useSaveDeleteActions(params: SaveDeleteParams) {
|
||||
const response = await deleteFile(client, currentSessionId, path, fileRepo);
|
||||
if (!response.success) {
|
||||
toast({
|
||||
title: "Delete failed",
|
||||
description: response.error || "Failed to delete file",
|
||||
title: t("editors:deleteFailed"),
|
||||
description: response.error || t("editors:failedToDeleteFile"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Delete failed",
|
||||
description:
|
||||
error instanceof Error ? error.message : "An error occurred while deleting the file",
|
||||
title: t("editors:deleteFailed"),
|
||||
description: error instanceof Error ? error.message : t("editors:errorWhileDeletingFile"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -1,44 +1,71 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
|
||||
type GitOperationResult = { success: boolean; output: string; error?: string };
|
||||
|
||||
/**
|
||||
* Resolve an operation name for `useGitWithFeedback`, optionally scoped to one
|
||||
* repository in a multi-repo task. The parenthesised repo form is a catalog
|
||||
* entry rather than a template literal so a locale can move or re-punctuate it.
|
||||
*/
|
||||
export function gitOperationLabel(
|
||||
// Deliberately not `TFunction`: callers include plain helpers that use the
|
||||
// module-level `t` from `@/lib/i18n`, whose signature is narrower.
|
||||
translate: (key: string, values?: Record<string, unknown>) => string,
|
||||
operationKey: string,
|
||||
repo?: string,
|
||||
): string {
|
||||
const operation = translate(operationKey);
|
||||
return repo ? translate("common:gitOperationScoped", { operation, repo }) : operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a git operation with toast feedback (loading → success/error).
|
||||
*
|
||||
* `operationName` is the ALREADY-TRANSLATED name of the operation ("Push",
|
||||
* "Rebase (kandev)"), which this hook interpolates into its own catalog
|
||||
* messages. Callers resolve it through `gitOperationLabel` below rather than
|
||||
* passing a literal: the three call sites used to pass English on purpose,
|
||||
* because this hook concatenated English around it and translating only one
|
||||
* half would have shipped a mixed-language toast.
|
||||
*/
|
||||
export function useGitWithFeedback() {
|
||||
const { t } = useTranslation();
|
||||
const { toast, updateToast } = useToast();
|
||||
|
||||
const run = useCallback(
|
||||
async (operation: () => Promise<GitOperationResult>, operationName: string) => {
|
||||
const toastId = toast({
|
||||
title: `${operationName}...`,
|
||||
title: t("common:gitOperationRunning", { operation: operationName }),
|
||||
variant: "loading",
|
||||
});
|
||||
try {
|
||||
const result = await operation();
|
||||
if (result.success) {
|
||||
updateToast(toastId, {
|
||||
title: `${operationName} successful`,
|
||||
description: result.output.slice(0, 200) || `${operationName} completed`,
|
||||
title: t("common:gitOperationSucceeded", { operation: operationName }),
|
||||
description:
|
||||
result.output.slice(0, 200) ||
|
||||
t("common:gitOperationCompleted", { operation: operationName }),
|
||||
variant: "success",
|
||||
});
|
||||
} else {
|
||||
updateToast(toastId, {
|
||||
title: `${operationName} failed`,
|
||||
description: result.error || "An error occurred",
|
||||
title: t("common:gitOperationFailed", { operation: operationName }),
|
||||
description: result.error || t("common:anErrorOccurred"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
updateToast(toastId, {
|
||||
title: `${operationName} failed`,
|
||||
description: e instanceof Error ? e.message : "An unexpected error occurred",
|
||||
title: t("common:gitOperationFailed", { operation: operationName }),
|
||||
description: e instanceof Error ? e.message : t("common:anUnexpectedErrorOccurred"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
[toast, updateToast],
|
||||
[t, toast, updateToast],
|
||||
);
|
||||
|
||||
return run;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useState } from "react";
|
||||
import { executeUtilityPrompt } from "@/lib/api/domains/utility-api";
|
||||
import { listTaskSessionMessages } from "@/lib/api/domains/session-api";
|
||||
import type { Message } from "@/lib/types/http";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type SummarizeSessionResult = {
|
||||
summary: string | null;
|
||||
@@ -39,7 +40,7 @@ export function useSummarizeSession() {
|
||||
conversation_history: transcript,
|
||||
});
|
||||
if (!result.success) {
|
||||
return { summary: null, error: result.error || "Summarize utility returned no result" };
|
||||
return { summary: null, error: result.error || t("task:summarizeReturnedNoResult") };
|
||||
}
|
||||
return { summary: result.response ?? null };
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { nativeNotifications } from "@/lib/desktop/native-notification-client";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export function useUpdateAvailableToast() {
|
||||
const notification = useAppStore((s) => s.updateAvailableNotification);
|
||||
@@ -19,7 +20,7 @@ export function useUpdateAvailableToast() {
|
||||
}
|
||||
shownRef.current.add(notification.version);
|
||||
|
||||
const title = notification.title || "Kandev update available";
|
||||
const title = notification.title || t("task:kandevUpdateAvailable");
|
||||
const body =
|
||||
notification.body ||
|
||||
`Kandev ${notification.version} is available. Open Settings > System > Updates to review it.`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { executeUtilityPrompt, type ExecutePromptRequest } from "@/lib/api/domains/utility-api";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { useSessionGitStatus } from "@/hooks/domains/session/use-session-git-status";
|
||||
@@ -52,6 +53,7 @@ export function useUtilityAgentGenerator({
|
||||
taskDescription,
|
||||
}: UseUtilityAgentGeneratorOptions) {
|
||||
const [generating, setGenerating] = useState<Set<GeneratorType>>(new Set());
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const gitStatus = useSessionGitStatus(sessionId);
|
||||
|
||||
@@ -103,8 +105,8 @@ export function useUtilityAgentGenerator({
|
||||
// Other generators need git context from an active session.
|
||||
if (!sessionId && type !== ENHANCE_PROMPT) {
|
||||
toast({
|
||||
title: "No active session",
|
||||
description: "Start a session first to use AI generation",
|
||||
title: t("task:noActiveSession"),
|
||||
description: t("task:noActiveSessionDescription"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
@@ -115,8 +117,8 @@ export function useUtilityAgentGenerator({
|
||||
const resp = await executeUtilityPrompt(buildRequest(type, options));
|
||||
if (!resp.success || !resp.response) {
|
||||
toast({
|
||||
title: "Generation failed",
|
||||
description: resp.error || "Failed to generate content",
|
||||
title: t("task:generationFailed"),
|
||||
description: resp.error || t("task:failedToGenerateContent"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
@@ -128,15 +130,15 @@ export function useUtilityAgentGenerator({
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Generation failed",
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
title: t("task:generationFailed"),
|
||||
description: error instanceof Error ? error.message : t("task:unknownError"),
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
clearType(type);
|
||||
}
|
||||
},
|
||||
[sessionId, buildRequest, clearType, toast],
|
||||
[sessionId, buildRequest, clearType, t, toast],
|
||||
);
|
||||
|
||||
return useGeneratorCallbacks(generate, generating);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import i18n from "i18next";
|
||||
import type { AgentUpdateJob, AgentUpdatePreview } from "@/lib/api";
|
||||
import { canApproveAgentRuntimeUpdate, resolveRuntimeVersionPair } from "./agent-runtime-update";
|
||||
|
||||
@@ -27,12 +28,18 @@ describe("resolveRuntimeVersionPair", () => {
|
||||
preview(),
|
||||
job({ current_version: "0.63.0", target_version: "0.63.0" }),
|
||||
),
|
||||
).toEqual({ currentVersion: "0.63.0", targetVersion: "0.63.0", versionsMatch: true });
|
||||
).toEqual({
|
||||
currentVersion: "0.63.0",
|
||||
hasCurrentVersion: true,
|
||||
targetVersion: "0.63.0",
|
||||
versionsMatch: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to preview versions before a job exists", () => {
|
||||
expect(resolveRuntimeVersionPair(preview())).toEqual({
|
||||
currentVersion: "0.62.0",
|
||||
hasCurrentVersion: true,
|
||||
targetVersion: "0.63.0",
|
||||
versionsMatch: false,
|
||||
});
|
||||
@@ -69,3 +76,50 @@ describe("canApproveAgentRuntimeUpdate", () => {
|
||||
expect(canApproveAgentRuntimeUpdate(state)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The missing-version predicate used to be `currentVersion !== "Unknown"`, a
|
||||
* comparison against the English placeholder. Once `currentVersion` became
|
||||
* localized, that test was true in every other locale, so a missing version read
|
||||
* as known and the approval gate opened. These pin the state to a flag and the
|
||||
* string to display only.
|
||||
*/
|
||||
describe("missing current version under a non-English locale", () => {
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
const missing = {
|
||||
preview: preview({ current_version: "" }),
|
||||
previewError: null,
|
||||
loading: false,
|
||||
updateInFlight: false,
|
||||
starting: false,
|
||||
installInFlight: false,
|
||||
};
|
||||
|
||||
it.each(["en", "pt-pt", "zh-cn", "pseudo"])("stays un-approvable in %s", async (locale) => {
|
||||
await i18n.changeLanguage(locale);
|
||||
|
||||
const pair = resolveRuntimeVersionPair(preview({ current_version: "" }));
|
||||
expect(pair.hasCurrentVersion).toBe(false);
|
||||
expect(canApproveAgentRuntimeUpdate(missing)).toBe(false);
|
||||
});
|
||||
|
||||
it("still shows a localized placeholder to the user", async () => {
|
||||
await i18n.changeLanguage("pseudo");
|
||||
const pair = resolveRuntimeVersionPair(preview({ current_version: "" }));
|
||||
|
||||
expect(pair.currentVersion).not.toBe("Unknown");
|
||||
expect(pair.currentVersion).toMatch(/[^\x20-\x7E]/);
|
||||
});
|
||||
|
||||
it("does not treat a localized placeholder as matching a target version", async () => {
|
||||
await i18n.changeLanguage("pt-pt");
|
||||
const pair = resolveRuntimeVersionPair(
|
||||
preview({ current_version: "", target_version: "0.63.0" }),
|
||||
);
|
||||
|
||||
expect(pair.versionsMatch).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import type { AgentUpdateJob, AgentUpdatePreview } from "@/lib/api";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type RuntimeVersionPair = {
|
||||
/** Display copy — localized, and therefore never compared. */
|
||||
currentVersion: string;
|
||||
/**
|
||||
* Whether a real version was reported. The predicate this replaced was
|
||||
* `currentVersion !== "Unknown"`, which compared against the English
|
||||
* placeholder; once `currentVersion` became localized that test was true in
|
||||
* every other locale, so a missing version read as known and the update
|
||||
* gate opened. docs/i18n.md calls this out under "Do not translate" — the
|
||||
* flag is the state, the string is only for the user.
|
||||
*/
|
||||
hasCurrentVersion: boolean;
|
||||
targetVersion?: string;
|
||||
versionsMatch: boolean;
|
||||
};
|
||||
@@ -10,12 +21,11 @@ export function resolveRuntimeVersionPair(
|
||||
preview: AgentUpdatePreview | null,
|
||||
job?: AgentUpdateJob,
|
||||
): RuntimeVersionPair {
|
||||
const currentVersion = job?.current_version || preview?.current_version || "Unknown";
|
||||
const reported = job?.current_version || preview?.current_version;
|
||||
const currentVersion = reported || t("common:unknown");
|
||||
const targetVersion = job?.target_version || preview?.target_version;
|
||||
const versionsMatch = Boolean(
|
||||
targetVersion && currentVersion !== "Unknown" && currentVersion === targetVersion,
|
||||
);
|
||||
return { currentVersion, targetVersion, versionsMatch };
|
||||
const versionsMatch = Boolean(targetVersion && reported && reported === targetVersion);
|
||||
return { currentVersion, hasCurrentVersion: Boolean(reported), targetVersion, versionsMatch };
|
||||
}
|
||||
|
||||
export function canApproveAgentRuntimeUpdate({
|
||||
@@ -35,11 +45,12 @@ export function canApproveAgentRuntimeUpdate({
|
||||
starting: boolean;
|
||||
installInFlight: boolean;
|
||||
}): boolean {
|
||||
const { currentVersion, targetVersion } = resolveRuntimeVersionPair(preview, job);
|
||||
const { currentVersion, hasCurrentVersion, targetVersion } = resolveRuntimeVersionPair(
|
||||
preview,
|
||||
job,
|
||||
);
|
||||
return (
|
||||
Boolean(
|
||||
preview && currentVersion !== "Unknown" && targetVersion && currentVersion !== targetVersion,
|
||||
) &&
|
||||
Boolean(preview && hasCurrentVersion && targetVersion && currentVersion !== targetVersion) &&
|
||||
!previewError &&
|
||||
!loading &&
|
||||
!updateInFlight &&
|
||||
|
||||
@@ -35,6 +35,9 @@ export async function createTaskPlan(
|
||||
}
|
||||
const response = await client.request("task.plan.create", {
|
||||
task_id: taskId,
|
||||
// Canonical English on purpose: the backend persists this into
|
||||
// `task_plans.title` / `task_plan_revisions.title`, so a localized default
|
||||
// would freeze whichever locale created the plan into the stored record.
|
||||
title: title || "Plan",
|
||||
content,
|
||||
created_by: "user",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IconAlertTriangle, IconLock } from "@tabler/icons-react";
|
||||
import type { CapabilityStatus } from "@/lib/types/http";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type CapabilityWarning = {
|
||||
Icon: typeof IconLock;
|
||||
@@ -16,19 +17,19 @@ export function getCapabilityWarning(
|
||||
return {
|
||||
Icon: IconLock,
|
||||
color: "text-amber-600 dark:text-amber-400",
|
||||
title: error || "Authentication required",
|
||||
title: error || t("agents:authenticationRequired"),
|
||||
};
|
||||
case "not_installed":
|
||||
return {
|
||||
Icon: IconAlertTriangle,
|
||||
color: "text-muted-foreground",
|
||||
title: error || "Agent CLI not installed",
|
||||
title: error || t("task:agentCliNotInstalled"),
|
||||
};
|
||||
case "failed":
|
||||
return {
|
||||
Icon: IconAlertTriangle,
|
||||
color: "text-red-600 dark:text-red-400",
|
||||
title: error || "Agent probe failed",
|
||||
title: error || t("task:agentProbeFailed"),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GitHubStatus } from "@/lib/types/github";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export function hasGitHubPersonalActor(status: GitHubStatus | null): boolean {
|
||||
const actor = status?.effective_personal_actor;
|
||||
@@ -18,7 +19,7 @@ export function getGitHubPersonalIdentityState(status: GitHubStatus): GitHubPers
|
||||
active && principal?.source === "github_app_user" && status.personal?.status === "active",
|
||||
);
|
||||
let actor = "Unavailable";
|
||||
if (active) actor = principal?.login ?? "Unavailable";
|
||||
if (active) actor = principal?.login ?? t("common:unavailable");
|
||||
else if (status.automation?.source === "github_app_installation") actor = "Not connected";
|
||||
return { active, actor, personalOAuthActive };
|
||||
}
|
||||
|
||||
@@ -16,34 +16,69 @@ export type BuiltInLayoutProfileId = Exclude<BuiltInPreset, "compact">;
|
||||
|
||||
export type BuiltInLayoutProfileDescriptor = {
|
||||
id: BuiltInLayoutProfileId;
|
||||
/** Canonical English; persisted into a saved override. Not for display. */
|
||||
name: string;
|
||||
/** Catalog key the settings list renders. Absent for product names. */
|
||||
nameKey?: string;
|
||||
description: string;
|
||||
descriptionKey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display name for a built-in profile, in the active locale.
|
||||
*
|
||||
* `descriptor.name` is the canonical English value `upsertBuiltInLayoutOverride`
|
||||
* persists; every surface that shows the name to a user goes through here so the
|
||||
* two cannot drift.
|
||||
*/
|
||||
export function builtInLayoutProfileName(
|
||||
descriptor: Pick<BuiltInLayoutProfileDescriptor, "name" | "nameKey">,
|
||||
translate: (key: string) => string,
|
||||
): string {
|
||||
return descriptor.nameKey ? translate(descriptor.nameKey) : descriptor.name;
|
||||
}
|
||||
|
||||
export type BuiltInLayoutProfile = BuiltInLayoutProfileDescriptor & {
|
||||
layout: LayoutState;
|
||||
};
|
||||
|
||||
/**
|
||||
* `name` is BOTH display copy and persisted data: `upsertBuiltInLayoutOverride`
|
||||
* copies it into the saved record the first time a built-in is customized. It
|
||||
* therefore stays canonical English, and `nameKey`/`descriptionKey` are what the
|
||||
* settings list renders — the same split the dockview panel registry uses. A
|
||||
* translated `name` here would write the current locale into a user's saved
|
||||
* layouts and leave it there after a switch.
|
||||
*
|
||||
* "VS Code" is a product name and so has no `nameKey`.
|
||||
*/
|
||||
export const BUILT_IN_LAYOUT_PROFILES: readonly BuiltInLayoutProfileDescriptor[] = [
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
nameKey: "settings:layoutProfileDefault",
|
||||
description: "Agent with Files, Changes, and Terminal",
|
||||
descriptionKey: "settings:layoutProfileDefaultDescription",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
name: "Plan Mode",
|
||||
nameKey: "settings:layoutProfilePlanMode",
|
||||
description: "Agent and Plan side by side",
|
||||
descriptionKey: "settings:layoutProfilePlanModeDescription",
|
||||
},
|
||||
{
|
||||
id: "preview",
|
||||
name: "Preview Mode",
|
||||
nameKey: "settings:layoutProfilePreviewMode",
|
||||
description: "Agent and Browser side by side",
|
||||
descriptionKey: "settings:layoutProfilePreviewModeDescription",
|
||||
},
|
||||
{
|
||||
id: "vscode",
|
||||
name: "VS Code",
|
||||
description: "Agent and VS Code side by side",
|
||||
descriptionKey: "settings:layoutProfileVsCodeDescription",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TaskSessionState, TaskState } from "@/lib/types/http";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export const RECENT_TASKS_STORAGE_KEY = "kandev.recentTasks.v1";
|
||||
export const RECENT_TASKS_CHANGED_EVENT = "kandev:recent-tasks-changed";
|
||||
@@ -33,7 +34,7 @@ function normalizeEntry(value: unknown): RecentTaskEntry | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const taskId = optionalString(value.taskId);
|
||||
if (!taskId) return null;
|
||||
const title = optionalString(value.title) ?? "Untitled task";
|
||||
const title = optionalString(value.title) ?? t("common:untitledTask");
|
||||
const visitedAt = optionalString(value.visitedAt) ?? new Date(0).toISOString();
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { DockviewApi, DockviewGroupPanel } from "dockview-react";
|
||||
import type { CommitDetailTarget } from "@/components/task/changes-diff-target";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { focusOrAddPanel } from "./dockview-layout-builders";
|
||||
import { TERMINAL_DEFAULT_ID } from "./layout-manager/constants";
|
||||
import { panelTitle } from "./layout-manager/panel-title";
|
||||
import {
|
||||
parsePluginPanelId,
|
||||
pluginPanelId,
|
||||
@@ -365,7 +367,7 @@ function buildFileDiffAction(get: StoreGet) {
|
||||
api,
|
||||
type: "file-diff",
|
||||
itemId,
|
||||
title: `Diff [${getFileName(path)}]`,
|
||||
title: t("task:panelFileDiff", { file: getFileName(path) }),
|
||||
params: {
|
||||
kind: "file",
|
||||
path,
|
||||
@@ -439,7 +441,7 @@ export function buildPanelActions(set: StoreSet, get: StoreGet) {
|
||||
id: "chat",
|
||||
component: "chat",
|
||||
tabComponent: "permanentTab",
|
||||
title: "Agent",
|
||||
title: panelTitle("chat"),
|
||||
position: { referenceGroup: centerGroupId },
|
||||
});
|
||||
},
|
||||
@@ -449,7 +451,7 @@ export function buildPanelActions(set: StoreSet, get: StoreGet) {
|
||||
addSimplePanel(api, groupId ?? rightTopGroupId, {
|
||||
id: "changes",
|
||||
component: "changes",
|
||||
title: "Changes",
|
||||
title: panelTitle("changes"),
|
||||
tabComponent: "changesTab",
|
||||
});
|
||||
},
|
||||
@@ -459,7 +461,7 @@ export function buildPanelActions(set: StoreSet, get: StoreGet) {
|
||||
addSimplePanel(api, groupId ?? rightTopGroupId, {
|
||||
id: "files",
|
||||
component: "files",
|
||||
title: "Files",
|
||||
title: panelTitle("files"),
|
||||
});
|
||||
},
|
||||
addDiffViewerPanel: (path?: string, content?: string, groupId?: string) => {
|
||||
@@ -469,7 +471,7 @@ export function buildPanelActions(set: StoreSet, get: StoreGet) {
|
||||
addSimplePanel(api, groupId ?? centerGroupId, {
|
||||
id: "diff-viewer",
|
||||
component: "diff-viewer",
|
||||
title: "Diff Viewer",
|
||||
title: t("task:panelDiffViewer"),
|
||||
params: { kind: "all" },
|
||||
});
|
||||
},
|
||||
@@ -483,7 +485,7 @@ export function buildPanelActions(set: StoreSet, get: StoreGet) {
|
||||
addSimplePanel(api, groupId ?? centerGroupId, {
|
||||
id: browserId,
|
||||
component: "browser",
|
||||
title: t("task:browser"),
|
||||
title: panelTitle("browser"),
|
||||
params: { url: url ?? "" },
|
||||
});
|
||||
},
|
||||
@@ -549,7 +551,7 @@ function buildReviewPanelActions(get: StoreGet) {
|
||||
focusOrAddPanel(api, {
|
||||
id,
|
||||
component: "pr-detail",
|
||||
title: prKey ? "Pull Request" : "PR Details",
|
||||
title: prKey ? t("task:panelPullRequest") : panelTitle("pr-detail"),
|
||||
position: { referenceGroup: targetGroupId },
|
||||
params: prKey ? { prKey } : undefined,
|
||||
});
|
||||
@@ -572,7 +574,7 @@ function buildReviewPanelActions(get: StoreGet) {
|
||||
focusOrAddPanel(api, {
|
||||
id,
|
||||
component: "mr-detail",
|
||||
title: "Merge Request",
|
||||
title: panelTitle("mr-detail"),
|
||||
position: { referenceGroup: canonical?.group.id ?? centerGroupId },
|
||||
params: { mrKey },
|
||||
});
|
||||
@@ -588,7 +590,7 @@ export function buildExtraPanelActions(get: StoreGet) {
|
||||
focusOrAddPanel(api, {
|
||||
id: "vscode",
|
||||
component: "vscode",
|
||||
title: "VS Code",
|
||||
title: panelTitle("vscode"),
|
||||
position: { referenceGroup: centerGroupId },
|
||||
});
|
||||
},
|
||||
@@ -603,7 +605,7 @@ export function buildExtraPanelActions(get: StoreGet) {
|
||||
focusOrAddPanel(api, {
|
||||
id: "vscode",
|
||||
component: "vscode",
|
||||
title: "VS Code",
|
||||
title: panelTitle("vscode"),
|
||||
position: { referenceGroup: centerGroupId },
|
||||
});
|
||||
},
|
||||
@@ -613,7 +615,7 @@ export function buildExtraPanelActions(get: StoreGet) {
|
||||
addSidePanel(
|
||||
api,
|
||||
centerGroupId,
|
||||
{ id: "plan", component: "plan", title: "Plan", tabComponent: "planTab" },
|
||||
{ id: "plan", component: "plan", title: panelTitle("plan"), tabComponent: "planTab" },
|
||||
opts,
|
||||
);
|
||||
},
|
||||
@@ -646,7 +648,7 @@ export function buildExtraPanelActions(get: StoreGet) {
|
||||
addSidePanel(
|
||||
api,
|
||||
centerGroupId,
|
||||
{ id: "todos", component: "todos", title: t("common:todos") },
|
||||
{ id: "todos", component: "todos", title: panelTitle("todos") },
|
||||
opts,
|
||||
);
|
||||
},
|
||||
@@ -673,7 +675,7 @@ export function buildExtraPanelActions(get: StoreGet) {
|
||||
// when there's more than one ordinary terminal in the task and
|
||||
// exposes a context menu for rename / park / destroy.
|
||||
tabComponent: "terminalTab",
|
||||
title: title ?? "Terminal",
|
||||
title: title ?? panelTitle(TERMINAL_DEFAULT_ID),
|
||||
params: { terminalId: id, environmentId, taskID },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -64,13 +64,48 @@ export const STRUCTURAL_COMPONENTS = new Set([
|
||||
"plugin-panel",
|
||||
]);
|
||||
|
||||
/** Default panel configurations for known panels. */
|
||||
export const PANEL_REGISTRY: Record<string, Omit<LayoutPanel, "id">> = {
|
||||
chat: { component: "chat", title: "Agent", tabComponent: "permanentTab" },
|
||||
plan: { component: "plan", title: "Plan", tabComponent: "planTab" },
|
||||
changes: { component: "changes", title: "Changes", tabComponent: "changesTab" },
|
||||
files: { component: "files", title: "Files" },
|
||||
browser: { component: "browser", title: "Browser", params: { url: "" } },
|
||||
/**
|
||||
* Default panel configurations for known panels.
|
||||
*
|
||||
* `title` and `titleKey` are deliberately two different things:
|
||||
*
|
||||
* - `title` is the CANONICAL, always-English value. `toSerializedDockview`
|
||||
* writes it into the stored layout JSON, so it has to be locale-independent
|
||||
* — otherwise the locale a layout happened to be saved in is frozen into it
|
||||
* and survives every later switch.
|
||||
* - `titleKey` is what the user reads. `panelTitle()` resolves it whenever a
|
||||
* panel is built or a saved layout is restored, so the tab always shows the
|
||||
* ACTIVE locale regardless of which one wrote the layout.
|
||||
*
|
||||
* This is the key/persisted-value split docs/i18n.md prescribes for a display
|
||||
* string that is also stored. Before it, two of these titles were translated at
|
||||
* creation (`t("task:browser")`, `t("common:todos")`) and the rest were not, so
|
||||
* the tab bar was half-translated AND persisted whichever locale created it.
|
||||
*
|
||||
* `vscode` has no `titleKey` on purpose: "VS Code" is a product name, and
|
||||
* `panelTitle()` falls back to `title` when a key is absent.
|
||||
*/
|
||||
export const PANEL_REGISTRY: Record<string, Omit<LayoutPanel, "id"> & { titleKey?: string }> = {
|
||||
chat: {
|
||||
component: "chat",
|
||||
title: "Agent",
|
||||
titleKey: "task:panelAgent",
|
||||
tabComponent: "permanentTab",
|
||||
},
|
||||
plan: { component: "plan", title: "Plan", titleKey: "task:panelPlan", tabComponent: "planTab" },
|
||||
changes: {
|
||||
component: "changes",
|
||||
title: "Changes",
|
||||
titleKey: "task:panelChanges",
|
||||
tabComponent: "changesTab",
|
||||
},
|
||||
files: { component: "files", title: "Files", titleKey: "task:panelFiles" },
|
||||
browser: {
|
||||
component: "browser",
|
||||
title: "Browser",
|
||||
titleKey: "task:browser",
|
||||
params: { url: "" },
|
||||
},
|
||||
vscode: { component: "vscode", title: "VS Code" },
|
||||
[TERMINAL_DEFAULT_ID]: {
|
||||
component: "terminal",
|
||||
@@ -81,18 +116,58 @@ export const PANEL_REGISTRY: Record<string, Omit<LayoutPanel, "id">> = {
|
||||
// session-page mount so the badge logic has a seq to display.
|
||||
tabComponent: "terminalTab",
|
||||
title: "Terminal",
|
||||
titleKey: "task:panelTerminal",
|
||||
params: { terminalId: "shell-default" },
|
||||
},
|
||||
"pr-detail": { component: "pr-detail", title: "PR Details" },
|
||||
"mr-detail": { component: "mr-detail", title: "Merge Request" },
|
||||
todos: { component: "todos", title: "Todos" },
|
||||
"pr-detail": { component: "pr-detail", title: "PR Details", titleKey: "task:panelPrDetails" },
|
||||
"mr-detail": {
|
||||
component: "mr-detail",
|
||||
title: "Merge Request",
|
||||
titleKey: "task:panelMergeRequest",
|
||||
},
|
||||
todos: { component: "todos", title: "Todos", titleKey: "common:todos" },
|
||||
};
|
||||
|
||||
/** Create a LayoutPanel from the registry by ID. */
|
||||
/**
|
||||
* `panelTitle()` — the locale-resolved counterpart of `canonicalPanelTitle()` —
|
||||
* lives in `./panel-title.ts`. It needs `@/lib/i18n`, and this module must stay
|
||||
* loadable from plain Node because e2e specs import constants from it; see that
|
||||
* file's comment.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registry entry for a panel.
|
||||
*
|
||||
* Falls back to the COMPONENT when the id is not an exact key, because several
|
||||
* panel families mint dynamic ids from their component — `browser:<url>`,
|
||||
* `pr-detail|<key>`, `mr-detail|<key>`. Matching on the id alone left those
|
||||
* outside the canonical/localized split entirely: they were created with a
|
||||
* localized title, captured verbatim into the saved layout, and then could not
|
||||
* be re-localized on restore, so the layout showed whichever locale created it.
|
||||
*/
|
||||
export function panelRegistryEntry(
|
||||
id: string,
|
||||
component?: string,
|
||||
): (typeof PANEL_REGISTRY)[string] | undefined {
|
||||
return PANEL_REGISTRY[id] ?? (component ? PANEL_REGISTRY[component] : undefined);
|
||||
}
|
||||
|
||||
/** The canonical English title stored in a saved layout, if the panel is known. */
|
||||
export function canonicalPanelTitle(id: string, component?: string): string | undefined {
|
||||
return panelRegistryEntry(id, component)?.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a LayoutPanel from the registry by ID.
|
||||
*
|
||||
* The title is canonical English: a `LayoutPanel` is the shape that gets
|
||||
* persisted, and `toSerializedDockview` localizes it when it reaches dockview.
|
||||
*/
|
||||
export function panel(id: string): LayoutPanel {
|
||||
const config = PANEL_REGISTRY[id];
|
||||
if (!config) throw new Error(`Unknown panel: ${id}`);
|
||||
return { id, ...config };
|
||||
const { titleKey: _titleKey, ...rest } = config;
|
||||
return { id, ...rest };
|
||||
}
|
||||
|
||||
/** Generate panel config for a session tab. */
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
import { panelRegistryEntry } from "./constants";
|
||||
|
||||
/**
|
||||
* The display title for a registry panel, in the active locale.
|
||||
*
|
||||
* Deliberately NOT in `constants.ts`. That module is imported by e2e specs
|
||||
* (`e2e/tests/layout/pane-resize-right.spec.ts` takes `TERMINAL_DEFAULT_ID` from
|
||||
* it), and Playwright loads spec files through Node rather than Vite. Pulling
|
||||
* `@/lib/i18n` into that graph makes the whole file fail to load with
|
||||
* `SyntaxError: Cannot use 'import.meta' outside a module` — and one unloadable
|
||||
* spec aborts the entire run, so it takes every shard down with it, not just
|
||||
* the layout tests. Keeping the i18n dependency in a separate module leaves
|
||||
* `constants.ts` importable from plain Node.
|
||||
*
|
||||
* Resolved at call time, never at module load — the registry stores the key.
|
||||
* Unknown ids (file diffs, plugin panels, per-session chat tabs) carry their own
|
||||
* title and are returned unchanged.
|
||||
*
|
||||
* This is the RENDER direction only. `LayoutState` — what gets saved — always
|
||||
* holds the canonical English title, so the localized form exists solely inside
|
||||
* dockview. `toSerializedDockview` localizes on the way in and
|
||||
* `panelFromDockviewPanel` canonicalizes on the way back out.
|
||||
*/
|
||||
export function panelTitle(id: string, fallback?: string, component?: string): string {
|
||||
const config = panelRegistryEntry(id, component);
|
||||
if (!config) return fallback ?? id;
|
||||
return config.titleKey ? t(config.titleKey) : config.title;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import i18n from "i18next";
|
||||
|
||||
import { canonicalPanelTitle, panel, PANEL_REGISTRY } from "./constants";
|
||||
import { panelTitle } from "./panel-title";
|
||||
import { toSerializedDockview } from "./serializer";
|
||||
import type { LayoutState } from "./types";
|
||||
|
||||
/**
|
||||
* Panel titles are BOTH display copy and persisted data: `toSerializedDockview`
|
||||
* writes them into the stored layout JSON. That is why the registry keeps the
|
||||
* two apart — `title` is canonical English for storage, `titleKey` resolves at
|
||||
* render for the tab bar.
|
||||
*
|
||||
* Before the split, two titles were translated at creation and the rest were
|
||||
* not, so a saved layout captured whichever locale created it and kept showing
|
||||
* it after a switch.
|
||||
*/
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
function layoutWith(panelIds: string[]): LayoutState {
|
||||
return {
|
||||
columns: [{ id: "col", groups: [{ id: "group", panels: panelIds.map(panel) }] }],
|
||||
} as unknown as LayoutState;
|
||||
}
|
||||
|
||||
describe("panelTitle", () => {
|
||||
it("resolves a registry panel through the catalog", async () => {
|
||||
expect(panelTitle("changes")).toBe("Changes");
|
||||
|
||||
await i18n.changeLanguage("pseudo");
|
||||
const pseudo = panelTitle("changes");
|
||||
|
||||
expect(pseudo).not.toBe("Changes");
|
||||
expect(pseudo).toMatch(/[^\x20-\x7E]/);
|
||||
});
|
||||
|
||||
it("falls back to the given title for a panel the registry does not know", () => {
|
||||
expect(panelTitle("diff:src/app.ts", "Diff [app.ts]")).toBe("Diff [app.ts]");
|
||||
});
|
||||
|
||||
const BROWSER_PANEL_ID = "browser:https://example.com";
|
||||
|
||||
it("resolves a dynamic id through its component", async () => {
|
||||
// `browser:<url>`, `pr-detail|<key>` and `mr-detail|<key>` are minted per
|
||||
// instance, so an exact-id lookup misses them. Without the component
|
||||
// fallback they were created localized, captured verbatim into the saved
|
||||
// layout, and could not be re-localized on restore — the layout then showed
|
||||
// whichever locale had created it.
|
||||
expect(panelTitle(BROWSER_PANEL_ID, undefined, "browser")).toBe("Browser");
|
||||
expect(canonicalPanelTitle(BROWSER_PANEL_ID, "browser")).toBe("Browser");
|
||||
expect(canonicalPanelTitle("mr-detail|kandev!7", "mr-detail")).toBe("Merge Request");
|
||||
|
||||
await i18n.changeLanguage("pseudo");
|
||||
expect(panelTitle(BROWSER_PANEL_ID, undefined, "browser")).not.toBe("Browser");
|
||||
// Storage stays canonical whatever the locale.
|
||||
expect(canonicalPanelTitle(BROWSER_PANEL_ID, "browser")).toBe("Browser");
|
||||
});
|
||||
|
||||
it("leaves a product name untranslated", async () => {
|
||||
await i18n.changeLanguage("pseudo");
|
||||
expect(panelTitle("vscode")).toBe("VS Code");
|
||||
});
|
||||
|
||||
it("gives every registry panel a title that is not the raw key", () => {
|
||||
const leaked = Object.keys(PANEL_REGISTRY).filter((id) => panelTitle(id).includes(":"));
|
||||
expect(leaked).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("layout round trip", () => {
|
||||
const titlesOf = (state: LayoutState) => {
|
||||
const serialized = toSerializedDockview(state, 1000, 800, new Map());
|
||||
return (serialized as unknown as { panels: Record<string, { title: string }> }).panels;
|
||||
};
|
||||
|
||||
it("localizes on the way into dockview", async () => {
|
||||
// `toSerializedDockview` feeds `api.fromJSON`, so this is what the tab bar
|
||||
// and the layout editor's preview render — it must follow the locale. The
|
||||
// first version of this split had the two directions the wrong way round,
|
||||
// which the pseudo-coverage oracle caught as English tab titles on the
|
||||
// layouts screen while every unit test passed.
|
||||
await i18n.changeLanguage("pseudo");
|
||||
const stored = titlesOf(layoutWith(["chat", "changes", "browser"]));
|
||||
|
||||
for (const title of ["chat", "changes", "browser"].map((id) => stored[id].title)) {
|
||||
expect(title).toMatch(/[^\x20-\x7E]/);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a stored LayoutState in canonical English", () => {
|
||||
// `panel()` builds the shape that gets persisted, so it must NOT localize —
|
||||
// otherwise the locale that created a layout is frozen into it.
|
||||
const built = layoutWith(["chat", "changes", "browser"]).columns[0].groups[0].panels;
|
||||
expect(built.map((p) => p.title)).toEqual(["Agent", "Changes", "Browser"]);
|
||||
});
|
||||
|
||||
it("builds the same canonical LayoutState in any locale", async () => {
|
||||
const english = layoutWith(["chat", "changes", "browser"]);
|
||||
await i18n.changeLanguage("pseudo");
|
||||
const pseudo = layoutWith(["chat", "changes", "browser"]);
|
||||
|
||||
expect(pseudo).toEqual(english);
|
||||
});
|
||||
|
||||
it("keeps the canonical title for every registry panel", () => {
|
||||
for (const id of Object.keys(PANEL_REGISTRY)) {
|
||||
expect(canonicalPanelTitle(id)).toBe(PANEL_REGISTRY[id].title);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,17 @@
|
||||
import type { DockviewApi, SerializedDockview } from "dockview-react";
|
||||
import type { LayoutState, LayoutColumn, LayoutGroup, LayoutPanel, LayoutNode } from "./types";
|
||||
import { computeColumnWidths, computeGroupHeights } from "./sizing";
|
||||
import { SIDEBAR_LOCK, KNOWN_PANEL_IDS, STRUCTURAL_COMPONENTS } from "./constants";
|
||||
import {
|
||||
SIDEBAR_LOCK,
|
||||
KNOWN_PANEL_IDS,
|
||||
STRUCTURAL_COMPONENTS,
|
||||
TERMINAL_DEFAULT_ID,
|
||||
canonicalPanelTitle,
|
||||
} from "./constants";
|
||||
import { panelTitle } from "./panel-title";
|
||||
|
||||
/** Canonical English, because `normalizePanel` produces a stored LayoutState. */
|
||||
const TERMINAL_CANONICAL_TITLE = canonicalPanelTitle(TERMINAL_DEFAULT_ID) ?? "Terminal";
|
||||
|
||||
// Dockview serialized grid node types (internal format)
|
||||
type SerializedLeafNode = {
|
||||
@@ -137,7 +147,12 @@ function serializePanels(state: LayoutState): Record<
|
||||
panels[panel.id] = {
|
||||
id: panel.id,
|
||||
contentComponent: panel.component,
|
||||
title: panel.title,
|
||||
// RENDER direction: this feeds `api.fromJSON`, so the tab the user
|
||||
// reads is resolved here, in the active locale. `LayoutState` itself
|
||||
// keeps the canonical English title — see `panelFromDockviewPanel`,
|
||||
// which canonicalizes again on capture so a translated title can
|
||||
// never reach storage.
|
||||
title: panelTitle(panel.id, panel.title, panel.component),
|
||||
...(panel.tabComponent ? { tabComponent: panel.tabComponent } : {}),
|
||||
...(panel.params ? { params: panel.params } : {}),
|
||||
};
|
||||
@@ -184,7 +199,9 @@ function panelFromDockviewPanel(panel: any): LayoutPanel {
|
||||
return {
|
||||
id: panel.id,
|
||||
component: panel.view?.contentComponent ?? panel.id,
|
||||
title: panel.title ?? panel.id,
|
||||
// CAPTURE direction: dockview holds the localized title, and this result is
|
||||
// what gets persisted, so a registry panel goes back to canonical English.
|
||||
title: canonicalPanelTitle(panel.id, panel.view?.contentComponent) ?? panel.title ?? panel.id,
|
||||
...(panel.view?.tabComponent ? { tabComponent: panel.view.tabComponent } : {}),
|
||||
...(panel.params ? { params: panel.params as Record<string, unknown> } : {}),
|
||||
};
|
||||
@@ -331,7 +348,7 @@ function normalizePanel(
|
||||
// terminals (shell-<uuid>) — preserve id + params so the live PTY
|
||||
// and store record match on restore. Coerce presentation in case
|
||||
// the saved layout predates the custom tab + plain title.
|
||||
return { ...p, tabComponent: "terminalTab", title: "Terminal" };
|
||||
return { ...p, tabComponent: "terminalTab", title: TERMINAL_CANONICAL_TITLE };
|
||||
}
|
||||
// Legacy ephemeral terminal id (e.g. `terminal-saved-1` from older
|
||||
// layouts) — mint a fresh id so it reconnects to a new PTY.
|
||||
@@ -341,16 +358,20 @@ function normalizePanel(
|
||||
id,
|
||||
params: { terminalId: id },
|
||||
tabComponent: "terminalTab",
|
||||
title: "Terminal",
|
||||
title: TERMINAL_CANONICAL_TITLE,
|
||||
};
|
||||
}
|
||||
|
||||
if (KNOWN_PANEL_IDS.has(p.id)) return p;
|
||||
// A layout saved before this split (or by an older build) can carry a
|
||||
// translated title; normalise it back to canonical English so the render
|
||||
// direction is the only place a locale is applied.
|
||||
if (KNOWN_PANEL_IDS.has(p.id))
|
||||
return { ...p, title: canonicalPanelTitle(p.id, p.component) ?? p.title };
|
||||
|
||||
if (p.component === "browser") {
|
||||
const url = (p.params?.url as string) ?? "";
|
||||
const id = url ? `browser:${url}` : `browser-saved-${++counters.browser}`;
|
||||
return { ...p, id, params: { url } };
|
||||
return { ...p, id, params: { url }, title: canonicalPanelTitle(id, p.component) ?? p.title };
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
isPRFeedbackComment,
|
||||
isWalkthroughComment,
|
||||
} from "./types";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
/**
|
||||
* Format diff review comments as human-readable markdown for sending to agent.
|
||||
@@ -109,7 +110,7 @@ export function formatWalkthroughCommentsAsMarkdown(comments: WalkthroughComment
|
||||
for (const c of comments) {
|
||||
const lineRange = c.startLine === c.endLine ? `${c.startLine}` : `${c.startLine}-${c.endLine}`;
|
||||
const repoPrefix = c.repo ? `${c.repo}/` : "";
|
||||
const title = c.walkthroughTitle || "Walkthrough";
|
||||
const title = c.walkthroughTitle || t("common:walkthrough");
|
||||
|
||||
lines.push(`**${title} · Step ${c.stepIndex + 1} / ${c.stepCount}**`);
|
||||
lines.push(`**${repoPrefix}${c.filePath}:${lineRange}**`);
|
||||
|
||||
@@ -14,6 +14,32 @@ export function createDefaultSidebarView(id: string, name: string): SidebarView
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog key for the built-in view's name.
|
||||
*
|
||||
* `SidebarView.name` is persisted and user-editable (the view manager renames
|
||||
* it in place), so the built-in's name stays canonical English in state and the
|
||||
* surfaces that display it resolve this key instead — the same key/persisted-
|
||||
* value split the layout profiles and dockview panel titles use. Translating
|
||||
* `name` here would write the current locale into the user's synced views.
|
||||
*/
|
||||
export const DEFAULT_VIEW_NAME_KEY = "sidebar:viewAllTasks";
|
||||
|
||||
export const DEFAULT_VIEW: SidebarView = createDefaultSidebarView(DEFAULT_VIEW_ID, "All tasks");
|
||||
|
||||
/**
|
||||
* Display name for a view.
|
||||
*
|
||||
* Only the built-in that still carries its canonical name resolves through the
|
||||
* catalog. The built-in is renameable like any other view, so keying off the id
|
||||
* alone would show the translated default over a name the user chose.
|
||||
*/
|
||||
export function sidebarViewName(
|
||||
view: Pick<SidebarView, "id" | "name">,
|
||||
translate: (key: string) => string,
|
||||
): string {
|
||||
const isUnrenamedBuiltIn = view.id === DEFAULT_VIEW_ID && view.name === DEFAULT_VIEW.name;
|
||||
return isUnrenamedBuiltIn ? translate(DEFAULT_VIEW_NAME_KEY) : view.name;
|
||||
}
|
||||
|
||||
export const DEFAULT_ACTIVE_VIEW_ID = DEFAULT_VIEW_ID;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { DEFAULT_VIEW, createDefaultSidebarView, sidebarViewName } from "./sidebar-view-builtins";
|
||||
|
||||
const translate = (key: string) => `«${key}»`;
|
||||
|
||||
/**
|
||||
* `SidebarView.name` is persisted AND user-editable, so the built-in's display
|
||||
* name resolves through the catalog while the stored value stays canonical
|
||||
* English. Keying only off the id would have shown the translated default over
|
||||
* a name the user typed.
|
||||
*/
|
||||
describe("sidebarViewName", () => {
|
||||
it("localizes the built-in view while it keeps its canonical name", () => {
|
||||
expect(sidebarViewName(DEFAULT_VIEW, translate)).toBe("«sidebar:viewAllTasks»");
|
||||
});
|
||||
|
||||
it("keeps a renamed built-in view showing the user's name", () => {
|
||||
const renamed = { ...DEFAULT_VIEW, name: "My stuff" };
|
||||
|
||||
expect(sidebarViewName(renamed, translate)).toBe("My stuff");
|
||||
});
|
||||
|
||||
it("never localizes a user-created view", () => {
|
||||
const custom = createDefaultSidebarView("view-abc", "Blocked work");
|
||||
|
||||
expect(sidebarViewName(custom, translate)).toBe("Blocked work");
|
||||
});
|
||||
|
||||
it("keeps the stored default name canonical English", () => {
|
||||
// What sync writes to the server; translating it would freeze a locale in.
|
||||
expect(DEFAULT_VIEW.name).toBe("All tasks");
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,15 @@ describe("unarchiveToastPayload", () => {
|
||||
expect(payload.description).not.toContain("feat/ok");
|
||||
});
|
||||
|
||||
it("uses the singular form for exactly one unrecoverable branch", () => {
|
||||
const payload = unarchiveToastPayload(
|
||||
response([{ task_id: "t1", repository_id: "r1", branch: "feat/gone", status: "missing" }]),
|
||||
);
|
||||
// Singular and plural now come from `_one`/`_other` rather than from two
|
||||
// ternaries at the call site, so both forms have to keep resolving.
|
||||
expect(payload.description).toContain("Branch feat/gone no longer exists");
|
||||
});
|
||||
|
||||
it("pluralizes when multiple branches are unrecoverable", () => {
|
||||
const payload = unarchiveToastPayload(
|
||||
response([
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { UnarchiveTaskResponse } from "@/lib/api/domains/kanban-api";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type UnarchiveToastPayload = {
|
||||
title: string;
|
||||
@@ -10,19 +11,26 @@ export type UnarchiveToastPayload = {
|
||||
// branch no longer exists anywhere (never pushed, local copy deleted by
|
||||
// archive), the prior work is unrecoverable and the user must be told the
|
||||
// next session starts fresh.
|
||||
//
|
||||
// The missing-branch sentence used to inflect itself
|
||||
// (`${plural ? "Branches" : "Branch"} … no longer ${plural ? "exist" : "exists"}`),
|
||||
// which puts the plural rule at the call site and cannot be translated into a
|
||||
// language whose rules differ from English. `count` plus `_one`/`_other` moves
|
||||
// it into the catalog, where each locale supplies as many forms as it needs.
|
||||
export function unarchiveToastPayload(result: UnarchiveTaskResponse): UnarchiveToastPayload {
|
||||
const missing = (result.recovery ?? []).filter((r) => r.status === "missing");
|
||||
if (missing.length > 0) {
|
||||
const branches = missing.map((r) => r.branch).join(", ");
|
||||
const plural = missing.length > 1;
|
||||
return {
|
||||
title: "Task unarchived",
|
||||
description: `${plural ? "Branches" : "Branch"} ${branches} no longer ${plural ? "exist" : "exists"} locally or on the remote — the next session starts fresh from the base branch.`,
|
||||
title: t("tasks:taskUnarchived"),
|
||||
description: t("tasks:unarchivedBranchesMissing", {
|
||||
count: missing.length,
|
||||
branches: missing.map((r) => r.branch).join(", "),
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Task unarchived",
|
||||
description: "The task has been restored.",
|
||||
title: t("tasks:taskUnarchived"),
|
||||
description: t("tasks:unarchivedTaskRestored"),
|
||||
variant: "success",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createTwoFilesPatch } from "diff";
|
||||
|
||||
import { djb2Hash } from "./hash";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
/**
|
||||
* Generate a unified diff between original and modified content
|
||||
@@ -65,5 +66,5 @@ export function formatDiffStats(additions: number, deletions: number): string {
|
||||
if (deletions > 0) {
|
||||
parts.push(`-${deletions}`);
|
||||
}
|
||||
return parts.join(" ") || "No changes";
|
||||
return parts.join(" ") || t("task:noChanges");
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
"failedToFetchCapabilities": "Failed to fetch capabilities",
|
||||
"modelOptionsAuthenticationRequired": "Sign in to this agent to load model options.",
|
||||
"modelOptionsResolutionFailed": "Model options could not be loaded.",
|
||||
"modelSettings": "Model settings",
|
||||
"resolvingModelOptions": "Loading model options…",
|
||||
"retryModelOptions": "Retry",
|
||||
"failedToLoadCommandPreview": "Failed to load command preview",
|
||||
@@ -186,6 +187,7 @@
|
||||
"searchModes": "Search modes...",
|
||||
"selectAModel": "Select a model...",
|
||||
"selectAModelShort": "Select a model",
|
||||
"selectModel": "Select model...",
|
||||
"sessionEnded": "Session ended.",
|
||||
"sessionEndedWithCode": "Session ended (exit {{code}}).",
|
||||
"sessionUnavailable": "Session is no longer available.",
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
"enabled": "Enabled",
|
||||
"enterValidOrganizationUrl": "Enter a valid organization URL to open its token settings.",
|
||||
"executorProfileId": "Executor profile ID",
|
||||
"failedToLinkPullRequest": "Failed to link Azure DevOps pull request.",
|
||||
"failedToLinkWorkItem": "Failed to link Azure DevOps work item.",
|
||||
"failedToLinkWorkItemNoProject": "Failed to link Azure DevOps work item: project is missing.",
|
||||
"failedToLoadConfig": "Failed to load Azure DevOps config: {{error}}",
|
||||
"failedToLoadQuickActions": "Failed to load quick actions.",
|
||||
"failedToSaveDefaultQueries": "Failed to save default queries",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"activeSubagents_other": "{{count}} subagents running",
|
||||
"activeTasks": "Active Tasks",
|
||||
"aDirectLocalExecutorProfileIsRequired": "A direct local executor profile is required to create and use an empty repository.",
|
||||
"agent": "Agent",
|
||||
"agentDashboard": "agent dashboard",
|
||||
"agentRun": "agent run",
|
||||
"agentRuns": "agent runs",
|
||||
@@ -13,6 +14,9 @@
|
||||
"andMoreFiles_one": "+{{count}} more",
|
||||
"andMoreFiles_other": "+{{count}} more",
|
||||
"anEnhancedPromptIsAvailable": "An enhanced prompt is available.",
|
||||
"anErrorOccurred": "An error occurred",
|
||||
"anUnexpectedErrorOccurred": "An unexpected error occurred",
|
||||
"anUnknownErrorOccurred": "An unknown error occurred",
|
||||
"apply": "Apply",
|
||||
"autoApproveAllPermissions": "Auto-approve all permissions",
|
||||
"automations": "Automations",
|
||||
@@ -36,9 +40,30 @@
|
||||
"cliPassthrough": "CLI passthrough",
|
||||
"close": "Close",
|
||||
"closeEsc": "Close (Esc)",
|
||||
"commandAddBrowserPanel": "Add Browser Panel",
|
||||
"commandAddBrowserPanelKeywords": "browser, preview, web, open browser preview, web preview, app preview",
|
||||
"commandAddChangesPanel": "Add Changes Panel",
|
||||
"commandAddChangesPanelKeywords": "changes, diff, git changes, changed files, source control, git diff, review changes",
|
||||
"commandAddPlanPanel": "Add Plan Panel",
|
||||
"commandAddPlanPanelKeywords": "plan, document, task plan, implementation plan, plan details",
|
||||
"commandAddTerminalPanel": "Add Terminal Panel",
|
||||
"commandAddTerminalPanelKeywords": "terminal, shell, console, new terminal, open terminal, command line",
|
||||
"commandAgentsSettingsKeywords": "agents, agent settings, agent profiles, installed agents, claude",
|
||||
"commandArchiveTaskKeywords": "archive, archive task, archive current task, close task, finish task, put away",
|
||||
"commandCancelTurn": "Cancel Turn",
|
||||
"commandCancelTurnKeywords": "cancel, stop, turn, cancel agent, stop agent, interrupt agent",
|
||||
"commandCommitChanges": "Commit Changes",
|
||||
"commandCommitChangesKeywords": "commit, git, save changes, git commit",
|
||||
"commandConfigChatKeywords": "config chat, config mode, configure kandev, workflow configuration, mcp configuration",
|
||||
"commandCreateFile": "Create File",
|
||||
"commandCreateFileKeywords": "create, new, file, add",
|
||||
"commandCreateFilePlaceholder": "File path relative to workspace root...",
|
||||
"commandCreateNewTask": "Create New Task",
|
||||
"commandCreateNewTaskKeywords": "new, create, task, add",
|
||||
"commandCreatePr": "Create PR",
|
||||
"commandCreatePrKeywords": "pull request, pr, open pull request, submit pull request, git",
|
||||
"commandCreateSubtask": "Create Subtask",
|
||||
"commandCreateSubtaskKeywords": "subtask, create, new subtask, new sub-task, child task",
|
||||
"commandExecutorsSettingsKeywords": "executors, executor profiles, execution environment, environment variables, runtime, compute",
|
||||
"commandGoToAllTasks": "Go to All Tasks",
|
||||
"commandGoToAllTasksKeywords": "tasks, list, all",
|
||||
@@ -51,11 +76,21 @@
|
||||
"commandGoToStats": "Go to Stats",
|
||||
"commandGoToStatsKeywords": "stats, statistics, analytics, metrics",
|
||||
"commandGroupActions": "Actions",
|
||||
"commandGroupAgent": "Agent",
|
||||
"commandGroupCommands": "Commands",
|
||||
"commandGroupGit": "Git",
|
||||
"commandGroupNavigation": "Navigation",
|
||||
"commandGroupPanels": "Panels",
|
||||
"commandGroupPreferences": "Preferences",
|
||||
"commandGroupSettings": "Settings",
|
||||
"commandGroupTasks": "Tasks",
|
||||
"commandGroupView": "View",
|
||||
"commandGroupWorkspace": "Workspace",
|
||||
"commandMergeKeywords": "merge, git, branch",
|
||||
"commandNewAgent": "New Agent",
|
||||
"commandNewAgentKeywords": "new, agent, session, start agent, new session",
|
||||
"commandOpenRecentTaskSwitcher": "Open Recent Task Switcher",
|
||||
"commandOpenRecentTaskSwitcherKeywords": "recent, task, switcher, history, recent tasks, switch task, switch recent tasks",
|
||||
"commandPaletteMode": "Command palette mode",
|
||||
"commandPreviewAddTerminalPanel": "Add Terminal Panel",
|
||||
"commandPreviewCommitChanges": "Commit Changes",
|
||||
@@ -63,10 +98,19 @@
|
||||
"commandPreviewPull": "Pull",
|
||||
"commandPreviewSearchTasks": "Search Tasks",
|
||||
"commandPromptsSettingsKeywords": "prompts, prompt settings, custom prompts, prompt snippets, prompt templates",
|
||||
"commandPullKeywords": "pull, git, pull changes, download changes",
|
||||
"commandPushKeywords": "push, git, push changes, push to remote, upload changes",
|
||||
"commandQuickChat": "Quick Chat",
|
||||
"commandQuickChatKeywords": "quick chat, new quick chat, quick question, ask agent",
|
||||
"commandRebaseKeywords": "rebase, git, branch",
|
||||
"commandSwitchToDarkMode": "Switch to Dark Mode",
|
||||
"commandSwitchToKanbanView": "Switch to Kanban View",
|
||||
"commandSwitchToKanbanViewKeywords": "kanban, board, view",
|
||||
"commandSwitchToLightMode": "Switch to Light Mode",
|
||||
"commandSwitchToListView": "Switch to List View",
|
||||
"commandSwitchToListViewKeywords": "list, table, view",
|
||||
"commandSwitchToPipelineView": "Switch to Pipeline View",
|
||||
"commandSwitchToPipelineViewKeywords": "pipeline, graph, view",
|
||||
"commandThemeKeywords": "theme, color theme, appearance",
|
||||
"commandWorkspaceSettingsKeywords": "workspace, workspaces",
|
||||
"commitsAndPushesToTheCurrent": "Commits and pushes to the current branch.",
|
||||
@@ -126,6 +170,29 @@
|
||||
"forwardStdinStdoutStraightToThe": "Forward stdin/stdout straight to the CLI subprocess. Disables ACP.",
|
||||
"getStarted": "Get Started",
|
||||
"githubCliNotAuthenticated": "GitHub CLI not authenticated",
|
||||
"gitOpAmendCommit": "Amend commit",
|
||||
"gitOpCommit": "Commit",
|
||||
"gitOperationFailedInRepos_one": "Failed in {{count}} repo — {{summary}}",
|
||||
"gitOperationFailedInRepos_other": "Failed in {{count}} repos — {{summary}}",
|
||||
"gitOperationPartialDescription_one": "{{operation}} succeeded in {{succeeded}} of {{count}} repo ({{repos}}); failed in {{summary}}",
|
||||
"gitOperationPartialDescription_other": "{{operation}} succeeded in {{succeeded}} of {{count}} repos ({{repos}}); failed in {{summary}}",
|
||||
"gitOperationPartiallySucceeded": "{{operation}} partially succeeded",
|
||||
"gitOperationRepoFailure": "{{repo}}: {{error}}",
|
||||
"gitOperationSucceededInRepos_one": "{{operation}} succeeded in {{count}} repo: {{repos}}",
|
||||
"gitOperationSucceededInRepos_other": "{{operation}} succeeded in {{count}} repos: {{repos}}",
|
||||
"gitOpForcePush": "Force Push",
|
||||
"gitOpHardReset": "Hard reset",
|
||||
"gitOpMerge": "Merge",
|
||||
"gitOpPull": "Pull",
|
||||
"gitOpPush": "Push",
|
||||
"gitOpRebase": "Rebase",
|
||||
"gitOperationCompleted": "{{operation}} completed",
|
||||
"gitOperationFailed": "{{operation}} failed",
|
||||
"gitOperationRunning": "{{operation}}...",
|
||||
"gitOperationScoped": "{{operation}} ({{repo}})",
|
||||
"gitOperationSucceeded": "{{operation}} successful",
|
||||
"gitOpRevertCommit": "Revert commit",
|
||||
"gitOpSoftReset": "Soft reset",
|
||||
"gitScopeSkipped": "Skipped {{scope}} because a child repository operation failed ({{failedScopes}}).",
|
||||
"goToFirstPage": "Go to first page",
|
||||
"goToLastPage": "Go to last page",
|
||||
@@ -234,6 +301,7 @@
|
||||
"probing": "Probing",
|
||||
"profileName": "Profile name",
|
||||
"prompts": "Prompts",
|
||||
"reference": "Reference",
|
||||
"refreshAriaLabel": "Refresh {{label}}",
|
||||
"refreshBranchesGitFetch": "Refresh branches (git fetch)",
|
||||
"refreshingLabel": "Refreshing {{label}}...",
|
||||
@@ -275,6 +343,7 @@
|
||||
"scopeFiles": "Files",
|
||||
"scopeTitleWithShortcut": "{{label}} ({{shortcut}})",
|
||||
"scratchWorkspace": "scratch workspace",
|
||||
"script": "Script",
|
||||
"searchEllipsis": "Search…",
|
||||
"searchFailedEditTheQueryOr": "Search failed. Edit the query or reopen search to retry.",
|
||||
"searchForFiles": "Search for files...",
|
||||
@@ -302,9 +371,12 @@
|
||||
"skip": "Skip",
|
||||
"stagesAndCommitsChangesUsingConventional": "Stages and commits changes using Conventional Commits.",
|
||||
"status": "Status",
|
||||
"step": "Step",
|
||||
"subtaskTitle": "Subtask title",
|
||||
"switchMode": "Switch mode",
|
||||
"system": "System",
|
||||
"task": "Task",
|
||||
"terminal": "Terminal",
|
||||
"todos": "Todos",
|
||||
"taskLoading": "Loading task...",
|
||||
"tasks": "Tasks",
|
||||
@@ -333,19 +405,25 @@
|
||||
"typeACommand": "Type a command...",
|
||||
"typeToSearchFiles": "Type to search files...",
|
||||
"typeToSearchTaskContents": "Type to search task contents…",
|
||||
"unavailable": "Unavailable",
|
||||
"unknown": "Unknown",
|
||||
"unknownError": "Unknown error",
|
||||
"unknownPanel": "Unknown panel: {{component}}",
|
||||
"unsavedChanges": "Unsaved changes",
|
||||
"untitledTask": "Untitled task",
|
||||
"usefulCommandsAgentSkills": "Useful commands & agent skills",
|
||||
"useScratchInstead": "Use scratch instead",
|
||||
"useThisFolder": "Use this folder",
|
||||
"viewFullReleaseOnGithub": "View full release on GitHub",
|
||||
"waitsForCiAndAutomatedReviews": "Waits for CI and automated reviews, then fixes failures and addresses comments.",
|
||||
"walkthrough": "Walkthrough",
|
||||
"whatSNew": "What's New",
|
||||
"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",
|
||||
"you": "You",
|
||||
"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"
|
||||
|
||||
@@ -19,10 +19,17 @@
|
||||
"copyLine": "Copy line {{lineNumber}}",
|
||||
"copyPath": "Copy path",
|
||||
"default": "default",
|
||||
"deleteFailed": "Delete failed",
|
||||
"deleteFile": "Delete file",
|
||||
"disableWordWrap": "Disable word wrap",
|
||||
"enableWordWrap": "Enable word wrap",
|
||||
"errorWhileDeletingFile": "An error occurred while deleting the file",
|
||||
"errorWhileSavingFile": "An error occurred while saving the file",
|
||||
"failedToDeleteFile": "Failed to delete file",
|
||||
"failedToSaveFile": "Failed to save file",
|
||||
"failedToSendComment": "Failed to send comment",
|
||||
"fileSavedOverwritten": "File saved (overwritten)",
|
||||
"fileSavedOverwrittenDescription": "The file was modified externally. Your version was saved.",
|
||||
"foldUnchangedLines": "Fold unchanged lines",
|
||||
"hideDiffIndicators": "Hide diff indicators",
|
||||
"highlight": "Highlight",
|
||||
@@ -35,6 +42,7 @@
|
||||
"noDiffAvailable": "No diff available",
|
||||
"noEditorsConfigured": "No editors configured",
|
||||
"openFolder": "Open folder",
|
||||
"openTaskSessionBeforeSending": "Open a task session before sending to the agent.",
|
||||
"openWith": "Open with...",
|
||||
"pasteLink": "Paste link...",
|
||||
"pleaseTryAgain": "Please try again.",
|
||||
@@ -43,6 +51,7 @@
|
||||
"reload": "Reload",
|
||||
"revertAllChanges": "Revert all changes",
|
||||
"revertChanges": "Revert changes",
|
||||
"saveFailed": "Save failed",
|
||||
"saving": "Saving...",
|
||||
"sentToTheAgent": "Sent to the agent.",
|
||||
"showAllLines": "Show all lines",
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"failedToDeleteReviewWatch": "Failed to delete review watch",
|
||||
"failedToLoadGithubWorkspaceSettings": "Failed to load GitHub workspace settings",
|
||||
"failedToMerge": "Failed to merge",
|
||||
"failedToReRequestReview": "Failed to re-request review",
|
||||
"failedToResetIssueWatch": "Failed to reset issue watch",
|
||||
"failedToResetReviewWatch": "Failed to reset review watch",
|
||||
"failedToSaveDefaultQueries": "Failed to save default queries",
|
||||
@@ -445,6 +446,7 @@
|
||||
"reviewCount_other": "{{count}} reviews",
|
||||
"reviewFollowUp": "Review follow-up",
|
||||
"reviewPermissions": "Review permissions",
|
||||
"reviewReRequestedFrom": "Review re-requested from {{reviewer}}",
|
||||
"reviewWatch": "review watch",
|
||||
"reviewWatchCreated": "Review watch created",
|
||||
"reviewWatchDeleted": "Review watch deleted",
|
||||
@@ -468,6 +470,7 @@
|
||||
"selectWorkflow": "Select workflow",
|
||||
"selectWorkspace": "Select workspace",
|
||||
"selectedRepositories": "Selected repositories",
|
||||
"selectWorkspaceBeforeRequestingReview": "Select a workspace before requesting review.",
|
||||
"setTheseExactUrlsCreateAClientSecret": "Set these exact URLs, create a client secret and webhook secret, and download a private key. This one-time setup expires {{expiresAt}}.",
|
||||
"showGithubApiLimits": "Show GitHub API limits",
|
||||
"showLess": "Show less",
|
||||
|
||||
@@ -316,7 +316,6 @@
|
||||
"mrAutomationExplainAutoFixRounds": "Explain auto-fix rounds",
|
||||
"mrAutomationAutoFixPromptSaveFailed": "Failed to save auto-fix prompt.",
|
||||
"mrAutomationAutoFixPromptResetFailed": "Failed to reset auto-fix prompt.",
|
||||
"mrPopoverOpenDetail": "Open merge request detail",
|
||||
"mrPopoverOpenInGitlab": "Open !{{mriid}} in GitLab",
|
||||
"mrPopoverUnlink": "Unlink !{{mriid}}",
|
||||
"mrPopoverPassRate": "Pass rate",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"allRepositories": "All repositories",
|
||||
"allWorkflows": "All workflows",
|
||||
"allWorkflowsTitleCase": "All Workflows",
|
||||
"allWorkspaces": "All workspaces",
|
||||
"approvalRequired": "Approval Required",
|
||||
"archive": "Archive",
|
||||
"archiveCount_one": "Archive {{count}}",
|
||||
|
||||
@@ -101,10 +101,15 @@
|
||||
"botTokenChatId": "{\"bot_token\": \"...\", \"chat_id\": \"...\"}",
|
||||
"branch": "Branch",
|
||||
"browseSkillsSh": "Browse skills.sh",
|
||||
"budgetActionBlockNewTasks": "block new tasks",
|
||||
"budgetActionNotifyOnly": "notify only",
|
||||
"budgetActionPauseAgent": "pause agent",
|
||||
"budgetAlert": "Budget Alert",
|
||||
"budgetAmount": "Budget: {{amount}}",
|
||||
"budgetCapConcurrencyAndExecutionEnvironment": "Budget cap, concurrency, and execution environment.",
|
||||
"budgetLimitsConcurrencyAndExecutionEnvironment": "Budget limits, concurrency, and execution environment.",
|
||||
"budgetPeriodMonthly": "monthly",
|
||||
"budgetPeriodTotal": "total",
|
||||
"budgetPolicies": "Budget Policies",
|
||||
"budgetPolicyCreated": "Budget policy created",
|
||||
"budgetPolicyDeleted": "Budget policy deleted",
|
||||
|
||||
@@ -347,10 +347,17 @@
|
||||
"layoutEditorForProfile": "{{name}} editor",
|
||||
"layoutProfile": "layout profile",
|
||||
"layoutProfileCopyName": "{{name}} copy",
|
||||
"layoutProfileDefault": "Default",
|
||||
"layoutProfileDefaultDescription": "Agent with Files, Changes, and Terminal",
|
||||
"layoutProfileName": "Layout profile name",
|
||||
"layoutProfileNamesMustNotBe": "Layout profile names must not be empty",
|
||||
"layoutProfilePlanMode": "Plan Mode",
|
||||
"layoutProfilePlanModeDescription": "Agent and Plan side by side",
|
||||
"layoutProfilePreviewMode": "Preview Mode",
|
||||
"layoutProfilePreviewModeDescription": "Agent and Browser side by side",
|
||||
"layoutProfiles": "Layout profiles",
|
||||
"layoutProfilesWereNotSaved": "Layout profiles were not saved",
|
||||
"layoutProfileVsCodeDescription": "Agent and VS Code side by side",
|
||||
"layouts": "Layouts",
|
||||
"ligatures": "Ligatures",
|
||||
"light": "Light",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"toggleTheme": "Toggle theme",
|
||||
"unsavedFilterChanges": "Unsaved filter changes",
|
||||
"viewAll": "All",
|
||||
"viewAllTasks": "All tasks",
|
||||
"viewLabel": "View: {{name}}",
|
||||
"whatsNew": "What's new",
|
||||
"work": "Work",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"agentCliNotInstalled": "Agent CLI not installed",
|
||||
"agentProbeFailed": "Agent probe failed",
|
||||
"aPullRequestAlreadyExistsFor": "A pull request already exists for this task",
|
||||
"archivedTask": "Archived task",
|
||||
"aSharedDocumentForYouAnd": "A shared document for you and the agent to collaborate on the approach",
|
||||
"aboutContextWindowSource": "About context window source",
|
||||
"activeBadge": "ACTIVE",
|
||||
@@ -269,6 +272,7 @@
|
||||
"couldNotAddSources": "Could not add sources. Your entries are still here to retry.",
|
||||
"couldNotLoadRepositories": "Could not load repositories: {{message}}",
|
||||
"couldNotResolveRemoteRepository": "Could not resolve remote repository: {{message}}",
|
||||
"couldNotSaveStatusBarOrder": "Could not save status bar order",
|
||||
"couldnTStartASession": "Couldn't start a session",
|
||||
"create": "Create",
|
||||
"createANewBranch": "Create a new branch",
|
||||
@@ -290,6 +294,7 @@
|
||||
"creatingEllipsis": "Creating...",
|
||||
"creatingFrom": "Creating {{shortName}} from <4>{{displayBranch}}</4>",
|
||||
"creatingFromTo": "Creating {{shortName}} from <4>{{displayBranch}}</4> to <7>{{baseBranch}}</7>",
|
||||
"credentialsAvailableInExecutor": "Credentials available in the selected executor",
|
||||
"ctrl": "Ctrl",
|
||||
"current": "current",
|
||||
"current2": "Current",
|
||||
@@ -401,14 +406,19 @@
|
||||
"environmentPrepared": "Environment prepared",
|
||||
"environmentPreparedOnFreshSandbox": "Environment prepared on a fresh sandbox",
|
||||
"environmentPreparedWithWarnings": "Environment prepared with warnings",
|
||||
"environmentReset": "Environment reset",
|
||||
"environmentResetFailed": "Reset failed: {{error}}",
|
||||
"environmentSetupFailed": "Environment setup failed",
|
||||
"environmentSetupFinishedWithErrors": "Environment setup finished with errors",
|
||||
"environmentStateCurrent": "Current state: {{state}}",
|
||||
"error": "Error",
|
||||
"errorData": "Error Data",
|
||||
"eventCount_one": "{{count}} event",
|
||||
"eventCount_other": "{{count}} events",
|
||||
"executor2": "Executor",
|
||||
"executorEnvironmentIsUnavailable": "Executor environment is unavailable.",
|
||||
"executorEnvironmentRunning": "Executor environment running",
|
||||
"executorEnvironmentStopped": "Executor environment stopped",
|
||||
"executorHintDocker": "A Docker container will be created from the selected base branch and checked out on a task branch.",
|
||||
"executorHintLocal": "The agent will run directly on the repository.",
|
||||
"executorHintWorktreeMulti": "A git worktree will be created for each repository in a parent folder. The agent runs in that parent folder so it can see every worktree side by side.",
|
||||
@@ -439,6 +449,8 @@
|
||||
"failedToChangeModel": "Failed to change model",
|
||||
"failedToDeleteDocument": "Failed to delete document",
|
||||
"failedToDeleteLayout": "Failed to delete layout",
|
||||
"failedToDiscardChanges": "Failed to discard changes",
|
||||
"failedToGenerateContent": "Failed to generate content",
|
||||
"failedToLinkGithubIssue": "Failed to link GitHub issue.",
|
||||
"failedToLinkGithubPullRequest": "Failed to link GitHub pull request.",
|
||||
"failedToLinkTarget": "Failed to link {{target}}.",
|
||||
@@ -454,13 +466,16 @@
|
||||
"failedToRenameItem": "Failed to rename item",
|
||||
"failedToRenameTask": "Failed to rename task",
|
||||
"failedToRestorePlan": "Failed to restore plan",
|
||||
"failedToResumeSession": "Failed to resume session",
|
||||
"failedToRevokeShare": "Failed to revoke share.",
|
||||
"failedToSaveDocument": "Failed to save document",
|
||||
"failedToSaveLayout": "Failed to save layout",
|
||||
"failedToSendComment": "Failed to send comment",
|
||||
"failedToSendComments": "Failed to send comments",
|
||||
"failedToSendMessage": "Failed to send message",
|
||||
"failedToStartTunnel": "Failed to start tunnel: {{error}}",
|
||||
"failedToStartVsCode": "Failed to start VS Code",
|
||||
"failedToStopTunnel": "Failed to stop tunnel: {{error}}",
|
||||
"failedToUnarchiveTask": "Failed to unarchive task",
|
||||
"failedToUnlinkGithubIssue": "Failed to unlink GitHub issue.",
|
||||
"failedToUpload": "Failed to upload {{name}}",
|
||||
@@ -519,6 +534,7 @@
|
||||
"fullImpactDetails": "Full impact details",
|
||||
"fullSize": "Full size {{alt}}",
|
||||
"generatingSummary": "Generating summary...",
|
||||
"generationFailed": "Generation failed",
|
||||
"gitActions": "Git actions",
|
||||
"gitPollMode": "Git poll: {{mode}} ({{label}})",
|
||||
"githubIssue": "GitHub Issue",
|
||||
@@ -599,6 +615,7 @@
|
||||
"kandevListWorkspaces": "Kandev: List Workspaces",
|
||||
"kandevMessageTask": "Kandev: Message Task",
|
||||
"kandevMoveTask": "Kandev: Move Task",
|
||||
"kandevUpdateAvailable": "Kandev update available",
|
||||
"kandevUpdateTask": "Kandev: Update Task",
|
||||
"kandevUpdateTaskPlan": "Kandev: Update Task Plan",
|
||||
"kandevWriteTaskDocument": "Kandev: Write Task Document",
|
||||
@@ -703,6 +720,9 @@
|
||||
"moreNotShown_one": "+ {{count}} more not shown",
|
||||
"moreNotShown_other": "+ {{count}} more not shown",
|
||||
"moreToolbarActions": "More toolbar actions",
|
||||
"moveFailed": "Move failed",
|
||||
"moveFailedFiles": "Some files could not be moved",
|
||||
"moveFailedUnexpected": "An error occurred while moving files",
|
||||
"moveHeadToThisCommitKeep": "Move HEAD to this commit. Keep all changes staged.",
|
||||
"moveHere": "Move here",
|
||||
"moveTaskToTheNextWorkflow": "Move task to the next workflow step",
|
||||
@@ -733,6 +753,7 @@
|
||||
"nextMessage": "Next message",
|
||||
"nextQuestion": "Next question",
|
||||
"noActiveSession": "No active session",
|
||||
"noActiveSessionDescription": "Start a session first to use AI generation",
|
||||
"noActiveTask": "No active task",
|
||||
"noActivityYet": "No activity yet",
|
||||
"noAgentFound": "No agent found.",
|
||||
@@ -796,6 +817,7 @@
|
||||
"noSavedLayouts": "No saved layouts",
|
||||
"noSessionsYetLaunchOneTo": "No sessions yet. Launch one to get started.",
|
||||
"noSteps": "No steps",
|
||||
"noStepsConfigured": "No steps configured",
|
||||
"noStepsFound": "No steps found.",
|
||||
"noSubIssues": "No sub-issues",
|
||||
"noTaskSelected": "No task selected",
|
||||
@@ -836,9 +858,20 @@
|
||||
"openWorkspaceSettings": "Open workspace settings",
|
||||
"option": "option {{index}}",
|
||||
"orTypeACustomAnswer": "Or type a custom answer…",
|
||||
"otherChanges": "Other changes",
|
||||
"output": "Output",
|
||||
"outputTokens": "Output tokens",
|
||||
"outputTruncated": "Output truncated",
|
||||
"panelAgent": "Agent",
|
||||
"panelChanges": "Changes",
|
||||
"panelDiffViewer": "Diff Viewer",
|
||||
"panelFileDiff": "Diff [{{file}}]",
|
||||
"panelFiles": "Files",
|
||||
"panelMergeRequest": "Merge Request",
|
||||
"panelPlan": "Plan",
|
||||
"panelPrDetails": "PR Details",
|
||||
"panelPullRequest": "Pull Request",
|
||||
"panelTerminal": "Terminal",
|
||||
"parent": "Parent",
|
||||
"parkedCount": "Parked ({{parked}})",
|
||||
"parkedTerminals": "Parked terminals",
|
||||
@@ -850,6 +883,7 @@
|
||||
"pendingReviewCommentsHelp_one": "{{count}} pending review comment. Click to expand the list — you can edit each comment, click the file path to jump to the source, or remove a comment with the trash icon.",
|
||||
"pendingReviewCommentsHelp_other": "{{count}} pending review comments. Click to expand the list — you can edit each comment, click the file path to jump to the source, or remove a comment with the trash icon.",
|
||||
"permissionRequested": "Permission requested",
|
||||
"permissionRequired": "Permission Required",
|
||||
"pickADifferentRepoLocalFolder": "Pick a different repo, local folder, or remote URL",
|
||||
"pickAStartingFolderOptional": "pick a starting folder (optional)",
|
||||
"pickOrPasteARepo": "Pick or paste a repo",
|
||||
@@ -1219,6 +1253,7 @@
|
||||
"statusInProgress": "In Progress",
|
||||
"statusInReview": "In Review",
|
||||
"statusTodo": "Todo",
|
||||
"statusUpdate": "Status update",
|
||||
"stepCount_one": "{{count}} step",
|
||||
"stepCount_other": "{{count}} steps",
|
||||
"stepFieldLabel": "step",
|
||||
@@ -1233,6 +1268,7 @@
|
||||
"stopVoiceInput": "Stop voice input",
|
||||
"stoppedWithAnError": "stopped with an error",
|
||||
"stopping": "Stopping…",
|
||||
"subagent": "Subagent",
|
||||
"subIssues": "Sub-issues",
|
||||
"subTasks": "Sub-tasks",
|
||||
"submit": "Submit",
|
||||
@@ -1244,6 +1280,7 @@
|
||||
"subtasksStayActiveUnlessYouTick": "Subtasks stay active unless you tick this. They may still be in progress.",
|
||||
"successful": "{{operationName}} successful",
|
||||
"summarize": "Summarize",
|
||||
"summarizeReturnedNoResult": "Summarize utility returned no result",
|
||||
"summarizeSession": "Summarize session",
|
||||
"summarizeSessionNamed": "Summarize {{label}}",
|
||||
"summarizing": "Summarizing...",
|
||||
@@ -1337,6 +1374,7 @@
|
||||
"tokenCount_other": "{{formatted}} tokens",
|
||||
"tokens": "tokens",
|
||||
"tool": "tool",
|
||||
"toolCall": "Tool call",
|
||||
"toolCallCount_one": "{{count}} tool call",
|
||||
"toolCallCount_other": "{{count}} tool calls",
|
||||
"toolCount_one": "{{count}} tool",
|
||||
@@ -1353,6 +1391,8 @@
|
||||
"truncatedParen": "(truncated)",
|
||||
"tryAgain": "Try again",
|
||||
"tunnel": "Tunnel",
|
||||
"tunnelStartedOnPort": "Tunnel started on port {{port}}",
|
||||
"tunnelStopped": "Tunnel stopped",
|
||||
"turnForDuration": "for {{duration}}",
|
||||
"turnMessageCount_one": "· {{count}} message",
|
||||
"turnMessageCount_other": "· {{count}} messages",
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"columnWorkflow": "Workflow",
|
||||
"delete": "Delete",
|
||||
"deleteTask": "Delete task",
|
||||
"failedToArchiveTask": "Failed to archive task",
|
||||
"failedToDeleteTask": "Failed to delete task",
|
||||
"failedToLoadTasks": "Failed to load tasks",
|
||||
"failedToUnarchiveTask": "Failed to unarchive task",
|
||||
"goToFirstPage": "Go to first page",
|
||||
"goToLastPage": "Go to last page",
|
||||
"goToNextPage": "Go to next page",
|
||||
@@ -39,6 +43,12 @@
|
||||
"sortTitleZA": "Title Z-A",
|
||||
"sortUpdatedNewest": "Updated newest",
|
||||
"sortUpdatedOldest": "Updated oldest",
|
||||
"taskArchived": "Task archived",
|
||||
"taskArchivedDescription": "The task has been archived successfully.",
|
||||
"taskUnarchived": "Task unarchived",
|
||||
"unarchive": "Unarchive",
|
||||
"unarchivedBranchesMissing_one": "Branch {{branches}} no longer exists locally or on the remote — the next session starts fresh from the base branch.",
|
||||
"unarchivedBranchesMissing_other": "Branches {{branches}} no longer exist locally or on the remote — the next session starts fresh from the base branch.",
|
||||
"unarchivedTaskRestored": "The task has been restored.",
|
||||
"unarchiveTask": "Unarchive task"
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"conflictTierMappingsTitle": "Ŵōŕķśƥàćē ţĩēŕ ḿàƥƥĩńĝś:",
|
||||
"conflictWatchersTitle": "Ŵàţćĥēŕś (ŵĩĺĺ ƀē ďĩśàƀĺēď):",
|
||||
"conflictUtilityAgentsTitle": "Ũţĩĺĩţŷ àĝēńţś (ŵĩĺĺ ķēēƥ śţàĺē ƀĩńďĩńĝś):",
|
||||
"disableProfileUtilityWarning": "Ũţĩĺĩţŷ àĝēńţś ũśē ţĥĩś ƥŕōƒĩĺē. Ďĩśàƀĺĩńĝ ĩţ śţōƥś ńēŵ ũţĩĺĩţŷ ćàĺĺś ũńţĩĺ ēàćĥ ƀĩńďĩńĝ ĩś ƒĩχēď.",
|
||||
"disableProfileUtilityWarning": "Ũţĩĺĩţŷ àĝēńţś ũśē ţĥĩś ƥŕōƒĩĺē. Ďĩśàƀĺĩńĝ ĩţ ŵĩĺĺ śţōƥ ńēŵ ũţĩĺĩţŷ ćàĺĺś ũńţĩĺ ēàćĥ ƀĩńďĩńĝ ĩś ŕēƥàĩŕēď.",
|
||||
"disableProfileAnyway": "Ďĩśàƀĺē ƥŕōƒĩĺē",
|
||||
"copied": "Ćōƥĩēď",
|
||||
"copyInstallCommand": "Ćōƥŷ ĩńśţàĺĺ ćōḿḿàńď",
|
||||
@@ -99,6 +99,7 @@
|
||||
"failedToFetchCapabilities": "Ƒàĩĺēď ţō ƒēţćĥ ćàƥàƀĩĺĩţĩēś",
|
||||
"modelOptionsAuthenticationRequired": "Śĩĝń ĩń ţō ţĥĩś àĝēńţ ţō ĺōàď ḿōďēĺ ōƥţĩōńś.",
|
||||
"modelOptionsResolutionFailed": "Ḿōďēĺ ōƥţĩōńś ćōũĺď ńōţ ƀē ĺōàďēď.",
|
||||
"modelSettings": "Ḿōďēĺ śēţţĩńĝś",
|
||||
"resolvingModelOptions": "Ĺōàďĩńĝ ḿōďēĺ ōƥţĩōńś…",
|
||||
"retryModelOptions": "Ŕēţŕŷ",
|
||||
"failedToLoadCommandPreview": "Ƒàĩĺēď ţō ĺōàď ćōḿḿàńď ƥŕēvĩēŵ",
|
||||
@@ -186,6 +187,7 @@
|
||||
"searchModes": "Śēàŕćĥ ḿōďēś...",
|
||||
"selectAModel": "Śēĺēćţ à ḿōďēĺ...",
|
||||
"selectAModelShort": "Śēĺēćţ à ḿōďēĺ",
|
||||
"selectModel": "Śēĺēćţ ḿōďēĺ...",
|
||||
"sessionEnded": "Śēśśĩōń ēńďēď.",
|
||||
"sessionEndedWithCode": "Śēśśĩōń ēńďēď (ēxĩţ {{code}}).",
|
||||
"sessionUnavailable": "Śēśśĩōń ĩś ńō ĺōńĝēŕ àvàĩĺàƀĺē.",
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
"enabled": "Ēńàƀĺēď",
|
||||
"enterValidOrganizationUrl": "Ēńţēŕ à vàĺĩď ōŕĝàńĩźàţĩōń ŨŔĹ ţō ōƥēń ĩţś ţōķēń śēţţĩńĝś.",
|
||||
"executorProfileId": "Ēxēćũţōŕ ƥŕōƒĩĺē ĨĎ",
|
||||
"failedToLinkPullRequest": "Ƒàĩĺēď ţō ĺĩńķ Àźũŕē ĎēvŌƥś ƥũĺĺ ŕēqũēśţ.",
|
||||
"failedToLinkWorkItem": "Ƒàĩĺēď ţō ĺĩńķ Àźũŕē ĎēvŌƥś ŵōŕķ ĩţēḿ.",
|
||||
"failedToLinkWorkItemNoProject": "Ƒàĩĺēď ţō ĺĩńķ Àźũŕē ĎēvŌƥś ŵōŕķ ĩţēḿ: ƥŕōĵēćţ ĩś ḿĩśśĩńĝ.",
|
||||
"failedToLoadConfig": "Ƒàĩĺēď ţō ĺōàď Àźũŕē ĎēvŌƥś ćōńƒĩĝ: {{error}}",
|
||||
"failedToLoadQuickActions": "Ƒàĩĺēď ţō ĺōàď qũĩćķ àćţĩōńś.",
|
||||
"failedToSaveDefaultQueries": "Ƒàĩĺēď ţō śàvē ďēƒàũĺţ qũēŕĩēś",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"activeSubagents_other": "{{count}} śũƀàĝēńţś ŕũńńĩńĝ",
|
||||
"activeTasks": "Àćţĩvē Ţàśķś",
|
||||
"aDirectLocalExecutorProfileIsRequired": "À ďĩŕēćţ ĺōćàĺ ēxēćũţōŕ ƥŕōƒĩĺē ĩś ŕēqũĩŕēď ţō ćŕēàţē àńď ũśē àń ēḿƥţŷ ŕēƥōśĩţōŕŷ.",
|
||||
"agent": "Àĝēńţ",
|
||||
"agentDashboard": "àĝēńţ ďàśĥƀōàŕď",
|
||||
"agentRun": "àĝēńţ ŕũń",
|
||||
"agentRuns": "àĝēńţ ŕũńś",
|
||||
@@ -13,6 +14,9 @@
|
||||
"andMoreFiles_one": "+{{count}} ḿōŕē",
|
||||
"andMoreFiles_other": "+{{count}} ḿōŕē",
|
||||
"anEnhancedPromptIsAvailable": "Àń ēńĥàńćēď ƥŕōḿƥţ ĩś àvàĩĺàƀĺē.",
|
||||
"anErrorOccurred": "Àń ēŕŕōŕ ōććũŕŕēď",
|
||||
"anUnexpectedErrorOccurred": "Àń ũńēxƥēćţēď ēŕŕōŕ ōććũŕŕēď",
|
||||
"anUnknownErrorOccurred": "Àń ũńķńōŵń ēŕŕōŕ ōććũŕŕēď",
|
||||
"apply": "Àƥƥĺŷ",
|
||||
"autoApproveAllPermissions": "Àũţō-àƥƥŕōvē àĺĺ ƥēŕḿĩśśĩōńś",
|
||||
"automations": "Àũţōḿàţĩōńś",
|
||||
@@ -36,9 +40,30 @@
|
||||
"cliPassthrough": "ĆĹĨ ƥàśśţĥŕōũĝĥ",
|
||||
"close": "Ćĺōśē",
|
||||
"closeEsc": "Ćĺōśē (Ēść)",
|
||||
"commandAddBrowserPanel": "Àďď Ɓŕōŵśēŕ Ƥàńēĺ",
|
||||
"commandAddBrowserPanelKeywords": "ƀŕōŵśēŕ, ƥŕēvĩēŵ, ŵēƀ, ōƥēń ƀŕōŵśēŕ ƥŕēvĩēŵ, ŵēƀ ƥŕēvĩēŵ, àƥƥ ƥŕēvĩēŵ",
|
||||
"commandAddChangesPanel": "Àďď Ćĥàńĝēś Ƥàńēĺ",
|
||||
"commandAddChangesPanelKeywords": "ćĥàńĝēś, ďĩƒƒ, ĝĩţ ćĥàńĝēś, ćĥàńĝēď ƒĩĺēś, śōũŕćē ćōńţŕōĺ, ĝĩţ ďĩƒƒ, ŕēvĩēŵ ćĥàńĝēś",
|
||||
"commandAddPlanPanel": "Àďď Ƥĺàń Ƥàńēĺ",
|
||||
"commandAddPlanPanelKeywords": "ƥĺàń, ďōćũḿēńţ, ţàśķ ƥĺàń, ĩḿƥĺēḿēńţàţĩōń ƥĺàń, ƥĺàń ďēţàĩĺś",
|
||||
"commandAddTerminalPanel": "Àďď Ţēŕḿĩńàĺ Ƥàńēĺ",
|
||||
"commandAddTerminalPanelKeywords": "ţēŕḿĩńàĺ, śĥēĺĺ, ćōńśōĺē, ńēŵ ţēŕḿĩńàĺ, ōƥēń ţēŕḿĩńàĺ, ćōḿḿàńď ĺĩńē",
|
||||
"commandAgentsSettingsKeywords": "àĝēńţś, àĝēńţ śēţţĩńĝś, àĝēńţ ƥŕōƒĩĺēś, ĩńśţàĺĺēď àĝēńţś, ćĺàũďē",
|
||||
"commandArchiveTaskKeywords": "àŕćĥĩvē, àŕćĥĩvē ţàśķ, àŕćĥĩvē ćũŕŕēńţ ţàśķ, ćĺōśē ţàśķ, ƒĩńĩśĥ ţàśķ, ƥũţ àŵàŷ",
|
||||
"commandCancelTurn": "Ćàńćēĺ Ţũŕń",
|
||||
"commandCancelTurnKeywords": "ćàńćēĺ, śţōƥ, ţũŕń, ćàńćēĺ àĝēńţ, śţōƥ àĝēńţ, ĩńţēŕŕũƥţ àĝēńţ",
|
||||
"commandCommitChanges": "Ćōḿḿĩţ Ćĥàńĝēś",
|
||||
"commandCommitChangesKeywords": "ćōḿḿĩţ, ĝĩţ, śàvē ćĥàńĝēś, ĝĩţ ćōḿḿĩţ",
|
||||
"commandConfigChatKeywords": "ćōńƒĩĝ ćĥàţ, ćōńƒĩĝ ḿōďē, ćōńƒĩĝũŕē ķàńďēv, ŵōŕķƒĺōŵ ćōńƒĩĝũŕàţĩōń, ḿćƥ ćōńƒĩĝũŕàţĩōń",
|
||||
"commandCreateFile": "Ćŕēàţē Ƒĩĺē",
|
||||
"commandCreateFileKeywords": "ćŕēàţē, ńēŵ, ƒĩĺē, àďď",
|
||||
"commandCreateFilePlaceholder": "Ƒĩĺē ƥàţĥ ŕēĺàţĩvē ţō ŵōŕķśƥàćē ŕōōţ...",
|
||||
"commandCreateNewTask": "Ćŕēàţē Ńēŵ Ţàśķ",
|
||||
"commandCreateNewTaskKeywords": "ńēŵ, ćŕēàţē, ţàśķ, àďď",
|
||||
"commandCreatePr": "Ćŕēàţē ƤŔ",
|
||||
"commandCreatePrKeywords": "ƥũĺĺ ŕēqũēśţ, ƥŕ, ōƥēń ƥũĺĺ ŕēqũēśţ, śũƀḿĩţ ƥũĺĺ ŕēqũēśţ, ĝĩţ",
|
||||
"commandCreateSubtask": "Ćŕēàţē Śũƀţàśķ",
|
||||
"commandCreateSubtaskKeywords": "śũƀţàśķ, ćŕēàţē, ńēŵ śũƀţàśķ, ńēŵ śũƀ-ţàśķ, ćĥĩĺď ţàśķ",
|
||||
"commandExecutorsSettingsKeywords": "ēxēćũţōŕś, ēxēćũţōŕ ƥŕōƒĩĺēś, ēxēćũţĩōń ēńvĩŕōńḿēńţ, ēńvĩŕōńḿēńţ vàŕĩàƀĺēś, ŕũńţĩḿē, ćōḿƥũţē",
|
||||
"commandGoToAllTasks": "Ĝō ţō Àĺĺ Ţàśķś",
|
||||
"commandGoToAllTasksKeywords": "ţàśķś, ĺĩśţ, àĺĺ",
|
||||
@@ -51,11 +76,21 @@
|
||||
"commandGoToStats": "Ĝō ţō Śţàţś",
|
||||
"commandGoToStatsKeywords": "śţàţś, śţàţĩśţĩćś, àńàĺŷţĩćś, ḿēţŕĩćś",
|
||||
"commandGroupActions": "Àćţĩōńś",
|
||||
"commandGroupAgent": "Àĝēńţ",
|
||||
"commandGroupCommands": "Ćōḿḿàńďś",
|
||||
"commandGroupGit": "Ĝĩţ",
|
||||
"commandGroupNavigation": "Ńàvĩĝàţĩōń",
|
||||
"commandGroupPanels": "Ƥàńēĺś",
|
||||
"commandGroupPreferences": "Ƥŕēƒēŕēńćēś",
|
||||
"commandGroupSettings": "Śēţţĩńĝś",
|
||||
"commandGroupTasks": "Ţàśķś",
|
||||
"commandGroupView": "Vĩēŵ",
|
||||
"commandGroupWorkspace": "Ŵōŕķśƥàćē",
|
||||
"commandMergeKeywords": "ḿēŕĝē, ĝĩţ, ƀŕàńćĥ",
|
||||
"commandNewAgent": "Ńēŵ Àĝēńţ",
|
||||
"commandNewAgentKeywords": "ńēŵ, àĝēńţ, śēśśĩōń, śţàŕţ àĝēńţ, ńēŵ śēśśĩōń",
|
||||
"commandOpenRecentTaskSwitcher": "Ōƥēń Ŕēćēńţ Ţàśķ Śŵĩţćĥēŕ",
|
||||
"commandOpenRecentTaskSwitcherKeywords": "ŕēćēńţ, ţàśķ, śŵĩţćĥēŕ, ĥĩśţōŕŷ, ŕēćēńţ ţàśķś, śŵĩţćĥ ţàśķ, śŵĩţćĥ ŕēćēńţ ţàśķś",
|
||||
"commandPaletteMode": "Ćōḿḿàńď ƥàĺēţţē ḿōďē",
|
||||
"commandPreviewAddTerminalPanel": "Àďď Ţēŕḿĩńàĺ Ƥàńēĺ",
|
||||
"commandPreviewCommitChanges": "Ćōḿḿĩţ Ćĥàńĝēś",
|
||||
@@ -63,10 +98,19 @@
|
||||
"commandPreviewPull": "Ƥũĺĺ",
|
||||
"commandPreviewSearchTasks": "Śēàŕćĥ Ţàśķś",
|
||||
"commandPromptsSettingsKeywords": "ƥŕōḿƥţś, ƥŕōḿƥţ śēţţĩńĝś, ćũśţōḿ ƥŕōḿƥţś, ƥŕōḿƥţ śńĩƥƥēţś, ƥŕōḿƥţ ţēḿƥĺàţēś",
|
||||
"commandPullKeywords": "ƥũĺĺ, ĝĩţ, ƥũĺĺ ćĥàńĝēś, ďōŵńĺōàď ćĥàńĝēś",
|
||||
"commandPushKeywords": "ƥũśĥ, ĝĩţ, ƥũśĥ ćĥàńĝēś, ƥũśĥ ţō ŕēḿōţē, ũƥĺōàď ćĥàńĝēś",
|
||||
"commandQuickChat": "Qũĩćķ Ćĥàţ",
|
||||
"commandQuickChatKeywords": "qũĩćķ ćĥàţ, ńēŵ qũĩćķ ćĥàţ, qũĩćķ qũēśţĩōń, àśķ àĝēńţ",
|
||||
"commandRebaseKeywords": "ŕēƀàśē, ĝĩţ, ƀŕàńćĥ",
|
||||
"commandSwitchToDarkMode": "Śŵĩţćĥ ţō Ďàŕķ Ḿōďē",
|
||||
"commandSwitchToKanbanView": "Śŵĩţćĥ ţō Ķàńƀàń Vĩēŵ",
|
||||
"commandSwitchToKanbanViewKeywords": "ķàńƀàń, ƀōàŕď, vĩēŵ",
|
||||
"commandSwitchToLightMode": "Śŵĩţćĥ ţō Ĺĩĝĥţ Ḿōďē",
|
||||
"commandSwitchToListView": "Śŵĩţćĥ ţō Ĺĩśţ Vĩēŵ",
|
||||
"commandSwitchToListViewKeywords": "ĺĩśţ, ţàƀĺē, vĩēŵ",
|
||||
"commandSwitchToPipelineView": "Śŵĩţćĥ ţō Ƥĩƥēĺĩńē Vĩēŵ",
|
||||
"commandSwitchToPipelineViewKeywords": "ƥĩƥēĺĩńē, ĝŕàƥĥ, vĩēŵ",
|
||||
"commandThemeKeywords": "ţĥēḿē, ćōĺōŕ ţĥēḿē, àƥƥēàŕàńćē",
|
||||
"commandWorkspaceSettingsKeywords": "ŵōŕķśƥàćē, ŵōŕķśƥàćēś",
|
||||
"commitsAndPushesToTheCurrent": "Ćōḿḿĩţś àńď ƥũśĥēś ţō ţĥē ćũŕŕēńţ ƀŕàńćĥ.",
|
||||
@@ -126,6 +170,29 @@
|
||||
"forwardStdinStdoutStraightToThe": "Ƒōŕŵàŕď śţďĩń/śţďōũţ śţŕàĩĝĥţ ţō ţĥē ĆĹĨ śũƀƥŕōćēśś. Ďĩśàƀĺēś ÀĆƤ.",
|
||||
"getStarted": "Ĝēţ Śţàŕţēď",
|
||||
"githubCliNotAuthenticated": "ĜĩţĤũƀ ĆĹĨ ńōţ àũţĥēńţĩćàţēď",
|
||||
"gitOpAmendCommit": "Àḿēńď ćōḿḿĩţ",
|
||||
"gitOpCommit": "Ćōḿḿĩţ",
|
||||
"gitOperationFailedInRepos_one": "Ƒàĩĺēď ĩń {{count}} ŕēƥō — {{summary}}",
|
||||
"gitOperationFailedInRepos_other": "Ƒàĩĺēď ĩń {{count}} ŕēƥōś — {{summary}}",
|
||||
"gitOperationPartialDescription_one": "{{operation}} śũććēēďēď ĩń {{succeeded}} ōƒ {{count}} ŕēƥō ({{repos}}); ƒàĩĺēď ĩń {{summary}}",
|
||||
"gitOperationPartialDescription_other": "{{operation}} śũććēēďēď ĩń {{succeeded}} ōƒ {{count}} ŕēƥōś ({{repos}}); ƒàĩĺēď ĩń {{summary}}",
|
||||
"gitOperationPartiallySucceeded": "{{operation}} ƥàŕţĩàĺĺŷ śũććēēďēď",
|
||||
"gitOperationRepoFailure": "{{repo}}: {{error}}",
|
||||
"gitOperationSucceededInRepos_one": "{{operation}} śũććēēďēď ĩń {{count}} ŕēƥō: {{repos}}",
|
||||
"gitOperationSucceededInRepos_other": "{{operation}} śũććēēďēď ĩń {{count}} ŕēƥōś: {{repos}}",
|
||||
"gitOpForcePush": "Ƒōŕćē Ƥũśĥ",
|
||||
"gitOpHardReset": "Ĥàŕď ŕēśēţ",
|
||||
"gitOpMerge": "Ḿēŕĝē",
|
||||
"gitOpPull": "Ƥũĺĺ",
|
||||
"gitOpPush": "Ƥũśĥ",
|
||||
"gitOpRebase": "Ŕēƀàśē",
|
||||
"gitOperationCompleted": "{{operation}} ćōḿƥĺēţēď",
|
||||
"gitOperationFailed": "{{operation}} ƒàĩĺēď",
|
||||
"gitOperationRunning": "{{operation}}...",
|
||||
"gitOperationScoped": "{{operation}} ({{repo}})",
|
||||
"gitOperationSucceeded": "{{operation}} śũććēśśƒũĺ",
|
||||
"gitOpRevertCommit": "Ŕēvēŕţ ćōḿḿĩţ",
|
||||
"gitOpSoftReset": "Śōƒţ ŕēśēţ",
|
||||
"gitScopeSkipped": "Śķĩƥƥēď {{scope}} ƀēćàũśē à ćĥĩĺď ŕēƥōśĩţōŕŷ ōƥēŕàţĩōń ƒàĩĺēď ({{failedScopes}}).",
|
||||
"goToFirstPage": "Ĝō ţō ƒĩŕśţ ƥàĝē",
|
||||
"goToLastPage": "Ĝō ţō ĺàśţ ƥàĝē",
|
||||
@@ -234,6 +301,7 @@
|
||||
"probing": "Ƥŕōƀĩńĝ",
|
||||
"profileName": "Ƥŕōƒĩĺē ńàḿē",
|
||||
"prompts": "Ƥŕōḿƥţś",
|
||||
"reference": "Ŕēƒēŕēńćē",
|
||||
"refreshAriaLabel": "Ŕēƒŕēśĥ {{label}}",
|
||||
"refreshBranchesGitFetch": "Ŕēƒŕēśĥ ƀŕàńćĥēś (ĝĩţ ƒēţćĥ)",
|
||||
"refreshingLabel": "Ŕēƒŕēśĥĩńĝ {{label}}...",
|
||||
@@ -275,6 +343,7 @@
|
||||
"scopeFiles": "Ƒĩĺēś",
|
||||
"scopeTitleWithShortcut": "{{label}} ({{shortcut}})",
|
||||
"scratchWorkspace": "śćŕàţćĥ ŵōŕķśƥàćē",
|
||||
"script": "Śćŕĩƥţ",
|
||||
"searchEllipsis": "Śēàŕćĥ…",
|
||||
"searchFailedEditTheQueryOr": "Śēàŕćĥ ƒàĩĺēď. Ēďĩţ ţĥē qũēŕŷ ōŕ ŕēōƥēń śēàŕćĥ ţō ŕēţŕŷ.",
|
||||
"searchForFiles": "Śēàŕćĥ ƒōŕ ƒĩĺēś...",
|
||||
@@ -302,9 +371,12 @@
|
||||
"skip": "Śķĩƥ",
|
||||
"stagesAndCommitsChangesUsingConventional": "Śţàĝēś àńď ćōḿḿĩţś ćĥàńĝēś ũśĩńĝ Ćōńvēńţĩōńàĺ Ćōḿḿĩţś.",
|
||||
"status": "Śţàţũś",
|
||||
"step": "Śţēƥ",
|
||||
"subtaskTitle": "Śũƀţàśķ ţĩţĺē",
|
||||
"switchMode": "Śŵĩţćĥ ḿōďē",
|
||||
"system": "Śŷśţēḿ",
|
||||
"task": "Ţàśķ",
|
||||
"terminal": "Ţēŕḿĩńàĺ",
|
||||
"todos": "Ţōďōś",
|
||||
"taskLoading": "Ĺōàďĩńĝ ţàśķ...",
|
||||
"tasks": "Ţàśķś",
|
||||
@@ -333,19 +405,25 @@
|
||||
"typeACommand": "Ţŷƥē à ćōḿḿàńď...",
|
||||
"typeToSearchFiles": "Ţŷƥē ţō śēàŕćĥ ƒĩĺēś...",
|
||||
"typeToSearchTaskContents": "Ţŷƥē ţō śēàŕćĥ ţàśķ ćōńţēńţś…",
|
||||
"unavailable": "Ũńàvàĩĺàƀĺē",
|
||||
"unknown": "Ũńķńōŵń",
|
||||
"unknownError": "Ũńķńōŵń ēŕŕōŕ",
|
||||
"unknownPanel": "Ũńķńōŵń ƥàńēĺ: {{component}}",
|
||||
"unsavedChanges": "Ũńśàvēď ćĥàńĝēś",
|
||||
"untitledTask": "Ũńţĩţĺēď ţàśķ",
|
||||
"usefulCommandsAgentSkills": "Ũśēƒũĺ ćōḿḿàńďś & àĝēńţ śķĩĺĺś",
|
||||
"useScratchInstead": "Ũśē śćŕàţćĥ ĩńśţēàď",
|
||||
"useThisFolder": "Ũśē ţĥĩś ƒōĺďēŕ",
|
||||
"viewFullReleaseOnGithub": "Vĩēŵ ƒũĺĺ ŕēĺēàśē ōń ĜĩţĤũƀ",
|
||||
"waitsForCiAndAutomatedReviews": "Ŵàĩţś ƒōŕ ĆĨ àńď àũţōḿàţēď ŕēvĩēŵś, ţĥēń ƒĩxēś ƒàĩĺũŕēś àńď àďďŕēśśēś ćōḿḿēńţś.",
|
||||
"walkthrough": "Ŵàĺķţĥŕōũĝĥ",
|
||||
"whatSNew": "Ŵĥàţ'ś Ńēŵ",
|
||||
"whenItsDoneTheAgentOpensAPr": "Ŵĥēń ĩţ'ś ďōńē, ţĥē àĝēńţ ōƥēńś à ƥũĺĺ ŕēqũēśţ ţō <2>{{repo}}</2> ƒōŕ ţĥē ḿàĩńţàĩńēŕś ţō ŕēvĩēŵ, śàvĩńĝ ţĥēḿ ţĩḿē àńď śĥĩƥƥĩńĝ ţĥē ĩḿƥŕōvēḿēńţ ţō ēvēŕŷōńē.",
|
||||
"workflow": "Ŵōŕķƒĺōŵ",
|
||||
"workflowsControlTheStepsAutomationAnd": "Ŵōŕķƒĺōŵś ćōńţŕōĺ ţĥē śţēƥś, àũţōḿàţĩōń, àńď àĝēńţ ƀēĥàvĩōŕ ƒōŕ ŷōũŕ ţàśķś. Ŷōũ ćàń àďď ḿōŕē ŵōŕķƒĺōŵś ƒŕōḿ Śēţţĩńĝś.",
|
||||
"workspace": "Ŵōŕķśƥàćē",
|
||||
"workspaces": "Ŵōŕķśƥàćēś",
|
||||
"you": "Ŷōũ",
|
||||
"youCanUseTheNavigationOr": "Ŷōũ ćàń ũśē ţĥē ńàvĩĝàţĩōń ōŕ ŕēĺōàď ţō ţŕŷ àĝàĩń.",
|
||||
"yourAgentImplementsItInThe": "Ŷōũŕ àĝēńţ ĩḿƥĺēḿēńţś ĩţ ĩń ţĥē ķàńďēv ŕēƥōśĩţōŕŷ, ŵĩţĥ ţēśţś",
|
||||
"youVerifyAndTestTheChange": "Ŷōũ vēŕĩƒŷ àńď ţēśţ ţĥē ćĥàńĝē ĩń à śēćōńď ķàńďēv ĩńśţàńćē"
|
||||
|
||||
@@ -19,10 +19,17 @@
|
||||
"copyLine": "Ćōƥŷ ĺĩńē {{lineNumber}}",
|
||||
"copyPath": "Ćōƥŷ ƥàţĥ",
|
||||
"default": "ďēƒàũĺţ",
|
||||
"deleteFailed": "Ďēĺēţē ƒàĩĺēď",
|
||||
"deleteFile": "Ďēĺēţē ƒĩĺē",
|
||||
"disableWordWrap": "Ďĩśàƀĺē ŵōŕď ŵŕàƥ",
|
||||
"enableWordWrap": "Ēńàƀĺē ŵōŕď ŵŕàƥ",
|
||||
"errorWhileDeletingFile": "Àń ēŕŕōŕ ōććũŕŕēď ŵĥĩĺē ďēĺēţĩńĝ ţĥē ƒĩĺē",
|
||||
"errorWhileSavingFile": "Àń ēŕŕōŕ ōććũŕŕēď ŵĥĩĺē śàvĩńĝ ţĥē ƒĩĺē",
|
||||
"failedToDeleteFile": "Ƒàĩĺēď ţō ďēĺēţē ƒĩĺē",
|
||||
"failedToSaveFile": "Ƒàĩĺēď ţō śàvē ƒĩĺē",
|
||||
"failedToSendComment": "Ƒàĩĺēď ţō śēńď ćōḿḿēńţ",
|
||||
"fileSavedOverwritten": "Ƒĩĺē śàvēď (ōvēŕŵŕĩţţēń)",
|
||||
"fileSavedOverwrittenDescription": "Ţĥē ƒĩĺē ŵàś ḿōďĩƒĩēď ēxţēŕńàĺĺŷ. Ŷōũŕ vēŕśĩōń ŵàś śàvēď.",
|
||||
"foldUnchangedLines": "Ƒōĺď ũńćĥàńĝēď ĺĩńēś",
|
||||
"hideDiffIndicators": "Ĥĩďē ďĩƒƒ ĩńďĩćàţōŕś",
|
||||
"highlight": "Ĥĩĝĥĺĩĝĥţ",
|
||||
@@ -35,6 +42,7 @@
|
||||
"noDiffAvailable": "Ńō ďĩƒƒ àvàĩĺàƀĺē",
|
||||
"noEditorsConfigured": "Ńō ēďĩţōŕś ćōńƒĩĝũŕēď",
|
||||
"openFolder": "Ōƥēń ƒōĺďēŕ",
|
||||
"openTaskSessionBeforeSending": "Ōƥēń à ţàśķ śēśśĩōń ƀēƒōŕē śēńďĩńĝ ţō ţĥē àĝēńţ.",
|
||||
"openWith": "Ōƥēń ŵĩţĥ...",
|
||||
"pasteLink": "Ƥàśţē ĺĩńķ...",
|
||||
"pleaseTryAgain": "Ƥĺēàśē ţŕŷ àĝàĩń.",
|
||||
@@ -43,6 +51,7 @@
|
||||
"reload": "Ŕēĺōàď",
|
||||
"revertAllChanges": "Ŕēvēŕţ àĺĺ ćĥàńĝēś",
|
||||
"revertChanges": "Ŕēvēŕţ ćĥàńĝēś",
|
||||
"saveFailed": "Śàvē ƒàĩĺēď",
|
||||
"saving": "Śàvĩńĝ...",
|
||||
"sentToTheAgent": "Śēńţ ţō ţĥē àĝēńţ.",
|
||||
"showAllLines": "Śĥōŵ àĺĺ ĺĩńēś",
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"failedToDeleteReviewWatch": "Ƒàĩĺēď ţō ďēĺēţē ŕēvĩēŵ ŵàţćĥ",
|
||||
"failedToLoadGithubWorkspaceSettings": "Ƒàĩĺēď ţō ĺōàď ĜĩţĤũƀ ŵōŕķśƥàćē śēţţĩńĝś",
|
||||
"failedToMerge": "Ƒàĩĺēď ţō ḿēŕĝē",
|
||||
"failedToReRequestReview": "Ƒàĩĺēď ţō ŕē-ŕēqũēśţ ŕēvĩēŵ",
|
||||
"failedToResetIssueWatch": "Ƒàĩĺēď ţō ŕēśēţ ĩśśũē ŵàţćĥ",
|
||||
"failedToResetReviewWatch": "Ƒàĩĺēď ţō ŕēśēţ ŕēvĩēŵ ŵàţćĥ",
|
||||
"failedToSaveDefaultQueries": "Ƒàĩĺēď ţō śàvē ďēƒàũĺţ qũēŕĩēś",
|
||||
@@ -445,6 +446,7 @@
|
||||
"reviewCount_other": "{{count}} ŕēvĩēŵś",
|
||||
"reviewFollowUp": "Ŕēvĩēŵ ƒōĺĺōŵ-ũƥ",
|
||||
"reviewPermissions": "Ŕēvĩēŵ ƥēŕḿĩśśĩōńś",
|
||||
"reviewReRequestedFrom": "Ŕēvĩēŵ ŕē-ŕēqũēśţēď ƒŕōḿ {{reviewer}}",
|
||||
"reviewWatch": "ŕēvĩēŵ ŵàţćĥ",
|
||||
"reviewWatchCreated": "Ŕēvĩēŵ ŵàţćĥ ćŕēàţēď",
|
||||
"reviewWatchDeleted": "Ŕēvĩēŵ ŵàţćĥ ďēĺēţēď",
|
||||
@@ -468,6 +470,7 @@
|
||||
"selectWorkflow": "Śēĺēćţ ŵōŕķƒĺōŵ",
|
||||
"selectWorkspace": "Śēĺēćţ ŵōŕķśƥàćē",
|
||||
"selectedRepositories": "Śēĺēćţēď ŕēƥōśĩţōŕĩēś",
|
||||
"selectWorkspaceBeforeRequestingReview": "Śēĺēćţ à ŵōŕķśƥàćē ƀēƒōŕē ŕēqũēśţĩńĝ ŕēvĩēŵ.",
|
||||
"setTheseExactUrlsCreateAClientSecret": "Śēţ ţĥēśē ēxàćţ ŨŔĹś, ćŕēàţē à ćĺĩēńţ śēćŕēţ àńď ŵēƀĥōōķ śēćŕēţ, àńď ďōŵńĺōàď à ƥŕĩvàţē ķēŷ. Ţĥĩś ōńē-ţĩḿē śēţũƥ ēxƥĩŕēś {{expiresAt}}.",
|
||||
"showGithubApiLimits": "Śĥōŵ ĜĩţĤũƀ ÀƤĨ ĺĩḿĩţś",
|
||||
"showLess": "Śĥōŵ ĺēśś",
|
||||
|
||||
@@ -316,7 +316,6 @@
|
||||
"mrAutomationExplainAutoFixRounds": "Ēxƥĺàĩń àũţō-ƒĩx ŕōũńďś",
|
||||
"mrAutomationAutoFixPromptSaveFailed": "Ƒàĩĺēď ţō śàvē àũţō-ƒĩx ƥŕōḿƥţ.",
|
||||
"mrAutomationAutoFixPromptResetFailed": "Ƒàĩĺēď ţō ŕēśēţ àũţō-ƒĩx ƥŕōḿƥţ.",
|
||||
"mrPopoverOpenDetail": "Ōƥēń ḿēŕĝē ŕēqũēśţ ďēţàĩĺ",
|
||||
"mrPopoverOpenInGitlab": "Ōƥēń !{{mriid}} ĩń ĜĩţĹàƀ",
|
||||
"mrPopoverUnlink": "Ũńĺĩńķ !{{mriid}}",
|
||||
"mrPopoverPassRate": "Ƥàśś ŕàţē",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"allRepositories": "Àĺĺ ŕēƥōśĩţōŕĩēś",
|
||||
"allWorkflows": "Àĺĺ ŵōŕķƒĺōŵś",
|
||||
"allWorkflowsTitleCase": "Àĺĺ Ŵōŕķƒĺōŵś",
|
||||
"allWorkspaces": "Àĺĺ ŵōŕķśƥàćēś",
|
||||
"approvalRequired": "Àƥƥŕōvàĺ Ŕēqũĩŕēď",
|
||||
"archive": "Àŕćĥĩvē",
|
||||
"archiveCount_one": "Àŕćĥĩvē {{count}}",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user