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.
@@ -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 "panel: children=N md=N events=N"
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. A children fetch
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) or pantheon_delegation_read (blocking until terminal → closes
1513
- * the entry). Shape-adapted from the SDK v2 ToolPart but duck-typed so the
1514
- * pure helpers are testable without the SDK. */
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 (or is missing its callID). */
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 (part.tool !== 'pantheon_delegate' && part.tool !== 'pantheon_delegation_read') return null
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 = typeof input.agent === 'string' ? input.agent : 'agent'
1622
- const description = typeof input.description === 'string' ? input.description : ''
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 → job launched (running). A COMPLETED
1695
- // delegate tool only means the job was registered — it stays running and
1696
- // we pick up alias + taskID from the output.
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: 'pantheon_delegate',
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: 'pantheon_delegate',
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)`). Pure w.r.t. I/O — used by the
1779
- * mount re-scan to re-seed the live map after compaction/attach. */
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' || part.tool === 'pantheon_delegation_read')
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 board alias.
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. Sorted running-first (compareDelegationEntries).
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
- : (child.title ?? ''),
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 !== '' ? ' ' + a().summary : ''}`}</text>
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) symptom is diagnosable:
2374
- // children>0 means the source works; events=0 means the event bus isn't
2375
- // delivering, and the 1s poll is what keeps the panel honest.
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
- `panel: children=0 md=${md.length} events=${eventRefreshCount} (no sessionID — history shown)`,
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
- `panel: children=${children.length} md=${md.length} events=${eventRefreshCount}`,
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() ? '\u25bc' : '\u25b6'} Delegations`}
2689
+ {`${showDelegations() ? '▼' : '▶'} Delegations`}
2616
2690
  </text>
2617
- <text fg={theme().textMuted}>{` (${String(visibleDelegations().length)})`}</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.7",
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.7",
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.7",
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 "panel: children=N md=N events=N"
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. A children fetch
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) or pantheon_delegation_read (blocking until terminal → closes
1513
- * the entry). Shape-adapted from the SDK v2 ToolPart but duck-typed so the
1514
- * pure helpers are testable without the SDK. */
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 (or is missing its callID). */
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 (part.tool !== 'pantheon_delegate' && part.tool !== 'pantheon_delegation_read') return null
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 = typeof input.agent === 'string' ? input.agent : 'agent'
1622
- const description = typeof input.description === 'string' ? input.description : ''
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 → job launched (running). A COMPLETED
1695
- // delegate tool only means the job was registered — it stays running and
1696
- // we pick up alias + taskID from the output.
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: 'pantheon_delegate',
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: 'pantheon_delegate',
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)`). Pure w.r.t. I/O — used by the
1779
- * mount re-scan to re-seed the live map after compaction/attach. */
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' || part.tool === 'pantheon_delegation_read')
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 board alias.
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. Sorted running-first (compareDelegationEntries).
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
- : (child.title ?? ''),
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 !== '' ? ' ' + a().summary : ''}`}</text>
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) symptom is diagnosable:
2374
- // children>0 means the source works; events=0 means the event bus isn't
2375
- // delivering, and the 1s poll is what keeps the panel honest.
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
- `panel: children=0 md=${md.length} events=${eventRefreshCount} (no sessionID — history shown)`,
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
- `panel: children=${children.length} md=${md.length} events=${eventRefreshCount}`,
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() ? '\u25bc' : '\u25b6'} Delegations`}
2689
+ {`${showDelegations() ? '▼' : '▶'} Delegations`}
2616
2690
  </text>
2617
- <text fg={theme().textMuted}>{` (${String(visibleDelegations().length)})`}</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