pantheon-opencode 1.5.0-beta.7 → 1.5.0-beta.9
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 +48 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/src/pantheon/background-job-board.ts +6 -1
- package/src/pantheon/delegation.ts +141 -5
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/plugins/tui/dist/tui.d.ts +31 -10
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +51 -22
- package/src/plugins/tui/dist/tui.tsx +111 -35
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +111 -35
|
@@ -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
|
|
@@ -1509,9 +1512,11 @@ export function mergeChildDelegationSources(
|
|
|
1509
1512
|
* Source of truth for jobs born while opencode is open. The TUI event bus
|
|
1510
1513
|
* (api.event.on) delivers `message.part.updated` for every part change;
|
|
1511
1514
|
* we only care about tool parts whose `tool` is pantheon_delegate (job
|
|
1512
|
-
* launch)
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
+
* launch), the native `task` subagent tool (same parentID === caller
|
|
1516
|
+
* mechanism — its rows render `[native]`), or pantheon_delegation_read
|
|
1517
|
+
* (blocking until terminal → closes the entry). Shape-adapted from the
|
|
1518
|
+
* SDK v2 ToolPart but duck-typed so the pure helpers are testable without
|
|
1519
|
+
* the SDK. */
|
|
1515
1520
|
|
|
1516
1521
|
/** Duck-typed subset of a tool part (SDK v2 `ToolPart` / `ToolState`). */
|
|
1517
1522
|
export type DelegationToolPart = {
|
|
@@ -1537,7 +1542,7 @@ export type LiveDelegationEntry = {
|
|
|
1537
1542
|
partID: string
|
|
1538
1543
|
/** Parent session the delegation was launched from. */
|
|
1539
1544
|
sessionID: string
|
|
1540
|
-
tool: 'pantheon_delegate' | 'pantheon_delegation_read'
|
|
1545
|
+
tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task'
|
|
1541
1546
|
/** Agent name (from the delegate input args). */
|
|
1542
1547
|
agent: string
|
|
1543
1548
|
description: string
|
|
@@ -1557,7 +1562,7 @@ export type ParsedDelegationToolPart = {
|
|
|
1557
1562
|
callID: string
|
|
1558
1563
|
partID: string
|
|
1559
1564
|
sessionID: string
|
|
1560
|
-
tool: 'pantheon_delegate' | 'pantheon_delegation_read'
|
|
1565
|
+
tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task'
|
|
1561
1566
|
/** null for read parts (no agent arg — the id targets an existing job). */
|
|
1562
1567
|
agent: string | null
|
|
1563
1568
|
description: string
|
|
@@ -1577,13 +1582,20 @@ const READ_ALIAS_PATTERN = /^[a-z]{2,8}-\d+$/i
|
|
|
1577
1582
|
|
|
1578
1583
|
/** Extract the tool name + args from a `message.part.updated` part and
|
|
1579
1584
|
* reduce it to what the panel needs. Returns null for anything that is
|
|
1580
|
-
* not a pantheon delegation tool part
|
|
1585
|
+
* not a pantheon delegation tool part, the native `task` subagent tool
|
|
1586
|
+
* (same parentID === caller mechanism — its children render `[native]`),
|
|
1587
|
+
* or is missing its callID. */
|
|
1581
1588
|
export function parseDelegationToolPart(
|
|
1582
1589
|
part: DelegationToolPart,
|
|
1583
1590
|
now = Date.now(),
|
|
1584
1591
|
): ParsedDelegationToolPart | null {
|
|
1585
1592
|
if (part.type !== 'tool') return null
|
|
1586
|
-
if (
|
|
1593
|
+
if (
|
|
1594
|
+
part.tool !== 'pantheon_delegate' &&
|
|
1595
|
+
part.tool !== 'pantheon_delegation_read' &&
|
|
1596
|
+
part.tool !== 'task'
|
|
1597
|
+
)
|
|
1598
|
+
return null
|
|
1587
1599
|
const callID = part.callID
|
|
1588
1600
|
if (callID === undefined || callID === '') return null
|
|
1589
1601
|
const sessionID = part.sessionID ?? ''
|
|
@@ -1618,10 +1630,22 @@ export function parseDelegationToolPart(
|
|
|
1618
1630
|
}
|
|
1619
1631
|
}
|
|
1620
1632
|
|
|
1621
|
-
const agent =
|
|
1622
|
-
|
|
1633
|
+
const agent =
|
|
1634
|
+
typeof input.agent === 'string'
|
|
1635
|
+
? input.agent
|
|
1636
|
+
: typeof input.subagent_type === 'string'
|
|
1637
|
+
? (input.subagent_type as string)
|
|
1638
|
+
: 'agent'
|
|
1639
|
+
const description =
|
|
1640
|
+
typeof input.description === 'string'
|
|
1641
|
+
? input.description
|
|
1642
|
+
: typeof input.prompt === 'string'
|
|
1643
|
+
? (input.prompt as string).slice(0, 120)
|
|
1644
|
+
: ''
|
|
1623
1645
|
// The alias/taskID only exist once the delegate tool COMPLETES (they are
|
|
1624
|
-
// returned in its output); a running/pending part has neither.
|
|
1646
|
+
// returned in its output); a running/pending part has neither. Native
|
|
1647
|
+
// `task` parts never carry a board alias — the children channel supplies
|
|
1648
|
+
// the child session id, so the row renders `[native]`.
|
|
1625
1649
|
let alias: string | null = null
|
|
1626
1650
|
let taskID: string | null = null
|
|
1627
1651
|
if (status === 'completed') {
|
|
@@ -1633,7 +1657,7 @@ export function parseDelegationToolPart(
|
|
|
1633
1657
|
callID,
|
|
1634
1658
|
partID: part.id ?? '',
|
|
1635
1659
|
sessionID,
|
|
1636
|
-
tool: 'pantheon_delegate',
|
|
1660
|
+
tool: part.tool === 'task' ? 'task' : 'pantheon_delegate',
|
|
1637
1661
|
agent,
|
|
1638
1662
|
description,
|
|
1639
1663
|
status,
|
|
@@ -1691,9 +1715,11 @@ export function reduceDelegationToolPart(
|
|
|
1691
1715
|
return changed
|
|
1692
1716
|
}
|
|
1693
1717
|
|
|
1694
|
-
// Delegate tool: pending/running →
|
|
1695
|
-
// delegate tool only means the job was
|
|
1696
|
-
// we pick up alias + taskID from the
|
|
1718
|
+
// Delegate tool (pantheon_delegate AND native task): pending/running →
|
|
1719
|
+
// job launched (running). A COMPLETED delegate tool only means the job was
|
|
1720
|
+
// registered — it stays running and we pick up alias + taskID from the
|
|
1721
|
+
// output. Native `task` parts never yield a board alias, so their rows
|
|
1722
|
+
// stay `[native]` until the children channel resolves them.
|
|
1697
1723
|
const existing = map.get(parsed.callID)
|
|
1698
1724
|
if (parsed.status === 'error') {
|
|
1699
1725
|
if (existing !== undefined && existing.state === 'error' && existing.updatedAt === parsed.endAt)
|
|
@@ -1702,7 +1728,7 @@ export function reduceDelegationToolPart(
|
|
|
1702
1728
|
callID: parsed.callID,
|
|
1703
1729
|
partID: parsed.partID,
|
|
1704
1730
|
sessionID: parsed.sessionID,
|
|
1705
|
-
tool:
|
|
1731
|
+
tool: parsed.tool,
|
|
1706
1732
|
agent: parsed.agent ?? 'agent',
|
|
1707
1733
|
description: parsed.description,
|
|
1708
1734
|
alias: existing?.alias ?? null,
|
|
@@ -1719,7 +1745,7 @@ export function reduceDelegationToolPart(
|
|
|
1719
1745
|
callID: parsed.callID,
|
|
1720
1746
|
partID: parsed.partID,
|
|
1721
1747
|
sessionID: parsed.sessionID,
|
|
1722
|
-
tool:
|
|
1748
|
+
tool: parsed.tool,
|
|
1723
1749
|
agent: parsed.agent ?? 'agent',
|
|
1724
1750
|
description: parsed.description,
|
|
1725
1751
|
alias: parsed.alias,
|
|
@@ -1772,11 +1798,14 @@ export function removeDelegationEntry(
|
|
|
1772
1798
|
return false
|
|
1773
1799
|
}
|
|
1774
1800
|
|
|
1775
|
-
/** Collect pantheon delegation tool parts from a session's messages.
|
|
1801
|
+
/** Collect pantheon delegation + native task tool parts from a session's messages.
|
|
1776
1802
|
* Messages may carry their parts inline (duck-typed `msg.parts`); when
|
|
1777
1803
|
* they don't, the optional `getParts(messageID)` callback is used (the TUI
|
|
1778
|
-
* SDK exposes `api.state.part(messageID)`).
|
|
1779
|
-
*
|
|
1804
|
+
* SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
|
|
1805
|
+
* child session with parentID = caller — the same mechanism as
|
|
1806
|
+
* pantheon_delegate — so its parts feed the live-map as the native signal
|
|
1807
|
+
* (rows render `[native]` via the children channel). Pure w.r.t. I/O — used
|
|
1808
|
+
* by the mount re-scan to re-seed the live map after compaction/attach. */
|
|
1780
1809
|
export function collectDelegationToolParts(
|
|
1781
1810
|
messages: readonly { id?: string; parts?: unknown[] }[] | undefined,
|
|
1782
1811
|
getParts?: (messageID: string) => readonly unknown[] | undefined,
|
|
@@ -1794,7 +1823,9 @@ export function collectDelegationToolParts(
|
|
|
1794
1823
|
const part = raw as DelegationToolPart
|
|
1795
1824
|
if (
|
|
1796
1825
|
part?.type === 'tool' &&
|
|
1797
|
-
(part.tool === 'pantheon_delegate' ||
|
|
1826
|
+
(part.tool === 'pantheon_delegate' ||
|
|
1827
|
+
part.tool === 'pantheon_delegation_read' ||
|
|
1828
|
+
part.tool === 'task')
|
|
1798
1829
|
) {
|
|
1799
1830
|
out.push(part)
|
|
1800
1831
|
}
|
|
@@ -2000,6 +2031,32 @@ export function delegationTag(entry: DelegationEntry): string {
|
|
|
2000
2031
|
return entry.source === 'children-only' ? '[native]' : `[pantheon:${entry.alias}]`
|
|
2001
2032
|
}
|
|
2002
2033
|
|
|
2034
|
+
/** Split a display list into native task() rows vs pantheon_delegate rows.
|
|
2035
|
+
* Native = source 'children-only' (no board report); everything else counts
|
|
2036
|
+
* as pantheon. Pure — powers the header breakdown + the hooks.log line. */
|
|
2037
|
+
export function countDelegationSources(entries: readonly DelegationEntry[]): {
|
|
2038
|
+
native: number
|
|
2039
|
+
pantheon: number
|
|
2040
|
+
total: number
|
|
2041
|
+
} {
|
|
2042
|
+
let native = 0
|
|
2043
|
+
for (const e of entries) if (e.source === 'children-only') native++
|
|
2044
|
+
return { native, pantheon: entries.length - native, total: entries.length }
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
/** Diagnostic hooks.log line for a panel re-fetch, with the children
|
|
2048
|
+
* breakdown (pantheon = children WITH a board report, native = children
|
|
2049
|
+
* WITHOUT one). Pure — the View logs the returned string verbatim. */
|
|
2050
|
+
export function formatPanelLogLine(
|
|
2051
|
+
children: number,
|
|
2052
|
+
pantheon: number,
|
|
2053
|
+
native: number,
|
|
2054
|
+
md: number,
|
|
2055
|
+
events: number,
|
|
2056
|
+
): string {
|
|
2057
|
+
return `panel: children=${children}(pantheon=${pantheon} native=${native}) md=${md} events=${events}`
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2003
2060
|
/** Turn child sessions (PRIMARY) enriched with md reports into the display
|
|
2004
2061
|
* list. One entry per child id (duplicates across re-fetches collapse).
|
|
2005
2062
|
* The md report is matched by `Task ID` (== child.id) and supplies alias,
|
|
@@ -2008,10 +2065,14 @@ export function delegationTag(entry: DelegationEntry): string {
|
|
|
2008
2065
|
* itself (fallback 'agent'), state derived from its status, startedAt from
|
|
2009
2066
|
* time.created. A report-less child is a NATIVE task() child (every
|
|
2010
2067
|
* child of the current session — pantheon_delegate OR the native `task()`
|
|
2011
|
-
* tool — carries parentID = caller), so it gets source 'children-only'
|
|
2012
|
-
* and the `[native]` tag instead of a
|
|
2068
|
+
* tool — carries parentID = caller), so it gets source 'children-only',
|
|
2069
|
+
* the internal alias 'native-task' and the `[native]` tag instead of a
|
|
2070
|
+
* board alias. The 'task nativa' description fallback keeps the row
|
|
2071
|
+
* non-empty when the child carries no title.
|
|
2013
2072
|
* Terminal md state wins over the derived state; a running md defers to
|
|
2014
|
-
* the child's live status.
|
|
2073
|
+
* the child's live status. A running child is NEVER archived — it always
|
|
2074
|
+
* lands in the active split (splitDelegationList active = running/retry).
|
|
2075
|
+
* Sorted running-first (compareDelegationEntries).
|
|
2015
2076
|
* Pure — no I/O. */
|
|
2016
2077
|
export function childrenToDelegationEntries(
|
|
2017
2078
|
children: readonly ChildDelegationLike[] | undefined,
|
|
@@ -2033,7 +2094,7 @@ export function childrenToDelegationEntries(
|
|
|
2033
2094
|
? mdEntry.state
|
|
2034
2095
|
: childStatusToState(child.status)
|
|
2035
2096
|
out.push({
|
|
2036
|
-
alias: mdEntry?.alias ?? 'task',
|
|
2097
|
+
alias: mdEntry?.alias ?? 'native-task',
|
|
2037
2098
|
sessionID: mdEntry?.sessionID ?? '',
|
|
2038
2099
|
taskID: child.id,
|
|
2039
2100
|
agent: mdEntry?.agent ?? child.agent ?? 'agent',
|
|
@@ -2046,7 +2107,9 @@ export function childrenToDelegationEntries(
|
|
|
2046
2107
|
description:
|
|
2047
2108
|
mdEntry !== undefined && mdEntry.description !== ''
|
|
2048
2109
|
? mdEntry.description
|
|
2049
|
-
:
|
|
2110
|
+
: child.title !== undefined && child.title !== ''
|
|
2111
|
+
? child.title
|
|
2112
|
+
: 'task nativa',
|
|
2050
2113
|
source: mdEntry !== undefined ? 'md' : 'children-only',
|
|
2051
2114
|
})
|
|
2052
2115
|
}
|
|
@@ -2241,7 +2304,7 @@ function DelegationRow(props: {
|
|
|
2241
2304
|
{(a) => (
|
|
2242
2305
|
<text
|
|
2243
2306
|
fg={theme().textMuted}
|
|
2244
|
-
>{` \u21b3 ${a().tool}${a().summary !== '' ?
|
|
2307
|
+
>{` \u21b3 ${a().tool}${a().summary !== '' ? ` ${a().summary}` : ''}`}</text>
|
|
2245
2308
|
)}
|
|
2246
2309
|
</Show>
|
|
2247
2310
|
</box>
|
|
@@ -2370,9 +2433,10 @@ function View(props: {
|
|
|
2370
2433
|
// remains on the 1s ticker to avoid unnecessary work.
|
|
2371
2434
|
const [animationNow, setAnimationNow] = createSignal(Date.now())
|
|
2372
2435
|
// Cumulative count of event-triggered refreshes — logged with every fetch
|
|
2373
|
-
// ("panel: children=N md=N events=N") so the (0)
|
|
2374
|
-
// children>0 means the source works; events=0
|
|
2375
|
-
// delivering, and the 1s poll is what keeps
|
|
2436
|
+
// ("panel: children=N(pantheon=M native=K) md=N events=N") so the (0)
|
|
2437
|
+
// symptom is diagnosable: children>0 means the source works; events=0
|
|
2438
|
+
// means the event bus isn't delivering, and the 1s poll is what keeps
|
|
2439
|
+
// the panel honest.
|
|
2376
2440
|
let eventRefreshCount = 0
|
|
2377
2441
|
// In-flight guard: overlap between the poll, events and the version effect
|
|
2378
2442
|
// collapses into one fetch — no duplicate work, no duplicate entries.
|
|
@@ -2404,7 +2468,7 @@ function View(props: {
|
|
|
2404
2468
|
if (sessionID === null) {
|
|
2405
2469
|
setChildDelegations(md)
|
|
2406
2470
|
panelLog.info(
|
|
2407
|
-
|
|
2471
|
+
`${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — history shown)`,
|
|
2408
2472
|
)
|
|
2409
2473
|
return
|
|
2410
2474
|
}
|
|
@@ -2447,8 +2511,15 @@ function View(props: {
|
|
|
2447
2511
|
)
|
|
2448
2512
|
setChildDelegations(mergeChildDelegationSources(childEntries, liveEntries))
|
|
2449
2513
|
// 4. diagnostic line — every re-fetch (silence-by-default, hooks.log).
|
|
2514
|
+
const counts = countDelegationSources(childEntries)
|
|
2450
2515
|
panelLog.info(
|
|
2451
|
-
|
|
2516
|
+
formatPanelLogLine(
|
|
2517
|
+
children.length,
|
|
2518
|
+
counts.pantheon,
|
|
2519
|
+
counts.native,
|
|
2520
|
+
md.length,
|
|
2521
|
+
eventRefreshCount,
|
|
2522
|
+
),
|
|
2452
2523
|
)
|
|
2453
2524
|
} finally {
|
|
2454
2525
|
delegationsInflight = null
|
|
@@ -2464,6 +2535,9 @@ function View(props: {
|
|
|
2464
2535
|
...delegationSplit().active,
|
|
2465
2536
|
...delegationSplit().recent,
|
|
2466
2537
|
])
|
|
2538
|
+
// Header breakdown: native task() rows vs pantheon_delegate rows, so
|
|
2539
|
+
// Sessions(3) with 3 native children reads "Delegations (3 native + 0 pantheon)".
|
|
2540
|
+
const delegationCounts = createMemo(() => countDelegationSources(visibleDelegations()))
|
|
2467
2541
|
const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page)
|
|
2468
2542
|
const clampArchivedPage = (page: number, pages: number) => {
|
|
2469
2543
|
const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)))
|
|
@@ -2612,9 +2686,11 @@ function View(props: {
|
|
|
2612
2686
|
{/* ── Delegations (session.children primary + .pantheon/delegations md enrichment) ── */}
|
|
2613
2687
|
<box onMouseDown={() => setShowDelegations((x) => !x)}>
|
|
2614
2688
|
<text fg={theme().text} attributes={1}>
|
|
2615
|
-
{`${showDelegations() ? '
|
|
2689
|
+
{`${showDelegations() ? '▼' : '▶'} Delegations`}
|
|
2616
2690
|
</text>
|
|
2617
|
-
<text
|
|
2691
|
+
<text
|
|
2692
|
+
fg={theme().textMuted}
|
|
2693
|
+
>{` (${String(delegationCounts().native)} native + ${String(delegationCounts().pantheon)} pantheon)`}</text>
|
|
2618
2694
|
</box>
|
|
2619
2695
|
<Show when={showDelegations()}>
|
|
2620
2696
|
<Show
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
|
-
"version": "1.5.0-beta.
|
|
3
|
+
"version": "1.5.0-beta.9",
|
|
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.9",
|
|
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.9",
|
|
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": {
|
|
@@ -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
|
|
@@ -1509,9 +1512,11 @@ export function mergeChildDelegationSources(
|
|
|
1509
1512
|
* Source of truth for jobs born while opencode is open. The TUI event bus
|
|
1510
1513
|
* (api.event.on) delivers `message.part.updated` for every part change;
|
|
1511
1514
|
* we only care about tool parts whose `tool` is pantheon_delegate (job
|
|
1512
|
-
* launch)
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
+
* launch), the native `task` subagent tool (same parentID === caller
|
|
1516
|
+
* mechanism — its rows render `[native]`), or pantheon_delegation_read
|
|
1517
|
+
* (blocking until terminal → closes the entry). Shape-adapted from the
|
|
1518
|
+
* SDK v2 ToolPart but duck-typed so the pure helpers are testable without
|
|
1519
|
+
* the SDK. */
|
|
1515
1520
|
|
|
1516
1521
|
/** Duck-typed subset of a tool part (SDK v2 `ToolPart` / `ToolState`). */
|
|
1517
1522
|
export type DelegationToolPart = {
|
|
@@ -1537,7 +1542,7 @@ export type LiveDelegationEntry = {
|
|
|
1537
1542
|
partID: string
|
|
1538
1543
|
/** Parent session the delegation was launched from. */
|
|
1539
1544
|
sessionID: string
|
|
1540
|
-
tool: 'pantheon_delegate' | 'pantheon_delegation_read'
|
|
1545
|
+
tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task'
|
|
1541
1546
|
/** Agent name (from the delegate input args). */
|
|
1542
1547
|
agent: string
|
|
1543
1548
|
description: string
|
|
@@ -1557,7 +1562,7 @@ export type ParsedDelegationToolPart = {
|
|
|
1557
1562
|
callID: string
|
|
1558
1563
|
partID: string
|
|
1559
1564
|
sessionID: string
|
|
1560
|
-
tool: 'pantheon_delegate' | 'pantheon_delegation_read'
|
|
1565
|
+
tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task'
|
|
1561
1566
|
/** null for read parts (no agent arg — the id targets an existing job). */
|
|
1562
1567
|
agent: string | null
|
|
1563
1568
|
description: string
|
|
@@ -1577,13 +1582,20 @@ const READ_ALIAS_PATTERN = /^[a-z]{2,8}-\d+$/i
|
|
|
1577
1582
|
|
|
1578
1583
|
/** Extract the tool name + args from a `message.part.updated` part and
|
|
1579
1584
|
* reduce it to what the panel needs. Returns null for anything that is
|
|
1580
|
-
* not a pantheon delegation tool part
|
|
1585
|
+
* not a pantheon delegation tool part, the native `task` subagent tool
|
|
1586
|
+
* (same parentID === caller mechanism — its children render `[native]`),
|
|
1587
|
+
* or is missing its callID. */
|
|
1581
1588
|
export function parseDelegationToolPart(
|
|
1582
1589
|
part: DelegationToolPart,
|
|
1583
1590
|
now = Date.now(),
|
|
1584
1591
|
): ParsedDelegationToolPart | null {
|
|
1585
1592
|
if (part.type !== 'tool') return null
|
|
1586
|
-
if (
|
|
1593
|
+
if (
|
|
1594
|
+
part.tool !== 'pantheon_delegate' &&
|
|
1595
|
+
part.tool !== 'pantheon_delegation_read' &&
|
|
1596
|
+
part.tool !== 'task'
|
|
1597
|
+
)
|
|
1598
|
+
return null
|
|
1587
1599
|
const callID = part.callID
|
|
1588
1600
|
if (callID === undefined || callID === '') return null
|
|
1589
1601
|
const sessionID = part.sessionID ?? ''
|
|
@@ -1618,10 +1630,22 @@ export function parseDelegationToolPart(
|
|
|
1618
1630
|
}
|
|
1619
1631
|
}
|
|
1620
1632
|
|
|
1621
|
-
const agent =
|
|
1622
|
-
|
|
1633
|
+
const agent =
|
|
1634
|
+
typeof input.agent === 'string'
|
|
1635
|
+
? input.agent
|
|
1636
|
+
: typeof input.subagent_type === 'string'
|
|
1637
|
+
? (input.subagent_type as string)
|
|
1638
|
+
: 'agent'
|
|
1639
|
+
const description =
|
|
1640
|
+
typeof input.description === 'string'
|
|
1641
|
+
? input.description
|
|
1642
|
+
: typeof input.prompt === 'string'
|
|
1643
|
+
? (input.prompt as string).slice(0, 120)
|
|
1644
|
+
: ''
|
|
1623
1645
|
// The alias/taskID only exist once the delegate tool COMPLETES (they are
|
|
1624
|
-
// returned in its output); a running/pending part has neither.
|
|
1646
|
+
// returned in its output); a running/pending part has neither. Native
|
|
1647
|
+
// `task` parts never carry a board alias — the children channel supplies
|
|
1648
|
+
// the child session id, so the row renders `[native]`.
|
|
1625
1649
|
let alias: string | null = null
|
|
1626
1650
|
let taskID: string | null = null
|
|
1627
1651
|
if (status === 'completed') {
|
|
@@ -1633,7 +1657,7 @@ export function parseDelegationToolPart(
|
|
|
1633
1657
|
callID,
|
|
1634
1658
|
partID: part.id ?? '',
|
|
1635
1659
|
sessionID,
|
|
1636
|
-
tool: 'pantheon_delegate',
|
|
1660
|
+
tool: part.tool === 'task' ? 'task' : 'pantheon_delegate',
|
|
1637
1661
|
agent,
|
|
1638
1662
|
description,
|
|
1639
1663
|
status,
|
|
@@ -1691,9 +1715,11 @@ export function reduceDelegationToolPart(
|
|
|
1691
1715
|
return changed
|
|
1692
1716
|
}
|
|
1693
1717
|
|
|
1694
|
-
// Delegate tool: pending/running →
|
|
1695
|
-
// delegate tool only means the job was
|
|
1696
|
-
// we pick up alias + taskID from the
|
|
1718
|
+
// Delegate tool (pantheon_delegate AND native task): pending/running →
|
|
1719
|
+
// job launched (running). A COMPLETED delegate tool only means the job was
|
|
1720
|
+
// registered — it stays running and we pick up alias + taskID from the
|
|
1721
|
+
// output. Native `task` parts never yield a board alias, so their rows
|
|
1722
|
+
// stay `[native]` until the children channel resolves them.
|
|
1697
1723
|
const existing = map.get(parsed.callID)
|
|
1698
1724
|
if (parsed.status === 'error') {
|
|
1699
1725
|
if (existing !== undefined && existing.state === 'error' && existing.updatedAt === parsed.endAt)
|
|
@@ -1702,7 +1728,7 @@ export function reduceDelegationToolPart(
|
|
|
1702
1728
|
callID: parsed.callID,
|
|
1703
1729
|
partID: parsed.partID,
|
|
1704
1730
|
sessionID: parsed.sessionID,
|
|
1705
|
-
tool:
|
|
1731
|
+
tool: parsed.tool,
|
|
1706
1732
|
agent: parsed.agent ?? 'agent',
|
|
1707
1733
|
description: parsed.description,
|
|
1708
1734
|
alias: existing?.alias ?? null,
|
|
@@ -1719,7 +1745,7 @@ export function reduceDelegationToolPart(
|
|
|
1719
1745
|
callID: parsed.callID,
|
|
1720
1746
|
partID: parsed.partID,
|
|
1721
1747
|
sessionID: parsed.sessionID,
|
|
1722
|
-
tool:
|
|
1748
|
+
tool: parsed.tool,
|
|
1723
1749
|
agent: parsed.agent ?? 'agent',
|
|
1724
1750
|
description: parsed.description,
|
|
1725
1751
|
alias: parsed.alias,
|
|
@@ -1772,11 +1798,14 @@ export function removeDelegationEntry(
|
|
|
1772
1798
|
return false
|
|
1773
1799
|
}
|
|
1774
1800
|
|
|
1775
|
-
/** Collect pantheon delegation tool parts from a session's messages.
|
|
1801
|
+
/** Collect pantheon delegation + native task tool parts from a session's messages.
|
|
1776
1802
|
* Messages may carry their parts inline (duck-typed `msg.parts`); when
|
|
1777
1803
|
* they don't, the optional `getParts(messageID)` callback is used (the TUI
|
|
1778
|
-
* SDK exposes `api.state.part(messageID)`).
|
|
1779
|
-
*
|
|
1804
|
+
* SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
|
|
1805
|
+
* child session with parentID = caller — the same mechanism as
|
|
1806
|
+
* pantheon_delegate — so its parts feed the live-map as the native signal
|
|
1807
|
+
* (rows render `[native]` via the children channel). Pure w.r.t. I/O — used
|
|
1808
|
+
* by the mount re-scan to re-seed the live map after compaction/attach. */
|
|
1780
1809
|
export function collectDelegationToolParts(
|
|
1781
1810
|
messages: readonly { id?: string; parts?: unknown[] }[] | undefined,
|
|
1782
1811
|
getParts?: (messageID: string) => readonly unknown[] | undefined,
|
|
@@ -1794,7 +1823,9 @@ export function collectDelegationToolParts(
|
|
|
1794
1823
|
const part = raw as DelegationToolPart
|
|
1795
1824
|
if (
|
|
1796
1825
|
part?.type === 'tool' &&
|
|
1797
|
-
(part.tool === 'pantheon_delegate' ||
|
|
1826
|
+
(part.tool === 'pantheon_delegate' ||
|
|
1827
|
+
part.tool === 'pantheon_delegation_read' ||
|
|
1828
|
+
part.tool === 'task')
|
|
1798
1829
|
) {
|
|
1799
1830
|
out.push(part)
|
|
1800
1831
|
}
|
|
@@ -2000,6 +2031,32 @@ export function delegationTag(entry: DelegationEntry): string {
|
|
|
2000
2031
|
return entry.source === 'children-only' ? '[native]' : `[pantheon:${entry.alias}]`
|
|
2001
2032
|
}
|
|
2002
2033
|
|
|
2034
|
+
/** Split a display list into native task() rows vs pantheon_delegate rows.
|
|
2035
|
+
* Native = source 'children-only' (no board report); everything else counts
|
|
2036
|
+
* as pantheon. Pure — powers the header breakdown + the hooks.log line. */
|
|
2037
|
+
export function countDelegationSources(entries: readonly DelegationEntry[]): {
|
|
2038
|
+
native: number
|
|
2039
|
+
pantheon: number
|
|
2040
|
+
total: number
|
|
2041
|
+
} {
|
|
2042
|
+
let native = 0
|
|
2043
|
+
for (const e of entries) if (e.source === 'children-only') native++
|
|
2044
|
+
return { native, pantheon: entries.length - native, total: entries.length }
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
/** Diagnostic hooks.log line for a panel re-fetch, with the children
|
|
2048
|
+
* breakdown (pantheon = children WITH a board report, native = children
|
|
2049
|
+
* WITHOUT one). Pure — the View logs the returned string verbatim. */
|
|
2050
|
+
export function formatPanelLogLine(
|
|
2051
|
+
children: number,
|
|
2052
|
+
pantheon: number,
|
|
2053
|
+
native: number,
|
|
2054
|
+
md: number,
|
|
2055
|
+
events: number,
|
|
2056
|
+
): string {
|
|
2057
|
+
return `panel: children=${children}(pantheon=${pantheon} native=${native}) md=${md} events=${events}`
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2003
2060
|
/** Turn child sessions (PRIMARY) enriched with md reports into the display
|
|
2004
2061
|
* list. One entry per child id (duplicates across re-fetches collapse).
|
|
2005
2062
|
* The md report is matched by `Task ID` (== child.id) and supplies alias,
|
|
@@ -2008,10 +2065,14 @@ export function delegationTag(entry: DelegationEntry): string {
|
|
|
2008
2065
|
* itself (fallback 'agent'), state derived from its status, startedAt from
|
|
2009
2066
|
* time.created. A report-less child is a NATIVE task() child (every
|
|
2010
2067
|
* child of the current session — pantheon_delegate OR the native `task()`
|
|
2011
|
-
* tool — carries parentID = caller), so it gets source 'children-only'
|
|
2012
|
-
* and the `[native]` tag instead of a
|
|
2068
|
+
* tool — carries parentID = caller), so it gets source 'children-only',
|
|
2069
|
+
* the internal alias 'native-task' and the `[native]` tag instead of a
|
|
2070
|
+
* board alias. The 'task nativa' description fallback keeps the row
|
|
2071
|
+
* non-empty when the child carries no title.
|
|
2013
2072
|
* Terminal md state wins over the derived state; a running md defers to
|
|
2014
|
-
* the child's live status.
|
|
2073
|
+
* the child's live status. A running child is NEVER archived — it always
|
|
2074
|
+
* lands in the active split (splitDelegationList active = running/retry).
|
|
2075
|
+
* Sorted running-first (compareDelegationEntries).
|
|
2015
2076
|
* Pure — no I/O. */
|
|
2016
2077
|
export function childrenToDelegationEntries(
|
|
2017
2078
|
children: readonly ChildDelegationLike[] | undefined,
|
|
@@ -2033,7 +2094,7 @@ export function childrenToDelegationEntries(
|
|
|
2033
2094
|
? mdEntry.state
|
|
2034
2095
|
: childStatusToState(child.status)
|
|
2035
2096
|
out.push({
|
|
2036
|
-
alias: mdEntry?.alias ?? 'task',
|
|
2097
|
+
alias: mdEntry?.alias ?? 'native-task',
|
|
2037
2098
|
sessionID: mdEntry?.sessionID ?? '',
|
|
2038
2099
|
taskID: child.id,
|
|
2039
2100
|
agent: mdEntry?.agent ?? child.agent ?? 'agent',
|
|
@@ -2046,7 +2107,9 @@ export function childrenToDelegationEntries(
|
|
|
2046
2107
|
description:
|
|
2047
2108
|
mdEntry !== undefined && mdEntry.description !== ''
|
|
2048
2109
|
? mdEntry.description
|
|
2049
|
-
:
|
|
2110
|
+
: child.title !== undefined && child.title !== ''
|
|
2111
|
+
? child.title
|
|
2112
|
+
: 'task nativa',
|
|
2050
2113
|
source: mdEntry !== undefined ? 'md' : 'children-only',
|
|
2051
2114
|
})
|
|
2052
2115
|
}
|
|
@@ -2241,7 +2304,7 @@ function DelegationRow(props: {
|
|
|
2241
2304
|
{(a) => (
|
|
2242
2305
|
<text
|
|
2243
2306
|
fg={theme().textMuted}
|
|
2244
|
-
>{` \u21b3 ${a().tool}${a().summary !== '' ?
|
|
2307
|
+
>{` \u21b3 ${a().tool}${a().summary !== '' ? ` ${a().summary}` : ''}`}</text>
|
|
2245
2308
|
)}
|
|
2246
2309
|
</Show>
|
|
2247
2310
|
</box>
|
|
@@ -2370,9 +2433,10 @@ function View(props: {
|
|
|
2370
2433
|
// remains on the 1s ticker to avoid unnecessary work.
|
|
2371
2434
|
const [animationNow, setAnimationNow] = createSignal(Date.now())
|
|
2372
2435
|
// Cumulative count of event-triggered refreshes — logged with every fetch
|
|
2373
|
-
// ("panel: children=N md=N events=N") so the (0)
|
|
2374
|
-
// children>0 means the source works; events=0
|
|
2375
|
-
// delivering, and the 1s poll is what keeps
|
|
2436
|
+
// ("panel: children=N(pantheon=M native=K) md=N events=N") so the (0)
|
|
2437
|
+
// symptom is diagnosable: children>0 means the source works; events=0
|
|
2438
|
+
// means the event bus isn't delivering, and the 1s poll is what keeps
|
|
2439
|
+
// the panel honest.
|
|
2376
2440
|
let eventRefreshCount = 0
|
|
2377
2441
|
// In-flight guard: overlap between the poll, events and the version effect
|
|
2378
2442
|
// collapses into one fetch — no duplicate work, no duplicate entries.
|
|
@@ -2404,7 +2468,7 @@ function View(props: {
|
|
|
2404
2468
|
if (sessionID === null) {
|
|
2405
2469
|
setChildDelegations(md)
|
|
2406
2470
|
panelLog.info(
|
|
2407
|
-
|
|
2471
|
+
`${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — history shown)`,
|
|
2408
2472
|
)
|
|
2409
2473
|
return
|
|
2410
2474
|
}
|
|
@@ -2447,8 +2511,15 @@ function View(props: {
|
|
|
2447
2511
|
)
|
|
2448
2512
|
setChildDelegations(mergeChildDelegationSources(childEntries, liveEntries))
|
|
2449
2513
|
// 4. diagnostic line — every re-fetch (silence-by-default, hooks.log).
|
|
2514
|
+
const counts = countDelegationSources(childEntries)
|
|
2450
2515
|
panelLog.info(
|
|
2451
|
-
|
|
2516
|
+
formatPanelLogLine(
|
|
2517
|
+
children.length,
|
|
2518
|
+
counts.pantheon,
|
|
2519
|
+
counts.native,
|
|
2520
|
+
md.length,
|
|
2521
|
+
eventRefreshCount,
|
|
2522
|
+
),
|
|
2452
2523
|
)
|
|
2453
2524
|
} finally {
|
|
2454
2525
|
delegationsInflight = null
|
|
@@ -2464,6 +2535,9 @@ function View(props: {
|
|
|
2464
2535
|
...delegationSplit().active,
|
|
2465
2536
|
...delegationSplit().recent,
|
|
2466
2537
|
])
|
|
2538
|
+
// Header breakdown: native task() rows vs pantheon_delegate rows, so
|
|
2539
|
+
// Sessions(3) with 3 native children reads "Delegations (3 native + 0 pantheon)".
|
|
2540
|
+
const delegationCounts = createMemo(() => countDelegationSources(visibleDelegations()))
|
|
2467
2541
|
const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page)
|
|
2468
2542
|
const clampArchivedPage = (page: number, pages: number) => {
|
|
2469
2543
|
const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)))
|
|
@@ -2612,9 +2686,11 @@ function View(props: {
|
|
|
2612
2686
|
{/* ── Delegations (session.children primary + .pantheon/delegations md enrichment) ── */}
|
|
2613
2687
|
<box onMouseDown={() => setShowDelegations((x) => !x)}>
|
|
2614
2688
|
<text fg={theme().text} attributes={1}>
|
|
2615
|
-
{`${showDelegations() ? '
|
|
2689
|
+
{`${showDelegations() ? '▼' : '▶'} Delegations`}
|
|
2616
2690
|
</text>
|
|
2617
|
-
<text
|
|
2691
|
+
<text
|
|
2692
|
+
fg={theme().textMuted}
|
|
2693
|
+
>{` (${String(delegationCounts().native)} native + ${String(delegationCounts().pantheon)} pantheon)`}</text>
|
|
2618
2694
|
</box>
|
|
2619
2695
|
<Show when={showDelegations()}>
|
|
2620
2696
|
<Show
|