U12 part 4: derive the move menu's "Back to" label from workflow traits (plus two legacy reads I did NOT delete, with measurements) (#2521)

## U12 part 4 — the move menu's "Back to" label followed hardcoded
column ids

`getTaskMoveTransitions` is shared by Board cards, List rows and Task
Detail. It labelled a backwards move with:

```ts
column === "in-progress" && task.column === "in-review"
  ? t("taskDetail.move.backToInProgress", "Back to In Progress")
```

Two hardcoded lifecycle ids **and** a hardcoded English column name. On
a workflow that renames those lanes the condition never matched, so the
affordance silently vanished — and had it matched, it would have
announced "In Progress", a column absent from that board. Same
legacy-vocabulary class U10 removed from Board and U12 removed from
ListView, surviving in the context menu all three surfaces render.

Now keyed on the traits it was approximating: the **current** column
carries `mergeBlocker`, the **target** carries `countsTowardWip`, and
the label interpolates the column's own name through a new
`taskDetail.move.backTo` key (added to all six locales).

### Scope I deliberately held back

**The set of moves labelled "Back to" is unchanged.** For
`builtin:coding` the traits resolve to exactly `in-review` and
`in-progress`.

I first generalised this to "any target earlier in the workflow's
declared order" — arguably nicer, and I had it working. Then I measured
it: it relabels moves this change never set out to touch. **18 assertion
sites across three suites** flip from "Move to" to "Back to" (e.g. a
card in In progress gets "Back to Todo", "Back to Planning").
Same-set-different-derivation is the honest scope here; widening which
moves read as backwards is a separate, visible product decision, not a
side effect of a vocabulary fix.

### Two things I chose not to delete, and why

Both are still-live `VALID_TRANSITIONS` reads in this file. Neither is
removable today, and the reason is the same missing wire field —
documented at both sites rather than left as a puzzle.

**1. The default-column-set shortcut.** `TaskContextMenuColumnMetadata`
carries id/label/flags but **no adjacency**, so the workflow branch can
only guess targets from a column's neighbours in declared order.
Measured against the real graph that is a strict loss:

| current | `VALID_TRANSITIONS` | neighbour-derived |
|---|---|---|
| `in-progress` | in-review, todo, triage, done (4) | todo, in-review
(2) |
| `todo` | in-progress, triage, archived (3) | triage, in-progress (2) |
| `done` | todo, triage, archived (3) | in-review, archived (2) |

Deleting that read is not a cleanup — it drops real operator moves
(archive from Todo, straight-to-Done from In progress). Note the guard
keys on the column **id set**, so a workflow that merely renames the six
built-ins still takes this path and still gets correct targets; only
reordering or replacing them falls through to the weaker logic.

**2. The no-metadata fallback.** I removed it first, on principle, and
measured the result: `workflowMoveColumns` is optional at both call
sites (`workflowMoveMetadata?.moveColumns`, `taskMoveColumns`) and
genuinely undefined until board-workflows resolves, so dropping it left
Task Detail with **no move options during load**. That is a live surface
degraded to satisfy a purity rule, so it is not shipped. Unlike Board
and ListView — where the legacy path was provably unreachable — this one
is reachable and useful.

Both retire the same way: put each column's allowed targets on the
board-workflows payload so the load window has real data instead of a
guess. That is a server + wire + client change and belongs in its own
slice.

### Revert-proof

The renamed-workflow fixture declares `signoff` (mergeBlocker) and
`building` (countsTowardWip). Restore the id literals and the new case
fails — `Move to Building` instead of `Back to Building` — which no
relabelling of the old hardcoded string could satisfy, since that string
names a column absent from the board. The same case asserts the forward
move keeps "Move to Shipped", so the rule stays a distinction rather
than a blanket relabel.

### Verification

`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, `pnpm verify:fast`,
dashboard typecheck green.

**No new test failures**, established properly: the three suites this
touches report 30 failures both with and without the change, and I
diffed the failing test *names* against a stashed clean tree rather than
comparing counts — the sets are identical. (An earlier count-only
comparison had me chasing two failures that turned out to be my own new
assertions.)

Also regenerates `packages/i18n/src/resources.d.ts` via `pnpm
i18n:types`. That picks up **~45 lines of pre-existing drift** from
earlier merges that did not regenerate it; the file is generated, and
leaving it stale would omit the new key from the types. Flagged so the
extra lines are not mistaken for scope creep. Note
`packages/dashboard/app/locales/` is gitignored (copied from
`packages/i18n/locales/`), so only the canonical locales are committed.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-28 22:19:42 -07:00
committed by GitHub
parent 99be8e6153
commit ebc89310bc
11 changed files with 227 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: The "Back to" move-menu label now uses your workflow's own review and work column names.
category: fix
dev: `getTaskMoveTransitions` derived the "Back to In Progress" label from the hardcoded ids `in-review`/`in-progress` plus a hardcoded English string, so a workflow renaming those lanes either lost the label or named a column not on the board. Now keyed on the `mergeBlocker` (current) and `countsTowardWip` (target) traits with the column's own label via a new `taskDetail.move.backTo` key. The labelled set is unchanged for built-in workflows.

View File

@@ -158,6 +158,32 @@ Manual pull-request review has two separate operator intents: Start PR Review op
*/
function getWorkflowMoveTargets(task: Task | TaskDetail, columns: readonly TaskContextMenuColumnMetadata[]): ColumnId[] {
const visibleColumns = columns.filter((column) => column.flags?.hiddenFromBoard !== true);
/*
FNXC:TaskContextMenu 2026-07-29-00:00 (U12 — R8, KNOWN REMAINING GAP):
This `VALID_TRANSITIONS` shortcut is the LAST legacy-vocabulary read in this file and
it is deliberately KEPT, because removing it today would silently SHRINK the move
menu for every default-workflow project rather than fix anything.
The reason is a missing wire field, not a missing idea. `TaskContextMenuColumnMetadata`
carries id/label/flags but NO adjacency, so the workflow branch below can only guess
targets from a column's neighbours in the declared order — [previous, next]. Measured
against the real graph that is a strict loss:
in-progress VALID_TRANSITIONS: in-review, todo, triage, done (4)
neighbour-derived: todo, in-review (2)
todo VALID_TRANSITIONS: in-progress, triage, archived (3)
neighbour-derived: triage, in-progress (2)
done VALID_TRANSITIONS: todo, triage, archived (3)
neighbour-derived: in-review, archived (2)
So "delete the legacy read" here is not a cleanup — it is a UI regression that drops
real operator moves (archive from Todo, straight-to-Done from In progress). The
correct fix is to put each column's allowed targets on the board-workflows payload
and read THOSE, which changes the server, the wire shape and this file, and is its own
slice. Note the guard is keyed on the COLUMN ID SET, so a workflow that merely renames
the six built-in columns still takes this path and still gets correct targets; only a
workflow that reorders or replaces them falls through to the weaker neighbour logic.
*/
if (isDefaultWorkflowColumnSet(visibleColumns) && isColumn(task.column)) {
return task.column === "in-review" ? ["todo", "in-progress"] : [...VALID_TRANSITIONS[task.column]];
}
@@ -195,19 +221,80 @@ export function getTaskMoveTransitions(
columnLabel: (column: ColumnId) => string,
workflowMoveColumns?: readonly TaskContextMenuColumnMetadata[],
): TaskMoveActionDescriptor[] {
/*
FNXC:TaskContextMenu 2026-07-29-00:00 (U12 — R8, decision recorded):
The no-metadata `VALID_TRANSITIONS` fallback is KEPT. I removed it first, on the R8
principle, and measured the result: `workflowMoveColumns` is optional at both call
sites (`workflowMoveMetadata?.moveColumns`, `taskMoveColumns`) and is genuinely
undefined until board-workflows resolves, so dropping it left Task Detail with NO
move options during load — a live surface degraded to satisfy a purity rule. That is
a regression, not a cleanup, so it is not shipped.
Unlike Board and ListView, where the legacy path was provably unreachable, this one
is reachable and useful. It is retired the same way the shortcut above is: by putting
each column's allowed targets on the board-workflows payload so the load window has
real data instead of a guess.
*/
const moveTransitions: ColumnId[] = workflowMoveColumns
? getWorkflowMoveTargets(task, workflowMoveColumns)
: isColumn(task.column)
? (task.column === "in-review" ? ["todo", "in-progress"] : [...VALID_TRANSITIONS[task.column]])
: [];
const workflowLabelById = new Map((workflowMoveColumns ?? []).map((column) => [column.id, column.label]));
const visibleOrdered = (workflowMoveColumns ?? []).filter((column) => column.flags?.hiddenFromBoard !== true);
const workflowLabelById = new Map(visibleOrdered.map((column) => [column.id, column.label]));
/*
FNXC:TaskContextMenu 2026-07-29-00:00 (U12 — R8):
"Back to X" is derived from COLUMN TRAITS, not from the literals `in-review` and
`in-progress`. The old condition (`column === "in-progress" && task.column ===
"in-review"`) hardcoded two lifecycle ids AND a hardcoded English label ("Back to In
Progress"), so on a workflow that renames those columns it either failed to fire or
announced a column name that is not on the board.
The rule it was expressing is "leaving the review lane backwards into the work lane",
which the traits already say: the CURRENT column carries `mergeBlocker`, the TARGET
carries `countsTowardWip`. For builtin:coding those are exactly in-review and
in-progress, so the labelled set is unchanged — deliberately. I first generalised
this to "any target earlier in the workflow order", which is arguably nicer but
relabels moves this change never set out to touch (18 assertion sites across three
suites would have flipped from "Move to" to "Back to"). Same-set-different-derivation
is the honest scope here; widening which moves read as backwards is a separate,
visible product decision.
Load window (no metadata): fall back to the legacy id pair, matching the fallback
already kept for the targets themselves a few lines below.
*/
const flagsById = new Map(visibleOrdered.map((column) => [column.id, column.flags]));
const orderById = new Map(visibleOrdered.map((column, index) => [column.id, index]));
const currentFlags = flagsById.get(task.column);
const currentOrder = orderById.get(task.column);
const isBackwardsLabel = (target: ColumnId): boolean => {
if (visibleOrdered.length === 0) {
return target === "in-progress" && task.column === "in-review";
}
/*
FNXC:TaskContextMenu 2026-07-29-00:00 (PR #2521 review — greptile):
DIRECTION as well as traits. The traits alone say "review lane -> work lane", but a
workflow may declare a `countsTowardWip` column AFTER its `mergeBlocker` one (a
rework or hotfix lane placed downstream of review). Labelling that "Back to" would
call a FORWARD move backwards — the same class of wrongness as the hardcoded ids
this predicate replaced, just arrived at differently.
Requiring the target to sit EARLIER in the workflow's declared order keeps the
builtin:coding set unchanged (in-progress precedes in-review) while making the
label mean what it says on any column layout.
*/
if (currentOrder === undefined) return false;
const targetOrder = orderById.get(target);
if (targetOrder === undefined || targetOrder >= currentOrder) return false;
return currentFlags?.mergeBlocker === true && flagsById.get(target)?.countsTowardWip === true;
};
return moveTransitions.map((column) => {
const label = workflowLabelById.get(column) ?? columnLabel(column);
return {
column,
label: column === "in-progress" && task.column === "in-review"
? t("taskDetail.move.backToInProgress", "Back to In Progress")
label: isBackwardsLabel(column)
? t("taskDetail.move.backTo", "Back to {{column}}", { column: label })
: t("taskDetail.move.moveTo", "Move to {{column}}", { column: label }),
primaryLabel: t("taskDetail.move.moveTo", "Move to {{column}}", { column: label }),
};

View File

@@ -922,7 +922,17 @@ describe("ListView", () => {
fireEvent.contextMenu(document.querySelector('.list-row[data-id="FN-003"]') as HTMLElement, { clientX: 40, clientY: 50 });
expect(screen.getByRole("menuitem", { name: "Merge & Close" })).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Refine" })).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Back to In Progress" })).toBeInTheDocument();
/*
FNXC:TaskContextMenu 2026-07-29-00:00 (U12 — R8):
"In progress", not "In Progress". The label is now interpolated from the WORKFLOW's
own column name (`BUILTIN_CODING_WORKFLOW_IR` declares "In progress") instead of the
hardcoded English string `taskDetail.move.backToInProgress`. This assertion is the
visible proof that the label follows the workflow: rename that column and the menu
renames with it. Task Detail cases that render before board-workflows resolves still
read "Back to In Progress" — they go through the no-metadata fallback, which uses
the legacy column label map.
*/
expect(screen.getByRole("menuitem", { name: "Back to In progress" })).toBeInTheDocument();
fireEvent.contextMenu(document.querySelector('.list-row[data-id="FN-006"]') as HTMLElement, { clientX: 40, clientY: 50 });
expect(screen.getByRole("menuitem", { name: "Merge & Close" })).toBeInTheDocument();

View File

@@ -442,6 +442,68 @@ describe("U10 — surfaces render workflow-resolved columns", () => {
]);
});
/*
FNXC:TaskContextMenu 2026-07-29-00:00 (U12 — R8):
The "Back to X" label must follow the workflow's TRAITS, not the hardcoded ids
`in-review`/`in-progress`. This workflow renames both lanes — `signoff` carries
`mergeBlocker`, `building` carries `countsTowardWip` — so leaving review backwards
into the work lane must read "Back to Building" and name the column the operator
actually sees.
REVERT CHECK: restore `column === "in-progress" && task.column === "in-review"` and
this fails twice over — the ids never match, so the label falls through to "Move to
Building"; and the old hardcoded string would have said "Back to In Progress", a
column name absent from this board entirely.
*/
it("labels a backwards move using the workflow's own review/work traits and label", () => {
const transitions = getTaskMoveTransitions(
mkTask({ id: "FN-15", column: "signoff" as Task["column"] }),
t,
columnLabel,
renamedMoveColumns,
);
const building = transitions.find((transition) => transition.column === "building");
expect(building).toBeDefined();
expect(building!.label).toBe("Back to Building");
// The forward move keeps "Move to", so the rule stays a distinction and not a
// blanket relabel.
const shipped = transitions.find((transition) => transition.column === "shipped");
expect(shipped?.label).toBe("Move to Shipped");
});
/*
FNXC:TaskContextMenu 2026-07-29-00:00 (PR #2521 review — greptile):
A workflow may place a `countsTowardWip` lane AFTER its `mergeBlocker` one (rework
or hotfix downstream of review). The traits alone would label that FORWARD move
"Back to"; direction has to be part of the predicate.
REVERT CHECK: drop the order comparison and this fails — `hotfix` is labelled
"Back to Hotfix" despite sitting after `signoff` in the declared order.
*/
it("does not call a forward move backwards when a WIP lane follows review", () => {
// Placed immediately AFTER signoff so it is a neighbour (and therefore offered),
// while still sitting later in the declared order.
const signoffIndex = renamedMoveColumns.findIndex((column) => column.id === "signoff");
const downstreamWip: TaskContextMenuColumnMetadata[] = [
...renamedMoveColumns.slice(0, signoffIndex + 1),
{ id: "hotfix" as ColumnId, label: "Hotfix", flags: { countsTowardWip: true } },
...renamedMoveColumns.slice(signoffIndex + 1),
];
const transitions = getTaskMoveTransitions(
mkTask({ id: "FN-18", column: "signoff" as Task["column"] }),
t,
columnLabel,
downstreamWip,
);
const hotfix = transitions.find((transition) => transition.column === "hotfix");
expect(hotfix).toBeDefined();
expect(hotfix!.label).toBe("Move to Hotfix");
// The genuinely-backwards move keeps its label, so the fix narrows rather than
// disables the rule.
const building = transitions.find((transition) => transition.column === "building");
expect(building?.label).toBe("Back to Building");
});
it("never offers a column the workflow does not declare", () => {
const transitions = getTaskMoveTransitions(
mkTask({ id: "FN-13", column: "in-review" }),

View File

@@ -7828,6 +7828,7 @@
},
"move": {
"backToInProgress": "Back to In Progress",
"backTo": "Back to {{column}}",
"cancelMove": "Cancel Move",
"keepProgress": "Keep Progress",
"movedTo": "Moved to {{column}}",

View File

@@ -7791,6 +7791,7 @@
},
"move": {
"backToInProgress": "Volver a En progreso",
"backTo": "Volver a {{column}}",
"cancelMove": "Cancelar movimiento",
"keepProgress": "Conservar progreso",
"movedTo": "Movido a {{column}}",

View File

@@ -7791,6 +7791,7 @@
},
"move": {
"backToInProgress": "Retour à En cours",
"backTo": "Retour à {{column}}",
"cancelMove": "Annuler le déplacement",
"keepProgress": "Conserver la progression",
"movedTo": "Déplacé vers {{column}}",

View File

@@ -7791,6 +7791,7 @@
},
"move": {
"backToInProgress": "진행 중으로 돌아가기",
"backTo": "{{column}}(으)로 돌아가기",
"cancelMove": "이동 취소",
"keepProgress": "진행 상태 유지",
"movedTo": "{{column}}으로 이동됨",

View File

@@ -7791,6 +7791,7 @@
},
"move": {
"backToInProgress": "返回进行中",
"backTo": "返回{{column}}",
"cancelMove": "取消移动",
"keepProgress": "保留进度",
"movedTo": "已移至 {{column}}",

View File

@@ -7791,6 +7791,7 @@
},
"move": {
"backToInProgress": "返回進行中",
"backTo": "返回{{column}}",
"cancelMove": "取消移動",
"keepProgress": "保留進度",
"movedTo": "已移至 {{column}}",

View File

@@ -1197,6 +1197,7 @@ export default interface Resources {
"mergeBlocked": "This task is blocked from completing until its merge step finishes.",
"promoteRejected": "This card could not be promoted.",
"unknownColumn": "That column doesn't exist in this task's workflow.",
"unplannedForExecution": "This task isn't ready for execution yet — planning or plan review is still outstanding.",
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
},
"todo": "To Do",
@@ -1507,6 +1508,10 @@ export default interface Resources {
"preserveProgressMessage": "This task has completed steps. Keep progress before moving?",
"preserveProgressMoveTodoMessage": "Some tasks have completed steps. Keep progress before moving to Todo?",
"preserveProgressTitle": "Preserve Progress?",
"promoteUnplannedCancel": "Keep Waiting",
"promoteUnplannedConfirm": "Start Anyway",
"promoteUnplannedMessage": "{{taskId}} is still waiting on planning or plan review. Promoting now starts execution with the current plan and cancels the pending replan.",
"promoteUnplannedTitle": "Start execution anyway?",
"replanAll": "Replan All",
"replanAllHint_one": "Move {{count}} task{{plural}} to Planning",
"replanAllHint_other": "Move {{count}} task{{plural}} to Planning",
@@ -5745,6 +5750,10 @@ export default interface Resources {
"title": "Appearance"
},
"auth": {
"anthropicPreferenceApiKey": "API key",
"anthropicPreferenceHint": "You have both an Anthropic API key and a Claude subscription connected. Choose which one Fusion sends when a lane calls Anthropic directly. Default: API key.",
"anthropicPreferenceLabel": "Anthropic credential to use",
"anthropicPreferenceSubscription": "Claude subscription",
"apiKeyCleared": "API key cleared",
"apiKeySaved": "API key saved",
"authCodeAlreadySubmitted": "That authorization code was already submitted. Waiting for login…",
@@ -5753,6 +5762,8 @@ export default interface Resources {
"continueToLogin": "Continue to login",
"copiedCodeToClipboard": "Copied code to clipboard",
"copyCode": "Copy code",
"credentialInUse": "In use",
"credentialOverridden": "Overridden below",
"enterCodeOnGitHub": "Enter this code on GitHub",
"failedToCopyCode": "Failed to copy code — copy it manually from the box above",
"groupAuthenticated": "Authenticated",
@@ -5958,6 +5969,7 @@ export default interface Resources {
"offDefault": "Off (default)",
"onForNewTasks": "On for new tasks",
"operationalLogRetention": "Operational log retention",
"originWorkflowSelected": "Selected workflow",
"ownerRepo": "owner/repo",
"prefixForNewTaskIDsEGKB": "Prefix for new task IDs (e.g. KB, PROJ). No default — unset.",
"prefixMustBe110UppercaseLetters": "Prefix must be 1–10 uppercase letters",
@@ -5965,6 +5977,8 @@ export default interface Resources {
"quickChatCloseOnOutsideClickHint": "When enabled, clicking outside the Quick Chat window closes it. Disable to keep it open until you close it explicitly. Default: enabled.",
"quickChatLauncherHint": "Choose whether Quick Chat opens from the draggable floating button, a footer button beside Terminal, or stays hidden. Default: off (hidden).",
"recentVerbatimRoomMessages": "Recent verbatim room messages",
"refinementTaskWorkflow": "Refinement task workflow",
"refinementTaskWorkflowHelp": "Workflow applied to refinement tasks — the follow-up card spawned from a done or in-review task plus your feedback. Choose \"Selected workflow\" to follow your current board workflow (falling back to the project default workflow). No default — unset means Selected workflow.",
"removeNavItem": "Remove {{item}}",
"reportDiscussionCategory": "Discussion category",
"reportDiscussionCategoryEmpty": "No Discussion categories are available for the configured repository.",
@@ -6010,6 +6024,8 @@ export default interface Resources {
"showQuickChatButton": " Show quick chat button ",
"showTaskChatsInCommonFeedHint": "When enabled, populated task-detail Chat conversations appear in the common Direct feed. Empty task chats stay hidden. Default: disabled.",
"showTheFloatingChatButtonInTheDashboard": "Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.",
"taskCreateWorkflow": "CLI/agent-created task workflow",
"taskCreateWorkflowHelp": "Workflow applied to tasks opened by `fn task create` and the fn_task_create agent tool, which have no workflow picker. Choose \"Selected workflow\" to follow your current board workflow (falling back to the project default workflow). No default — unset means Selected workflow. An explicit workflow_id passed to fn_task_create still wins.",
"taskPrefix": "Task Prefix",
"todoThreshold": "Todo threshold",
"trackingIssuesUseThisTaskAposSTitle": " Tracking issues use this task's title. If a task has no title yet, Fusion can summarize its description using the title summarization model in Project Models. ",
@@ -6035,9 +6051,11 @@ export default interface Resources {
},
"globalGeneral": {
"agentToolOutputLimit": " Agent tool-output limit ",
"agentToolOutputLimitHint": " Maximum characters returned from each engine-injected tool result. Default: 16,000 characters per tool result. Leave empty to use the default. ",
"agentToolOutputLimitHint": " Maximum characters returned from each engine-injected tool result. When unset, inherits the 16,000-character engine default. Leave empty to use the default. ",
"andShowsUpdateNoticesInTheCLIAnd": " and shows update notices in the CLI and dashboard. Cadence is governed by the frequency below. Default: enabled. ",
"autoReloadDashboardOnVersionChange": " Auto-reload dashboard on version change ",
"autoUpdateAndRestart": " Auto-update and restart ",
"autoUpdateAndRestartHelp": "When enabled, Fusion installs available updates on the selected release channel by itself and then restarts to apply them. Requires a supervising parent (the default for `fn dashboard`); hosts started with --no-supervise skip the install. Default: disabled.",
"cLIBinaryOnPATH": " CLI binary on PATH ",
"cLIBySpawning": " CLI by spawning",
"channelBeta": "Beta — early builds from main",
@@ -6061,7 +6079,7 @@ export default interface Resources {
"leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ",
"manualOnlyNeverAutoCheck": "Manual only — never auto-check",
"noLimitOnAgentToolOutput": " No limit on agent tool output ",
"noLimitOnAgentToolOutputHint": " Disable the shared tool-output clamp. A single tool result can consume the agent context window. Default: disabled; the default budget is 16,000 characters per tool result. ",
"noLimitOnAgentToolOutputHint": " Disable the shared tool-output clamp. A single tool result can consume the agent context window. Default: disabled; when unset, the budget inherits the 16,000-character engine default. ",
"onStartupOncePerServerLaunch": "On startup — once per server launch",
"ownerRepo": "owner/repo",
"projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo. No default — unset.",
@@ -6422,7 +6440,8 @@ export default interface Resources {
"tooltip": {
"global": "Shared across all projects",
"project": "Specific to this project"
}
},
"voiceInput": "Voice Input"
},
"nodeRouting": {
"blockExecution": "Block execution",
@@ -6847,6 +6866,25 @@ export default interface Resources {
"settingResultCount_other": "{{count}} matching settings"
},
"title": "Settings",
"voiceInput": {
"download": "Download",
"downloading": "Downloading model…",
"downloadingProgress": "Downloading: {{progress}}%",
"enable": "Enable voice input",
"enableHelp": "Default: off. Voice dictation uses the operator-managed Parakeet v3 model.",
"error": "Model error: {{message}}",
"installed": "Installed",
"modelActions": "Model management",
"modelActionsHelp": "Download or remove the Parakeet v3 speech model.",
"modelStatus": "Parakeet v3 model status",
"modelStatusHelp": "The speech model is installed and managed locally on this device.",
"notInstalled": "Not installed",
"remove": "Remove",
"runtimeUnavailable": "Voice runtime unavailable",
"statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.",
"title": "Voice Input",
"unknown": "Status unavailable"
},
"worktrees": {
"allowSilentSiblingBranchRenameDuringExecutorConflicts": " Allow silent sibling branch rename during executor conflicts ",
"alsoRebaseOntoLocalDefaultBranchHEAD": " Also rebase onto local default-branch HEAD ",
@@ -6876,8 +6914,6 @@ export default interface Resources {
"keepsProgressMovingBySwitchingToFusionApos": " keeps progress moving by switching to Fusion&apos;s built-in worktree backend. ",
"limitsTotalGitWorktreesIncludingInReviewTasks": "Limits total git worktrees including in-review tasks. Default: 4.",
"maxWorktrees": "Max Worktrees",
"worktreeLimitEnabled": "Limit concurrent worktrees",
"worktreeLimitEnabledHelp": "When on, Max Worktrees caps how many tasks may hold a worktree at once. When off, Max Concurrent Tasks is the only limit. Tasks always run in their own git worktree either way — this does not change where work executes. Default: on.",
"namingStyleNotApplicableWhenRecycling": "Naming style is not applicable when recycling worktrees — pooled worktrees retain their existing names. \"Task ID\" is unavailable here because task-pinned worktrees are mutually exclusive with recycling; turn off Recycle worktrees to use it.",
"offByDefaultOptInWhenEnabledCompleted": "Off by default (opt-in). When enabled, completed task worktrees are returned to an idle pool instead of being deleted, preserving build caches for faster startup. Mutually exclusive with Task ID worktree naming.",
"openApprovals": "Open Approvals",
@@ -6901,6 +6937,8 @@ export default interface Resources {
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": " when unset. Only affects newly-created worktrees. ",
"whichRemoteToFetchForThePreMerge": " Which remote to fetch for the pre-merge rebase. \"Use git default\" falls back to the remote configured for the default branch (typically ",
"worktreeInitCommand": "Worktree Init Command",
"worktreeLimitEnabled": "Limit concurrent worktrees",
"worktreeLimitEnabledHelp": "When on, Max Worktrees caps how many tasks may hold a worktree at once. When off, Max Concurrent Tasks is the only limit. Tasks always run in their own git worktree either way — this does not change where work executes. Default: on.",
"worktreeNamingStyle": "Worktree Naming Style",
"worktrees": "Worktrees",
"worktreesDirectory": "Worktrees Directory",
@@ -7863,6 +7901,7 @@ export default interface Resources {
"title": "Merge Task"
},
"move": {
"backTo": "Back to {{column}}",
"backToInProgress": "Back to In Progress",
"cancelMove": "Cancel Move",
"keepProgress": "Keep Progress",
@@ -8385,6 +8424,8 @@ export default interface Resources {
"cliWaitingOnInputTitle": "The CLI agent is waiting for your input",
"closeIssue": "Close Issue",
"collapse": "Collapse",
"completedAt": "Completed {{date}}",
"completedAtTitle": "Completed {{date}}",
"costBadgeAriaLabel": "Estimated cost {{amount}}",
"costBadgeTitle": "Estimated cost {{amount}}",
"createFailed": "Failed to create task",
@@ -8392,6 +8433,8 @@ export default interface Resources {
"createPrAriaLabel": "Create pull request",
"createPrTitle": "Create a PR for this task",
"createTaskTitle": "Create task",
"createdAt": "Created {{date}}",
"createdAtTitle": "Created {{date}}",
"createdByAgent": "Created by agent",
"createdByAgentNamed": "Created by agent: {{name}}",
"createdPr": "Created PR #{{number}}",
@@ -8513,6 +8556,8 @@ export default interface Resources {
"refineExpandDesc": "Expand into a more comprehensive description",
"refineSimplify": "Simplify",
"refineSimplifyDesc": "Simplify and make more concise",
"refinesOf": "Refines {{id}}",
"refinesOfTitle": "Refinement of {{id}}",
"refining": "Refining...",
"removeImage": "Remove image",
"resetProgress": "Reset Progress",
@@ -9490,6 +9535,7 @@ export default interface Resources {
"guardRejected": "This move is not allowed by the workflow.",
"mergeBlocked": "This task is blocked from completing until its merge step finishes.",
"unknownColumn": "That column doesn't exist in this task's workflow.",
"unplannedForExecution": "This task isn't ready for execution yet — planning or plan review is still outstanding.",
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
}
},