pantheon-opencode 1.5.0-beta.4 → 1.5.0-beta.6
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 +61 -0
- package/README.md +44 -1
- package/README.pt-BR.md +42 -0
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +13 -10
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/code_mode_server.py +35 -20
- package/scripts/doctor.mjs +178 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +95 -35
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/release-beta-version.mjs +2 -7
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +50 -4
- package/src/mcp/code_mode_server.py +35 -20
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/shared-board.ts +37 -0
- package/src/plugin.ts +6 -8
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +40 -8
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +253 -97
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +188 -16
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +188 -16
|
@@ -959,6 +959,7 @@ export type DelegationEntry = {
|
|
|
959
959
|
agent: string
|
|
960
960
|
state:
|
|
961
961
|
| 'running'
|
|
962
|
+
| 'retry'
|
|
962
963
|
| 'completed'
|
|
963
964
|
| 'error'
|
|
964
965
|
| 'startup_failed'
|
|
@@ -1195,27 +1196,46 @@ export function tuiLogPath(projectRoot: string): string {
|
|
|
1195
1196
|
* falling back to startedAt, descending). Shared by the md reader and
|
|
1196
1197
|
* mergeDelegationSources. */
|
|
1197
1198
|
export function compareDelegationEntries(a: DelegationEntry, b: DelegationEntry): number {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1199
|
+
const isActive = (st: DelegationEntry['state']) =>
|
|
1200
|
+
st === 'running' || st === 'retry' || st === 'stale-running' ? 1 : 0
|
|
1201
|
+
const aRun = isActive(a.state)
|
|
1202
|
+
const bRun = isActive(b.state)
|
|
1200
1203
|
if (aRun !== bRun) return bRun - aRun
|
|
1201
1204
|
return (b.updatedAt ?? b.startedAt) - (a.updatedAt ?? a.startedAt)
|
|
1202
1205
|
}
|
|
1203
1206
|
|
|
1204
|
-
/**
|
|
1205
|
-
* recent terminal reports
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1207
|
+
/** Split the panel list: active jobs (running/retry, stale-marked) first,
|
|
1208
|
+
* then the most recent terminal reports, then the archived tail (paginated
|
|
1209
|
+
* in the View). Pure — so the history-only panel (no sessionID) is testable
|
|
1210
|
+
* without the TUI runtime. */
|
|
1211
|
+
export function splitDelegationList(
|
|
1212
|
+
all: readonly DelegationEntry[],
|
|
1213
|
+
maxRecent = 8,
|
|
1214
|
+
now = Date.now(),
|
|
1215
|
+
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1216
|
+
): { active: DelegationEntry[]; recent: DelegationEntry[]; archived: DelegationEntry[] } {
|
|
1217
|
+
const active = all
|
|
1218
|
+
.filter((d) => d.state === 'running' || d.state === 'retry')
|
|
1219
|
+
.map((d) => markStaleIfRunning(d, now, staleThresholdMs))
|
|
1220
|
+
const terminal = all.filter((d) => d.state !== 'running' && d.state !== 'retry')
|
|
1221
|
+
return {
|
|
1222
|
+
active,
|
|
1223
|
+
recent: terminal.slice(0, maxRecent),
|
|
1224
|
+
archived: terminal.slice(maxRecent),
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** The list the panel actually renders (kept for the header count and
|
|
1229
|
+
* existing tests): active jobs first, then the most recent terminal
|
|
1230
|
+
* reports (capped) — the archived tail is rendered separately. Pure. */
|
|
1208
1231
|
export function visibleDelegationList(
|
|
1209
1232
|
all: readonly DelegationEntry[],
|
|
1210
1233
|
maxTerminal = 8,
|
|
1211
1234
|
now = Date.now(),
|
|
1212
1235
|
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1213
1236
|
): DelegationEntry[] {
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
.map((d) => markStaleIfRunning(d, now, staleThresholdMs))
|
|
1217
|
-
const terminal = all.filter((d) => d.state !== 'running').slice(0, maxTerminal)
|
|
1218
|
-
return [...running, ...terminal]
|
|
1237
|
+
const { active, recent } = splitDelegationList(all, maxTerminal, now, staleThresholdMs)
|
|
1238
|
+
return [...active, ...recent]
|
|
1219
1239
|
}
|
|
1220
1240
|
|
|
1221
1241
|
/** Default stale-running threshold: 30 minutes. */
|
|
@@ -1294,6 +1314,7 @@ export function delegationActivity(entry: DelegationEntry): DelegationActivity {
|
|
|
1294
1314
|
}
|
|
1295
1315
|
|
|
1296
1316
|
export function delegationActivityLabel(entry: DelegationEntry): string {
|
|
1317
|
+
if (entry.state === 'retry') return 'RETRYING'
|
|
1297
1318
|
switch (delegationActivity(entry)) {
|
|
1298
1319
|
case 'delegating':
|
|
1299
1320
|
return 'DELEGATING'
|
|
@@ -1318,6 +1339,89 @@ export function delegationSpinnerFrame(now: number): string {
|
|
|
1318
1339
|
return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0]
|
|
1319
1340
|
}
|
|
1320
1341
|
|
|
1342
|
+
/* ─── Live tool activity per child session (beta.6) ────────────────────────
|
|
1343
|
+
* The delegation panel's activity line ("↳ bash npm test") — what the agent
|
|
1344
|
+
* is doing RIGHT NOW. Sourced from message.part.updated tool parts: every
|
|
1345
|
+
* pending/running tool part updates a per-child-session record (latest wins).
|
|
1346
|
+
* Module-level by design: the sidebar remounts on route changes and the
|
|
1347
|
+
* "what is each agent doing" state should survive that (OMO pattern). */
|
|
1348
|
+
|
|
1349
|
+
export type ToolActivity = {
|
|
1350
|
+
tool: string
|
|
1351
|
+
summary: string
|
|
1352
|
+
at: number
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** Bounded tracker: child session id → latest running tool call. */
|
|
1356
|
+
const latestToolActivity = new Map<string, ToolActivity>()
|
|
1357
|
+
const MAX_TOOL_ACTIVITY_ENTRIES = 200
|
|
1358
|
+
const TOOL_ACTIVITY_TTL_MS = 5 * 60 * 1000
|
|
1359
|
+
|
|
1360
|
+
/** Archived-section pagination state — module-level so it survives sidebar
|
|
1361
|
+
* remounts (route changes) without leaking into the component tree. */
|
|
1362
|
+
const archivedUiState = { page: 0 }
|
|
1363
|
+
|
|
1364
|
+
/** First usable summary string from common tool input fields. */
|
|
1365
|
+
function summarizeToolInput(input: Record<string, unknown> | undefined): string {
|
|
1366
|
+
if (!input) return ''
|
|
1367
|
+
const candidates = ['command', 'description', 'prompt', 'filePath', 'pattern', 'url', 'query']
|
|
1368
|
+
for (const key of candidates) {
|
|
1369
|
+
const value = input[key]
|
|
1370
|
+
if (typeof value === 'string' && value.trim() !== '') return value
|
|
1371
|
+
}
|
|
1372
|
+
return ''
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** Reduce one message.part.updated tool part to displayable activity.
|
|
1376
|
+
* Returns null for non-tool parts, completed/error parts (no live activity)
|
|
1377
|
+
* or parts without a session id. Pure. */
|
|
1378
|
+
export function extractToolActivity(
|
|
1379
|
+
part: {
|
|
1380
|
+
type?: string
|
|
1381
|
+
tool?: string
|
|
1382
|
+
sessionID?: string
|
|
1383
|
+
state?: { status?: string; input?: Record<string, unknown> }
|
|
1384
|
+
},
|
|
1385
|
+
now = Date.now(),
|
|
1386
|
+
): { sessionID: string; activity: ToolActivity } | null {
|
|
1387
|
+
if (part?.type !== 'tool') return null
|
|
1388
|
+
const status = part.state?.status
|
|
1389
|
+
if (status !== 'pending' && status !== 'running') return null
|
|
1390
|
+
const sessionID = typeof part.sessionID === 'string' ? part.sessionID : ''
|
|
1391
|
+
if (sessionID === '') return null
|
|
1392
|
+
const tool = typeof part.tool === 'string' && part.tool !== '' ? part.tool : 'tool'
|
|
1393
|
+
const summary = summarizeToolInput(part.state?.input).replace(/\s+/g, ' ').trim().slice(0, 48)
|
|
1394
|
+
return { sessionID, activity: { tool, summary, at: now } }
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** Record an activity sample (bounded map, oldest dropped). */
|
|
1398
|
+
export function trackToolActivity(
|
|
1399
|
+
map: Map<string, ToolActivity>,
|
|
1400
|
+
sessionID: string,
|
|
1401
|
+
activity: ToolActivity,
|
|
1402
|
+
): void {
|
|
1403
|
+
if (sessionID === '') return
|
|
1404
|
+
if (map.size >= MAX_TOOL_ACTIVITY_ENTRIES) {
|
|
1405
|
+
const oldest = map.keys().next().value
|
|
1406
|
+
if (oldest !== undefined) map.delete(oldest)
|
|
1407
|
+
}
|
|
1408
|
+
map.set(sessionID, activity)
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/** Latest live activity for a child session, or null when absent/stale. */
|
|
1412
|
+
export function latestToolActivityFor(
|
|
1413
|
+
map: Map<string, ToolActivity>,
|
|
1414
|
+
sessionID: string | undefined,
|
|
1415
|
+
now = Date.now(),
|
|
1416
|
+
ttlMs = TOOL_ACTIVITY_TTL_MS,
|
|
1417
|
+
): ToolActivity | null {
|
|
1418
|
+
if (!sessionID) return null
|
|
1419
|
+
const hit = map.get(sessionID)
|
|
1420
|
+
if (hit === undefined) return null
|
|
1421
|
+
if (now - hit.at > ttlMs) return null
|
|
1422
|
+
return hit
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1321
1425
|
/** Merge the immediate tool-event channel into the child-session channel.
|
|
1322
1426
|
*
|
|
1323
1427
|
* Children remain the durable source, while live entries make a delegation
|
|
@@ -1880,9 +1984,10 @@ export type ChildDelegationLike = {
|
|
|
1880
1984
|
* (the child is actively working), idle → completed, unknown → running
|
|
1881
1985
|
* (fail-open: a freshly-seen child is assumed active; the 1s poll + md
|
|
1882
1986
|
* correct it as soon as terminal data exists). */
|
|
1883
|
-
export function childStatusToState(status: string | undefined): 'running' | 'completed' {
|
|
1987
|
+
export function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry' {
|
|
1884
1988
|
if (status === 'idle') return 'completed'
|
|
1885
|
-
|
|
1989
|
+
if (status === 'retry') return 'retry'
|
|
1990
|
+
return 'running' // busy or unknown
|
|
1886
1991
|
}
|
|
1887
1992
|
|
|
1888
1993
|
/** Row tag for a delegation entry: `[native]` for native task() children
|
|
@@ -2070,6 +2175,8 @@ function DelegationRow(props: {
|
|
|
2070
2175
|
switch (props.job.state) {
|
|
2071
2176
|
case 'running':
|
|
2072
2177
|
return t.warning
|
|
2178
|
+
case 'retry':
|
|
2179
|
+
return t.warning
|
|
2073
2180
|
case 'stale-running':
|
|
2074
2181
|
return t.error // warning color for stale delegations
|
|
2075
2182
|
case 'completed':
|
|
@@ -2083,6 +2190,7 @@ function DelegationRow(props: {
|
|
|
2083
2190
|
|
|
2084
2191
|
const marker = createMemo(() => {
|
|
2085
2192
|
if (props.job.state === 'running') return `${delegationSpinnerFrame(props.animationNow)} `
|
|
2193
|
+
if (props.job.state === 'retry') return '\u27f3 ' // ⟳ retrying
|
|
2086
2194
|
if (props.job.state === 'stale-running') return '\u26a0 ' // warning triangle
|
|
2087
2195
|
switch (props.job.state) {
|
|
2088
2196
|
case 'completed':
|
|
@@ -2106,6 +2214,12 @@ function DelegationRow(props: {
|
|
|
2106
2214
|
return line.length > 180 ? `${line.slice(0, 177)}\u2026` : line
|
|
2107
2215
|
})
|
|
2108
2216
|
|
|
2217
|
+
// beta.6: live tool call the child is running RIGHT NOW ("↳ bash npm test"),
|
|
2218
|
+
// fed by message.part.updated activity tracking. Null while idle/done.
|
|
2219
|
+
const activity = createMemo(() =>
|
|
2220
|
+
latestToolActivityFor(latestToolActivity, props.job.taskID, props.now),
|
|
2221
|
+
)
|
|
2222
|
+
|
|
2109
2223
|
// Click a row → navigate to the child session (route API guarded in the
|
|
2110
2224
|
// helper). Enter-key binding is left out (per-row keymap wiring is not
|
|
2111
2225
|
// worth the complexity in this sidebar); mouse is enabled in tui.json.
|
|
@@ -2123,6 +2237,13 @@ function DelegationRow(props: {
|
|
|
2123
2237
|
>{` ${props.job.agent} \u2014 ${delegationActivityLabel(props.job)}`}</text>
|
|
2124
2238
|
</box>
|
|
2125
2239
|
<text fg={theme().textMuted}>{detail()}</text>
|
|
2240
|
+
<Show when={activity()}>
|
|
2241
|
+
{(a) => (
|
|
2242
|
+
<text
|
|
2243
|
+
fg={theme().textMuted}
|
|
2244
|
+
>{` \u21b3 ${a().tool}${a().summary !== '' ? ' ' + a().summary : ''}`}</text>
|
|
2245
|
+
)}
|
|
2246
|
+
</Show>
|
|
2126
2247
|
</box>
|
|
2127
2248
|
)
|
|
2128
2249
|
}
|
|
@@ -2335,9 +2456,20 @@ function View(props: {
|
|
|
2335
2456
|
})()
|
|
2336
2457
|
return delegationsInflight
|
|
2337
2458
|
}
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
|
|
2459
|
+
// beta.6: active (running/retry) first, then the 8 most recent terminal
|
|
2460
|
+
// reports, then the archived tail (paginated). Pagination state lives at
|
|
2461
|
+
// module level so sidebar remounts keep the user's page.
|
|
2462
|
+
const delegationSplit = createMemo(() => splitDelegationList(childDelegations()))
|
|
2463
|
+
const visibleDelegations = createMemo(() => [
|
|
2464
|
+
...delegationSplit().active,
|
|
2465
|
+
...delegationSplit().recent,
|
|
2466
|
+
])
|
|
2467
|
+
const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page)
|
|
2468
|
+
const clampArchivedPage = (page: number, pages: number) => {
|
|
2469
|
+
const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)))
|
|
2470
|
+
archivedUiState.page = clamped
|
|
2471
|
+
setArchivedPage(clamped)
|
|
2472
|
+
}
|
|
2341
2473
|
|
|
2342
2474
|
// ── Event subscriptions for live session/delegation updates ──
|
|
2343
2475
|
onMount(() => {
|
|
@@ -2504,6 +2636,41 @@ function View(props: {
|
|
|
2504
2636
|
/>
|
|
2505
2637
|
)}
|
|
2506
2638
|
</For>
|
|
2639
|
+
<Show when={delegationSplit().archived.length > 0}>
|
|
2640
|
+
{(() => {
|
|
2641
|
+
const archived = delegationSplit().archived
|
|
2642
|
+
const pageSize = 8
|
|
2643
|
+
const pages = Math.ceil(archived.length / pageSize)
|
|
2644
|
+
const page = Math.min(archivedPage(), pages - 1)
|
|
2645
|
+
const slice = archived.slice(page * pageSize, page * pageSize + pageSize)
|
|
2646
|
+
return (
|
|
2647
|
+
<box flexDirection="column">
|
|
2648
|
+
<box flexDirection="row" marginLeft={0}>
|
|
2649
|
+
<text
|
|
2650
|
+
fg={theme().textMuted}
|
|
2651
|
+
onMouseDown={() => clampArchivedPage(page - 1, pages)}
|
|
2652
|
+
>{`\u2039 `}</text>
|
|
2653
|
+
<text fg={theme().textMuted}>{`Archived (${archived.length}) `}</text>
|
|
2654
|
+
<text fg={theme().textMuted}>{`${page + 1}/${pages}`}</text>
|
|
2655
|
+
<text
|
|
2656
|
+
fg={theme().textMuted}
|
|
2657
|
+
onMouseDown={() => clampArchivedPage(page + 1, pages)}
|
|
2658
|
+
>{` \u203a`}</text>
|
|
2659
|
+
</box>
|
|
2660
|
+
<For each={slice}>
|
|
2661
|
+
{(job) => (
|
|
2662
|
+
<DelegationRow
|
|
2663
|
+
api={props.api}
|
|
2664
|
+
job={job}
|
|
2665
|
+
now={now()}
|
|
2666
|
+
animationNow={animationNow()}
|
|
2667
|
+
/>
|
|
2668
|
+
)}
|
|
2669
|
+
</For>
|
|
2670
|
+
</box>
|
|
2671
|
+
)
|
|
2672
|
+
})()}
|
|
2673
|
+
</Show>
|
|
2507
2674
|
</box>
|
|
2508
2675
|
</Show>
|
|
2509
2676
|
</Show>
|
|
@@ -2552,6 +2719,11 @@ const tui: TuiPlugin = (api, _options, _meta) => {
|
|
|
2552
2719
|
const props = (event?.properties ?? {}) as { part?: unknown; info?: { part?: unknown } }
|
|
2553
2720
|
const part = (props.part ?? props.info?.part) as DelegationToolPart | undefined
|
|
2554
2721
|
if (part === undefined) return
|
|
2722
|
+
// beta.6: track the live tool call per child session (panel's
|
|
2723
|
+
// activity line). Fire-and-forget — display-only state.
|
|
2724
|
+
const sample = extractToolActivity(part as any)
|
|
2725
|
+
if (sample !== null)
|
|
2726
|
+
trackToolActivity(latestToolActivity, sample.sessionID, sample.activity)
|
|
2555
2727
|
if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump()
|
|
2556
2728
|
}),
|
|
2557
2729
|
)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.6",
|
|
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.6",
|
|
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.6",
|
|
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": {
|
|
@@ -959,6 +959,7 @@ export type DelegationEntry = {
|
|
|
959
959
|
agent: string
|
|
960
960
|
state:
|
|
961
961
|
| 'running'
|
|
962
|
+
| 'retry'
|
|
962
963
|
| 'completed'
|
|
963
964
|
| 'error'
|
|
964
965
|
| 'startup_failed'
|
|
@@ -1195,27 +1196,46 @@ export function tuiLogPath(projectRoot: string): string {
|
|
|
1195
1196
|
* falling back to startedAt, descending). Shared by the md reader and
|
|
1196
1197
|
* mergeDelegationSources. */
|
|
1197
1198
|
export function compareDelegationEntries(a: DelegationEntry, b: DelegationEntry): number {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1199
|
+
const isActive = (st: DelegationEntry['state']) =>
|
|
1200
|
+
st === 'running' || st === 'retry' || st === 'stale-running' ? 1 : 0
|
|
1201
|
+
const aRun = isActive(a.state)
|
|
1202
|
+
const bRun = isActive(b.state)
|
|
1200
1203
|
if (aRun !== bRun) return bRun - aRun
|
|
1201
1204
|
return (b.updatedAt ?? b.startedAt) - (a.updatedAt ?? a.startedAt)
|
|
1202
1205
|
}
|
|
1203
1206
|
|
|
1204
|
-
/**
|
|
1205
|
-
* recent terminal reports
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1207
|
+
/** Split the panel list: active jobs (running/retry, stale-marked) first,
|
|
1208
|
+
* then the most recent terminal reports, then the archived tail (paginated
|
|
1209
|
+
* in the View). Pure — so the history-only panel (no sessionID) is testable
|
|
1210
|
+
* without the TUI runtime. */
|
|
1211
|
+
export function splitDelegationList(
|
|
1212
|
+
all: readonly DelegationEntry[],
|
|
1213
|
+
maxRecent = 8,
|
|
1214
|
+
now = Date.now(),
|
|
1215
|
+
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1216
|
+
): { active: DelegationEntry[]; recent: DelegationEntry[]; archived: DelegationEntry[] } {
|
|
1217
|
+
const active = all
|
|
1218
|
+
.filter((d) => d.state === 'running' || d.state === 'retry')
|
|
1219
|
+
.map((d) => markStaleIfRunning(d, now, staleThresholdMs))
|
|
1220
|
+
const terminal = all.filter((d) => d.state !== 'running' && d.state !== 'retry')
|
|
1221
|
+
return {
|
|
1222
|
+
active,
|
|
1223
|
+
recent: terminal.slice(0, maxRecent),
|
|
1224
|
+
archived: terminal.slice(maxRecent),
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** The list the panel actually renders (kept for the header count and
|
|
1229
|
+
* existing tests): active jobs first, then the most recent terminal
|
|
1230
|
+
* reports (capped) — the archived tail is rendered separately. Pure. */
|
|
1208
1231
|
export function visibleDelegationList(
|
|
1209
1232
|
all: readonly DelegationEntry[],
|
|
1210
1233
|
maxTerminal = 8,
|
|
1211
1234
|
now = Date.now(),
|
|
1212
1235
|
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1213
1236
|
): DelegationEntry[] {
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
.map((d) => markStaleIfRunning(d, now, staleThresholdMs))
|
|
1217
|
-
const terminal = all.filter((d) => d.state !== 'running').slice(0, maxTerminal)
|
|
1218
|
-
return [...running, ...terminal]
|
|
1237
|
+
const { active, recent } = splitDelegationList(all, maxTerminal, now, staleThresholdMs)
|
|
1238
|
+
return [...active, ...recent]
|
|
1219
1239
|
}
|
|
1220
1240
|
|
|
1221
1241
|
/** Default stale-running threshold: 30 minutes. */
|
|
@@ -1294,6 +1314,7 @@ export function delegationActivity(entry: DelegationEntry): DelegationActivity {
|
|
|
1294
1314
|
}
|
|
1295
1315
|
|
|
1296
1316
|
export function delegationActivityLabel(entry: DelegationEntry): string {
|
|
1317
|
+
if (entry.state === 'retry') return 'RETRYING'
|
|
1297
1318
|
switch (delegationActivity(entry)) {
|
|
1298
1319
|
case 'delegating':
|
|
1299
1320
|
return 'DELEGATING'
|
|
@@ -1318,6 +1339,89 @@ export function delegationSpinnerFrame(now: number): string {
|
|
|
1318
1339
|
return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0]
|
|
1319
1340
|
}
|
|
1320
1341
|
|
|
1342
|
+
/* ─── Live tool activity per child session (beta.6) ────────────────────────
|
|
1343
|
+
* The delegation panel's activity line ("↳ bash npm test") — what the agent
|
|
1344
|
+
* is doing RIGHT NOW. Sourced from message.part.updated tool parts: every
|
|
1345
|
+
* pending/running tool part updates a per-child-session record (latest wins).
|
|
1346
|
+
* Module-level by design: the sidebar remounts on route changes and the
|
|
1347
|
+
* "what is each agent doing" state should survive that (OMO pattern). */
|
|
1348
|
+
|
|
1349
|
+
export type ToolActivity = {
|
|
1350
|
+
tool: string
|
|
1351
|
+
summary: string
|
|
1352
|
+
at: number
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** Bounded tracker: child session id → latest running tool call. */
|
|
1356
|
+
const latestToolActivity = new Map<string, ToolActivity>()
|
|
1357
|
+
const MAX_TOOL_ACTIVITY_ENTRIES = 200
|
|
1358
|
+
const TOOL_ACTIVITY_TTL_MS = 5 * 60 * 1000
|
|
1359
|
+
|
|
1360
|
+
/** Archived-section pagination state — module-level so it survives sidebar
|
|
1361
|
+
* remounts (route changes) without leaking into the component tree. */
|
|
1362
|
+
const archivedUiState = { page: 0 }
|
|
1363
|
+
|
|
1364
|
+
/** First usable summary string from common tool input fields. */
|
|
1365
|
+
function summarizeToolInput(input: Record<string, unknown> | undefined): string {
|
|
1366
|
+
if (!input) return ''
|
|
1367
|
+
const candidates = ['command', 'description', 'prompt', 'filePath', 'pattern', 'url', 'query']
|
|
1368
|
+
for (const key of candidates) {
|
|
1369
|
+
const value = input[key]
|
|
1370
|
+
if (typeof value === 'string' && value.trim() !== '') return value
|
|
1371
|
+
}
|
|
1372
|
+
return ''
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** Reduce one message.part.updated tool part to displayable activity.
|
|
1376
|
+
* Returns null for non-tool parts, completed/error parts (no live activity)
|
|
1377
|
+
* or parts without a session id. Pure. */
|
|
1378
|
+
export function extractToolActivity(
|
|
1379
|
+
part: {
|
|
1380
|
+
type?: string
|
|
1381
|
+
tool?: string
|
|
1382
|
+
sessionID?: string
|
|
1383
|
+
state?: { status?: string; input?: Record<string, unknown> }
|
|
1384
|
+
},
|
|
1385
|
+
now = Date.now(),
|
|
1386
|
+
): { sessionID: string; activity: ToolActivity } | null {
|
|
1387
|
+
if (part?.type !== 'tool') return null
|
|
1388
|
+
const status = part.state?.status
|
|
1389
|
+
if (status !== 'pending' && status !== 'running') return null
|
|
1390
|
+
const sessionID = typeof part.sessionID === 'string' ? part.sessionID : ''
|
|
1391
|
+
if (sessionID === '') return null
|
|
1392
|
+
const tool = typeof part.tool === 'string' && part.tool !== '' ? part.tool : 'tool'
|
|
1393
|
+
const summary = summarizeToolInput(part.state?.input).replace(/\s+/g, ' ').trim().slice(0, 48)
|
|
1394
|
+
return { sessionID, activity: { tool, summary, at: now } }
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** Record an activity sample (bounded map, oldest dropped). */
|
|
1398
|
+
export function trackToolActivity(
|
|
1399
|
+
map: Map<string, ToolActivity>,
|
|
1400
|
+
sessionID: string,
|
|
1401
|
+
activity: ToolActivity,
|
|
1402
|
+
): void {
|
|
1403
|
+
if (sessionID === '') return
|
|
1404
|
+
if (map.size >= MAX_TOOL_ACTIVITY_ENTRIES) {
|
|
1405
|
+
const oldest = map.keys().next().value
|
|
1406
|
+
if (oldest !== undefined) map.delete(oldest)
|
|
1407
|
+
}
|
|
1408
|
+
map.set(sessionID, activity)
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/** Latest live activity for a child session, or null when absent/stale. */
|
|
1412
|
+
export function latestToolActivityFor(
|
|
1413
|
+
map: Map<string, ToolActivity>,
|
|
1414
|
+
sessionID: string | undefined,
|
|
1415
|
+
now = Date.now(),
|
|
1416
|
+
ttlMs = TOOL_ACTIVITY_TTL_MS,
|
|
1417
|
+
): ToolActivity | null {
|
|
1418
|
+
if (!sessionID) return null
|
|
1419
|
+
const hit = map.get(sessionID)
|
|
1420
|
+
if (hit === undefined) return null
|
|
1421
|
+
if (now - hit.at > ttlMs) return null
|
|
1422
|
+
return hit
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1321
1425
|
/** Merge the immediate tool-event channel into the child-session channel.
|
|
1322
1426
|
*
|
|
1323
1427
|
* Children remain the durable source, while live entries make a delegation
|
|
@@ -1880,9 +1984,10 @@ export type ChildDelegationLike = {
|
|
|
1880
1984
|
* (the child is actively working), idle → completed, unknown → running
|
|
1881
1985
|
* (fail-open: a freshly-seen child is assumed active; the 1s poll + md
|
|
1882
1986
|
* correct it as soon as terminal data exists). */
|
|
1883
|
-
export function childStatusToState(status: string | undefined): 'running' | 'completed' {
|
|
1987
|
+
export function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry' {
|
|
1884
1988
|
if (status === 'idle') return 'completed'
|
|
1885
|
-
|
|
1989
|
+
if (status === 'retry') return 'retry'
|
|
1990
|
+
return 'running' // busy or unknown
|
|
1886
1991
|
}
|
|
1887
1992
|
|
|
1888
1993
|
/** Row tag for a delegation entry: `[native]` for native task() children
|
|
@@ -2070,6 +2175,8 @@ function DelegationRow(props: {
|
|
|
2070
2175
|
switch (props.job.state) {
|
|
2071
2176
|
case 'running':
|
|
2072
2177
|
return t.warning
|
|
2178
|
+
case 'retry':
|
|
2179
|
+
return t.warning
|
|
2073
2180
|
case 'stale-running':
|
|
2074
2181
|
return t.error // warning color for stale delegations
|
|
2075
2182
|
case 'completed':
|
|
@@ -2083,6 +2190,7 @@ function DelegationRow(props: {
|
|
|
2083
2190
|
|
|
2084
2191
|
const marker = createMemo(() => {
|
|
2085
2192
|
if (props.job.state === 'running') return `${delegationSpinnerFrame(props.animationNow)} `
|
|
2193
|
+
if (props.job.state === 'retry') return '\u27f3 ' // ⟳ retrying
|
|
2086
2194
|
if (props.job.state === 'stale-running') return '\u26a0 ' // warning triangle
|
|
2087
2195
|
switch (props.job.state) {
|
|
2088
2196
|
case 'completed':
|
|
@@ -2106,6 +2214,12 @@ function DelegationRow(props: {
|
|
|
2106
2214
|
return line.length > 180 ? `${line.slice(0, 177)}\u2026` : line
|
|
2107
2215
|
})
|
|
2108
2216
|
|
|
2217
|
+
// beta.6: live tool call the child is running RIGHT NOW ("↳ bash npm test"),
|
|
2218
|
+
// fed by message.part.updated activity tracking. Null while idle/done.
|
|
2219
|
+
const activity = createMemo(() =>
|
|
2220
|
+
latestToolActivityFor(latestToolActivity, props.job.taskID, props.now),
|
|
2221
|
+
)
|
|
2222
|
+
|
|
2109
2223
|
// Click a row → navigate to the child session (route API guarded in the
|
|
2110
2224
|
// helper). Enter-key binding is left out (per-row keymap wiring is not
|
|
2111
2225
|
// worth the complexity in this sidebar); mouse is enabled in tui.json.
|
|
@@ -2123,6 +2237,13 @@ function DelegationRow(props: {
|
|
|
2123
2237
|
>{` ${props.job.agent} \u2014 ${delegationActivityLabel(props.job)}`}</text>
|
|
2124
2238
|
</box>
|
|
2125
2239
|
<text fg={theme().textMuted}>{detail()}</text>
|
|
2240
|
+
<Show when={activity()}>
|
|
2241
|
+
{(a) => (
|
|
2242
|
+
<text
|
|
2243
|
+
fg={theme().textMuted}
|
|
2244
|
+
>{` \u21b3 ${a().tool}${a().summary !== '' ? ' ' + a().summary : ''}`}</text>
|
|
2245
|
+
)}
|
|
2246
|
+
</Show>
|
|
2126
2247
|
</box>
|
|
2127
2248
|
)
|
|
2128
2249
|
}
|
|
@@ -2335,9 +2456,20 @@ function View(props: {
|
|
|
2335
2456
|
})()
|
|
2336
2457
|
return delegationsInflight
|
|
2337
2458
|
}
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
|
|
2459
|
+
// beta.6: active (running/retry) first, then the 8 most recent terminal
|
|
2460
|
+
// reports, then the archived tail (paginated). Pagination state lives at
|
|
2461
|
+
// module level so sidebar remounts keep the user's page.
|
|
2462
|
+
const delegationSplit = createMemo(() => splitDelegationList(childDelegations()))
|
|
2463
|
+
const visibleDelegations = createMemo(() => [
|
|
2464
|
+
...delegationSplit().active,
|
|
2465
|
+
...delegationSplit().recent,
|
|
2466
|
+
])
|
|
2467
|
+
const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page)
|
|
2468
|
+
const clampArchivedPage = (page: number, pages: number) => {
|
|
2469
|
+
const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)))
|
|
2470
|
+
archivedUiState.page = clamped
|
|
2471
|
+
setArchivedPage(clamped)
|
|
2472
|
+
}
|
|
2341
2473
|
|
|
2342
2474
|
// ── Event subscriptions for live session/delegation updates ──
|
|
2343
2475
|
onMount(() => {
|
|
@@ -2504,6 +2636,41 @@ function View(props: {
|
|
|
2504
2636
|
/>
|
|
2505
2637
|
)}
|
|
2506
2638
|
</For>
|
|
2639
|
+
<Show when={delegationSplit().archived.length > 0}>
|
|
2640
|
+
{(() => {
|
|
2641
|
+
const archived = delegationSplit().archived
|
|
2642
|
+
const pageSize = 8
|
|
2643
|
+
const pages = Math.ceil(archived.length / pageSize)
|
|
2644
|
+
const page = Math.min(archivedPage(), pages - 1)
|
|
2645
|
+
const slice = archived.slice(page * pageSize, page * pageSize + pageSize)
|
|
2646
|
+
return (
|
|
2647
|
+
<box flexDirection="column">
|
|
2648
|
+
<box flexDirection="row" marginLeft={0}>
|
|
2649
|
+
<text
|
|
2650
|
+
fg={theme().textMuted}
|
|
2651
|
+
onMouseDown={() => clampArchivedPage(page - 1, pages)}
|
|
2652
|
+
>{`\u2039 `}</text>
|
|
2653
|
+
<text fg={theme().textMuted}>{`Archived (${archived.length}) `}</text>
|
|
2654
|
+
<text fg={theme().textMuted}>{`${page + 1}/${pages}`}</text>
|
|
2655
|
+
<text
|
|
2656
|
+
fg={theme().textMuted}
|
|
2657
|
+
onMouseDown={() => clampArchivedPage(page + 1, pages)}
|
|
2658
|
+
>{` \u203a`}</text>
|
|
2659
|
+
</box>
|
|
2660
|
+
<For each={slice}>
|
|
2661
|
+
{(job) => (
|
|
2662
|
+
<DelegationRow
|
|
2663
|
+
api={props.api}
|
|
2664
|
+
job={job}
|
|
2665
|
+
now={now()}
|
|
2666
|
+
animationNow={animationNow()}
|
|
2667
|
+
/>
|
|
2668
|
+
)}
|
|
2669
|
+
</For>
|
|
2670
|
+
</box>
|
|
2671
|
+
)
|
|
2672
|
+
})()}
|
|
2673
|
+
</Show>
|
|
2507
2674
|
</box>
|
|
2508
2675
|
</Show>
|
|
2509
2676
|
</Show>
|
|
@@ -2552,6 +2719,11 @@ const tui: TuiPlugin = (api, _options, _meta) => {
|
|
|
2552
2719
|
const props = (event?.properties ?? {}) as { part?: unknown; info?: { part?: unknown } }
|
|
2553
2720
|
const part = (props.part ?? props.info?.part) as DelegationToolPart | undefined
|
|
2554
2721
|
if (part === undefined) return
|
|
2722
|
+
// beta.6: track the live tool call per child session (panel's
|
|
2723
|
+
// activity line). Fire-and-forget — display-only state.
|
|
2724
|
+
const sample = extractToolActivity(part as any)
|
|
2725
|
+
if (sample !== null)
|
|
2726
|
+
trackToolActivity(latestToolActivity, sample.sessionID, sample.activity)
|
|
2555
2727
|
if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump()
|
|
2556
2728
|
}),
|
|
2557
2729
|
)
|