From 1f08d65b5cbab139997655a061a9c5854d87f613 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 07:40:54 -0300 Subject: [PATCH 1/8] chore(release): v1.5.0-beta.19 --- .zenodo.json | 6 +- CHANGELOG.md | 14 ++ CITATION.cff | 8 +- package-lock.json | 4 +- package.json | 2 +- plugin.json | 2 +- pyproject.toml | 2 +- src/plugins/tui/dist/tui.d.ts | 59 ++++++-- src/plugins/tui/dist/tui.d.ts.map | 2 +- src/plugins/tui/dist/tui.js | 88 ++++++++---- src/plugins/tui/package-lock.json | 4 +- src/plugins/tui/package.json | 2 +- src/plugins/tui/src/index.tsx | 108 +++++++++++---- tests/pantheon/tui-delegations.test.ts | 185 ++++++++++++++++++++++++- 14 files changed, 407 insertions(+), 79 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 9d191f5c..8d6a5c83 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -1,18 +1,18 @@ { "title": "Pantheon", "upload_type": "software", - "description": "Pantheon v1.5.0-beta.18 is an OpenCode plugin and installer for coordinating 14 specialized agents. The repository includes reusable skills, slash-command definitions, MCP server implementations, documentation, and tests. This release records version 1.5.0-beta.18 synchronized across package.json, package-lock.json, plugin.json, pyproject.toml, and src/plugins/tui/package.json.\n\nO Pantheon v1.5.0-beta.18 é um plugin e instalador para OpenCode que coordena 14 agentes especializados. O repositório inclui skills reutilizáveis, definições de comandos slash, implementações de servidores MCP, documentação e testes. Esta release registra a versão 1.5.0-beta.18 sincronizada em package.json, package-lock.json, plugin.json, pyproject.toml e src/plugins/tui/package.json.", + "description": "Pantheon v1.5.0-beta.19 is an OpenCode plugin and installer for coordinating 14 specialized agents. The repository includes reusable skills, slash-command definitions, MCP server implementations, documentation, and tests. This release records version 1.5.0-beta.19 synchronized across package.json, package-lock.json, plugin.json, pyproject.toml, and src/plugins/tui/package.json.\n\nO Pantheon v1.5.0-beta.19 é um plugin e instalador para OpenCode que coordena 14 agentes especializados. O repositório inclui skills reutilizáveis, definições de comandos slash, implementações de servidores MCP, documentação e testes. Esta release registra a versão 1.5.0-beta.19 sincronizada em package.json, package-lock.json, plugin.json, pyproject.toml e src/plugins/tui/package.json.", "creators": [ { "name": "Igor Leite da Silva" } ], - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "publication_date": "2026-09-16", "license": "MIT", "language": "eng", "keywords": ["opencode", "ai-agents", "multi-agent", "orchestration", "developer-tools"], - "url": "https://github.com/ils15/pantheon-opencode/releases/tag/v1.5.0-beta.18", + "url": "https://github.com/ils15/pantheon-opencode/releases/tag/v1.5.0-beta.19", "related_identifiers": [ { "identifier": "https://github.com/ils15/pantheon-opencode", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fa1b0db..c7e6a768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## ✅ Closed Issues +## [v1.5.0-beta.19] - 2026-09-16 + +<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will + move this section to a versioned entry and reset the template below. --> + +## 🆕 What's New + +- **Improved — painel Delegations**: status ausente de children antigas agora é + `done` (não `running`); grace 60s para sessões recém-criadas; janela de + recência 24h (filtra histórico); ordenação newest-first; ceiling esconde + antigos. +- **Improved — animação**: spinner ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ agora tem timer + independente (80ms/frame), separado do poll de dados — fluido, não mais + "travado". ## [v1.5.0-beta.18] - 2026-09-16 <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will diff --git a/CITATION.cff b/CITATION.cff index 40cf4863..0dbf7cde 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,15 +2,15 @@ cff-version: 1.2.0 message: "If you use Pantheon, please cite this software." title: "Pantheon" abstract: >- - Pantheon v1.5.0-beta.18 is an OpenCode plugin and installer for coordinating + Pantheon v1.5.0-beta.19 is an OpenCode plugin and installer for coordinating 14 specialized agents. The repository includes reusable skills, slash-command definitions, MCP server implementations, documentation, and - tests. O Pantheon v1.5.0-beta.18 é um plugin e instalador para OpenCode que + tests. O Pantheon v1.5.0-beta.19 é um plugin e instalador para OpenCode que coordena 14 agentes especializados, com skills reutilizáveis, comandos slash, servidores MCP, documentação e testes. -version: 1.5.0-beta.18 +version: 1.5.0-beta.19 date-released: 2026-09-16 -url: https://github.com/ils15/pantheon-opencode/releases/tag/v1.5.0-beta.18 +url: https://github.com/ils15/pantheon-opencode/releases/tag/v1.5.0-beta.19 doi: 10.5281/zenodo.22306637 repository-code: https://github.com/ils15/pantheon-opencode license: MIT diff --git a/package-lock.json b/package-lock.json index 379fd2e7..7457b07a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pantheon-opencode", - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pantheon-opencode", - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7c6b6cc8..70d9554c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pantheon-opencode", - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.", "type": "module", "publishConfig": { diff --git a/plugin.json b/plugin.json index df26e472..c146e0ab 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "pantheon", "publisher": "ils15", - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "displayName": "Pantheon", "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.", "agents": "./src/agents", diff --git a/pyproject.toml b/pyproject.toml index d41c6219..0991c7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "pantheon" -version = "1.5.0-beta.18" +version = "1.5.0-beta.19" description = "Multi-agent orchestration framework" requires-python = ">=3.11" dependencies = [] diff --git a/src/plugins/tui/dist/tui.d.ts b/src/plugins/tui/dist/tui.d.ts index 2b8dd092..c7e8e7df 100644 --- a/src/plugins/tui/dist/tui.d.ts +++ b/src/plugins/tui/dist/tui.d.ts @@ -104,6 +104,24 @@ declare const IDLE_SILENCE_MS: number; * constants only control which rows enter the TUI window. */ declare const DELEGATION_DONE_RETENTION_MS: number; declare const DELEGATION_FAILED_RETENTION_MS: number; +/** Grace window for an ABSENT child status. `api.state.session.status()` only + * carries the sessions that are currently ALIVE; every finished child of a + * long-lived session is missing from that map. A child with no status is + * therefore treated as terminal (done) — treating it as running made every + * historical child of the session show up as "active" (the "197 children, + * all active" regression). The one exception is a freshly-spawned child that + * has not been registered by the status API yet: within this window of its + * last activity (`time.updated`, falling back to `time.created`) it still + * reads as running. */ +declare const DELEGATION_CHILD_STATUS_GRACE_MS: number; +/** Recency window for the children channel. `session.children` returns EVERY + * child the focused session ever spawned, so a long session accumulates + * hundreds of historical rows that would inflate the panel. A child whose + * last activity (its own time, or its matched report's Finalized timestamp) + * is older than this window is dropped BEFORE it can enter the list. 24h + * keeps the sidebar scoped to current work while still covering long + * delegations. A child with no timestamp at all is kept (fail-open). */ +declare const DELEGATION_CHILDREN_RECENCY_MS: number; /** Alias-less NATIVE task() live entries never receive a report alias (the * task tool output carries none), so the 30s alias-less prune in * mergeChildDelegationSources must not apply to them — 5 minutes covers a @@ -130,9 +148,10 @@ declare function delegationElapsed(entry: DelegationEntry, now: number): string; type DelegationActivity = 'delegating' | 'working' | 'reading' | 'completed' | 'error' | 'cancelled'; declare function delegationActivity(entry: DelegationEntry): DelegationActivity; declare function delegationActivityLabel(entry: DelegationEntry): string; -/** Return a deterministic spinner frame. The View ticks this every 1000ms - * (not 140ms — the fast tick flickered without adding information). */ -declare function delegationSpinnerFrame(now: number): string; +/** Return the spinner glyph for the given frame counter. The frame counter + * is incremented every ~80ms by a dedicated animation timer, decoupled from + * the data poll cycle. Pure — no side effects. */ +declare function delegationSpinnerFrame(frame: number): string; /** Every state the row knows how to draw: the delegation lifecycle states * derived from the children status + md reports (`completed`, `error`, * `cancelled`, `startup_failed`, `startup_unknown`) plus the TUI-only @@ -364,11 +383,27 @@ type ChildDelegationLike = { updated?: number; }; }; -/** Map a child status type to a display state. busy/retry → running - * (the child is actively working), idle → completed, unknown → running - * (fail-open: a freshly-seen child is assumed active; the 1s poll + md - * correct it as soon as terminal data exists). */ -declare function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry'; +/** Map a child status type to a display state. + * + * Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is + * any other/unknown status — a status the panel does not recognise is not + * evidence of activity. An ABSENT status is the historical case: the status + * map (`api.state.session.status`) contains only currently-alive sessions, so + * every finished child of a long session is missing from it. Defaulting that + * to running made the whole session history render as "active" (the + * "197 children, all active" regression), so an absent status is terminal + * (done) UNLESS the child was active within `graceMs` — a just-spawned child + * that the status API has not registered yet must still render as running. + * + * @param status the `api.state.session.status(id)?.type`, or undefined + * @param time the child session time fields (`created`/`updated`) + * @param now current wall-clock ms (injectable for tests) + * @param graceMs recency window for the absent-status running assumption + * Pure — no I/O. */ +declare function childStatusToState(status: string | undefined, time?: { + created?: number; + updated?: number; +}, now?: number, graceMs?: number): 'running' | 'completed' | 'retry'; /** Status-only row model: ONE glyph + ONE identity per row. * * A row is @@ -386,9 +421,9 @@ declare function delegationRowStatus(state: DelegationDisplayState): DelegationR declare const DELEGATION_ROW_GLYPHS: Readonly>; declare function delegationRowGlyph(status: DelegationRowStatus): string; /** Row marker (` `) — the single state channel. `active` animates - * through the 1s spinner for the given tick; every other kind is static. - * Pure. */ -declare function delegationRowMarker(state: DelegationDisplayState, now?: number): string; + * through the spinner for the given frame counter (incremented every ~80ms); + * every other kind is static. Pure. */ +declare function delegationRowMarker(state: DelegationDisplayState, frame?: number): string; /** ONE identity per row: the short report alias (`apo-1`); a row without a * report alias (native task() child `native-task` / `native-`, or an * alias-less native live row `live-` kept as children-only) shows @@ -473,5 +508,5 @@ declare const plugin: TuiPluginModule & { setup: () => Promise; }; //#endregion -export { ChildDelegationLike, DELEGATION_ALIAS_WIDTH, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, DelegationActivity, DelegationDisplayState, DelegationEntry, DelegationRowStatus, DelegationStateTone, DelegationToolPart, IDLE_SILENCE_MS, LiveDelegationEntry, LiveDelegationStore, NATIVE_LIVE_ALIASLESS_TTL_MS, ParsedDelegationToolPart, STALE_RUNNING_THRESHOLD_MS, ToolActivity, TuiSessionSources, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList }; +export { ChildDelegationLike, DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, DelegationActivity, DelegationDisplayState, DelegationEntry, DelegationRowStatus, DelegationStateTone, DelegationToolPart, IDLE_SILENCE_MS, LiveDelegationEntry, LiveDelegationStore, NATIVE_LIVE_ALIASLESS_TTL_MS, ParsedDelegationToolPart, STALE_RUNNING_THRESHOLD_MS, ToolActivity, TuiSessionSources, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList }; //# sourceMappingURL=tui.d.ts.map diff --git a/src/plugins/tui/dist/tui.d.ts.map b/src/plugins/tui/dist/tui.d.ts.map index cc87b629..9207f2d9 100644 --- a/src/plugins/tui/dist/tui.d.ts.map +++ b/src/plugins/tui/dist/tui.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"} +{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"} diff --git a/src/plugins/tui/dist/tui.js b/src/plugins/tui/dist/tui.js index 49ca8473..b561b859 100644 --- a/src/plugins/tui/dist/tui.js +++ b/src/plugins/tui/dist/tui.js @@ -165,7 +165,7 @@ async function detectVersion(api) { if (ver) return ver; } } catch {} - return "1.5.0-beta.18"; + return "1.5.0-beta.19"; } /** * usage-bar — AI subscription usage gauge for the opencode TUI. @@ -914,6 +914,24 @@ const IDLE_SILENCE_MS = 60 * 1e3; * constants only control which rows enter the TUI window. */ const DELEGATION_DONE_RETENTION_MS = 120 * 1e3; const DELEGATION_FAILED_RETENTION_MS = 600 * 1e3; +/** Grace window for an ABSENT child status. `api.state.session.status()` only +* carries the sessions that are currently ALIVE; every finished child of a +* long-lived session is missing from that map. A child with no status is +* therefore treated as terminal (done) — treating it as running made every +* historical child of the session show up as "active" (the "197 children, +* all active" regression). The one exception is a freshly-spawned child that +* has not been registered by the status API yet: within this window of its +* last activity (`time.updated`, falling back to `time.created`) it still +* reads as running. */ +const DELEGATION_CHILD_STATUS_GRACE_MS = 60 * 1e3; +/** Recency window for the children channel. `session.children` returns EVERY +* child the focused session ever spawned, so a long session accumulates +* hundreds of historical rows that would inflate the panel. A child whose +* last activity (its own time, or its matched report's Finalized timestamp) +* is older than this window is dropped BEFORE it can enter the list. 24h +* keeps the sidebar scoped to current work while still covering long +* delegations. A child with no timestamp at all is kept (fail-open). */ +const DELEGATION_CHILDREN_RECENCY_MS = 1440 * 60 * 1e3; /** Alias-less NATIVE task() live entries never receive a report alias (the * task tool output carries none), so the 30s alias-less prune in * mergeChildDelegationSources must not apply to them — 5 minutes covers a @@ -989,10 +1007,11 @@ const DELEGATION_SPINNER_FRAMES = [ "⠇", "⠏" ]; -/** Return a deterministic spinner frame. The View ticks this every 1000ms -* (not 140ms — the fast tick flickered without adding information). */ -function delegationSpinnerFrame(now) { - const index = Math.floor(Math.max(0, now) / 1e3) % DELEGATION_SPINNER_FRAMES.length; +/** Return the spinner glyph for the given frame counter. The frame counter +* is incremented every ~80ms by a dedicated animation timer, decoupled from +* the data poll cycle. Pure — no side effects. */ +function delegationSpinnerFrame(frame) { + const index = Math.abs(Math.floor(frame)) % DELEGATION_SPINNER_FRAMES.length; return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0]; } /** Every state the row knows how to draw: the delegation lifecycle states @@ -1451,14 +1470,30 @@ function buildChildrenPath(id) { return safeSessionPath(id); } /** Duck-typed subset of a child Session (+ its live status type). */ -/** Map a child status type to a display state. busy/retry → running -* (the child is actively working), idle → completed, unknown → running -* (fail-open: a freshly-seen child is assumed active; the 1s poll + md -* correct it as soon as terminal data exists). */ -function childStatusToState(status) { - if (status === "idle") return "completed"; +/** Map a child status type to a display state. +* +* Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is +* any other/unknown status — a status the panel does not recognise is not +* evidence of activity. An ABSENT status is the historical case: the status +* map (`api.state.session.status`) contains only currently-alive sessions, so +* every finished child of a long session is missing from it. Defaulting that +* to running made the whole session history render as "active" (the +* "197 children, all active" regression), so an absent status is terminal +* (done) UNLESS the child was active within `graceMs` — a just-spawned child +* that the status API has not registered yet must still render as running. +* +* @param status the `api.state.session.status(id)?.type`, or undefined +* @param time the child session time fields (`created`/`updated`) +* @param now current wall-clock ms (injectable for tests) +* @param graceMs recency window for the absent-status running assumption +* Pure — no I/O. */ +function childStatusToState(status, time, now = Date.now(), graceMs = DELEGATION_CHILD_STATUS_GRACE_MS) { if (status === "retry") return "retry"; - return "running"; + if (status === "busy") return "running"; + if (status !== void 0) return "completed"; + const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0); + if (lastActivity > 0 && now - lastActivity < graceMs) return "running"; + return "completed"; } /** Status-only row model: ONE glyph + ONE identity per row. * @@ -1494,11 +1529,11 @@ function delegationRowGlyph(status) { return DELEGATION_ROW_GLYPHS[status]; } /** Row marker (` `) — the single state channel. `active` animates -* through the 1s spinner for the given tick; every other kind is static. -* Pure. */ -function delegationRowMarker(state, now = Date.now()) { +* through the spinner for the given frame counter (incremented every ~80ms); +* every other kind is static. Pure. */ +function delegationRowMarker(state, frame = 0) { const status = delegationRowStatus(state); - return `${status === "active" ? delegationSpinnerFrame(now) : delegationRowGlyph(status)} `; + return `${status === "active" ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)} `; } /** ONE identity per row: the short report alias (`apo-1`); a row without a * report alias (native task() child `native-task` / `native-`, or an @@ -1637,7 +1672,10 @@ function formatPanelLogLine(children, pantheon, native, md, events) { * `parentSessionID` (the focused session id) is stamped on report-less * children so the row carries its origin session; omit it and they stay * unscoped (''). -* Sorted running-first (compareDelegationEntries). +* Children outside {@link DELEGATION_CHILDREN_RECENCY_MS} are dropped before +* they can enter the list (the per-session child list grows for the whole +* session lifetime); the survivors are sorted running-first, most recent +* first (compareDelegationEntries), so a ceiling hides the OLDEST rows. * Pure — no I/O. */ /** Temporal guard for the children channel (same recycled-alias class as the * md merge): an md terminal report matched by taskID may still belong to @@ -1662,7 +1700,9 @@ function childrenToDelegationEntries(children, md, now = Date.now(), parentSessi if (child.id === "" || seen.has(child.id)) continue; seen.add(child.id); const mdEntry = byTaskID.get(child.id); - const state = mdEntry !== void 0 && mdEntry.state !== "running" && mdTerminalCoversChild(mdEntry, child) ? mdEntry.state : childStatusToState(child.status); + const lastActivity = Math.max(child.time?.updated ?? 0, child.time?.created ?? 0, mdEntry?.updatedAt ?? 0); + if (lastActivity > 0 && now - lastActivity > 864e5) continue; + const state = mdEntry !== void 0 && mdEntry.state !== "running" && mdTerminalCoversChild(mdEntry, child) ? mdEntry.state : childStatusToState(child.status, child.time, now); out.push({ alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`, sessionID: mdEntry?.sessionID ?? parentSessionID, @@ -1771,7 +1811,7 @@ function DelegationRow(props) { default: return t.textMuted; } }); - const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow)); + const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame)); const lead = createMemo(() => formatDelegationRowLead(props.job, marker())); const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now)); const description = createMemo(() => truncateDelegationDescription(props.job.description)); @@ -1878,7 +1918,7 @@ function View(props) { const panelLog = createTuiLogger(projectRoot()); const [childDelegations, setChildDelegations] = createSignal([]); const [now, setNow] = createSignal(Date.now()); - const [animationNow, setAnimationNow] = createSignal(Date.now()); + const [frame, setFrame] = createSignal(0); let eventRefreshCount = 0; let delegationsInflight = null; const refreshDelegations = () => { @@ -1964,7 +2004,7 @@ function View(props) { refreshDelegations(); }, 1e3); cleanup.push(() => clearInterval(poll)); - const animation = setInterval(() => setAnimationNow(Date.now()), 1e3); + const animation = setInterval(() => setFrame((f) => f + 1), 80); cleanup.push(() => clearInterval(animation)); try { const sdk = props.api.state?.session; @@ -2138,8 +2178,8 @@ function View(props) { get now() { return now(); }, - get animationNow() { - return animationNow(); + get frame() { + return frame(); } }) }), null); @@ -2237,6 +2277,6 @@ const plugin = { setup: async () => {} }; //#endregion -export { DELEGATION_ALIAS_WIDTH, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList }; +export { DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList }; //# sourceMappingURL=tui.js.map diff --git a/src/plugins/tui/package-lock.json b/src/plugins/tui/package-lock.json index dac2d511..077efdb1 100644 --- a/src/plugins/tui/package-lock.json +++ b/src/plugins/tui/package-lock.json @@ -1,12 +1,12 @@ { "name": "pantheon-tui", - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pantheon-tui", - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "license": "MIT", "dependencies": { "@opencode-ai/plugin": ">=1.14.0", diff --git a/src/plugins/tui/package.json b/src/plugins/tui/package.json index a7206dff..13bceadf 100644 --- a/src/plugins/tui/package.json +++ b/src/plugins/tui/package.json @@ -1,7 +1,7 @@ { "name": "pantheon-tui", "type": "module", - "version": "1.5.0-beta.18", + "version": "1.5.0-beta.19", "description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)", "license": "MIT", "exports": { diff --git a/src/plugins/tui/src/index.tsx b/src/plugins/tui/src/index.tsx index a1637d27..f0960fd6 100644 --- a/src/plugins/tui/src/index.tsx +++ b/src/plugins/tui/src/index.tsx @@ -1293,6 +1293,26 @@ export const IDLE_SILENCE_MS = 60 * 1000 export const DELEGATION_DONE_RETENTION_MS = 2 * 60 * 1000 export const DELEGATION_FAILED_RETENTION_MS = 10 * 60 * 1000 +/** Grace window for an ABSENT child status. `api.state.session.status()` only + * carries the sessions that are currently ALIVE; every finished child of a + * long-lived session is missing from that map. A child with no status is + * therefore treated as terminal (done) — treating it as running made every + * historical child of the session show up as "active" (the "197 children, + * all active" regression). The one exception is a freshly-spawned child that + * has not been registered by the status API yet: within this window of its + * last activity (`time.updated`, falling back to `time.created`) it still + * reads as running. */ +export const DELEGATION_CHILD_STATUS_GRACE_MS = 60 * 1000 + +/** Recency window for the children channel. `session.children` returns EVERY + * child the focused session ever spawned, so a long session accumulates + * hundreds of historical rows that would inflate the panel. A child whose + * last activity (its own time, or its matched report's Finalized timestamp) + * is older than this window is dropped BEFORE it can enter the list. 24h + * keeps the sidebar scoped to current work while still covering long + * delegations. A child with no timestamp at all is kept (fail-open). */ +export const DELEGATION_CHILDREN_RECENCY_MS = 24 * 60 * 60 * 1000 + /** Alias-less NATIVE task() live entries never receive a report alias (the * task tool output carries none), so the 30s alias-less prune in * mergeChildDelegationSources must not apply to them — 5 minutes covers a @@ -1387,10 +1407,11 @@ export function delegationActivityLabel(entry: DelegationEntry): string { const DELEGATION_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] -/** Return a deterministic spinner frame. The View ticks this every 1000ms - * (not 140ms — the fast tick flickered without adding information). */ -export function delegationSpinnerFrame(now: number): string { - const index = Math.floor(Math.max(0, now) / 1_000) % DELEGATION_SPINNER_FRAMES.length +/** Return the spinner glyph for the given frame counter. The frame counter + * is incremented every ~80ms by a dedicated animation timer, decoupled from + * the data poll cycle. Pure — no side effects. */ +export function delegationSpinnerFrame(frame: number): string { + const index = Math.abs(Math.floor(frame)) % DELEGATION_SPINNER_FRAMES.length return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0] } @@ -2140,14 +2161,36 @@ export type ChildDelegationLike = { } } -/** Map a child status type to a display state. busy/retry → running - * (the child is actively working), idle → completed, unknown → running - * (fail-open: a freshly-seen child is assumed active; the 1s poll + md - * correct it as soon as terminal data exists). */ -export function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry' { - if (status === 'idle') return 'completed' +/** Map a child status type to a display state. + * + * Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is + * any other/unknown status — a status the panel does not recognise is not + * evidence of activity. An ABSENT status is the historical case: the status + * map (`api.state.session.status`) contains only currently-alive sessions, so + * every finished child of a long session is missing from it. Defaulting that + * to running made the whole session history render as "active" (the + * "197 children, all active" regression), so an absent status is terminal + * (done) UNLESS the child was active within `graceMs` — a just-spawned child + * that the status API has not registered yet must still render as running. + * + * @param status the `api.state.session.status(id)?.type`, or undefined + * @param time the child session time fields (`created`/`updated`) + * @param now current wall-clock ms (injectable for tests) + * @param graceMs recency window for the absent-status running assumption + * Pure — no I/O. */ +export function childStatusToState( + status: string | undefined, + time?: { created?: number; updated?: number }, + now = Date.now(), + graceMs = DELEGATION_CHILD_STATUS_GRACE_MS, +): 'running' | 'completed' | 'retry' { if (status === 'retry') return 'retry' - return 'running' // busy or unknown + if (status === 'busy') return 'running' + if (status !== undefined) return 'completed' // idle + any other explicit terminal + // Absent status: running only while the child's last activity is fresh. + const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0) + if (lastActivity > 0 && now - lastActivity < graceMs) return 'running' + return 'completed' } /** Status-only row model: ONE glyph + ONE identity per row. @@ -2193,11 +2236,11 @@ export function delegationRowGlyph(status: DelegationRowStatus): string { } /** Row marker (` `) — the single state channel. `active` animates - * through the 1s spinner for the given tick; every other kind is static. - * Pure. */ -export function delegationRowMarker(state: DelegationDisplayState, now = Date.now()): string { + * through the spinner for the given frame counter (incremented every ~80ms); + * every other kind is static. Pure. */ +export function delegationRowMarker(state: DelegationDisplayState, frame = 0): string { const status = delegationRowStatus(state) - const glyph = status === 'active' ? delegationSpinnerFrame(now) : delegationRowGlyph(status) + const glyph = status === 'active' ? delegationSpinnerFrame(frame) : delegationRowGlyph(status) return `${glyph} ` } @@ -2375,7 +2418,10 @@ export function formatPanelLogLine( * `parentSessionID` (the focused session id) is stamped on report-less * children so the row carries its origin session; omit it and they stay * unscoped (''). - * Sorted running-first (compareDelegationEntries). + * Children outside {@link DELEGATION_CHILDREN_RECENCY_MS} are dropped before + * they can enter the list (the per-session child list grows for the whole + * session lifetime); the survivors are sorted running-first, most recent + * first (compareDelegationEntries), so a ceiling hides the OLDEST rows. * Pure — no I/O. */ /** Temporal guard for the children channel (same recycled-alias class as the * md merge): an md terminal report matched by taskID may still belong to @@ -2409,10 +2455,22 @@ export function childrenToDelegationEntries( if (child.id === '' || seen.has(child.id)) continue seen.add(child.id) const mdEntry = byTaskID.get(child.id) + // Recency window (source-level): the children API returns EVERY child the + // session ever spawned, so without this the whole session history enters + // the list (the "197 children" panel). Effective recency = the latest of + // the child's own activity and its matched report's Finalized timestamp; + // a child with NO timestamp at all is kept (fail-open — it cannot be + // judged). Oldest rows are the ones dropped. + const lastActivity = Math.max( + child.time?.updated ?? 0, + child.time?.created ?? 0, + mdEntry?.updatedAt ?? 0, + ) + if (lastActivity > 0 && now - lastActivity > DELEGATION_CHILDREN_RECENCY_MS) continue const state = mdEntry !== undefined && mdEntry.state !== 'running' && mdTerminalCoversChild(mdEntry, child) ? mdEntry.state - : childStatusToState(child.status) + : childStatusToState(child.status, child.time, now) out.push({ alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`, // A report-less child IS a child of the focused session @@ -2593,7 +2651,7 @@ function DelegationRow(props: { api: TuiPluginApi job: DelegationEntry now: number - animationNow: number + frame: number }) { const theme = () => props.api.theme.current @@ -2615,7 +2673,7 @@ function DelegationRow(props: { } }) - const marker = createMemo(() => delegationRowMarker(props.job.state, props.animationNow)) + const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame)) const lead = createMemo(() => formatDelegationRowLead(props.job, marker())) const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now)) const description = createMemo(() => truncateDelegationDescription(props.job.description)) @@ -2775,9 +2833,9 @@ function View(props: { // (oh-my-opencode-slim pattern): re-fetch children + re-read the md every // second so the panel updates even when NO event ever fires. const [now, setNow] = createSignal(Date.now()) - // Separate fast ticker for the spinner. The elapsed label intentionally - // remains on the 1s ticker to avoid unnecessary work. - const [animationNow, setAnimationNow] = createSignal(Date.now()) + // Frame counter for the spinner animation — incremented every ~80ms by a + // dedicated fast timer, completely decoupled from the data poll cycle. + const [frame, setFrame] = createSignal(0) // Cumulative count of event-triggered refreshes — logged with every fetch // ("panel: children=N(pantheon=M native=K) md=N events=N") so the (0) // symptom is diagnosable: children>0 means the source works; events=0 @@ -2952,7 +3010,9 @@ function View(props: { void refreshDelegations() }, 1_000) cleanup.push(() => clearInterval(poll)) - const animation = setInterval(() => setAnimationNow(Date.now()), 1_000) + // Fast spinner animation: 80ms frame counter, decoupled from data poll. + // Only the spinner glyph depends on this; elapsed + data use the 1s timer. + const animation = setInterval(() => setFrame((f) => f + 1), 80) cleanup.push(() => clearInterval(animation)) // Compaction recovery: `message.part.removed` clears live entries while @@ -3069,7 +3129,7 @@ function View(props: { api={props.api} job={job} now={now()} - animationNow={animationNow()} + frame={frame()} /> )} diff --git a/tests/pantheon/tui-delegations.test.ts b/tests/pantheon/tui-delegations.test.ts index 874fc5d7..fb67d867 100644 --- a/tests/pantheon/tui-delegations.test.ts +++ b/tests/pantheon/tui-delegations.test.ts @@ -32,6 +32,8 @@ import { childStatusToState, collectDelegationToolParts, createDelegationRowOpenHandler, + DELEGATION_CHILD_STATUS_GRACE_MS, + DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_DONE_RETENTION_MS, DELEGATION_FAILED_RETENTION_MS, DELEGATION_VISIBLE_CEILING, @@ -60,6 +62,8 @@ import { splitDelegationList, toDelegationEntry, visibleDelegationList, + delegationSpinnerFrame, + delegationRowMarker, } from '../../src/plugins/tui/src/index.tsx' // ─── Fixtures (real header shapes from renderDelegationMarkdown) ──────── @@ -884,13 +888,50 @@ async function main() { // ─── Children channel (childStatusToState + childrenToDelegationEntries) ─ await testAsync( - 'children: childStatusToState — busy→running, retry→retry, idle→completed, unknown→running', + 'children: childStatusToState — busy/retry→running, idle/absent→done (fresh absent child → running)', async () => { assert.equal(childStatusToState('busy'), 'running') assert.equal(childStatusToState('retry'), 'retry') assert.equal(childStatusToState('idle'), 'completed') - assert.equal(childStatusToState(undefined), 'running', 'no status → running (fail-open)') - assert.equal(childStatusToState('weird'), 'running', 'unknown status → running (fail-open)') + assert.equal( + childStatusToState(undefined), + 'completed', + 'absent status → done (historical child, NOT running)', + ) + assert.equal( + childStatusToState('weird'), + 'completed', + 'unknown status → done (not evidence of life)', + ) + + // Grace window: an absent status is running ONLY while the child is + // fresh — a just-spawned child the status API has not registered yet. + const now = 1_000_000 + assert.equal( + childStatusToState(undefined, { created: now - 10_000 }, now), + 'running', + 'absent status within the grace window (time.created) → running', + ) + assert.equal( + childStatusToState(undefined, { updated: now - 10_000 }, now), + 'running', + 'recent time.updated counts as fresh activity', + ) + assert.equal( + childStatusToState(undefined, { created: now - DELEGATION_CHILD_STATUS_GRACE_MS - 1 }, now), + 'completed', + 'absent status past the grace window → done', + ) + assert.equal( + childStatusToState(undefined, { created: 1000, updated: 2000 }, now), + 'completed', + 'absent status with an old timestamp → done', + ) + assert.equal( + childStatusToState(undefined, { created: now - 10_000 }, now, 0), + 'completed', + 'grace window is injectable (0 = always terminal)', + ) }, ) @@ -987,6 +1028,89 @@ async function main() { assert.deepEqual(first.map((e) => e.taskID).sort(), ['ses_child_a', 'ses_child_b']) }) + await testAsync( + 'children: recency window drops historical rows, survivors are newest-first', + async () => { + const now = 1_000_000_000_000 + const children: ChildDelegationLike[] = [ + { + id: 'ses_historical', + title: 'historical', + status: 'idle', + time: { + created: now - DELEGATION_CHILDREN_RECENCY_MS - 1, + updated: now - DELEGATION_CHILDREN_RECENCY_MS - 1, + }, + }, + { + id: 'ses_recent_new', + title: 'recent new', + status: 'idle', + time: { created: now - 60_000, updated: now - 30_000 }, + }, + { + id: 'ses_recent_old', + title: 'recent old', + status: 'idle', + time: { created: now - 120_000, updated: now - 90_000 }, + }, + ] + const entries = childrenToDelegationEntries(children, [], now) + assert.deepEqual( + entries.map((e) => e.taskID), + ['ses_recent_new', 'ses_recent_old'], + 'historical child is dropped; the rest are ordered newest-first', + ) + // Exactly at the window boundary the child is still kept (age == window). + assert.equal( + childrenToDelegationEntries( + [ + { + id: 'ses_boundary', + status: 'idle', + time: { + created: now - DELEGATION_CHILDREN_RECENCY_MS, + updated: now - DELEGATION_CHILDREN_RECENCY_MS, + }, + }, + ], + [], + now, + ).length, + 1, + 'a child exactly at the window boundary survives', + ) + // Fail-open: a child with no timestamp at all is never dropped. + assert.equal( + childrenToDelegationEntries([{ id: 'ses_untimed', status: 'idle' }], [], now).length, + 1, + 'untimed child kept (cannot be judged)', + ) + // A fresh report (Finalized) keeps a child whose own times are old. + const md = delegation({ + taskID: 'ses_reported', + state: 'completed', + startedAt: now - DELEGATION_CHILDREN_RECENCY_MS - 10_000, + updatedAt: now - 1_000, + }) + assert.equal( + childrenToDelegationEntries( + [ + { + id: 'ses_reported', + status: 'idle', + time: { created: now - DELEGATION_CHILDREN_RECENCY_MS - 5_000 }, + }, + ], + [md], + now, + ).length, + 1, + 'recent report keeps the child inside the window', + ) + }, + ) + await testAsync( 'children-only: native identity + per-child alias + parentSession stamp', async () => { @@ -1264,6 +1388,24 @@ async function main() { }, ) + await testAsync('ceiling: newest-first ordering makes "… +N" hide the OLDEST rows', async () => { + const now = 1_000_000 + const entries = [ + delegation({ alias: 'oldest', updatedAt: now - 100_000 }), + delegation({ alias: 'newer', updatedAt: now - 30_000 }), + delegation({ alias: 'newest', updatedAt: now - 5_000 }), + ] + const { visible, hidden, hiddenActive, hiddenTerminal } = ceilingDelegationList(entries, 2, now) + assert.deepEqual( + visible.map((entry) => entry.alias), + ['newest', 'newer'], + 'the two most recent rows render', + ) + assert.equal(hidden, 1, 'the oldest row is the one collapsed') + assert.equal(hiddenActive, 0) + assert.equal(hiddenTerminal, 1) + }) + await testAsync( 'split: active + recent cap; running native children never archived', async () => { @@ -1794,6 +1936,43 @@ async function main() { } }) + // ─── Spinner frame (decoupled animation) ───────────────────────────────── + + await testAsync('spinner: frame=0 and frame=1 produce different glyphs', async () => { + const glyph0 = delegationSpinnerFrame(0) + const glyph1 = delegationSpinnerFrame(1) + assert.ok(glyph0.length > 0, 'frame 0 must produce a glyph') + assert.ok(glyph1.length > 0, 'frame 1 must produce a glyph') + assert.notEqual(glyph0, glyph1, 'consecutive frames must differ') + }) + + await testAsync('spinner: frame counter wraps around the frame array', async () => { + const frames = 10 // DELEGATION_SPINNER_FRAMES.length + const glyph0 = delegationSpinnerFrame(0) + const glyphWrap = delegationSpinnerFrame(frames) + assert.equal(glyph0, glyphWrap, 'frame N must equal frame 0 after full cycle') + }) + + await testAsync('spinner: negative frame handled via abs()', async () => { + const glyph = delegationSpinnerFrame(-3) + assert.ok(glyph.length > 0, 'negative frame must produce a glyph') + assert.equal(glyph, delegationSpinnerFrame(3), 'abs(-3) must equal 3') + }) + + await testAsync('rowMarker: running state uses spinner frame, not static glyph', async () => { + const marker0 = delegationRowMarker('running', 0) + const marker1 = delegationRowMarker('running', 1) + assert.notEqual(marker0, marker1, 'running markers at different frames must differ') + assert.ok(marker0.endsWith(' '), 'marker must have trailing space') + }) + + await testAsync('rowMarker: completed state uses static glyph regardless of frame', async () => { + const marker0 = delegationRowMarker('completed', 0) + const marker99 = delegationRowMarker('completed', 99) + assert.equal(marker0, marker99, 'terminal markers are frame-independent') + assert.ok(marker0.startsWith('✓'), 'completed marker must be ✓') + }) + // ─── Report ──────────────────────────────────────────────────────────── const passed = results.filter((r) => r.passed).length From 5af07c431afbdf3fac05dccb52a1a5e2e109eb72 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 07:43:21 -0300 Subject: [PATCH 2/8] fix: sort imports in tui-delegations test (Biome lint) --- tests/pantheon/tui-delegations.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pantheon/tui-delegations.test.ts b/tests/pantheon/tui-delegations.test.ts index fb67d867..af9314c6 100644 --- a/tests/pantheon/tui-delegations.test.ts +++ b/tests/pantheon/tui-delegations.test.ts @@ -26,8 +26,8 @@ import { join } from 'node:path' import { buildChildrenPath, - type ChildDelegationLike, ceilingDelegationList, + type ChildDelegationLike, childrenToDelegationEntries, childStatusToState, collectDelegationToolParts, @@ -39,6 +39,8 @@ import { DELEGATION_VISIBLE_CEILING, type DelegationEntry, delegationRowIdentity, + delegationRowMarker, + delegationSpinnerFrame, delegationStateTone, filterDelegationsToSession, formatDelegationHeader, @@ -61,9 +63,7 @@ import { seedLiveDelegationMap, splitDelegationList, toDelegationEntry, - visibleDelegationList, - delegationSpinnerFrame, - delegationRowMarker, + visibleDelegationList } from '../../src/plugins/tui/src/index.tsx' // ─── Fixtures (real header shapes from renderDelegationMarkdown) ──────── From 53d12f0a6a157edb1c1d45ee041d2eee7360c9ea Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 07:44:55 -0300 Subject: [PATCH 3/8] fix: biome organize imports (type sort) --- tests/pantheon/tui-delegations.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pantheon/tui-delegations.test.ts b/tests/pantheon/tui-delegations.test.ts index af9314c6..b0f35ab4 100644 --- a/tests/pantheon/tui-delegations.test.ts +++ b/tests/pantheon/tui-delegations.test.ts @@ -26,8 +26,8 @@ import { join } from 'node:path' import { buildChildrenPath, - ceilingDelegationList, type ChildDelegationLike, + ceilingDelegationList, childrenToDelegationEntries, childStatusToState, collectDelegationToolParts, @@ -63,7 +63,7 @@ import { seedLiveDelegationMap, splitDelegationList, toDelegationEntry, - visibleDelegationList + visibleDelegationList, } from '../../src/plugins/tui/src/index.tsx' // ─── Fixtures (real header shapes from renderDelegationMarkdown) ──────── From 0d38ea43fa6a22d6c035c22c9cdf45bd2ca93790 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 07:47:39 -0300 Subject: [PATCH 4/8] fix: remove unused imports in sync-artifacts test --- tests/sync-artifacts.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sync-artifacts.test.mjs b/tests/sync-artifacts.test.mjs index e7a17e1a..4d75fa29 100644 --- a/tests/sync-artifacts.test.mjs +++ b/tests/sync-artifacts.test.mjs @@ -8,7 +8,7 @@ * Run: node --test tests/sync-artifacts.test.mjs */ import assert from 'node:assert/strict' -import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' From fa2ef3b813c991a56bf1e02e4d8a9d002a0ad622 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 07:50:05 -0300 Subject: [PATCH 5/8] fix: replace non-null assertions with optional chaining in file-persistence test --- tests/pantheon/file-persistence.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pantheon/file-persistence.test.ts b/tests/pantheon/file-persistence.test.ts index 2055bc44..30984d04 100644 --- a/tests/pantheon/file-persistence.test.ts +++ b/tests/pantheon/file-persistence.test.ts @@ -429,8 +429,8 @@ async function main() { const recovered = board.get('orphan') assert.ok(recovered) - assert.equal(recovered!.state, 'error') - assert.ok(recovered!.lastStatusError?.includes('Process restarted')) + assert.equal(recovered?.state, 'error') + assert.ok(recovered?.lastStatusError?.includes('Process restarted')) } finally { rmSync(tmpDir, { recursive: true, force: true }) } From 1f998622d6c8a9172217792c8ba9c713a71821e0 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 07:54:08 -0300 Subject: [PATCH 6/8] fix: remaining non-null assertions in file-persistence test --- tests/pantheon/file-persistence.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/pantheon/file-persistence.test.ts b/tests/pantheon/file-persistence.test.ts index 30984d04..b2c14e3b 100644 --- a/tests/pantheon/file-persistence.test.ts +++ b/tests/pantheon/file-persistence.test.ts @@ -207,14 +207,14 @@ async function main() { const loaded = await adapter.loadAllJobs() assert.equal(loaded.length, 2) - const jobA = loaded.find((r) => r.taskID === 'job-a')! + const jobA = loaded.find((r) => r.taskID === 'job-a') assert.ok(jobA) - assert.equal(jobA.state, 'completed') - assert.equal(jobA.resultSummary, 'Done A') + assert.equal(jobA?.state, 'completed') + assert.equal(jobA?.resultSummary, 'Done A') - const jobB = loaded.find((r) => r.taskID === 'job-b')! + const jobB = loaded.find((r) => r.taskID === 'job-b') assert.ok(jobB) - assert.equal(jobB.state, 'running') + assert.equal(jobB?.state, 'running') } finally { rmSync(tmpDir, { recursive: true, force: true }) } @@ -262,7 +262,7 @@ async function main() { const loaded = await adapter.loadAllJobs() assert.equal(loaded.length, 1) // only real-job from state.json - assert.equal(loaded[0]!.taskID, 'real-job') + assert.equal(loaded[0]?.taskID, 'real-job') } finally { rmSync(tmpDir, { recursive: true, force: true }) } From f81a73c7873b95f14397bf9f1fbc2b8a9a1a81cc Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 07:58:55 -0300 Subject: [PATCH 7/8] fix: all remaining Biome lint errors --- src/plugins/tui/src/index.tsx | 9 +-------- tests/pantheon/delegation-enforce.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/plugins/tui/src/index.tsx b/src/plugins/tui/src/index.tsx index f0960fd6..a6d87b1c 100644 --- a/src/plugins/tui/src/index.tsx +++ b/src/plugins/tui/src/index.tsx @@ -3124,14 +3124,7 @@ function View(props: { > - {(job) => ( - - )} + {(job) => } 0}> diff --git a/tests/pantheon/delegation-enforce.test.ts b/tests/pantheon/delegation-enforce.test.ts index 3de323e7..715a2326 100644 --- a/tests/pantheon/delegation-enforce.test.ts +++ b/tests/pantheon/delegation-enforce.test.ts @@ -77,9 +77,9 @@ async function main() { for (const tool of DEFAULT_BLOCKED_TOOLS) { const err = await runGuard(guard, tool, 'ses_ro') assert.ok(err, `tool "${tool}" must be denied in a read-only session`) - assert.match(err!.message, /read-only/i, `message must explain WHY (${tool})`) + assert.match(err?.message, /read-only/i, `message must explain WHY (${tool})`) assert.match( - err!.message, + err?.message, /task|dispatch|delegate/i, `message must say WHAT TO DO INSTEAD (${tool})`, ) @@ -236,7 +236,7 @@ async function main() { for (const tool of DEFAULT_BLOCKED_TOOLS) { const err = await runGuard(guard, tool, sid) assert.ok(err, `tool "${tool}" must be denied for a plugin-populated read-only session`) - assert.match(err!.message, /read-only/i) + assert.match(err?.message, /read-only/i) } } }, From 209f2744601b4f98f7edc53e6a6be753e467b966 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 16 Sep 2026 08:00:29 -0300 Subject: [PATCH 8/8] fix: rebuild TUI dist sourcemap (CI freshness check) --- src/plugins/tui/dist/tui.d.ts.map | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/tui/dist/tui.d.ts.map b/src/plugins/tui/dist/tui.d.ts.map index 9207f2d9..9ef05d5f 100644 --- a/src/plugins/tui/dist/tui.d.ts.map +++ b/src/plugins/tui/dist/tui.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"} +{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}