feat(dashboard): success/failure edge authoring with cycle guard and interpreter-only banner

This commit is contained in:
gsxdsm
2026-06-04 19:35:34 -07:00
parent 297a00d3de
commit ffc4d8a6ed
22 changed files with 1570 additions and 1270 deletions

View File

@@ -95,6 +95,10 @@ export function validateLinearity(ir: WorkflowIr): WorkflowCompileError | null {
return new WorkflowCompileError(`node '${node.id}' has no outgoing edge`);
}
if (outs.length > 1) {
// NOTE: the `require the workflow interpreter (deferred)` suffix is matched
// by the dashboard editor (WorkflowNodeEditor handleSave, KTD-4) to render
// an info-tone "interpreter-only" banner instead of an error. Keep both
// interpreter-deferred messages carrying this exact suffix in sync.
return new WorkflowCompileError(
`node '${node.id}' branches into ${outs.length} edges — graphs with branches require the workflow interpreter (deferred)`,
);

View File

@@ -202,6 +202,14 @@
font-size: 0.85rem;
}
/* Info-tone banner (KTD-4): branching graph runs on the interpreter only — not a
* failure, so it uses the info token rather than the warning treatment. */
.wf-editor-banner--info {
border-bottom-color: var(--ws-info);
color: var(--ws-info);
background: color-mix(in srgb, var(--ws-info) 6%, var(--bg-secondary));
}
.wf-editor-canvas {
flex: 1;
min-height: 0;
@@ -418,6 +426,16 @@
stroke-width: 2;
}
/* Failure edges (R2): a distinct dash pattern from rework plus an error-token
* stroke. Two-channel rule — the condition label is always rendered (third
* channel is color) so failure edges stay distinguishable in low-contrast
* themes. */
.react-flow__edge.wf-edge-failure .react-flow__edge-path {
stroke: var(--ws-error);
stroke-dasharray: 2 4;
stroke-width: 2;
}
.wf-code-source {
font-family: var(--font-mono, monospace);
font-size: 0.72rem;

View File

@@ -7,7 +7,6 @@ import {
Background,
Controls,
MiniMap,
addEdge,
useNodesState,
useEdgesState,
type Connection,
@@ -51,6 +50,10 @@ import {
isColumnBandNode,
foreachChildFlowId,
shortConditionLabel,
edgeClassName,
edgeConditionEditability,
buildConnectionEdge,
WF_EDGE_INTERACTION_WIDTH,
FOREACH_GROUP_WIDTH,
FOREACH_GROUP_HEIGHT,
FOREACH_CHILD_X,
@@ -131,6 +134,10 @@ function InnerEditor({
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
// Info-tone state (KTD-4): set when a save compiles-rejects solely because the
// graph branches (interpreter-only), distinct from the warning-toned
// validationError used for genuine problems.
const [interpreterOnly, setInterpreterOnly] = useState<boolean>(false);
const [nodes, setNodes, onNodesChange] = useNodesState<FlowNode<WorkflowFlowNodeData>>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<FlowEdge>([]);
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
@@ -225,6 +232,7 @@ function InnerEditor({
setSelectedNodeId(null);
setSelectedEdgeId(null);
setValidationError(null);
setInterpreterOnly(false);
}, [activeWorkflow, setNodes, setEdges]);
// Server-reported node error (e.g. seam-in-branch) attributed to a node id.
@@ -240,13 +248,28 @@ function InnerEditor({
});
}, [columns, setNodes]);
// Append a new (success) edge directly rather than via React Flow's addEdge,
// which dedupes on source/target/handles and would block parallel
// success+failure edges between the same pair (KTD-3). buildConnectionEdge
// reimplements addEdge's sanity guards plus the author-time cycle guard (KTD-9).
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) =>
addEdge({ ...connection, label: "success", data: { condition: "success" } }, eds),
);
const result = buildConnectionEdge(connection, edges, nodes);
if ("error" in result) {
if (result.error === "cycle") {
addToast(
t(
"workflowNodes.cycleBlocked",
"That connection would create a cycle — only rework edges inside a for-each template may loop back",
),
"warning",
);
}
return;
}
setEdges((eds) => [...eds, result.edge]);
},
[setEdges],
[edges, nodes, setEdges, addToast, t],
);
// Dragging a step node into a column band sets node.column (position-based
@@ -372,7 +395,7 @@ function InnerEditor({
data: { ...(e.data ?? {}), condition, kind: rework ? "rework" : undefined },
type: rework ? "step" : undefined,
animated: rework,
className: rework ? "wf-edge-rework" : undefined,
className: edgeClassName(condition, rework),
};
}),
);
@@ -459,6 +482,7 @@ function InnerEditor({
setSaving(true);
setValidationError(null);
setInterpreterOnly(false);
setServerNodeError(null);
try {
const { ir, layout } = flowToIr(
@@ -475,9 +499,19 @@ function InnerEditor({
await compileWorkflow(updated.id, projectId);
addToast(t("workflows.saved", "Workflow saved"), "success");
} catch (compileErr) {
setValidationError(
getErrorMessage(compileErr) || t("workflows.savedNotCompilable", "Workflow saved but cannot be compiled"),
);
const compileMsg = getErrorMessage(compileErr) || "";
// KTD-4: branching graphs reject with this shared suffix from
// workflow-compiler.ts (both the fan-out and off-main-path messages).
// Such a graph still runs on the interpreter — present it as info, not a
// warning. NOTE: this string is coupled to the compiler's message; if
// that wording changes, update both sites (see compiler message site).
if (compileMsg.includes("require the workflow interpreter (deferred)")) {
setInterpreterOnly(true);
} else {
setValidationError(
compileMsg || t("workflows.savedNotCompilable", "Workflow saved but cannot be compiled"),
);
}
}
} catch (err) {
const message = getErrorMessage(err) || t("workflows.saveFailed", "Failed to save workflow");
@@ -530,12 +564,13 @@ function InnerEditor({
const selectedNode = nodes.find((n) => n.id === selectedNodeId) ?? null;
const selectedEdge = edges.find((e) => e.id === selectedEdgeId) ?? null;
// The edge inspector's verdict/rework controls apply only when the edge's
// source node is a step-review node (KTD-4).
const selectedEdgeSourceIsReview = useMemo(() => {
if (!selectedEdge) return false;
// The edge inspector renders different controls per source-node kind (KTD-2):
// step-review → verdict controls; prompt/script/gate/code/foreach →
// success/failure select; everything else → a read-only condition note.
const selectedEdgeEditability = useMemo(() => {
if (!selectedEdge) return "readonly" as const;
const src = nodes.find((n) => n.id === selectedEdge.source);
return src?.data.kind === "step-review";
return edgeConditionEditability(src?.data.kind);
}, [selectedEdge, nodes]);
// Artifacts the active workflow declares (KTD-12). The parse-steps inspector
@@ -711,6 +746,18 @@ function InnerEditor({
{validationError}
</div>
)}
{interpreterOnly && (
<div
className="wf-editor-banner wf-editor-banner--info"
role="status"
data-testid="wf-interpreter-only-banner"
>
{t(
"workflowNodes.interpreterOnly",
"This workflow branches, so it runs on the graph interpreter — it can't compile to the linear step engine, but it will still run.",
)}
</div>
)}
{unplaced.length > 0 && (
<div className="wf-editor-banner wf-editor-banner--warn" role="alert" data-testid="wf-unplaced-summary">
{t("workflowColumns.unplacedCount", "{{count}} nodes not placed in a column", {
@@ -741,6 +788,7 @@ function InnerEditor({
setSelectedNodeId(null);
setSelectedEdgeId(null);
}}
defaultEdgeOptions={{ interactionWidth: WF_EDGE_INTERACTION_WIDTH }}
fitView
>
<Background />
@@ -1350,7 +1398,7 @@ function InnerEditor({
<aside className="wf-editor-inspector" data-testid="wf-edge-inspector">
<h3>{t("workflowNodes.edgeInspector", "Edge")}</h3>
<fieldset className="wf-inspector-fields" disabled={isBuiltin}>
{selectedEdgeSourceIsReview ? (
{selectedEdgeEditability === "verdicts" ? (
<>
<label className="wf-field">
<span>{t("workflowNodes.edgeVerdict", "Review verdict")}</span>
@@ -1389,6 +1437,18 @@ function InnerEditor({
)}
</p>
</>
) : selectedEdgeEditability === "conditions" ? (
<label className="wf-field">
<span>{t("workflowNodes.edgeCondition", "Condition")}</span>
<select
data-testid="wf-edge-condition"
value={String(selectedEdge.data?.condition ?? "success")}
onChange={(e) => updateSelectedEdge({ condition: e.target.value })}
>
<option value="success">success</option>
<option value="failure">failure</option>
</select>
</label>
) : (
<p className="wf-inspector-note">
{t(

View File

@@ -652,3 +652,56 @@ describe("WorkflowNodeEditor — built-in stepwise selection render path", () =>
expect(approve?.kind).toBeUndefined();
});
});
// ── U2: edge-condition authoring (compile-banner split) ─────────────────────
describe("WorkflowNodeEditor — U2 interpreter-only banner", () => {
beforeEach(() => {
vi.mocked(fetchTraits).mockResolvedValue(TRAIT_CATALOG);
vi.mocked(fetchStepParsers).mockResolvedValue(["step-headings", "json-steps"]);
vi.mocked(updateWorkflow).mockImplementation(async (_id, updates) => ({
...v2Def(),
...(updates as object),
}));
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
async function saveActive() {
await screen.findByText("Save");
await waitFor(() => expect(screen.getAllByLabelText(/Column name/i).length).toBeGreaterThan(0));
fireEvent.click(screen.getByText("Save").closest("button")!);
await waitFor(() => expect(updateWorkflow).toHaveBeenCalled());
}
it("shows an info-tone status banner (not an error) when compile rejects with the interpreter-deferred suffix", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
vi.mocked(compileWorkflow).mockRejectedValue(
new Error(
"node 'step' branches into 2 edges — graphs with branches require the workflow interpreter (deferred)",
),
);
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
await saveActive();
const banner = await screen.findByTestId("wf-interpreter-only-banner");
expect(banner).toHaveAttribute("role", "status");
expect(banner.className).toMatch(/wf-editor-banner--info/);
// No alert-toned error banner.
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
it("keeps the warning error banner for other (non-interpreter) compile errors", async () => {
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
vi.mocked(compileWorkflow).mockRejectedValue(new Error("node 'step' has no outgoing edge"));
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
await saveActive();
const banner = await screen.findByRole("alert");
expect(banner).toHaveTextContent(/no outgoing edge/i);
expect(screen.queryByTestId("wf-interpreter-only-banner")).not.toBeInTheDocument();
});
});

View File

@@ -14,6 +14,10 @@ import {
foreachChildFlowId,
templateNodeIdFromChild,
shortConditionLabel,
edgeClassName,
edgeConditionEditability,
wouldCreateCycle,
buildConnectionEdge,
COLUMN_BAND_HEIGHT,
WF_CARD_WIDTH,
WF_CARD_MAX_WIDTH,
@@ -457,3 +461,143 @@ describe("card dimension constants (U1)", () => {
expect(FOREACH_CHILD_Y + WF_CARD_HEIGHT).toBeLessThanOrEqual(FOREACH_GROUP_HEIGHT);
});
});
describe("edge-condition authoring (U2)", () => {
it("round-trips a failure condition through flowToIr → irToFlow with class + label", () => {
const ir: WorkflowDefinition["ir"] = {
version: "v1",
name: "wf",
nodes: [
{ id: "start", kind: "start" },
{ id: "n1", kind: "prompt", config: { prompt: "do" } },
{ id: "ok", kind: "prompt", config: { prompt: "ok" } },
{ id: "bad", kind: "prompt", config: { prompt: "bad" } },
{ id: "end", kind: "end" },
],
edges: [
{ from: "start", to: "n1", condition: "success" },
{ from: "n1", to: "ok", condition: "success" },
{ from: "n1", to: "bad", condition: "failure" },
{ from: "ok", to: "end", condition: "success" },
{ from: "bad", to: "end", condition: "success" },
],
};
const { nodes, edges } = irToFlow(makeDef(ir));
const failFlow = edges.find((e) => e.source === "n1" && e.target === "bad")!;
expect(failFlow.data?.condition).toBe("failure");
expect(failFlow.label).toBe("failure");
expect(failFlow.className).toBe("wf-edge-failure");
const { ir: out } = flowToIr("wf", nodes, edges);
const irFail = out.edges.find((e) => e.from === "n1" && e.to === "bad");
expect(irFail?.condition).toBe("failure");
});
it("preserves parallel success+failure edges between the same pair through the round-trip", () => {
const flowNodes: FlowNode<WorkflowFlowNodeData>[] = [
{ id: "a", type: "prompt", position: { x: 0, y: 0 }, data: { kind: "prompt", label: "a" } },
{ id: "b", type: "prompt", position: { x: 100, y: 0 }, data: { kind: "prompt", label: "b" } },
];
const flowEdges = [
{ id: "e-1", source: "a", target: "b", data: { condition: "success" } },
{ id: "e-2", source: "a", target: "b", data: { condition: "failure" } },
];
const { ir } = flowToIr("wf", flowNodes, flowEdges);
expect(ir.edges).toHaveLength(2);
expect(ir.edges.map((e) => e.condition).sort()).toEqual(["failure", "success"]);
const { edges: reFlow } = irToFlow(makeDef(ir));
expect(reFlow).toHaveLength(2);
const ids = new Set(reFlow.map((e) => e.id));
expect(ids.size).toBe(2);
});
it("edgeClassName: failure → wf-edge-failure, rework precedence, success → undefined", () => {
expect(edgeClassName("failure", false)).toBe("wf-edge-failure");
expect(edgeClassName("success", false)).toBeUndefined();
expect(edgeClassName("failure", true)).toBe("wf-edge-rework");
});
it("edgeConditionEditability gates by source kind (KTD-2)", () => {
expect(edgeConditionEditability("step-review")).toBe("verdicts");
expect(edgeConditionEditability("prompt")).toBe("conditions");
expect(edgeConditionEditability("script")).toBe("conditions");
expect(edgeConditionEditability("gate")).toBe("conditions");
expect(edgeConditionEditability("code")).toBe("conditions");
expect(edgeConditionEditability("foreach")).toBe("conditions");
expect(edgeConditionEditability("split")).toBe("readonly");
expect(edgeConditionEditability("parse-steps")).toBe("readonly");
expect(edgeConditionEditability("start")).toBe("readonly");
expect(edgeConditionEditability(undefined)).toBe("readonly");
});
it("wouldCreateCycle detects back-edges and ignores forward + rework edges", () => {
const chain = [
{ id: "1", source: "a", target: "b", data: { condition: "success" } },
{ id: "2", source: "b", target: "c", data: { condition: "success" } },
];
// c → a closes the loop a→b→c→a.
expect(wouldCreateCycle(chain, "c", "a")).toBe(true);
// a → c is forward, no cycle.
expect(wouldCreateCycle(chain, "a", "c")).toBe(false);
// self-loop.
expect(wouldCreateCycle(chain, "a", "a")).toBe(true);
// rework edges are excluded from the reachability walk.
const withRework = [
{ id: "1", source: "a", target: "b", data: { condition: "success" } },
{ id: "2", source: "b", target: "c", data: { condition: "success", kind: "rework" } },
];
// c only reachable from b via a rework edge, so c→a is NOT a (non-rework) cycle.
expect(wouldCreateCycle(withRework, "c", "a")).toBe(false);
});
it("buildConnectionEdge: builds a success edge, rejects missing endpoints, duplicates, cycles", () => {
const nodes: FlowNode<WorkflowFlowNodeData>[] = [
{ id: "a", type: "prompt", position: { x: 0, y: 0 }, data: { kind: "prompt", label: "a" } },
{ id: "b", type: "prompt", position: { x: 100, y: 0 }, data: { kind: "prompt", label: "b" } },
{ id: "c", type: "prompt", position: { x: 200, y: 0 }, data: { kind: "prompt", label: "c" } },
];
const edges = [
{ id: "1", source: "a", target: "b", data: { condition: "success" } },
{ id: "2", source: "b", target: "c", data: { condition: "success" } },
];
// happy path: new success edge with a unique id + interactionWidth.
const ok = buildConnectionEdge({ source: "a", target: "c" }, edges, nodes);
expect("edge" in ok).toBe(true);
if ("edge" in ok) {
expect(ok.edge.data?.condition).toBe("success");
expect(ok.edge.label).toBe("success");
expect(ok.edge.interactionWidth).toBeGreaterThan(0);
expect(typeof ok.edge.id).toBe("string");
}
// missing endpoint.
expect(buildConnectionEdge({ source: "a", target: null }, edges, nodes)).toEqual({
error: "missing-endpoint",
});
// duplicate of the same condition.
expect(buildConnectionEdge({ source: "a", target: "b" }, edges, nodes)).toEqual({
error: "duplicate",
});
// cycle: c→a closes a→b→c→a.
expect(buildConnectionEdge({ source: "c", target: "a" }, edges, nodes)).toEqual({
error: "cycle",
});
});
it("buildConnectionEdge exempts intra-foreach-template connections from the cycle guard", () => {
const nodes: FlowNode<WorkflowFlowNodeData>[] = [
{ id: "g", type: "foreach", position: { x: 0, y: 0 }, data: { kind: "foreach", label: "g" } },
{ id: "g::a", type: "prompt", position: { x: 0, y: 0 }, parentId: "g", data: { kind: "prompt", label: "a" } },
{ id: "g::b", type: "prompt", position: { x: 0, y: 0 }, parentId: "g", data: { kind: "prompt", label: "b" } },
];
const edges = [{ id: "1", source: "g::a", target: "g::b", data: { condition: "success" } }];
// b→a would be a cycle, but both are children of the same group → allowed.
const res = buildConnectionEdge({ source: "g::b", target: "g::a" }, edges, nodes);
expect("edge" in res).toBe(true);
});
});

View File

@@ -152,8 +152,19 @@ function foreachConfigOf(node: WorkflowIrNode): WorkflowForeachConfig | undefine
return cfg as WorkflowForeachConfig;
}
/** CSS class for an edge given its condition + rework kind. Rework takes
* precedence; failure edges get the distinct failure styling; success and other
* conditions get no class (default styling). R2's two-channel rule (label always
* rendered + dash pattern) plus color is enforced by the CSS for these classes. */
export function edgeClassName(condition: string, isRework: boolean): string | undefined {
if (isRework) return "wf-edge-rework";
if (condition === "failure") return "wf-edge-failure";
return undefined;
}
/** Build a React Flow edge from an IR edge. Rework edges (KTD-5) carry kind so
* the editor renders them dashed in the accent color. */
* the editor renders them dashed in the accent color. Failure edges (R2) carry
* the wf-edge-failure class for a distinct dash + error-token stroke. */
function irEdgeToFlow(edge: WorkflowIrEdge, index: number, idScope = ""): FlowEdge {
const condition = edge.condition ?? "success";
const isRework = edge.kind === "rework";
@@ -165,11 +176,16 @@ function irEdgeToFlow(edge: WorkflowIrEdge, index: number, idScope = ""): FlowEd
data: { condition, kind: isRework ? "rework" : undefined },
type: isRework ? "step" : undefined,
animated: isRework,
className: isRework ? "wf-edge-rework" : undefined,
className: edgeClassName(condition, isRework),
interactionWidth: WF_EDGE_INTERACTION_WIDTH,
markerEnd: undefined,
};
}
/** Default edge hit-target width (px) so edges are clickable/tappable even when
* visually thin. Applied per-edge (defaultEdgeOptions only seeds new edges). */
export const WF_EDGE_INTERACTION_WIDTH = 24;
/** Short display label for an edge condition. `outcome:<verdict>` conditions
* render as the verdict alone (KTD-4); everything else verbatim. */
export function shortConditionLabel(condition: string): string {
@@ -399,6 +415,123 @@ function flowEdgeToIr(edge: FlowEdge, groupId?: string): WorkflowIrEdge {
return { from, to, condition, ...(isRework ? { kind: "rework" as const } : {}) };
}
// ── Edge-condition authoring (U2) ────────────────────────────────────────────
/** Editor node kinds whose edges expose a success/failure condition select
* (KTD-2). step-review uses verdict controls; all other kinds are read-only. */
const CONDITION_EDITABLE_KINDS = new Set<string>(["prompt", "script", "gate", "code", "foreach"]);
/** Decide what the edge inspector renders for an edge sourced from `sourceKind`:
* - "verdicts": step-review verdict select + rework checkbox (existing);
* - "conditions": success/failure native select (KTD-2);
* - "readonly": a read-only condition note. Pure so the gating is unit-testable
* without rendering edges (jsdom can't). */
export function edgeConditionEditability(
sourceKind: string | undefined,
): "verdicts" | "conditions" | "readonly" {
if (sourceKind === "step-review") return "verdicts";
if (sourceKind && CONDITION_EDITABLE_KINDS.has(sourceKind)) return "conditions";
return "readonly";
}
/** True when adding an edge source→target would create a cycle, i.e. `target`
* can already reach `source` by walking existing non-rework edges (rework edges
* are the only legal cycles and are excluded from the reachability walk, per
* KTD-9). Pure + exported so the connect-time guard is testable at the mapping
* layer. */
export function wouldCreateCycle(edges: FlowEdge[], source: string, target: string): boolean {
if (source === target) return true;
// Build adjacency over non-rework edges only.
const adj = new Map<string, string[]>();
for (const e of edges) {
if ((e.data?.kind as string | undefined) === "rework") continue;
const arr = adj.get(e.source) ?? [];
arr.push(e.target);
adj.set(e.source, arr);
}
// Can `target` reach `source`? If so, the new source→target edge closes a loop.
const seen = new Set<string>();
const stack = [target];
while (stack.length) {
const cur = stack.pop()!;
if (cur === source) return true;
if (seen.has(cur)) continue;
seen.add(cur);
for (const next of adj.get(cur) ?? []) stack.push(next);
}
return false;
}
let edgeSeq = 0;
/** Allocate a globally-unique edge id (mirrors the editor's newNodeId pattern).
* Used by buildConnectionEdge so parallel success+failure edges between the same
* pair don't collide (KTD-3). */
export function newEdgeId(): string {
edgeSeq += 1;
return `e-${Date.now().toString(36)}-${edgeSeq}`;
}
/** Result of attempting to build an edge from a React Flow connection. */
export type BuildConnectionResult =
| { edge: FlowEdge }
| { error: "missing-endpoint" | "duplicate" | "cycle" };
/** Construct a new success edge for a React Flow connection, reimplementing the
* sanity guards React Flow's addEdge provided (KTD-3) plus the author-time cycle
* guard (KTD-9). Returns an error tag instead of an edge when the connection is
* rejected so the caller can surface a toast:
* - missing-endpoint: source or target absent;
* - duplicate: an edge with the same source+target+condition already exists
* (parallel edges of a DIFFERENT condition between the same pair ARE allowed);
* - cycle: the connection would close a non-rework loop, and the endpoints are
* not both children of the same foreach template (intra-template rework
* cycles are authored separately and exempt).
*/
export function buildConnectionEdge(
connection: { source?: string | null; target?: string | null },
edges: FlowEdge[],
nodes: FlowNode<WorkflowFlowNodeData>[],
): BuildConnectionResult {
const source = connection.source ?? undefined;
const target = connection.target ?? undefined;
if (!source || !target) return { error: "missing-endpoint" };
const condition = "success";
// Skip exact duplicates of the SAME condition (a second identical edge is
// pointless); different conditions between the same pair are allowed.
const isDuplicate = edges.some(
(e) =>
e.source === source &&
e.target === target &&
((e.data?.condition as string | undefined) ?? "success") === condition,
);
if (isDuplicate) return { error: "duplicate" };
// Cycle guard (KTD-9). Exempt connections where both endpoints are children of
// the same foreach template — those may legitimately be rework cycles authored
// separately; the simplest correct rule applies the guard only to non-template
// connections.
const srcNode = nodes.find((n) => n.id === source);
const tgtNode = nodes.find((n) => n.id === target);
const bothTemplateChildren =
!!srcNode?.parentId && srcNode.parentId === tgtNode?.parentId;
if (!bothTemplateChildren && wouldCreateCycle(edges, source, target)) {
return { error: "cycle" };
}
return {
edge: {
id: newEdgeId(),
source,
target,
label: shortConditionLabel(condition),
data: { condition, kind: undefined },
className: edgeClassName(condition, false),
interactionWidth: WF_EDGE_INTERACTION_WIDTH,
},
};
}
// ── Client-side validation (U10) ─────────────────────────────────────────────
//
// The server's parseWorkflowIr (run on PATCH) is the authority for structural

View File

@@ -6112,6 +6112,12 @@
"switchToPlainText": "Switch to plain text",
"yes": "Yes"
},
"taskFields": {
"moreFields": "Additional fields",
"orphaned": "Orphaned fields",
"saveFailed": "Failed to save field",
"unset": "—"
},
"taskForm": {
"addDependencies": "Add dependencies",
"attachHint": "You can also paste images or drag & drop",
@@ -6734,8 +6740,8 @@
"optionColor": "Option color",
"optionLabel": "Option label",
"optionN": "Option {{n}}",
"optionValue": "Option value",
"options": "Options",
"optionValue": "Option value",
"placement": "Placement",
"placementCard": "Card badge",
"placementDetail": "Detail (inline)",
@@ -6754,6 +6760,8 @@
"codeNote": "Runs sandboxed TypeScript. Syntax is validated at save.",
"codeSource": "Source (TypeScript)",
"codeTimeout": "Timeout (ms)",
"cycleBlocked": "That connection would create a cycle — only rework edges inside a for-each template may loop back",
"edgeCondition": "Condition",
"edgeConditionLabel": "Condition: {{condition}}",
"edgeInspector": "Edge",
"edgeNoVerdict": "— success (no verdict) —",
@@ -6775,6 +6783,7 @@
"foreachWorktree": "Per-step worktree",
"gateBlocks": "Gate (blocks)",
"gateMode": "Gate mode",
"interpreterOnly": "This workflow branches, so it runs on the graph interpreter — it can't compile to the linear step engine, but it will still run.",
"joinAll": "All branches",
"joinAny": "Any branch",
"joinMode": "Join mode",
@@ -6795,14 +6804,7 @@
"reviewPlan": "Plan review",
"reviewType": "Review type",
"splitNote": "Branches run concurrently from this node. Execute and merge seams are not allowed inside a branch.",
"stepExecuteLabel": "Step execute",
"summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript",
"summaryGateAdvisory": "Advisory",
"summaryGateBlocks": "Gate (blocks)",
"summaryHoldRelease": "Release: {{release}}",
"summaryNotConfigured": "Not configured",
"summaryReviewType": "{{type}} review"
"stepExecuteLabel": "Step execute"
},
"workflows": {
"duplicateToCustomize": "Duplicate to customize",
@@ -6833,11 +6835,5 @@
"installRequestTitle": "Worktrunk install request",
"sha256": "SHA-256",
"version": "Version"
},
"taskFields": {
"unset": "—",
"moreFields": "Additional fields",
"orphaned": "Orphaned fields",
"saveFailed": "Failed to save field"
}
}

View File

@@ -1,9 +1,4 @@
{
"actions": {
"cancel": "Cancel",
"close": "Close",
"save": "Save"
},
"agents": {
"ratings": {
"trendDeclining": "↓ Declining",
@@ -34,7 +29,6 @@
"minutesAgo_other": "{{count}}m ago"
}
},
"archive": "Archive",
"board": {
"rejection": {
"capacityExhausted": "That column is at capacity. Try again when a slot frees up.",
@@ -44,7 +38,6 @@
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
}
},
"cancel": "Cancel",
"chat": {
"failedToGetResponse": "Failed to get response",
"failureReferenceId": "ID",
@@ -54,25 +47,15 @@
"openMailboxMessage": "Open mailbox message",
"toolCallArgsPrefix": "args",
"toolCallResultPrefix": "result",
"toolCallsCount_one": "{{count}} tool calls",
"toolCallsCount_other": "{{count}} tool calls",
"toolCallsHeader": "Tool calls",
"toolCallStatusCompleted": "completed",
"toolCallStatusError": "error",
"toolCallStatusErrors": "errors",
"toolCallStatusRunning": "running",
"toolCallsCount_one": "{{count}} tool calls",
"toolCallsCount_other": "{{count}} tool calls",
"toolCallsHeader": "Tool calls",
"viewFailureDetails": "View failure details"
},
"close": "Close",
"columns": {
"archived": "Archived",
"done": "Done",
"in-progress": "In Progress",
"in-review": "In Review",
"todo": "Todo",
"triage": "Planning"
},
"delete": "Delete",
"health": {
"anomaly": {
"duplicateActiveId": "Duplicate active task ID",
@@ -110,13 +93,6 @@
"modelSetToDefault": "{{label}} model set to default"
}
},
"nodeStatus": {
"connecting": "Connecting",
"error": "Error",
"offline": "Offline",
"online": "Online",
"unknown": "Unknown"
},
"nodes": {
"auth": {
"differ": "Auth credentials differ",
@@ -137,7 +113,13 @@
"stopped": "Stopped"
}
},
"refresh": "Refresh",
"nodeStatus": {
"connecting": "Connecting",
"error": "Error",
"offline": "Offline",
"online": "Online",
"unknown": "Unknown"
},
"research": {
"providerGitHub": "GitHub",
"providerLlmSynthesis": "LLM Synthesis",
@@ -145,7 +127,6 @@
"providerPageFetch": "Page Fetch",
"providerWebSearch": "Web Search"
},
"retry": "Retry",
"routing": {
"policyLabel": {
"block": "Block execution",
@@ -205,7 +186,6 @@
"zai": "GLM models by Zhipu AI — strong multilingual support"
}
},
"skip": "Skip",
"taskForm": {
"nodeStatusConnecting": "Connecting",
"nodeStatusError": "Error",
@@ -220,7 +200,6 @@
"refreshSourceInitialLoad": "Initial load",
"refreshSourceManual": "Manual"
},
"tryAgain": "Try Again",
"workflow": {
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
@@ -230,5 +209,14 @@
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"waitingForOutput": "Waiting for agent output…"
},
"workflowNodes": {
"summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript",
"summaryGateAdvisory": "Advisory",
"summaryGateBlocks": "Gate (blocks)",
"summaryHoldRelease": "Release: {{release}}",
"summaryNotConfigured": "Not configured",
"summaryReviewType": "{{type}} review"
}
}

View File

@@ -6112,6 +6112,12 @@
"switchToPlainText": "Cambiar a texto sin formato",
"yes": "Sí"
},
"taskFields": {
"moreFields": "Campos adicionales",
"orphaned": "Campos huérfanos",
"saveFailed": "No se pudo guardar el campo",
"unset": "—"
},
"taskForm": {
"addDependencies": "Agregar dependencias",
"attachHint": "También puedes pegar imágenes o arrastrar y soltar",
@@ -6715,11 +6721,47 @@
"unplacedCount_one": "",
"unplacedCount_other": ""
},
"workflowFields": {
"add": "Agregar campo",
"addOption": "Agregar opción",
"badge": "Mostrar como insignia",
"default": "Predeterminado",
"defaultLabel": "Valor predeterminado",
"defaultTrue": "Activado por defecto",
"duplicateId": "Ya existe un campo con ese id",
"editId": "Editar id",
"empty": "Aún no hay campos personalizados. Agrega un campo para ampliar el formulario y las tarjetas de la tarea.",
"idLabel": "Id del campo",
"idWarn": "Cambiar el id descarta los valores almacenados con el id anterior (quitar + agregar).",
"nameLabel": "Nombre del campo",
"newFieldName": "Nuevo campo",
"newOptionLabel": "Opción 1",
"noDefault": "— ninguno —",
"optionColor": "Color de la opción",
"optionLabel": "Etiqueta de la opción",
"optionN": "Opción {{n}}",
"options": "Opciones",
"optionValue": "Valor de la opción",
"placement": "Ubicación",
"placementCard": "Insignia de tarjeta",
"placementDetail": "Detalle (en línea)",
"placementSection": "Sección de detalle",
"readOnlyHint": "Los flujos de trabajo integrados son de solo lectura: duplica para editar",
"remove": "Quitar campo",
"removeOption": "Quitar opción",
"required": "Obligatorio",
"title": "Campos",
"typeLabel": "Tipo",
"widget": "Control",
"widgetDefault": "Predeterminado"
},
"workflowNodes": {
"advisory": "",
"codeNote": "Ejecuta TypeScript en un entorno aislado. La sintaxis se valida al guardar.",
"codeSource": "Origen (TypeScript)",
"codeTimeout": "Tiempo de espera (ms)",
"cycleBlocked": "",
"edgeCondition": "",
"edgeConditionLabel": "Condición: {{condition}}",
"edgeInspector": "Conexión",
"edgeNoVerdict": "— éxito (sin veredicto) —",
@@ -6741,6 +6783,7 @@
"foreachWorktree": "Árbol de trabajo por paso",
"gateBlocks": "Compuerta (bloquea)",
"gateMode": "Modo de compuerta",
"interpreterOnly": "",
"joinAll": "Todas las ramas",
"joinAny": "Cualquier rama",
"joinMode": "Modo de unión",
@@ -6761,14 +6804,7 @@
"reviewPlan": "Plan review",
"reviewType": "Review type",
"splitNote": "Las ramas se ejecutan de forma concurrente desde este nodo. No se permiten uniones de ejecución y fusión dentro de una rama.",
"stepExecuteLabel": "Step execute",
"summaryAwaitInput": "",
"summaryCodeDefault": "",
"summaryGateAdvisory": "",
"summaryGateBlocks": "",
"summaryHoldRelease": "",
"summaryNotConfigured": "",
"summaryReviewType": ""
"stepExecuteLabel": "Step execute"
},
"workflows": {
"duplicateToCustomize": "",
@@ -6799,45 +6835,5 @@
"installRequestTitle": "Solicitud de instalación de Worktrunk",
"sha256": "SHA-256",
"version": "Versión"
},
"taskFields": {
"unset": "—",
"moreFields": "Campos adicionales",
"orphaned": "Campos huérfanos",
"saveFailed": "No se pudo guardar el campo"
},
"workflowFields": {
"add": "Agregar campo",
"addOption": "Agregar opción",
"badge": "Mostrar como insignia",
"default": "Predeterminado",
"defaultLabel": "Valor predeterminado",
"defaultTrue": "Activado por defecto",
"duplicateId": "Ya existe un campo con ese id",
"editId": "Editar id",
"empty": "Aún no hay campos personalizados. Agrega un campo para ampliar el formulario y las tarjetas de la tarea.",
"idLabel": "Id del campo",
"idWarn": "Cambiar el id descarta los valores almacenados con el id anterior (quitar + agregar).",
"nameLabel": "Nombre del campo",
"newFieldName": "Nuevo campo",
"newOptionLabel": "Opción 1",
"noDefault": "— ninguno —",
"optionColor": "Color de la opción",
"optionLabel": "Etiqueta de la opción",
"optionN": "Opción {{n}}",
"optionValue": "Valor de la opción",
"options": "Opciones",
"placement": "Ubicación",
"placementCard": "Insignia de tarjeta",
"placementDetail": "Detalle (en línea)",
"placementSection": "Sección de detalle",
"readOnlyHint": "Los flujos de trabajo integrados son de solo lectura: duplica para editar",
"remove": "Quitar campo",
"removeOption": "Quitar opción",
"required": "Obligatorio",
"title": "Campos",
"typeLabel": "Tipo",
"widget": "Control",
"widgetDefault": "Predeterminado"
}
}

View File

@@ -1,9 +1,4 @@
{
"actions": {
"cancel": "Cancelar",
"close": "Cerrar",
"save": "Guardar"
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -34,7 +29,6 @@
"minutesAgo_other": ""
}
},
"archive": "Archivar",
"board": {
"rejection": {
"capacityExhausted": "",
@@ -44,7 +38,6 @@
"workflowMismatch": ""
}
},
"cancel": "Cancelar",
"chat": {
"failedToGetResponse": "",
"failureReferenceId": "",
@@ -54,25 +47,15 @@
"openMailboxMessage": "",
"toolCallArgsPrefix": "",
"toolCallResultPrefix": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"toolCallStatusCompleted": "",
"toolCallStatusError": "",
"toolCallStatusErrors": "",
"toolCallStatusRunning": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"viewFailureDetails": ""
},
"close": "Cerrar",
"columns": {
"archived": "Archivado",
"done": "Hecho",
"in-progress": "En progreso",
"in-review": "En revisión",
"todo": "Por hacer",
"triage": "Planificación"
},
"delete": "Eliminar",
"health": {
"anomaly": {
"duplicateActiveId": "",
@@ -110,13 +93,6 @@
"modelSetToDefault": ""
}
},
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"nodes": {
"auth": {
"differ": "",
@@ -137,7 +113,13 @@
"stopped": ""
}
},
"refresh": "Actualizar",
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"research": {
"providerGitHub": "",
"providerLlmSynthesis": "",
@@ -145,7 +127,6 @@
"providerPageFetch": "",
"providerWebSearch": ""
},
"retry": "Reintentar",
"routing": {
"policyLabel": {
"block": "",
@@ -205,7 +186,6 @@
"zai": ""
}
},
"skip": "Omitir",
"taskForm": {
"nodeStatusConnecting": "",
"nodeStatusError": "",
@@ -220,7 +200,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"tryAgain": "Reintentar",
"workflow": {
"postMerge": "",
"preMerge": "",
@@ -230,5 +209,14 @@
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",
"summaryGateAdvisory": "",
"summaryGateBlocks": "",
"summaryHoldRelease": "",
"summaryNotConfigured": "",
"summaryReviewType": ""
}
}

View File

@@ -6112,6 +6112,12 @@
"switchToPlainText": "Basculer vers du texte brut",
"yes": "Oui"
},
"taskFields": {
"moreFields": "Champs supplémentaires",
"orphaned": "Champs orphelins",
"saveFailed": "Échec de l'enregistrement du champ",
"unset": "—"
},
"taskForm": {
"addDependencies": "Ajouter des dépendances",
"attachHint": "Vous pouvez aussi coller des images ou les glisser-déposer",
@@ -6715,11 +6721,47 @@
"unplacedCount_one": "",
"unplacedCount_other": ""
},
"workflowFields": {
"add": "Ajouter un champ",
"addOption": "Ajouter une option",
"badge": "Afficher comme badge",
"default": "Par défaut",
"defaultLabel": "Valeur par défaut",
"defaultTrue": "Activé par défaut",
"duplicateId": "Un champ avec cet id existe déjà",
"editId": "Modifier l'id",
"empty": "Aucun champ personnalisé pour l'instant. Ajoutez un champ pour étendre le formulaire et les cartes de la tâche.",
"idLabel": "Id du champ",
"idWarn": "Modifier l'id supprime les valeurs stockées sous l'ancien id (retirer + ajouter).",
"nameLabel": "Nom du champ",
"newFieldName": "Nouveau champ",
"newOptionLabel": "Option 1",
"noDefault": "— aucun —",
"optionColor": "Couleur de l'option",
"optionLabel": "Libellé de l'option",
"optionN": "Option {{n}}",
"options": "Options",
"optionValue": "Valeur de l'option",
"placement": "Emplacement",
"placementCard": "Badge de carte",
"placementDetail": "Détail (en ligne)",
"placementSection": "Section de détail",
"readOnlyHint": "Les flux de travail intégrés sont en lecture seule — dupliquez pour modifier",
"remove": "Retirer le champ",
"removeOption": "Retirer l'option",
"required": "Obligatoire",
"title": "Champs",
"typeLabel": "Type",
"widget": "Composant",
"widgetDefault": "Par défaut"
},
"workflowNodes": {
"advisory": "",
"codeNote": "Exécute du TypeScript en bac à sable. La syntaxe est validée à l’enregistrement.",
"codeSource": "Source (TypeScript)",
"codeTimeout": "Délai d’expiration (ms)",
"cycleBlocked": "",
"edgeCondition": "",
"edgeConditionLabel": "Condition : {{condition}}",
"edgeInspector": "Lien",
"edgeNoVerdict": "— succès (aucun verdict) —",
@@ -6741,6 +6783,7 @@
"foreachWorktree": "Arbre de travail par étape",
"gateBlocks": "Barrière (bloque)",
"gateMode": "Mode de barrière",
"interpreterOnly": "",
"joinAll": "Toutes les branches",
"joinAny": "N’importe quelle branche",
"joinMode": "Mode de jointure",
@@ -6761,14 +6804,7 @@
"reviewPlan": "Plan review",
"reviewType": "Review type",
"splitNote": "Les branches s’exécutent simultanément depuis ce nœud. Les jointures d’exécution et de fusion ne sont pas autorisées dans une branche.",
"stepExecuteLabel": "Step execute",
"summaryAwaitInput": "",
"summaryCodeDefault": "",
"summaryGateAdvisory": "",
"summaryGateBlocks": "",
"summaryHoldRelease": "",
"summaryNotConfigured": "",
"summaryReviewType": ""
"stepExecuteLabel": "Step execute"
},
"workflows": {
"duplicateToCustomize": "",
@@ -6799,45 +6835,5 @@
"installRequestTitle": "Demande d'installation de Worktrunk",
"sha256": "SHA-256",
"version": "Version"
},
"taskFields": {
"unset": "—",
"moreFields": "Champs supplémentaires",
"orphaned": "Champs orphelins",
"saveFailed": "Échec de l'enregistrement du champ"
},
"workflowFields": {
"add": "Ajouter un champ",
"addOption": "Ajouter une option",
"badge": "Afficher comme badge",
"default": "Par défaut",
"defaultLabel": "Valeur par défaut",
"defaultTrue": "Activé par défaut",
"duplicateId": "Un champ avec cet id existe déjà",
"editId": "Modifier l'id",
"empty": "Aucun champ personnalisé pour l'instant. Ajoutez un champ pour étendre le formulaire et les cartes de la tâche.",
"idLabel": "Id du champ",
"idWarn": "Modifier l'id supprime les valeurs stockées sous l'ancien id (retirer + ajouter).",
"nameLabel": "Nom du champ",
"newFieldName": "Nouveau champ",
"newOptionLabel": "Option 1",
"noDefault": "— aucun —",
"optionColor": "Couleur de l'option",
"optionLabel": "Libellé de l'option",
"optionN": "Option {{n}}",
"optionValue": "Valeur de l'option",
"options": "Options",
"placement": "Emplacement",
"placementCard": "Badge de carte",
"placementDetail": "Détail (en ligne)",
"placementSection": "Section de détail",
"readOnlyHint": "Les flux de travail intégrés sont en lecture seule — dupliquez pour modifier",
"remove": "Retirer le champ",
"removeOption": "Retirer l'option",
"required": "Obligatoire",
"title": "Champs",
"typeLabel": "Type",
"widget": "Composant",
"widgetDefault": "Par défaut"
}
}

View File

@@ -1,9 +1,4 @@
{
"actions": {
"cancel": "Annuler",
"close": "Fermer",
"save": "Enregistrer"
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -34,7 +29,6 @@
"minutesAgo_other": ""
}
},
"archive": "Archiver",
"board": {
"rejection": {
"capacityExhausted": "",
@@ -44,7 +38,6 @@
"workflowMismatch": ""
}
},
"cancel": "Annuler",
"chat": {
"failedToGetResponse": "",
"failureReferenceId": "",
@@ -54,25 +47,15 @@
"openMailboxMessage": "",
"toolCallArgsPrefix": "",
"toolCallResultPrefix": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"toolCallStatusCompleted": "",
"toolCallStatusError": "",
"toolCallStatusErrors": "",
"toolCallStatusRunning": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"viewFailureDetails": ""
},
"close": "Fermer",
"columns": {
"archived": "Archivé",
"done": "Terminé",
"in-progress": "En cours",
"in-review": "En revue",
"todo": "À faire",
"triage": "Planification"
},
"delete": "Supprimer",
"health": {
"anomaly": {
"duplicateActiveId": "",
@@ -110,13 +93,6 @@
"modelSetToDefault": ""
}
},
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"nodes": {
"auth": {
"differ": "",
@@ -137,7 +113,13 @@
"stopped": ""
}
},
"refresh": "Actualiser",
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"research": {
"providerGitHub": "",
"providerLlmSynthesis": "",
@@ -145,7 +127,6 @@
"providerPageFetch": "",
"providerWebSearch": ""
},
"retry": "Réessayer",
"routing": {
"policyLabel": {
"block": "",
@@ -205,7 +186,6 @@
"zai": ""
}
},
"skip": "Ignorer",
"taskForm": {
"nodeStatusConnecting": "",
"nodeStatusError": "",
@@ -220,7 +200,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"tryAgain": "Réessayer",
"workflow": {
"postMerge": "",
"preMerge": "",
@@ -230,5 +209,14 @@
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",
"summaryGateAdvisory": "",
"summaryGateBlocks": "",
"summaryHoldRelease": "",
"summaryNotConfigured": "",
"summaryReviewType": ""
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"agentRunLogsBackHint": "[Esc/q] 실행 목록으로 돌아가기",
"agentRunLogsTitle": "실행 로그 ({{index}})",
"agentsFooterHints": "[s] 시작 [x] 중지 [D] 삭제 [r] 새로고침 [Tab] 포커스 ↑↓ 선택",
"agentsListTitle_one": "",
"agentsListTitle_other": "",
"agentsNoAgents": "에이전트가 없습니다.",
"agentStarted": "에이전트 시작됨",
@@ -44,6 +45,7 @@
"filesEmpty": "(비어 있음)",
"filesEmptyFile": "(빈 파일)",
"filesFooterHints": "[Tab] 창 전환 [↑↓/jk] 이동 [Enter] 열기 [←/→] 접기/펼치기 [.] 숨김 [w] 줄 바꿈 [p] 프로젝트 [r] 새로고침",
"filesMoreLines_one": "",
"filesMoreLines_other": "",
"filesSelectProject": "프로젝트 선택",
"filesSelectToPreview": "미리볼 파일을 선택하세요",
@@ -151,6 +153,7 @@
"settingsFooterHints": "[Tab] 패널 전환 ↑↓ 설정 선택 [Space] 불리언 토글 [+/-] 숫자 조정 [←/→] 열거형 순환 [C/V/X/P/L/U/K/R] 원격 작업",
"settingsInteractivePanelTitle": "설정",
"settingsLoadingSettings": "설정 불러오는 중…",
"settingsMoreModels_one": "",
"settingsMoreModels_other": "",
"settingsPanelTitle": "설정",
"settingsPersistentTokenRegenerated": "영구 토큰이 재생성됨",
@@ -218,9 +221,6 @@
"utilitiesKillVitest": "Vitest 프로세스 종료",
"utilitiesPanelTitle": "유틸리티",
"utilitiesRefreshStats": "통계 새로고침",
"utilitiesToggleEnginePause": "엔진 일시 정지 전환",
"agentsListTitle_one": "",
"filesMoreLines_one": "",
"settingsMoreModels_one": ""
"utilitiesToggleEnginePause": "엔진 일시 정지 전환"
}
}

View File

@@ -1,9 +1,4 @@
{
"actions": {
"cancel": "취소",
"close": "닫기",
"save": "저장"
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -34,7 +29,6 @@
"minutesAgo_other": ""
}
},
"archive": "보관",
"board": {
"rejection": {
"capacityExhausted": "",
@@ -44,7 +38,6 @@
"workflowMismatch": ""
}
},
"cancel": "취소",
"chat": {
"failedToGetResponse": "",
"failureReferenceId": "",
@@ -54,25 +47,15 @@
"openMailboxMessage": "",
"toolCallArgsPrefix": "",
"toolCallResultPrefix": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"toolCallStatusCompleted": "",
"toolCallStatusError": "",
"toolCallStatusErrors": "",
"toolCallStatusRunning": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"viewFailureDetails": ""
},
"close": "닫기",
"columns": {
"archived": "보관됨",
"done": "완료",
"in-progress": "진행 중",
"in-review": "검토 중",
"todo": "할 일",
"triage": "계획"
},
"delete": "삭제",
"health": {
"anomaly": {
"duplicateActiveId": "",
@@ -110,13 +93,6 @@
"modelSetToDefault": ""
}
},
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"nodes": {
"auth": {
"differ": "",
@@ -137,7 +113,13 @@
"stopped": ""
}
},
"refresh": "새로고침",
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"research": {
"providerGitHub": "",
"providerLlmSynthesis": "",
@@ -145,7 +127,6 @@
"providerPageFetch": "",
"providerWebSearch": ""
},
"retry": "재시도",
"routing": {
"policyLabel": {
"block": "",
@@ -205,7 +186,6 @@
"zai": ""
}
},
"skip": "건너뛰기",
"taskForm": {
"nodeStatusConnecting": "",
"nodeStatusError": "",
@@ -220,7 +200,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"tryAgain": "다시 시도",
"workflow": {
"postMerge": "",
"preMerge": "",
@@ -230,5 +209,14 @@
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",
"summaryGateAdvisory": "",
"summaryGateBlocks": "",
"summaryHoldRelease": "",
"summaryNotConfigured": "",
"summaryReviewType": ""
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"agentRunLogsBackHint": "[Esc/q] 返回运行列表",
"agentRunLogsTitle": "运行日志({{index}})",
"agentsFooterHints": "[s] 启动 [x] 停止 [D] 删除 [r] 刷新 [Tab] 焦点 ↑↓ 选择",
"agentsListTitle_one": "",
"agentsListTitle_other": "",
"agentsNoAgents": "未找到代理。",
"agentStarted": "代理已启动",
@@ -44,6 +45,7 @@
"filesEmpty": "(空)",
"filesEmptyFile": "(空文件)",
"filesFooterHints": "[Tab] 切换面板 [↑↓/jk] 移动 [Enter] 打开 [←/→] 折叠/展开 [.] 隐藏文件 [w] 换行 [p] 项目 [r] 重载",
"filesMoreLines_one": "",
"filesMoreLines_other": "",
"filesSelectProject": "选择项目",
"filesSelectToPreview": "选择文件以预览",
@@ -151,6 +153,7 @@
"settingsFooterHints": "[Tab] 切换面板 ↑↓ 选择设置 [Space] 切换布尔 [+/-] 调整数值 [←/→] 循环枚举 [C/V/X/P/L/U/K/R] 远程操作",
"settingsInteractivePanelTitle": "设置",
"settingsLoadingSettings": "正在加载设置…",
"settingsMoreModels_one": "",
"settingsMoreModels_other": "",
"settingsPanelTitle": "设置",
"settingsPersistentTokenRegenerated": "持久令牌已重新生成",
@@ -218,9 +221,6 @@
"utilitiesKillVitest": "终止 Vitest 进程",
"utilitiesPanelTitle": "工具",
"utilitiesRefreshStats": "刷新统计",
"utilitiesToggleEnginePause": "切换引擎暂停",
"agentsListTitle_one": "",
"filesMoreLines_one": "",
"settingsMoreModels_one": ""
"utilitiesToggleEnginePause": "切换引擎暂停"
}
}

View File

@@ -1,9 +1,4 @@
{
"actions": {
"cancel": "取消",
"close": "关闭",
"save": "保存"
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -34,7 +29,6 @@
"minutesAgo_other": ""
}
},
"archive": "归档",
"board": {
"rejection": {
"capacityExhausted": "",
@@ -44,7 +38,6 @@
"workflowMismatch": ""
}
},
"cancel": "取消",
"chat": {
"failedToGetResponse": "",
"failureReferenceId": "",
@@ -54,25 +47,15 @@
"openMailboxMessage": "",
"toolCallArgsPrefix": "",
"toolCallResultPrefix": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"toolCallStatusCompleted": "",
"toolCallStatusError": "",
"toolCallStatusErrors": "",
"toolCallStatusRunning": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"viewFailureDetails": ""
},
"close": "关闭",
"columns": {
"archived": "已归档",
"done": "已完成",
"in-progress": "进行中",
"in-review": "审核中",
"todo": "待办",
"triage": "规划"
},
"delete": "删除",
"health": {
"anomaly": {
"duplicateActiveId": "",
@@ -110,13 +93,6 @@
"modelSetToDefault": ""
}
},
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"nodes": {
"auth": {
"differ": "",
@@ -137,7 +113,13 @@
"stopped": ""
}
},
"refresh": "刷新",
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"research": {
"providerGitHub": "",
"providerLlmSynthesis": "",
@@ -145,7 +127,6 @@
"providerPageFetch": "",
"providerWebSearch": ""
},
"retry": "重试",
"routing": {
"policyLabel": {
"block": "",
@@ -205,7 +186,6 @@
"zai": ""
}
},
"skip": "跳过",
"taskForm": {
"nodeStatusConnecting": "",
"nodeStatusError": "",
@@ -220,7 +200,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"tryAgain": "重试",
"workflow": {
"postMerge": "",
"preMerge": "",
@@ -230,5 +209,14 @@
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",
"summaryGateAdvisory": "",
"summaryGateBlocks": "",
"summaryHoldRelease": "",
"summaryNotConfigured": "",
"summaryReviewType": ""
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"agentRunLogsBackHint": "[Esc/q] 返回執行列表",
"agentRunLogsTitle": "執行日誌({{index}})",
"agentsFooterHints": "[s] 啟動 [x] 停止 [D] 刪除 [r] 重新整理 [Tab] 焦點 ↑↓ 選擇",
"agentsListTitle_one": "",
"agentsListTitle_other": "",
"agentsNoAgents": "找不到代理。",
"agentStarted": "代理已啟動",
@@ -44,6 +45,7 @@
"filesEmpty": "(空)",
"filesEmptyFile": "(空檔案)",
"filesFooterHints": "[Tab] 切換面板 [↑↓/jk] 移動 [Enter] 開啟 [←/→] 折疊/展開 [.] 隱藏檔案 [w] 換行 [p] 專案 [r] 重新載入",
"filesMoreLines_one": "",
"filesMoreLines_other": "",
"filesSelectProject": "選擇專案",
"filesSelectToPreview": "選擇檔案以預覽",
@@ -151,6 +153,7 @@
"settingsFooterHints": "[Tab] 切換面板 ↑↓ 選擇設定 [Space] 切換布林 [+/-] 調整數值 [←/→] 循環枚舉 [C/V/X/P/L/U/K/R] 遠端操作",
"settingsInteractivePanelTitle": "設定",
"settingsLoadingSettings": "正在載入設定…",
"settingsMoreModels_one": "",
"settingsMoreModels_other": "",
"settingsPanelTitle": "設定",
"settingsPersistentTokenRegenerated": "持久金鑰已重新產生",
@@ -218,9 +221,6 @@
"utilitiesKillVitest": "終止 Vitest 行程",
"utilitiesPanelTitle": "工具",
"utilitiesRefreshStats": "重新整理統計",
"utilitiesToggleEnginePause": "切換引擎暫停",
"agentsListTitle_one": "",
"filesMoreLines_one": "",
"settingsMoreModels_one": ""
"utilitiesToggleEnginePause": "切換引擎暫停"
}
}

View File

@@ -1,9 +1,4 @@
{
"actions": {
"cancel": "取消",
"close": "關閉",
"save": "儲存"
},
"agents": {
"ratings": {
"trendDeclining": "",
@@ -34,7 +29,6 @@
"minutesAgo_other": ""
}
},
"archive": "封存",
"board": {
"rejection": {
"capacityExhausted": "",
@@ -44,7 +38,6 @@
"workflowMismatch": ""
}
},
"cancel": "取消",
"chat": {
"failedToGetResponse": "",
"failureReferenceId": "",
@@ -54,25 +47,15 @@
"openMailboxMessage": "",
"toolCallArgsPrefix": "",
"toolCallResultPrefix": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"toolCallStatusCompleted": "",
"toolCallStatusError": "",
"toolCallStatusErrors": "",
"toolCallStatusRunning": "",
"toolCallsCount_one": "",
"toolCallsCount_other": "",
"toolCallsHeader": "",
"viewFailureDetails": ""
},
"close": "關閉",
"columns": {
"archived": "已封存",
"done": "已完成",
"in-progress": "進行中",
"in-review": "審查中",
"todo": "待辦",
"triage": "規劃"
},
"delete": "刪除",
"health": {
"anomaly": {
"duplicateActiveId": "",
@@ -110,13 +93,6 @@
"modelSetToDefault": ""
}
},
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"nodes": {
"auth": {
"differ": "",
@@ -137,7 +113,13 @@
"stopped": ""
}
},
"refresh": "重新整理",
"nodeStatus": {
"connecting": "",
"error": "",
"offline": "",
"online": "",
"unknown": ""
},
"research": {
"providerGitHub": "",
"providerLlmSynthesis": "",
@@ -145,7 +127,6 @@
"providerPageFetch": "",
"providerWebSearch": ""
},
"retry": "重試",
"routing": {
"policyLabel": {
"block": "",
@@ -205,7 +186,6 @@
"zai": ""
}
},
"skip": "略過",
"taskForm": {
"nodeStatusConnecting": "",
"nodeStatusError": "",
@@ -220,7 +200,6 @@
"refreshSourceInitialLoad": "",
"refreshSourceManual": ""
},
"tryAgain": "重試",
"workflow": {
"postMerge": "",
"preMerge": "",
@@ -230,5 +209,14 @@
"statusRunning": "",
"statusSkipped": "",
"waitingForOutput": ""
},
"workflowNodes": {
"summaryAwaitInput": "",
"summaryCodeDefault": "",
"summaryGateAdvisory": "",
"summaryGateBlocks": "",
"summaryHoldRelease": "",
"summaryNotConfigured": "",
"summaryReviewType": ""
}
}

View File

@@ -6762,6 +6762,8 @@ export default interface Resources {
"codeNote": "Runs sandboxed TypeScript. Syntax is validated at save.",
"codeSource": "Source (TypeScript)",
"codeTimeout": "Timeout (ms)",
"cycleBlocked": "That connection would create a cycle — only rework edges inside a for-each template may loop back",
"edgeCondition": "Condition",
"edgeConditionLabel": "Condition: {{condition}}",
"edgeInspector": "Edge",
"edgeNoVerdict": "— success (no verdict) —",
@@ -6783,6 +6785,7 @@ export default interface Resources {
"foreachWorktree": "Per-step worktree",
"gateBlocks": "Gate (blocks)",
"gateMode": "Gate mode",
"interpreterOnly": "This workflow branches, so it runs on the graph interpreter — it can't compile to the linear step engine, but it will still run.",
"joinAll": "All branches",
"joinAny": "Any branch",
"joinMode": "Join mode",
@@ -6803,14 +6806,7 @@ export default interface Resources {
"reviewPlan": "Plan review",
"reviewType": "Review type",
"splitNote": "Branches run concurrently from this node. Execute and merge seams are not allowed inside a branch.",
"stepExecuteLabel": "Step execute",
"summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript",
"summaryGateAdvisory": "Advisory",
"summaryGateBlocks": "Gate (blocks)",
"summaryHoldRelease": "Release: {{release}}",
"summaryNotConfigured": "Not configured",
"summaryReviewType": "{{type}} review"
"stepExecuteLabel": "Step execute"
},
"workflowSelector": {
"switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?",
@@ -7070,11 +7066,6 @@ export default interface Resources {
}
},
"common": {
"actions": {
"cancel": "Cancel",
"close": "Close",
"save": "Save"
},
"agents": {
"ratings": {
"trendDeclining": "↓ Declining",
@@ -7105,7 +7096,6 @@ export default interface Resources {
"minutesAgo_other": "{{count}}m ago"
}
},
"archive": "Archive",
"board": {
"rejection": {
"capacityExhausted": "That column is at capacity. Try again when a slot frees up.",
@@ -7115,7 +7105,6 @@ export default interface Resources {
"workflowMismatch": "Drag can't move a card between workflows. Use the workflow switcher instead."
}
},
"cancel": "Cancel",
"chat": {
"failedToGetResponse": "Failed to get response",
"failureReferenceId": "ID",
@@ -7134,16 +7123,6 @@ export default interface Resources {
"toolCallsHeader": "Tool calls",
"viewFailureDetails": "View failure details"
},
"close": "Close",
"columns": {
"archived": "Archived",
"done": "Done",
"in-progress": "In Progress",
"in-review": "In Review",
"todo": "Todo",
"triage": "Planning"
},
"delete": "Delete",
"health": {
"anomaly": {
"duplicateActiveId": "Duplicate active task ID",
@@ -7208,7 +7187,6 @@ export default interface Resources {
"stopped": "Stopped"
}
},
"refresh": "Refresh",
"research": {
"providerGitHub": "GitHub",
"providerLlmSynthesis": "LLM Synthesis",
@@ -7216,7 +7194,6 @@ export default interface Resources {
"providerPageFetch": "Page Fetch",
"providerWebSearch": "Web Search"
},
"retry": "Retry",
"routing": {
"policyLabel": {
"block": "Block execution",
@@ -7276,7 +7253,6 @@ export default interface Resources {
"zai": "GLM models by Zhipu AI — strong multilingual support"
}
},
"skip": "Skip",
"taskForm": {
"nodeStatusConnecting": "Connecting",
"nodeStatusError": "Error",
@@ -7291,7 +7267,6 @@ export default interface Resources {
"refreshSourceInitialLoad": "Initial load",
"refreshSourceManual": "Manual"
},
"tryAgain": "Try Again",
"workflow": {
"postMerge": "Post-merge",
"preMerge": "Pre-merge",
@@ -7301,6 +7276,15 @@ export default interface Resources {
"statusRunning": "Running…",
"statusSkipped": "Skipped",
"waitingForOutput": "Waiting for agent output…"
},
"workflowNodes": {
"summaryAwaitInput": "Waits for user input",
"summaryCodeDefault": "TypeScript",
"summaryGateAdvisory": "Advisory",
"summaryGateBlocks": "Gate (blocks)",
"summaryHoldRelease": "Release: {{release}}",
"summaryNotConfigured": "Not configured",
"summaryReviewType": "{{type}} review"
}
},
"errors": {