pantheon-opencode 1.3.3 → 1.3.5-beta.52.c75b04d

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.
Files changed (52) hide show
  1. package/AGENTS.md +919 -0
  2. package/README.md +157 -15
  3. package/bin/pantheon-init.mjs +177 -120
  4. package/opencode.json +3 -6
  5. package/package.json +8 -4
  6. package/scripts/build-agents-md.mjs +195 -0
  7. package/scripts/cost.mjs +17 -0
  8. package/scripts/doctor.mjs +56 -22
  9. package/scripts/hooks/on-subagent-delegation-stop.sh +22 -5
  10. package/scripts/install/opencode.mjs +189 -38
  11. package/scripts/install/plugin.mjs +112 -28
  12. package/scripts/install-mcp.mjs +1 -1
  13. package/scripts/redaction-gate.mjs +69 -0
  14. package/scripts/release-beta-version.mjs +80 -0
  15. package/scripts/release-notes.mjs +317 -0
  16. package/scripts/test-context-compression.sh +5 -2
  17. package/scripts/validate-package.mjs +56 -0
  18. package/scripts/versioning.mjs +117 -18
  19. package/src/agents/zeus.md +6 -2
  20. package/src/pantheon/background-job-board.ts +24 -38
  21. package/src/pantheon/chat-reminders.ts +79 -0
  22. package/src/pantheon/compaction-assert.ts +160 -0
  23. package/src/pantheon/cost-command.ts +57 -18
  24. package/src/pantheon/delegation-compaction.ts +143 -16
  25. package/src/pantheon/delegation-finalize.ts +8 -1
  26. package/src/pantheon/delegation-notify.ts +19 -157
  27. package/src/pantheon/delegation.ts +330 -28
  28. package/src/pantheon/dispatch-guard.ts +9 -4
  29. package/src/pantheon/goal-loop.ts +7 -4
  30. package/src/pantheon/hashline/tool.ts +2 -1
  31. package/src/pantheon/logger.ts +117 -0
  32. package/src/pantheon/plugin-once.ts +76 -0
  33. package/src/pantheon/preemptive-compact.ts +76 -0
  34. package/src/pantheon/presets.d.mts +31 -0
  35. package/src/pantheon/presets.mjs +105 -3
  36. package/src/pantheon/todo-enforcer.ts +79 -27
  37. package/src/pantheon/todo-preserve.ts +295 -0
  38. package/src/plugin.ts +177 -50
  39. package/src/plugins/pantheon-hooks.ts +35 -22
  40. package/src/plugins/tui/dist/server.d.ts +17 -0
  41. package/src/plugins/tui/dist/server.d.ts.map +1 -0
  42. package/src/plugins/tui/dist/server.js +20 -0
  43. package/src/plugins/tui/dist/server.js.map +1 -0
  44. package/src/plugins/tui/dist/tui.d.ts +303 -1
  45. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  46. package/src/plugins/tui/dist/tui.js +977 -350
  47. package/src/plugins/tui/dist/tui.tsx +1427 -150
  48. package/src/plugins/tui/package.json +4 -3
  49. package/src/plugins/tui/src/index.tsx +1427 -150
  50. package/src/plugins/tui/src/server.ts +15 -0
  51. package/src/plugins/tui/tsdown.config.ts +1 -1
  52. package/src/routing.yml +3 -1
@@ -186,7 +186,14 @@ import { appendFile, mkdir } from 'node:fs/promises'
186
186
  import { join } from 'node:path'
187
187
  import type { Plugin, PluginInput } from '@opencode-ai/plugin'
188
188
  import type { Part } from '@opencode-ai/sdk'
189
+ import {
190
+ CHAT_REMINDER_TTL_MS,
191
+ drainFreshChatReminders,
192
+ enqueueChatReminder,
193
+ pendingChatReminders,
194
+ } from '../pantheon/chat-reminders.ts'
189
195
  import { type HookPayload, type HookResult, runHook } from './hook-runner.ts'
196
+ import { pantheonPluginOnce } from '../pantheon/plugin-once.ts'
190
197
 
191
198
  /** Tools that represent a subagent delegation (opencode `task` tool etc.). */
192
199
  const DELEGATION_TOOL_RE = /^(task|.*delegate.*|.*subagent.*)$/i
@@ -424,6 +431,15 @@ const TASK_ID_TEXT_RE = /\btask\s+id="(ses_[^"]+)"/i
424
431
  * opencode 1.18.13 drops tui.toast.show events (untagged), so every signal
425
432
  * that would fire a toast is ALSO queued here and injected into the next user
426
433
  * message by the 'chat.message' hook as a single <system-reminder> text part.
434
+ *
435
+ * The buffer itself lives in the SHARED chat-reminders.ts module: this plugin
436
+ * file must export EXACTLY ONE function-valued export (legacy loader —
437
+ * see the IMPORTANT note at the top), so the enqueue API cannot be exported
438
+ * from here. Importing the shared module instead gives src/plugin.ts (a
439
+ * separate plugin file, loaded via plain dynamic import) the SAME buffer
440
+ * instance — release-134 Phase 4 (compaction-assert.ts) enqueues
441
+ * post-compaction state re-assertions here and this hook delivers them.
442
+ *
427
443
  * Bounded: at most CHAT_REMINDER_MAX entries, each expiring
428
444
  * CHAT_REMINDER_TTL_MS after its tool event. Consumed (and cleared) by the
429
445
  * chat.message hook. Full-buffer enqueues are SKIPPED, never unbounded — the
@@ -432,20 +448,6 @@ const TASK_ID_TEXT_RE = /\btask\s+id="(ses_[^"]+)"/i
432
448
  * session.idle flushes the buffer into ONE fresh aggregated entry (see
433
449
  * flushIdleReminders) so completions survive until the next user message.
434
450
  */
435
- const CHAT_REMINDER_MAX = 10
436
- const CHAT_REMINDER_TTL_MS = 60_000
437
- const pendingChatReminders: { text: string; at: number }[] = []
438
-
439
- /** Queue one chat-reminder line, pruning expired entries first. Never throws. */
440
- function enqueueChatReminder(text: string): void {
441
- const now = Date.now()
442
- for (let i = pendingChatReminders.length - 1; i >= 0; i--) {
443
- const r = pendingChatReminders[i]
444
- if (r !== undefined && now - r.at > CHAT_REMINDER_TTL_MS) pendingChatReminders.splice(i, 1)
445
- }
446
- if (pendingChatReminders.length >= CHAT_REMINDER_MAX) return
447
- pendingChatReminders.push({ text, at: now })
448
- }
449
451
 
450
452
  /** Variants accepted by the opencode TUI showToast (TuiShowToastData.body.variant). */
451
453
  type ToastVariant = 'info' | 'success' | 'warning' | 'error'
@@ -1124,10 +1126,13 @@ function flushIdleReminders(ctx: HookContext): void {
1124
1126
 
1125
1127
  // ONE aggregated entry, fresh timestamp — chat.message delivers it next.
1126
1128
  enqueueChatReminder(lines.join('\n'))
1129
+ // 1.3.4 dedup: log a SUMMARY here (count only), never the joined body — the
1130
+ // SAME lines used to be echoed twice (idle-flush with " | ", chat-reminder
1131
+ // delivery with "\n"). The content is logged exactly once, at delivery.
1127
1132
  void reportFailure(
1128
1133
  ctx,
1129
- `[pantheon-hooks:idle-flush] ${lines.join(' | ')}`,
1130
- { script: 'idle-flush', count: fresh.length },
1134
+ `[pantheon-hooks:idle-flush] ${fresh.length} reminder(s) flushed into the chat buffer (${lines.length} line(s) after aggregation)`,
1135
+ { script: 'idle-flush', count: fresh.length, lineCount: lines.length },
1131
1136
  'info',
1132
1137
  )
1133
1138
  }
@@ -1203,6 +1208,17 @@ async function safeRun(
1203
1208
  }
1204
1209
 
1205
1210
  const plugin: Plugin = async ({ client, directory }) => {
1211
+ // release-134: runtime guard against DOUBLE registration (duplicate toasts +
1212
+ // duplicate task_ids in one process — 2026-08-12 live logs). opencode
1213
+ // merges the global config (npm-package plugin paths) with the project
1214
+ // config (repo source paths), so this module loads from TWO filesystem
1215
+ // paths in the same process and the factory would otherwise run twice —
1216
+ // every hook (tool.execute.before/after, chat.message, event) firing
1217
+ // TWICE. The second invocation becomes a no-op (empty hooks object) so
1218
+ // hooks register exactly once.
1219
+ if (pantheonPluginOnce('pantheon:hooks')) {
1220
+ return {}
1221
+ }
1206
1222
  const ctx: HookContext = { client, directory }
1207
1223
  return {
1208
1224
  'tool.execute.before': async (input, output) => {
@@ -1445,11 +1461,8 @@ const plugin: Plugin = async ({ client, directory }) => {
1445
1461
  // Early return BEFORE the buffer drain keeps the reminder queued for
1446
1462
  // the parent's next real (msg_-ID'd) message.
1447
1463
  if (!input.messageID) return
1448
- const now = Date.now()
1449
- const fresh = pendingChatReminders.filter((r) => now - r.at <= CHAT_REMINDER_TTL_MS)
1450
- pendingChatReminders.length = 0
1451
- if (fresh.length === 0) return
1452
- const body = fresh.map((r) => r.text).join('\n')
1464
+ const body = drainFreshChatReminders()
1465
+ if (body === undefined) return
1453
1466
  // Minimal TextPart — the core backfills messageID/sessionID for
1454
1467
  // hook-injected parts (see opencode chat.message trigger pipeline).
1455
1468
  // The guard above guarantees input.messageID is a non-empty string,
@@ -1464,7 +1477,7 @@ const plugin: Plugin = async ({ client, directory }) => {
1464
1477
  await reportFailure(
1465
1478
  ctx,
1466
1479
  `[pantheon-hooks:chat-reminder] ${body}`,
1467
- { script: 'chat-reminder', count: fresh.length },
1480
+ { script: 'chat-reminder' },
1468
1481
  'info',
1469
1482
  )
1470
1483
  } catch {
@@ -0,0 +1,17 @@
1
+ //#region src/server.d.ts
2
+ /**
3
+ * No-op server stub for the opencode plugin loader.
4
+ *
5
+ * The opencode plugin host requires every plugin (TUI included) to expose a
6
+ * `server()` entry via `exports["./server"]` (or package.json `main`). When
7
+ * that entry is missing the host fails to attach the plugin's server half and
8
+ * the TUI runtime never wires the reactive channel — the initial JSX mounts
9
+ * but effects/refresh/events stay dead (the "mudo panel" symptom).
10
+ *
11
+ * This stub satisfies the loader contract; pantheon-tui is TUI-only and has
12
+ * no background server work of its own.
13
+ */
14
+ declare function server(): {};
15
+ //#endregion
16
+ export { server as default };
17
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;;;;;iBAYwB"}
@@ -0,0 +1,20 @@
1
+ //#region src/server.ts
2
+ /**
3
+ * No-op server stub for the opencode plugin loader.
4
+ *
5
+ * The opencode plugin host requires every plugin (TUI included) to expose a
6
+ * `server()` entry via `exports["./server"]` (or package.json `main`). When
7
+ * that entry is missing the host fails to attach the plugin's server half and
8
+ * the TUI runtime never wires the reactive channel — the initial JSX mounts
9
+ * but effects/refresh/events stay dead (the "mudo panel" symptom).
10
+ *
11
+ * This stub satisfies the loader contract; pantheon-tui is TUI-only and has
12
+ * no background server work of its own.
13
+ */
14
+ function server() {
15
+ return {};
16
+ }
17
+ //#endregion
18
+ export { server as default };
19
+
20
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * No-op server stub for the opencode plugin loader.\n *\n * The opencode plugin host requires every plugin (TUI included) to expose a\n * `server()` entry via `exports[\"./server\"]` (or package.json `main`). When\n * that entry is missing the host fails to attach the plugin's server half and\n * the TUI runtime never wires the reactive channel — the initial JSX mounts\n * but effects/refresh/events stay dead (the \"mudo panel\" symptom).\n *\n * This stub satisfies the loader contract; pantheon-tui is TUI-only and has\n * no background server work of its own.\n */\nexport default function server() {\n return {}\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAwB,SAAS;CAC/B,OAAO,CAAC;AACV"}
@@ -1,8 +1,310 @@
1
1
  import { TuiPluginModule } from "@opencode-ai/plugin/tui";
2
2
  //#region src/index.d.ts
3
+ type DelegationEntry = {
4
+ /** Job alias, e.g. "apo-1" (from the H1 title, falling back to filename). */
5
+ alias: string;
6
+ /** Parent session the job was launched from (dir name under .pantheon/delegations). */
7
+ sessionID: string;
8
+ /** Child session id (= board task id, from the `Task ID` header). The
9
+ * children channel always sets it from the child session itself; the md
10
+ * channel parses it so child↔md matching works by taskID. */
11
+ taskID?: string;
12
+ /** Agent name, e.g. "apollo". */
13
+ agent: string;
14
+ state: 'running' | 'completed' | 'error' | 'cancelled';
15
+ /** Epoch ms of the `Started` header. */
16
+ startedAt: number;
17
+ /** Epoch ms of the `Finalized` header — null while still running. */
18
+ updatedAt: number | null;
19
+ timedOut: boolean;
20
+ description: string;
21
+ /** True while the panel is waiting for pantheon_delegation_read. */
22
+ read?: boolean;
23
+ /** Internal provenance used to keep a finalized md report authoritative.
24
+ * 'children-only' = a native task() child session with NO board report
25
+ * (rendered with the distinct `[task]` tag); 'md' = board report wins. */
26
+ source?: 'child' | 'live' | 'md' | 'children-only';
27
+ };
28
+ /** Parse one delegation report md header into a structured entry.
29
+ * Returns null (skip) when the file is not a recognizable report:
30
+ * missing agent/state/startedAt, an unknown state, or an unparsable
31
+ * Started timestamp. The alias falls back to the file name when the H1
32
+ * title is missing. Pure — no I/O.
33
+ *
34
+ * Linear, single-pass over `raw.split('\n')` with plain string operations
35
+ * (startsWith/indexOf/slice) — zero regex, so worst case is O(bytes) even
36
+ * on adversarial whitespace-heavy input (ReDoS regression, CodeQL 12x HIGH). */
37
+ declare function parseDelegationMarkdown(raw: string, fileAlias?: string, sessionID?: string): DelegationEntry | null;
38
+ /** Read every delegation report under `<dir>/<sessionID>/<alias>.md`.
39
+ * Fail-open: a missing/unreadable directory yields [], and each unreadable
40
+ * or malformed file is skipped individually. Entries are sorted running
41
+ * first, then terminal by `updatedAt` (most recent first) so the panel can
42
+ * render them in order directly. */
43
+ declare function readDelegationEntries(dir: string): Promise<DelegationEntry[]>;
44
+ /** Read delegation reports from EVERY session under
45
+ * `<root>/.pantheon/delegations/<sessionID>/<alias>.md` — the panel's
46
+ * HISTORY channel. Unlike the children/live channels it does NOT depend on a
47
+ * resolved sessionID: with no focused session (null/placeholder), the panel
48
+ * still shows the reports from all past sessions (running first, Finalized
49
+ * desc — the sort applied by readDelegationEntries). Fail-open: a
50
+ * missing/unreadable directory yields []. */
51
+ declare function readAllDelegationEntries(root: string): Promise<DelegationEntry[]>;
52
+ /** Resolve the directory where the job board writes delegation md reports.
53
+ * The board writes `.pantheon/delegations` RELATIVE to the server cwd,
54
+ * which the TUI exposes as `TuiState.path.directory`. `project` does NOT
55
+ * exist on `TuiState.path` (the old `state?.project ?? state?.worktree`
56
+ * resolution was always undefined for the first term) and `worktree` is
57
+ * `/` when there is no git (e.g. the sandbox test project) — a root of
58
+ * `''` or `'/'` must fall back to `process.cwd()`. */
59
+ declare function resolveDelegationsDir(state: {
60
+ directory?: string;
61
+ worktree?: string;
62
+ } | undefined, cwd?: string): string;
63
+ /** Project root derived from the delegations dir: `<root>/.pantheon/delegations`
64
+ * → `<root>`. Used to point the panel logger at the REAL hooks.log — passing
65
+ * the delegations dir (or its dirname) directly made createTuiLogger append
66
+ * to `<root>/.pantheon/.pantheon/logs/hooks.log`, a nested empty dir the real
67
+ * log never saw. Pure — no I/O. */
68
+ declare function panelLogDir(delegationsDir: string): string;
69
+ /** Where the panel logger appends lines: `<projectRoot>/.pantheon/logs/hooks.log`.
70
+ * Pure — testable without the runtime. */
71
+ declare function tuiLogPath(projectRoot: string): string;
72
+ /** Sort delegations: running first, then terminal by recency (updatedAt,
73
+ * falling back to startedAt, descending). Shared by the md reader and
74
+ * mergeDelegationSources. */
75
+ declare function compareDelegationEntries(a: DelegationEntry, b: DelegationEntry): number;
76
+ /** The list the panel actually renders: running jobs first, then the most
77
+ * recent terminal reports (capped). Pure — so the history-only panel (no
78
+ * sessionID) is testable without the TUI runtime. The header count uses the
79
+ * same "running + recentes" list. */
80
+ declare function visibleDelegationList(all: readonly DelegationEntry[], maxTerminal?: number): DelegationEntry[];
81
+ /** Compact elapsed-time label: "5m 12s", "1h 30m", "2d 4h" — ticks every
82
+ * second for running jobs. */
83
+ declare function fmtElapsed(ms: number): string;
84
+ /** Elapsed label for one entry: running → ticks `now - startedAt`, terminal
85
+ * → fixed `updatedAt - startedAt` (em dash when no finalized timestamp). */
86
+ declare function delegationElapsed(entry: DelegationEntry, now: number): string;
87
+ /** The activity labels shown by the animated row. Keeping this pure makes the
88
+ * state machine testable without booting OpenCode's renderer. */
89
+ type DelegationActivity = 'delegating' | 'working' | 'reading' | 'completed' | 'error' | 'cancelled';
90
+ declare function delegationActivity(entry: DelegationEntry): DelegationActivity;
91
+ declare function delegationActivityLabel(entry: DelegationEntry): string;
92
+ /** Return a deterministic spinner frame. The View ticks this every 140ms. */
93
+ declare function delegationSpinnerFrame(now: number): string;
94
+ /** Merge the immediate tool-event channel into the child-session channel.
95
+ *
96
+ * Children remain the durable source, while live entries make a delegation
97
+ * visible before the child API/report catches up. A finalized md entry wins
98
+ * over a stale live entry; a child-only row is upgraded with live agent,
99
+ * alias, phase and timestamps. */
100
+ declare function mergeChildDelegationSources(children: readonly DelegationEntry[], live: readonly LiveDelegationEntry[]): DelegationEntry[];
101
+ /** Duck-typed subset of a tool part (SDK v2 `ToolPart` / `ToolState`). */
102
+ type DelegationToolPart = {
103
+ id?: string;
104
+ callID?: string;
105
+ sessionID?: string;
106
+ type?: string;
107
+ tool?: string;
108
+ state?: {
109
+ status?: string;
110
+ input?: Record<string, unknown>;
111
+ output?: string;
112
+ error?: string;
113
+ time?: {
114
+ start?: number;
115
+ end?: number;
116
+ };
117
+ };
118
+ };
119
+ /** One live delegation tracked in-memory, keyed by the delegate callID. */
120
+ type LiveDelegationEntry = {
121
+ /** Tool call id of the pantheon_delegate part (stable across events). */
122
+ callID: string;
123
+ /** Part id (for message.part.removed cleanup). */
124
+ partID: string;
125
+ /** Parent session the delegation was launched from. */
126
+ sessionID: string;
127
+ tool: 'pantheon_delegate' | 'pantheon_delegation_read';
128
+ /** Agent name (from the delegate input args). */
129
+ agent: string;
130
+ description: string;
131
+ /** Known after the delegate tool completes (parsed from its output). */
132
+ alias: string | null;
133
+ /** Child session id (parsed from the delegate output). */
134
+ taskID: string | null;
135
+ state: 'running' | 'completed' | 'error' | 'cancelled';
136
+ startedAt: number;
137
+ updatedAt: number | null;
138
+ /** True once a pantheon_delegation_read for this job has been observed. */
139
+ read: boolean;
140
+ };
141
+ /** Result of parsing one tool part into lifecycle-relevant fields. */
142
+ type ParsedDelegationToolPart = {
143
+ callID: string;
144
+ partID: string;
145
+ sessionID: string;
146
+ tool: 'pantheon_delegate' | 'pantheon_delegation_read';
147
+ /** null for read parts (no agent arg — the id targets an existing job). */
148
+ agent: string | null;
149
+ description: string;
150
+ status: 'pending' | 'running' | 'completed' | 'error';
151
+ alias: string | null;
152
+ taskID: string | null;
153
+ startedAt: number;
154
+ endAt: number | null;
155
+ };
156
+ /** Extract the tool name + args from a `message.part.updated` part and
157
+ * reduce it to what the panel needs. Returns null for anything that is
158
+ * not a pantheon delegation tool part (or is missing its callID). */
159
+ declare function parseDelegationToolPart(part: DelegationToolPart, now?: number): ParsedDelegationToolPart | null;
160
+ /** Apply one tool part to the live map. Returns true when the map changed.
161
+ * Pure w.r.t. I/O — only mutates `map`. */
162
+ declare function reduceDelegationToolPart(map: Map<string, LiveDelegationEntry>, part: DelegationToolPart, now?: number): boolean;
163
+ /** Remove a live entry by part id (message.part.removed) or call id.
164
+ * Returns true when something was removed. */
165
+ declare function removeDelegationEntry(map: Map<string, LiveDelegationEntry>, partIDOrCallID: string): boolean;
166
+ /** Collect pantheon delegation tool parts from a session's messages.
167
+ * Messages may carry their parts inline (duck-typed `msg.parts`); when
168
+ * they don't, the optional `getParts(messageID)` callback is used (the TUI
169
+ * SDK exposes `api.state.part(messageID)`). Pure w.r.t. I/O — used by the
170
+ * mount re-scan to re-seed the live map after compaction/attach. */
171
+ declare function collectDelegationToolParts(messages: readonly {
172
+ id?: string;
173
+ parts?: unknown[];
174
+ }[] | undefined, getParts?: (messageID: string) => readonly unknown[] | undefined): DelegationToolPart[];
175
+ /** Apply a batch of tool parts (in message order) to the live map. Used on
176
+ * mount to re-seed entries that `message.part.removed` (compaction) wiped,
177
+ * from the session's existing tool parts. Returns how many parts changed
178
+ * the map (0 on the second identical seed — idempotent, no extra bumps). */
179
+ declare function seedLiveDelegationMap(map: Map<string, LiveDelegationEntry>, parts: readonly DelegationToolPart[], now?: number): number;
180
+ /** Convert a live entry into the shared display shape. Alias falls back to
181
+ * a `live-<callID>` prefix while the delegate tool has not completed yet. */
182
+ declare function toDelegationEntry(live: LiveDelegationEntry): DelegationEntry;
183
+ /** Combine the live channel with the md (historical) channel into one
184
+ * display list. Dedupes by (sessionID, alias) — aliases are per-parent-
185
+ * session, so the same alias in different sessions stays separate. A
186
+ * terminal md entry is authoritative over a live running entry for the
187
+ * same job (it carries Finalized/timedOut/cancelled from finalize). */
188
+ declare function mergeDelegationSources(live: readonly LiveDelegationEntry[], md: readonly DelegationEntry[]): DelegationEntry[];
189
+ /** Server-aligned session id validity: opencode rejects anything not starting
190
+ * with "ses" (SchemaError). This deliberately mirrors that exact contract —
191
+ * nothing stricter, nothing looser — so a template placeholder ("{sessionID}"),
192
+ * its URL-encoded form ("%7BsessionID%7D", what the server reported in the
193
+ * schema error), an empty/undefined value, or a foreign id (e.g. "wrk_") can
194
+ * never reach a path and error-spam the log. Confirmed: "{sessionID}" starts
195
+ * with "{" and "%7BsessionID%7D" with "%" — both fail startsWith("ses"), so
196
+ * the placeholder is rejected WITHOUT an explicit denylist (covered by tests). */
197
+ declare function isValidSessionId(id: unknown): id is string;
198
+ /** Sources the sidebar can resolve the CURRENT session id from. Duck-typed
199
+ * subsets of TuiPluginApi / TuiState / TuiRouteCurrent so the helper stays
200
+ * pure and testable without the TUI runtime. */
201
+ type TuiSessionSources = {
202
+ /** sidebar_content slot prop (`session_id`). */
203
+ sessionID?: string | null;
204
+ api?: {
205
+ /** Runtime state superset — may expose the current session id. */
206
+ state?: {
207
+ sessionID?: unknown;
208
+ };
209
+ /** Typed route: { name: 'session', params: { sessionID } } when in one. */
210
+ route?: {
211
+ current?: {
212
+ name?: string;
213
+ params?: Record<string, unknown>;
214
+ };
215
+ };
216
+ } | null;
217
+ };
218
+ /** Resolve the current session id for the sidebar. Order: slot prop →
219
+ * api.state.sessionID (runtime superset) → api.route.current.params.sessionID
220
+ * (typed route). Every source is validated; invalid/absent → next source.
221
+ * NEVER returns a placeholder or non-ses id. Null → callers MUST skip the
222
+ * fetch (empty panel, zero errors). Pure — no I/O, no runtime required. */
223
+ declare function resolveCurrentSessionID(sources: TuiSessionSources): string | null;
224
+ /** THE single choke point for every `session.children` / session-API path.
225
+ * Returns `{ path: { id } }` ONLY for a server-valid session id; returns
226
+ * null for anything else (placeholder, empty, foreign id) so the caller
227
+ * skips the call entirely instead of sending an unsubstituted placeholder
228
+ * (the "%7BsessionID%7D" regression). Every session-API call site MUST go
229
+ * through this function (enforced by the source-scan test in
230
+ * tests/pantheon/tui-delegations.test.ts). */
231
+ declare function safeSessionPath(id: unknown): {
232
+ path: {
233
+ id: string;
234
+ };
235
+ } | null;
236
+ /** Build the `session.children` path ONLY from a validated session id.
237
+ * Delegates to {@link safeSessionPath} — the single choke point. Returns
238
+ * null for null/invalid ids so the caller skips the fetch instead of
239
+ * sending an unsubstituted placeholder (the "%7BsessionID%7D" regression). */
240
+ declare function buildChildrenPath(id: string | null | undefined): {
241
+ path: {
242
+ id: string;
243
+ };
244
+ } | null;
245
+ /** Duck-typed subset of a child Session (+ its live status type). */
246
+ type ChildDelegationLike = {
247
+ /** Child session id (= board task id). */
248
+ id: string;
249
+ /** Session title — the delegate's description or prompt prefix. */
250
+ title?: string;
251
+ /** Agent name when the child session carries one (duck-typed; native
252
+ * task() children may expose it, board reports always do). */
253
+ agent?: string;
254
+ /** Status type from api.state.session.status: 'busy' | 'retry' | 'idle',
255
+ * or undefined when the status API is unavailable. */
256
+ status?: string;
257
+ time?: {
258
+ created?: number;
259
+ updated?: number;
260
+ };
261
+ };
262
+ /** Map a child status type to a display state. busy/retry → running
263
+ * (the child is actively working), idle → completed, unknown → running
264
+ * (fail-open: a freshly-seen child is assumed active; the 1s poll + md
265
+ * correct it as soon as terminal data exists). */
266
+ declare function childStatusToState(status: string | undefined): 'running' | 'completed';
267
+ /** Row tag for a delegation entry: `[task]` for native task() children
268
+ * (source 'children-only' — no board report), `[<alias>]` for board rows
269
+ * ([apo-1]). The panel renders the tag with a distinct style so native
270
+ * task() children are visually separable from pantheon_delegate jobs. */
271
+ declare function delegationTag(entry: DelegationEntry): string;
272
+ /** Turn child sessions (PRIMARY) enriched with md reports into the display
273
+ * list. One entry per child id (duplicates across re-fetches collapse).
274
+ * The md report is matched by `Task ID` (== child.id) and supplies alias,
275
+ * agent, description, terminal state and duration. A child without a
276
+ * report still renders: description from its title, agent from the child
277
+ * itself (fallback 'agent'), state derived from its status, startedAt from
278
+ * time.created. A report-less child is a NATIVE task() child (every
279
+ * child of the current session — pantheon_delegate OR the native `task()`
280
+ * tool — carries parentID = caller), so it gets source 'children-only'
281
+ * and the `[task]` tag instead of a board alias.
282
+ * Terminal md state wins over the derived state; a running md defers to
283
+ * the child's live status. Sorted running-first (compareDelegationEntries).
284
+ * Pure — no I/O. */
285
+ declare function childrenToDelegationEntries(children: readonly ChildDelegationLike[] | undefined, md: readonly DelegationEntry[], now?: number): DelegationEntry[];
286
+ /** Navigate the TUI to a child session (click/Enter on a delegation row).
287
+ * Returns false when the route API is unavailable or the target id is
288
+ * missing/placeholder — the row stays inert instead of crashing. Only a
289
+ * server-valid session id ("ses...") ever reaches the router, so an
290
+ * unsubstituted "{sessionID}" placeholder can never be routed. */
291
+ declare function navigateToDelegationSession(route: {
292
+ navigate?: (name: string, params?: Record<string, unknown>) => void;
293
+ } | undefined, taskID: string | undefined): boolean;
294
+ /** Plugin-level live delegation store shared with the event subscriptions
295
+ * in `tui()`: the map of live entries + a version signal bumped on every
296
+ * mutation. The View subscribes to the version (in an effect) to refresh the
297
+ * durable child list and also reads the map as an optimistic live source. */
298
+ type LiveDelegationStore = {
299
+ map: Map<string, LiveDelegationEntry>;
300
+ /** Reactive version getter — View reads it inside an effect to re-fetch. */
301
+ version: () => number;
302
+ /** Bump the version after a live mutation. */
303
+ bump: () => void;
304
+ };
3
305
  declare const plugin: TuiPluginModule & {
4
306
  id: string;
5
307
  };
6
308
  //#endregion
7
- export { plugin as default };
309
+ export { ChildDelegationLike, DelegationActivity, DelegationEntry, DelegationToolPart, LiveDelegationEntry, LiveDelegationStore, ParsedDelegationToolPart, TuiSessionSources, buildChildrenPath, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationSpinnerFrame, delegationTag, fmtElapsed, isValidSessionId, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, safeSessionPath, seedLiveDelegationMap, toDelegationEntry, tuiLogPath, visibleDelegationList };
8
310
  //# sourceMappingURL=tui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;cA8qCM,QAAQ;EAAoB"}
1
+ {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KAm5BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EAEA;;EAEA;EACA;EACA;;EAEA;;;;EAIA;;;;;;;;;;;iBAsBc,wBACd,aACA,oBACA,qBACC;;;;;;iBA8GmB,sBAAsB,cAAc,QAAQ;;;;;;;;iBAwC5C,yBAAyB,eAAe,QAAQ;;;;;;;;iBAWtD,sBACd;EAAS;EAAoB;eAC7B;;;;;;iBAYc,YAAY;;;iBAMZ,WAAW;;;;iBAOX,yBAAyB,GAAG,iBAAiB,GAAG;;;;;iBAWhD,sBACd,cAAc,mBACd,uBACC;;;iBAQa,WAAW;;;iBAcX,kBAAkB,OAAO,iBAAiB;;;KAO9C;iBAQI,mBAAmB,OAAO,kBAAkB;iBAS5C,wBAAwB,OAAO;;iBAoB/B,uBAAuB;;;;;;;iBAWvB,4BACd,mBAAmB,mBACnB,eAAe,wBACd;;KA2DS;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;;;;;iBASnB,cAAc,OAAO;;;;;;;;;;;;;;iBAiBrB,4BACd,mBAAmB,mCACnB,aAAa,mBACb,eACC;;;;;;iBAwCa,4BACd;EAAS,YAAY,cAAc,SAAS;eAC5C;;;;;KA8KU;EACV,KAAK,YAAY;;EAEjB;;EAEA;;cAicI,QAAQ;EAAoB"}