feat(i18n): localize Settings → Workflows (#2201)

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
João Salavisa
2026-08-03 15:30:27 +02:00
committed by GitHub
parent 0cfdf86d02
commit 03d5f98cb6
41 changed files with 1552 additions and 464 deletions
@@ -12,6 +12,11 @@ import {
type Workspace,
} from "@/lib/types/http";
// Seeded step definitions used whenever no template supplies default steps —
// the Custom option, and any template whose `default_steps` is absent or empty.
// These names are PERSISTED as the workflow's step names, so they deliberately
// stay English — translating them would write localized values into the
// database.
export const DEFAULT_CUSTOM_STEPS: StepDefinition[] = [
{ name: "Todo", position: 0, color: "bg-slate-500" },
{ name: "In Progress", position: 1, color: "bg-blue-500" },
@@ -113,6 +118,7 @@ export function useWorkflowCreation({
const workflow: Workflow = {
id: toWorkflowId(tempId),
workspace_id: toWorkspaceId(workspace.id),
// Persisted workflow name; the fallback is data, not copy.
name: newWorkflowName.trim() || template?.name || "New Workflow",
description: template?.description,
workflow_template_id: template?.id,
@@ -1,6 +1,8 @@
"use client";
import { useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { t as translate } from "@/lib/i18n";
import Link from "@/components/routing/app-link";
import { useRouter } from "@/lib/routing/client-router";
import { IconGripVertical, IconArrowsShuffle } from "@tabler/icons-react";
@@ -100,8 +102,8 @@ function useWorkflowImportExport(
setIsExportDialogOpen(true);
} catch (error) {
toast({
title: "Failed to export workflows",
description: error instanceof Error ? error.message : "Request failed",
title: translate("workflows:failedToExportWorkflows"),
description: error instanceof Error ? error.message : translate("common:requestFailed"),
variant: "error",
});
}
@@ -126,16 +128,18 @@ function useWorkflowImportExport(
const created = result.created ?? [];
const skipped = result.skipped ?? [];
const parts: string[] = [];
if (created.length > 0) parts.push(`Created: ${created.join(", ")}`);
if (skipped.length > 0) parts.push(`Skipped (already exist): ${skipped.join(", ")}`);
toast({ title: "Import complete", description: parts.join(". ") });
if (created.length > 0)
parts.push(translate("workflows:importCreated", { names: created.join(", ") }));
if (skipped.length > 0)
parts.push(translate("workflows:importSkipped", { names: skipped.join(", ") }));
toast({ title: translate("workflows:importCompleteTitle"), description: parts.join(". ") });
setIsImportDialogOpen(false);
setImportYaml("");
if (created.length > 0) router.refresh();
} catch (error) {
toast({
title: "Failed to import workflows",
description: error instanceof Error ? error.message : "Invalid YAML",
title: translate("workflows:failedToImportWorkflows"),
description: error instanceof Error ? error.message : translate("workflows:invalidYaml"),
variant: "error",
});
} finally {
@@ -405,12 +409,13 @@ function WorkflowList({
}
function WorkflowDialogs({ page }: { page: ReturnType<typeof useWorkspaceWorkflowsPage> }) {
const { t } = useTranslation();
return (
<>
<WorkflowExportDialog
open={page.isExportDialogOpen}
onOpenChange={page.setIsExportDialogOpen}
title="Export Workflows"
title={t("workflows:exportWorkflowsTitle")}
content={page.exportYaml}
/>
<ImportWorkflowsDialog
@@ -443,6 +448,7 @@ export function WorkspaceWorkflowsClient({
workflows,
workflowTemplates,
}: WorkspaceWorkflowsClientProps) {
const { t } = useTranslation();
const page = useWorkspaceWorkflowsPage(workspace, workflows, workflowTemplates);
const [syncDialogOpen, setSyncDialogOpen] = useState(false);
@@ -454,17 +460,21 @@ export function WorkspaceWorkflowsClient({
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 className="text-2xl font-bold">{workspace.name}</h2>
<p className="text-sm text-muted-foreground mt-1">Manage workflows for this workspace.</p>
<p className="text-sm text-muted-foreground mt-1">
{t("workflows:manageWorkflowsForThisWorkspace")}
</p>
</div>
<Button asChild variant="outline" size="sm">
<Link href={`/settings/workspace/${workspace.id}`}>Workspace settings</Link>
<Link href={`/settings/workspace/${workspace.id}`}>
{t("workflows:workspaceSettings")}
</Link>
</Button>
</div>
<Separator />
<SettingsSection
icon={<IconArrowsShuffle className="h-5 w-5" />}
title="Workflows"
description="Create autonomous pipelines with automated transitions or manual workflows where you move tasks yourself"
title={t("workflows:workflows")}
description={t("workflows:workflowsSectionDescription")}
action={
<WorkflowSectionActions
onExport={page.handleExportAll}
@@ -540,7 +550,7 @@ function useWorkflowOrderDraft({
if (!workspace) return;
const persistedOrder = currentOrder.map((id) => idMappings.current.get(id) ?? id);
if (persistedOrder.some((id) => id.startsWith(TEMP_WORKFLOW_PREFIX))) {
throw new Error("Save workflows before ordering them");
throw new Error(translate("workflows:saveWorkflowsBeforeOrdering"));
}
if (persistedOrder.length > 0) {
await reorderWorkflowsAction(workspace.id, persistedOrder);
@@ -1,5 +1,6 @@
"use client";
import { useTranslation } from "react-i18next";
import { Button } from "@kandev/ui/button";
import { Label } from "@kandev/ui/label";
import { Input } from "@kandev/ui/input";
@@ -9,6 +10,9 @@ import { Textarea } from "@kandev/ui/textarea";
import { cn } from "@/lib/utils";
import type { WorkflowTemplate } from "@/lib/types/http";
const YAML_PLACEHOLDER =
"version: 1\ntype: kandev_workflow\nworkflows:\n - name: My Workflow\n steps: [...]";
type ImportWorkflowsDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -30,15 +34,16 @@ export function ImportWorkflowsDialog({
onImport,
importLoading,
}: ImportWorkflowsDialogProps) {
const { t } = useTranslation();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Import Workflows</DialogTitle>
<DialogTitle>{t("workflows:importWorkflowsTitle")}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Upload YAML file</Label>
<Label>{t("workflows:uploadYamlFile")}</Label>
<input
ref={fileInputRef}
type="file"
@@ -48,11 +53,11 @@ export function ImportWorkflowsDialog({
/>
</div>
<div className="space-y-2">
<Label>Or paste YAML</Label>
<Label>{t("workflows:orPasteYaml")}</Label>
<Textarea
placeholder={
"version: 1\ntype: kandev_workflow\nworkflows:\n - name: My Workflow\n steps: [...]"
}
// The placeholder is a sample of the kandev_workflow export
// payload — a wire format, so its keys stay untranslated.
placeholder={YAML_PLACEHOLDER}
value={importYaml}
onChange={(e) => onImportYamlChange(e.target.value)}
className="font-mono text-xs max-h-96 overflow-y-auto"
@@ -61,14 +66,14 @@ export function ImportWorkflowsDialog({
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
Cancel
{t("common:cancel")}
</Button>
<Button
onClick={onImport}
disabled={!importYaml.trim() || importLoading}
className="cursor-pointer"
>
{importLoading ? "Importing..." : "Import"}
{importLoading ? t("workflows:importing") : t("workflows:import")}
</Button>
</DialogFooter>
</DialogContent>
@@ -138,6 +143,7 @@ export function CreateWorkflowDialog({
onCreate,
createLoading = false,
}: CreateWorkflowDialogProps) {
const { t } = useTranslation();
const handleOpenChange = (nextOpen: boolean) => {
if (createLoading && !nextOpen) return;
onOpenChange(nextOpen);
@@ -150,14 +156,14 @@ export function CreateWorkflowDialog({
data-testid="create-workflow-dialog"
>
<DialogHeader>
<DialogTitle>Add Workflow</DialogTitle>
<DialogTitle>{t("workflows:addWorkflow")}</DialogTitle>
</DialogHeader>
<div className="space-y-6 overflow-y-auto">
<div className="space-y-2">
<Label htmlFor="workflowName">Name</Label>
<Label htmlFor="workflowName">{t("workflows:name")}</Label>
<Input
id="workflowName"
placeholder="My Project Workflow"
placeholder={t("workflows:workflowNamePlaceholder")}
value={workflowName}
onChange={(e) => onWorkflowNameChange(e.target.value)}
data-testid="workflow-name-input"
@@ -165,7 +171,7 @@ export function CreateWorkflowDialog({
</div>
{workflowTemplates.length > 0 && (
<div className="space-y-2">
<Label>Template</Label>
<Label>{t("workflows:template")}</Label>
<RadioGroup
value={selectedTemplateId ?? "custom"}
onValueChange={(v) => onSelectedTemplateChange(v === "custom" ? null : v)}
@@ -189,9 +195,9 @@ export function CreateWorkflowDialog({
>
<RadioGroupItem value="custom" id="custom" className="mt-0.5" />
<div className="flex flex-col gap-1.5">
<span className="font-medium">Custom</span>
<span className="font-medium">{t("workflows:customTemplate")}</span>
<span className="text-sm text-muted-foreground">
Create your own agentic workflow from scratch.
{t("workflows:customTemplateDescription")}
</span>
</div>
</label>
@@ -207,7 +213,7 @@ export function CreateWorkflowDialog({
disabled={createLoading}
className="cursor-pointer"
>
Cancel
{t("common:cancel")}
</Button>
<Button
onClick={onCreate}
@@ -216,7 +222,7 @@ export function CreateWorkflowDialog({
data-testid="confirm-create-workflow"
data-dialog-default-action
>
{createLoading ? "Adding..." : "Add Workflow"}
{createLoading ? t("workflows:adding") : t("workflows:addWorkflow")}
</Button>
</DialogFooter>
</DialogContent>
@@ -3,6 +3,7 @@
import { useRef, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { deleteWorkflowAction } from "@/app/actions/workspaces";
import { t } from "@/lib/i18n";
import type { Workflow, WorkflowStep } from "@/lib/types/http";
import { useToast } from "@/components/toast-provider";
import { SettingsSaveCancelledError, useSettingsSaveContributor } from "./settings-save-provider";
@@ -102,7 +103,7 @@ export function useWorkflowDraftContributor(args: WorkflowDraftContributorArgs)
revision: persistence.revision,
isDirty: args.isWorkflowDirty || stepsDirty,
canSave: workflow.name.trim().length > 0,
invalidReason: workflow.name.trim() ? undefined : "Workflow name is required",
invalidReason: workflow.name.trim() ? undefined : t("workflows:workflowNameIsRequired"),
save: async (submittedRevision) => {
if (!workflow.id.startsWith(TEMP_WORKFLOW_PREFIX)) {
await persistence.persistSubmittedDraft(submittedRevision);
@@ -121,8 +122,8 @@ export function useWorkflowDraftContributor(args: WorkflowDraftContributorArgs)
} catch (error) {
if (!guardReturned) throw error;
toast({
title: "Failed to save workflow changes",
description: error instanceof Error ? error.message : "Request failed",
title: t("workflows:failedToSaveWorkflowChanges"),
description: error instanceof Error ? error.message : t("common:requestFailed"),
variant: "error",
});
}
@@ -136,7 +137,7 @@ export function useWorkflowDraftContributor(args: WorkflowDraftContributorArgs)
if (workflow.id.startsWith(TEMP_WORKFLOW_PREFIX) && persistedDraft) {
await deleteWorkflowAction(persistedDraft.id);
} else if (persistence.saveFailedRef.current) {
throw new Error("Retry the partial workflow save before leaving");
throw new Error(t("workflows:retryPartialSaveBeforeLeaving"));
}
args.setWorkflowSteps(savedWorkflowSteps);
args.onDiscardWorkflow();
@@ -3,6 +3,7 @@
import { useEffect, useRef } from "react";
import type { Workflow, WorkflowStep } from "@/lib/types/http";
import { generateUUID } from "@/lib/utils";
import { t } from "@/lib/i18n";
import { useToast } from "@/components/toast-provider";
import { useSerializedMutationQueue } from "./use-serialized-mutation-queue";
import type { WorkflowMutationGuardController } from "./workflow-mutation-guard";
@@ -21,9 +22,12 @@ import {
bulkMoveTasks,
} from "@/app/actions/workspaces";
const FALLBACK_ERROR_MESSAGE = "Request failed";
const TEMP_WORKFLOW_PREFIX = "temp-workflow-";
function fallbackErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : t("common:requestFailed");
}
type WorkflowStepActionsParams = {
workflow: Workflow;
isNewWorkflow?: boolean;
@@ -71,6 +75,10 @@ function openStepDeleteDialog({
setStepDeleteOpen(true);
}
// Seeded default for a brand-new step. Both fields are PERSISTED verbatim —
// `name` becomes the step's stored name and `color` its Tailwind class — so
// neither is translated; translating the name would write a localized string
// into the database.
const NEW_STEP_DEFAULTS = { name: "New Step", color: "bg-slate-500" } as const;
function createDraftStep(workflow: Workflow, position: number): WorkflowStep {
@@ -87,11 +95,7 @@ function createDraftStep(workflow: Workflow, position: number): WorkflowStep {
function useWorkflowStepMutationQueue(toast: WorkflowStepActionsParams["toast"]) {
return useSerializedMutationQueue((errorTitle, error) => {
toast({
title: errorTitle,
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
variant: "error",
});
toast({ title: errorTitle, description: fallbackErrorMessage(error), variant: "error" });
});
}
@@ -132,8 +136,8 @@ function createRemoveWorkflowStepHandler(
({ task_count: taskCount } = await getStepTaskCount(stepId));
} catch (error) {
toast({
title: "Failed to check workflow step tasks",
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
title: t("workflows:failedToCheckWorkflowStepTasks"),
description: fallbackErrorMessage(error),
variant: "error",
});
return;
@@ -476,8 +480,8 @@ export function useWorkflowDeleteHandlers({
wfDel.setDeleteOpen(true);
} catch (error) {
toast({
title: "Failed to check workflow tasks",
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
title: t("workflows:failedToCheckWorkflowTasks"),
description: fallbackErrorMessage(error),
variant: "error",
});
} finally {
@@ -494,8 +498,8 @@ export function useWorkflowDeleteHandlers({
wfDel.setDeleteOpen(false);
} catch (error) {
toast({
title: "Failed to delete workflow",
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
title: t("workflows:failedToDeleteWorkflow"),
description: fallbackErrorMessage(error),
variant: "error",
});
}
@@ -515,8 +519,8 @@ export function useWorkflowDeleteHandlers({
wfDel.setDeleteOpen(false);
} catch (error) {
toast({
title: "Failed to migrate tasks",
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
title: t("workflows:failedToMigrateTasks"),
description: fallbackErrorMessage(error),
variant: "error",
});
} finally {
@@ -589,14 +593,14 @@ export function useStepDeleteHandlers({
target_step_id: stepDel.targetStepForMigration,
});
await deleteWorkflowStepAction(stepDel.stepToDelete!);
}, "Failed to migrate tasks");
}, t("workflows:failedToMigrateTasks"));
};
const handleDeleteStepAndTasks = async () => {
if (!stepDel.stepToDelete) return;
await runStepDelete(
() => deleteWorkflowStepAction(stepDel.stepToDelete!),
"Failed to delete step",
t("workflows:failedToDeleteStep"),
);
};
@@ -622,8 +626,8 @@ export async function handleExportWorkflow({
setExportOpen(true);
} catch (error) {
toast({
title: "Failed to export workflow",
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
title: t("workflows:failedToExportWorkflow"),
description: fallbackErrorMessage(error),
variant: "error",
});
}
@@ -0,0 +1,157 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import type { Workflow, WorkflowStep } from "@/lib/types/http";
import { StepDeleteDialog, WorkflowDeleteDialog } from "./workflow-card-dialogs";
/**
* These dialogs used to build their descriptions by concatenation — an English
* `s` for the plural and a leading-space suffix for the unsaved-changes hint.
* Both are now catalog messages (`count` + `_one`/`_other`, and a joined
* sentence), so the shapes that would silently regress are pinned here:
*
* - the singular / plural task counts in each of the three step variants,
* - the space between the base sentence and the unsaved-changes hint,
* - the step name arriving as an interpolated value rather than baked into
* the message (steps are renamed freely, so it is user data).
*/
const STEP_NAME = "In Review";
const NO_WORKFLOWS: Workflow[] = [];
const NO_STEPS: WorkflowStep[] = [];
// Only `id` and `name` are read (the migration <Select>), but the fixture
// satisfies the whole contract so a field added to WorkflowStep fails here
// rather than being silently absent behind an `as` cast.
function step(id: string, name: string): WorkflowStep {
return {
id,
workflow_id: "workflow-1" as WorkflowStep["workflow_id"],
name,
position: 0,
color: "bg-slate-500",
created_at: "",
updated_at: "",
};
}
function renderWorkflowDelete(taskCount: number | null, hasUnsavedChanges = false) {
render(
<WorkflowDeleteDialog
open
onOpenChange={vi.fn()}
workflowTaskCount={taskCount}
otherWorkflows={NO_WORKFLOWS}
targetWorkflowId=""
setTargetWorkflowId={vi.fn()}
targetWorkflowSteps={NO_STEPS}
targetStepId=""
setTargetStepId={vi.fn()}
migrateLoading={false}
deleteLoading={false}
onDelete={vi.fn()}
onMigrateAndDelete={vi.fn()}
hasUnsavedChanges={hasUnsavedChanges}
/>,
);
}
function renderStepDelete(
stepTaskCount: number | null,
stepsForMigration: WorkflowStep[] = [],
hasUnsavedChanges = false,
) {
render(
<StepDeleteDialog
open
onOpenChange={vi.fn()}
stepName={STEP_NAME}
stepTaskCount={stepTaskCount}
stepsForMigration={stepsForMigration}
targetStep=""
setTargetStep={vi.fn()}
loading={false}
pending={false}
onMigrateAndDelete={vi.fn()}
onDeleteAndTasks={vi.fn()}
hasUnsavedChanges={hasUnsavedChanges}
/>,
);
}
const migrationTarget = [step("step-2", "Done")];
describe("WorkflowDeleteDialog description", () => {
afterEach(cleanup);
it("uses the singular task form for exactly one task", () => {
renderWorkflowDelete(1);
expect(
screen.getByText(
"This workflow has 1 task. Choose where to migrate them, or delete the workflow and archive the tasks.",
),
).toBeTruthy();
});
it("uses the plural task form for more than one task", () => {
renderWorkflowDelete(3);
expect(
screen.getByText(
"This workflow has 3 tasks. Choose where to migrate them, or delete the workflow and archive the tasks.",
),
).toBeTruthy();
});
it("falls back to the no-tasks sentence and appends the unsaved-changes hint with a space", () => {
renderWorkflowDelete(0, true);
expect(
screen.getByText(
"This will permanently delete the workflow and all its steps. Unsaved workflow changes will be discarded.",
),
).toBeTruthy();
});
});
describe("StepDeleteDialog description", () => {
afterEach(cleanup);
it("names the step without tasks", () => {
renderStepDelete(0);
expect(
screen.getByText(`This will permanently delete the ${STEP_NAME} workflow step.`),
).toBeTruthy();
});
it("inflects the migration sentence on the task count", () => {
renderStepDelete(1, migrationTarget);
expect(
screen.getByText(
`${STEP_NAME} has 1 task. Choose where to migrate them, or delete the step and its tasks.`,
),
).toBeTruthy();
cleanup();
renderStepDelete(2, migrationTarget);
expect(
screen.getByText(
`${STEP_NAME} has 2 tasks. Choose where to migrate them, or delete the step and its tasks.`,
),
).toBeTruthy();
});
it("inflects the no-migration-target sentence on the task count", () => {
renderStepDelete(1);
expect(screen.getByText(`Deleting ${STEP_NAME} will also affect its 1 task.`)).toBeTruthy();
cleanup();
renderStepDelete(4);
expect(screen.getByText(`Deleting ${STEP_NAME} will also affect its 4 tasks.`)).toBeTruthy();
});
it("appends the unsaved-step hint as a separate sentence", () => {
renderStepDelete(2, migrationTarget, true);
expect(
screen.getByText(
`${STEP_NAME} has 2 tasks. Choose where to migrate them, or delete the step and its tasks. Unsaved step changes will be discarded.`,
),
).toBeTruthy();
});
});
@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { Button } from "@kandev/ui/button";
import { Label } from "@kandev/ui/label";
import {
@@ -28,12 +30,17 @@ type WorkflowDeleteDialogProps = {
hasUnsavedChanges: boolean;
};
function workflowDeleteDescription(taskCount: number | null, hasUnsavedChanges: boolean): string {
function workflowDeleteDescription(
t: TFunction,
taskCount: number | null,
hasUnsavedChanges: boolean,
): string {
const hasTasks = taskCount !== null && taskCount > 0;
const base = hasTasks
? `This workflow has ${taskCount} task${taskCount === 1 ? "" : "s"}. Choose where to migrate them, or delete the workflow and archive the tasks.`
: "This will permanently delete the workflow and all its steps.";
return `${base}${hasUnsavedChanges ? " Unsaved workflow changes will be discarded." : ""}`;
? t("workflows:deleteWorkflowWithTasks", { count: taskCount })
: t("workflows:deleteWorkflowNoTasks");
if (!hasUnsavedChanges) return base;
return `${base} ${t("workflows:unsavedWorkflowChangesDiscarded")}`;
}
export function WorkflowDeleteDialog({
@@ -52,23 +59,24 @@ export function WorkflowDeleteDialog({
onMigrateAndDelete,
hasUnsavedChanges,
}: WorkflowDeleteDialogProps) {
const { t } = useTranslation();
const hasTasks = workflowTaskCount !== null && workflowTaskCount > 0;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete workflow</DialogTitle>
<DialogTitle>{t("workflows:deleteWorkflowDialogTitle")}</DialogTitle>
<DialogDescription>
{workflowDeleteDescription(workflowTaskCount, hasUnsavedChanges)}
{workflowDeleteDescription(t, workflowTaskCount, hasUnsavedChanges)}
</DialogDescription>
</DialogHeader>
{hasTasks && otherWorkflows.length > 0 && (
<div className="space-y-3 py-2">
<div className="space-y-2">
<Label>Target Workflow</Label>
<Label>{t("workflows:targetWorkflow")}</Label>
<Select value={targetWorkflowId} onValueChange={setTargetWorkflowId}>
<SelectTrigger>
<SelectValue placeholder="Select workflow" />
<SelectValue placeholder={t("workflows:selectWorkflow")} />
</SelectTrigger>
<SelectContent>
{otherWorkflows.map((w) => (
@@ -81,10 +89,10 @@ export function WorkflowDeleteDialog({
</div>
{targetWorkflowSteps.length > 0 && (
<div className="space-y-2">
<Label>Target Step</Label>
<Label>{t("workflows:targetStep")}</Label>
<Select value={targetStepId} onValueChange={setTargetStepId}>
<SelectTrigger>
<SelectValue placeholder="Select step" />
<SelectValue placeholder={t("workflows:selectStep")} />
</SelectTrigger>
<SelectContent>
{targetWorkflowSteps.map((s) => (
@@ -105,7 +113,7 @@ export function WorkflowDeleteDialog({
onClick={() => onOpenChange(false)}
className="cursor-pointer"
>
Cancel
{t("common:cancel")}
</Button>
{hasTasks && otherWorkflows.length > 0 && (
<Button
@@ -114,7 +122,7 @@ export function WorkflowDeleteDialog({
disabled={!targetWorkflowId || !targetStepId || migrateLoading || deleteLoading}
className="cursor-pointer"
>
{migrateLoading ? "Migrating..." : "Migrate & Delete"}
{migrateLoading ? t("workflows:migrating") : t("workflows:migrateAndDelete")}
</Button>
)}
<Button
@@ -124,7 +132,7 @@ export function WorkflowDeleteDialog({
disabled={deleteLoading || migrateLoading}
className="cursor-pointer"
>
{hasTasks ? "Delete & Archive Tasks" : "Delete Workflow"}
{hasTasks ? t("workflows:deleteAndArchiveTasks") : t("workflows:deleteWorkflow")}
</Button>
</DialogFooter>
</DialogContent>
@@ -148,16 +156,18 @@ type StepDeleteDialogProps = {
};
function stepDeleteDescription(
t: TFunction,
stepName: string,
stepTaskCount: number | null,
hasMigrationTarget: boolean,
) {
if (!stepTaskCount) return `This will permanently delete the ${stepName} workflow step.`;
const taskLabel = `${stepTaskCount} task${stepTaskCount === 1 ? "" : "s"}`;
// `stepName` is user data (steps are renamed freely), so it always travels as
// an interpolated value and never as part of the message.
if (!stepTaskCount) return t("workflows:deleteStepNoTasks", { stepName });
if (hasMigrationTarget) {
return `${stepName} has ${taskLabel}. Choose where to migrate them, or delete the step and its tasks.`;
return t("workflows:deleteStepWithMigration", { stepName, count: stepTaskCount });
}
return `Deleting ${stepName} will also affect its ${taskLabel}.`;
return t("workflows:deleteStepAffectsTasks", { stepName, count: stepTaskCount });
}
export function StepDeleteDialog({
@@ -174,24 +184,30 @@ export function StepDeleteDialog({
onDeleteAndTasks,
hasUnsavedChanges,
}: StepDeleteDialogProps) {
const { t } = useTranslation();
const hasTasks = stepTaskCount !== null && stepTaskCount > 0;
const description = stepDeleteDescription(stepName, stepTaskCount, stepsForMigration.length > 0);
const baseDescription = stepDeleteDescription(
t,
stepName,
stepTaskCount,
stepsForMigration.length > 0,
);
const description = hasUnsavedChanges
? `${baseDescription} ${t("workflows:unsavedStepChangesDiscarded")}`
: baseDescription;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete step</DialogTitle>
<DialogDescription>
{description}
{hasUnsavedChanges ? " Unsaved step changes will be discarded." : ""}
</DialogDescription>
<DialogTitle>{t("workflows:deleteStepDialogTitle")}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{stepsForMigration.length > 0 && (
<div className="space-y-2 py-2">
<Label>Target Step</Label>
<Label>{t("workflows:targetStep")}</Label>
<Select value={targetStep} onValueChange={setTargetStep} disabled={loading || pending}>
<SelectTrigger>
<SelectValue placeholder="Select step" />
<SelectValue placeholder={t("workflows:selectStep")} />
</SelectTrigger>
<SelectContent>
{stepsForMigration.map((s) => (
@@ -205,7 +221,7 @@ export function StepDeleteDialog({
)}
{pending && !loading && (
<p className="text-sm text-muted-foreground" role="status">
Waiting for the failed change to be retried.
{t("workflows:waitingForRetry")}
</p>
)}
<DialogFooter>
@@ -215,7 +231,7 @@ export function StepDeleteDialog({
onClick={() => onOpenChange(false)}
className="cursor-pointer"
>
Cancel
{t("common:cancel")}
</Button>
{stepsForMigration.length > 0 && (
<Button
@@ -224,7 +240,7 @@ export function StepDeleteDialog({
disabled={!targetStep || loading || pending}
className="cursor-pointer"
>
{loading ? "Migrating..." : "Migrate & Delete Step"}
{loading ? t("workflows:migrating") : t("workflows:migrateAndDeleteStep")}
</Button>
)}
<Button
@@ -234,7 +250,7 @@ export function StepDeleteDialog({
disabled={loading || pending}
className="cursor-pointer"
>
{hasTasks ? "Delete Step & Tasks" : "Delete Step"}
{hasTasks ? t("workflows:deleteStepAndTasks") : t("workflows:deleteStep")}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,6 +1,7 @@
"use client";
import { IconDownload, IconTrash } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { Button } from "@kandev/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useToast } from "@/components/toast-provider";
@@ -17,9 +18,6 @@ type WorkflowCardHeaderActionsProps = {
readOnly: boolean;
};
const SYNCED_READ_ONLY_REASON =
"Managed by workflow sync — edit or remove it in the synced repository";
export function WorkflowCardHeaderActions({
workflowId,
setExportYaml,
@@ -30,13 +28,14 @@ export function WorkflowCardHeaderActions({
exportDisabled,
readOnly,
}: WorkflowCardHeaderActionsProps) {
const { t } = useTranslation();
return (
<div className="flex flex-wrap justify-end gap-2">
<Tooltip>
<TooltipTrigger asChild>
<span
tabIndex={exportDisabled ? 0 : undefined}
aria-label={exportDisabled ? "Save the workflow before exporting." : undefined}
aria-label={exportDisabled ? t("workflows:saveBeforeExporting") : undefined}
>
<Button
type="button"
@@ -48,11 +47,11 @@ export function WorkflowCardHeaderActions({
disabled={exportDisabled}
>
<IconDownload className="h-4 w-4 mr-2" />
Export
{t("workflows:export")}
</Button>
</span>
</TooltipTrigger>
{exportDisabled && <TooltipContent>Save the workflow before exporting.</TooltipContent>}
{exportDisabled && <TooltipContent>{t("workflows:saveBeforeExporting")}</TooltipContent>}
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -63,8 +62,8 @@ export function WorkflowCardHeaderActions({
onClick={() => {
void onDeleteClick().catch((error) => {
toast({
title: "Failed to delete workflow",
description: error instanceof Error ? error.message : "Request failed",
title: t("workflows:failedToDeleteWorkflow"),
description: error instanceof Error ? error.message : t("common:requestFailed"),
variant: "error",
});
});
@@ -74,11 +73,11 @@ export function WorkflowCardHeaderActions({
data-testid="delete-workflow-button"
>
<IconTrash className="h-4 w-4 mr-2" />
Delete Workflow
{t("workflows:deleteWorkflow")}
</Button>
</span>
</TooltipTrigger>
{readOnly && <TooltipContent>{SYNCED_READ_ONLY_REASON}</TooltipContent>}
{readOnly && <TooltipContent>{t("workflows:syncedReadOnlyReason")}</TooltipContent>}
</Tooltip>
</div>
);
+19 -12
View File
@@ -1,6 +1,8 @@
"use client";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { t as translate } from "@/lib/i18n";
import { CardContent } from "@kandev/ui/card";
import { Input } from "@kandev/ui/input";
import { Label } from "@kandev/ui/label";
@@ -75,7 +77,8 @@ function useWorkflowSteps(
setSavedWorkflowSteps(res.steps ?? []);
}
} catch {
if (!cancelled) toast({ title: "Failed to load workflow steps", variant: "error" });
if (!cancelled)
toast({ title: translate("workflows:failedToLoadWorkflowSteps"), variant: "error" });
} finally {
if (!cancelled) setWorkflowLoading(false);
}
@@ -243,19 +246,20 @@ function WorkflowCardBody({
stepActions,
readOnly,
}: WorkflowCardBodyProps) {
const { t } = useTranslation();
const healthyProfiles = useHealthyAgentProfiles(workflow.agent_profile_id);
return (
<>
<Label>Workflow details</Label>
<Label>{t("workflows:workflowDetails")}</Label>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-2">
<div className="flex-1 space-y-1.5">
<Label className="flex items-center gap-2">
<span>Workflow Name</span>
<span>{t("workflows:workflowName")}</span>
{readOnly && <WorkflowSyncedBadge sourcePath={workflow.source_path} />}
{readOnly && (
<span className="text-xs text-muted-foreground">
Read-only — managed by workflow sync
{t("workflows:readOnlyManagedBySync")}
</span>
)}
</Label>
@@ -268,8 +272,8 @@ function WorkflowCardBody({
</div>
<div className="w-full space-y-1.5 sm:w-[240px] sm:shrink-0">
<Label className="flex items-center gap-1">
<span>Agent Profile</span>
<HelpTip text="Default agent profile for tasks in this workflow. When set, the agent selector is locked in the task creation dialog." />
<span>{t("workflows:agentProfile")}</span>
<HelpTip text={t("workflows:agentProfileHelp")} />
</Label>
<Select
value={workflow.agent_profile_id || "none"}
@@ -287,11 +291,11 @@ function WorkflowCardBody({
"agent_profile_id",
)}
>
<SelectValue placeholder="None (use task default)" />
<SelectValue placeholder={t("workflows:noneUseTaskDefault")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none" className="cursor-pointer">
None (use task default)
{t("workflows:noneUseTaskDefault")}
</SelectItem>
{healthyProfiles.map((p) => (
<SelectItem key={p.id} value={p.id} className="cursor-pointer">
@@ -303,9 +307,9 @@ function WorkflowCardBody({
</div>
</div>
<div className="space-y-2">
<Label>Workflow Steps</Label>
<Label>{t("workflows:workflowSteps")}</Label>
{workflowLoading ? (
<div className="text-sm text-muted-foreground">Loading workflow steps...</div>
<div className="text-sm text-muted-foreground">{t("workflows:loadingWorkflowSteps")}</div>
) : (
<WorkflowPipelineEditor
steps={workflowSteps}
@@ -338,6 +342,7 @@ function WorkflowCardDialogs({
hasUnsavedChanges,
mutationGuard,
}: WorkflowCardDialogsProps) {
const { t } = useTranslation();
return (
<>
<WorkflowDeleteDialog
@@ -359,7 +364,7 @@ function WorkflowCardDialogs({
<WorkflowExportDialog
open={exportOpen}
onOpenChange={setExportOpen}
title="Export Workflow"
title={t("workflows:exportWorkflowTitle")}
content={exportYaml}
/>
<StepDeleteDialog
@@ -475,6 +480,7 @@ function useWorkflowCardState(props: WorkflowCardProps) {
}
export function WorkflowCard(props: WorkflowCardProps) {
const { t } = useTranslation();
const { workflow, savedWorkflow, otherWorkflows = [], onUpdateWorkflow } = props;
const s = useWorkflowCardState(props);
const visibleSavedSteps = savedWorkflow ? s.savedWorkflowSteps : [];
@@ -529,7 +535,8 @@ export function WorkflowCard(props: WorkflowCardProps) {
exportYaml={s.exportYaml}
stepDel={s.stepDel}
stepToDeleteName={
s.workflowSteps.find((step) => step.id === s.stepDel.stepToDelete)?.name ?? "selected"
s.workflowSteps.find((step) => step.id === s.stepDel.stepToDelete)?.name ??
t("workflows:selectedStepFallback")
}
stepsForStepMigration={s.stepsForStepMigration}
stepDeleteHandlers={s.stepDeleteHandlers}
@@ -1,6 +1,7 @@
"use client";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { IconAlertTriangle, IconArrowRight, IconUser } from "@tabler/icons-react";
import { Alert, AlertDescription, AlertTitle } from "@kandev/ui/alert";
import {
@@ -20,27 +21,28 @@ import type {
import type { WorkflowMutationProposal } from "./workflow-mutation-guard";
import { cn } from "@/lib/utils";
const PROMPT_SOURCE_TEXT = {
task_description: (stepName: string) =>
`"${stepName}" has no step prompt, so re-entering it sends the task description.`,
step_prompt_with_task_description: (stepName: string) =>
`Re-entering "${stepName}" sends its rendered step prompt including the task description.`,
step_prompt: (stepName: string) =>
`Re-entering "${stepName}" sends its step prompt instead of the task description.`,
// `promptSource`, `trigger` and `actionKind` are wire values from the analyzer.
// Each maps to a catalog key resolved at render; the step name it describes is
// user data and travels as an interpolated value, never inside the message.
const PROMPT_SOURCE_KEYS = {
task_description: "workflows:promptSourceTaskDescription",
step_prompt_with_task_description: "workflows:promptSourceStepPromptWithTaskDescription",
step_prompt: "workflows:promptSourceStepPrompt",
} as const;
const TRIGGER_LABELS = {
on_turn_start: "On turn start",
on_turn_complete: "On turn complete",
const TRIGGER_LABEL_KEYS = {
on_turn_start: "workflows:onTurnStartTrigger",
on_turn_complete: "workflows:onTurnCompleteTrigger",
} as const;
const ACTION_LABELS = {
move_to_next: "Move to next step",
move_to_previous: "Move to previous step",
move_to_step: "Move to specific step",
const ACTION_LABEL_KEYS = {
move_to_next: "workflows:moveToNextStep",
move_to_previous: "workflows:moveToPreviousStep",
move_to_step: "workflows:moveToSpecificStep",
} as const;
function CycleHop({ hop, index }: { hop: WorkflowReplayCycleHop; index: number }) {
const { t } = useTranslation();
return (
<li className="min-w-0 rounded-md border bg-background/60 p-3">
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2">
@@ -52,19 +54,19 @@ function CycleHop({ hop, index }: { hop: WorkflowReplayCycleHop; index: number }
</div>
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
<span className="whitespace-nowrap rounded bg-muted px-2 py-1">
{TRIGGER_LABELS[hop.trigger]}
{t(TRIGGER_LABEL_KEYS[hop.trigger])}
</span>
<span className="whitespace-nowrap rounded bg-muted px-2 py-1">
{ACTION_LABELS[hop.actionKind]}
{t(ACTION_LABEL_KEYS[hop.actionKind])}
</span>
</div>
{hop.requiresUserInvolvement && (
<div className="mt-2 flex items-center gap-1.5 text-xs font-medium text-foreground">
<IconUser className="size-3.5 shrink-0" aria-hidden="true" />
<span>User action required</span>
<span>{t("workflows:userActionRequired")}</span>
</div>
)}
<span className="sr-only">Hop {index + 1}</span>
<span className="sr-only">{t("workflows:hopNumber", { index: index + 1 })}</span>
</li>
);
}
@@ -74,8 +76,11 @@ export function WorkflowCycleDiagnostic({
}: {
diagnostic: WorkflowReplayCycleDiagnostic;
}) {
const { t } = useTranslation();
const isBlocking = diagnostic.severity === "blocking";
const promptText = PROMPT_SOURCE_TEXT[diagnostic.promptSource](diagnostic.autoStartStepName);
const promptText = t(PROMPT_SOURCE_KEYS[diagnostic.promptSource], {
stepName: diagnostic.autoStartStepName,
});
return (
<Alert
@@ -88,16 +93,24 @@ export function WorkflowCycleDiagnostic({
>
<IconAlertTriangle className="mt-0.5 size-4" aria-hidden="true" />
<AlertTitle className="text-sm">
{isBlocking ? "Automatic workflow cycle" : "Potential repeated agent run"}
{isBlocking
? t("workflows:automaticWorkflowCycle")
: t("workflows:potentialRepeatedAgentRun")}
</AlertTitle>
<AlertDescription className="min-w-0 space-y-3 text-left text-sm text-pretty">
<p>
{isBlocking
? `This path re-enters "${diagnostic.autoStartStepName}" and can start the agent again without another user action.`
: `This path can re-enter "${diagnostic.autoStartStepName}" and start the agent again after a user action.`}
? t("workflows:workflowCycleBlockingBody", {
stepName: diagnostic.autoStartStepName,
})
: t("workflows:workflowCycleWarningBody", {
stepName: diagnostic.autoStartStepName,
})}
</p>
<ol
aria-label={`Replay path for ${diagnostic.autoStartStepName}`}
aria-label={t("workflows:workflowCycleReplayPathFor", {
stepName: diagnostic.autoStartStepName,
})}
className="grid min-w-0 gap-2"
>
{diagnostic.trace.map((hop, index) => (
@@ -125,8 +138,10 @@ export function WorkflowCycleGuardDialog({
onCancel,
onConfirm,
}: WorkflowCycleGuardDialogProps) {
const { t } = useTranslation();
const isBlocking = proposal?.severity === "blocking";
const actionLabel = proposal?.intent === "create" ? "Create anyway" : "Apply anyway";
const actionLabel =
proposal?.intent === "create" ? t("workflows:createAnyway") : t("workflows:applyAnyway");
const confirming = useRef(false);
const handleConfirm = async () => {
@@ -150,12 +165,12 @@ export function WorkflowCycleGuardDialog({
>
<AlertDialogHeader className="place-items-start p-4 pb-3 text-left sm:p-6 sm:pb-4">
<AlertDialogTitle className="text-lg">
{isBlocking ? "Workflow cycle blocked" : "Confirm workflow cycle"}
{isBlocking ? t("workflows:workflowCycleBlocked") : t("workflows:confirmWorkflowCycle")}
</AlertDialogTitle>
<AlertDialogDescription className="text-left text-sm">
{isBlocking
? "Change the workflow steps to remove the automatic cycle before continuing."
: "Review the repeated agent run before continuing."}
? t("workflows:workflowCycleBlockedDescription")
: t("workflows:confirmWorkflowCycleDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<div
@@ -170,7 +185,7 @@ export function WorkflowCycleGuardDialog({
</div>
<AlertDialogFooter className="border-t bg-background p-4 sm:px-6">
<AlertDialogCancel className="min-h-12 w-full cursor-pointer sm:w-auto">
{isBlocking ? "Return to workflow" : "Cancel"}
{isBlocking ? t("workflows:returnToWorkflow") : t("common:cancel")}
</AlertDialogCancel>
{!isBlocking && (
<AlertDialogAction
@@ -1,6 +1,7 @@
"use client";
import { IconCheck, IconCopy } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { Button } from "@kandev/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@kandev/ui/dialog";
import { Textarea } from "@kandev/ui/textarea";
@@ -19,6 +20,7 @@ export function WorkflowExportDialog({
title,
content,
}: WorkflowExportDialogProps) {
const { t } = useTranslation();
const { copied, copy } = useCopyToClipboard();
return (
@@ -30,7 +32,7 @@ export function WorkflowExportDialog({
<Textarea readOnly value={content} className="font-mono text-xs max-h-96 overflow-y-auto" />
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
Close
{t("workflows:close")}
</Button>
<Button onClick={() => copy(content)} className="cursor-pointer">
{copied ? (
@@ -38,7 +40,7 @@ export function WorkflowExportDialog({
) : (
<IconCopy className="h-4 w-4 mr-2" />
)}
{copied ? "Copied" : "Copy"}
{copied ? t("workflows:copied") : t("workflows:copy")}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,36 +1,47 @@
"use client";
import { IconInfoCircle } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip";
import type { WorkflowStep } from "@/lib/types/http";
import type { ScriptPlaceholder } from "@/components/settings/profile-edit/script-editor-completions";
export const STEP_PROMPT_PLACEHOLDERS: ScriptPlaceholder[] = [
{
key: "task_prompt",
description: "The original task description provided by the user",
example: "Implement user authentication with OAuth2",
executor_types: [],
},
];
/**
* Monaco completion entries for the step-prompt editor. `key` is the
* substitution token the backend expands (`{{task_prompt}}`) and is never
* translated; only the description and example are copy, so the list is built
* at render rather than frozen at module scope where `t()` cannot run.
*/
export function stepPromptPlaceholders(t: (key: string) => string): ScriptPlaceholder[] {
return [
{
key: "task_prompt",
description: t("workflows:stepPromptPlaceholderTaskPrompt"),
example: t("workflows:stepPromptPlaceholderExample"),
executor_types: [],
},
];
}
export function HelpTip({
text,
testId,
ariaLabel = "More information",
ariaLabel,
}: {
text: string;
testId?: string;
ariaLabel?: string;
}) {
const { t } = useTranslation();
const label = ariaLabel ?? t("workflows:moreInformation");
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground/50 hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label={ariaLabel}
className="inline-flex h-5 w-5 shrink-0 cursor-help items-center justify-center rounded-sm text-muted-foreground/50 hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label={label}
data-testid={testId}
>
<IconInfoCircle className="h-3.5 w-3.5" />
@@ -42,21 +53,26 @@ export function HelpTip({
);
}
// `value` is the Tailwind class persisted as `WorkflowStep.color`; only
// `labelKey` is copy, and it resolves at render (see `StepConfigHeader`).
export const STEP_COLORS = [
{ value: "bg-slate-500", label: "Gray" },
{ value: "bg-red-500", label: "Red" },
{ value: "bg-orange-500", label: "Orange" },
{ value: "bg-yellow-500", label: "Yellow" },
{ value: "bg-green-500", label: "Green" },
{ value: "bg-cyan-500", label: "Cyan" },
{ value: "bg-blue-500", label: "Blue" },
{ value: "bg-indigo-500", label: "Indigo" },
{ value: "bg-purple-500", label: "Purple" },
{ value: "bg-slate-500", labelKey: "workflows:colorGray" },
{ value: "bg-red-500", labelKey: "workflows:colorRed" },
{ value: "bg-orange-500", labelKey: "workflows:colorOrange" },
{ value: "bg-yellow-500", labelKey: "workflows:colorYellow" },
{ value: "bg-green-500", labelKey: "workflows:colorGreen" },
{ value: "bg-cyan-500", labelKey: "workflows:colorCyan" },
{ value: "bg-blue-500", labelKey: "workflows:colorBlue" },
{ value: "bg-indigo-500", labelKey: "workflows:colorIndigo" },
{ value: "bg-purple-500", labelKey: "workflows:colorPurple" },
];
// The `prompt` bodies are deliberately NOT translated: clicking a template
// writes the text into `WorkflowStep.prompt`, which is persisted and sent to
// the agent verbatim. Only the button `labelKey` is copy.
export const PROMPT_TEMPLATES = [
{
label: "Plan",
labelKey: "workflows:templatePlan",
prompt: `Analyze the task and create a detailed implementation plan.
{{task_prompt}}
@@ -70,7 +86,7 @@ INSTRUCTIONS:
Output the plan as a numbered list. Be specific about file paths, function names, and the approach for each step. Do NOT implement anything yet — only plan.`,
},
{
label: "Code Review",
labelKey: "workflows:templateCodeReview",
prompt: `Please review the changed files in the current git worktree.
STEP 1: Determine what to review
@@ -108,7 +124,7 @@ PERFORMANCE: Algorithmic or resource usage problems with measurable impact
Now review the changes.`,
},
{
label: "Security Audit",
labelKey: "workflows:templateSecurityAudit",
prompt: `Perform a security audit on the changed files in the current git worktree.
{{task_prompt}}
@@ -66,11 +66,11 @@ function StepAgentProfileSelect({
)}
>
<IconRobot className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<SelectValue placeholder="No profile override" />
<SelectValue placeholder={t("workflows:noProfileOverride")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none" className="cursor-pointer">
No profile override
{t("workflows:noProfileOverride")}
</SelectItem>
{healthyProfiles.map((p) => (
<SelectItem key={p.id} value={p.id} className="cursor-pointer">
@@ -83,8 +83,8 @@ function StepAgentProfileSelect({
testId={`${step.id}-agent-profile-help`}
text={
hasConditionalSessionConfig
? t("settings:removeConditionalSessionConfigBeforeProfile")
: t("settings:overrideAgentProfileHelp")
? t("workflows:removeConditionalSessionConfigBeforeProfile")
: t("workflows:overrideAgentProfileHelp")
}
/>
<SessionConfigToggle
@@ -120,6 +120,7 @@ function StepConfigHeader({
readOnly,
debouncedUpdateName,
}: StepConfigHeaderProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-3 border-b border-border px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 flex-1 flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
@@ -131,7 +132,7 @@ function StepConfigHeader({
onLocalNameChange(e.target.value);
debouncedUpdateName(e.target.value);
}}
placeholder="Step name"
placeholder={t("workflows:stepNamePlaceholder")}
disabled={readOnly}
className="h-8 w-full sm:max-w-[240px]"
data-settings-dirty={!savedStep || localName !== savedStep.name}
@@ -148,14 +149,14 @@ function StepConfigHeader({
className="h-8 w-full sm:w-[120px]"
data-settings-dirty={isWorkflowStepValueDirty(step, savedStep, (item) => item.color)}
>
<SelectValue placeholder="Color" />
<SelectValue placeholder={t("workflows:color")} />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start">
{STEP_COLORS.map((color) => (
<SelectItem key={color.value} value={color.value}>
<div className="flex items-center gap-2">
<div className={cn("w-3 h-3 rounded-full", color.value)} />
{color.label}
{t(color.labelKey)}
</div>
</SelectItem>
))}
@@ -177,7 +178,7 @@ function StepConfigHeader({
className="h-8 self-end cursor-pointer text-destructive hover:text-destructive sm:self-auto"
>
<IconTrash className="h-3.5 w-3.5 mr-1" />
Delete
{t("workflows:delete")}
</Button>
</div>
);
@@ -191,6 +192,7 @@ type StepAutoArchiveRowProps = {
};
function StepAutoArchiveRow({ step, savedStep, onUpdate, readOnly }: StepAutoArchiveRowProps) {
const { t } = useTranslation();
const isDirty = isWorkflowStepValueDirty(
step,
savedStep,
@@ -209,12 +211,12 @@ function StepAutoArchiveRow({ step, savedStep, onUpdate, readOnly }: StepAutoArc
data-settings-dirty={isDirty}
/>
<Label htmlFor={`${step.id}-auto-archive`} className="text-sm">
Auto-archive
{t("workflows:autoArchive")}
</Label>
<HelpTip text="Automatically archive tasks after they have been in this step for a set number of hours. Useful for the last step of a workflow (e.g., Done) to keep the board clean." />
<HelpTip text={t("workflows:autoArchiveHelp")} />
{(step.auto_archive_after_hours ?? 0) > 0 && (
<>
<span className="text-sm text-muted-foreground">after</span>
<span className="text-sm text-muted-foreground">{t("workflows:autoArchiveAfter")}</span>
<Input
id={`${step.id}-auto-archive-hours`}
type="number"
@@ -229,7 +231,7 @@ function StepAutoArchiveRow({ step, savedStep, onUpdate, readOnly }: StepAutoArc
disabled={readOnly}
data-settings-dirty={isDirty}
/>
<span className="text-sm text-muted-foreground">hours</span>
<span className="text-sm text-muted-foreground">{t("workflows:autoArchiveHours")}</span>
</>
)}
</div>
@@ -283,6 +285,102 @@ type StepBehaviorSectionProps = {
readOnly: boolean;
};
type StepToggleRowsProps = Omit<StepBehaviorSectionProps, "steps">;
// The six on-enter / board-behavior toggles. Split out of StepBehaviorSection
// so that component stays under the 100-line function cap.
function StepToggleRows({
step,
savedStep,
onUpdate,
toggleOnEnterAction,
readOnly,
}: StepToggleRowsProps) {
const { t } = useTranslation();
return (
<>
<StepCheckboxRow
id={`${step.id}-start-step`}
checked={step.is_start_step === true}
onCheckedChange={(c) => !readOnly && onUpdate({ is_start_step: c })}
disabled={readOnly}
label={t("workflows:startStep")}
helpText={t("workflows:startStepHelp")}
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) => item.is_start_step ?? false)}
/>
<StepCheckboxRow
id={`${step.id}-auto-start`}
checked={hasOnEnterAction(step, "auto_start_agent")}
onCheckedChange={() => !readOnly && toggleOnEnterAction("auto_start_agent")}
disabled={readOnly}
label={t("workflows:autoStartAgent")}
helpText={t("workflows:autoStartAgentHelp")}
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) =>
hasOnEnterAction(item, "auto_start_agent"),
)}
/>
<StepCheckboxRow
id={`${step.id}-plan-mode`}
checked={hasOnEnterAction(step, "enable_plan_mode")}
onCheckedChange={() => !readOnly && toggleOnEnterAction("enable_plan_mode")}
disabled={readOnly}
label={t("workflows:planMode")}
helpText={t("workflows:planModeHelp")}
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) =>
hasOnEnterAction(item, "enable_plan_mode"),
)}
/>
<StepCheckboxRow
id={`${step.id}-reset-context`}
checked={hasOnEnterAction(step, "reset_agent_context")}
onCheckedChange={() => !readOnly && toggleOnEnterAction("reset_agent_context")}
disabled={readOnly || !!step.agent_profile_id}
label={t("workflows:resetAgentContext")}
helpText={
step.agent_profile_id
? t("workflows:resetAgentContextHelpWithProfile")
: t("workflows:resetAgentContextHelp")
}
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) =>
hasOnEnterAction(item, "reset_agent_context"),
)}
/>
<StepCheckboxRow
id={`${step.id}-manual-move`}
checked={step.allow_manual_move !== false}
onCheckedChange={(c) => !readOnly && onUpdate({ allow_manual_move: c })}
disabled={readOnly}
label={t("workflows:allowManualMove")}
helpText={t("workflows:allowManualMoveHelp")}
isDirty={isWorkflowStepValueDirty(
step,
savedStep,
(item) => item.allow_manual_move ?? true,
)}
/>
<StepCheckboxRow
id={`${step.id}-command-panel`}
checked={step.show_in_command_panel !== false}
onCheckedChange={(c) => !readOnly && onUpdate({ show_in_command_panel: c })}
disabled={readOnly}
label={t("workflows:showInCommandPanel")}
helpText={t("workflows:showInCommandPanelHelp")}
isDirty={isWorkflowStepValueDirty(
step,
savedStep,
(item) => item.show_in_command_panel ?? false,
)}
/>
<StepAutoArchiveRow
step={step}
savedStep={savedStep}
onUpdate={onUpdate}
readOnly={readOnly}
/>
</>
);
}
function StepBehaviorSection({
step,
savedStep,
@@ -294,82 +392,11 @@ function StepBehaviorSection({
return (
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-x-6 gap-y-2">
<StepCheckboxRow
id={`${step.id}-start-step`}
checked={step.is_start_step === true}
onCheckedChange={(c) => !readOnly && onUpdate({ is_start_step: c })}
disabled={readOnly}
label="Start step"
helpText="New tasks start in this step. Only one step per workflow can be the start step."
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) => item.is_start_step ?? false)}
/>
<StepCheckboxRow
id={`${step.id}-auto-start`}
checked={hasOnEnterAction(step, "auto_start_agent")}
onCheckedChange={() => !readOnly && toggleOnEnterAction("auto_start_agent")}
disabled={readOnly}
label="Auto-start agent"
helpText="Automatically start the agent when a task enters this step."
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) =>
hasOnEnterAction(item, "auto_start_agent"),
)}
/>
<StepCheckboxRow
id={`${step.id}-plan-mode`}
checked={hasOnEnterAction(step, "enable_plan_mode")}
onCheckedChange={() => !readOnly && toggleOnEnterAction("enable_plan_mode")}
disabled={readOnly}
label="Plan mode"
helpText="Agent proposes a plan instead of making changes directly."
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) =>
hasOnEnterAction(item, "enable_plan_mode"),
)}
/>
<StepCheckboxRow
id={`${step.id}-reset-context`}
checked={hasOnEnterAction(step, "reset_agent_context")}
onCheckedChange={() => !readOnly && toggleOnEnterAction("reset_agent_context")}
disabled={readOnly || !!step.agent_profile_id}
label="Reset agent context"
helpText={
step.agent_profile_id
? "Not needed — switching agent profiles already creates a new session with fresh context."
: "Restart the agent with a fresh conversation context when entering this step. Useful for review steps that need an unbiased perspective."
}
isDirty={isWorkflowStepValueDirty(step, savedStep, (item) =>
hasOnEnterAction(item, "reset_agent_context"),
)}
/>
<StepCheckboxRow
id={`${step.id}-manual-move`}
checked={step.allow_manual_move !== false}
onCheckedChange={(c) => !readOnly && onUpdate({ allow_manual_move: c })}
disabled={readOnly}
label="Allow manual move"
helpText="Allow dragging tasks into this step on the board."
isDirty={isWorkflowStepValueDirty(
step,
savedStep,
(item) => item.allow_manual_move ?? true,
)}
/>
<StepCheckboxRow
id={`${step.id}-command-panel`}
checked={step.show_in_command_panel !== false}
onCheckedChange={(c) => !readOnly && onUpdate({ show_in_command_panel: c })}
disabled={readOnly}
label="Show in command panel"
helpText="Show tasks in this step when opening the command panel (Cmd+K). Useful for hiding backlog or done steps from quick access."
isDirty={isWorkflowStepValueDirty(
step,
savedStep,
(item) => item.show_in_command_panel ?? false,
)}
/>
<StepAutoArchiveRow
<StepToggleRows
step={step}
savedStep={savedStep}
onUpdate={onUpdate}
toggleOnEnterAction={toggleOnEnterAction}
readOnly={readOnly}
/>
</div>
@@ -411,6 +438,7 @@ function StepTransitionsSection({
toggleOnExitAction,
readOnly,
}: StepTransitionsSectionProps) {
const { t } = useTranslation();
const otherSteps = steps.filter((s) => s.id !== step.id);
const planModeEnabled = hasOnEnterAction(step, "enable_plan_mode");
@@ -418,7 +446,7 @@ function StepTransitionsSection({
<div className="space-y-3">
<div className="flex items-center gap-1.5">
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Transitions
{t("workflows:transitions")}
</Label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
@@ -452,8 +480,8 @@ function StepTransitionsSection({
{planModeEnabled && (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className="text-xs font-medium">On Exit</Label>
<HelpTip text="Runs when leaving this step (before entering the next step)." />
<Label className="text-xs font-medium">{t("workflows:onExit")}</Label>
<HelpTip text={t("workflows:onExitHelp")} />
</div>
<div className="flex items-center gap-2">
<Checkbox
@@ -469,9 +497,9 @@ function StepTransitionsSection({
)}
/>
<Label htmlFor={`${step.id}-exit-disable-plan`} className="text-sm">
Disable plan mode
{t("workflows:disablePlanMode")}
</Label>
<HelpTip text="Keep plan mode on for every turn in this step, then turn it off only when the task moves to another step." />
<HelpTip text={t("workflows:disablePlanModeOnExitHelp")} />
</div>
</div>
)}
@@ -47,10 +47,14 @@ describe("TurnCompleteSelect cancel completion policy", () => {
const checkbox = screen.getByTestId("step-1-cancel-completion-checkbox");
expect(checkbox.getAttribute("aria-checked")).toBe("false");
// This file stubs react-i18next with an identity `t`, so keys render as
// keys. `HelpTip`'s default aria-label is now a catalog key rather than a
// hardcoded string, so the expectation follows the same convention as the
// queryByText below.
expect(screen.getByTestId("step-1-cancel-completion-help").getAttribute("aria-label")).toBe(
"More information",
"workflows:moreInformation",
);
expect(screen.queryByText("settings:runCompletionActionsWhenTurnCancelledHelp")).toBeNull();
expect(screen.queryByText("workflows:runCompletionActionsWhenTurnCancelledHelp")).toBeNull();
fireEvent.click(checkbox);
expect(onUpdate).toHaveBeenCalledWith({ cancel_triggers_turn_complete: true });
});
@@ -1,9 +1,9 @@
"use client";
import { useTranslation } from "react-i18next";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { Checkbox } from "@kandev/ui/checkbox";
import { Label } from "@kandev/ui/label";
import { useTranslation } from "react-i18next";
import type {
WorkflowStep,
OnEnterAction,
@@ -120,12 +120,13 @@ export function TurnStartSelect({
setOnTurnStartTransition,
readOnly,
}: StepSelectProps & { setOnTurnStartTransition: (t: string) => void }) {
const { t } = useTranslation();
const transitionType = getOnTurnStartTransitionType(step);
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className="text-xs font-medium">On Turn Start</Label>
<HelpTip text="Runs when a user sends a message. Use for review cycles (e.g., move back to In Progress on feedback)." />
<Label className="text-xs font-medium">{t("workflows:onTurnStartLabel")}</Label>
<HelpTip text={t("workflows:onTurnStartHelp")} />
</div>
<Select
value={transitionType}
@@ -143,13 +144,13 @@ export function TurnStartSelect({
getOnTurnStartTransitionType,
)}
>
<SelectValue placeholder="Select action" />
<SelectValue placeholder={t("workflows:selectAction")} />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start">
<SelectItem value="none">Do nothing</SelectItem>
<SelectItem value="move_to_next">Move to next step</SelectItem>
<SelectItem value="move_to_previous">Move to previous step</SelectItem>
<SelectItem value="move_to_step">Move to specific step</SelectItem>
<SelectItem value="none">{t("workflows:doNothing")}</SelectItem>
<SelectItem value="move_to_next">{t("workflows:moveToNextStep")}</SelectItem>
<SelectItem value="move_to_previous">{t("workflows:moveToPreviousStep")}</SelectItem>
<SelectItem value="move_to_step">{t("workflows:moveToSpecificStep")}</SelectItem>
</SelectContent>
</Select>
{transitionType === "move_to_step" && (
@@ -178,7 +179,7 @@ export function TurnStartSelect({
?.step_id ?? "",
)}
>
<SelectValue placeholder="Select step" />
<SelectValue placeholder={t("workflows:selectStep")} />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start">
{otherSteps.map((s) => (
@@ -218,6 +219,7 @@ function TurnCompleteTargetSelect({
onUpdate,
readOnly,
}: StepSelectProps) {
const { t } = useTranslation();
const updateTarget = (stepId: string) => {
if (readOnly) return;
const currentEvents = step.events ?? {};
@@ -237,7 +239,7 @@ function TurnCompleteTargetSelect({
className="w-full h-8"
data-settings-dirty={isWorkflowStepValueDirty(step, savedStep, getTurnCompleteTargetStepId)}
>
<SelectValue placeholder="Select step" />
<SelectValue placeholder={t("workflows:selectStep")} />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start">
{otherSteps.map((candidate) => (
@@ -263,12 +265,13 @@ export function TurnCompleteSelect({
planModeEnabled,
readOnly,
}: TurnCompleteSelectProps) {
const { t } = useTranslation();
const transitionType = getTransitionType(step);
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className="text-xs font-medium">On Turn Complete</Label>
<HelpTip text="Runs after the agent finishes a turn. Use to auto-advance tasks through the pipeline." />
<Label className="text-xs font-medium">{t("workflows:onTurnCompleteLabel")}</Label>
<HelpTip text={t("workflows:onTurnCompleteHelp")} />
</div>
<Select
value={transitionType}
@@ -282,13 +285,13 @@ export function TurnCompleteSelect({
className="w-full h-8"
data-settings-dirty={isWorkflowStepValueDirty(step, savedStep, getTransitionType)}
>
<SelectValue placeholder="Select action" />
<SelectValue placeholder={t("workflows:selectAction")} />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start">
<SelectItem value="none">Do nothing (wait for user)</SelectItem>
<SelectItem value="move_to_next">Move to next step</SelectItem>
<SelectItem value="move_to_previous">Move to previous step</SelectItem>
<SelectItem value="move_to_step">Move to specific step</SelectItem>
<SelectItem value="none">{t("workflows:doNothingWaitForUser")}</SelectItem>
<SelectItem value="move_to_next">{t("workflows:moveToNextStep")}</SelectItem>
<SelectItem value="move_to_previous">{t("workflows:moveToPreviousStep")}</SelectItem>
<SelectItem value="move_to_step">{t("workflows:moveToSpecificStep")}</SelectItem>
</SelectContent>
</Select>
{transitionType === "move_to_step" && (
@@ -313,9 +316,9 @@ export function TurnCompleteSelect({
data-settings-dirty={isWorkflowStepValueDirty(step, savedStep, hasDisablePlanMode)}
/>
<Label htmlFor={`${step.id}-disable-plan`} className="text-sm">
Disable plan mode on complete
{t("workflows:disablePlanModeOnComplete")}
</Label>
<HelpTip text="Turn off plan mode after the agent finishes a turn, even when the task remains in this step." />
<HelpTip text={t("workflows:disablePlanModeOnCompleteHelp")} />
</div>
)}
{transitionType !== "none" && (
@@ -352,6 +355,7 @@ export function ChildrenCompletedSelect({
setChildrenCompletedTransition,
readOnly,
}: ChildrenCompletedSelectProps) {
const { t } = useTranslation();
const transitionType = getChildrenCompletedTransitionType(step);
const configuredTargetStepId =
(step.events?.on_children_completed?.find((a) => a.type === "move_to_step")?.config
@@ -361,11 +365,11 @@ export function ChildrenCompletedSelect({
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label className="text-xs font-medium">When Child Tasks Complete</Label>
<Label className="text-xs font-medium">{t("workflows:whenChildTasksComplete")}</Label>
<HelpTip
testId={`${step.id}-children-completed-help`}
ariaLabel="How child task completion transitions work"
text="Use this on a parent task step. When every active direct child task is COMPLETED, FAILED, or CANCELLED, Kandev runs this transition once. Archived and ephemeral child tasks are ignored. Grandchildren do not count here, and nothing runs if the parent has no child tasks."
ariaLabel={t("workflows:childrenCompletedHelpAria")}
text={t("workflows:childrenCompletedHelp")}
/>
</div>
<Select
@@ -388,14 +392,14 @@ export function ChildrenCompletedSelect({
getChildrenCompletedTransitionType,
)}
>
<SelectValue placeholder="Select action" />
<SelectValue placeholder={t("workflows:selectAction")} />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start">
<SelectItem value="none">Do nothing</SelectItem>
<SelectItem value="move_to_next">Move to next step</SelectItem>
<SelectItem value="move_to_previous">Move to previous step</SelectItem>
<SelectItem value="none">{t("workflows:doNothing")}</SelectItem>
<SelectItem value="move_to_next">{t("workflows:moveToNextStep")}</SelectItem>
<SelectItem value="move_to_previous">{t("workflows:moveToPreviousStep")}</SelectItem>
<SelectItem value="move_to_step" disabled={!defaultTargetStepId}>
Move to specific step
{t("workflows:moveToSpecificStep")}
</SelectItem>
</SelectContent>
</Select>
@@ -425,7 +429,7 @@ export function ChildrenCompletedSelect({
?.config?.step_id ?? "",
)}
>
<SelectValue placeholder="Select step" />
<SelectValue placeholder={t("workflows:selectStep")} />
</SelectTrigger>
<SelectContent position="popper" side="bottom" align="start">
{otherSteps.map((s) => (
@@ -443,6 +447,10 @@ export function ChildrenCompletedSelect({
);
}
// The MCP tool name the agent calls; an identifier, not copy, so it is
// interpolated as a value rather than living in the catalog.
const STEP_COMPLETE_TOOL = "step_complete_kandev";
// --- ExplicitCompletionToggle ---
// ADR 0015: when checked, on_turn_complete transitions only fire after the
// agent calls the `step_complete_kandev` MCP tool. Bare turn-end leaves the
@@ -483,9 +491,11 @@ export function ExplicitCompletionToggle({
htmlFor={`${step.id}-require-signal`}
className="flex min-h-11 min-w-0 cursor-pointer items-center text-sm md:min-h-0"
>
{t("settings:waitForAgentCompletionSignal")}
{t("workflows:waitForAgentCompletionSignal")}
</Label>
<HelpTip text="Only auto-advance once the agent calls step_complete_kandev. Otherwise turn-end is treated as completion." />
<HelpTip
text={t("workflows:waitForAgentCompletionSignalHelp", { tool: STEP_COMPLETE_TOOL })}
/>
</div>
);
}
@@ -521,11 +531,11 @@ export function CancelCompletionToggle({
data-testid={`${step.id}-cancel-completion-label`}
className="flex min-h-11 min-w-0 cursor-pointer items-center text-sm md:min-h-0"
>
{t("settings:runCompletionActionsWhenTurnCancelled")}
{t("workflows:runCompletionActionsWhenTurnCancelled")}
</Label>
<HelpTip
testId={`${step.id}-cancel-completion-help`}
text={t("settings:runCompletionActionsWhenTurnCancelledHelp")}
text={t("workflows:runCompletionActionsWhenTurnCancelledHelp")}
/>
</div>
);
@@ -1,5 +1,6 @@
"use client";
import { useTranslation } from "react-i18next";
import { Input } from "@kandev/ui/input";
import { Label } from "@kandev/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
@@ -28,6 +29,7 @@ export function StepWipControls({
onUpdate,
readOnly,
}: StepWipControlsProps) {
const { t } = useTranslation();
const otherSteps = steps.filter((s) => s.id !== step.id);
const pullFromValue = step.pull_from_step_id || "none";
const pullFromSelectID = `${step.id}-pull-from-step`;
@@ -37,9 +39,9 @@ export function StepWipControls({
<div className="space-y-1.5">
<div className="flex items-center gap-1.5">
<Label htmlFor={`${step.id}-wip-limit`} className="text-xs font-medium">
WIP limit
{t("workflows:wipLimit")}
</Label>
<HelpTip text="Maximum admitted (active) tasks in this step. Overflow stays visible in a queue. Use 0 for unlimited." />
<HelpTip text={t("workflows:wipLimitHelp")} />
</div>
<Input
id={`${step.id}-wip-limit`}
@@ -64,9 +66,9 @@ export function StepWipControls({
<div className="space-y-1.5">
<div className="flex items-center gap-1.5">
<Label htmlFor={pullFromSelectID} className="text-xs font-medium">
Pull from
{t("workflows:pullFrom")}
</Label>
<HelpTip text="Optional feeder step to pull queued work from when this step has capacity." />
<HelpTip text={t("workflows:pullFromHelp")} />
</div>
<Select
value={pullFromValue}
@@ -86,11 +88,11 @@ export function StepWipControls({
(item) => item.pull_from_step_id ?? "",
)}
>
<SelectValue placeholder="No feeder step" />
<SelectValue placeholder={t("workflows:noFeederStep")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none" className="cursor-pointer">
No feeder step
{t("workflows:noFeederStep")}
</SelectItem>
{otherSteps.map((candidate) => (
<SelectItem key={candidate.id} value={candidate.id} className="cursor-pointer">
@@ -100,10 +102,7 @@ export function StepWipControls({
</SelectContent>
</Select>
</div>
<p className="text-xs text-muted-foreground sm:col-span-2">
WIP limits active work, not visibility. Overflow remains on the board until capacity opens;
if a selected feeder is also full, new task creation is rejected.
</p>
<p className="text-xs text-muted-foreground sm:col-span-2">{t("workflows:wipLimitsNote")}</p>
</div>
);
}
@@ -1,6 +1,7 @@
"use client";
import { useMemo, useState, useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import {
DndContext,
closestCenter,
@@ -54,12 +55,14 @@ function getTransitionType(step: WorkflowStep): string {
return action?.type ?? "none";
}
function getTransitionLabel(step: WorkflowStep): string {
const t = getTransitionType(step);
if (t === "move_to_next") return "auto";
if (t === "move_to_previous") return "back";
if (t === "move_to_step") return "goto";
return "manual";
// The transition `type`s are wire values; only the connector caption is copy,
// so the mapping yields a catalog key that the caller resolves at render.
function transitionLabelKey(step: WorkflowStep): string {
const type = getTransitionType(step);
if (type === "move_to_next") return "workflows:transitionAuto";
if (type === "move_to_previous") return "workflows:transitionBack";
if (type === "move_to_step") return "workflows:transitionGoto";
return "workflows:transitionManual";
}
// --- Pipeline Node ---
@@ -83,6 +86,7 @@ function PipelineNode({
onRemove,
readOnly = false,
}: PipelineNodeProps) {
const { t } = useTranslation();
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: step.id,
});
@@ -115,7 +119,7 @@ function PipelineNode({
<IconRosetteNumber1 className="h-3.5 w-3.5" />
</div>
</TooltipTrigger>
<TooltipContent>Start step</TooltipContent>
<TooltipContent>{t("workflows:startStep")}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
@@ -140,15 +144,19 @@ function PipelineNode({
<StepCapabilityIcons
events={step.events}
agentProfileId={step.agent_profile_id}
fallback={<span className="text-xs text-muted-foreground/50">manual</span>}
fallback={
<span className="text-xs text-muted-foreground/50">
{t("workflows:transitionManual")}
</span>
}
/>
{isReplayCycleAffected && (
<span
className="flex items-center gap-1 text-xs font-medium text-amber-700 dark:text-amber-300"
aria-label={`${step.name} is part of a replay cycle`}
aria-label={t("workflows:stepIsPartOfReplayCycle", { stepName: step.name })}
>
<IconAlertTriangle className="size-3 shrink-0" aria-hidden="true" />
Cycle
{t("workflows:cycle")}
</span>
)}
</div>
@@ -170,14 +178,17 @@ function PipelineNode({
// --- Connector ---
function PipelineConnector({ label }: { label: string }) {
function PipelineConnector({ labelKey }: { labelKey: string }) {
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center shrink-0 px-1">
<div className="flex items-center gap-0.5 text-muted-foreground/60">
<div className="w-4 h-px bg-border" />
<IconChevronRight className="h-3 w-3" />
</div>
<span className="text-[10px] text-muted-foreground/50 leading-none mt-0.5">{label}</span>
<span className="text-[10px] text-muted-foreground/50 leading-none mt-0.5">
{t(labelKey)}
</span>
</div>
);
}
@@ -209,7 +220,7 @@ function PipelineArea({
<div className="flex items-center gap-0 py-2 px-1">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
{index > 0 && <PipelineConnector label={getTransitionLabel(steps[index - 1])} />}
{index > 0 && <PipelineConnector labelKey={transitionLabelKey(steps[index - 1])} />}
<PipelineNode
step={step}
isDirty={isWorkflowStepDirty(step, savedStepsById.get(step.id))}
@@ -1,6 +1,7 @@
"use client";
import { IconDownload, IconPlus, IconUpload } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { Button } from "@kandev/ui/button";
import { WorkflowSyncButton } from "@/components/settings/workflow-sync-section";
@@ -19,6 +20,7 @@ export function WorkflowSectionActions({
onAdd,
onGitHubSync,
}: WorkflowSectionActionsProps) {
const { t } = useTranslation();
return (
// sm:justify-end keeps wrapped rows right-aligned next to the section
// title; below sm the toolbar sits under the title, left-aligned.
@@ -32,7 +34,7 @@ export function WorkflowSectionActions({
className="cursor-pointer"
>
<IconDownload className="h-4 w-4 mr-2" />
Export All
{t("workflows:exportAll")}
</Button>
<Button
type="button"
@@ -42,7 +44,7 @@ export function WorkflowSectionActions({
className="cursor-pointer"
>
<IconUpload className="h-4 w-4 mr-2" />
Import
{t("workflows:import")}
</Button>
<Button
type="button"
@@ -52,7 +54,7 @@ export function WorkflowSectionActions({
data-testid="add-workflow-button"
>
<IconPlus className="h-4 w-4 mr-2" />
Add Workflow
{t("workflows:addWorkflow")}
</Button>
</div>
);
@@ -0,0 +1,56 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { i18n } from "@/lib/i18n";
import type { SessionConfigCarryWarning } from "@/lib/workflows/session-config-carry-analysis";
import { SessionConfigCarryWarningPanel } from "./workflow-session-config-carry-warning";
/**
* The carry-forward sentence used to be baked into the analyzer's result, which
* froze it at the locale of the last analysis: the panel's own labels would
* follow a locale switch while the sentence stayed behind. It is resolved here
* now, so these tests pin both the interpolation and that it tracks the locale.
*/
const WARNING: SessionConfigCarryWarning = {
agentName: "codex",
sourceStepId: "step-1",
sourceStepName: "In Progress",
configOptions: {},
};
function renderPanel() {
render(
<SessionConfigCarryWarningPanel warnings={[WARNING]} onChoose={vi.fn()} readOnly={false} />,
);
}
describe("SessionConfigCarryWarningPanel", () => {
afterEach(async () => {
cleanup();
await i18n.changeLanguage("en");
});
it("interpolates the source step and agent names into the sentence", () => {
renderPanel();
expect(
screen.getByText(
"Settings changed in In Progress may carry into this step for codex. Choose keep, restore original, or set new values.",
),
).toBeTruthy();
});
it("re-resolves the sentence when the locale changes while mounted", async () => {
renderPanel();
const english = screen.getByText(/Settings changed in In Progress/);
expect(english).toBeTruthy();
await i18n.changeLanguage("pseudo");
// The names are interpolated values, so they survive verbatim; the copy
// around them must have changed, which is what a baked-in message could not do.
const pseudo = screen.getByText(/In Progress/);
expect(pseudo.textContent).toContain("In Progress");
expect(pseudo.textContent).toContain("codex");
expect(pseudo.textContent).not.toContain("Settings changed in");
});
});
@@ -25,7 +25,7 @@ export function SessionConfigCarryWarningPanel({
>
<div className="flex items-start gap-2 text-xs text-amber-800 dark:text-amber-100">
<IconAlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{t("settings:sessionConfigCarryForwardWarning")}</span>
<span>{t("workflows:sessionConfigCarryForwardWarning")}</span>
</div>
<div className="space-y-2">
{warnings.map((warning) => (
@@ -33,7 +33,14 @@ export function SessionConfigCarryWarningPanel({
key={`${warning.agentName}-${warning.sourceStepId}`}
className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
>
<span className="min-w-0 text-xs text-muted-foreground">{warning.message}</span>
<span className="min-w-0 text-xs text-muted-foreground">
{/* Resolved here rather than in the analyzer so the sentence
follows a locale switch; the step and agent names are data. */}
{t("workflows:sessionConfigCarryForwardWarningMessage", {
stepName: warning.sourceStepName,
agentName: warning.agentName,
})}
</span>
<div className="flex shrink-0 flex-wrap gap-1.5">
<Button
type="button"
@@ -43,7 +50,7 @@ export function SessionConfigCarryWarningPanel({
disabled={readOnly || disabled}
onClick={() => onChoose(warning, "keep")}
>
{t("settings:sessionConfigKeep")}
{t("workflows:sessionConfigKeep")}
</Button>
<Button
type="button"
@@ -53,7 +60,7 @@ export function SessionConfigCarryWarningPanel({
disabled={readOnly || disabled}
onClick={() => onChoose(warning, "restore_original")}
>
{t("settings:sessionConfigRestore")}
{t("workflows:sessionConfigRestore")}
</Button>
<Button
type="button"
@@ -63,7 +70,7 @@ export function SessionConfigCarryWarningPanel({
disabled={readOnly || disabled}
onClick={() => onChoose(warning, "set")}
>
{t("settings:sessionConfigSetNew")}
{t("workflows:sessionConfigSetNew")}
</Button>
</div>
</div>
@@ -78,11 +78,11 @@ export function SessionConfigToggle({
data-testid={`${step.id}-override-original-session`}
/>
<Label htmlFor={`${step.id}-override-original-session`} className="text-sm">
{t("settings:overrideOriginalSessionOptions")}
{t("workflows:overrideOriginalSessionOptions")}
</Label>
<HelpTip
testId={`${step.id}-override-original-session-help`}
text={t("settings:overrideOriginalSessionOptionsHelp")}
text={t("workflows:overrideOriginalSessionOptionsHelp")}
/>
</div>
);
@@ -202,7 +202,7 @@ function SessionConfigBody({
<>
{step.agent_profile_id && action && (
<p className="rounded-md border border-amber-500/30 bg-amber-500/10 p-2 text-xs text-amber-200">
{t("settings:sessionConfigFixedProfileWarning")}
{t("workflows:sessionConfigFixedProfileWarning")}
</p>
)}
{!action && warnings.length > 0 && !step.agent_profile_id && (
@@ -297,7 +297,7 @@ function SessionConfigRuleList({
/>
))}
{rules.length === 0 && (
<p className="text-xs text-muted-foreground">{t("settings:sessionConfigEmptyRules")}</p>
<p className="text-xs text-muted-foreground">{t("workflows:sessionConfigEmptyRules")}</p>
)}
</div>
);
@@ -323,13 +323,13 @@ function SessionConfigOptionsHeader({
<div className="flex min-w-0 items-start gap-2">
<div className="min-w-0">
<Label className="text-sm font-medium">
{t("settings:configureOriginalSessionOptions")}
{t("workflows:configureOriginalSessionOptions")}
</Label>
<p className="text-xs text-muted-foreground">
{t("settings:configureOriginalSessionOptionsDescription")}
{t("workflows:configureOriginalSessionOptionsDescription")}
</p>
</div>
<HelpTip text={t("settings:sessionConfigRulesHelp")} />
<HelpTip text={t("workflows:sessionConfigRulesHelp")} />
</div>
{!readOnly && !step.agent_profile_id && (
<Button
@@ -342,7 +342,7 @@ function SessionConfigOptionsHeader({
data-testid={`${step.id}-add-session-config-rule`}
>
<IconPlus className="mr-1.5 h-4 w-4" />
{t("settings:addCondition")}
{t("workflows:addCondition")}
</Button>
)}
</div>
@@ -101,7 +101,7 @@ function SessionConfigRuleHeader({
return (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Label className="text-xs font-medium text-muted-foreground sm:w-24">
{t("settings:whenAgentIs")}
{t("workflows:whenAgentIs")}
</Label>
<Select
value={rule.agent_name}
@@ -120,7 +120,7 @@ function SessionConfigRuleHeader({
className="min-h-10 w-full cursor-pointer sm:flex-1"
data-testid={`session-config-agent-${index}`}
>
<SelectValue placeholder={t("settings:chooseAgentFamily")} />
<SelectValue placeholder={t("workflows:chooseAgentFamily")} />
</SelectTrigger>
<SelectContent>
{choices.map((choice) => (
@@ -144,9 +144,9 @@ function SessionConfigRuleHeader({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="set">{t("settings:setSettings")}</SelectItem>
<SelectItem value="keep">{t("settings:keepCurrent")}</SelectItem>
<SelectItem value="restore_original">{t("settings:restoreOriginal")}</SelectItem>
<SelectItem value="set">{t("workflows:setSettings")}</SelectItem>
<SelectItem value="keep">{t("workflows:keepCurrent")}</SelectItem>
<SelectItem value="restore_original">{t("workflows:restoreOriginal")}</SelectItem>
</SelectContent>
</Select>
<Button
@@ -156,7 +156,7 @@ function SessionConfigRuleHeader({
className="min-h-10 cursor-pointer self-end text-destructive hover:text-destructive sm:self-auto"
onClick={onRemove}
disabled={readOnly}
aria-label={t("settings:removeAgentCondition", { index: index + 1 })}
aria-label={t("workflows:removeAgentCondition", { index: index + 1 })}
>
<IconTrash className="h-4 w-4" />
</Button>
@@ -209,13 +209,13 @@ function SessionConfigRuleSettings({
})
}
disabled={readOnly}
placeholder={t("settings:chooseModelAndSessionSettings")}
ariaLabel={t("settings:settingsForAgent", { agent: rule.agent_name })}
placeholder={t("workflows:chooseModelAndSessionSettings")}
ariaLabel={t("workflows:settingsForAgent", { agent: rule.agent_name })}
triggerClassName="min-h-11 w-full sm:min-h-10"
/>
) : (
<p className="rounded-md border border-border/60 p-2 text-xs text-muted-foreground">
{t("settings:sessionConfigModelOptionsUnavailable")}
{t("workflows:sessionConfigModelOptionsUnavailable")}
</p>
)}
</div>
@@ -1,11 +1,17 @@
import type { Workflow, WorkflowStep } from "@/lib/types/http";
import type { useToast } from "@/components/toast-provider";
import { t } from "@/lib/i18n";
import { generateUUID } from "@/lib/utils";
import { createWorkflowStepAction, updateWorkflowStepAction } from "@/app/actions/workspaces";
const FALLBACK_ERROR_MESSAGE = "Request failed";
// See `workflow-card-actions.ts`: both fields are persisted verbatim, so the
// seeded step name deliberately stays English.
const NEW_STEP_DEFAULTS = { name: "New Step", color: "bg-slate-500" } as const;
function fallbackErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : t("common:requestFailed");
}
type WorkflowStepsSetter = (
updater: ((previous: WorkflowStep[]) => WorkflowStep[]) | WorkflowStep[],
) => void;
@@ -51,8 +57,8 @@ export async function addRemoteStep(
setWorkflowSteps((previous) => [...previous, created]);
} catch (error) {
toast({
title: "Failed to add workflow step",
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
title: t("workflows:failedToAddWorkflowStep"),
description: fallbackErrorMessage(error),
variant: "error",
});
}
@@ -87,8 +93,8 @@ export async function updateRemoteWorkflowStep({
setWorkflowSteps((previous) => applyWorkflowStepUpdates(previous, stepId, updated));
} catch (error) {
toast({
title: "Failed to update workflow step",
description: error instanceof Error ? error.message : FALLBACK_ERROR_MESSAGE,
title: t("workflows:failedToUpdateWorkflowStep"),
description: fallbackErrorMessage(error),
variant: "error",
});
}
@@ -10,7 +10,7 @@ import {
} from "@/components/settings/profile-edit/script-editor";
import {
HelpTip,
STEP_PROMPT_PLACEHOLDERS,
stepPromptPlaceholders,
PROMPT_TEMPLATES,
} from "./workflow-pipeline-editor-helpers";
@@ -38,16 +38,16 @@ export function StepPromptSection({
htmlFor={`${step.id}-prompt`}
className="text-xs font-medium uppercase tracking-wider text-muted-foreground"
>
{t("settings:stepPrompt")}
{t("workflows:stepPrompt")}
</Label>
<HelpTip text={t("settings:stepPromptHelp", { taskPrompt: "{{task_prompt}}" })} />
<HelpTip text={t("workflows:stepPromptHelp", { taskPrompt: "{{task_prompt}}" })} />
</div>
{!readOnly && (
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-[11px] text-muted-foreground/60">{t("settings:templates")}</span>
<span className="text-[11px] text-muted-foreground/60">{t("workflows:templates")}</span>
{PROMPT_TEMPLATES.map((template) => (
<button
key={template.label}
key={template.labelKey}
type="button"
onClick={() => {
onLocalPromptChange(template.prompt);
@@ -55,7 +55,7 @@ export function StepPromptSection({
}}
className="cursor-pointer rounded-md border border-border bg-muted/50 px-2 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{template.label}
{t(template.labelKey)}
</button>
))}
</div>
@@ -75,12 +75,12 @@ export function StepPromptSection({
height={computeEditorHeight(localPrompt)}
lineNumbers="off"
readOnly={readOnly}
placeholders={STEP_PROMPT_PLACEHOLDERS}
placeholders={stepPromptPlaceholders(t)}
mentionPrompts={prompts}
/>
</div>
<p className="text-[11px] text-muted-foreground/60">
{t("settings:stepPromptUsageHint", {
{t("workflows:stepPromptUsageHint", {
open: "{{",
taskPrompt: "{{task_prompt}}",
})}
@@ -1,5 +1,6 @@
"use client";
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogContent,
@@ -12,14 +13,12 @@ import { Button } from "@kandev/ui/button";
import { Input } from "@kandev/ui/input";
import { Label } from "@kandev/ui/label";
import { Switch } from "@kandev/ui/switch";
import type { TFunction } from "i18next";
import type {
WorkflowSyncController,
WorkflowSyncFormState,
} from "@/hooks/domains/settings/use-workflow-sync";
const HELP_TEXT =
"The directory should contain workflow export files (.yml/.yaml/.json) in the kandev_workflow format — the same format produced by workflow export.";
type RepoUrlFieldProps = {
url: string;
invalid: boolean;
@@ -32,9 +31,10 @@ type RepoUrlFieldProps = {
// branch, and directory. The resolved target is echoed underneath so the
// structured fields stay visible to the user.
function RepoUrlField({ url, invalid, resolved, onChange }: RepoUrlFieldProps) {
const { t } = useTranslation();
return (
<div className="space-y-1.5">
<Label htmlFor="workflow-sync-url">Repository link</Label>
<Label htmlFor="workflow-sync-url">{t("workflows:repositoryLink")}</Label>
<Input
id="workflow-sync-url"
data-testid="workflow-sync-url-input"
@@ -44,10 +44,10 @@ function RepoUrlField({ url, invalid, resolved, onChange }: RepoUrlFieldProps) {
aria-invalid={invalid}
/>
{invalid ? (
<p className="text-xs text-destructive">Not a recognized GitHub repository link.</p>
<p className="text-xs text-destructive">{t("workflows:notARecognizedGitHubLink")}</p>
) : (
<p className="text-xs text-muted-foreground" data-testid="workflow-sync-resolved">
{resolved || "Paste a GitHub link — /tree/… links carry the branch and directory too."}
{resolved || t("workflows:pasteGitHubLinkHint")}
</p>
)}
</div>
@@ -64,6 +64,7 @@ type FieldsProps = {
// from the pasted link (or defaults to main) and is echoed in the resolved
// summary under the link input.
function PollFields({ form, update }: FieldsProps) {
const { t } = useTranslation();
return (
<div className="space-y-1.5">
<div className="flex flex-wrap items-center gap-3">
@@ -75,12 +76,12 @@ function PollFields({ form, update }: FieldsProps) {
className="cursor-pointer"
/>
<Label htmlFor="workflow-sync-poll-toggle" className="cursor-pointer">
Auto-sync
{t("workflows:autoSync")}
</Label>
{form.poll_enabled && (
<div className="ml-auto flex items-center gap-2">
<Label htmlFor="workflow-sync-interval" className="sr-only">
Poll interval (seconds)
{t("workflows:pollIntervalSeconds")}
</Label>
<Input
id="workflow-sync-interval"
@@ -91,14 +92,12 @@ function PollFields({ form, update }: FieldsProps) {
value={form.interval_seconds}
onChange={(e) => update("interval_seconds", Number(e.target.value) || 0)}
/>
<span className="text-xs text-muted-foreground">seconds</span>
<span className="text-xs text-muted-foreground">{t("workflows:seconds")}</span>
</div>
)}
</div>
<p className="text-xs text-muted-foreground">
{form.poll_enabled
? "Checks the repository on this interval (minimum 60s)."
: "Syncs only run when you press Sync now."}
{form.poll_enabled ? t("workflows:pollEnabledHint") : t("workflows:pollDisabledHint")}
</p>
</div>
);
@@ -114,6 +113,7 @@ type WorkflowSyncDialogProps = {
// itself after a successful save or removal; failures keep it open with the
// error surfaced via toast.
export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDialogProps) {
const { t } = useTranslation();
const hasConfig = !!sync.config;
const intervalInvalid =
sync.form.poll_enabled &&
@@ -126,7 +126,13 @@ export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDia
!sync.form.repo_owner.trim() ||
!sync.form.repo_name.trim();
const resolved = sync.form.repo_owner
? `Syncing ${sync.form.repo_owner}/${sync.form.repo_name} @ ${sync.form.branch || "main"} — directory ${sync.form.path || "(repository root)"}.`
? t("workflows:syncResolvedTarget", {
owner: sync.form.repo_owner,
repo: sync.form.repo_name,
// `main` is git's default branch name, not copy.
branch: sync.form.branch || "main",
directory: sync.form.path || t("workflows:repositoryRoot"),
})
: "";
const handleSave = async () => {
@@ -140,10 +146,8 @@ export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDia
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg" data-testid="workflow-sync-dialog">
<DialogHeader>
<DialogTitle>GitHub Sync</DialogTitle>
<DialogDescription>
Automatically sync workflow definitions from a GitHub repository into this workspace.
</DialogDescription>
<DialogTitle>{t("workflows:githubSync")}</DialogTitle>
<DialogDescription>{t("workflows:githubSyncDescription")}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<RepoUrlField
@@ -153,7 +157,7 @@ export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDia
onChange={sync.setUrlInput}
/>
<PollFields form={sync.form} update={sync.update} />
<p className="text-xs text-muted-foreground">{HELP_TEXT}</p>
<p className="text-xs text-muted-foreground">{t("workflows:syncDirectoryHelp")}</p>
</div>
<DialogFooter>
{hasConfig && (
@@ -165,7 +169,7 @@ export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDia
className="sm:mr-auto cursor-pointer"
data-testid="workflow-sync-remove"
>
Remove
{t("workflows:remove")}
</Button>
)}
<Button
@@ -176,7 +180,7 @@ export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDia
data-testid="workflow-sync-save"
data-dialog-default-action
>
{saveLabel(sync.saving, hasConfig)}
{saveLabel(t, sync.saving, hasConfig)}
</Button>
</DialogFooter>
</DialogContent>
@@ -184,7 +188,7 @@ export function WorkflowSyncDialog({ open, onOpenChange, sync }: WorkflowSyncDia
);
}
function saveLabel(saving: boolean, hasConfig: boolean): string {
if (saving) return "Saving...";
return hasConfig ? "Update" : "Save";
function saveLabel(t: TFunction, saving: boolean, hasConfig: boolean): string {
if (saving) return t("workflows:saving");
return hasConfig ? t("workflows:update") : t("workflows:save");
}
@@ -1,6 +1,7 @@
"use client";
import { IconBrandGithub } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { Button } from "@kandev/ui/button";
import { Separator } from "@kandev/ui/separator";
import { WorkflowSyncDialog } from "@/components/settings/workflow-sync-dialog";
@@ -12,6 +13,7 @@ import { useWorkflowSync } from "@/hooks/domains/settings/use-workflow-sync";
// with the caller so button and section can sit in different parts of the
// layout.
export function WorkflowSyncButton({ onClick }: { onClick: () => void }) {
const { t } = useTranslation();
return (
<Button
type="button"
@@ -22,7 +24,7 @@ export function WorkflowSyncButton({ onClick }: { onClick: () => void }) {
data-testid="workflow-sync-open"
>
<IconBrandGithub className="h-4 w-4 mr-2" />
GitHub Sync
{t("workflows:githubSync")}
</Button>
);
}
@@ -0,0 +1,136 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import type { WorkflowSyncConfig } from "@/lib/types/workflow-sync";
import { WorkflowSyncStatusCard } from "./workflow-sync-status-banner";
/**
* The headline is a `<Trans>` whose `<1>` addresses the bold `<span>`
* positionally, so a prettier reflow can silently reassemble it into empty
* fragments without failing anything (see docs/i18n.md). The assertions below
* reconstruct the whole sentence and check the repo slug is inside the bold
* element, which is what a drifted index would break.
*
* The metadata line is the other shape worth pinning: it is joined from three
* separately-translated parts, and the repo path / interval are values.
*/
function config(overrides: Partial<WorkflowSyncConfig> = {}): WorkflowSyncConfig {
// No `as` cast: the fixture must satisfy the real contract, so a field added
// to WorkflowSyncConfig fails here rather than being silently absent.
// `last_synced_at` is left off — it is optional and absent until the first
// sync attempt, which is the state most of these cases exercise.
return {
workspace_id: "workspace-1",
repo_owner: "kdlbs",
repo_name: "kandev",
branch: "main",
path: ".kandev/workflows",
interval_seconds: 300,
poll_enabled: true,
last_ok: true,
last_error: "",
last_warnings: [],
created_at: "2026-08-01T00:00:00Z",
updated_at: "2026-08-01T00:00:00Z",
...overrides,
};
}
describe("WorkflowSyncStatusCard", () => {
afterEach(cleanup);
it("keeps the repository slug inside the bold span of the headline", () => {
render(<WorkflowSyncStatusCard config={config()} syncing={false} onSyncNow={vi.fn()} />);
const slug = screen.getByText("kdlbs/kandev");
expect(slug.className).toContain("font-semibold");
expect(slug.parentElement?.textContent).toBe("Syncing from kdlbs/kandev");
});
it("joins the metadata parts while auto-sync is on and no sync has run", () => {
render(<WorkflowSyncStatusCard config={config()} syncing={false} onSyncNow={vi.fn()} />);
expect(
screen.getByText("Directory .kandev/workflows · every 300s · waiting for first sync…"),
).toBeTruthy();
});
// The interval is a count, so it goes through i18next plural selection rather
// than being interpolated as an opaque value.
it("selects the singular interval form for one second", () => {
render(
<WorkflowSyncStatusCard
config={config({ interval_seconds: 1 })}
syncing={false}
onSyncNow={vi.fn()}
/>,
);
expect(
screen.getByText("Directory .kandev/workflows · every 1s · waiting for first sync…"),
).toBeTruthy();
});
// `formatRelative` routes its buckets through i18next; date-fns would render
// English inside a translated sentence.
it("renders the last-sync time through the locale-aware relative formatter", () => {
const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
render(
<WorkflowSyncStatusCard
config={config({ last_synced_at: threeHoursAgo })}
syncing={false}
onSyncNow={vi.fn()}
/>,
);
expect(screen.getByText(/last synced 3h ago$/)).toBeTruthy();
});
it("labels a failed attempt separately from a successful sync", () => {
render(
<WorkflowSyncStatusCard
config={config({ last_ok: false, last_synced_at: new Date().toISOString() })}
syncing={false}
onSyncNow={vi.fn()}
/>,
);
expect(screen.getByText(/last attempt just now$/)).toBeTruthy();
});
it("labels an empty directory as the repository root and reports auto-sync off", () => {
render(
<WorkflowSyncStatusCard
config={config({ path: "", poll_enabled: false })}
syncing={false}
onSyncNow={vi.fn()}
/>,
);
expect(
screen.getByText(
"Directory (repository root) · auto-sync off · not synced yet — use Sync now",
),
).toBeTruthy();
});
it("falls back to a generic message when a failed sync carries no error text", () => {
render(
<WorkflowSyncStatusCard
config={config({
last_ok: false,
last_synced_at: new Date().toISOString(),
last_error: "",
})}
syncing={false}
onSyncNow={vi.fn()}
/>,
);
expect(screen.getByText("Sync failed")).toBeTruthy();
});
it("renders backend warnings verbatim — they are server text, not catalog copy", () => {
render(
<WorkflowSyncStatusCard
config={config({ last_warnings: ["skipped bad-workflow.yml: invalid step id"] })}
syncing={false}
onSyncNow={vi.fn()}
/>,
);
expect(screen.getByText("skipped bad-workflow.yml: invalid step id")).toBeTruthy();
});
});
@@ -1,6 +1,6 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import { Trans, useTranslation } from "react-i18next";
import {
IconAlertTriangle,
IconCheck,
@@ -12,6 +12,8 @@ import { Alert, AlertDescription } from "@kandev/ui/alert";
import { Badge } from "@kandev/ui/badge";
import { Button } from "@kandev/ui/button";
import { useTick } from "@/components/integrations/auth-status-banner";
import type { TFunction } from "i18next";
import { formatRelative } from "@/lib/i18n/formats";
import type { WorkflowSyncConfig } from "@/lib/types/workflow-sync";
type SyncState = "waiting" | "ok" | "failed";
@@ -27,20 +29,27 @@ function StateIcon({ state }: { state: SyncState }) {
return <IconClock className="h-4 w-4 text-muted-foreground" />;
}
function lastSyncedLabel(config: WorkflowSyncConfig): string {
function lastSyncedLabel(t: TFunction, config: WorkflowSyncConfig): string {
if (config.last_synced_at) {
const when = formatDistanceToNow(new Date(config.last_synced_at), { addSuffix: true });
return config.last_ok ? `last synced ${when}` : `last attempt ${when}`;
// `formatRelative` routes its buckets through i18next; date-fns'
// `formatDistanceToNow` would render English inside a translated sentence.
const when = formatRelative(config.last_synced_at);
return config.last_ok
? t("workflows:lastSynced", { when })
: t("workflows:lastAttempt", { when });
}
return config.poll_enabled ? "waiting for first sync…" : "not synced yet — use Sync now";
return config.poll_enabled ? t("workflows:waitingForFirstSync") : t("workflows:notSyncedYet");
}
function MetadataLine({ config }: { config: WorkflowSyncConfig }) {
const { t } = useTranslation();
useTick(30_000);
const parts = [
`Directory ${config.path || "(repository root)"}`,
config.poll_enabled ? `every ${config.interval_seconds}s` : "auto-sync off",
lastSyncedLabel(config),
t("workflows:directoryLine", { path: config.path || t("workflows:repositoryRoot") }),
config.poll_enabled
? t("workflows:everySeconds", { count: config.interval_seconds })
: t("workflows:autoSyncOff"),
lastSyncedLabel(t, config),
];
return <p className="text-xs text-muted-foreground">{parts.join(" · ")}</p>;
}
@@ -83,6 +92,7 @@ export function WorkflowSyncStatusCard({
syncing,
onSyncNow,
}: WorkflowSyncStatusCardProps) {
const { t } = useTranslation();
const state = syncState(config);
return (
<div
@@ -94,10 +104,12 @@ export function WorkflowSyncStatusCard({
<div className="flex items-center gap-2 text-sm">
<StateIcon state={state} />
<span>
Syncing from{" "}
<span className="font-semibold">
{config.repo_owner}/{config.repo_name}
</span>
<Trans
i18nKey="workflows:syncingFromRepository"
values={{ repository: `${config.repo_owner}/${config.repo_name}` }}
>
Syncing from <span className="font-semibold" />
</Trans>
</span>
<Badge variant="secondary">{config.branch}</Badge>
</div>
@@ -115,12 +127,12 @@ export function WorkflowSyncStatusCard({
) : (
<IconRefresh className="h-4 w-4 mr-2" />
)}
Sync now
{t("workflows:syncNow")}
</Button>
</div>
<MetadataLine config={config} />
{state === "failed" && (
<p className="text-xs text-destructive">{config.last_error || "Sync failed"}</p>
<p className="text-xs text-destructive">{config.last_error || t("workflows:syncFailed")}</p>
)}
<WarningsAlert warnings={config.last_warnings ?? []} />
</div>
@@ -0,0 +1,40 @@
import { afterEach, describe, expect, it } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { TooltipProvider } from "@kandev/ui/tooltip";
import { WorkflowSyncedBadge } from "./workflow-synced-badge";
/**
* The tooltip interpolates `sourcePath` — a repository path, i.e. data — and
* falls back to a translated phrase when the workflow carries none. jsdom does
* not open a Radix tooltip from synthetic hover, so the sentence is asserted
* through the keyboard-focus path (see apps/web/AGENTS.md).
*/
describe("WorkflowSyncedBadge", () => {
afterEach(cleanup);
it("interpolates the source path into the read-only explanation", async () => {
render(
<TooltipProvider>
<WorkflowSyncedBadge sourcePath=".kandev/workflows/review.yml" />
</TooltipProvider>,
);
screen.getByTestId("workflow-synced-badge").focus();
const tooltip = await screen.findAllByText(
"Read-only - managed by workflow sync from .kandev/workflows/review.yml. Edit or remove it in the synced repository.",
);
expect(tooltip.length).toBeGreaterThan(0);
});
it("falls back to a translated phrase when no source path is known", async () => {
render(
<TooltipProvider>
<WorkflowSyncedBadge />
</TooltipProvider>,
);
screen.getByTestId("workflow-synced-badge").focus();
const tooltip = await screen.findAllByText(
"Read-only - managed by workflow sync from a configured repository. Edit or remove it in the synced repository.",
);
expect(tooltip.length).toBeGreaterThan(0);
});
});
@@ -1,7 +1,9 @@
import { useTranslation } from "react-i18next";
import { Badge } from "@kandev/ui/badge";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
export function WorkflowSyncedBadge({ sourcePath }: { sourcePath?: string }) {
const { t } = useTranslation();
return (
<Tooltip>
<TooltipTrigger asChild>
@@ -11,12 +13,13 @@ export function WorkflowSyncedBadge({ sourcePath }: { sourcePath?: string }) {
className="text-xs cursor-default"
data-testid="workflow-synced-badge"
>
Synced
{t("workflows:synced")}
</Badge>
</TooltipTrigger>
<TooltipContent>
Read-only - managed by workflow sync from {sourcePath || "a configured repository"}. Edit or
remove it in the synced repository.
{t("workflows:syncedBadgeTooltip", {
source: sourcePath || t("workflows:aConfiguredRepository"),
})}
</TooltipContent>
</Tooltip>
);
+12 -9
View File
@@ -1,5 +1,6 @@
import type React from "react";
import type { Icon } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import {
IconArrowRight,
IconClipboard,
@@ -24,7 +25,8 @@ const TRANSITION_TYPES = ["move_to_next", "move_to_previous", "move_to_step"];
type CapabilityDef = {
key: string;
icon: Icon;
tooltip: string;
/** Catalog key: `key` and the action `type`s it checks are wire values. */
tooltipKey: string;
check: (events: KanbanStepEvents) => boolean;
};
@@ -32,37 +34,37 @@ const CAPABILITIES: CapabilityDef[] = [
{
key: "onTurnStart",
icon: IconMessageForward,
tooltip: "On user message",
tooltipKey: "workflows:onUserMessage",
check: (e) => e.on_turn_start?.some((a) => TRANSITION_TYPES.includes(a.type)) ?? false,
},
{
key: "autoStart",
icon: IconRobot,
tooltip: "Auto-start agent",
tooltipKey: "workflows:autoStartAgent",
check: (e) => e.on_enter?.some((a) => a.type === "auto_start_agent") ?? false,
},
{
key: "planMode",
icon: IconClipboard,
tooltip: "Plan mode",
tooltipKey: "workflows:planMode",
check: (e) => e.on_enter?.some((a) => a.type === "enable_plan_mode") ?? false,
},
{
key: "resetContext",
icon: IconRefresh,
tooltip: "Reset agent context",
tooltipKey: "workflows:resetAgentContext",
check: (e) => e.on_enter?.some((a) => a.type === "reset_agent_context") ?? false,
},
{
key: "transition",
icon: IconArrowRight,
tooltip: "Auto-transition",
tooltipKey: "workflows:autoTransition",
check: (e) => e.on_turn_complete?.some((a) => TRANSITION_TYPES.includes(a.type)) ?? false,
},
{
key: "onExit",
icon: IconDoorExit,
tooltip: "On exit actions",
tooltipKey: "workflows:onExitActions",
check: (e) => (e.on_exit?.length ?? 0) > 0,
},
];
@@ -73,6 +75,7 @@ export function StepCapabilityIcons({
className,
fallback,
}: StepCapabilityIconsProps) {
const { t } = useTranslation();
const defaultClassName = "flex items-center gap-1.5 text-muted-foreground";
const activeCapabilities = events ? CAPABILITIES.filter((cap) => cap.check(events)) : [];
const hasAgentProfile = Boolean(agentProfileId);
@@ -89,7 +92,7 @@ export function StepCapabilityIcons({
<TooltipTrigger asChild>
<IconUserCog className="h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent>Custom agent profile</TooltipContent>
<TooltipContent>{t("workflows:customAgentProfile")}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
@@ -101,7 +104,7 @@ export function StepCapabilityIcons({
<TooltipTrigger asChild>
<IconComponent className="h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent>{cap.tooltip}</TooltipContent>
<TooltipContent>{t(cap.tooltipKey)}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
@@ -152,6 +152,14 @@ const SCREENS: Array<{ name: string; url: string; allow?: string[] }> = [
// The Sentry run also surfaced `components/integrations/auth-status-banner.tsx`
// ("Authenticated", "· checked <relative>") and `@kandev/ui`'s built-in dialog
// "Close" label — both shared, both out of scope for a per-integration PR.
//
// NOT YET, for the same reason: "settings — workspace workflows"
// (`/settings/workspace/:id/workflows`). The workflow editor's own copy is
// fully migrated and was verified with this oracle against a live instance,
// but the route expands the same un-migrated Workspaces branch of the settings
// nav. It also renders the workspace's own name and its workflow/step names,
// which are user data — so the entry needs the nav migrated first, not an
// allowlist.
];
/**
+86
View File
@@ -628,4 +628,90 @@ export const i18nGuardFiles = [
// can prove they stay migrated.
"hooks/domains/settings/use-agent-runtime-updates.ts",
"hooks/domains/settings/use-dynamic-models.ts",
// Settings → Workspace → Workflows: the workflow editor end to end — the
// route, the list and its reorder/import/export chrome, the workflow card,
// the pipeline/step editor with its WIP and transition controls, the
// session-config editor and rule cards, the replay-cycle diagnostic, and the
// GitHub-sync and export/import dialogs. Copy lives in a new `workflows`
// namespace; the 27 workflow-editor keys that already sat in `settings` moved
// there in this PR so the surface is not split across two catalogs.
//
// The closure was re-derived from the route's imports rather than from the
// string counter. `components/workflow-selector-row.tsx` is deliberately
// ABSENT: the counter attributed it here, but the only path to it from this
// route runs through the settings save provider into the config-chat →
// quick-chat → task-create-dialog tree, so it belongs to the task-create
// surface, not this one. Same for `components/task-create-dialog-*` and
// `components/task/chat/messages/workflow-step-message-badge.tsx`.
//
// `components/integrations/auth-status-banner.tsx` is also deliberately
// absent. This surface reaches it only for `useTick`, the shared 30s
// re-render hook, which carries no copy; every literal in that file belongs to
// the integration status banner, so listing it would pull an un-migrated
// component into this PR. It is the same call as `sentry-issue-common.tsx`
// above.
//
// The `.ts` entries hold no JSX, so `mode: "jsx-only"` never inspects them;
// the entries record that they are migrated (toast titles, the sync
// `confirm()`, thrown save/order errors, and the carry-forward warning
// sentence all reach the user) and only the pseudo-locale can prove it stays
// that way.
//
// Deliberately left in English, because each is PERSISTED rather than
// rendered: the seeded `New Step` name in `workflow-card-actions.ts` and
// `workflow-step-mutations.ts`, the `New Workflow` fallback and the
// `DEFAULT_CUSTOM_STEPS` names (`Todo` / `In Progress` / `Review` / `Done`) in
// `use-workflow-creation.ts`, and the three `PROMPT_TEMPLATES` bodies in
// `workflow-pipeline-editor-helpers.tsx` — the template button writes its
// prompt into `WorkflowStep.prompt`, which is stored and sent to the agent
// verbatim. Translating any of them would write a localized string into the
// database. Same reasoning as the Jira/Linear/Sentry default watch prompts.
//
// Also English by design: the `kandev_workflow` YAML sample in the import
// dialog (a wire format), the GitHub repository-link example, git's default
// branch name `main`, the `{{task_prompt}}` substitution token, and the
// `step_complete_kandev` MCP tool name — all interpolated as values so the
// pseudo-locale cannot turn them into dead pointers.
//
// Wire values stay wire values, with only their labels as copy resolved at
// render from a catalog key: the step `color` Tailwind classes
// (`STEP_COLORS`), the transition action `type`s (`move_to_next` and
// friends), the capability `key`s in `step-capability-icons.tsx`, the replay
// diagnostic's `promptSource` / `trigger` / `actionKind`, the sync `state`
// union, and the `configure_session` rule `operation`s. Workflow and step
// NAMES are user data throughout and are always interpolated as values,
// never built into a message by concatenation.
"app/settings/workspace/[id]/workflows/**/*.{ts,tsx}",
"app/settings/workspace/use-workflow-creation.ts",
"app/settings/workspace/workspace-workflows-client.tsx",
"app/settings/workspace/workspace-workflows-dialogs.tsx",
"components/settings/use-workflow-draft-contributor.ts",
"components/settings/workflow-card-actions.ts",
"components/settings/workflow-card-dialogs.tsx",
"components/settings/workflow-card-header-actions.tsx",
"components/settings/workflow-card.tsx",
"components/settings/workflow-cycle-diagnostic.tsx",
"components/settings/workflow-export-dialog.tsx",
"components/settings/workflow-pipeline-editor-helpers.tsx",
"components/settings/workflow-pipeline-editor-panels.tsx",
"components/settings/workflow-pipeline-editor-step-actions.tsx",
"components/settings/workflow-pipeline-editor-wip-controls.tsx",
"components/settings/workflow-pipeline-editor.tsx",
"components/settings/workflow-section-actions.tsx",
"components/settings/workflow-session-config-carry-warning.tsx",
"components/settings/workflow-session-config-editor.tsx",
"components/settings/workflow-session-config-rule-card.tsx",
"components/settings/workflow-step-mutations.ts",
"components/settings/workflow-step-prompt-section.tsx",
"components/settings/workflow-sync-dialog.tsx",
"components/settings/workflow-sync-section.tsx",
"components/settings/workflow-sync-status-banner.tsx",
"components/settings/workflow-synced-badge.tsx",
"hooks/domains/settings/use-workflow-sync.ts",
"lib/workflows/session-config-carry-analysis.ts",
// Shared with the task board's `components/task/workflow-stepper.tsx`, which
// is not migrated. The guard is per-file, so that surface is unaffected; the
// icons render here on every pipeline node, so leaving them would have left
// plain English inside a card this PR claims is done.
"components/step-capability-icons.tsx",
];
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { t } from "@/lib/i18n";
import { useToast } from "@/components/toast-provider";
import { useRouter } from "@/lib/routing/client-router";
import { INTEGRATION_STATUS_REFRESH_MS } from "@/hooks/domains/integrations/use-integration-availability";
@@ -121,9 +122,12 @@ function useWorkflowSyncForm() {
type SyncToast = { description: string; variant?: "success" | "error" | "default" };
function syncOutcomeToast(error: string | undefined, warnings: string[] | undefined): SyncToast {
if (error) return { description: `Sync failed: ${error}`, variant: "error" };
if (warnings?.length) return { description: "Sync completed with warnings", variant: "default" };
return { description: "Workflow sync completed", variant: "success" };
if (error)
return { description: t("workflows:syncFailedWithError", { error }), variant: "error" };
if (warnings?.length) {
return { description: t("workflows:syncCompletedWithWarnings"), variant: "default" };
}
return { description: t("workflows:workflowSyncCompleted"), variant: "success" };
}
type InitialLoadDeps = {
@@ -153,7 +157,7 @@ function useWorkflowSyncInitialLoad(
.catch((err) => {
if (cancelled) return;
toast({
description: `Failed to load workflow sync config: ${String(err)}`,
description: t("workflows:failedToLoadSyncConfig", { error: String(err) }),
variant: "error",
});
})
@@ -194,10 +198,10 @@ export function useWorkflowSync(workspaceId: string) {
const saved = await setWorkflowSyncConfig(payload, { workspaceId });
setConfig(saved);
reset(saved);
toast({ description: "Workflow sync configuration saved", variant: "success" });
toast({ description: t("workflows:syncConfigSaved"), variant: "success" });
return true;
} catch (err) {
toast({ description: `Save failed: ${String(err)}`, variant: "error" });
toast({ description: t("workflows:saveFailed", { error: String(err) }), variant: "error" });
return false;
} finally {
setSaving(false);
@@ -205,22 +209,23 @@ export function useWorkflowSync(workspaceId: string) {
}, [workspaceId, form, toast, reset]);
const handleDelete = useCallback(async () => {
if (
!confirm("Remove workflow sync configuration? This will not delete already-synced workflows.")
) {
if (!confirm(t("workflows:removeSyncConfirm"))) {
return false;
}
try {
await deleteWorkflowSyncConfig({ workspaceId });
setConfig(null);
reset(null);
toast({ description: "Workflow sync removed — synced workflows are editable again" });
toast({ description: t("workflows:syncRemoved") });
// Released workflows lose their read-only state server-side; reload the
// page data so the cards unlock without a manual refresh.
router.refresh();
return true;
} catch (err) {
toast({ description: `Delete failed: ${String(err)}`, variant: "error" });
toast({
description: t("workflows:syncRemoveFailed", { error: String(err) }),
variant: "error",
});
return false;
}
}, [workspaceId, toast, reset, router]);
@@ -238,7 +243,10 @@ export function useWorkflowSync(workspaceId: string) {
router.refresh();
}
} catch (err) {
toast({ description: `Sync failed: ${String(err)}`, variant: "error" });
toast({
description: t("workflows:syncFailedWithError", { error: String(err) }),
variant: "error",
});
} finally {
setSyncing(false);
}
@@ -12,7 +12,6 @@ export type SessionConfigCarryWarning = {
sourceStepName: string;
model?: string;
configOptions: Record<string, string>;
message: string;
};
type CarryState = "original" | "changed";
@@ -129,7 +128,6 @@ function carryWarning(agentName: string, source: ChangedSource): SessionConfigCa
sourceStepName: source.step.name,
model: source.rule.operation === "set" ? source.rule.model : undefined,
configOptions: source.rule.operation === "set" ? { ...(source.rule.config_options ?? {}) } : {},
message: `Settings changed in ${source.step.name} may carry into this step for ${agentName}. Choose keep, restore original, or set new values.`,
};
}
+1
View File
@@ -16,6 +16,7 @@
"mAgo": "{{diffMin}}m ago",
"plugins": "Plugins",
"prompts": "Prompts",
"requestFailed": "Request failed",
"reset": "Reset",
"resetting": "Resetting\u2026",
"resetWatchChecking": "Checking how many tasks would be deleted\u2026",
-30
View File
@@ -276,10 +276,7 @@
"restoreTheOriginalBuiltInLayout": "Restore the original built-in layout and discard its override.",
"resumeTheMostRecentlyOpenedTask": "Resume the most recently opened task in this workspace on this device.",
"retrySave": "Retry save",
"runCompletionActionsWhenTurnCancelled": "Run completion actions when a turn is cancelled",
"runCompletionActionsWhenTurnCancelledHelp": "Applies only when a user explicitly cancels a turn. The destination step’s on-enter actions may immediately start another agent turn.",
"runningSprites": "Running Sprites",
"waitForAgentCompletionSignal": "Wait for agent completion signal",
"sameShortcutAs": "Same shortcut as: {{shortcuts}}",
"saveAndLeave": "Save and leave",
"saveChanges": "Save changes",
@@ -377,33 +374,6 @@
"agentGeneratedTaskTitlesDescription": "When enabled, new task and subtask forms hide the manual title field and require a prompt. Kandev shows a provisional title using the prompt's first six words immediately, then asks the agent to replace it with a concise title targeting about three words. If the agent cannot rename the task, the provisional title remains available for you to edit. Existing and edited tasks keep their normal title field.",
"useAgentForNewTaskTitles": "Use the agent for new task titles",
"agentGeneratedTaskTitlesDisabledHint": "Disabled by default; save this setting before opening a creation dialog.",
"sessionConfigCarryForwardWarning": "Earlier workflow settings may carry into this step because the original conversation tab is reused.",
"sessionConfigKeep": "Keep",
"sessionConfigRestore": "Restore",
"sessionConfigSetNew": "Set new",
"sessionConfigFixedProfileWarning": "Remove the fixed profile override before configuring the original session. The two behaviors are mutually exclusive.",
"sessionConfigEmptyRules": "Add a condition to choose which initial agent receives settings.",
"sessionConfigRulesHelp": "Rules apply to the task's first agent session. They never create or switch conversation tabs.",
"addCondition": "Add condition",
"whenAgentIs": "When agent is",
"chooseAgentFamily": "Choose agent family",
"setSettings": "Set settings",
"keepCurrent": "Keep current",
"restoreOriginal": "Restore original",
"removeAgentCondition": "Remove agent condition {{index}}",
"chooseModelAndSessionSettings": "Choose model and session settings",
"settingsForAgent": "Settings for {{agent}}",
"sessionConfigModelOptionsUnavailable": "Model options are unavailable for this agent. Refresh agent capabilities before saving this condition.",
"stepPrompt": "Step Prompt",
"stepPromptHelp": "Custom instructions for the agent on this step. Use {{taskPrompt}} to include the task description.",
"templates": "Templates:",
"stepPromptUsageHint": "Type {{open}} for placeholders ({{taskPrompt}} inserts the task description) or @ to reference a saved prompt by name — its content is attached as hidden context, and editing the saved prompt updates every step that references it. Note: {{taskPrompt}} only expands in the step prompt itself, not inside a referenced saved prompt.",
"removeConditionalSessionConfigBeforeProfile": "Remove the conditional session configuration before choosing a fixed agent profile for this step.",
"overrideAgentProfileHelp": "Override the agent profile for this step. A different profile creates a new session with fresh context when entering this step.",
"overrideOriginalSessionOptions": "Override original session options",
"overrideOriginalSessionOptionsHelp": "Keep the original conversation tab while changing its session settings for this step. For example, start with model 5.6 Sol and switch to 5.6 Luna for implementation work.",
"configureOriginalSessionOptions": "Configure original settings options",
"configureOriginalSessionOptionsDescription": "Change the existing conversation tab only when its initial agent family matches a rule.",
"logsPageTitle": "Logs",
"logsPageDescription": "Download a bounded diagnostic ZIP containing frontend and backend logs.",
"diagnosticReviewBeforeSharing": "Review before sharing",
+242
View File
@@ -0,0 +1,242 @@
{
"aConfiguredRepository": "a configured repository",
"addCondition": "Add condition",
"adding": "Adding...",
"addWorkflow": "Add Workflow",
"agentProfile": "Agent Profile",
"agentProfileHelp": "Default agent profile for tasks in this workflow. When set, the agent selector is locked in the task creation dialog.",
"allowManualMove": "Allow manual move",
"allowManualMoveHelp": "Allow dragging tasks into this step on the board.",
"applyAnyway": "Apply anyway",
"autoArchive": "Auto-archive",
"autoArchiveAfter": "after",
"autoArchiveHelp": "Automatically archive tasks after they have been in this step for a set number of hours. Useful for the last step of a workflow (e.g., Done) to keep the board clean.",
"autoArchiveHours": "hours",
"automaticWorkflowCycle": "Automatic workflow cycle",
"autoStartAgent": "Auto-start agent",
"autoStartAgentHelp": "Automatically start the agent when a task enters this step.",
"autoSync": "Auto-sync",
"autoSyncOff": "auto-sync off",
"autoTransition": "Auto-transition",
"childrenCompletedHelp": "Use this on a parent task step. When every active direct child task is COMPLETED, FAILED, or CANCELLED, Kandev runs this transition once. Archived and ephemeral child tasks are ignored. Grandchildren do not count here, and nothing runs if the parent has no child tasks.",
"childrenCompletedHelpAria": "How child task completion transitions work",
"chooseAgentFamily": "Choose agent family",
"chooseModelAndSessionSettings": "Choose model and session settings",
"close": "Close",
"color": "Color",
"colorBlue": "Blue",
"colorCyan": "Cyan",
"colorGray": "Gray",
"colorGreen": "Green",
"colorIndigo": "Indigo",
"colorOrange": "Orange",
"colorPurple": "Purple",
"colorRed": "Red",
"colorYellow": "Yellow",
"configureOriginalSessionOptions": "Configure original settings options",
"configureOriginalSessionOptionsDescription": "Change the existing conversation tab only when its initial agent family matches a rule.",
"confirmWorkflowCycle": "Confirm workflow cycle",
"confirmWorkflowCycleDescription": "Review the repeated agent run before continuing.",
"copied": "Copied",
"copy": "Copy",
"createAnyway": "Create anyway",
"customAgentProfile": "Custom agent profile",
"customTemplate": "Custom",
"customTemplateDescription": "Create your own agentic workflow from scratch.",
"cycle": "Cycle",
"delete": "Delete",
"deleteAndArchiveTasks": "Delete & Archive Tasks",
"deleteStep": "Delete Step",
"deleteStepAffectsTasks_one": "Deleting {{stepName}} will also affect its {{count}} task.",
"deleteStepAffectsTasks_other": "Deleting {{stepName}} will also affect its {{count}} tasks.",
"deleteStepAndTasks": "Delete Step & Tasks",
"deleteStepDialogTitle": "Delete step",
"deleteStepNoTasks": "This will permanently delete the {{stepName}} workflow step.",
"deleteStepWithMigration_one": "{{stepName}} has {{count}} task. Choose where to migrate them, or delete the step and its tasks.",
"deleteStepWithMigration_other": "{{stepName}} has {{count}} tasks. Choose where to migrate them, or delete the step and its tasks.",
"deleteWorkflow": "Delete Workflow",
"deleteWorkflowDialogTitle": "Delete workflow",
"deleteWorkflowNoTasks": "This will permanently delete the workflow and all its steps.",
"deleteWorkflowWithTasks_one": "This workflow has {{count}} task. Choose where to migrate them, or delete the workflow and archive the tasks.",
"deleteWorkflowWithTasks_other": "This workflow has {{count}} tasks. Choose where to migrate them, or delete the workflow and archive the tasks.",
"directoryLine": "Directory {{path}}",
"disablePlanMode": "Disable plan mode",
"disablePlanModeOnComplete": "Disable plan mode on complete",
"disablePlanModeOnCompleteHelp": "Turn off plan mode after the agent finishes a turn, even when the task remains in this step.",
"disablePlanModeOnExitHelp": "Keep plan mode on for every turn in this step, then turn it off only when the task moves to another step.",
"doNothing": "Do nothing",
"doNothingWaitForUser": "Do nothing (wait for user)",
"everySeconds_one": "every {{count}}s",
"everySeconds_other": "every {{count}}s",
"export": "Export",
"exportAll": "Export All",
"exportWorkflowsTitle": "Export Workflows",
"exportWorkflowTitle": "Export Workflow",
"failedToAddWorkflowStep": "Failed to add workflow step",
"failedToCheckWorkflowStepTasks": "Failed to check workflow step tasks",
"failedToCheckWorkflowTasks": "Failed to check workflow tasks",
"failedToDeleteStep": "Failed to delete step",
"failedToDeleteWorkflow": "Failed to delete workflow",
"failedToExportWorkflow": "Failed to export workflow",
"failedToExportWorkflows": "Failed to export workflows",
"failedToImportWorkflows": "Failed to import workflows",
"failedToLoadSyncConfig": "Failed to load workflow sync config: {{error}}",
"failedToLoadWorkflowSteps": "Failed to load workflow steps",
"failedToMigrateTasks": "Failed to migrate tasks",
"failedToSaveWorkflowChanges": "Failed to save workflow changes",
"failedToUpdateWorkflowStep": "Failed to update workflow step",
"githubSync": "GitHub Sync",
"githubSyncDescription": "Automatically sync workflow definitions from a GitHub repository into this workspace.",
"hopNumber": "Hop {{index}}",
"import": "Import",
"importCompleteTitle": "Import complete",
"importCreated": "Created: {{names}}",
"importing": "Importing...",
"importSkipped": "Skipped (already exist): {{names}}",
"importWorkflowsTitle": "Import Workflows",
"invalidYaml": "Invalid YAML",
"keepCurrent": "Keep current",
"lastAttempt": "last attempt {{when}}",
"lastSynced": "last synced {{when}}",
"loadingWorkflowSteps": "Loading workflow steps...",
"manageWorkflowsForThisWorkspace": "Manage workflows for this workspace.",
"migrateAndDelete": "Migrate & Delete",
"migrateAndDeleteStep": "Migrate & Delete Step",
"migrating": "Migrating...",
"moreInformation": "More information",
"moveToNextStep": "Move to next step",
"moveToPreviousStep": "Move to previous step",
"moveToSpecificStep": "Move to specific step",
"name": "Name",
"noFeederStep": "No feeder step",
"noneUseTaskDefault": "None (use task default)",
"noProfileOverride": "No profile override",
"notARecognizedGitHubLink": "Not a recognized GitHub repository link.",
"notSyncedYet": "not synced yet — use Sync now",
"onExit": "On Exit",
"onExitActions": "On exit actions",
"onExitHelp": "Runs when leaving this step (before entering the next step).",
"onTurnCompleteHelp": "Runs after the agent finishes a turn. Use to auto-advance tasks through the pipeline.",
"onTurnCompleteLabel": "On Turn Complete",
"onTurnCompleteTrigger": "On turn complete",
"onTurnStartHelp": "Runs when a user sends a message. Use for review cycles (e.g., move back to In Progress on feedback).",
"onTurnStartLabel": "On Turn Start",
"onTurnStartTrigger": "On turn start",
"onUserMessage": "On user message",
"orPasteYaml": "Or paste YAML",
"overrideAgentProfileHelp": "Override the agent profile for this step. A different profile creates a new session with fresh context when entering this step.",
"overrideOriginalSessionOptions": "Override original session options",
"overrideOriginalSessionOptionsHelp": "Keep the original conversation tab while changing its session settings for this step. For example, start with model 5.6 Sol and switch to 5.6 Luna for implementation work.",
"pasteGitHubLinkHint": "Paste a GitHub link — /tree/… links carry the branch and directory too.",
"planMode": "Plan mode",
"planModeHelp": "Agent proposes a plan instead of making changes directly.",
"pollDisabledHint": "Syncs only run when you press Sync now.",
"pollEnabledHint": "Checks the repository on this interval (minimum 60s).",
"pollIntervalSeconds": "Poll interval (seconds)",
"potentialRepeatedAgentRun": "Potential repeated agent run",
"promptSourceStepPrompt": "Re-entering \"{{stepName}}\" sends its step prompt instead of the task description.",
"promptSourceStepPromptWithTaskDescription": "Re-entering \"{{stepName}}\" sends its rendered step prompt including the task description.",
"promptSourceTaskDescription": "\"{{stepName}}\" has no step prompt, so re-entering it sends the task description.",
"pullFrom": "Pull from",
"pullFromHelp": "Optional feeder step to pull queued work from when this step has capacity.",
"readOnlyManagedBySync": "Read-only — managed by workflow sync",
"remove": "Remove",
"removeAgentCondition": "Remove agent condition {{index}}",
"removeConditionalSessionConfigBeforeProfile": "Remove the conditional session configuration before choosing a fixed agent profile for this step.",
"removeSyncConfirm": "Remove workflow sync configuration? This will not delete already-synced workflows.",
"repositoryLink": "Repository link",
"repositoryRoot": "(repository root)",
"resetAgentContext": "Reset agent context",
"resetAgentContextHelp": "Restart the agent with a fresh conversation context when entering this step. Useful for review steps that need an unbiased perspective.",
"resetAgentContextHelpWithProfile": "Not needed — switching agent profiles already creates a new session with fresh context.",
"restoreOriginal": "Restore original",
"retryPartialSaveBeforeLeaving": "Retry the partial workflow save before leaving",
"returnToWorkflow": "Return to workflow",
"runCompletionActionsWhenTurnCancelled": "Run completion actions when a turn is cancelled",
"runCompletionActionsWhenTurnCancelledHelp": "Applies only when a user explicitly cancels a turn. The destination step’s on-enter actions may immediately start another agent turn.",
"save": "Save",
"saveBeforeExporting": "Save the workflow before exporting.",
"saveFailed": "Save failed: {{error}}",
"saveWorkflowsBeforeOrdering": "Save workflows before ordering them",
"saving": "Saving...",
"seconds": "seconds",
"selectAction": "Select action",
"selectedStepFallback": "selected",
"selectStep": "Select step",
"selectWorkflow": "Select workflow",
"sessionConfigCarryForwardWarning": "Earlier workflow settings may carry into this step because the original conversation tab is reused.",
"sessionConfigCarryForwardWarningMessage": "Settings changed in {{stepName}} may carry into this step for {{agentName}}. Choose keep, restore original, or set new values.",
"sessionConfigEmptyRules": "Add a condition to choose which initial agent receives settings.",
"sessionConfigFixedProfileWarning": "Remove the fixed profile override before configuring the original session. The two behaviors are mutually exclusive.",
"sessionConfigKeep": "Keep",
"sessionConfigModelOptionsUnavailable": "Model options are unavailable for this agent. Refresh agent capabilities before saving this condition.",
"sessionConfigRestore": "Restore",
"sessionConfigRulesHelp": "Rules apply to the task's first agent session. They never create or switch conversation tabs.",
"sessionConfigSetNew": "Set new",
"setSettings": "Set settings",
"settingsForAgent": "Settings for {{agent}}",
"showInCommandPanel": "Show in command panel",
"showInCommandPanelHelp": "Show tasks in this step when opening the command panel (Cmd+K). Useful for hiding backlog or done steps from quick access.",
"startStep": "Start step",
"startStepHelp": "New tasks start in this step. Only one step per workflow can be the start step.",
"stepIsPartOfReplayCycle": "{{stepName}} is part of a replay cycle",
"stepNamePlaceholder": "Step name",
"stepPrompt": "Step Prompt",
"stepPromptHelp": "Custom instructions for the agent on this step. Use {{taskPrompt}} to include the task description.",
"stepPromptPlaceholderExample": "Implement user authentication with OAuth2",
"stepPromptPlaceholderTaskPrompt": "The original task description provided by the user",
"stepPromptUsageHint": "Type {{open}} for placeholders ({{taskPrompt}} inserts the task description) or @ to reference a saved prompt by name — its content is attached as hidden context, and editing the saved prompt updates every step that references it. Note: {{taskPrompt}} only expands in the step prompt itself, not inside a referenced saved prompt.",
"syncCompletedWithWarnings": "Sync completed with warnings",
"syncConfigSaved": "Workflow sync configuration saved",
"syncDirectoryHelp": "The directory should contain workflow export files (.yml/.yaml/.json) in the kandev_workflow format — the same format produced by workflow export.",
"synced": "Synced",
"syncedBadgeTooltip": "Read-only - managed by workflow sync from {{source}}. Edit or remove it in the synced repository.",
"syncedReadOnlyReason": "Managed by workflow sync — edit or remove it in the synced repository",
"syncFailed": "Sync failed",
"syncFailedWithError": "Sync failed: {{error}}",
"syncingFromRepository": "Syncing from <1>{{repository}}</1>",
"syncNow": "Sync now",
"syncRemoved": "Workflow sync removed — synced workflows are editable again",
"syncRemoveFailed": "Delete failed: {{error}}",
"syncResolvedTarget": "Syncing {{owner}}/{{repo}} @ {{branch}} — directory {{directory}}.",
"targetStep": "Target Step",
"targetWorkflow": "Target Workflow",
"template": "Template",
"templateCodeReview": "Code Review",
"templatePlan": "Plan",
"templates": "Templates:",
"templateSecurityAudit": "Security Audit",
"transitionAuto": "auto",
"transitionBack": "back",
"transitionGoto": "goto",
"transitionManual": "manual",
"transitions": "Transitions",
"unsavedStepChangesDiscarded": "Unsaved step changes will be discarded.",
"unsavedWorkflowChangesDiscarded": "Unsaved workflow changes will be discarded.",
"update": "Update",
"uploadYamlFile": "Upload YAML file",
"userActionRequired": "User action required",
"waitForAgentCompletionSignal": "Wait for agent completion signal",
"waitForAgentCompletionSignalHelp": "Only auto-advance once the agent calls {{tool}}. Otherwise turn-end is treated as completion.",
"waitingForFirstSync": "waiting for first sync…",
"waitingForRetry": "Waiting for the failed change to be retried.",
"whenAgentIs": "When agent is",
"whenChildTasksComplete": "When Child Tasks Complete",
"wipLimit": "WIP limit",
"wipLimitHelp": "Maximum admitted (active) tasks in this step. Overflow stays visible in a queue. Use 0 for unlimited.",
"wipLimitsNote": "WIP limits active work, not visibility. Overflow remains on the board until capacity opens; if a selected feeder is also full, new task creation is rejected.",
"workflowCycleBlocked": "Workflow cycle blocked",
"workflowCycleBlockedDescription": "Change the workflow steps to remove the automatic cycle before continuing.",
"workflowCycleBlockingBody": "This path re-enters \"{{stepName}}\" and can start the agent again without another user action.",
"workflowCycleReplayPathFor": "Replay path for {{stepName}}",
"workflowCycleWarningBody": "This path can re-enter \"{{stepName}}\" and start the agent again after a user action.",
"workflowDetails": "Workflow details",
"workflowName": "Workflow Name",
"workflowNameIsRequired": "Workflow name is required",
"workflowNamePlaceholder": "My Project Workflow",
"workflows": "Workflows",
"workflowsSectionDescription": "Create autonomous pipelines with automated transitions or manual workflows where you move tasks yourself",
"workflowSteps": "Workflow Steps",
"workflowSyncCompleted": "Workflow sync completed",
"workspaceSettings": "Workspace settings"
}
+1
View File
@@ -16,6 +16,7 @@
"mAgo": "{{diffMin}}ḿ àĝō",
"plugins": "Ƥĺũĝĩńś",
"prompts": "Ƥŕōḿƥţś",
"requestFailed": "Ŕēqũēśţ ƒàĩĺēď",
"reset": "Ŕēśēţ",
"resetting": "Ŕēśēţţĩńĝ…",
"resetWatchChecking": "Ćĥēćķĩńĝ ĥōŵ ḿàńŷ ţàśķś ŵōũĺď ƀē ďēĺēţēď…",
-30
View File
@@ -276,10 +276,7 @@
"restoreTheOriginalBuiltInLayout": "Ŕēśţōŕē ţĥē ōŕĩĝĩńàĺ ƀũĩĺţ-ĩń ĺàŷōũţ àńď ďĩśćàŕď ĩţś ōvēŕŕĩďē.",
"resumeTheMostRecentlyOpenedTask": "Ŕēśũḿē ţĥē ḿōśţ ŕēćēńţĺŷ ōƥēńēď ţàśķ ĩń ţĥĩś ŵōŕķśƥàćē ōń ţĥĩś ďēvĩćē.",
"retrySave": "Ŕēţŕŷ śàvē",
"runCompletionActionsWhenTurnCancelled": "Ŕũń ćōḿƥĺēţĩōń àćţĩōńś ŵĥēń à ţũŕń ĩś ćàńćēĺĺēď",
"runCompletionActionsWhenTurnCancelledHelp": "Àƥƥĺĩēś ōńĺŷ ŵĥēń à ũśēŕ ēxƥĺĩćĩţĺŷ ćàńćēĺś à ţũŕń. Ŧĥē ďēśţĩńàţĩōń śţēƥ’ś ōń-ēńţēŕ àćţĩōńś ḿàŷ ĩḿḿēďĩàţēĺŷ śţàŕţ àńōţĥēŕ àĝēńţ ţũŕń.",
"runningSprites": "Ŕũńńĩńĝ Śƥŕĩţēś",
"waitForAgentCompletionSignal": "Ŵàĩţ ƒōŕ àĝēńţ ćōḿƥĺēţĩōń śĩĝńàĺ",
"sameShortcutAs": "Śàḿē śĥōŕţćũţ àś: {{shortcuts}}",
"saveAndLeave": "Śàvē àńď ĺēàvē",
"saveChanges": "Śàvē ćĥàńĝēś",
@@ -377,33 +374,6 @@
"agentGeneratedTaskTitlesDescription": "Ŵĥēń ēńàƀĺēď, ńēŵ ţàśķ àńď śũƀţàśķ ƒōŕḿś ĥĩďē ţĥē ḿàńũàĺ ţĩţĺē ƒĩēĺď àńď ŕēqũĩŕē à ƥŕōḿƥţ. Ķàńďēv śĥōŵś à ƥŕōvĩśĩōńàĺ ţĩţĺē ũśĩńĝ ţĥē ƥŕōḿƥţ'ś ƒĩŕśţ śĩx ŵōŕďś ĩḿḿēďĩàţēĺŷ, ţĥēń àśķś ţĥē àĝēńţ ţō ŕēƥĺàćē ĩţ ŵĩţĥ à ćōńćĩśē ţĩţĺē ţàŕĝēţĩńĝ àƀōũţ ţĥŕēē ŵōŕďś. Ĩƒ ţĥē àĝēńţ ćàńńōţ ŕēńàḿē ţĥē ţàśķ, ţĥē ƥŕōvĩśĩōńàĺ ţĩţĺē ŕēḿàĩńś àvàĩĺàƀĺē ƒōŕ ŷōũ ţō ēďĩţ. Ēxĩśţĩńĝ àńď ēďĩţēď ţàśķś ķēēƥ ţĥēĩŕ ńōŕḿàĺ ţĩţĺē ƒĩēĺď.",
"useAgentForNewTaskTitles": "Ũśē ţĥē àĝēńţ ƒōŕ ńēŵ ţàśķ ţĩţĺēś",
"agentGeneratedTaskTitlesDisabledHint": "Ďĩśàƀĺēď ƀŷ ďēƒàũĺţ; śàvē ţĥĩś śēţţĩńĝ ƀēƒōŕē ōƥēńĩńĝ à ćŕēàţĩōń ďĩàĺōĝ.",
"sessionConfigCarryForwardWarning": "Ēàŕĺĩēŕ ŵōŕķƒĺōŵ śēţţĩńĝś ḿàŷ ćàŕŕŷ ĩńţō ţĥĩś śţēƥ ƀēćàũśē ţĥē ōŕĩĝĩńàĺ ćōńvēŕśàţĩōń ţàƀ ĩś ŕēũśēď.",
"sessionConfigKeep": "Ķēēƥ",
"sessionConfigRestore": "Ŕēśţōŕē",
"sessionConfigSetNew": "Śēţ ńēŵ",
"sessionConfigFixedProfileWarning": "Ŕēḿōvē ţĥē ƒĩxēď ƥŕōƒĩĺē ōvēŕŕĩďē ƀēƒōŕē ćōńƒĩĝũŕĩńĝ ţĥē ōŕĩĝĩńàĺ śēśśĩōń. Ţĥē ţŵō ƀēĥàvĩōŕś àŕē ḿũţũàĺĺŷ ēxćĺũśĩvē.",
"sessionConfigEmptyRules": "Àďď à ćōńďĩţĩōń ţō ćĥōōśē ŵĥĩćĥ ĩńĩţĩàĺ àĝēńţ ŕēćēĩvēś śēţţĩńĝś.",
"sessionConfigRulesHelp": "Ŕũĺēś àƥƥĺŷ ţō ţĥē ţàśķ'ś ƒĩŕśţ àĝēńţ śēśśĩōń. Ţĥēŷ ńēvēŕ ćŕēàţē ōŕ śŵĩţćĥ ćōńvēŕśàţĩōń ţàƀś.",
"addCondition": "Àďď ćōńďĩţĩōń",
"whenAgentIs": "Ŵĥēń àĝēńţ ĩś",
"chooseAgentFamily": "Ćĥōōśē àĝēńţ ƒàḿĩĺŷ",
"setSettings": "Śēţ śēţţĩńĝś",
"keepCurrent": "Ķēēƥ ćũŕŕēńţ",
"restoreOriginal": "Ŕēśţōŕē ōŕĩĝĩńàĺ",
"removeAgentCondition": "Ŕēḿōvē àĝēńţ ćōńďĩţĩōń {{index}}",
"chooseModelAndSessionSettings": "Ćĥōōśē ḿōďēĺ àńď śēśśĩōń śēţţĩńĝś",
"settingsForAgent": "Śēţţĩńĝś ƒōŕ {{agent}}",
"sessionConfigModelOptionsUnavailable": "Ḿōďēĺ ōƥţĩōńś àŕē ũńàvàĩĺàƀĺē ƒōŕ ţĥĩś àĝēńţ. Ŕēƒŕēśĥ àĝēńţ ćàƥàƀĩĺĩţĩēś ƀēƒōŕē śàvĩńĝ ţĥĩś ćōńďĩţĩōń.",
"stepPrompt": "Śţēƥ Ƥŕōḿƥţ",
"stepPromptHelp": "Ćũśţōḿ ĩńśţŕũćţĩōńś ƒōŕ ţĥē àĝēńţ ōń ţĥĩś śţēƥ. Ũśē {{taskPrompt}} ţō ĩńćĺũďē ţĥē ţàśķ ďēśćŕĩƥţĩōń.",
"templates": "Ţēḿƥĺàţēś:",
"stepPromptUsageHint": "Ţŷƥē {{open}} ƒōŕ ƥĺàćēĥōĺďēŕś ({{taskPrompt}} ĩńśēŕţś ţĥē ţàśķ ďēśćŕĩƥţĩōń) ōŕ @ ţō ŕēƒēŕēńćē à śàvēď ƥŕōḿƥţ ƀŷ ńàḿē — ĩţś ćōńţēńţ ĩś àţţàćĥēď àś ĥĩďďēń ćōńţēxţ, àńď ēďĩţĩńĝ ţĥē śàvēď ƥŕōḿƥţ ũƥďàţēś ēvēŕŷ śţēƥ ţĥàţ ŕēƒēŕēńćēś ĩţ. Ńōţē: {{taskPrompt}} ōńĺŷ ēxƥàńďś ĩń ţĥē śţēƥ ƥŕōḿƥţ ĩţśēĺƒ, ńōţ ĩńśĩďē à ŕēƒēŕēńćēď śàvēď ƥŕōḿƥţ.",
"removeConditionalSessionConfigBeforeProfile": "Ŕēḿōvē ţĥē ćōńďĩţĩōńàĺ śēśśĩōń ćōńƒĩĝũŕàţĩōń ƀēƒōŕē ćĥōōśĩńĝ à ƒĩxēď àĝēńţ ƥŕōƒĩĺē ƒōŕ ţĥĩś śţēƥ.",
"overrideAgentProfileHelp": "Ōvēŕŕĩďē ţĥē àĝēńţ ƥŕōƒĩĺē ƒōŕ ţĥĩś śţēƥ. À ďĩƒƒēŕēńţ ƥŕōƒĩĺē ćŕēàţēś à ńēŵ śēśśĩōń ŵĩţĥ ƒŕēśĥ ćōńţēxţ ŵĥēń ēńţēŕĩńĝ ţĥĩś śţēƥ.",
"overrideOriginalSessionOptions": "Ōvēŕŕĩďē ōŕĩĝĩńàĺ śēśśĩōń ōƥţĩōńś",
"overrideOriginalSessionOptionsHelp": "Ķēēƥ ţĥē ōŕĩĝĩńàĺ ćōńvēŕśàţĩōń ţàƀ ŵĥĩĺē ćĥàńĝĩńĝ ĩţś śēśśĩōń śēţţĩńĝś ƒōŕ ţĥĩś śţēƥ. Ƒōŕ ēxàḿƥĺē, śţàŕţ ŵĩţĥ ḿōďēĺ 5.6 Śōĺ àńď śŵĩţćĥ ţō 5.6 Ĺũńà ƒōŕ ĩḿƥĺēḿēńţàţĩōń ŵōŕķ.",
"configureOriginalSessionOptions": "Ćōńƒĩĝũŕē ōŕĩĝĩńàĺ śēţţĩńĝś ōƥţĩōńś",
"configureOriginalSessionOptionsDescription": "Ćĥàńĝē ţĥē ēxĩśţĩńĝ ćōńvēŕśàţĩōń ţàƀ ōńĺŷ ŵĥēń ĩţś ĩńĩţĩàĺ àĝēńţ ƒàḿĩĺŷ ḿàţćĥēś à ŕũĺē.",
"logsPageTitle": "Ĺōĝś",
"logsPageDescription": "Ďōŵńĺōàď à ƀōũńďēď ďĩàĝńōśţĩć ŹĨƤ ćōńţàĩńĩńĝ ƒŕōńţēńď àńď ƀàćķēńď ĺōĝś.",
"diagnosticReviewBeforeSharing": "Ŕēvĩēŵ ƀēƒōŕē śĥàŕĩńĝ",
+242
View File
@@ -0,0 +1,242 @@
{
"aConfiguredRepository": "à ćōńƒĩĝũŕēď ŕēƥōśĩţōŕŷ",
"addCondition": "Àďď ćōńďĩţĩōń",
"adding": "Àďďĩńĝ...",
"addWorkflow": "Àďď Ŵōŕķƒĺōŵ",
"agentProfile": "Àĝēńţ Ƥŕōƒĩĺē",
"agentProfileHelp": "Ďēƒàũĺţ àĝēńţ ƥŕōƒĩĺē ƒōŕ ţàśķś ĩń ţĥĩś ŵōŕķƒĺōŵ. Ŵĥēń śēţ, ţĥē àĝēńţ śēĺēćţōŕ ĩś ĺōćķēď ĩń ţĥē ţàśķ ćŕēàţĩōń ďĩàĺōĝ.",
"allowManualMove": "Àĺĺōŵ ḿàńũàĺ ḿōvē",
"allowManualMoveHelp": "Àĺĺōŵ ďŕàĝĝĩńĝ ţàśķś ĩńţō ţĥĩś śţēƥ ōń ţĥē ƀōàŕď.",
"applyAnyway": "Àƥƥĺŷ àńŷŵàŷ",
"autoArchive": "Àũţō-àŕćĥĩvē",
"autoArchiveAfter": "àƒţēŕ",
"autoArchiveHelp": "Àũţōḿàţĩćàĺĺŷ àŕćĥĩvē ţàśķś àƒţēŕ ţĥēŷ ĥàvē ƀēēń ĩń ţĥĩś śţēƥ ƒōŕ à śēţ ńũḿƀēŕ ōƒ ĥōũŕś. Ũśēƒũĺ ƒōŕ ţĥē ĺàśţ śţēƥ ōƒ à ŵōŕķƒĺōŵ (ē.ĝ., Ďōńē) ţō ķēēƥ ţĥē ƀōàŕď ćĺēàń.",
"autoArchiveHours": "ĥōũŕś",
"automaticWorkflowCycle": "Àũţōḿàţĩć ŵōŕķƒĺōŵ ćŷćĺē",
"autoStartAgent": "Àũţō-śţàŕţ àĝēńţ",
"autoStartAgentHelp": "Àũţōḿàţĩćàĺĺŷ śţàŕţ ţĥē àĝēńţ ŵĥēń à ţàśķ ēńţēŕś ţĥĩś śţēƥ.",
"autoSync": "Àũţō-śŷńć",
"autoSyncOff": "àũţō-śŷńć ōƒƒ",
"autoTransition": "Àũţō-ţŕàńśĩţĩōń",
"childrenCompletedHelp": "Ũśē ţĥĩś ōń à ƥàŕēńţ ţàśķ śţēƥ. Ŵĥēń ēvēŕŷ àćţĩvē ďĩŕēćţ ćĥĩĺď ţàśķ ĩś ĆŌḾƤĹĒŢĒĎ, ƑÀĨĹĒĎ, ōŕ ĆÀŃĆĒĹĹĒĎ, Ķàńďēv ŕũńś ţĥĩś ţŕàńśĩţĩōń ōńćē. Àŕćĥĩvēď àńď ēƥĥēḿēŕàĺ ćĥĩĺď ţàśķś àŕē ĩĝńōŕēď. Ĝŕàńďćĥĩĺďŕēń ďō ńōţ ćōũńţ ĥēŕē, àńď ńōţĥĩńĝ ŕũńś ĩƒ ţĥē ƥàŕēńţ ĥàś ńō ćĥĩĺď ţàśķś.",
"childrenCompletedHelpAria": "Ĥōŵ ćĥĩĺď ţàśķ ćōḿƥĺēţĩōń ţŕàńśĩţĩōńś ŵōŕķ",
"chooseAgentFamily": "Ćĥōōśē àĝēńţ ƒàḿĩĺŷ",
"chooseModelAndSessionSettings": "Ćĥōōśē ḿōďēĺ àńď śēśśĩōń śēţţĩńĝś",
"close": "Ćĺōśē",
"color": "Ćōĺōŕ",
"colorBlue": "Ɓĺũē",
"colorCyan": "Ćŷàń",
"colorGray": "Ĝŕàŷ",
"colorGreen": "Ĝŕēēń",
"colorIndigo": "Ĩńďĩĝō",
"colorOrange": "Ōŕàńĝē",
"colorPurple": "Ƥũŕƥĺē",
"colorRed": "Ŕēď",
"colorYellow": "Ŷēĺĺōŵ",
"configureOriginalSessionOptions": "Ćōńƒĩĝũŕē ōŕĩĝĩńàĺ śēţţĩńĝś ōƥţĩōńś",
"configureOriginalSessionOptionsDescription": "Ćĥàńĝē ţĥē ēxĩśţĩńĝ ćōńvēŕśàţĩōń ţàƀ ōńĺŷ ŵĥēń ĩţś ĩńĩţĩàĺ àĝēńţ ƒàḿĩĺŷ ḿàţćĥēś à ŕũĺē.",
"confirmWorkflowCycle": "Ćōńƒĩŕḿ ŵōŕķƒĺōŵ ćŷćĺē",
"confirmWorkflowCycleDescription": "Ŕēvĩēŵ ţĥē ŕēƥēàţēď àĝēńţ ŕũń ƀēƒōŕē ćōńţĩńũĩńĝ.",
"copied": "Ćōƥĩēď",
"copy": "Ćōƥŷ",
"createAnyway": "Ćŕēàţē àńŷŵàŷ",
"customAgentProfile": "Ćũśţōḿ àĝēńţ ƥŕōƒĩĺē",
"customTemplate": "Ćũśţōḿ",
"customTemplateDescription": "Ćŕēàţē ŷōũŕ ōŵń àĝēńţĩć ŵōŕķƒĺōŵ ƒŕōḿ śćŕàţćĥ.",
"cycle": "Ćŷćĺē",
"delete": "Ďēĺēţē",
"deleteAndArchiveTasks": "Ďēĺēţē & Àŕćĥĩvē Ţàśķś",
"deleteStep": "Ďēĺēţē Śţēƥ",
"deleteStepAffectsTasks_one": "Ďēĺēţĩńĝ {{stepName}} ŵĩĺĺ àĺśō àƒƒēćţ ĩţś {{count}} ţàśķ.",
"deleteStepAffectsTasks_other": "Ďēĺēţĩńĝ {{stepName}} ŵĩĺĺ àĺśō àƒƒēćţ ĩţś {{count}} ţàśķś.",
"deleteStepAndTasks": "Ďēĺēţē Śţēƥ & Ţàśķś",
"deleteStepDialogTitle": "Ďēĺēţē śţēƥ",
"deleteStepNoTasks": "Ţĥĩś ŵĩĺĺ ƥēŕḿàńēńţĺŷ ďēĺēţē ţĥē {{stepName}} ŵōŕķƒĺōŵ śţēƥ.",
"deleteStepWithMigration_one": "{{stepName}} ĥàś {{count}} ţàśķ. Ćĥōōśē ŵĥēŕē ţō ḿĩĝŕàţē ţĥēḿ, ōŕ ďēĺēţē ţĥē śţēƥ àńď ĩţś ţàśķś.",
"deleteStepWithMigration_other": "{{stepName}} ĥàś {{count}} ţàśķś. Ćĥōōśē ŵĥēŕē ţō ḿĩĝŕàţē ţĥēḿ, ōŕ ďēĺēţē ţĥē śţēƥ àńď ĩţś ţàśķś.",
"deleteWorkflow": "Ďēĺēţē Ŵōŕķƒĺōŵ",
"deleteWorkflowDialogTitle": "Ďēĺēţē ŵōŕķƒĺōŵ",
"deleteWorkflowNoTasks": "Ţĥĩś ŵĩĺĺ ƥēŕḿàńēńţĺŷ ďēĺēţē ţĥē ŵōŕķƒĺōŵ àńď àĺĺ ĩţś śţēƥś.",
"deleteWorkflowWithTasks_one": "Ţĥĩś ŵōŕķƒĺōŵ ĥàś {{count}} ţàśķ. Ćĥōōśē ŵĥēŕē ţō ḿĩĝŕàţē ţĥēḿ, ōŕ ďēĺēţē ţĥē ŵōŕķƒĺōŵ àńď àŕćĥĩvē ţĥē ţàśķś.",
"deleteWorkflowWithTasks_other": "Ţĥĩś ŵōŕķƒĺōŵ ĥàś {{count}} ţàśķś. Ćĥōōśē ŵĥēŕē ţō ḿĩĝŕàţē ţĥēḿ, ōŕ ďēĺēţē ţĥē ŵōŕķƒĺōŵ àńď àŕćĥĩvē ţĥē ţàśķś.",
"directoryLine": "Ďĩŕēćţōŕŷ {{path}}",
"disablePlanMode": "Ďĩśàƀĺē ƥĺàń ḿōďē",
"disablePlanModeOnComplete": "Ďĩśàƀĺē ƥĺàń ḿōďē ōń ćōḿƥĺēţē",
"disablePlanModeOnCompleteHelp": "Ţũŕń ōƒƒ ƥĺàń ḿōďē àƒţēŕ ţĥē àĝēńţ ƒĩńĩśĥēś à ţũŕń, ēvēń ŵĥēń ţĥē ţàśķ ŕēḿàĩńś ĩń ţĥĩś śţēƥ.",
"disablePlanModeOnExitHelp": "Ķēēƥ ƥĺàń ḿōďē ōń ƒōŕ ēvēŕŷ ţũŕń ĩń ţĥĩś śţēƥ, ţĥēń ţũŕń ĩţ ōƒƒ ōńĺŷ ŵĥēń ţĥē ţàśķ ḿōvēś ţō àńōţĥēŕ śţēƥ.",
"doNothing": "Ďō ńōţĥĩńĝ",
"doNothingWaitForUser": "Ďō ńōţĥĩńĝ (ŵàĩţ ƒōŕ ũśēŕ)",
"everySeconds_one": "ēvēŕŷ {{count}}ś",
"everySeconds_other": "ēvēŕŷ {{count}}ś",
"export": "Ēxƥōŕţ",
"exportAll": "Ēxƥōŕţ Àĺĺ",
"exportWorkflowsTitle": "Ēxƥōŕţ Ŵōŕķƒĺōŵś",
"exportWorkflowTitle": "Ēxƥōŕţ Ŵōŕķƒĺōŵ",
"failedToAddWorkflowStep": "Ƒàĩĺēď ţō àďď ŵōŕķƒĺōŵ śţēƥ",
"failedToCheckWorkflowStepTasks": "Ƒàĩĺēď ţō ćĥēćķ ŵōŕķƒĺōŵ śţēƥ ţàśķś",
"failedToCheckWorkflowTasks": "Ƒàĩĺēď ţō ćĥēćķ ŵōŕķƒĺōŵ ţàśķś",
"failedToDeleteStep": "Ƒàĩĺēď ţō ďēĺēţē śţēƥ",
"failedToDeleteWorkflow": "Ƒàĩĺēď ţō ďēĺēţē ŵōŕķƒĺōŵ",
"failedToExportWorkflow": "Ƒàĩĺēď ţō ēxƥōŕţ ŵōŕķƒĺōŵ",
"failedToExportWorkflows": "Ƒàĩĺēď ţō ēxƥōŕţ ŵōŕķƒĺōŵś",
"failedToImportWorkflows": "Ƒàĩĺēď ţō ĩḿƥōŕţ ŵōŕķƒĺōŵś",
"failedToLoadSyncConfig": "Ƒàĩĺēď ţō ĺōàď ŵōŕķƒĺōŵ śŷńć ćōńƒĩĝ: {{error}}",
"failedToLoadWorkflowSteps": "Ƒàĩĺēď ţō ĺōàď ŵōŕķƒĺōŵ śţēƥś",
"failedToMigrateTasks": "Ƒàĩĺēď ţō ḿĩĝŕàţē ţàśķś",
"failedToSaveWorkflowChanges": "Ƒàĩĺēď ţō śàvē ŵōŕķƒĺōŵ ćĥàńĝēś",
"failedToUpdateWorkflowStep": "Ƒàĩĺēď ţō ũƥďàţē ŵōŕķƒĺōŵ śţēƥ",
"githubSync": "ĜĩţĤũƀ Śŷńć",
"githubSyncDescription": "Àũţōḿàţĩćàĺĺŷ śŷńć ŵōŕķƒĺōŵ ďēƒĩńĩţĩōńś ƒŕōḿ à ĜĩţĤũƀ ŕēƥōśĩţōŕŷ ĩńţō ţĥĩś ŵōŕķśƥàćē.",
"hopNumber": "Ĥōƥ {{index}}",
"import": "Ĩḿƥōŕţ",
"importCompleteTitle": "Ĩḿƥōŕţ ćōḿƥĺēţē",
"importCreated": "Ćŕēàţēď: {{names}}",
"importing": "Ĩḿƥōŕţĩńĝ...",
"importSkipped": "Śķĩƥƥēď (àĺŕēàďŷ ēxĩśţ): {{names}}",
"importWorkflowsTitle": "Ĩḿƥōŕţ Ŵōŕķƒĺōŵś",
"invalidYaml": "Ĩńvàĺĩď ŶÀḾĹ",
"keepCurrent": "Ķēēƥ ćũŕŕēńţ",
"lastAttempt": "ĺàśţ àţţēḿƥţ {{when}}",
"lastSynced": "ĺàśţ śŷńćēď {{when}}",
"loadingWorkflowSteps": "Ĺōàďĩńĝ ŵōŕķƒĺōŵ śţēƥś...",
"manageWorkflowsForThisWorkspace": "Ḿàńàĝē ŵōŕķƒĺōŵś ƒōŕ ţĥĩś ŵōŕķśƥàćē.",
"migrateAndDelete": "Ḿĩĝŕàţē & Ďēĺēţē",
"migrateAndDeleteStep": "Ḿĩĝŕàţē & Ďēĺēţē Śţēƥ",
"migrating": "Ḿĩĝŕàţĩńĝ...",
"moreInformation": "Ḿōŕē ĩńƒōŕḿàţĩōń",
"moveToNextStep": "Ḿōvē ţō ńēxţ śţēƥ",
"moveToPreviousStep": "Ḿōvē ţō ƥŕēvĩōũś śţēƥ",
"moveToSpecificStep": "Ḿōvē ţō śƥēćĩƒĩć śţēƥ",
"name": "Ńàḿē",
"noFeederStep": "Ńō ƒēēďēŕ śţēƥ",
"noneUseTaskDefault": "Ńōńē (ũśē ţàśķ ďēƒàũĺţ)",
"noProfileOverride": "Ńō ƥŕōƒĩĺē ōvēŕŕĩďē",
"notARecognizedGitHubLink": "Ńōţ à ŕēćōĝńĩźēď ĜĩţĤũƀ ŕēƥōśĩţōŕŷ ĺĩńķ.",
"notSyncedYet": "ńōţ śŷńćēď ŷēţ — ũśē Śŷńć ńōŵ",
"onExit": "Ōń Ēxĩţ",
"onExitActions": "Ōń ēxĩţ àćţĩōńś",
"onExitHelp": "Ŕũńś ŵĥēń ĺēàvĩńĝ ţĥĩś śţēƥ (ƀēƒōŕē ēńţēŕĩńĝ ţĥē ńēxţ śţēƥ).",
"onTurnCompleteHelp": "Ŕũńś àƒţēŕ ţĥē àĝēńţ ƒĩńĩśĥēś à ţũŕń. Ũśē ţō àũţō-àďvàńćē ţàśķś ţĥŕōũĝĥ ţĥē ƥĩƥēĺĩńē.",
"onTurnCompleteLabel": "Ōń Ţũŕń Ćōḿƥĺēţē",
"onTurnCompleteTrigger": "Ōń ţũŕń ćōḿƥĺēţē",
"onTurnStartHelp": "Ŕũńś ŵĥēń à ũśēŕ śēńďś à ḿēśśàĝē. Ũśē ƒōŕ ŕēvĩēŵ ćŷćĺēś (ē.ĝ., ḿōvē ƀàćķ ţō Ĩń Ƥŕōĝŕēśś ōń ƒēēďƀàćķ).",
"onTurnStartLabel": "Ōń Ţũŕń Śţàŕţ",
"onTurnStartTrigger": "Ōń ţũŕń śţàŕţ",
"onUserMessage": "Ōń ũśēŕ ḿēśśàĝē",
"orPasteYaml": "Ōŕ ƥàśţē ŶÀḾĹ",
"overrideAgentProfileHelp": "Ōvēŕŕĩďē ţĥē àĝēńţ ƥŕōƒĩĺē ƒōŕ ţĥĩś śţēƥ. À ďĩƒƒēŕēńţ ƥŕōƒĩĺē ćŕēàţēś à ńēŵ śēśśĩōń ŵĩţĥ ƒŕēśĥ ćōńţēxţ ŵĥēń ēńţēŕĩńĝ ţĥĩś śţēƥ.",
"overrideOriginalSessionOptions": "Ōvēŕŕĩďē ōŕĩĝĩńàĺ śēśśĩōń ōƥţĩōńś",
"overrideOriginalSessionOptionsHelp": "Ķēēƥ ţĥē ōŕĩĝĩńàĺ ćōńvēŕśàţĩōń ţàƀ ŵĥĩĺē ćĥàńĝĩńĝ ĩţś śēśśĩōń śēţţĩńĝś ƒōŕ ţĥĩś śţēƥ. Ƒōŕ ēxàḿƥĺē, śţàŕţ ŵĩţĥ ḿōďēĺ 5.6 Śōĺ àńď śŵĩţćĥ ţō 5.6 Ĺũńà ƒōŕ ĩḿƥĺēḿēńţàţĩōń ŵōŕķ.",
"pasteGitHubLinkHint": "Ƥàśţē à ĜĩţĤũƀ ĺĩńķ — /ţŕēē/… ĺĩńķś ćàŕŕŷ ţĥē ƀŕàńćĥ àńď ďĩŕēćţōŕŷ ţōō.",
"planMode": "Ƥĺàń ḿōďē",
"planModeHelp": "Àĝēńţ ƥŕōƥōśēś à ƥĺàń ĩńśţēàď ōƒ ḿàķĩńĝ ćĥàńĝēś ďĩŕēćţĺŷ.",
"pollDisabledHint": "Śŷńćś ōńĺŷ ŕũń ŵĥēń ŷōũ ƥŕēśś Śŷńć ńōŵ.",
"pollEnabledHint": "Ćĥēćķś ţĥē ŕēƥōśĩţōŕŷ ōń ţĥĩś ĩńţēŕvàĺ (ḿĩńĩḿũḿ 60ś).",
"pollIntervalSeconds": "Ƥōĺĺ ĩńţēŕvàĺ (śēćōńďś)",
"potentialRepeatedAgentRun": "Ƥōţēńţĩàĺ ŕēƥēàţēď àĝēńţ ŕũń",
"promptSourceStepPrompt": "Ŕē-ēńţēŕĩńĝ \"{{stepName}}\" śēńďś ĩţś śţēƥ ƥŕōḿƥţ ĩńśţēàď ōƒ ţĥē ţàśķ ďēśćŕĩƥţĩōń.",
"promptSourceStepPromptWithTaskDescription": "Ŕē-ēńţēŕĩńĝ \"{{stepName}}\" śēńďś ĩţś ŕēńďēŕēď śţēƥ ƥŕōḿƥţ ĩńćĺũďĩńĝ ţĥē ţàśķ ďēśćŕĩƥţĩōń.",
"promptSourceTaskDescription": "\"{{stepName}}\" ĥàś ńō śţēƥ ƥŕōḿƥţ, śō ŕē-ēńţēŕĩńĝ ĩţ śēńďś ţĥē ţàśķ ďēśćŕĩƥţĩōń.",
"pullFrom": "Ƥũĺĺ ƒŕōḿ",
"pullFromHelp": "Ōƥţĩōńàĺ ƒēēďēŕ śţēƥ ţō ƥũĺĺ qũēũēď ŵōŕķ ƒŕōḿ ŵĥēń ţĥĩś śţēƥ ĥàś ćàƥàćĩţŷ.",
"readOnlyManagedBySync": "Ŕēàď-ōńĺŷ — ḿàńàĝēď ƀŷ ŵōŕķƒĺōŵ śŷńć",
"remove": "Ŕēḿōvē",
"removeAgentCondition": "Ŕēḿōvē àĝēńţ ćōńďĩţĩōń {{index}}",
"removeConditionalSessionConfigBeforeProfile": "Ŕēḿōvē ţĥē ćōńďĩţĩōńàĺ śēśśĩōń ćōńƒĩĝũŕàţĩōń ƀēƒōŕē ćĥōōśĩńĝ à ƒĩxēď àĝēńţ ƥŕōƒĩĺē ƒōŕ ţĥĩś śţēƥ.",
"removeSyncConfirm": "Ŕēḿōvē ŵōŕķƒĺōŵ śŷńć ćōńƒĩĝũŕàţĩōń? Ţĥĩś ŵĩĺĺ ńōţ ďēĺēţē àĺŕēàďŷ-śŷńćēď ŵōŕķƒĺōŵś.",
"repositoryLink": "Ŕēƥōśĩţōŕŷ ĺĩńķ",
"repositoryRoot": "(ŕēƥōśĩţōŕŷ ŕōōţ)",
"resetAgentContext": "Ŕēśēţ àĝēńţ ćōńţēxţ",
"resetAgentContextHelp": "Ŕēśţàŕţ ţĥē àĝēńţ ŵĩţĥ à ƒŕēśĥ ćōńvēŕśàţĩōń ćōńţēxţ ŵĥēń ēńţēŕĩńĝ ţĥĩś śţēƥ. Ũśēƒũĺ ƒōŕ ŕēvĩēŵ śţēƥś ţĥàţ ńēēď àń ũńƀĩàśēď ƥēŕśƥēćţĩvē.",
"resetAgentContextHelpWithProfile": "Ńōţ ńēēďēď — śŵĩţćĥĩńĝ àĝēńţ ƥŕōƒĩĺēś àĺŕēàďŷ ćŕēàţēś à ńēŵ śēśśĩōń ŵĩţĥ ƒŕēśĥ ćōńţēxţ.",
"restoreOriginal": "Ŕēśţōŕē ōŕĩĝĩńàĺ",
"retryPartialSaveBeforeLeaving": "Ŕēţŕŷ ţĥē ƥàŕţĩàĺ ŵōŕķƒĺōŵ śàvē ƀēƒōŕē ĺēàvĩńĝ",
"returnToWorkflow": "Ŕēţũŕń ţō ŵōŕķƒĺōŵ",
"runCompletionActionsWhenTurnCancelled": "Ŕũń ćōḿƥĺēţĩōń àćţĩōńś ŵĥēń à ţũŕń ĩś ćàńćēĺĺēď",
"runCompletionActionsWhenTurnCancelledHelp": "Àƥƥĺĩēś ōńĺŷ ŵĥēń à ũśēŕ ēxƥĺĩćĩţĺŷ ćàńćēĺś à ţũŕń. Ţĥē ďēśţĩńàţĩōń śţēƥ’ś ōń-ēńţēŕ àćţĩōńś ḿàŷ ĩḿḿēďĩàţēĺŷ śţàŕţ àńōţĥēŕ àĝēńţ ţũŕń.",
"save": "Śàvē",
"saveBeforeExporting": "Śàvē ţĥē ŵōŕķƒĺōŵ ƀēƒōŕē ēxƥōŕţĩńĝ.",
"saveFailed": "Śàvē ƒàĩĺēď: {{error}}",
"saveWorkflowsBeforeOrdering": "Śàvē ŵōŕķƒĺōŵś ƀēƒōŕē ōŕďēŕĩńĝ ţĥēḿ",
"saving": "Śàvĩńĝ...",
"seconds": "śēćōńďś",
"selectAction": "Śēĺēćţ àćţĩōń",
"selectedStepFallback": "śēĺēćţēď",
"selectStep": "Śēĺēćţ śţēƥ",
"selectWorkflow": "Śēĺēćţ ŵōŕķƒĺōŵ",
"sessionConfigCarryForwardWarning": "Ēàŕĺĩēŕ ŵōŕķƒĺōŵ śēţţĩńĝś ḿàŷ ćàŕŕŷ ĩńţō ţĥĩś śţēƥ ƀēćàũśē ţĥē ōŕĩĝĩńàĺ ćōńvēŕśàţĩōń ţàƀ ĩś ŕēũśēď.",
"sessionConfigCarryForwardWarningMessage": "Śēţţĩńĝś ćĥàńĝēď ĩń {{stepName}} ḿàŷ ćàŕŕŷ ĩńţō ţĥĩś śţēƥ ƒōŕ {{agentName}}. Ćĥōōśē ķēēƥ, ŕēśţōŕē ōŕĩĝĩńàĺ, ōŕ śēţ ńēŵ vàĺũēś.",
"sessionConfigEmptyRules": "Àďď à ćōńďĩţĩōń ţō ćĥōōśē ŵĥĩćĥ ĩńĩţĩàĺ àĝēńţ ŕēćēĩvēś śēţţĩńĝś.",
"sessionConfigFixedProfileWarning": "Ŕēḿōvē ţĥē ƒĩxēď ƥŕōƒĩĺē ōvēŕŕĩďē ƀēƒōŕē ćōńƒĩĝũŕĩńĝ ţĥē ōŕĩĝĩńàĺ śēśśĩōń. Ţĥē ţŵō ƀēĥàvĩōŕś àŕē ḿũţũàĺĺŷ ēxćĺũśĩvē.",
"sessionConfigKeep": "Ķēēƥ",
"sessionConfigModelOptionsUnavailable": "Ḿōďēĺ ōƥţĩōńś àŕē ũńàvàĩĺàƀĺē ƒōŕ ţĥĩś àĝēńţ. Ŕēƒŕēśĥ àĝēńţ ćàƥàƀĩĺĩţĩēś ƀēƒōŕē śàvĩńĝ ţĥĩś ćōńďĩţĩōń.",
"sessionConfigRestore": "Ŕēśţōŕē",
"sessionConfigRulesHelp": "Ŕũĺēś àƥƥĺŷ ţō ţĥē ţàśķ'ś ƒĩŕśţ àĝēńţ śēśśĩōń. Ţĥēŷ ńēvēŕ ćŕēàţē ōŕ śŵĩţćĥ ćōńvēŕśàţĩōń ţàƀś.",
"sessionConfigSetNew": "Śēţ ńēŵ",
"setSettings": "Śēţ śēţţĩńĝś",
"settingsForAgent": "Śēţţĩńĝś ƒōŕ {{agent}}",
"showInCommandPanel": "Śĥōŵ ĩń ćōḿḿàńď ƥàńēĺ",
"showInCommandPanelHelp": "Śĥōŵ ţàśķś ĩń ţĥĩś śţēƥ ŵĥēń ōƥēńĩńĝ ţĥē ćōḿḿàńď ƥàńēĺ (Ćḿď+Ķ). Ũśēƒũĺ ƒōŕ ĥĩďĩńĝ ƀàćķĺōĝ ōŕ ďōńē śţēƥś ƒŕōḿ qũĩćķ àććēśś.",
"startStep": "Śţàŕţ śţēƥ",
"startStepHelp": "Ńēŵ ţàśķś śţàŕţ ĩń ţĥĩś śţēƥ. Ōńĺŷ ōńē śţēƥ ƥēŕ ŵōŕķƒĺōŵ ćàń ƀē ţĥē śţàŕţ śţēƥ.",
"stepIsPartOfReplayCycle": "{{stepName}} ĩś ƥàŕţ ōƒ à ŕēƥĺàŷ ćŷćĺē",
"stepNamePlaceholder": "Śţēƥ ńàḿē",
"stepPrompt": "Śţēƥ Ƥŕōḿƥţ",
"stepPromptHelp": "Ćũśţōḿ ĩńśţŕũćţĩōńś ƒōŕ ţĥē àĝēńţ ōń ţĥĩś śţēƥ. Ũśē {{taskPrompt}} ţō ĩńćĺũďē ţĥē ţàśķ ďēśćŕĩƥţĩōń.",
"stepPromptPlaceholderExample": "Ĩḿƥĺēḿēńţ ũśēŕ àũţĥēńţĩćàţĩōń ŵĩţĥ ŌÀũţĥ2",
"stepPromptPlaceholderTaskPrompt": "Ţĥē ōŕĩĝĩńàĺ ţàśķ ďēśćŕĩƥţĩōń ƥŕōvĩďēď ƀŷ ţĥē ũśēŕ",
"stepPromptUsageHint": "Ţŷƥē {{open}} ƒōŕ ƥĺàćēĥōĺďēŕś ({{taskPrompt}} ĩńśēŕţś ţĥē ţàśķ ďēśćŕĩƥţĩōń) ōŕ @ ţō ŕēƒēŕēńćē à śàvēď ƥŕōḿƥţ ƀŷ ńàḿē — ĩţś ćōńţēńţ ĩś àţţàćĥēď àś ĥĩďďēń ćōńţēxţ, àńď ēďĩţĩńĝ ţĥē śàvēď ƥŕōḿƥţ ũƥďàţēś ēvēŕŷ śţēƥ ţĥàţ ŕēƒēŕēńćēś ĩţ. Ńōţē: {{taskPrompt}} ōńĺŷ ēxƥàńďś ĩń ţĥē śţēƥ ƥŕōḿƥţ ĩţśēĺƒ, ńōţ ĩńśĩďē à ŕēƒēŕēńćēď śàvēď ƥŕōḿƥţ.",
"syncCompletedWithWarnings": "Śŷńć ćōḿƥĺēţēď ŵĩţĥ ŵàŕńĩńĝś",
"syncConfigSaved": "Ŵōŕķƒĺōŵ śŷńć ćōńƒĩĝũŕàţĩōń śàvēď",
"syncDirectoryHelp": "Ţĥē ďĩŕēćţōŕŷ śĥōũĺď ćōńţàĩń ŵōŕķƒĺōŵ ēxƥōŕţ ƒĩĺēś (.ŷḿĺ/.ŷàḿĺ/.ĵśōń) ĩń ţĥē ķàńďēv_ŵōŕķƒĺōŵ ƒōŕḿàţ — ţĥē śàḿē ƒōŕḿàţ ƥŕōďũćēď ƀŷ ŵōŕķƒĺōŵ ēxƥōŕţ.",
"synced": "Śŷńćēď",
"syncedBadgeTooltip": "Ŕēàď-ōńĺŷ - ḿàńàĝēď ƀŷ ŵōŕķƒĺōŵ śŷńć ƒŕōḿ {{source}}. Ēďĩţ ōŕ ŕēḿōvē ĩţ ĩń ţĥē śŷńćēď ŕēƥōśĩţōŕŷ.",
"syncedReadOnlyReason": "Ḿàńàĝēď ƀŷ ŵōŕķƒĺōŵ śŷńć — ēďĩţ ōŕ ŕēḿōvē ĩţ ĩń ţĥē śŷńćēď ŕēƥōśĩţōŕŷ",
"syncFailed": "Śŷńć ƒàĩĺēď",
"syncFailedWithError": "Śŷńć ƒàĩĺēď: {{error}}",
"syncingFromRepository": "Śŷńćĩńĝ ƒŕōḿ <1>{{repository}}</1>",
"syncNow": "Śŷńć ńōŵ",
"syncRemoved": "Ŵōŕķƒĺōŵ śŷńć ŕēḿōvēď — śŷńćēď ŵōŕķƒĺōŵś àŕē ēďĩţàƀĺē àĝàĩń",
"syncRemoveFailed": "Ďēĺēţē ƒàĩĺēď: {{error}}",
"syncResolvedTarget": "Śŷńćĩńĝ {{owner}}/{{repo}} @ {{branch}} — ďĩŕēćţōŕŷ {{directory}}.",
"targetStep": "Ţàŕĝēţ Śţēƥ",
"targetWorkflow": "Ţàŕĝēţ Ŵōŕķƒĺōŵ",
"template": "Ţēḿƥĺàţē",
"templateCodeReview": "Ćōďē Ŕēvĩēŵ",
"templatePlan": "Ƥĺàń",
"templates": "Ţēḿƥĺàţēś:",
"templateSecurityAudit": "Śēćũŕĩţŷ Àũďĩţ",
"transitionAuto": "àũţō",
"transitionBack": "ƀàćķ",
"transitionGoto": "ĝōţō",
"transitionManual": "ḿàńũàĺ",
"transitions": "Ţŕàńśĩţĩōńś",
"unsavedStepChangesDiscarded": "Ũńśàvēď śţēƥ ćĥàńĝēś ŵĩĺĺ ƀē ďĩśćàŕďēď.",
"unsavedWorkflowChangesDiscarded": "Ũńśàvēď ŵōŕķƒĺōŵ ćĥàńĝēś ŵĩĺĺ ƀē ďĩśćàŕďēď.",
"update": "Ũƥďàţē",
"uploadYamlFile": "Ũƥĺōàď ŶÀḾĹ ƒĩĺē",
"userActionRequired": "Ũśēŕ àćţĩōń ŕēqũĩŕēď",
"waitForAgentCompletionSignal": "Ŵàĩţ ƒōŕ àĝēńţ ćōḿƥĺēţĩōń śĩĝńàĺ",
"waitForAgentCompletionSignalHelp": "Ōńĺŷ àũţō-àďvàńćē ōńćē ţĥē àĝēńţ ćàĺĺś {{tool}}. Ōţĥēŕŵĩśē ţũŕń-ēńď ĩś ţŕēàţēď àś ćōḿƥĺēţĩōń.",
"waitingForFirstSync": "ŵàĩţĩńĝ ƒōŕ ƒĩŕśţ śŷńć…",
"waitingForRetry": "Ŵàĩţĩńĝ ƒōŕ ţĥē ƒàĩĺēď ćĥàńĝē ţō ƀē ŕēţŕĩēď.",
"whenAgentIs": "Ŵĥēń àĝēńţ ĩś",
"whenChildTasksComplete": "Ŵĥēń Ćĥĩĺď Ţàśķś Ćōḿƥĺēţē",
"wipLimit": "ŴĨƤ ĺĩḿĩţ",
"wipLimitHelp": "Ḿàxĩḿũḿ àďḿĩţţēď (àćţĩvē) ţàśķś ĩń ţĥĩś śţēƥ. Ōvēŕƒĺōŵ śţàŷś vĩśĩƀĺē ĩń à qũēũē. Ũśē 0 ƒōŕ ũńĺĩḿĩţēď.",
"wipLimitsNote": "ŴĨƤ ĺĩḿĩţś àćţĩvē ŵōŕķ, ńōţ vĩśĩƀĩĺĩţŷ. Ōvēŕƒĺōŵ ŕēḿàĩńś ōń ţĥē ƀōàŕď ũńţĩĺ ćàƥàćĩţŷ ōƥēńś; ĩƒ à śēĺēćţēď ƒēēďēŕ ĩś àĺśō ƒũĺĺ, ńēŵ ţàśķ ćŕēàţĩōń ĩś ŕēĵēćţēď.",
"workflowCycleBlocked": "Ŵōŕķƒĺōŵ ćŷćĺē ƀĺōćķēď",
"workflowCycleBlockedDescription": "Ćĥàńĝē ţĥē ŵōŕķƒĺōŵ śţēƥś ţō ŕēḿōvē ţĥē àũţōḿàţĩć ćŷćĺē ƀēƒōŕē ćōńţĩńũĩńĝ.",
"workflowCycleBlockingBody": "Ţĥĩś ƥàţĥ ŕē-ēńţēŕś \"{{stepName}}\" àńď ćàń śţàŕţ ţĥē àĝēńţ àĝàĩń ŵĩţĥōũţ àńōţĥēŕ ũśēŕ àćţĩōń.",
"workflowCycleReplayPathFor": "Ŕēƥĺàŷ ƥàţĥ ƒōŕ {{stepName}}",
"workflowCycleWarningBody": "Ţĥĩś ƥàţĥ ćàń ŕē-ēńţēŕ \"{{stepName}}\" àńď śţàŕţ ţĥē àĝēńţ àĝàĩń àƒţēŕ à ũśēŕ àćţĩōń.",
"workflowDetails": "Ŵōŕķƒĺōŵ ďēţàĩĺś",
"workflowName": "Ŵōŕķƒĺōŵ Ńàḿē",
"workflowNameIsRequired": "Ŵōŕķƒĺōŵ ńàḿē ĩś ŕēqũĩŕēď",
"workflowNamePlaceholder": "Ḿŷ Ƥŕōĵēćţ Ŵōŕķƒĺōŵ",
"workflows": "Ŵōŕķƒĺōŵś",
"workflowsSectionDescription": "Ćŕēàţē àũţōńōḿōũś ƥĩƥēĺĩńēś ŵĩţĥ àũţōḿàţēď ţŕàńśĩţĩōńś ōŕ ḿàńũàĺ ŵōŕķƒĺōŵś ŵĥēŕē ŷōũ ḿōvē ţàśķś ŷōũŕśēĺƒ",
"workflowSteps": "Ŵōŕķƒĺōŵ Śţēƥś",
"workflowSyncCompleted": "Ŵōŕķƒĺōŵ śŷńć ćōḿƥĺēţēď",
"workspaceSettings": "Ŵōŕķśƥàćē śēţţĩńĝś"
}