pantheon-opencode 1.5.0-beta.17 → 1.5.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/src/plugins/tui/dist/tui.d.ts +63 -27
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +82 -36
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +114 -49
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
18
18
|
|
|
19
19
|
## ✅ Closed Issues
|
|
20
20
|
|
|
21
|
+
## [v1.5.0-beta.19] - 2026-09-16
|
|
22
|
+
|
|
23
|
+
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
|
24
|
+
move this section to a versioned entry and reset the template below. -->
|
|
25
|
+
|
|
26
|
+
## 🆕 What's New
|
|
27
|
+
|
|
28
|
+
- **Improved — painel Delegations**: status ausente de children antigas agora é
|
|
29
|
+
`done` (não `running`); grace 60s para sessões recém-criadas; janela de
|
|
30
|
+
recência 24h (filtra histórico); ordenação newest-first; ceiling esconde
|
|
31
|
+
antigos.
|
|
32
|
+
- **Improved — animação**: spinner ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ agora tem timer
|
|
33
|
+
independente (80ms/frame), separado do poll de dados — fluido, não mais
|
|
34
|
+
"travado".
|
|
35
|
+
## [v1.5.0-beta.18] - 2026-09-16
|
|
36
|
+
|
|
37
|
+
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
|
38
|
+
move this section to a versioned entry and reset the template below. -->
|
|
39
|
+
|
|
40
|
+
## 🐞 Fixed
|
|
41
|
+
|
|
42
|
+
- **Painel Delegations voltou a funcionar (crítico):** o canal
|
|
43
|
+
`session.children` da TUI chamava a API com a shape v1 (`{ path: { id } }`)
|
|
44
|
+
num cliente **v2** que espera `{ sessionID }`; o placeholder não era
|
|
45
|
+
substituído (`/session/%7BsessionID%7D/children`), o host rejeitava cada
|
|
46
|
+
poll (~181k erros/s) e o painel ficava vazio ("No delegations"). Corrigido
|
|
47
|
+
`safeSessionPath`/`buildChildrenPath` para `{ sessionID }`, removidos os
|
|
48
|
+
`as any` que escondiam o mismatch, + teste de regressão exato. Auditoria
|
|
49
|
+
confirmou que só a TUI (v2) estava errada (o `plugin.ts` usa SDK v1 de
|
|
50
|
+
propósito).
|
|
51
|
+
- **Higiene de testes:** testes que importam o plugin sem isolamento agora
|
|
52
|
+
fazem `chdir` para um projeto temporário antes do import — não criam mais
|
|
53
|
+
entradas no board real (fim dos estados fantasma em desenvolvimento).
|
|
54
|
+
|
|
21
55
|
## [v1.5.0-beta.17] - 2026-09-15
|
|
22
56
|
|
|
23
57
|
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.5.0-beta.
|
|
4
|
+
"version": "1.5.0-beta.19",
|
|
5
5
|
"displayName": "Pantheon",
|
|
6
6
|
"description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
|
|
7
7
|
"agents": "./src/agents",
|
package/pyproject.toml
CHANGED
|
@@ -104,6 +104,24 @@ declare const IDLE_SILENCE_MS: number;
|
|
|
104
104
|
* constants only control which rows enter the TUI window. */
|
|
105
105
|
declare const DELEGATION_DONE_RETENTION_MS: number;
|
|
106
106
|
declare const DELEGATION_FAILED_RETENTION_MS: number;
|
|
107
|
+
/** Grace window for an ABSENT child status. `api.state.session.status()` only
|
|
108
|
+
* carries the sessions that are currently ALIVE; every finished child of a
|
|
109
|
+
* long-lived session is missing from that map. A child with no status is
|
|
110
|
+
* therefore treated as terminal (done) — treating it as running made every
|
|
111
|
+
* historical child of the session show up as "active" (the "197 children,
|
|
112
|
+
* all active" regression). The one exception is a freshly-spawned child that
|
|
113
|
+
* has not been registered by the status API yet: within this window of its
|
|
114
|
+
* last activity (`time.updated`, falling back to `time.created`) it still
|
|
115
|
+
* reads as running. */
|
|
116
|
+
declare const DELEGATION_CHILD_STATUS_GRACE_MS: number;
|
|
117
|
+
/** Recency window for the children channel. `session.children` returns EVERY
|
|
118
|
+
* child the focused session ever spawned, so a long session accumulates
|
|
119
|
+
* hundreds of historical rows that would inflate the panel. A child whose
|
|
120
|
+
* last activity (its own time, or its matched report's Finalized timestamp)
|
|
121
|
+
* is older than this window is dropped BEFORE it can enter the list. 24h
|
|
122
|
+
* keeps the sidebar scoped to current work while still covering long
|
|
123
|
+
* delegations. A child with no timestamp at all is kept (fail-open). */
|
|
124
|
+
declare const DELEGATION_CHILDREN_RECENCY_MS: number;
|
|
107
125
|
/** Alias-less NATIVE task() live entries never receive a report alias (the
|
|
108
126
|
* task tool output carries none), so the 30s alias-less prune in
|
|
109
127
|
* mergeChildDelegationSources must not apply to them — 5 minutes covers a
|
|
@@ -130,9 +148,10 @@ declare function delegationElapsed(entry: DelegationEntry, now: number): string;
|
|
|
130
148
|
type DelegationActivity = 'delegating' | 'working' | 'reading' | 'completed' | 'error' | 'cancelled';
|
|
131
149
|
declare function delegationActivity(entry: DelegationEntry): DelegationActivity;
|
|
132
150
|
declare function delegationActivityLabel(entry: DelegationEntry): string;
|
|
133
|
-
/** Return
|
|
134
|
-
*
|
|
135
|
-
|
|
151
|
+
/** Return the spinner glyph for the given frame counter. The frame counter
|
|
152
|
+
* is incremented every ~80ms by a dedicated animation timer, decoupled from
|
|
153
|
+
* the data poll cycle. Pure — no side effects. */
|
|
154
|
+
declare function delegationSpinnerFrame(frame: number): string;
|
|
136
155
|
/** Every state the row knows how to draw: the delegation lifecycle states
|
|
137
156
|
* derived from the children status + md reports (`completed`, `error`,
|
|
138
157
|
* `cancelled`, `startup_failed`, `startup_unknown`) plus the TUI-only
|
|
@@ -326,25 +345,26 @@ type TuiSessionSources = {
|
|
|
326
345
|
* fetch (empty panel, zero errors). Pure — no I/O, no runtime required. */
|
|
327
346
|
declare function resolveCurrentSessionID(sources: TuiSessionSources): string | null;
|
|
328
347
|
/** THE single choke point for every `session.children` / session-API path.
|
|
329
|
-
* Returns
|
|
330
|
-
* null for anything else (placeholder,
|
|
331
|
-
* skips the call entirely instead of
|
|
332
|
-
* (the "%7BsessionID%7D" regression).
|
|
333
|
-
*
|
|
334
|
-
*
|
|
348
|
+
* Returns the v2 SDK parameter shape `{ sessionID }` ONLY for a
|
|
349
|
+
* server-valid session id; returns null for anything else (placeholder,
|
|
350
|
+
* empty, foreign id) so the caller skips the call entirely instead of
|
|
351
|
+
* sending an unsubstituted placeholder (the "%7BsessionID%7D" regression).
|
|
352
|
+
* The TUI client is `@opencode-ai/sdk/v2`, whose session methods take a
|
|
353
|
+
* FLAT parameter object (`{ sessionID }`), NOT the v1 `{ path: { id } }`
|
|
354
|
+
* envelope — passing the v1 shape left the v2 `{sessionID}` URL template
|
|
355
|
+
* unsubstituted (`/session/%7BsessionID%7D/children`). Every session-API
|
|
356
|
+
* call site MUST go through this function (enforced by the source-scan
|
|
357
|
+
* test in tests/pantheon/tui-delegations.test.ts). */
|
|
335
358
|
declare function safeSessionPath(id: unknown): {
|
|
336
|
-
|
|
337
|
-
id: string;
|
|
338
|
-
};
|
|
359
|
+
sessionID: string;
|
|
339
360
|
} | null;
|
|
340
|
-
/** Build the `session.children`
|
|
361
|
+
/** Build the `session.children` parameters ONLY from a validated session id.
|
|
341
362
|
* Delegates to {@link safeSessionPath} — the single choke point. Returns
|
|
342
|
-
* null for null/invalid ids so the caller
|
|
343
|
-
* sending an unsubstituted placeholder (the
|
|
363
|
+
* the v2 SDK shape `{ sessionID }`; null for null/invalid ids so the caller
|
|
364
|
+
* skips the fetch instead of sending an unsubstituted placeholder (the
|
|
365
|
+
* "%7BsessionID%7D" regression). */
|
|
344
366
|
declare function buildChildrenPath(id: string | null | undefined): {
|
|
345
|
-
|
|
346
|
-
id: string;
|
|
347
|
-
};
|
|
367
|
+
sessionID: string;
|
|
348
368
|
} | null;
|
|
349
369
|
/** Duck-typed subset of a child Session (+ its live status type). */
|
|
350
370
|
type ChildDelegationLike = {
|
|
@@ -363,11 +383,27 @@ type ChildDelegationLike = {
|
|
|
363
383
|
updated?: number;
|
|
364
384
|
};
|
|
365
385
|
};
|
|
366
|
-
/** Map a child status type to a display state.
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
|
|
386
|
+
/** Map a child status type to a display state.
|
|
387
|
+
*
|
|
388
|
+
* Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is
|
|
389
|
+
* any other/unknown status — a status the panel does not recognise is not
|
|
390
|
+
* evidence of activity. An ABSENT status is the historical case: the status
|
|
391
|
+
* map (`api.state.session.status`) contains only currently-alive sessions, so
|
|
392
|
+
* every finished child of a long session is missing from it. Defaulting that
|
|
393
|
+
* to running made the whole session history render as "active" (the
|
|
394
|
+
* "197 children, all active" regression), so an absent status is terminal
|
|
395
|
+
* (done) UNLESS the child was active within `graceMs` — a just-spawned child
|
|
396
|
+
* that the status API has not registered yet must still render as running.
|
|
397
|
+
*
|
|
398
|
+
* @param status the `api.state.session.status(id)?.type`, or undefined
|
|
399
|
+
* @param time the child session time fields (`created`/`updated`)
|
|
400
|
+
* @param now current wall-clock ms (injectable for tests)
|
|
401
|
+
* @param graceMs recency window for the absent-status running assumption
|
|
402
|
+
* Pure — no I/O. */
|
|
403
|
+
declare function childStatusToState(status: string | undefined, time?: {
|
|
404
|
+
created?: number;
|
|
405
|
+
updated?: number;
|
|
406
|
+
}, now?: number, graceMs?: number): 'running' | 'completed' | 'retry';
|
|
371
407
|
/** Status-only row model: ONE glyph + ONE identity per row.
|
|
372
408
|
*
|
|
373
409
|
* A row is
|
|
@@ -385,9 +421,9 @@ declare function delegationRowStatus(state: DelegationDisplayState): DelegationR
|
|
|
385
421
|
declare const DELEGATION_ROW_GLYPHS: Readonly<Record<DelegationRowStatus, string>>;
|
|
386
422
|
declare function delegationRowGlyph(status: DelegationRowStatus): string;
|
|
387
423
|
/** Row marker (`<glyph> `) — the single state channel. `active` animates
|
|
388
|
-
* through the
|
|
389
|
-
* Pure. */
|
|
390
|
-
declare function delegationRowMarker(state: DelegationDisplayState,
|
|
424
|
+
* through the spinner for the given frame counter (incremented every ~80ms);
|
|
425
|
+
* every other kind is static. Pure. */
|
|
426
|
+
declare function delegationRowMarker(state: DelegationDisplayState, frame?: number): string;
|
|
391
427
|
/** ONE identity per row: the short report alias (`apo-1`); a row without a
|
|
392
428
|
* report alias (native task() child `native-task` / `native-<id>`, or an
|
|
393
429
|
* alias-less native live row `live-<callID>` kept as children-only) shows
|
|
@@ -472,5 +508,5 @@ declare const plugin: TuiPluginModule & {
|
|
|
472
508
|
setup: () => Promise<void>;
|
|
473
509
|
};
|
|
474
510
|
//#endregion
|
|
475
|
-
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 };
|
|
511
|
+
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 };
|
|
476
512
|
//# sourceMappingURL=tui.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KA07BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EAUA;;EAEA;EACA;EACA;;EAEA;;;;;;EAUA;;;;;;;;;;;iBAsBc,wBACd,aACA,oBACA,qBACC;;;;;;iBAkHmB,sBAAsB,cAAc,QAAQ;;;;;;;;;iBAyC5C,yBAAyB,eAAe,QAAQ;;;;;;;iBAUtD,oBACd;EAAS;EAAoB;eAC7B;;;;iBAUc,sBACd;EAAS;EAAoB;eAC7B;;;;;;iBAUc,YAAY;;;iBAMZ,WAAW;;;;iBAOX,yBAAyB,GAAG,iBAAiB,GAAG;;;;;iBAahD,oBACd,cAAc,mBACd,oBACA,cACA;EACG,QAAQ;EAAmB,QAAQ;;;;;iBAmCxB,sBACd,cAAc,mBACd,sBACA,cACA,4BACC;;cAMU;;;;cAKA;;;cAIA;cACA;;;;;cAMA;;;;;;;;;iBAUG,mBACd,OAAO,iBACP,aACA,uBACC;;;iBAWa,WAAW;;;;iBAeX,kBAAkB,OAAO,iBAAiB;;;KAQ9C;iBAQI,mBAAmB,OAAO,kBAAkB;iBAc5C,wBAAwB,OAAO
|
|
1
|
+
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}
|
|
@@ -165,7 +165,7 @@ async function detectVersion(api) {
|
|
|
165
165
|
if (ver) return ver;
|
|
166
166
|
}
|
|
167
167
|
} catch {}
|
|
168
|
-
return "1.5.0-beta.
|
|
168
|
+
return "1.5.0-beta.19";
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
171
|
* usage-bar — AI subscription usage gauge for the opencode TUI.
|
|
@@ -914,6 +914,24 @@ const IDLE_SILENCE_MS = 60 * 1e3;
|
|
|
914
914
|
* constants only control which rows enter the TUI window. */
|
|
915
915
|
const DELEGATION_DONE_RETENTION_MS = 120 * 1e3;
|
|
916
916
|
const DELEGATION_FAILED_RETENTION_MS = 600 * 1e3;
|
|
917
|
+
/** Grace window for an ABSENT child status. `api.state.session.status()` only
|
|
918
|
+
* carries the sessions that are currently ALIVE; every finished child of a
|
|
919
|
+
* long-lived session is missing from that map. A child with no status is
|
|
920
|
+
* therefore treated as terminal (done) — treating it as running made every
|
|
921
|
+
* historical child of the session show up as "active" (the "197 children,
|
|
922
|
+
* all active" regression). The one exception is a freshly-spawned child that
|
|
923
|
+
* has not been registered by the status API yet: within this window of its
|
|
924
|
+
* last activity (`time.updated`, falling back to `time.created`) it still
|
|
925
|
+
* reads as running. */
|
|
926
|
+
const DELEGATION_CHILD_STATUS_GRACE_MS = 60 * 1e3;
|
|
927
|
+
/** Recency window for the children channel. `session.children` returns EVERY
|
|
928
|
+
* child the focused session ever spawned, so a long session accumulates
|
|
929
|
+
* hundreds of historical rows that would inflate the panel. A child whose
|
|
930
|
+
* last activity (its own time, or its matched report's Finalized timestamp)
|
|
931
|
+
* is older than this window is dropped BEFORE it can enter the list. 24h
|
|
932
|
+
* keeps the sidebar scoped to current work while still covering long
|
|
933
|
+
* delegations. A child with no timestamp at all is kept (fail-open). */
|
|
934
|
+
const DELEGATION_CHILDREN_RECENCY_MS = 1440 * 60 * 1e3;
|
|
917
935
|
/** Alias-less NATIVE task() live entries never receive a report alias (the
|
|
918
936
|
* task tool output carries none), so the 30s alias-less prune in
|
|
919
937
|
* mergeChildDelegationSources must not apply to them — 5 minutes covers a
|
|
@@ -989,10 +1007,11 @@ const DELEGATION_SPINNER_FRAMES = [
|
|
|
989
1007
|
"⠇",
|
|
990
1008
|
"⠏"
|
|
991
1009
|
];
|
|
992
|
-
/** Return
|
|
993
|
-
*
|
|
994
|
-
|
|
995
|
-
|
|
1010
|
+
/** Return the spinner glyph for the given frame counter. The frame counter
|
|
1011
|
+
* is incremented every ~80ms by a dedicated animation timer, decoupled from
|
|
1012
|
+
* the data poll cycle. Pure — no side effects. */
|
|
1013
|
+
function delegationSpinnerFrame(frame) {
|
|
1014
|
+
const index = Math.abs(Math.floor(frame)) % DELEGATION_SPINNER_FRAMES.length;
|
|
996
1015
|
return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0];
|
|
997
1016
|
}
|
|
998
1017
|
/** Every state the row knows how to draw: the delegation lifecycle states
|
|
@@ -1428,32 +1447,53 @@ function resolveCurrentSessionID(sources) {
|
|
|
1428
1447
|
return null;
|
|
1429
1448
|
}
|
|
1430
1449
|
/** THE single choke point for every `session.children` / session-API path.
|
|
1431
|
-
* Returns
|
|
1432
|
-
* null for anything else (placeholder,
|
|
1433
|
-
* skips the call entirely instead of
|
|
1434
|
-
* (the "%7BsessionID%7D" regression).
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1450
|
+
* Returns the v2 SDK parameter shape `{ sessionID }` ONLY for a
|
|
1451
|
+
* server-valid session id; returns null for anything else (placeholder,
|
|
1452
|
+
* empty, foreign id) so the caller skips the call entirely instead of
|
|
1453
|
+
* sending an unsubstituted placeholder (the "%7BsessionID%7D" regression).
|
|
1454
|
+
* The TUI client is `@opencode-ai/sdk/v2`, whose session methods take a
|
|
1455
|
+
* FLAT parameter object (`{ sessionID }`), NOT the v1 `{ path: { id } }`
|
|
1456
|
+
* envelope — passing the v1 shape left the v2 `{sessionID}` URL template
|
|
1457
|
+
* unsubstituted (`/session/%7BsessionID%7D/children`). Every session-API
|
|
1458
|
+
* call site MUST go through this function (enforced by the source-scan
|
|
1459
|
+
* test in tests/pantheon/tui-delegations.test.ts). */
|
|
1437
1460
|
function safeSessionPath(id) {
|
|
1438
1461
|
if (!isValidSessionId(id)) return null;
|
|
1439
|
-
return {
|
|
1462
|
+
return { sessionID: id };
|
|
1440
1463
|
}
|
|
1441
|
-
/** Build the `session.children`
|
|
1464
|
+
/** Build the `session.children` parameters ONLY from a validated session id.
|
|
1442
1465
|
* Delegates to {@link safeSessionPath} — the single choke point. Returns
|
|
1443
|
-
* null for null/invalid ids so the caller
|
|
1444
|
-
* sending an unsubstituted placeholder (the
|
|
1466
|
+
* the v2 SDK shape `{ sessionID }`; null for null/invalid ids so the caller
|
|
1467
|
+
* skips the fetch instead of sending an unsubstituted placeholder (the
|
|
1468
|
+
* "%7BsessionID%7D" regression). */
|
|
1445
1469
|
function buildChildrenPath(id) {
|
|
1446
1470
|
return safeSessionPath(id);
|
|
1447
1471
|
}
|
|
1448
1472
|
/** Duck-typed subset of a child Session (+ its live status type). */
|
|
1449
|
-
/** Map a child status type to a display state.
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
|
|
1454
|
-
|
|
1473
|
+
/** Map a child status type to a display state.
|
|
1474
|
+
*
|
|
1475
|
+
* Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is
|
|
1476
|
+
* any other/unknown status — a status the panel does not recognise is not
|
|
1477
|
+
* evidence of activity. An ABSENT status is the historical case: the status
|
|
1478
|
+
* map (`api.state.session.status`) contains only currently-alive sessions, so
|
|
1479
|
+
* every finished child of a long session is missing from it. Defaulting that
|
|
1480
|
+
* to running made the whole session history render as "active" (the
|
|
1481
|
+
* "197 children, all active" regression), so an absent status is terminal
|
|
1482
|
+
* (done) UNLESS the child was active within `graceMs` — a just-spawned child
|
|
1483
|
+
* that the status API has not registered yet must still render as running.
|
|
1484
|
+
*
|
|
1485
|
+
* @param status the `api.state.session.status(id)?.type`, or undefined
|
|
1486
|
+
* @param time the child session time fields (`created`/`updated`)
|
|
1487
|
+
* @param now current wall-clock ms (injectable for tests)
|
|
1488
|
+
* @param graceMs recency window for the absent-status running assumption
|
|
1489
|
+
* Pure — no I/O. */
|
|
1490
|
+
function childStatusToState(status, time, now = Date.now(), graceMs = DELEGATION_CHILD_STATUS_GRACE_MS) {
|
|
1455
1491
|
if (status === "retry") return "retry";
|
|
1456
|
-
return "running";
|
|
1492
|
+
if (status === "busy") return "running";
|
|
1493
|
+
if (status !== void 0) return "completed";
|
|
1494
|
+
const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0);
|
|
1495
|
+
if (lastActivity > 0 && now - lastActivity < graceMs) return "running";
|
|
1496
|
+
return "completed";
|
|
1457
1497
|
}
|
|
1458
1498
|
/** Status-only row model: ONE glyph + ONE identity per row.
|
|
1459
1499
|
*
|
|
@@ -1489,11 +1529,11 @@ function delegationRowGlyph(status) {
|
|
|
1489
1529
|
return DELEGATION_ROW_GLYPHS[status];
|
|
1490
1530
|
}
|
|
1491
1531
|
/** Row marker (`<glyph> `) — the single state channel. `active` animates
|
|
1492
|
-
* through the
|
|
1493
|
-
* Pure. */
|
|
1494
|
-
function delegationRowMarker(state,
|
|
1532
|
+
* through the spinner for the given frame counter (incremented every ~80ms);
|
|
1533
|
+
* every other kind is static. Pure. */
|
|
1534
|
+
function delegationRowMarker(state, frame = 0) {
|
|
1495
1535
|
const status = delegationRowStatus(state);
|
|
1496
|
-
return `${status === "active" ? delegationSpinnerFrame(
|
|
1536
|
+
return `${status === "active" ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)} `;
|
|
1497
1537
|
}
|
|
1498
1538
|
/** ONE identity per row: the short report alias (`apo-1`); a row without a
|
|
1499
1539
|
* report alias (native task() child `native-task` / `native-<id>`, or an
|
|
@@ -1632,7 +1672,10 @@ function formatPanelLogLine(children, pantheon, native, md, events) {
|
|
|
1632
1672
|
* `parentSessionID` (the focused session id) is stamped on report-less
|
|
1633
1673
|
* children so the row carries its origin session; omit it and they stay
|
|
1634
1674
|
* unscoped ('').
|
|
1635
|
-
*
|
|
1675
|
+
* Children outside {@link DELEGATION_CHILDREN_RECENCY_MS} are dropped before
|
|
1676
|
+
* they can enter the list (the per-session child list grows for the whole
|
|
1677
|
+
* session lifetime); the survivors are sorted running-first, most recent
|
|
1678
|
+
* first (compareDelegationEntries), so a ceiling hides the OLDEST rows.
|
|
1636
1679
|
* Pure — no I/O. */
|
|
1637
1680
|
/** Temporal guard for the children channel (same recycled-alias class as the
|
|
1638
1681
|
* md merge): an md terminal report matched by taskID may still belong to
|
|
@@ -1657,7 +1700,9 @@ function childrenToDelegationEntries(children, md, now = Date.now(), parentSessi
|
|
|
1657
1700
|
if (child.id === "" || seen.has(child.id)) continue;
|
|
1658
1701
|
seen.add(child.id);
|
|
1659
1702
|
const mdEntry = byTaskID.get(child.id);
|
|
1660
|
-
const
|
|
1703
|
+
const lastActivity = Math.max(child.time?.updated ?? 0, child.time?.created ?? 0, mdEntry?.updatedAt ?? 0);
|
|
1704
|
+
if (lastActivity > 0 && now - lastActivity > 864e5) continue;
|
|
1705
|
+
const state = mdEntry !== void 0 && mdEntry.state !== "running" && mdTerminalCoversChild(mdEntry, child) ? mdEntry.state : childStatusToState(child.status, child.time, now);
|
|
1661
1706
|
out.push({
|
|
1662
1707
|
alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
|
|
1663
1708
|
sessionID: mdEntry?.sessionID ?? parentSessionID,
|
|
@@ -1766,7 +1811,7 @@ function DelegationRow(props) {
|
|
|
1766
1811
|
default: return t.textMuted;
|
|
1767
1812
|
}
|
|
1768
1813
|
});
|
|
1769
|
-
const marker = createMemo(() => delegationRowMarker(props.job.state, props.
|
|
1814
|
+
const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame));
|
|
1770
1815
|
const lead = createMemo(() => formatDelegationRowLead(props.job, marker()));
|
|
1771
1816
|
const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now));
|
|
1772
1817
|
const description = createMemo(() => truncateDelegationDescription(props.job.description));
|
|
@@ -1873,7 +1918,7 @@ function View(props) {
|
|
|
1873
1918
|
const panelLog = createTuiLogger(projectRoot());
|
|
1874
1919
|
const [childDelegations, setChildDelegations] = createSignal([]);
|
|
1875
1920
|
const [now, setNow] = createSignal(Date.now());
|
|
1876
|
-
const [
|
|
1921
|
+
const [frame, setFrame] = createSignal(0);
|
|
1877
1922
|
let eventRefreshCount = 0;
|
|
1878
1923
|
let delegationsInflight = null;
|
|
1879
1924
|
const refreshDelegations = () => {
|
|
@@ -1898,7 +1943,8 @@ function View(props) {
|
|
|
1898
1943
|
}
|
|
1899
1944
|
let children = [];
|
|
1900
1945
|
try {
|
|
1901
|
-
const
|
|
1946
|
+
const childrenPath = buildChildrenPath(sessionID);
|
|
1947
|
+
const result = childrenPath ? await props.api.client.session.children(childrenPath) : void 0;
|
|
1902
1948
|
const data = result?.data ?? result;
|
|
1903
1949
|
children = Array.isArray(data) ? data : [];
|
|
1904
1950
|
} catch (err) {
|
|
@@ -1908,7 +1954,7 @@ function View(props) {
|
|
|
1908
1954
|
const resolveStatus = (childID) => {
|
|
1909
1955
|
if (!isValidSessionId(childID)) return void 0;
|
|
1910
1956
|
try {
|
|
1911
|
-
return state
|
|
1957
|
+
return state.session.status(childID)?.type;
|
|
1912
1958
|
} catch {
|
|
1913
1959
|
return;
|
|
1914
1960
|
}
|
|
@@ -1958,7 +2004,7 @@ function View(props) {
|
|
|
1958
2004
|
refreshDelegations();
|
|
1959
2005
|
}, 1e3);
|
|
1960
2006
|
cleanup.push(() => clearInterval(poll));
|
|
1961
|
-
const animation = setInterval(() =>
|
|
2007
|
+
const animation = setInterval(() => setFrame((f) => f + 1), 80);
|
|
1962
2008
|
cleanup.push(() => clearInterval(animation));
|
|
1963
2009
|
try {
|
|
1964
2010
|
const sdk = props.api.state?.session;
|
|
@@ -2132,8 +2178,8 @@ function View(props) {
|
|
|
2132
2178
|
get now() {
|
|
2133
2179
|
return now();
|
|
2134
2180
|
},
|
|
2135
|
-
get
|
|
2136
|
-
return
|
|
2181
|
+
get frame() {
|
|
2182
|
+
return frame();
|
|
2137
2183
|
}
|
|
2138
2184
|
})
|
|
2139
2185
|
}), null);
|
|
@@ -2231,6 +2277,6 @@ const plugin = {
|
|
|
2231
2277
|
setup: async () => {}
|
|
2232
2278
|
};
|
|
2233
2279
|
//#endregion
|
|
2234
|
-
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 };
|
|
2280
|
+
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 };
|
|
2235
2281
|
|
|
2236
2282
|
//# sourceMappingURL=tui.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.19",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pantheon-tui",
|
|
9
|
-
"version": "1.5.0-beta.
|
|
9
|
+
"version": "1.5.0-beta.19",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": ">=1.14.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.0-beta.
|
|
4
|
+
"version": "1.5.0-beta.19",
|
|
5
5
|
"description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -1293,6 +1293,26 @@ export const IDLE_SILENCE_MS = 60 * 1000
|
|
|
1293
1293
|
export const DELEGATION_DONE_RETENTION_MS = 2 * 60 * 1000
|
|
1294
1294
|
export const DELEGATION_FAILED_RETENTION_MS = 10 * 60 * 1000
|
|
1295
1295
|
|
|
1296
|
+
/** Grace window for an ABSENT child status. `api.state.session.status()` only
|
|
1297
|
+
* carries the sessions that are currently ALIVE; every finished child of a
|
|
1298
|
+
* long-lived session is missing from that map. A child with no status is
|
|
1299
|
+
* therefore treated as terminal (done) — treating it as running made every
|
|
1300
|
+
* historical child of the session show up as "active" (the "197 children,
|
|
1301
|
+
* all active" regression). The one exception is a freshly-spawned child that
|
|
1302
|
+
* has not been registered by the status API yet: within this window of its
|
|
1303
|
+
* last activity (`time.updated`, falling back to `time.created`) it still
|
|
1304
|
+
* reads as running. */
|
|
1305
|
+
export const DELEGATION_CHILD_STATUS_GRACE_MS = 60 * 1000
|
|
1306
|
+
|
|
1307
|
+
/** Recency window for the children channel. `session.children` returns EVERY
|
|
1308
|
+
* child the focused session ever spawned, so a long session accumulates
|
|
1309
|
+
* hundreds of historical rows that would inflate the panel. A child whose
|
|
1310
|
+
* last activity (its own time, or its matched report's Finalized timestamp)
|
|
1311
|
+
* is older than this window is dropped BEFORE it can enter the list. 24h
|
|
1312
|
+
* keeps the sidebar scoped to current work while still covering long
|
|
1313
|
+
* delegations. A child with no timestamp at all is kept (fail-open). */
|
|
1314
|
+
export const DELEGATION_CHILDREN_RECENCY_MS = 24 * 60 * 60 * 1000
|
|
1315
|
+
|
|
1296
1316
|
/** Alias-less NATIVE task() live entries never receive a report alias (the
|
|
1297
1317
|
* task tool output carries none), so the 30s alias-less prune in
|
|
1298
1318
|
* mergeChildDelegationSources must not apply to them — 5 minutes covers a
|
|
@@ -1387,10 +1407,11 @@ export function delegationActivityLabel(entry: DelegationEntry): string {
|
|
|
1387
1407
|
|
|
1388
1408
|
const DELEGATION_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
1389
1409
|
|
|
1390
|
-
/** Return
|
|
1391
|
-
*
|
|
1392
|
-
|
|
1393
|
-
|
|
1410
|
+
/** Return the spinner glyph for the given frame counter. The frame counter
|
|
1411
|
+
* is incremented every ~80ms by a dedicated animation timer, decoupled from
|
|
1412
|
+
* the data poll cycle. Pure — no side effects. */
|
|
1413
|
+
export function delegationSpinnerFrame(frame: number): string {
|
|
1414
|
+
const index = Math.abs(Math.floor(frame)) % DELEGATION_SPINNER_FRAMES.length
|
|
1394
1415
|
return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0]
|
|
1395
1416
|
}
|
|
1396
1417
|
|
|
@@ -2034,7 +2055,7 @@ export function filterDelegationsToSession(
|
|
|
2034
2055
|
|
|
2035
2056
|
/* ─── Current-session resolution + path guard (placeholder regression) ──
|
|
2036
2057
|
* The sidebar forwarded `props.session_id` verbatim into
|
|
2037
|
-
* `session.children({
|
|
2058
|
+
* `session.children({ sessionID })`. When the TUI runtime renders the
|
|
2038
2059
|
* sidebar without a focused session it leaves the route template
|
|
2039
2060
|
* UNSUBSTITUTED — the literal "{sessionID}" string — which opencode's server
|
|
2040
2061
|
* rejected every poll (SchemaError: Expected a string starting with "ses",
|
|
@@ -2098,22 +2119,27 @@ export function resolveCurrentSessionID(sources: TuiSessionSources): string | nu
|
|
|
2098
2119
|
}
|
|
2099
2120
|
|
|
2100
2121
|
/** THE single choke point for every `session.children` / session-API path.
|
|
2101
|
-
* Returns
|
|
2102
|
-
* null for anything else (placeholder,
|
|
2103
|
-
* skips the call entirely instead of
|
|
2104
|
-
* (the "%7BsessionID%7D" regression).
|
|
2105
|
-
*
|
|
2106
|
-
*
|
|
2107
|
-
|
|
2122
|
+
* Returns the v2 SDK parameter shape `{ sessionID }` ONLY for a
|
|
2123
|
+
* server-valid session id; returns null for anything else (placeholder,
|
|
2124
|
+
* empty, foreign id) so the caller skips the call entirely instead of
|
|
2125
|
+
* sending an unsubstituted placeholder (the "%7BsessionID%7D" regression).
|
|
2126
|
+
* The TUI client is `@opencode-ai/sdk/v2`, whose session methods take a
|
|
2127
|
+
* FLAT parameter object (`{ sessionID }`), NOT the v1 `{ path: { id } }`
|
|
2128
|
+
* envelope — passing the v1 shape left the v2 `{sessionID}` URL template
|
|
2129
|
+
* unsubstituted (`/session/%7BsessionID%7D/children`). Every session-API
|
|
2130
|
+
* call site MUST go through this function (enforced by the source-scan
|
|
2131
|
+
* test in tests/pantheon/tui-delegations.test.ts). */
|
|
2132
|
+
export function safeSessionPath(id: unknown): { sessionID: string } | null {
|
|
2108
2133
|
if (!isValidSessionId(id)) return null
|
|
2109
|
-
return {
|
|
2134
|
+
return { sessionID: id }
|
|
2110
2135
|
}
|
|
2111
2136
|
|
|
2112
|
-
/** Build the `session.children`
|
|
2137
|
+
/** Build the `session.children` parameters ONLY from a validated session id.
|
|
2113
2138
|
* Delegates to {@link safeSessionPath} — the single choke point. Returns
|
|
2114
|
-
* null for null/invalid ids so the caller
|
|
2115
|
-
* sending an unsubstituted placeholder (the
|
|
2116
|
-
|
|
2139
|
+
* the v2 SDK shape `{ sessionID }`; null for null/invalid ids so the caller
|
|
2140
|
+
* skips the fetch instead of sending an unsubstituted placeholder (the
|
|
2141
|
+
* "%7BsessionID%7D" regression). */
|
|
2142
|
+
export function buildChildrenPath(id: string | null | undefined): { sessionID: string } | null {
|
|
2117
2143
|
return safeSessionPath(id)
|
|
2118
2144
|
}
|
|
2119
2145
|
|
|
@@ -2135,14 +2161,36 @@ export type ChildDelegationLike = {
|
|
|
2135
2161
|
}
|
|
2136
2162
|
}
|
|
2137
2163
|
|
|
2138
|
-
/** Map a child status type to a display state.
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
|
|
2143
|
-
|
|
2164
|
+
/** Map a child status type to a display state.
|
|
2165
|
+
*
|
|
2166
|
+
* Only `busy`/`retry` are EXPLICIT live states. `idle` is terminal and so is
|
|
2167
|
+
* any other/unknown status — a status the panel does not recognise is not
|
|
2168
|
+
* evidence of activity. An ABSENT status is the historical case: the status
|
|
2169
|
+
* map (`api.state.session.status`) contains only currently-alive sessions, so
|
|
2170
|
+
* every finished child of a long session is missing from it. Defaulting that
|
|
2171
|
+
* to running made the whole session history render as "active" (the
|
|
2172
|
+
* "197 children, all active" regression), so an absent status is terminal
|
|
2173
|
+
* (done) UNLESS the child was active within `graceMs` — a just-spawned child
|
|
2174
|
+
* that the status API has not registered yet must still render as running.
|
|
2175
|
+
*
|
|
2176
|
+
* @param status the `api.state.session.status(id)?.type`, or undefined
|
|
2177
|
+
* @param time the child session time fields (`created`/`updated`)
|
|
2178
|
+
* @param now current wall-clock ms (injectable for tests)
|
|
2179
|
+
* @param graceMs recency window for the absent-status running assumption
|
|
2180
|
+
* Pure — no I/O. */
|
|
2181
|
+
export function childStatusToState(
|
|
2182
|
+
status: string | undefined,
|
|
2183
|
+
time?: { created?: number; updated?: number },
|
|
2184
|
+
now = Date.now(),
|
|
2185
|
+
graceMs = DELEGATION_CHILD_STATUS_GRACE_MS,
|
|
2186
|
+
): 'running' | 'completed' | 'retry' {
|
|
2144
2187
|
if (status === 'retry') return 'retry'
|
|
2145
|
-
|
|
2188
|
+
if (status === 'busy') return 'running'
|
|
2189
|
+
if (status !== undefined) return 'completed' // idle + any other explicit terminal
|
|
2190
|
+
// Absent status: running only while the child's last activity is fresh.
|
|
2191
|
+
const lastActivity = Math.max(time?.updated ?? 0, time?.created ?? 0)
|
|
2192
|
+
if (lastActivity > 0 && now - lastActivity < graceMs) return 'running'
|
|
2193
|
+
return 'completed'
|
|
2146
2194
|
}
|
|
2147
2195
|
|
|
2148
2196
|
/** Status-only row model: ONE glyph + ONE identity per row.
|
|
@@ -2188,11 +2236,11 @@ export function delegationRowGlyph(status: DelegationRowStatus): string {
|
|
|
2188
2236
|
}
|
|
2189
2237
|
|
|
2190
2238
|
/** Row marker (`<glyph> `) — the single state channel. `active` animates
|
|
2191
|
-
* through the
|
|
2192
|
-
* Pure. */
|
|
2193
|
-
export function delegationRowMarker(state: DelegationDisplayState,
|
|
2239
|
+
* through the spinner for the given frame counter (incremented every ~80ms);
|
|
2240
|
+
* every other kind is static. Pure. */
|
|
2241
|
+
export function delegationRowMarker(state: DelegationDisplayState, frame = 0): string {
|
|
2194
2242
|
const status = delegationRowStatus(state)
|
|
2195
|
-
const glyph = status === 'active' ? delegationSpinnerFrame(
|
|
2243
|
+
const glyph = status === 'active' ? delegationSpinnerFrame(frame) : delegationRowGlyph(status)
|
|
2196
2244
|
return `${glyph} `
|
|
2197
2245
|
}
|
|
2198
2246
|
|
|
@@ -2370,7 +2418,10 @@ export function formatPanelLogLine(
|
|
|
2370
2418
|
* `parentSessionID` (the focused session id) is stamped on report-less
|
|
2371
2419
|
* children so the row carries its origin session; omit it and they stay
|
|
2372
2420
|
* unscoped ('').
|
|
2373
|
-
*
|
|
2421
|
+
* Children outside {@link DELEGATION_CHILDREN_RECENCY_MS} are dropped before
|
|
2422
|
+
* they can enter the list (the per-session child list grows for the whole
|
|
2423
|
+
* session lifetime); the survivors are sorted running-first, most recent
|
|
2424
|
+
* first (compareDelegationEntries), so a ceiling hides the OLDEST rows.
|
|
2374
2425
|
* Pure — no I/O. */
|
|
2375
2426
|
/** Temporal guard for the children channel (same recycled-alias class as the
|
|
2376
2427
|
* md merge): an md terminal report matched by taskID may still belong to
|
|
@@ -2404,10 +2455,22 @@ export function childrenToDelegationEntries(
|
|
|
2404
2455
|
if (child.id === '' || seen.has(child.id)) continue
|
|
2405
2456
|
seen.add(child.id)
|
|
2406
2457
|
const mdEntry = byTaskID.get(child.id)
|
|
2458
|
+
// Recency window (source-level): the children API returns EVERY child the
|
|
2459
|
+
// session ever spawned, so without this the whole session history enters
|
|
2460
|
+
// the list (the "197 children" panel). Effective recency = the latest of
|
|
2461
|
+
// the child's own activity and its matched report's Finalized timestamp;
|
|
2462
|
+
// a child with NO timestamp at all is kept (fail-open — it cannot be
|
|
2463
|
+
// judged). Oldest rows are the ones dropped.
|
|
2464
|
+
const lastActivity = Math.max(
|
|
2465
|
+
child.time?.updated ?? 0,
|
|
2466
|
+
child.time?.created ?? 0,
|
|
2467
|
+
mdEntry?.updatedAt ?? 0,
|
|
2468
|
+
)
|
|
2469
|
+
if (lastActivity > 0 && now - lastActivity > DELEGATION_CHILDREN_RECENCY_MS) continue
|
|
2407
2470
|
const state =
|
|
2408
2471
|
mdEntry !== undefined && mdEntry.state !== 'running' && mdTerminalCoversChild(mdEntry, child)
|
|
2409
2472
|
? mdEntry.state
|
|
2410
|
-
: childStatusToState(child.status)
|
|
2473
|
+
: childStatusToState(child.status, child.time, now)
|
|
2411
2474
|
out.push({
|
|
2412
2475
|
alias: mdEntry?.alias ?? `native-${child.id.slice(-4)}`,
|
|
2413
2476
|
// A report-less child IS a child of the focused session
|
|
@@ -2588,7 +2651,7 @@ function DelegationRow(props: {
|
|
|
2588
2651
|
api: TuiPluginApi
|
|
2589
2652
|
job: DelegationEntry
|
|
2590
2653
|
now: number
|
|
2591
|
-
|
|
2654
|
+
frame: number
|
|
2592
2655
|
}) {
|
|
2593
2656
|
const theme = () => props.api.theme.current
|
|
2594
2657
|
|
|
@@ -2610,7 +2673,7 @@ function DelegationRow(props: {
|
|
|
2610
2673
|
}
|
|
2611
2674
|
})
|
|
2612
2675
|
|
|
2613
|
-
const marker = createMemo(() => delegationRowMarker(props.job.state, props.
|
|
2676
|
+
const marker = createMemo(() => delegationRowMarker(props.job.state, props.frame))
|
|
2614
2677
|
const lead = createMemo(() => formatDelegationRowLead(props.job, marker()))
|
|
2615
2678
|
const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now))
|
|
2616
2679
|
const description = createMemo(() => truncateDelegationDescription(props.job.description))
|
|
@@ -2770,9 +2833,9 @@ function View(props: {
|
|
|
2770
2833
|
// (oh-my-opencode-slim pattern): re-fetch children + re-read the md every
|
|
2771
2834
|
// second so the panel updates even when NO event ever fires.
|
|
2772
2835
|
const [now, setNow] = createSignal(Date.now())
|
|
2773
|
-
//
|
|
2774
|
-
//
|
|
2775
|
-
const [
|
|
2836
|
+
// Frame counter for the spinner animation — incremented every ~80ms by a
|
|
2837
|
+
// dedicated fast timer, completely decoupled from the data poll cycle.
|
|
2838
|
+
const [frame, setFrame] = createSignal(0)
|
|
2776
2839
|
// Cumulative count of event-triggered refreshes — logged with every fetch
|
|
2777
2840
|
// ("panel: children=N(pantheon=M native=K) md=N events=N") so the (0)
|
|
2778
2841
|
// symptom is diagnosable: children>0 means the source works; events=0
|
|
@@ -2786,7 +2849,9 @@ function View(props: {
|
|
|
2786
2849
|
if (delegationsInflight !== null) return delegationsInflight
|
|
2787
2850
|
delegationsInflight = (async () => {
|
|
2788
2851
|
try {
|
|
2789
|
-
|
|
2852
|
+
// Typed (no `as any`): TuiState.session.status is a v2 sync accessor
|
|
2853
|
+
// taking the session id directly — same shape as the client call.
|
|
2854
|
+
const state = props.api.state
|
|
2790
2855
|
// 0. md reports from EVERY session under .pantheon/delegations — the
|
|
2791
2856
|
// ENRICHMENT channel. Read unfiltered (taskID match supplies alias/
|
|
2792
2857
|
// agent/description/terminal state); the active-session filter after
|
|
@@ -2817,13 +2882,18 @@ function View(props: {
|
|
|
2817
2882
|
return
|
|
2818
2883
|
}
|
|
2819
2884
|
// 2. children — PRIMARY source (SDK: api.client.session.children).
|
|
2885
|
+
// v2 SDK: `children` takes a FLAT `{ sessionID }` parameter object.
|
|
2886
|
+
// The v1 `{ path: { id } }` envelope is NOT understood by the v2
|
|
2887
|
+
// client — it left the URL template unsubstituted and the server
|
|
2888
|
+
// rejected every poll ("%7BsessionID%7D" regression).
|
|
2820
2889
|
let children: ChildDelegationLike[] = []
|
|
2821
2890
|
try {
|
|
2822
2891
|
// buildChildrenPath re-validates: with sessionID !== null it is
|
|
2823
2892
|
// never null, so no placeholder can reach the wire.
|
|
2824
|
-
const
|
|
2825
|
-
|
|
2826
|
-
|
|
2893
|
+
const childrenPath = buildChildrenPath(sessionID)
|
|
2894
|
+
const result = childrenPath
|
|
2895
|
+
? await props.api.client.session.children(childrenPath)
|
|
2896
|
+
: undefined
|
|
2827
2897
|
const data = (result?.data ?? result) as unknown
|
|
2828
2898
|
children = Array.isArray(data) ? (data as ChildDelegationLike[]) : []
|
|
2829
2899
|
} catch (err) {
|
|
@@ -2837,7 +2907,7 @@ function View(props: {
|
|
|
2837
2907
|
// path — "%7BsessionID%7D").
|
|
2838
2908
|
if (!isValidSessionId(childID)) return undefined
|
|
2839
2909
|
try {
|
|
2840
|
-
return state
|
|
2910
|
+
return state.session.status(childID)?.type
|
|
2841
2911
|
} catch {
|
|
2842
2912
|
return undefined // status API unavailable — child state fallback
|
|
2843
2913
|
}
|
|
@@ -2940,7 +3010,9 @@ function View(props: {
|
|
|
2940
3010
|
void refreshDelegations()
|
|
2941
3011
|
}, 1_000)
|
|
2942
3012
|
cleanup.push(() => clearInterval(poll))
|
|
2943
|
-
|
|
3013
|
+
// Fast spinner animation: 80ms frame counter, decoupled from data poll.
|
|
3014
|
+
// Only the spinner glyph depends on this; elapsed + data use the 1s timer.
|
|
3015
|
+
const animation = setInterval(() => setFrame((f) => f + 1), 80)
|
|
2944
3016
|
cleanup.push(() => clearInterval(animation))
|
|
2945
3017
|
|
|
2946
3018
|
// Compaction recovery: `message.part.removed` clears live entries while
|
|
@@ -3052,14 +3124,7 @@ function View(props: {
|
|
|
3052
3124
|
>
|
|
3053
3125
|
<box marginLeft={1} flexDirection="column">
|
|
3054
3126
|
<For each={delegationCeiling().visible}>
|
|
3055
|
-
{(job) => (
|
|
3056
|
-
<DelegationRow
|
|
3057
|
-
api={props.api}
|
|
3058
|
-
job={job}
|
|
3059
|
-
now={now()}
|
|
3060
|
-
animationNow={animationNow()}
|
|
3061
|
-
/>
|
|
3062
|
-
)}
|
|
3127
|
+
{(job) => <DelegationRow api={props.api} job={job} now={now()} frame={frame()} />}
|
|
3063
3128
|
</For>
|
|
3064
3129
|
<Show when={delegationCeiling().hidden > 0}>
|
|
3065
3130
|
<text fg={theme().textMuted}>
|