From cd15a15744de580e11e8de40bb8942f0154ec65c Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:11:04 -0300 Subject: [PATCH] feat(canvas): Translate run canvas UI to English Translate the ASCII pane and web canvas copy, including statuses, history, detail actions, queue messages, and SSE connection text. Keep active counts focused on current workers and bump the package to 0.5.3.\n\nCo-Authored-By: Codex --- package-lock.json | 4 +- package.json | 2 +- plugin/mods/agents/pane.ts | 125 +++++++++-------- plugin/statusline.sh | 1 - scripts/pane-probe.sh | 2 +- src/cli/commands/web.ts | 2 +- src/web/canvas-page.ts | 164 +++++++++++----------- tests/mods-agents/pane.test.ts | 241 ++++++++++++++++++--------------- tests/statusline.test.ts | 10 +- tests/web-command.test.ts | 27 +++- 10 files changed, 315 insertions(+), 263 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70c666e..4222d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codedeck", - "version": "0.5.2", + "version": "0.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codedeck", - "version": "0.5.2", + "version": "0.5.3", "license": "MIT", "dependencies": { "commander": "^12.1.0", diff --git a/package.json b/package.json index f6c28db..dcb7acf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codedeck", - "version": "0.5.2", + "version": "0.5.3", "description": "CodeDeck, a local runtime for coding agents with session management, process supervision, event normalization, and git isolation", "type": "module", "bin": { diff --git a/plugin/mods/agents/pane.ts b/plugin/mods/agents/pane.ts index 58d88f9..dfe6b92 100644 --- a/plugin/mods/agents/pane.ts +++ b/plugin/mods/agents/pane.ts @@ -18,6 +18,9 @@ import type { PaneRow, PaneSnapshot, SessionRow } from "./types.js"; /** Cards never stretch past this, however wide the dock is. */ export const MAX_CARD_COLUMNS = 56; +/** Keep finished work useful without turning the pane into a transcript. */ +const HISTORY_PREVIEW_ROWS = 3; + /** Below this many usable columns the pane draws nothing rather than garbage. */ export const MIN_COLUMNS = 24; @@ -50,12 +53,12 @@ const STATUS_DOT = vocab({ needs_input: "◉", }); const STATUS_WORD = vocab({ - working: "Trabalhando agora", - starting: "Subindo", - needs_input: "Esperando voce", - completed: "Concluida", - stopped: "Em pausa", - failed: "Falhou", + working: "Working now", + starting: "Starting", + needs_input: "Waiting for you", + completed: "Completed", + stopped: "Paused", + failed: "Failed", }); const HARNESS_GLYPH = vocab({ claude: "▲", @@ -205,15 +208,15 @@ function rowAge(iso: unknown): number { } // One drawable piece of the pane: an orchestrator card, a worker card, or a -// collapsed finished row. `size` counts the block's own lines and never the -// connector above it: only the assembled order knows where a stem hangs, so -// `build` receives `connects`, whether the bottom edge grows the stem -// junction, and `lead`, whether a stem is drawn above the block. +// compact history section. `count` is how many sessions disappear if the block +// is evicted to make room for the frame and footer. `size` counts only the +// block's own lines; the assembled order adds any connector above it. type Block = { - kind: "card" | "line"; + kind: "card" | "history"; size: number; age: number; id: string; + count: number; build: (connects: boolean, lead: boolean) => string[]; }; @@ -242,6 +245,9 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined // snapshot, so it is folded into "finished": the most recently touched // sessions are the live ones and stay inside the budget. const finished = Math.max(0, workerTotal - working - waiting); + const finishedRows = rows.filter( + (row) => !LIVE.has(row.status ?? "") && !WAIT.has(row.status ?? ""), + ); const W = Math.trunc(columns); const IN = W - 2; @@ -276,6 +282,7 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined size: body.length + 2, age: ageValue, id, + count: 1, build: (connects, lead) => [ ...(lead ? [stem()] : []), cardTop(), @@ -289,16 +296,16 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined if (orchestrator !== undefined) { blocks.push( cardBlock(Number.POSITIVE_INFINITY, "", [ - ` ${glyph(orchestrator.agent)} ● Orquestrador`, - ` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agentes`, + ` ${glyph(orchestrator.agent)} ● Orchestrator`, + ` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`, ]), ); } for (const row of rows) { const status = row.status ?? ""; - // Rule 4: live rows draw as full cards; everything else collapses to one - // line naming its glyph, dot, id, name and age. + // Live rows stay prominent because they need attention. Finished rows are + // collected below into one compact history section. if (LIVE.has(status) || WAIT.has(status)) { const when = age(row.updatedAt, now); const word = statusWord(status); @@ -309,57 +316,67 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined when === "" ? ` ${word}` : ` ${word} · ${when}`, ]), ); - } else { + } + } + + if (finished > 0) { + const preview = finishedRows.slice(0, HISTORY_PREVIEW_ROWS); + const historyLines = [` ─ HISTORY · ${finished} finished`]; + for (const row of preview) { const when = age(row.updatedAt, now); - const line = ` ${glyph(row.agent)} ${dot(row.status)} ${cell(row.id)} ${cell(row.name)}${ - when === "" ? "" : ` · ${when}` - }`; - blocks.push({ - kind: "line", - size: 1, - age: rowAge(row.updatedAt), - id: cell(row.id), - build: (_connects, lead) => [...(lead ? [stem()] : []), frameRow(line)], - }); + historyLines.push( + ` ${glyph(row.agent)} ${cell(row.id)} ${cell(row.name)}${when === "" ? "" : ` · ${when}`}`, + ); } + const older = finished - preview.length; + if (older > 0) historyLines.push(` +${older} earlier`); + + blocks.push({ + kind: "history", + size: historyLines.length, + age: rowAge(preview[0]?.updatedAt), + id: "history", + count: finished, + build: (_connects, lead) => [...(lead ? [stem()] : []), ...historyLines.map(frameRow)], + }); } // Whether the block at index i hangs from the one above it by a stem. The // first block is the root and never leads with one. A card is always joined - // to what sits above it; a collapsed line only when it opens a run, so a - // run of one liners costs a single row per entry. + // to what sits above it; history only gets a stem when it follows a card. const stemAbove = (list: Block[], i: number): boolean => i > 0 && (list[i].kind === "card" || list[i - 1].kind === "card"); // Rule 3: these ten lines are the frame. Whenever anything is drawn at all // they are drawn, and they are never the lines that the fit cuts. const head = [ - frameTop(`Canvas do run ${cell(source.runId)}`), - frameRow(` ${total} sessoes · ${working} trabalhando`), - frameRow(` ${waiting} esperando voce · ${finished} prontas`), + frameTop(`Run canvas ${cell(source.runId)}`), + frameRow(` ${total} sessions · ${working} working`), + frameRow(` ${waiting} waiting for you · ${finished} finished`), frameSep(), frameRow(""), ]; const tail = [ frameRow(""), frameSep(), - frameRow(" ● trabalha ◉ te espera ○ pronta"), + frameRow(" ● working ◉ waiting ○ finished"), frameRow(" ▲ Claude ■ OpenCode ◆ Codex ⬟ OMP"), frameBot(), ]; const fixed = head.length + tail.length; - // The hidden line keeps its own stem only when it hangs off a full card; - // after a run of one liners it extends the list and costs a single row. + // The hidden line keeps its own stem only when it hangs off a full card. const hiddenSize = (list: Block[]): number => list.length > 0 && list[list.length - 1].kind === "card" ? 2 : 1; let kept = blocks; let dropped = 0; + let showHidden = false; if (limit !== undefined) { // Rule 7: the frame alone does not fit, so nothing is drawn at all. if (limit < fixed) return []; - // Rule 5: collapsed lines leave first, oldest first; then full cards, - // oldest first. The hidden count grows with every row that leaves (rule 6). + // History leaves first, then full cards, with the oldest block leaving + // first within each group. The hidden count follows the block's session + // count rather than its number of rendered lines. const victims = [...blocks].sort( (a, b) => Number(a.kind === "card") - Number(b.kind === "card") || @@ -368,22 +385,19 @@ function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined ); const sizeOf = (list: Block[]) => list.reduce((sum, block, i) => sum + block.size + (stemAbove(list, i) ? 1 : 0), 0); - const needed = () => fixed + sizeOf(kept) + (hidden + dropped > 0 ? hiddenSize(kept) : 0); + const needed = () => fixed + sizeOf(kept); for (let vi = 0; needed() > limit && vi < victims.length; vi += 1) { kept = kept.filter((block) => block !== victims[vi]); - dropped += 1; - } - // Rule 6 outranks one more agent, but the height itself outranks - // everything: every block has left and the hidden line still does not - // fit, so the frame stands alone. kept is empty by here: the loop only - // runs out of victims once every block is gone. - if (needed() > limit && kept.length === 0 && hidden + dropped > 0) { - return [...head, ...tail]; + dropped += victims[vi].count; } + // A hidden line is useful only when it does not displace a live card. + showHidden = dropped > 0 && fixed + sizeOf(kept) + hiddenSize(kept) <= limit; } - const hiddenTotal = hidden + dropped; - const hiddenRow = frameRow(` +${hiddenTotal} agentes ocultos`); + // `hidden` is already represented by the history block: the source does not + // expose the statuses of rows that were cut before formatting. + const hiddenTotal = showHidden ? dropped : 0; + const hiddenRow = frameRow(` +${hiddenTotal} hidden agents`); const out: string[] = [...head]; for (let i = 0; i < kept.length; i += 1) { const connects = i < kept.length - 1 || hiddenTotal > 0; @@ -421,20 +435,19 @@ export function formatPane( /** * Label for the button drawn above the prompt, the one affordance that brings - * the pane back after the engine's own close box took it away. That close is - * invisible to this module (verified live: clicking the pane's X fires no hook - * at all), so the label never claims to know whether the pane is open and the - * button only ever opens. Never throws: it is read inside a render hook. + * the pane back after the engine's own close box took it away. It reports only + * current attention states, never the historical run total. Never throws: it + * is read inside a render hook. */ export function paneButtonLabel(snapshot: PaneSnapshot | undefined): string { - if (!snapshot || typeof snapshot !== "object") return "agentes"; + if (!snapshot || typeof snapshot !== "object") return "agents"; try { - const total = count((snapshot as Partial).total); const rows = Array.isArray(snapshot.rows) ? snapshot.rows : []; const working = rows.filter((row) => LIVE.has(row?.status ?? "")).length; - const head = `${total} ${total === 1 ? "agente" : "agentes"}`; - return working > 0 ? `${head} · ${working} trabalhando` : head; + if (working > 0) return `${working} working`; + const waiting = rows.filter((row) => WAIT.has(row?.status ?? "")).length; + return waiting > 0 ? `${waiting} waiting for you` : "no active agents"; } catch { - return "agentes"; + return "agents"; } } diff --git a/plugin/statusline.sh b/plugin/statusline.sh index 084c0f9..b7bfc8c 100755 --- a/plugin/statusline.sh +++ b/plugin/statusline.sh @@ -253,7 +253,6 @@ const fields = [ contextField(), tokenField(), runUsage ? runField() : localField(), - runUsage && paint(TEXT, String(Math.max(0, Math.round(runUsage.activeSessionCount))) + " agents"), ].filter(Boolean); writeSync(1, fields.join(paint(MUTED, " · "))); diff --git a/scripts/pane-probe.sh b/scripts/pane-probe.sh index 5f282bf..88ce82e 100755 --- a/scripts/pane-probe.sh +++ b/scripts/pane-probe.sh @@ -32,7 +32,7 @@ SESSION="pane-probe-$$-${RANDOM}" COLS=200 ROWS=50 TIMEOUT=45 -FRAME_TEXT="Canvas do run $RUN_ID" +FRAME_TEXT="Run canvas $RUN_ID" probe_char=$'\xc3\xa9' if [[ ${#probe_char} -ne 1 ]]; then diff --git a/src/cli/commands/web.ts b/src/cli/commands/web.ts index 274744a..93fbae2 100644 --- a/src/cli/commands/web.ts +++ b/src/cli/commands/web.ts @@ -55,7 +55,7 @@ export function sseNamed(name: string, data: unknown): string { return `event: ${name}\n${sseData(data)}`; } -export function sseComment(text = "conectado"): string { +export function sseComment(text = "connected"): string { return `: ${text}\n\n`; } diff --git a/src/web/canvas-page.ts b/src/web/canvas-page.ts index 8247d35..2af4cac 100644 --- a/src/web/canvas-page.ts +++ b/src/web/canvas-page.ts @@ -119,28 +119,28 @@ export const CANVAS_PAGE: string = `
-

Canvas das runs

conectando...

+

Run canvas

connecting...

- - - + + +
-
TrabalhandoEsperando vocêEm pausaConcluída
-

AGORA MESMO

    -
    arraste para organizar · scroll dá zoom · clique num agente para o detalhe
    -