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:
7
.changeset/u12-context-menu-back-to-label.md
Normal file
7
.changeset/u12-context-menu-back-to-label.md
Normal 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.
|
||||||
@@ -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[] {
|
function getWorkflowMoveTargets(task: Task | TaskDetail, columns: readonly TaskContextMenuColumnMetadata[]): ColumnId[] {
|
||||||
const visibleColumns = columns.filter((column) => column.flags?.hiddenFromBoard !== true);
|
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)) {
|
if (isDefaultWorkflowColumnSet(visibleColumns) && isColumn(task.column)) {
|
||||||
return task.column === "in-review" ? ["todo", "in-progress"] : [...VALID_TRANSITIONS[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,
|
columnLabel: (column: ColumnId) => string,
|
||||||
workflowMoveColumns?: readonly TaskContextMenuColumnMetadata[],
|
workflowMoveColumns?: readonly TaskContextMenuColumnMetadata[],
|
||||||
): TaskMoveActionDescriptor[] {
|
): 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
|
const moveTransitions: ColumnId[] = workflowMoveColumns
|
||||||
? getWorkflowMoveTargets(task, workflowMoveColumns)
|
? getWorkflowMoveTargets(task, workflowMoveColumns)
|
||||||
: isColumn(task.column)
|
: isColumn(task.column)
|
||||||
? (task.column === "in-review" ? ["todo", "in-progress"] : [...VALID_TRANSITIONS[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) => {
|
return moveTransitions.map((column) => {
|
||||||
const label = workflowLabelById.get(column) ?? columnLabel(column);
|
const label = workflowLabelById.get(column) ?? columnLabel(column);
|
||||||
return {
|
return {
|
||||||
column,
|
column,
|
||||||
label: column === "in-progress" && task.column === "in-review"
|
label: isBackwardsLabel(column)
|
||||||
? t("taskDetail.move.backToInProgress", "Back to In Progress")
|
? t("taskDetail.move.backTo", "Back to {{column}}", { column: label })
|
||||||
: t("taskDetail.move.moveTo", "Move to {{column}}", { column: label }),
|
: t("taskDetail.move.moveTo", "Move to {{column}}", { column: label }),
|
||||||
primaryLabel: t("taskDetail.move.moveTo", "Move to {{column}}", { column: label }),
|
primaryLabel: t("taskDetail.move.moveTo", "Move to {{column}}", { column: label }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -922,7 +922,17 @@ describe("ListView", () => {
|
|||||||
fireEvent.contextMenu(document.querySelector('.list-row[data-id="FN-003"]') as HTMLElement, { clientX: 40, clientY: 50 });
|
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: "Merge & Close" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("menuitem", { name: "Refine" })).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 });
|
fireEvent.contextMenu(document.querySelector('.list-row[data-id="FN-006"]') as HTMLElement, { clientX: 40, clientY: 50 });
|
||||||
expect(screen.getByRole("menuitem", { name: "Merge & Close" })).toBeInTheDocument();
|
expect(screen.getByRole("menuitem", { name: "Merge & Close" })).toBeInTheDocument();
|
||||||
|
|||||||
@@ -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", () => {
|
it("never offers a column the workflow does not declare", () => {
|
||||||
const transitions = getTaskMoveTransitions(
|
const transitions = getTaskMoveTransitions(
|
||||||
mkTask({ id: "FN-13", column: "in-review" }),
|
mkTask({ id: "FN-13", column: "in-review" }),
|
||||||
|
|||||||
@@ -7828,6 +7828,7 @@
|
|||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
"backToInProgress": "Back to In Progress",
|
"backToInProgress": "Back to In Progress",
|
||||||
|
"backTo": "Back to {{column}}",
|
||||||
"cancelMove": "Cancel Move",
|
"cancelMove": "Cancel Move",
|
||||||
"keepProgress": "Keep Progress",
|
"keepProgress": "Keep Progress",
|
||||||
"movedTo": "Moved to {{column}}",
|
"movedTo": "Moved to {{column}}",
|
||||||
|
|||||||
@@ -7791,6 +7791,7 @@
|
|||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
"backToInProgress": "Volver a En progreso",
|
"backToInProgress": "Volver a En progreso",
|
||||||
|
"backTo": "Volver a {{column}}",
|
||||||
"cancelMove": "Cancelar movimiento",
|
"cancelMove": "Cancelar movimiento",
|
||||||
"keepProgress": "Conservar progreso",
|
"keepProgress": "Conservar progreso",
|
||||||
"movedTo": "Movido a {{column}}",
|
"movedTo": "Movido a {{column}}",
|
||||||
|
|||||||
@@ -7791,6 +7791,7 @@
|
|||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
"backToInProgress": "Retour à En cours",
|
"backToInProgress": "Retour à En cours",
|
||||||
|
"backTo": "Retour à {{column}}",
|
||||||
"cancelMove": "Annuler le déplacement",
|
"cancelMove": "Annuler le déplacement",
|
||||||
"keepProgress": "Conserver la progression",
|
"keepProgress": "Conserver la progression",
|
||||||
"movedTo": "Déplacé vers {{column}}",
|
"movedTo": "Déplacé vers {{column}}",
|
||||||
|
|||||||
@@ -7791,6 +7791,7 @@
|
|||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
"backToInProgress": "진행 중으로 돌아가기",
|
"backToInProgress": "진행 중으로 돌아가기",
|
||||||
|
"backTo": "{{column}}(으)로 돌아가기",
|
||||||
"cancelMove": "이동 취소",
|
"cancelMove": "이동 취소",
|
||||||
"keepProgress": "진행 상태 유지",
|
"keepProgress": "진행 상태 유지",
|
||||||
"movedTo": "{{column}}으로 이동됨",
|
"movedTo": "{{column}}으로 이동됨",
|
||||||
|
|||||||
@@ -7791,6 +7791,7 @@
|
|||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
"backToInProgress": "返回进行中",
|
"backToInProgress": "返回进行中",
|
||||||
|
"backTo": "返回{{column}}",
|
||||||
"cancelMove": "取消移动",
|
"cancelMove": "取消移动",
|
||||||
"keepProgress": "保留进度",
|
"keepProgress": "保留进度",
|
||||||
"movedTo": "已移至 {{column}}",
|
"movedTo": "已移至 {{column}}",
|
||||||
|
|||||||
@@ -7791,6 +7791,7 @@
|
|||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
"backToInProgress": "返回進行中",
|
"backToInProgress": "返回進行中",
|
||||||
|
"backTo": "返回{{column}}",
|
||||||
"cancelMove": "取消移動",
|
"cancelMove": "取消移動",
|
||||||
"keepProgress": "保留進度",
|
"keepProgress": "保留進度",
|
||||||
"movedTo": "已移至 {{column}}",
|
"movedTo": "已移至 {{column}}",
|
||||||
|
|||||||
56
packages/i18n/src/resources.d.ts
vendored
56
packages/i18n/src/resources.d.ts
vendored
@@ -1197,6 +1197,7 @@ export default interface Resources {
|
|||||||
"mergeBlocked": "This task is blocked from completing until its merge step finishes.",
|
"mergeBlocked": "This task is blocked from completing until its merge step finishes.",
|
||||||
"promoteRejected": "This card could not be promoted.",
|
"promoteRejected": "This card could not be promoted.",
|
||||||
"unknownColumn": "That column doesn't exist in this task's workflow.",
|
"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."
|
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
|
||||||
},
|
},
|
||||||
"todo": "To Do",
|
"todo": "To Do",
|
||||||
@@ -1507,6 +1508,10 @@ export default interface Resources {
|
|||||||
"preserveProgressMessage": "This task has completed steps. Keep progress before moving?",
|
"preserveProgressMessage": "This task has completed steps. Keep progress before moving?",
|
||||||
"preserveProgressMoveTodoMessage": "Some tasks have completed steps. Keep progress before moving to Todo?",
|
"preserveProgressMoveTodoMessage": "Some tasks have completed steps. Keep progress before moving to Todo?",
|
||||||
"preserveProgressTitle": "Preserve Progress?",
|
"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",
|
"replanAll": "Replan All",
|
||||||
"replanAllHint_one": "Move {{count}} task{{plural}} to Planning",
|
"replanAllHint_one": "Move {{count}} task{{plural}} to Planning",
|
||||||
"replanAllHint_other": "Move {{count}} task{{plural}} to Planning",
|
"replanAllHint_other": "Move {{count}} task{{plural}} to Planning",
|
||||||
@@ -5745,6 +5750,10 @@ export default interface Resources {
|
|||||||
"title": "Appearance"
|
"title": "Appearance"
|
||||||
},
|
},
|
||||||
"auth": {
|
"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",
|
"apiKeyCleared": "API key cleared",
|
||||||
"apiKeySaved": "API key saved",
|
"apiKeySaved": "API key saved",
|
||||||
"authCodeAlreadySubmitted": "That authorization code was already submitted. Waiting for login…",
|
"authCodeAlreadySubmitted": "That authorization code was already submitted. Waiting for login…",
|
||||||
@@ -5753,6 +5762,8 @@ export default interface Resources {
|
|||||||
"continueToLogin": "Continue to login",
|
"continueToLogin": "Continue to login",
|
||||||
"copiedCodeToClipboard": "Copied code to clipboard",
|
"copiedCodeToClipboard": "Copied code to clipboard",
|
||||||
"copyCode": "Copy code",
|
"copyCode": "Copy code",
|
||||||
|
"credentialInUse": "In use",
|
||||||
|
"credentialOverridden": "Overridden below",
|
||||||
"enterCodeOnGitHub": "Enter this code on GitHub",
|
"enterCodeOnGitHub": "Enter this code on GitHub",
|
||||||
"failedToCopyCode": "Failed to copy code — copy it manually from the box above",
|
"failedToCopyCode": "Failed to copy code — copy it manually from the box above",
|
||||||
"groupAuthenticated": "Authenticated",
|
"groupAuthenticated": "Authenticated",
|
||||||
@@ -5958,6 +5969,7 @@ export default interface Resources {
|
|||||||
"offDefault": "Off (default)",
|
"offDefault": "Off (default)",
|
||||||
"onForNewTasks": "On for new tasks",
|
"onForNewTasks": "On for new tasks",
|
||||||
"operationalLogRetention": "Operational log retention",
|
"operationalLogRetention": "Operational log retention",
|
||||||
|
"originWorkflowSelected": "Selected workflow",
|
||||||
"ownerRepo": "owner/repo",
|
"ownerRepo": "owner/repo",
|
||||||
"prefixForNewTaskIDsEGKB": "Prefix for new task IDs (e.g. KB, PROJ). No default — unset.",
|
"prefixForNewTaskIDsEGKB": "Prefix for new task IDs (e.g. KB, PROJ). No default — unset.",
|
||||||
"prefixMustBe110UppercaseLetters": "Prefix must be 1–10 uppercase letters",
|
"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.",
|
"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).",
|
"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",
|
"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}}",
|
"removeNavItem": "Remove {{item}}",
|
||||||
"reportDiscussionCategory": "Discussion category",
|
"reportDiscussionCategory": "Discussion category",
|
||||||
"reportDiscussionCategoryEmpty": "No Discussion categories are available for the configured repository.",
|
"reportDiscussionCategoryEmpty": "No Discussion categories are available for the configured repository.",
|
||||||
@@ -6010,6 +6024,8 @@ export default interface Resources {
|
|||||||
"showQuickChatButton": " Show quick chat button ",
|
"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.",
|
"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.",
|
"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",
|
"taskPrefix": "Task Prefix",
|
||||||
"todoThreshold": "Todo threshold",
|
"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. ",
|
"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": {
|
"globalGeneral": {
|
||||||
"agentToolOutputLimit": " Agent tool-output limit ",
|
"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. ",
|
"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 ",
|
"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 ",
|
"cLIBinaryOnPATH": " CLI binary on PATH ",
|
||||||
"cLIBySpawning": " CLI by spawning",
|
"cLIBySpawning": " CLI by spawning",
|
||||||
"channelBeta": "Beta — early builds from main",
|
"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 ",
|
"leaveBothThinkingTogglesOffToKeepThe": " Leave both thinking toggles off to keep the original default behavior. This only controls persisted ",
|
||||||
"manualOnlyNeverAutoCheck": "Manual only — never auto-check",
|
"manualOnlyNeverAutoCheck": "Manual only — never auto-check",
|
||||||
"noLimitOnAgentToolOutput": " No limit on agent tool output ",
|
"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",
|
"onStartupOncePerServerLaunch": "On startup — once per server launch",
|
||||||
"ownerRepo": "owner/repo",
|
"ownerRepo": "owner/repo",
|
||||||
"projectsInheritThisValueWhenTheyDoNot": "Projects inherit this value when they do not set a project default tracking repo. No default — unset.",
|
"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": {
|
"tooltip": {
|
||||||
"global": "Shared across all projects",
|
"global": "Shared across all projects",
|
||||||
"project": "Specific to this project"
|
"project": "Specific to this project"
|
||||||
}
|
},
|
||||||
|
"voiceInput": "Voice Input"
|
||||||
},
|
},
|
||||||
"nodeRouting": {
|
"nodeRouting": {
|
||||||
"blockExecution": "Block execution",
|
"blockExecution": "Block execution",
|
||||||
@@ -6847,6 +6866,25 @@ export default interface Resources {
|
|||||||
"settingResultCount_other": "{{count}} matching settings"
|
"settingResultCount_other": "{{count}} matching settings"
|
||||||
},
|
},
|
||||||
"title": "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": {
|
"worktrees": {
|
||||||
"allowSilentSiblingBranchRenameDuringExecutorConflicts": " Allow silent sibling branch rename during executor conflicts ",
|
"allowSilentSiblingBranchRenameDuringExecutorConflicts": " Allow silent sibling branch rename during executor conflicts ",
|
||||||
"alsoRebaseOntoLocalDefaultBranchHEAD": " Also rebase onto local default-branch HEAD ",
|
"alsoRebaseOntoLocalDefaultBranchHEAD": " Also rebase onto local default-branch HEAD ",
|
||||||
@@ -6876,8 +6914,6 @@ export default interface Resources {
|
|||||||
"keepsProgressMovingBySwitchingToFusionApos": " keeps progress moving by switching to Fusion's built-in worktree backend. ",
|
"keepsProgressMovingBySwitchingToFusionApos": " keeps progress moving by switching to Fusion's built-in worktree backend. ",
|
||||||
"limitsTotalGitWorktreesIncludingInReviewTasks": "Limits total git worktrees including in-review tasks. Default: 4.",
|
"limitsTotalGitWorktreesIncludingInReviewTasks": "Limits total git worktrees including in-review tasks. Default: 4.",
|
||||||
"maxWorktrees": "Max Worktrees",
|
"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.",
|
"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.",
|
"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",
|
"openApprovals": "Open Approvals",
|
||||||
@@ -6901,6 +6937,8 @@ export default interface Resources {
|
|||||||
"whenUnsetOnlyAffectsNewlyCreatedWorktrees": " when unset. Only affects newly-created worktrees. ",
|
"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 ",
|
"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",
|
"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",
|
"worktreeNamingStyle": "Worktree Naming Style",
|
||||||
"worktrees": "Worktrees",
|
"worktrees": "Worktrees",
|
||||||
"worktreesDirectory": "Worktrees Directory",
|
"worktreesDirectory": "Worktrees Directory",
|
||||||
@@ -7863,6 +7901,7 @@ export default interface Resources {
|
|||||||
"title": "Merge Task"
|
"title": "Merge Task"
|
||||||
},
|
},
|
||||||
"move": {
|
"move": {
|
||||||
|
"backTo": "Back to {{column}}",
|
||||||
"backToInProgress": "Back to In Progress",
|
"backToInProgress": "Back to In Progress",
|
||||||
"cancelMove": "Cancel Move",
|
"cancelMove": "Cancel Move",
|
||||||
"keepProgress": "Keep Progress",
|
"keepProgress": "Keep Progress",
|
||||||
@@ -8385,6 +8424,8 @@ export default interface Resources {
|
|||||||
"cliWaitingOnInputTitle": "The CLI agent is waiting for your input",
|
"cliWaitingOnInputTitle": "The CLI agent is waiting for your input",
|
||||||
"closeIssue": "Close Issue",
|
"closeIssue": "Close Issue",
|
||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
|
"completedAt": "Completed {{date}}",
|
||||||
|
"completedAtTitle": "Completed {{date}}",
|
||||||
"costBadgeAriaLabel": "Estimated cost {{amount}}",
|
"costBadgeAriaLabel": "Estimated cost {{amount}}",
|
||||||
"costBadgeTitle": "Estimated cost {{amount}}",
|
"costBadgeTitle": "Estimated cost {{amount}}",
|
||||||
"createFailed": "Failed to create task",
|
"createFailed": "Failed to create task",
|
||||||
@@ -8392,6 +8433,8 @@ export default interface Resources {
|
|||||||
"createPrAriaLabel": "Create pull request",
|
"createPrAriaLabel": "Create pull request",
|
||||||
"createPrTitle": "Create a PR for this task",
|
"createPrTitle": "Create a PR for this task",
|
||||||
"createTaskTitle": "Create task",
|
"createTaskTitle": "Create task",
|
||||||
|
"createdAt": "Created {{date}}",
|
||||||
|
"createdAtTitle": "Created {{date}}",
|
||||||
"createdByAgent": "Created by agent",
|
"createdByAgent": "Created by agent",
|
||||||
"createdByAgentNamed": "Created by agent: {{name}}",
|
"createdByAgentNamed": "Created by agent: {{name}}",
|
||||||
"createdPr": "Created PR #{{number}}",
|
"createdPr": "Created PR #{{number}}",
|
||||||
@@ -8513,6 +8556,8 @@ export default interface Resources {
|
|||||||
"refineExpandDesc": "Expand into a more comprehensive description",
|
"refineExpandDesc": "Expand into a more comprehensive description",
|
||||||
"refineSimplify": "Simplify",
|
"refineSimplify": "Simplify",
|
||||||
"refineSimplifyDesc": "Simplify and make more concise",
|
"refineSimplifyDesc": "Simplify and make more concise",
|
||||||
|
"refinesOf": "Refines {{id}}",
|
||||||
|
"refinesOfTitle": "Refinement of {{id}}",
|
||||||
"refining": "Refining...",
|
"refining": "Refining...",
|
||||||
"removeImage": "Remove image",
|
"removeImage": "Remove image",
|
||||||
"resetProgress": "Reset Progress",
|
"resetProgress": "Reset Progress",
|
||||||
@@ -9490,6 +9535,7 @@ export default interface Resources {
|
|||||||
"guardRejected": "This move is not allowed by the workflow.",
|
"guardRejected": "This move is not allowed by the workflow.",
|
||||||
"mergeBlocked": "This task is blocked from completing until its merge step finishes.",
|
"mergeBlocked": "This task is blocked from completing until its merge step finishes.",
|
||||||
"unknownColumn": "That column doesn't exist in this task's workflow.",
|
"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."
|
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user