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.
- package/AGENTS.md +919 -0
- package/README.md +157 -15
- package/bin/pantheon-init.mjs +177 -120
- package/opencode.json +3 -6
- package/package.json +8 -4
- package/scripts/build-agents-md.mjs +195 -0
- package/scripts/cost.mjs +17 -0
- package/scripts/doctor.mjs +56 -22
- package/scripts/hooks/on-subagent-delegation-stop.sh +22 -5
- package/scripts/install/opencode.mjs +189 -38
- package/scripts/install/plugin.mjs +112 -28
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/redaction-gate.mjs +69 -0
- package/scripts/release-beta-version.mjs +80 -0
- package/scripts/release-notes.mjs +317 -0
- package/scripts/test-context-compression.sh +5 -2
- package/scripts/validate-package.mjs +56 -0
- package/scripts/versioning.mjs +117 -18
- package/src/agents/zeus.md +6 -2
- package/src/pantheon/background-job-board.ts +24 -38
- package/src/pantheon/chat-reminders.ts +79 -0
- package/src/pantheon/compaction-assert.ts +160 -0
- package/src/pantheon/cost-command.ts +57 -18
- package/src/pantheon/delegation-compaction.ts +143 -16
- package/src/pantheon/delegation-finalize.ts +8 -1
- package/src/pantheon/delegation-notify.ts +19 -157
- package/src/pantheon/delegation.ts +330 -28
- package/src/pantheon/dispatch-guard.ts +9 -4
- package/src/pantheon/goal-loop.ts +7 -4
- package/src/pantheon/hashline/tool.ts +2 -1
- package/src/pantheon/logger.ts +117 -0
- package/src/pantheon/plugin-once.ts +76 -0
- package/src/pantheon/preemptive-compact.ts +76 -0
- package/src/pantheon/presets.d.mts +31 -0
- package/src/pantheon/presets.mjs +105 -3
- package/src/pantheon/todo-enforcer.ts +79 -27
- package/src/pantheon/todo-preserve.ts +295 -0
- package/src/plugin.ts +177 -50
- package/src/plugins/pantheon-hooks.ts +35 -22
- package/src/plugins/tui/dist/server.d.ts +17 -0
- package/src/plugins/tui/dist/server.d.ts.map +1 -0
- package/src/plugins/tui/dist/server.js +20 -0
- package/src/plugins/tui/dist/server.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +303 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +977 -350
- package/src/plugins/tui/dist/tui.tsx +1427 -150
- package/src/plugins/tui/package.json +4 -3
- package/src/plugins/tui/src/index.tsx +1427 -150
- package/src/plugins/tui/src/server.ts +15 -0
- package/src/plugins/tui/tsdown.config.ts +1 -1
- 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.
|
|
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
|
|
1449
|
-
|
|
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'
|
|
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":";;
|
|
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"}
|