pantheon-opencode 1.5.0-beta.8 → 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 CHANGED
@@ -18,6 +18,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
18
18
 
19
19
  ## ✅ Closed Issues
20
20
 
21
+ ## [v1.5.0-beta.9] - 2026-09-14
22
+
23
+ <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
24
+ move this section to a versioned entry and reset the template below. -->
25
+
26
+ ## 🆕 What's New
27
+
28
+ - **TUI delegations nativas (gated):** painel Delegations renderiza N delegações native + M pantheon com tag [native], live task em tempo real, click navega para sessão. Implementado em `src/plugins/tui/src/index.tsx` com dist rebuildado.
29
+ - **93 testes TUI delegations:** suite `tests/pantheon/tui-delegations.test.ts` com 93/93 passando (tsc + biome OK).
30
+ - **E2E beta.8 PASS (referência):** baseline E2E da beta.8 mantido como referência de estabilidade para beta.9.
31
+
32
+ ## 🐞 Fixed
33
+
34
+ - Removido diretório `testyes/` (artefato de teste).
35
+ - Truncado `hooks.log` para baseline limpo de beta.9.
36
+
21
37
  ## [v1.5.0-beta.8] - 2026-09-13
22
38
 
23
39
  <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.5.0-beta.8",
3
+ "version": "1.5.0-beta.9",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon",
3
3
  "publisher": "ils15",
4
- "version": "1.5.0-beta.8",
4
+ "version": "1.5.0-beta.9",
5
5
  "displayName": "Pantheon",
6
6
  "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
7
7
  "agents": "./src/agents",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
4
4
 
5
5
  [project]
6
6
  name = "pantheon"
7
- version = "1.5.0-beta.8"
7
+ version = "1.5.0-beta.9"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
@@ -171,7 +171,7 @@ type LiveDelegationEntry = {
171
171
  partID: string;
172
172
  /** Parent session the delegation was launched from. */
173
173
  sessionID: string;
174
- tool: 'pantheon_delegate' | 'pantheon_delegation_read';
174
+ tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task';
175
175
  /** Agent name (from the delegate input args). */
176
176
  agent: string;
177
177
  description: string;
@@ -190,7 +190,7 @@ type ParsedDelegationToolPart = {
190
190
  callID: string;
191
191
  partID: string;
192
192
  sessionID: string;
193
- tool: 'pantheon_delegate' | 'pantheon_delegation_read';
193
+ tool: 'pantheon_delegate' | 'pantheon_delegation_read' | 'task';
194
194
  /** null for read parts (no agent arg — the id targets an existing job). */
195
195
  agent: string | null;
196
196
  description: string;
@@ -202,7 +202,9 @@ type ParsedDelegationToolPart = {
202
202
  };
203
203
  /** Extract the tool name + args from a `message.part.updated` part and
204
204
  * reduce it to what the panel needs. Returns null for anything that is
205
- * not a pantheon delegation tool part (or is missing its callID). */
205
+ * not a pantheon delegation tool part, the native `task` subagent tool
206
+ * (same parentID === caller mechanism — its children render `[native]`),
207
+ * or is missing its callID. */
206
208
  declare function parseDelegationToolPart(part: DelegationToolPart, now?: number): ParsedDelegationToolPart | null;
207
209
  /** Apply one tool part to the live map. Returns true when the map changed.
208
210
  * Pure w.r.t. I/O — only mutates `map`. */
@@ -210,11 +212,14 @@ declare function reduceDelegationToolPart(map: Map<string, LiveDelegationEntry>,
210
212
  /** Remove a live entry by part id (message.part.removed) or call id.
211
213
  * Returns true when something was removed. */
212
214
  declare function removeDelegationEntry(map: Map<string, LiveDelegationEntry>, partIDOrCallID: string): boolean;
213
- /** Collect pantheon delegation tool parts from a session's messages.
215
+ /** Collect pantheon delegation + native task tool parts from a session's messages.
214
216
  * Messages may carry their parts inline (duck-typed `msg.parts`); when
215
217
  * they don't, the optional `getParts(messageID)` callback is used (the TUI
216
- * SDK exposes `api.state.part(messageID)`). Pure w.r.t. I/O — used by the
217
- * mount re-scan to re-seed the live map after compaction/attach. */
218
+ * SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
219
+ * child session with parentID = caller — the same mechanism as
220
+ * pantheon_delegate — so its parts feed the live-map as the native signal
221
+ * (rows render `[native]` via the children channel). Pure w.r.t. I/O — used
222
+ * by the mount re-scan to re-seed the live map after compaction/attach. */
218
223
  declare function collectDelegationToolParts(messages: readonly {
219
224
  id?: string;
220
225
  parts?: unknown[];
@@ -318,6 +323,18 @@ declare function childStatusToState(status: string | undefined): 'running' | 'co
318
323
  * panel renders the tag with a distinct style so native task() children are
319
324
  * visually separable from pantheon_delegate jobs. */
320
325
  declare function delegationTag(entry: DelegationEntry): string;
326
+ /** Split a display list into native task() rows vs pantheon_delegate rows.
327
+ * Native = source 'children-only' (no board report); everything else counts
328
+ * as pantheon. Pure — powers the header breakdown + the hooks.log line. */
329
+ declare function countDelegationSources(entries: readonly DelegationEntry[]): {
330
+ native: number;
331
+ pantheon: number;
332
+ total: number;
333
+ };
334
+ /** Diagnostic hooks.log line for a panel re-fetch, with the children
335
+ * breakdown (pantheon = children WITH a board report, native = children
336
+ * WITHOUT one). Pure — the View logs the returned string verbatim. */
337
+ declare function formatPanelLogLine(children: number, pantheon: number, native: number, md: number, events: number): string;
321
338
  /** Turn child sessions (PRIMARY) enriched with md reports into the display
322
339
  * list. One entry per child id (duplicates across re-fetches collapse).
323
340
  * The md report is matched by `Task ID` (== child.id) and supplies alias,
@@ -326,10 +343,14 @@ declare function delegationTag(entry: DelegationEntry): string;
326
343
  * itself (fallback 'agent'), state derived from its status, startedAt from
327
344
  * time.created. A report-less child is a NATIVE task() child (every
328
345
  * child of the current session — pantheon_delegate OR the native `task()`
329
- * tool — carries parentID = caller), so it gets source 'children-only'
330
- * and the `[native]` tag instead of a board alias.
346
+ * tool — carries parentID = caller), so it gets source 'children-only',
347
+ * the internal alias 'native-task' and the `[native]` tag instead of a
348
+ * board alias. The 'task nativa' description fallback keeps the row
349
+ * non-empty when the child carries no title.
331
350
  * Terminal md state wins over the derived state; a running md defers to
332
- * the child's live status. Sorted running-first (compareDelegationEntries).
351
+ * the child's live status. A running child is NEVER archived — it always
352
+ * lands in the active split (splitDelegationList active = running/retry).
353
+ * Sorted running-first (compareDelegationEntries).
333
354
  * Pure — no I/O. */
334
355
  declare function childrenToDelegationEntries(children: readonly ChildDelegationLike[] | undefined, md: readonly DelegationEntry[], now?: number): DelegationEntry[];
335
356
  /** Navigate the TUI to a child session (click/Enter on a delegation row).
@@ -356,5 +377,5 @@ declare const plugin: TuiPluginModule & {
356
377
  setup: () => Promise<void>;
357
378
  };
358
379
  //#endregion
359
- export { ChildDelegationLike, DelegationActivity, DelegationEntry, DelegationToolPart, IDLE_SILENCE_MS, LiveDelegationEntry, LiveDelegationStore, ParsedDelegationToolPart, STALE_RUNNING_THRESHOLD_MS, ToolActivity, TuiSessionSources, buildChildrenPath, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationSpinnerFrame, delegationTag, extractToolActivity, fmtElapsed, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, tuiLogPath, visibleDelegationList };
380
+ export { ChildDelegationLike, DelegationActivity, DelegationEntry, DelegationToolPart, IDLE_SILENCE_MS, LiveDelegationEntry, LiveDelegationStore, ParsedDelegationToolPart, STALE_RUNNING_THRESHOLD_MS, ToolActivity, TuiSessionSources, buildChildrenPath, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationSpinnerFrame, delegationTag, extractToolActivity, fmtElapsed, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, tuiLogPath, visibleDelegationList };
360
381
  //# sourceMappingURL=tui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KAo7BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EAUA;;EAEA;EACA;EACA;;EAEA;;;;EAIA;;;;;;;;;;;iBAsBc,wBACd,aACA,oBACA,qBACC;;;;;;iBAkHmB,sBAAsB,cAAc,QAAQ;;;;;;;;iBAwC5C,yBAAyB,eAAe,QAAQ;;;;;;;;iBAWtD,sBACd;EAAS;EAAoB;eAC7B;;;;;;iBAYc,YAAY;;;iBAMZ,WAAW;;;;iBAOX,yBAAyB,GAAG,iBAAiB,GAAG;;;;;iBAahD,oBACd,cAAc,mBACd,oBACA,cACA;EACG,QAAQ;EAAmB,QAAQ;EAAmB,UAAU;;;;;iBAerD,sBACd,cAAc,mBACd,sBACA,cACA,4BACC;;cAMU;;;;cAKA;;;;;;;;;iBAUG,mBACd,OAAO,iBACP,aACA,uBACC;;;iBAWa,WAAW;;;iBAcX,kBAAkB,OAAO,iBAAiB;;;KAQ9C;iBAQI,mBAAmB,OAAO,kBAAkB;iBAc5C,wBAAwB,OAAO;;iBAqB/B,uBAAuB;KAY3B;EACV;EACA;EACA;;;;;iBA0Bc,oBACd;EACE;EACA;EACA;EACA;IAAU;IAAiB,QAAQ;;GAErC;EACG;EAAmB,UAAU;;;iBAYlB,kBACd,KAAK,YAAY,eACjB,mBACA,UAAU;;iBAWI,sBACd,KAAK,YAAY,eACjB,+BACA,cACA,iBACC;;;;;;;iBAca,4BACd,mBAAmB,mBACnB,eAAe,wBACd;;KAmFS;EACV;EACA;EACA;EACA;EACA;EACA;IACE;IACA,QAAQ;IACR;IACA;IACA;MAAS;MAAgB;;;;;KAKjB;;EAEV;;EAEA;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;EACA;EACA;EACA;;EAEA;;;KAIU;EACV;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;iBAac,wBACd,MAAM,oBACN,eACC;;;iBA+Ea,yBACd,KAAK,YAAY,sBACjB,MAAM,oBACN;;;iBA+Fc,sBACd,KAAK,YAAY,sBACjB;;;;;;iBAiBc,2BACd;EAAqB;EAAa;iBAClC,YAAY,uDACX;;;;;iBA2Ba,sBACd,KAAK,YAAY,sBACjB,gBAAgB,sBAChB;;;iBAWc,kBAAkB,MAAM,sBAAsB;;;;;;iBAqB9C,uBACd,eAAe,uBACf,aAAa,oBACZ;;;;;;;;;iBA8Da,iBAAiB,cAAc;;;;KAOnC;;EAEV;EACA;;IAEE;MAAU;;;IAEV;MAAU;QAAY;QAAe,SAAS;;;;;;;;;;iBASlC,wBAAwB,SAAS;;;;;;;;iBAmBjC,gBAAgB;EAAgB;IAAQ;;;;;;;iBASxC,kBAAkB;EAAkC;IAAQ;;;;KAKhE;;EAEV;;EAEA;;;EAGA;;;EAGA;EACA;IACE;IACA;;;;;;;iBAQY,mBAAmB;;;;;;;iBAYnB,cAAc,OAAO;;;;;;;;;;;;;;iBAiBrB,4BACd,mBAAmB,mCACnB,aAAa,mBACb,eACC;;;;;;iBA0Ca,4BACd;EAAS,YAAY,cAAc,SAAS;eAC5C;;;;;KAiMU;EACV,KAAK,YAAY;;EAEjB;;EAEA;;cA0fI,QAAQ;EAAoB;EAAY,aAAa"}
1
+ {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KAu7BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EAUA;;EAEA;EACA;EACA;;EAEA;;;;EAIA;;;;;;;;;;;iBAsBc,wBACd,aACA,oBACA,qBACC;;;;;;iBAkHmB,sBAAsB,cAAc,QAAQ;;;;;;;;iBAwC5C,yBAAyB,eAAe,QAAQ;;;;;;;;iBAWtD,sBACd;EAAS;EAAoB;eAC7B;;;;;;iBAYc,YAAY;;;iBAMZ,WAAW;;;;iBAOX,yBAAyB,GAAG,iBAAiB,GAAG;;;;;iBAahD,oBACd,cAAc,mBACd,oBACA,cACA;EACG,QAAQ;EAAmB,QAAQ;EAAmB,UAAU;;;;;iBAerD,sBACd,cAAc,mBACd,sBACA,cACA,4BACC;;cAMU;;;;cAKA;;;;;;;;;iBAUG,mBACd,OAAO,iBACP,aACA,uBACC;;;iBAWa,WAAW;;;iBAcX,kBAAkB,OAAO,iBAAiB;;;KAQ9C;iBAQI,mBAAmB,OAAO,kBAAkB;iBAc5C,wBAAwB,OAAO;;iBAqB/B,uBAAuB;KAY3B;EACV;EACA;EACA;;;;;iBA0Bc,oBACd;EACE;EACA;EACA;EACA;IAAU;IAAiB,QAAQ;;GAErC;EACG;EAAmB,UAAU;;;iBAYlB,kBACd,KAAK,YAAY,eACjB,mBACA,UAAU;;iBAWI,sBACd,KAAK,YAAY,eACjB,+BACA,cACA,iBACC;;;;;;;iBAca,4BACd,mBAAmB,mBACnB,eAAe,wBACd;;KAqFS;EACV;EACA;EACA;EACA;EACA;EACA;IACE;IACA,QAAQ;IACR;IACA;IACA;MAAS;MAAgB;;;;;KAKjB;;EAEV;;EAEA;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;EACA;EACA;EACA;;EAEA;;;KAIU;EACV;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAec,wBACd,MAAM,oBACN,eACC;;;iBAgGa,yBACd,KAAK,YAAY,sBACjB,MAAM,oBACN;;;iBAiGc,sBACd,KAAK,YAAY,sBACjB;;;;;;;;;iBAoBc,2BACd;EAAqB;EAAa;iBAClC,YAAY,uDACX;;;;;iBA6Ba,sBACd,KAAK,YAAY,sBACjB,gBAAgB,sBAChB;;;iBAWc,kBAAkB,MAAM,sBAAsB;;;;;;iBAqB9C,uBACd,eAAe,uBACf,aAAa,oBACZ;;;;;;;;;iBA8Da,iBAAiB,cAAc;;;;KAOnC;;EAEV;EACA;;IAEE;MAAU;;;IAEV;MAAU;QAAY;QAAe,SAAS;;;;;;;;;;iBASlC,wBAAwB,SAAS;;;;;;;;iBAmBjC,gBAAgB;EAAgB;IAAQ;;;;;;;iBASxC,kBAAkB;EAAkC;IAAQ;;;;KAKhE;;EAEV;;EAEA;;;EAGA;;;EAGA;EACA;IACE;IACA;;;;;;;iBAQY,mBAAmB;;;;;;;iBAYnB,cAAc,OAAO;;;;iBAOrB,uBAAuB,kBAAkB;EACvD;EACA;EACA;;;;;iBAUc,mBACd,kBACA,kBACA,gBACA,YACA;;;;;;;;;;;;;;;;;;iBAsBc,4BACd,mBAAmB,mCACnB,aAAa,mBACb,eACC;;;;;;iBA4Ca,4BACd;EAAS,YAAY,cAAc,SAAS;eAC5C;;;;;KAiMU;EACV,KAAK,YAAY;;EAEjB;;EAEA;;cAugBI,QAAQ;EAAoB;EAAY,aAAa"}
@@ -164,7 +164,7 @@ async function detectVersion(api) {
164
164
  if (ver) return ver;
165
165
  }
166
166
  } catch {}
167
- return "1.5.0-beta.8";
167
+ return "1.5.0-beta.9";
168
168
  }
169
169
  /**
170
170
  * usage-bar — AI subscription usage gauge for the opencode TUI.
@@ -1095,10 +1095,12 @@ const DELEGATE_TASKID_PATTERN = /\(task\s+([a-z0-9_]+)\)/i;
1095
1095
  const READ_ALIAS_PATTERN = /^[a-z]{2,8}-\d+$/i;
1096
1096
  /** Extract the tool name + args from a `message.part.updated` part and
1097
1097
  * reduce it to what the panel needs. Returns null for anything that is
1098
- * not a pantheon delegation tool part (or is missing its callID). */
1098
+ * not a pantheon delegation tool part, the native `task` subagent tool
1099
+ * (same parentID === caller mechanism — its children render `[native]`),
1100
+ * or is missing its callID. */
1099
1101
  function parseDelegationToolPart(part, now = Date.now()) {
1100
1102
  if (part.type !== "tool") return null;
1101
- if (part.tool !== "pantheon_delegate" && part.tool !== "pantheon_delegation_read") return null;
1103
+ if (part.tool !== "pantheon_delegate" && part.tool !== "pantheon_delegation_read" && part.tool !== "task") return null;
1102
1104
  const callID = part.callID;
1103
1105
  if (callID === void 0 || callID === "") return null;
1104
1106
  const sessionID = part.sessionID ?? "";
@@ -1130,8 +1132,8 @@ function parseDelegationToolPart(part, now = Date.now()) {
1130
1132
  endAt
1131
1133
  };
1132
1134
  }
1133
- const agent = typeof input.agent === "string" ? input.agent : "agent";
1134
- const description = typeof input.description === "string" ? input.description : "";
1135
+ const agent = typeof input.agent === "string" ? input.agent : typeof input.subagent_type === "string" ? input.subagent_type : "agent";
1136
+ const description = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt.slice(0, 120) : "";
1135
1137
  let alias = null;
1136
1138
  let taskID = null;
1137
1139
  if (status === "completed") {
@@ -1143,7 +1145,7 @@ function parseDelegationToolPart(part, now = Date.now()) {
1143
1145
  callID,
1144
1146
  partID: part.id ?? "",
1145
1147
  sessionID,
1146
- tool: "pantheon_delegate",
1148
+ tool: part.tool === "task" ? "task" : "pantheon_delegate",
1147
1149
  agent,
1148
1150
  description,
1149
1151
  status,
@@ -1192,7 +1194,7 @@ function reduceDelegationToolPart(map, part, now = Date.now()) {
1192
1194
  callID: parsed.callID,
1193
1195
  partID: parsed.partID,
1194
1196
  sessionID: parsed.sessionID,
1195
- tool: "pantheon_delegate",
1197
+ tool: parsed.tool,
1196
1198
  agent: parsed.agent ?? "agent",
1197
1199
  description: parsed.description,
1198
1200
  alias: existing?.alias ?? null,
@@ -1209,7 +1211,7 @@ function reduceDelegationToolPart(map, part, now = Date.now()) {
1209
1211
  callID: parsed.callID,
1210
1212
  partID: parsed.partID,
1211
1213
  sessionID: parsed.sessionID,
1212
- tool: "pantheon_delegate",
1214
+ tool: parsed.tool,
1213
1215
  agent: parsed.agent ?? "agent",
1214
1216
  description: parsed.description,
1215
1217
  alias: parsed.alias,
@@ -1254,11 +1256,14 @@ function removeDelegationEntry(map, partIDOrCallID) {
1254
1256
  }
1255
1257
  return false;
1256
1258
  }
1257
- /** Collect pantheon delegation tool parts from a session's messages.
1259
+ /** Collect pantheon delegation + native task tool parts from a session's messages.
1258
1260
  * Messages may carry their parts inline (duck-typed `msg.parts`); when
1259
1261
  * they don't, the optional `getParts(messageID)` callback is used (the TUI
1260
- * SDK exposes `api.state.part(messageID)`). Pure w.r.t. I/O — used by the
1261
- * mount re-scan to re-seed the live map after compaction/attach. */
1262
+ * SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
1263
+ * child session with parentID = caller — the same mechanism as
1264
+ * pantheon_delegate — so its parts feed the live-map as the native signal
1265
+ * (rows render `[native]` via the children channel). Pure w.r.t. I/O — used
1266
+ * by the mount re-scan to re-seed the live map after compaction/attach. */
1262
1267
  function collectDelegationToolParts(messages, getParts) {
1263
1268
  const out = [];
1264
1269
  for (const msg of messages ?? []) {
@@ -1268,7 +1273,7 @@ function collectDelegationToolParts(messages, getParts) {
1268
1273
  if (!parts) continue;
1269
1274
  for (const raw of parts) {
1270
1275
  const part = raw;
1271
- if (part?.type === "tool" && (part.tool === "pantheon_delegate" || part.tool === "pantheon_delegation_read")) out.push(part);
1276
+ if (part?.type === "tool" && (part.tool === "pantheon_delegate" || part.tool === "pantheon_delegation_read" || part.tool === "task")) out.push(part);
1272
1277
  }
1273
1278
  }
1274
1279
  return out;
@@ -1393,6 +1398,24 @@ function childStatusToState(status) {
1393
1398
  function delegationTag(entry) {
1394
1399
  return entry.source === "children-only" ? "[native]" : `[pantheon:${entry.alias}]`;
1395
1400
  }
1401
+ /** Split a display list into native task() rows vs pantheon_delegate rows.
1402
+ * Native = source 'children-only' (no board report); everything else counts
1403
+ * as pantheon. Pure — powers the header breakdown + the hooks.log line. */
1404
+ function countDelegationSources(entries) {
1405
+ let native = 0;
1406
+ for (const e of entries) if (e.source === "children-only") native++;
1407
+ return {
1408
+ native,
1409
+ pantheon: entries.length - native,
1410
+ total: entries.length
1411
+ };
1412
+ }
1413
+ /** Diagnostic hooks.log line for a panel re-fetch, with the children
1414
+ * breakdown (pantheon = children WITH a board report, native = children
1415
+ * WITHOUT one). Pure — the View logs the returned string verbatim. */
1416
+ function formatPanelLogLine(children, pantheon, native, md, events) {
1417
+ return `panel: children=${children}(pantheon=${pantheon} native=${native}) md=${md} events=${events}`;
1418
+ }
1396
1419
  /** Turn child sessions (PRIMARY) enriched with md reports into the display
1397
1420
  * list. One entry per child id (duplicates across re-fetches collapse).
1398
1421
  * The md report is matched by `Task ID` (== child.id) and supplies alias,
@@ -1401,10 +1424,14 @@ function delegationTag(entry) {
1401
1424
  * itself (fallback 'agent'), state derived from its status, startedAt from
1402
1425
  * time.created. A report-less child is a NATIVE task() child (every
1403
1426
  * child of the current session — pantheon_delegate OR the native `task()`
1404
- * tool — carries parentID = caller), so it gets source 'children-only'
1405
- * and the `[native]` tag instead of a board alias.
1427
+ * tool — carries parentID = caller), so it gets source 'children-only',
1428
+ * the internal alias 'native-task' and the `[native]` tag instead of a
1429
+ * board alias. The 'task nativa' description fallback keeps the row
1430
+ * non-empty when the child carries no title.
1406
1431
  * Terminal md state wins over the derived state; a running md defers to
1407
- * the child's live status. Sorted running-first (compareDelegationEntries).
1432
+ * the child's live status. A running child is NEVER archived — it always
1433
+ * lands in the active split (splitDelegationList active = running/retry).
1434
+ * Sorted running-first (compareDelegationEntries).
1408
1435
  * Pure — no I/O. */
1409
1436
  function childrenToDelegationEntries(children, md, now = Date.now()) {
1410
1437
  const byTaskID = /* @__PURE__ */ new Map();
@@ -1417,7 +1444,7 @@ function childrenToDelegationEntries(children, md, now = Date.now()) {
1417
1444
  const mdEntry = byTaskID.get(child.id);
1418
1445
  const state = mdEntry !== void 0 && mdEntry.state !== "running" ? mdEntry.state : childStatusToState(child.status);
1419
1446
  out.push({
1420
- alias: mdEntry?.alias ?? "task",
1447
+ alias: mdEntry?.alias ?? "native-task",
1421
1448
  sessionID: mdEntry?.sessionID ?? "",
1422
1449
  taskID: child.id,
1423
1450
  agent: mdEntry?.agent ?? child.agent ?? "agent",
@@ -1425,7 +1452,7 @@ function childrenToDelegationEntries(children, md, now = Date.now()) {
1425
1452
  startedAt: mdEntry?.startedAt ?? child.time?.created ?? now,
1426
1453
  updatedAt: mdEntry?.updatedAt ?? (state === "running" || state === "stale-running" ? null : child.time?.updated ?? null),
1427
1454
  timedOut: mdEntry?.timedOut ?? false,
1428
- description: mdEntry !== void 0 && mdEntry.description !== "" ? mdEntry.description : child.title ?? "",
1455
+ description: mdEntry !== void 0 && mdEntry.description !== "" ? mdEntry.description : child.title !== void 0 && child.title !== "" ? child.title : "task nativa",
1429
1456
  source: mdEntry !== void 0 ? "md" : "children-only"
1430
1457
  });
1431
1458
  }
@@ -1549,7 +1576,7 @@ function DelegationRow(props) {
1549
1576
  },
1550
1577
  children: (a) => (() => {
1551
1578
  var _el$19 = createElement("text");
1552
- insert(_el$19, () => ` \u21b3 ${a().tool}${a().summary !== "" ? " " + a().summary : ""}`);
1579
+ insert(_el$19, () => ` \u21b3 ${a().tool}${a().summary !== "" ? ` ${a().summary}` : ""}`);
1553
1580
  effect((_$p) => setProp(_el$19, "fg", theme().textMuted, _$p));
1554
1581
  return _el$19;
1555
1582
  })()
@@ -1647,7 +1674,7 @@ function View(props) {
1647
1674
  });
1648
1675
  if (sessionID === null) {
1649
1676
  setChildDelegations(md);
1650
- panelLog.info(`panel: children=0 md=${md.length} events=${eventRefreshCount} (no sessionID — history shown)`);
1677
+ panelLog.info(`${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — history shown)`);
1651
1678
  return;
1652
1679
  }
1653
1680
  let children = [];
@@ -1674,7 +1701,8 @@ function View(props) {
1674
1701
  for (const [k, v] of props.liveStore.map) if (v.alias === null && v.taskID === null && Date.now() - v.startedAt > 3e4) props.liveStore.map.delete(k);
1675
1702
  const liveEntries = [...props.liveStore.map.values()].filter((entry) => entry.sessionID === sessionID);
1676
1703
  setChildDelegations(mergeChildDelegationSources(childEntries, liveEntries));
1677
- panelLog.info(`panel: children=${children.length} md=${md.length} events=${eventRefreshCount}`);
1704
+ const counts = countDelegationSources(childEntries);
1705
+ panelLog.info(formatPanelLogLine(children.length, counts.pantheon, counts.native, md.length, eventRefreshCount));
1678
1706
  } finally {
1679
1707
  delegationsInflight = null;
1680
1708
  }
@@ -1683,6 +1711,7 @@ function View(props) {
1683
1711
  };
1684
1712
  const delegationSplit = createMemo(() => splitDelegationList(childDelegations()));
1685
1713
  const visibleDelegations = createMemo(() => [...delegationSplit().active, ...delegationSplit().recent]);
1714
+ const delegationCounts = createMemo(() => countDelegationSources(visibleDelegations()));
1686
1715
  const [archivedPage, setArchivedPage] = createSignal(archivedUiState.page);
1687
1716
  const clampArchivedPage = (page, pages) => {
1688
1717
  const clamped = Math.max(0, Math.min(page, Math.max(0, pages - 1)));
@@ -1852,7 +1881,7 @@ function View(props) {
1852
1881
  setProp(_el$28, "onMouseDown", () => setShowDelegations((x) => !x));
1853
1882
  setProp(_el$29, "attributes", 1);
1854
1883
  insert(_el$29, () => `${showDelegations() ? "▼" : "▶"} Delegations`);
1855
- insert(_el$30, () => ` (${String(visibleDelegations().length)})`);
1884
+ insert(_el$30, () => ` (${String(delegationCounts().native)} native + ${String(delegationCounts().pantheon)} pantheon)`);
1856
1885
  insert(_el$22, createComponent(Show, {
1857
1886
  get when() {
1858
1887
  return showDelegations();
@@ -2033,6 +2062,6 @@ const plugin = {
2033
2062
  setup: async () => {}
2034
2063
  };
2035
2064
  //#endregion
2036
- export { IDLE_SILENCE_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationSpinnerFrame, delegationTag, extractToolActivity, fmtElapsed, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, tuiLogPath, visibleDelegationList };
2065
+ export { IDLE_SILENCE_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationSpinnerFrame, delegationTag, extractToolActivity, fmtElapsed, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, tuiLogPath, visibleDelegationList };
2037
2066
 
2038
2067
  //# sourceMappingURL=tui.js.map
@@ -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.8",
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.8",
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.8",
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