pantheon-opencode 1.4.3 → 1.5.0-beta.10
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/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/manifest.json +21 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/CHANGELOG.md +1026 -0
- package/README.md +292 -1080
- package/README.pt-BR.md +308 -0
- package/bin/pantheon-init.mjs +207 -31
- package/opencode.json +1 -0
- package/package.json +34 -14
- package/plugin.json +15 -0
- package/pyproject.toml +65 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +451 -44
- package/scripts/cost.mjs +30 -14
- package/scripts/doctor.mjs +306 -129
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +23 -10
- package/scripts/install/config-migration.mjs +399 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +396 -121
- package/scripts/install/plugin.mjs +89 -49
- package/scripts/install/shared.mjs +55 -159
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +45 -232
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +798 -60
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +313 -0
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +135 -42
- package/scripts/test-opencode-v1-v2-sandbox.sh +862 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +27 -16
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +451 -44
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +798 -60
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +110 -2
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +356 -69
- package/src/pantheon/delegate-manager.ts +766 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +13 -12
- package/src/pantheon/delegation-finalize.ts +406 -0
- package/src/pantheon/delegation-notify.ts +212 -0
- package/src/pantheon/delegation-toast.ts +73 -0
- package/src/pantheon/delegation.ts +1337 -0
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/goal-loop.ts +2 -2
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/hashline/tool.ts +2 -2
- package/src/pantheon/model-command.ts +2 -1
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +689 -0
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +148 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +824 -0
- package/src/plugin.ts +261 -51
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/tui.d.ts +116 -24
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +416 -149
- package/src/plugins/tui/dist/tui.tsx +456 -87
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +462 -95
- package/src/skills/README.md +0 -2
- package/src/pantheon/tool-context.ts +0 -41
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
import { Buffer } from 'node:buffer'
|
|
49
49
|
import type { Dirent } from 'node:fs'
|
|
50
|
-
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
50
|
+
import { appendFile, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
51
51
|
import { homedir } from 'node:os'
|
|
52
52
|
import { dirname, join } from 'node:path'
|
|
53
53
|
import { fileURLToPath } from 'node:url'
|
|
@@ -892,18 +892,18 @@ async function setupUsageBar(api: TuiPluginApi) {
|
|
|
892
892
|
* (`client.session.children` + `session.parentID === current`).
|
|
893
893
|
* Live status comes from `api.state.session.status(childID)`
|
|
894
894
|
* (busy/retry → running; idle → terminal). This does NOT depend on
|
|
895
|
-
*
|
|
896
|
-
*
|
|
895
|
+
* tool-part events, which runtime 1.18.13 may not deliver for
|
|
896
|
+
* pantheon_delegate (the "(0) even while delegating" symptom).
|
|
897
897
|
*
|
|
898
898
|
* 1b. NATIVE task() CHILDREN — `session.children` returns ALL children,
|
|
899
899
|
* and the native `task()` tool ALSO spawns a child session with
|
|
900
900
|
* parentID = caller. A child WITHOUT a board report is a native
|
|
901
901
|
* task() child: childrenToDelegationEntries tags it source
|
|
902
|
-
* 'children-only' with the `
|
|
902
|
+
* 'children-only' with the `nat:` row tag (distinct info color),
|
|
903
903
|
* so native task() work is visible in the panel alongside board
|
|
904
|
-
* delegates (
|
|
905
|
-
* board row for the same child (a child WITH a report keeps the
|
|
906
|
-
* entry, source 'md').
|
|
904
|
+
* delegates (`pan:apo-1`) — never filtered out, never duplicated
|
|
905
|
+
* with a board row for the same child (a child WITH a report keeps the
|
|
906
|
+
* md entry, source 'md').
|
|
907
907
|
*
|
|
908
908
|
* 2. ENRICHMENT + HISTORY — markdown reports the job board persists under
|
|
909
909
|
* `.pantheon/delegations/<sessionID>/<alias>.md` (written by
|
|
@@ -921,9 +921,12 @@ async function setupUsageBar(api: TuiPluginApi) {
|
|
|
921
921
|
* children; a 1s safety poll (oh-my-opencode-slim pattern) re-fetches
|
|
922
922
|
* children + re-reads the md so the panel updates EVEN without events.
|
|
923
923
|
*
|
|
924
|
-
* 4. DIAGNOSTICS — every re-fetch logs
|
|
924
|
+
* 4. DIAGNOSTICS — every re-fetch logs
|
|
925
|
+
* "panel: children=N(pantheon=M native=K) md=N events=N"
|
|
925
926
|
* to `.pantheon/logs/hooks.log` (silence-by-default, createPantheonLogger
|
|
926
|
-
* policy) so the (0) symptom is diagnosable from disk
|
|
927
|
+
* policy) so the (0) symptom is diagnosable from disk: children>0 means
|
|
928
|
+
* the source works, pantheon/native splits board vs task() children.
|
|
929
|
+
* A children fetch
|
|
927
930
|
* failure logs "panel: error <msg>" (previously silenced), and a missing
|
|
928
931
|
* current session (null/placeholder sessionID) logs "panel: children=0
|
|
929
932
|
* md=N ... (no sessionID — history shown)" — the md HISTORY renders
|
|
@@ -959,6 +962,7 @@ export type DelegationEntry = {
|
|
|
959
962
|
agent: string
|
|
960
963
|
state:
|
|
961
964
|
| 'running'
|
|
965
|
+
| 'retry'
|
|
962
966
|
| 'completed'
|
|
963
967
|
| 'error'
|
|
964
968
|
| 'startup_failed'
|
|
@@ -973,9 +977,13 @@ export type DelegationEntry = {
|
|
|
973
977
|
description: string
|
|
974
978
|
/** True while the panel is waiting for pantheon_delegation_read. */
|
|
975
979
|
read?: boolean
|
|
980
|
+
// NOTE: the nested/tree view is BACKLOG. `session.children` returns direct
|
|
981
|
+
// children only, so no real grandchild rows exist yet; the parentTaskID /
|
|
982
|
+
// childCount fields + tree-connector helpers were removed as dead code.
|
|
983
|
+
// Reintroduce them once nested rows are actually produced.
|
|
976
984
|
/** Internal provenance used to keep a finalized md report authoritative.
|
|
977
985
|
* 'children-only' = a native task() child session with NO board report
|
|
978
|
-
* (rendered with the distinct `
|
|
986
|
+
* (rendered with the distinct `nat:` prefix); 'md' = board report wins. */
|
|
979
987
|
source?: 'child' | 'live' | 'md' | 'children-only'
|
|
980
988
|
}
|
|
981
989
|
|
|
@@ -1195,27 +1203,52 @@ export function tuiLogPath(projectRoot: string): string {
|
|
|
1195
1203
|
* falling back to startedAt, descending). Shared by the md reader and
|
|
1196
1204
|
* mergeDelegationSources. */
|
|
1197
1205
|
export function compareDelegationEntries(a: DelegationEntry, b: DelegationEntry): number {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1206
|
+
const isActive = (st: DelegationEntry['state']) =>
|
|
1207
|
+
st === 'running' || st === 'retry' || st === 'stale-running' ? 1 : 0
|
|
1208
|
+
const aRun = isActive(a.state)
|
|
1209
|
+
const bRun = isActive(b.state)
|
|
1200
1210
|
if (aRun !== bRun) return bRun - aRun
|
|
1201
1211
|
return (b.updatedAt ?? b.startedAt) - (a.updatedAt ?? a.startedAt)
|
|
1202
1212
|
}
|
|
1203
1213
|
|
|
1204
|
-
/**
|
|
1205
|
-
* recent terminal reports
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1214
|
+
/** Split the panel list: active jobs (running/retry, stale-marked) first,
|
|
1215
|
+
* then the most recent terminal reports, then the archived tail (paginated
|
|
1216
|
+
* in the View). Pure — so the history-only panel (no sessionID) is testable
|
|
1217
|
+
* without the TUI runtime. */
|
|
1218
|
+
export function splitDelegationList(
|
|
1219
|
+
all: readonly DelegationEntry[],
|
|
1220
|
+
maxRecent = 8,
|
|
1221
|
+
now = Date.now(),
|
|
1222
|
+
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1223
|
+
): { active: DelegationEntry[]; recent: DelegationEntry[]; archived: DelegationEntry[] } {
|
|
1224
|
+
// Active = running OR retry OR the display-only stale-running. A stale row
|
|
1225
|
+
// is still a live job (backend state unchanged), so it must never fall into
|
|
1226
|
+
// the terminal/archived tail — archiving a running delegation hid the row
|
|
1227
|
+
// the user launched.
|
|
1228
|
+
const isActiveState = (st: DelegationEntry['state']): boolean =>
|
|
1229
|
+
st === 'running' || st === 'retry' || st === 'stale-running'
|
|
1230
|
+
const active = all
|
|
1231
|
+
.filter((d) => isActiveState(d.state))
|
|
1232
|
+
.map((d) => markStaleIfRunning(d, now, staleThresholdMs))
|
|
1233
|
+
const terminal = all.filter((d) => !isActiveState(d.state))
|
|
1234
|
+
return {
|
|
1235
|
+
active,
|
|
1236
|
+
recent: terminal.slice(0, maxRecent),
|
|
1237
|
+
archived: terminal.slice(maxRecent),
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** The list the panel actually renders (kept for the header count and
|
|
1242
|
+
* existing tests): active jobs first, then the most recent terminal
|
|
1243
|
+
* reports (capped) — the archived tail is rendered separately. Pure. */
|
|
1208
1244
|
export function visibleDelegationList(
|
|
1209
1245
|
all: readonly DelegationEntry[],
|
|
1210
1246
|
maxTerminal = 8,
|
|
1211
1247
|
now = Date.now(),
|
|
1212
1248
|
staleThresholdMs = 30 * 60 * 1000, // 30 minutes
|
|
1213
1249
|
): 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]
|
|
1250
|
+
const { active, recent } = splitDelegationList(all, maxTerminal, now, staleThresholdMs)
|
|
1251
|
+
return [...active, ...recent]
|
|
1219
1252
|
}
|
|
1220
1253
|
|
|
1221
1254
|
/** Default stale-running threshold: 30 minutes. */
|
|
@@ -1261,10 +1294,11 @@ export function fmtElapsed(ms: number): string {
|
|
|
1261
1294
|
return `${seconds}s`
|
|
1262
1295
|
}
|
|
1263
1296
|
|
|
1264
|
-
/** Elapsed label for one entry:
|
|
1265
|
-
*
|
|
1297
|
+
/** Elapsed label for one entry: an ACTIVE entry (running/retry/stale-running)
|
|
1298
|
+
* ticks `now - startedAt`; a terminal one is fixed at
|
|
1299
|
+
* `updatedAt - startedAt` (em dash when no finalized timestamp). */
|
|
1266
1300
|
export function delegationElapsed(entry: DelegationEntry, now: number): string {
|
|
1267
|
-
if (entry.state === 'running' || entry.state === 'stale-running')
|
|
1301
|
+
if (entry.state === 'running' || entry.state === 'retry' || entry.state === 'stale-running')
|
|
1268
1302
|
return fmtElapsed(now - entry.startedAt)
|
|
1269
1303
|
return entry.updatedAt !== null ? fmtElapsed(entry.updatedAt - entry.startedAt) : '\u2014'
|
|
1270
1304
|
}
|
|
@@ -1294,6 +1328,7 @@ export function delegationActivity(entry: DelegationEntry): DelegationActivity {
|
|
|
1294
1328
|
}
|
|
1295
1329
|
|
|
1296
1330
|
export function delegationActivityLabel(entry: DelegationEntry): string {
|
|
1331
|
+
if (entry.state === 'retry') return 'RETRYING'
|
|
1297
1332
|
switch (delegationActivity(entry)) {
|
|
1298
1333
|
case 'delegating':
|
|
1299
1334
|
return 'DELEGATING'
|
|
@@ -1312,12 +1347,96 @@ export function delegationActivityLabel(entry: DelegationEntry): string {
|
|
|
1312
1347
|
|
|
1313
1348
|
const DELEGATION_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
1314
1349
|
|
|
1315
|
-
/** Return a deterministic spinner frame. The View ticks this every
|
|
1350
|
+
/** Return a deterministic spinner frame. The View ticks this every 1000ms
|
|
1351
|
+
* (not 140ms — the fast tick flickered without adding information). */
|
|
1316
1352
|
export function delegationSpinnerFrame(now: number): string {
|
|
1317
|
-
const index = Math.floor(Math.max(0, now) /
|
|
1353
|
+
const index = Math.floor(Math.max(0, now) / 1_000) % DELEGATION_SPINNER_FRAMES.length
|
|
1318
1354
|
return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0]
|
|
1319
1355
|
}
|
|
1320
1356
|
|
|
1357
|
+
/* ─── Live tool activity per child session (beta.6) ────────────────────────
|
|
1358
|
+
* The delegation panel's activity line ("↳ bash npm test") — what the agent
|
|
1359
|
+
* is doing RIGHT NOW. Sourced from message.part.updated tool parts: every
|
|
1360
|
+
* pending/running tool part updates a per-child-session record (latest wins).
|
|
1361
|
+
* Module-level by design: the sidebar remounts on route changes and the
|
|
1362
|
+
* "what is each agent doing" state should survive that (OMO pattern). */
|
|
1363
|
+
|
|
1364
|
+
export type ToolActivity = {
|
|
1365
|
+
tool: string
|
|
1366
|
+
summary: string
|
|
1367
|
+
at: number
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/** Bounded tracker: child session id → latest running tool call. */
|
|
1371
|
+
const latestToolActivity = new Map<string, ToolActivity>()
|
|
1372
|
+
const MAX_TOOL_ACTIVITY_ENTRIES = 200
|
|
1373
|
+
const TOOL_ACTIVITY_TTL_MS = 5 * 60 * 1000
|
|
1374
|
+
|
|
1375
|
+
/** Archived-section pagination state — module-level so it survives sidebar
|
|
1376
|
+
* remounts (route changes) without leaking into the component tree. */
|
|
1377
|
+
const archivedUiState = { page: 0 }
|
|
1378
|
+
|
|
1379
|
+
/** First usable summary string from common tool input fields. */
|
|
1380
|
+
function summarizeToolInput(input: Record<string, unknown> | undefined): string {
|
|
1381
|
+
if (!input) return ''
|
|
1382
|
+
const candidates = ['command', 'description', 'prompt', 'filePath', 'pattern', 'url', 'query']
|
|
1383
|
+
for (const key of candidates) {
|
|
1384
|
+
const value = input[key]
|
|
1385
|
+
if (typeof value === 'string' && value.trim() !== '') return value
|
|
1386
|
+
}
|
|
1387
|
+
return ''
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/** Reduce one message.part.updated tool part to displayable activity.
|
|
1391
|
+
* Returns null for non-tool parts, completed/error parts (no live activity)
|
|
1392
|
+
* or parts without a session id. Pure. */
|
|
1393
|
+
export function extractToolActivity(
|
|
1394
|
+
part: {
|
|
1395
|
+
type?: string
|
|
1396
|
+
tool?: string
|
|
1397
|
+
sessionID?: string
|
|
1398
|
+
state?: { status?: string; input?: Record<string, unknown> }
|
|
1399
|
+
},
|
|
1400
|
+
now = Date.now(),
|
|
1401
|
+
): { sessionID: string; activity: ToolActivity } | null {
|
|
1402
|
+
if (part?.type !== 'tool') return null
|
|
1403
|
+
const status = part.state?.status
|
|
1404
|
+
if (status !== 'pending' && status !== 'running') return null
|
|
1405
|
+
const sessionID = typeof part.sessionID === 'string' ? part.sessionID : ''
|
|
1406
|
+
if (sessionID === '') return null
|
|
1407
|
+
const tool = typeof part.tool === 'string' && part.tool !== '' ? part.tool : 'tool'
|
|
1408
|
+
const summary = summarizeToolInput(part.state?.input).replace(/\s+/g, ' ').trim().slice(0, 48)
|
|
1409
|
+
return { sessionID, activity: { tool, summary, at: now } }
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** Record an activity sample (bounded map, oldest dropped). */
|
|
1413
|
+
export function trackToolActivity(
|
|
1414
|
+
map: Map<string, ToolActivity>,
|
|
1415
|
+
sessionID: string,
|
|
1416
|
+
activity: ToolActivity,
|
|
1417
|
+
): void {
|
|
1418
|
+
if (sessionID === '') return
|
|
1419
|
+
if (map.size >= MAX_TOOL_ACTIVITY_ENTRIES) {
|
|
1420
|
+
const oldest = map.keys().next().value
|
|
1421
|
+
if (oldest !== undefined) map.delete(oldest)
|
|
1422
|
+
}
|
|
1423
|
+
map.set(sessionID, activity)
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/** Latest live activity for a child session, or null when absent/stale. */
|
|
1427
|
+
export function latestToolActivityFor(
|
|
1428
|
+
map: Map<string, ToolActivity>,
|
|
1429
|
+
sessionID: string | undefined,
|
|
1430
|
+
now = Date.now(),
|
|
1431
|
+
ttlMs = TOOL_ACTIVITY_TTL_MS,
|
|
1432
|
+
): ToolActivity | null {
|
|
1433
|
+
if (!sessionID) return null
|
|
1434
|
+
const hit = map.get(sessionID)
|
|
1435
|
+
if (hit === undefined) return null
|
|
1436
|
+
if (now - hit.at > ttlMs) return null
|
|
1437
|
+
return hit
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1321
1440
|
/** Merge the immediate tool-event channel into the child-session channel.
|
|
1322
1441
|
*
|
|
1323
1442
|
* Children remain the durable source, while live entries make a delegation
|
|
@@ -1405,9 +1524,11 @@ export function mergeChildDelegationSources(
|
|
|
1405
1524
|
* Source of truth for jobs born while opencode is open. The TUI event bus
|
|
1406
1525
|
* (api.event.on) delivers `message.part.updated` for every part change;
|
|
1407
1526
|
* we only care about tool parts whose `tool` is pantheon_delegate (job
|
|
1408
|
-
* launch)
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1527
|
+
* launch), the native `task` subagent tool (same parentID === caller
|
|
1528
|
+
* mechanism — its rows render `nat:`), or pantheon_delegation_read
|
|
1529
|
+
* (blocking until terminal → closes the entry). Shape-adapted from the
|
|
1530
|
+
* SDK v2 ToolPart but duck-typed so the pure helpers are testable without
|
|
1531
|
+
* the SDK. */
|
|
1411
1532
|
|
|
1412
1533
|
/** Duck-typed subset of a tool part (SDK v2 `ToolPart` / `ToolState`). */
|
|
1413
1534
|
export type DelegationToolPart = {
|
|
@@ -1433,7 +1554,7 @@ export type LiveDelegationEntry = {
|
|
|
1433
1554
|
partID: string
|
|
1434
1555
|
/** Parent session the delegation was launched from. */
|
|
1435
1556
|
sessionID: string
|
|
1436
|
-
tool: 'pantheon_delegate' | 'pantheon_delegation_read'
|
|
1557
|
+
tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task'
|
|
1437
1558
|
/** Agent name (from the delegate input args). */
|
|
1438
1559
|
agent: string
|
|
1439
1560
|
description: string
|
|
@@ -1453,7 +1574,7 @@ export type ParsedDelegationToolPart = {
|
|
|
1453
1574
|
callID: string
|
|
1454
1575
|
partID: string
|
|
1455
1576
|
sessionID: string
|
|
1456
|
-
tool: 'pantheon_delegate' | 'pantheon_delegation_read'
|
|
1577
|
+
tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task'
|
|
1457
1578
|
/** null for read parts (no agent arg — the id targets an existing job). */
|
|
1458
1579
|
agent: string | null
|
|
1459
1580
|
description: string
|
|
@@ -1473,13 +1594,20 @@ const READ_ALIAS_PATTERN = /^[a-z]{2,8}-\d+$/i
|
|
|
1473
1594
|
|
|
1474
1595
|
/** Extract the tool name + args from a `message.part.updated` part and
|
|
1475
1596
|
* reduce it to what the panel needs. Returns null for anything that is
|
|
1476
|
-
* not a pantheon delegation tool part
|
|
1597
|
+
* not a pantheon delegation tool part, the native `task` subagent tool
|
|
1598
|
+
* (same parentID === caller mechanism — its children render `nat:`),
|
|
1599
|
+
* or is missing its callID. */
|
|
1477
1600
|
export function parseDelegationToolPart(
|
|
1478
1601
|
part: DelegationToolPart,
|
|
1479
1602
|
now = Date.now(),
|
|
1480
1603
|
): ParsedDelegationToolPart | null {
|
|
1481
1604
|
if (part.type !== 'tool') return null
|
|
1482
|
-
if (
|
|
1605
|
+
if (
|
|
1606
|
+
part.tool !== 'pantheon_delegate' &&
|
|
1607
|
+
part.tool !== 'pantheon_delegation_read' &&
|
|
1608
|
+
part.tool !== 'task'
|
|
1609
|
+
)
|
|
1610
|
+
return null
|
|
1483
1611
|
const callID = part.callID
|
|
1484
1612
|
if (callID === undefined || callID === '') return null
|
|
1485
1613
|
const sessionID = part.sessionID ?? ''
|
|
@@ -1514,10 +1642,22 @@ export function parseDelegationToolPart(
|
|
|
1514
1642
|
}
|
|
1515
1643
|
}
|
|
1516
1644
|
|
|
1517
|
-
const agent =
|
|
1518
|
-
|
|
1645
|
+
const agent =
|
|
1646
|
+
typeof input.agent === 'string'
|
|
1647
|
+
? input.agent
|
|
1648
|
+
: typeof input.subagent_type === 'string'
|
|
1649
|
+
? (input.subagent_type as string)
|
|
1650
|
+
: 'agent'
|
|
1651
|
+
const description =
|
|
1652
|
+
typeof input.description === 'string'
|
|
1653
|
+
? input.description
|
|
1654
|
+
: typeof input.prompt === 'string'
|
|
1655
|
+
? (input.prompt as string).slice(0, 120)
|
|
1656
|
+
: ''
|
|
1519
1657
|
// The alias/taskID only exist once the delegate tool COMPLETES (they are
|
|
1520
|
-
// returned in its output); a running/pending part has neither.
|
|
1658
|
+
// returned in its output); a running/pending part has neither. Native
|
|
1659
|
+
// `task` parts never carry a board alias — the children channel supplies
|
|
1660
|
+
// the child session id, so the row renders `nat:`.
|
|
1521
1661
|
let alias: string | null = null
|
|
1522
1662
|
let taskID: string | null = null
|
|
1523
1663
|
if (status === 'completed') {
|
|
@@ -1529,7 +1669,7 @@ export function parseDelegationToolPart(
|
|
|
1529
1669
|
callID,
|
|
1530
1670
|
partID: part.id ?? '',
|
|
1531
1671
|
sessionID,
|
|
1532
|
-
tool: 'pantheon_delegate',
|
|
1672
|
+
tool: part.tool === 'task' ? 'task' : 'pantheon_delegate',
|
|
1533
1673
|
agent,
|
|
1534
1674
|
description,
|
|
1535
1675
|
status,
|
|
@@ -1587,9 +1727,11 @@ export function reduceDelegationToolPart(
|
|
|
1587
1727
|
return changed
|
|
1588
1728
|
}
|
|
1589
1729
|
|
|
1590
|
-
// Delegate tool: pending/running →
|
|
1591
|
-
// delegate tool only means the job was
|
|
1592
|
-
// we pick up alias + taskID from the
|
|
1730
|
+
// Delegate tool (pantheon_delegate AND native task): pending/running →
|
|
1731
|
+
// job launched (running). A COMPLETED delegate tool only means the job was
|
|
1732
|
+
// registered — it stays running and we pick up alias + taskID from the
|
|
1733
|
+
// output. Native `task` parts never yield a board alias, so their rows
|
|
1734
|
+
// stay `nat:` until the children channel resolves them.
|
|
1593
1735
|
const existing = map.get(parsed.callID)
|
|
1594
1736
|
if (parsed.status === 'error') {
|
|
1595
1737
|
if (existing !== undefined && existing.state === 'error' && existing.updatedAt === parsed.endAt)
|
|
@@ -1598,7 +1740,7 @@ export function reduceDelegationToolPart(
|
|
|
1598
1740
|
callID: parsed.callID,
|
|
1599
1741
|
partID: parsed.partID,
|
|
1600
1742
|
sessionID: parsed.sessionID,
|
|
1601
|
-
tool:
|
|
1743
|
+
tool: parsed.tool,
|
|
1602
1744
|
agent: parsed.agent ?? 'agent',
|
|
1603
1745
|
description: parsed.description,
|
|
1604
1746
|
alias: existing?.alias ?? null,
|
|
@@ -1615,7 +1757,7 @@ export function reduceDelegationToolPart(
|
|
|
1615
1757
|
callID: parsed.callID,
|
|
1616
1758
|
partID: parsed.partID,
|
|
1617
1759
|
sessionID: parsed.sessionID,
|
|
1618
|
-
tool:
|
|
1760
|
+
tool: parsed.tool,
|
|
1619
1761
|
agent: parsed.agent ?? 'agent',
|
|
1620
1762
|
description: parsed.description,
|
|
1621
1763
|
alias: parsed.alias,
|
|
@@ -1668,11 +1810,14 @@ export function removeDelegationEntry(
|
|
|
1668
1810
|
return false
|
|
1669
1811
|
}
|
|
1670
1812
|
|
|
1671
|
-
/** Collect pantheon delegation tool parts from a session's messages.
|
|
1813
|
+
/** Collect pantheon delegation + native task tool parts from a session's messages.
|
|
1672
1814
|
* Messages may carry their parts inline (duck-typed `msg.parts`); when
|
|
1673
1815
|
* they don't, the optional `getParts(messageID)` callback is used (the TUI
|
|
1674
|
-
* SDK exposes `api.state.part(messageID)`).
|
|
1675
|
-
*
|
|
1816
|
+
* SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
|
|
1817
|
+
* child session with parentID = caller — the same mechanism as
|
|
1818
|
+
* pantheon_delegate — so its parts feed the live-map as the native signal
|
|
1819
|
+
* (rows render `nat:` via the children channel). Pure w.r.t. I/O — used
|
|
1820
|
+
* by the mount re-scan to re-seed the live map after compaction/attach. */
|
|
1676
1821
|
export function collectDelegationToolParts(
|
|
1677
1822
|
messages: readonly { id?: string; parts?: unknown[] }[] | undefined,
|
|
1678
1823
|
getParts?: (messageID: string) => readonly unknown[] | undefined,
|
|
@@ -1690,7 +1835,9 @@ export function collectDelegationToolParts(
|
|
|
1690
1835
|
const part = raw as DelegationToolPart
|
|
1691
1836
|
if (
|
|
1692
1837
|
part?.type === 'tool' &&
|
|
1693
|
-
(part.tool === 'pantheon_delegate' ||
|
|
1838
|
+
(part.tool === 'pantheon_delegate' ||
|
|
1839
|
+
part.tool === 'pantheon_delegation_read' ||
|
|
1840
|
+
part.tool === 'task')
|
|
1694
1841
|
) {
|
|
1695
1842
|
out.push(part)
|
|
1696
1843
|
}
|
|
@@ -1880,17 +2027,139 @@ export type ChildDelegationLike = {
|
|
|
1880
2027
|
* (the child is actively working), idle → completed, unknown → running
|
|
1881
2028
|
* (fail-open: a freshly-seen child is assumed active; the 1s poll + md
|
|
1882
2029
|
* correct it as soon as terminal data exists). */
|
|
1883
|
-
export function childStatusToState(status: string | undefined): 'running' | 'completed' {
|
|
2030
|
+
export function childStatusToState(status: string | undefined): 'running' | 'completed' | 'retry' {
|
|
1884
2031
|
if (status === 'idle') return 'completed'
|
|
1885
|
-
|
|
2032
|
+
if (status === 'retry') return 'retry'
|
|
2033
|
+
return 'running' // busy or unknown
|
|
1886
2034
|
}
|
|
1887
2035
|
|
|
1888
|
-
/**
|
|
1889
|
-
* (
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
2036
|
+
/** Short row prefix for a delegation entry (one of the two visual channels
|
|
2037
|
+
* that split native task() work from pantheon_delegate jobs — the other is
|
|
2038
|
+
* {@link delegationIcon}):
|
|
2039
|
+
*
|
|
2040
|
+
* - `nat:<agent>` — a native task() child (source 'children-only', no board
|
|
2041
|
+
* report). The child session carries no board alias, so the agent IS the
|
|
2042
|
+
* identity.
|
|
2043
|
+
* - `pan:<alias>` — a pantheon_delegate board row (`pan:apo-1`), the same
|
|
2044
|
+
* alias the manager prints in pantheon_delegation_list.
|
|
2045
|
+
*
|
|
2046
|
+
* Short prefixes keep the narrow sidebar readable (the old
|
|
2047
|
+
* `pan:apo-1`/`nat:` tags ate the row width). */
|
|
1892
2048
|
export function delegationTag(entry: DelegationEntry): string {
|
|
1893
|
-
return entry.source === 'children-only' ?
|
|
2049
|
+
return entry.source === 'children-only' ? `nat:${entry.agent}` : `pan:${entry.alias}`
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/** Row glyph: hollow diamond `◇` for native task() children (info color,
|
|
2053
|
+
* outline) vs filled diamond `◆` for pantheon_delegate rows (state color).
|
|
2054
|
+
* Shape + color are independent channels, so the split stays legible even
|
|
2055
|
+
* without color. */
|
|
2056
|
+
export function delegationIcon(entry: DelegationEntry): string {
|
|
2057
|
+
return entry.source === 'children-only' ? '\u25c7' : '\u25c6'
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/** Row identity after the status marker + glyph. Pantheon rows append the
|
|
2061
|
+
* agent (`pan:apo-1 apollo`); native rows already carry it inside the tag
|
|
2062
|
+
* (`nat:apollo`) and must not duplicate it. */
|
|
2063
|
+
export function formatDelegationIdentity(entry: DelegationEntry): string {
|
|
2064
|
+
return entry.source === 'children-only'
|
|
2065
|
+
? delegationTag(entry)
|
|
2066
|
+
: `${delegationTag(entry)} ${entry.agent}`
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
/** Max description width on the row detail line — the old 180-char slice
|
|
2070
|
+
* wrapped the sidebar; 44 keeps one readable line. */
|
|
2071
|
+
export const DELEGATION_DESCRIPTION_MAX = 44
|
|
2072
|
+
|
|
2073
|
+
/** Lazily-built grapheme segmenter. `Intl.Segmenter` keeps ZWJ emoji families
|
|
2074
|
+
* (and skin-tone/variation sequences) intact; `Array.from` (code points) is
|
|
2075
|
+
* the fallback for runtimes without it. */
|
|
2076
|
+
let graphemeSegmenter: Intl.Segmenter | undefined
|
|
2077
|
+
|
|
2078
|
+
/** Split `text` into display graphemes so truncation never cuts a multi-unit
|
|
2079
|
+
* emoji in half (surrogate pair → mojibake). */
|
|
2080
|
+
function splitGraphemes(text: string): string[] {
|
|
2081
|
+
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
2082
|
+
graphemeSegmenter ??= new Intl.Segmenter('pt', { granularity: 'grapheme' })
|
|
2083
|
+
return Array.from(graphemeSegmenter.segment(text), (s) => s.segment)
|
|
2084
|
+
}
|
|
2085
|
+
return Array.from(text)
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/** Truncate a description to `max` graphemes appending `…` when cut. Exact
|
|
2089
|
+
* `max`-length text is left untouched. Grapheme-granular, so an emoji made of
|
|
2090
|
+
* several code points is never split into mojibake at the boundary. Pure. */
|
|
2091
|
+
export function truncateDelegationDescription(
|
|
2092
|
+
text: string,
|
|
2093
|
+
max = DELEGATION_DESCRIPTION_MAX,
|
|
2094
|
+
): string {
|
|
2095
|
+
const graphemes = splitGraphemes(text)
|
|
2096
|
+
if (graphemes.length <= max) return text
|
|
2097
|
+
return `${graphemes.slice(0, Math.max(0, max - 1)).join('')}\u2026`
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/** Fixed elapsed-time box: right-aligned to `width` (default 8) so elapsed
|
|
2101
|
+
* values line up across rows, e.g. ` 12s`. A label that already fills
|
|
2102
|
+
* the box is returned untouched. Pure. */
|
|
2103
|
+
export const DELEGATION_ELAPSED_WIDTH = 8
|
|
2104
|
+
|
|
2105
|
+
export function formatDelegationElapsed(
|
|
2106
|
+
entry: DelegationEntry,
|
|
2107
|
+
now: number,
|
|
2108
|
+
width = DELEGATION_ELAPSED_WIDTH,
|
|
2109
|
+
): string {
|
|
2110
|
+
const label = delegationElapsed(entry, now)
|
|
2111
|
+
if (label.length >= width) return label
|
|
2112
|
+
return `${' '.repeat(width - label.length)}${label}`
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
/** First row line: `<marker><glyph> <identity>`. The marker already carries
|
|
2116
|
+
* its trailing space. Pure — the row renders the returned string verbatim. */
|
|
2117
|
+
export function formatDelegationRow(entry: DelegationEntry, marker: string): string {
|
|
2118
|
+
return `${marker}${delegationIcon(entry)} ${formatDelegationIdentity(entry)}`
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// Tree connectors (`isNestedDelegation` / `delegationTreePrefix`) were removed
|
|
2122
|
+
// as dead code: `session.children` returns direct children only, so no row is
|
|
2123
|
+
// ever actually nested and the connectors were never populated in production.
|
|
2124
|
+
// BACKLOG: produce real grandchild rows first, then reintroduce them.
|
|
2125
|
+
|
|
2126
|
+
/** Header summary: `(N active · M done · nat:K pan:M)`. Active counts
|
|
2127
|
+
* running/retry AND display-only stale-running (a stale row is still a live
|
|
2128
|
+
* job — it must never read as done). Pure. */
|
|
2129
|
+
export function formatDelegationHeader(entries: readonly DelegationEntry[]): string {
|
|
2130
|
+
let active = 0
|
|
2131
|
+
for (const e of entries) {
|
|
2132
|
+
if (e.state === 'running' || e.state === 'retry' || e.state === 'stale-running') active++
|
|
2133
|
+
}
|
|
2134
|
+
const done = entries.length - active
|
|
2135
|
+
const counts = countDelegationSources(entries)
|
|
2136
|
+
return `(${active} active \u00b7 ${done} done \u00b7 nat:${counts.native} pan:${counts.pantheon})`
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/** Split a display list into native task() rows vs pantheon_delegate rows.
|
|
2140
|
+
* Native = source 'children-only' (no board report); everything else counts
|
|
2141
|
+
* as pantheon. Pure — powers the header breakdown + the hooks.log line. */
|
|
2142
|
+
export function countDelegationSources(entries: readonly DelegationEntry[]): {
|
|
2143
|
+
native: number
|
|
2144
|
+
pantheon: number
|
|
2145
|
+
total: number
|
|
2146
|
+
} {
|
|
2147
|
+
let native = 0
|
|
2148
|
+
for (const e of entries) if (e.source === 'children-only') native++
|
|
2149
|
+
return { native, pantheon: entries.length - native, total: entries.length }
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/** Diagnostic hooks.log line for a panel re-fetch, with the children
|
|
2153
|
+
* breakdown (pantheon = children WITH a board report, native = children
|
|
2154
|
+
* WITHOUT one). Pure — the View logs the returned string verbatim. */
|
|
2155
|
+
export function formatPanelLogLine(
|
|
2156
|
+
children: number,
|
|
2157
|
+
pantheon: number,
|
|
2158
|
+
native: number,
|
|
2159
|
+
md: number,
|
|
2160
|
+
events: number,
|
|
2161
|
+
): string {
|
|
2162
|
+
return `panel: children=${children}(pantheon=${pantheon} native=${native}) md=${md} events=${events}`
|
|
1894
2163
|
}
|
|
1895
2164
|
|
|
1896
2165
|
/** Turn child sessions (PRIMARY) enriched with md reports into the display
|
|
@@ -1901,10 +2170,14 @@ export function delegationTag(entry: DelegationEntry): string {
|
|
|
1901
2170
|
* itself (fallback 'agent'), state derived from its status, startedAt from
|
|
1902
2171
|
* time.created. A report-less child is a NATIVE task() child (every
|
|
1903
2172
|
* child of the current session — pantheon_delegate OR the native `task()`
|
|
1904
|
-
* tool — carries parentID = caller), so it gets source 'children-only'
|
|
1905
|
-
* and the `
|
|
2173
|
+
* tool — carries parentID = caller), so it gets source 'children-only',
|
|
2174
|
+
* the internal alias 'native-task' and the `nat:` tag instead of a
|
|
2175
|
+
* board alias. The 'task nativa' description fallback keeps the row
|
|
2176
|
+
* non-empty when the child carries no title.
|
|
1906
2177
|
* Terminal md state wins over the derived state; a running md defers to
|
|
1907
|
-
* the child's live status.
|
|
2178
|
+
* the child's live status. A running child is NEVER archived — it always
|
|
2179
|
+
* lands in the active split (splitDelegationList active = running/retry).
|
|
2180
|
+
* Sorted running-first (compareDelegationEntries).
|
|
1908
2181
|
* Pure — no I/O. */
|
|
1909
2182
|
export function childrenToDelegationEntries(
|
|
1910
2183
|
children: readonly ChildDelegationLike[] | undefined,
|
|
@@ -1926,7 +2199,7 @@ export function childrenToDelegationEntries(
|
|
|
1926
2199
|
? mdEntry.state
|
|
1927
2200
|
: childStatusToState(child.status)
|
|
1928
2201
|
out.push({
|
|
1929
|
-
alias: mdEntry?.alias ?? 'task',
|
|
2202
|
+
alias: mdEntry?.alias ?? 'native-task',
|
|
1930
2203
|
sessionID: mdEntry?.sessionID ?? '',
|
|
1931
2204
|
taskID: child.id,
|
|
1932
2205
|
agent: mdEntry?.agent ?? child.agent ?? 'agent',
|
|
@@ -1939,7 +2212,9 @@ export function childrenToDelegationEntries(
|
|
|
1939
2212
|
description:
|
|
1940
2213
|
mdEntry !== undefined && mdEntry.description !== ''
|
|
1941
2214
|
? mdEntry.description
|
|
1942
|
-
:
|
|
2215
|
+
: child.title !== undefined && child.title !== ''
|
|
2216
|
+
? child.title
|
|
2217
|
+
: 'task nativa',
|
|
1943
2218
|
source: mdEntry !== undefined ? 'md' : 'children-only',
|
|
1944
2219
|
})
|
|
1945
2220
|
}
|
|
@@ -2005,8 +2280,19 @@ function createTuiLogger(projectRoot: string | undefined, module = 'pantheon-tui
|
|
|
2005
2280
|
`${lines.map((l) => `[${stamp}] [${module}] ${l}`).join('\n')}\n`,
|
|
2006
2281
|
'utf8',
|
|
2007
2282
|
)
|
|
2008
|
-
} catch {
|
|
2009
|
-
/* best-effort file log — never break the panel over logging
|
|
2283
|
+
} catch (err) {
|
|
2284
|
+
/* best-effort file log — never break the panel over logging, but do
|
|
2285
|
+
* NOT swallow the failure silently: a missing symbol here (e.g.
|
|
2286
|
+
* `appendFile` used without an import) previously threw an invisible
|
|
2287
|
+
* ReferenceError and the log silently stopped writing. Gated by the
|
|
2288
|
+
* echo flag to avoid the TUI-pollution bug described above. */
|
|
2289
|
+
if (echo) {
|
|
2290
|
+
try {
|
|
2291
|
+
process.stderr.write(`[${module}] file-log failed: ${formatArg(err)}\n`)
|
|
2292
|
+
} catch {
|
|
2293
|
+
/* stderr unavailable — never break the panel */
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2010
2296
|
}
|
|
2011
2297
|
})()
|
|
2012
2298
|
if (echo) {
|
|
@@ -2068,6 +2354,8 @@ function DelegationRow(props: {
|
|
|
2068
2354
|
switch (props.job.state) {
|
|
2069
2355
|
case 'running':
|
|
2070
2356
|
return t.warning
|
|
2357
|
+
case 'retry':
|
|
2358
|
+
return t.warning
|
|
2071
2359
|
case 'stale-running':
|
|
2072
2360
|
return t.error // warning color for stale delegations
|
|
2073
2361
|
case 'completed':
|
|
@@ -2081,6 +2369,7 @@ function DelegationRow(props: {
|
|
|
2081
2369
|
|
|
2082
2370
|
const marker = createMemo(() => {
|
|
2083
2371
|
if (props.job.state === 'running') return `${delegationSpinnerFrame(props.animationNow)} `
|
|
2372
|
+
if (props.job.state === 'retry') return '\u27f3 ' // ⟳ retrying
|
|
2084
2373
|
if (props.job.state === 'stale-running') return '\u26a0 ' // warning triangle
|
|
2085
2374
|
switch (props.job.state) {
|
|
2086
2375
|
case 'completed':
|
|
@@ -2092,17 +2381,20 @@ function DelegationRow(props: {
|
|
|
2092
2381
|
}
|
|
2093
2382
|
})
|
|
2094
2383
|
|
|
2095
|
-
//
|
|
2096
|
-
// `
|
|
2097
|
-
//
|
|
2384
|
+
// Two visual channels split native task() from pantheon_delegate: the tag
|
|
2385
|
+
// prefix (`nat:`/`pan:`, via delegationTag) and the glyph color — info
|
|
2386
|
+
// (cyan) for native, state color for board rows. The glyph shape (◇/◆) is
|
|
2387
|
+
// the third, color-independent channel.
|
|
2098
2388
|
const tagColor = createMemo(() => (props.job.source === 'children-only' ? theme().info : color()))
|
|
2099
2389
|
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2390
|
+
const description = createMemo(() => truncateDelegationDescription(props.job.description))
|
|
2391
|
+
const elapsed = createMemo(() => formatDelegationElapsed(props.job, props.now))
|
|
2392
|
+
|
|
2393
|
+
// beta.6: live tool call the child is running RIGHT NOW ("↳ bash npm test"),
|
|
2394
|
+
// fed by message.part.updated activity tracking. Null while idle/done.
|
|
2395
|
+
const activity = createMemo(() =>
|
|
2396
|
+
latestToolActivityFor(latestToolActivity, props.job.taskID, props.now),
|
|
2397
|
+
)
|
|
2106
2398
|
|
|
2107
2399
|
// Click a row → navigate to the child session (route API guarded in the
|
|
2108
2400
|
// helper). Enter-key binding is left out (per-row keymap wiring is not
|
|
@@ -2114,13 +2406,23 @@ function DelegationRow(props: {
|
|
|
2114
2406
|
return (
|
|
2115
2407
|
<box onMouseDown={open}>
|
|
2116
2408
|
<box flexDirection="row">
|
|
2117
|
-
<text fg={
|
|
2118
|
-
<text fg={tagColor()}>{delegationTag(props.job)}</text>
|
|
2119
|
-
<text
|
|
2120
|
-
fg={color()}
|
|
2121
|
-
>{` ${props.job.agent} \u2014 ${delegationActivityLabel(props.job)}`}</text>
|
|
2409
|
+
<text fg={tagColor()}>{formatDelegationRow(props.job, marker())}</text>
|
|
2122
2410
|
</box>
|
|
2123
|
-
|
|
2411
|
+
{/* Elapsed is ALWAYS rendered (outside the optional-description Show) so
|
|
2412
|
+
* an active job with no description never loses its timer. */}
|
|
2413
|
+
<box flexDirection="row" justifyContent={description() !== '' ? 'space-between' : 'flex-end'}>
|
|
2414
|
+
<Show when={description() !== ''}>
|
|
2415
|
+
<text fg={theme().textMuted}>{description()}</text>
|
|
2416
|
+
</Show>
|
|
2417
|
+
<text fg={theme().textMuted}>{elapsed()}</text>
|
|
2418
|
+
</box>
|
|
2419
|
+
<Show when={activity()}>
|
|
2420
|
+
{(a) => (
|
|
2421
|
+
<text
|
|
2422
|
+
fg={theme().textMuted}
|
|
2423
|
+
>{` \u21b3 ${truncateDelegationDescription(`${a().tool}${a().summary !== '' ? ` ${a().summary}` : ''}`)}`}</text>
|
|
2424
|
+
)}
|
|
2425
|
+
</Show>
|
|
2124
2426
|
</box>
|
|
2125
2427
|
)
|
|
2126
2428
|
}
|
|
@@ -2247,9 +2549,10 @@ function View(props: {
|
|
|
2247
2549
|
// remains on the 1s ticker to avoid unnecessary work.
|
|
2248
2550
|
const [animationNow, setAnimationNow] = createSignal(Date.now())
|
|
2249
2551
|
// Cumulative count of event-triggered refreshes — logged with every fetch
|
|
2250
|
-
// ("panel: children=N md=N events=N") so the (0)
|
|
2251
|
-
// children>0 means the source works; events=0
|
|
2252
|
-
// delivering, and the 1s poll is what keeps
|
|
2552
|
+
// ("panel: children=N(pantheon=M native=K) md=N events=N") so the (0)
|
|
2553
|
+
// symptom is diagnosable: children>0 means the source works; events=0
|
|
2554
|
+
// means the event bus isn't delivering, and the 1s poll is what keeps
|
|
2555
|
+
// the panel honest.
|
|
2253
2556
|
let eventRefreshCount = 0
|
|
2254
2557
|
// In-flight guard: overlap between the poll, events and the version effect
|
|
2255
2558
|
// collapses into one fetch — no duplicate work, no duplicate entries.
|
|
@@ -2281,7 +2584,7 @@ function View(props: {
|
|
|
2281
2584
|
if (sessionID === null) {
|
|
2282
2585
|
setChildDelegations(md)
|
|
2283
2586
|
panelLog.info(
|
|
2284
|
-
|
|
2587
|
+
`${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — history shown)`,
|
|
2285
2588
|
)
|
|
2286
2589
|
return
|
|
2287
2590
|
}
|
|
@@ -2324,8 +2627,15 @@ function View(props: {
|
|
|
2324
2627
|
)
|
|
2325
2628
|
setChildDelegations(mergeChildDelegationSources(childEntries, liveEntries))
|
|
2326
2629
|
// 4. diagnostic line — every re-fetch (silence-by-default, hooks.log).
|
|
2630
|
+
const counts = countDelegationSources(childEntries)
|
|
2327
2631
|
panelLog.info(
|
|
2328
|
-
|
|
2632
|
+
formatPanelLogLine(
|
|
2633
|
+
children.length,
|
|
2634
|
+
counts.pantheon,
|
|
2635
|
+
counts.native,
|
|
2636
|
+
md.length,
|
|
2637
|
+
eventRefreshCount,
|
|
2638
|
+
),
|
|
2329
2639
|
)
|
|
2330
2640
|
} finally {
|
|
2331
2641
|
delegationsInflight = null
|
|
@@ -2333,9 +2643,24 @@ function View(props: {
|
|
|
2333
2643
|
})()
|
|
2334
2644
|
return delegationsInflight
|
|
2335
2645
|
}
|
|
2336
|
-
//
|
|
2337
|
-
//
|
|
2338
|
-
|
|
2646
|
+
// beta.6: active (running/retry) first, then the 8 most recent terminal
|
|
2647
|
+
// reports, then the archived tail (paginated). Pagination state lives at
|
|
2648
|
+
// module level so sidebar remounts keep the user's page.
|
|
2649
|
+
const delegationSplit = createMemo(() => splitDelegationList(childDelegations()))
|
|
2650
|
+
const visibleDelegations = createMemo(() => [
|
|
2651
|
+
...delegationSplit().active,
|
|
2652
|
+
...delegationSplit().recent,
|
|
2653
|
+
])
|
|
2654
|
+
// Header summary over the FULL list (active + recent + archived): active
|
|
2655
|
+
// vs done plus the native/pantheon breakdown, so a busy session reads
|
|
2656
|
+
// "Delegations (2 active · 9 done · nat:3 pan:8)".
|
|
2657
|
+
const delegationHeader = createMemo(() => formatDelegationHeader(childDelegations()))
|
|
2658
|
+
const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page)
|
|
2659
|
+
const clampArchivedPage = (page: number, pages: number) => {
|
|
2660
|
+
const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)))
|
|
2661
|
+
archivedUiState.page = clamped
|
|
2662
|
+
setArchivedPage(clamped)
|
|
2663
|
+
}
|
|
2339
2664
|
|
|
2340
2665
|
// ── Event subscriptions for live session/delegation updates ──
|
|
2341
2666
|
onMount(() => {
|
|
@@ -2383,7 +2708,7 @@ function View(props: {
|
|
|
2383
2708
|
void refreshDelegations()
|
|
2384
2709
|
}, 1_000)
|
|
2385
2710
|
cleanup.push(() => clearInterval(poll))
|
|
2386
|
-
const animation = setInterval(() => setAnimationNow(Date.now()),
|
|
2711
|
+
const animation = setInterval(() => setAnimationNow(Date.now()), 1_000)
|
|
2387
2712
|
cleanup.push(() => clearInterval(animation))
|
|
2388
2713
|
|
|
2389
2714
|
// Compaction recovery: `message.part.removed` clears live entries while
|
|
@@ -2478,9 +2803,9 @@ function View(props: {
|
|
|
2478
2803
|
{/* ── Delegations (session.children primary + .pantheon/delegations md enrichment) ── */}
|
|
2479
2804
|
<box onMouseDown={() => setShowDelegations((x) => !x)}>
|
|
2480
2805
|
<text fg={theme().text} attributes={1}>
|
|
2481
|
-
{`${showDelegations() ? '
|
|
2806
|
+
{`${showDelegations() ? '▼' : '▶'} Delegations`}
|
|
2482
2807
|
</text>
|
|
2483
|
-
<text fg={theme().textMuted}>{`
|
|
2808
|
+
<text fg={theme().textMuted}>{` ${delegationHeader()}`}</text>
|
|
2484
2809
|
</box>
|
|
2485
2810
|
<Show when={showDelegations()}>
|
|
2486
2811
|
<Show
|
|
@@ -2502,6 +2827,41 @@ function View(props: {
|
|
|
2502
2827
|
/>
|
|
2503
2828
|
)}
|
|
2504
2829
|
</For>
|
|
2830
|
+
<Show when={delegationSplit().archived.length > 0}>
|
|
2831
|
+
{(() => {
|
|
2832
|
+
const archived = delegationSplit().archived
|
|
2833
|
+
const pageSize = 8
|
|
2834
|
+
const pages = Math.ceil(archived.length / pageSize)
|
|
2835
|
+
const page = Math.min(archivedPage(), pages - 1)
|
|
2836
|
+
const slice = archived.slice(page * pageSize, page * pageSize + pageSize)
|
|
2837
|
+
return (
|
|
2838
|
+
<box flexDirection="column">
|
|
2839
|
+
<box flexDirection="row" marginLeft={0}>
|
|
2840
|
+
<text
|
|
2841
|
+
fg={theme().textMuted}
|
|
2842
|
+
onMouseDown={() => clampArchivedPage(page - 1, pages)}
|
|
2843
|
+
>{`\u2039 `}</text>
|
|
2844
|
+
<text fg={theme().textMuted}>{`Archived (${archived.length}) `}</text>
|
|
2845
|
+
<text fg={theme().textMuted}>{`${page + 1}/${pages}`}</text>
|
|
2846
|
+
<text
|
|
2847
|
+
fg={theme().textMuted}
|
|
2848
|
+
onMouseDown={() => clampArchivedPage(page + 1, pages)}
|
|
2849
|
+
>{` \u203a`}</text>
|
|
2850
|
+
</box>
|
|
2851
|
+
<For each={slice}>
|
|
2852
|
+
{(job) => (
|
|
2853
|
+
<DelegationRow
|
|
2854
|
+
api={props.api}
|
|
2855
|
+
job={job}
|
|
2856
|
+
now={now()}
|
|
2857
|
+
animationNow={animationNow()}
|
|
2858
|
+
/>
|
|
2859
|
+
)}
|
|
2860
|
+
</For>
|
|
2861
|
+
</box>
|
|
2862
|
+
)
|
|
2863
|
+
})()}
|
|
2864
|
+
</Show>
|
|
2505
2865
|
</box>
|
|
2506
2866
|
</Show>
|
|
2507
2867
|
</Show>
|
|
@@ -2550,6 +2910,11 @@ const tui: TuiPlugin = (api, _options, _meta) => {
|
|
|
2550
2910
|
const props = (event?.properties ?? {}) as { part?: unknown; info?: { part?: unknown } }
|
|
2551
2911
|
const part = (props.part ?? props.info?.part) as DelegationToolPart | undefined
|
|
2552
2912
|
if (part === undefined) return
|
|
2913
|
+
// beta.6: track the live tool call per child session (panel's
|
|
2914
|
+
// activity line). Fire-and-forget — display-only state.
|
|
2915
|
+
const sample = extractToolActivity(part as any)
|
|
2916
|
+
if (sample !== null)
|
|
2917
|
+
trackToolActivity(latestToolActivity, sample.sessionID, sample.activity)
|
|
2553
2918
|
if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump()
|
|
2554
2919
|
}),
|
|
2555
2920
|
)
|
|
@@ -2591,9 +2956,13 @@ const tui: TuiPlugin = (api, _options, _meta) => {
|
|
|
2591
2956
|
})
|
|
2592
2957
|
}
|
|
2593
2958
|
|
|
2594
|
-
const plugin: TuiPluginModule & { id: string } = {
|
|
2959
|
+
const plugin: TuiPluginModule & { id: string; setup: () => Promise<void> } = {
|
|
2595
2960
|
id: 'pantheon.tui',
|
|
2596
2961
|
tui,
|
|
2962
|
+
// Beta loader contract: every `plugins` directory entry must expose a
|
|
2963
|
+
// no-op `setup` so the entry loads as `pantheon.tui` with zero errors
|
|
2964
|
+
// (sandbox probe evidence). The TUI itself boots via `tui()`.
|
|
2965
|
+
setup: async () => {},
|
|
2597
2966
|
}
|
|
2598
2967
|
|
|
2599
2968
|
export default plugin
|