pantheon-opencode 1.5.0 → 1.5.2
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 +27 -0
- package/README.md +63 -2
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/pyproject.toml +2 -1
- package/scripts/doctor.mjs +228 -2
- package/scripts/install/state.mjs +27 -2
- package/scripts/sync-tui.mjs +11 -5
- package/scripts/zenodo-artifact.mjs +77 -4
- package/src/agents/mnemosyne.md +11 -1
- package/src/agents/zeus.md +1 -0
- package/src/mcp/memory_mcp_server.py +81 -27
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +19 -24
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +22 -81
- package/src/plugins/tui/package-lock.json +5 -2
- package/src/plugins/tui/package.json +4 -1
- package/src/plugins/tui/src/index.tsx +54 -151
- package/src/mcp/toon_codec.py +0 -489
|
@@ -6,6 +6,10 @@ for semantic memory with hybrid search (vector cosine + FTS5 BM25).
|
|
|
6
6
|
|
|
7
7
|
Total footprint: ~50MB (sqlite-vec + fastembed) vs ~1.4GB (old chromadb).
|
|
8
8
|
|
|
9
|
+
Both embedding backends are optional: if either fails to import the server
|
|
10
|
+
still starts and serves FTS5-only search (``PANTHEON_MEMORY_EMBED=off``
|
|
11
|
+
disables the vector pipeline explicitly).
|
|
12
|
+
|
|
9
13
|
Tools:
|
|
10
14
|
memory_store — Store with automatic embedding
|
|
11
15
|
memory_search — Hybrid vector + FTS5 keyword search
|
|
@@ -30,9 +34,21 @@ from datetime import UTC, datetime
|
|
|
30
34
|
from pathlib import Path
|
|
31
35
|
from typing import Any
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
# Embedding backends are optional (issue #159): a failed fastembed/sqlite_vec
|
|
38
|
+
# install (network error, incompatible wheel) must not kill the server — it
|
|
39
|
+
# degrades to FTS-only search instead of crashing before the MCP handshake.
|
|
40
|
+
try:
|
|
41
|
+
import sqlite_vec
|
|
42
|
+
except ImportError: # pragma: no cover - degraded mode
|
|
43
|
+
sqlite_vec = None # type: ignore[assignment]
|
|
44
|
+
|
|
34
45
|
from _pantheon_paths import pantheon_home
|
|
35
|
-
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from fastembed import TextEmbedding
|
|
49
|
+
except ImportError: # pragma: no cover - degraded mode
|
|
50
|
+
TextEmbedding = None # type: ignore[assignment]
|
|
51
|
+
|
|
36
52
|
from mcp.server.fastmcp import FastMCP
|
|
37
53
|
|
|
38
54
|
try:
|
|
@@ -52,6 +68,12 @@ except ImportError: # pragma: no cover - deployed runtime may need path fix
|
|
|
52
68
|
DB_PATH = pantheon_home() / "memory" / "memory.db"
|
|
53
69
|
EMBED_CACHE = Path.home() / ".cache" / "fastembed"
|
|
54
70
|
|
|
71
|
+
# PANTHEON_MEMORY_EMBED=off disables the embedding/vector pipeline so the
|
|
72
|
+
# server serves FTS-only search (issue #159). Default "on" keeps the current
|
|
73
|
+
# hybrid vector + FTS behavior. The flag is also forced off automatically
|
|
74
|
+
# when fastembed failed to import.
|
|
75
|
+
_EMBED_DISABLED = frozenset({"off", "0", "false", "no"})
|
|
76
|
+
|
|
55
77
|
|
|
56
78
|
_BYTE_UNIT = 1024
|
|
57
79
|
|
|
@@ -81,11 +103,6 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
81
103
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_ns_key
|
|
82
104
|
ON memories(namespace, key);
|
|
83
105
|
|
|
84
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
85
|
-
id INTEGER PRIMARY KEY,
|
|
86
|
-
embedding float[384]
|
|
87
|
-
);
|
|
88
|
-
|
|
89
106
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
90
107
|
namespace, key, value, metadata,
|
|
91
108
|
content='memories', content_rowid='id',
|
|
@@ -113,6 +130,14 @@ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
|
113
130
|
END;
|
|
114
131
|
"""
|
|
115
132
|
|
|
133
|
+
# Vector store requires the sqlite-vec extension; created only when available.
|
|
134
|
+
VEC_SCHEMA_SQL = """
|
|
135
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
136
|
+
id INTEGER PRIMARY KEY,
|
|
137
|
+
embedding float[384]
|
|
138
|
+
);
|
|
139
|
+
"""
|
|
140
|
+
|
|
116
141
|
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
117
142
|
|
|
118
143
|
mcp = FastMCP(
|
|
@@ -129,7 +154,8 @@ def _get_db() -> sqlite3.Connection:
|
|
|
129
154
|
"""Get or create the SQLite connection singleton.
|
|
130
155
|
|
|
131
156
|
Creates DB directory, applies WAL/performance pragmas, registers
|
|
132
|
-
the sqlite-vec extension, and runs schema init.
|
|
157
|
+
the sqlite-vec extension, and runs schema init. When sqlite-vec is
|
|
158
|
+
unavailable the vector table is skipped — search degrades to FTS-only.
|
|
133
159
|
"""
|
|
134
160
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
135
161
|
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
|
|
@@ -137,9 +163,10 @@ def _get_db() -> sqlite3.Connection:
|
|
|
137
163
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
138
164
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
139
165
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
140
|
-
# Register sqlite-vec extension
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
# Register sqlite-vec extension (optional; enables vector search)
|
|
167
|
+
if sqlite_vec is not None:
|
|
168
|
+
sqlite_vec.load(conn)
|
|
169
|
+
conn.executescript(VEC_SCHEMA_SQL)
|
|
143
170
|
conn.executescript(SCHEMA_SQL)
|
|
144
171
|
if _codemap is not None:
|
|
145
172
|
with contextlib.suppress(Exception):
|
|
@@ -155,13 +182,30 @@ def _get_conn() -> sqlite3.Connection:
|
|
|
155
182
|
# ── Embedding ─────────────────────────────────────────────────────────────────
|
|
156
183
|
|
|
157
184
|
|
|
185
|
+
def _embeddings_enabled() -> bool:
|
|
186
|
+
"""Whether the embedding/vector pipeline is active.
|
|
187
|
+
|
|
188
|
+
Controlled by ``PANTHEON_MEMORY_EMBED`` (``on`` by default; ``off``,
|
|
189
|
+
``0``, ``false`` or ``no`` disable it). Automatically disabled when
|
|
190
|
+
``fastembed`` failed to import, so the server degrades to FTS-only
|
|
191
|
+
search instead of crashing at startup (issue #159).
|
|
192
|
+
"""
|
|
193
|
+
if TextEmbedding is None:
|
|
194
|
+
return False
|
|
195
|
+
flag = os.environ.get("PANTHEON_MEMORY_EMBED", "on").strip().lower()
|
|
196
|
+
return flag not in _EMBED_DISABLED
|
|
197
|
+
|
|
198
|
+
|
|
158
199
|
@functools.cache
|
|
159
|
-
def _get_embedder() -> TextEmbedding:
|
|
200
|
+
def _get_embedder() -> TextEmbedding | None:
|
|
160
201
|
"""Lazy-load fastembed model (auto-downloads on first call, ~30MB).
|
|
161
202
|
|
|
162
203
|
Uses BAAI/bge-small-en-v1.5 (384-dim, CPU, ONNX) — no PyTorch needed.
|
|
163
|
-
Model is cached in ~/.cache/fastembed/.
|
|
204
|
+
Model is cached in ~/.cache/fastembed/. Returns None when fastembed is
|
|
205
|
+
unavailable.
|
|
164
206
|
"""
|
|
207
|
+
if TextEmbedding is None:
|
|
208
|
+
return None
|
|
165
209
|
os.environ.setdefault("TQDM_DISABLE", "1")
|
|
166
210
|
return TextEmbedding(
|
|
167
211
|
model_name="BAAI/bge-small-en-v1.5",
|
|
@@ -169,9 +213,18 @@ def _get_embedder() -> TextEmbedding:
|
|
|
169
213
|
)
|
|
170
214
|
|
|
171
215
|
|
|
172
|
-
def _embed(text: str) -> list[float]:
|
|
173
|
-
"""Generate a 384-dim embedding vector for the given text.
|
|
174
|
-
|
|
216
|
+
def _embed(text: str) -> list[float] | None:
|
|
217
|
+
"""Generate a 384-dim embedding vector for the given text.
|
|
218
|
+
|
|
219
|
+
Returns None when embeddings are disabled (``PANTHEON_MEMORY_EMBED=off``
|
|
220
|
+
or fastembed missing) — callers fall back to FTS-only search.
|
|
221
|
+
"""
|
|
222
|
+
if not _embeddings_enabled():
|
|
223
|
+
return None
|
|
224
|
+
embedder = _get_embedder()
|
|
225
|
+
if embedder is None:
|
|
226
|
+
return None
|
|
227
|
+
return next(iter(embedder.embed(text))).tolist()
|
|
175
228
|
|
|
176
229
|
|
|
177
230
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -341,14 +394,14 @@ def memory_store(
|
|
|
341
394
|
)
|
|
342
395
|
entry_id = cur.lastrowid
|
|
343
396
|
|
|
344
|
-
# Generate embedding
|
|
397
|
+
# Generate embedding (None when disabled → FTS-only mode)
|
|
345
398
|
embedding = _embed(value)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
399
|
+
if embedding is not None and sqlite_vec is not None:
|
|
400
|
+
embedding_json = json.dumps(embedding)
|
|
401
|
+
db.execute(
|
|
402
|
+
"INSERT INTO vec_memories(id, embedding) VALUES (?, ?)",
|
|
403
|
+
[entry_id, embedding_json],
|
|
404
|
+
)
|
|
352
405
|
db.commit()
|
|
353
406
|
except sqlite3.IntegrityError as e:
|
|
354
407
|
db.rollback()
|
|
@@ -406,8 +459,8 @@ def memory_search( # noqa: C901, PLR0912
|
|
|
406
459
|
vec_ids: list[int] = []
|
|
407
460
|
fts_ids: list[int] = []
|
|
408
461
|
|
|
409
|
-
# 2. Vector search
|
|
410
|
-
if query_vec is not None:
|
|
462
|
+
# 2. Vector search (skipped when embeddings or sqlite-vec are unavailable)
|
|
463
|
+
if query_vec is not None and sqlite_vec is not None:
|
|
411
464
|
nvec = top_k * 2
|
|
412
465
|
try:
|
|
413
466
|
if namespace:
|
|
@@ -564,8 +617,9 @@ def memory_forget(
|
|
|
564
617
|
db = _get_conn()
|
|
565
618
|
try:
|
|
566
619
|
if col == "id":
|
|
567
|
-
# Vec table cascade
|
|
568
|
-
|
|
620
|
+
# Vec table cascade (absent when sqlite-vec is unavailable)
|
|
621
|
+
if sqlite_vec is not None:
|
|
622
|
+
db.execute("DELETE FROM vec_memories WHERE id = ?", [id])
|
|
569
623
|
cur = db.execute("DELETE FROM memories WHERE id = ?", [id])
|
|
570
624
|
else:
|
|
571
625
|
cur = db.execute(sql, params)
|
|
@@ -181,7 +181,7 @@ import { getSharedBoard } from '../pantheon/shared-board.ts'
|
|
|
181
181
|
import { type HookPayload, type HookResult, runHook } from './hook-runner.ts'
|
|
182
182
|
|
|
183
183
|
/** Tools that represent a subagent delegation (opencode `task` tool etc.). */
|
|
184
|
-
const DELEGATION_TOOL_RE = /^(task|.*
|
|
184
|
+
const DELEGATION_TOOL_RE = /^(task|.*subagent.*)$/i
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
187
|
* When set to a truthy value ("1" or "debug"), zero-exit audit-hook output is
|
|
@@ -18,8 +18,6 @@ type DelegationEntry = {
|
|
|
18
18
|
updatedAt: number | null;
|
|
19
19
|
timedOut: boolean;
|
|
20
20
|
description: string;
|
|
21
|
-
/** True while the panel is waiting for pantheon_delegation_read. */
|
|
22
|
-
read?: boolean;
|
|
23
21
|
/** Internal provenance used to keep a finalized md report authoritative.
|
|
24
22
|
* 'children-only' = a native task() child session with NO delegate report
|
|
25
23
|
* (a native child row); 'md' = the child has a matching report;
|
|
@@ -145,7 +143,7 @@ declare function fmtElapsed(ms: number): string;
|
|
|
145
143
|
declare function delegationElapsed(entry: DelegationEntry, now: number): string;
|
|
146
144
|
/** The activity labels shown by the animated row. Keeping this pure makes the
|
|
147
145
|
* state machine testable without booting OpenCode's renderer. */
|
|
148
|
-
type DelegationActivity = 'delegating' | 'working' | '
|
|
146
|
+
type DelegationActivity = 'delegating' | 'working' | 'completed' | 'error' | 'cancelled';
|
|
149
147
|
declare function delegationActivity(entry: DelegationEntry): DelegationActivity;
|
|
150
148
|
declare function delegationActivityLabel(entry: DelegationEntry): string;
|
|
151
149
|
/** Return the spinner glyph for the given frame counter. The frame counter
|
|
@@ -217,36 +215,34 @@ type DelegationToolPart = {
|
|
|
217
215
|
};
|
|
218
216
|
};
|
|
219
217
|
};
|
|
220
|
-
/** One live delegation tracked in-memory, keyed by the
|
|
218
|
+
/** One live delegation tracked in-memory, keyed by the task callID. */
|
|
221
219
|
type LiveDelegationEntry = {
|
|
222
|
-
/** Tool call id of the
|
|
220
|
+
/** Tool call id of the task part (stable across events). */
|
|
223
221
|
callID: string;
|
|
224
222
|
/** Part id (for message.part.removed cleanup). */
|
|
225
223
|
partID: string;
|
|
226
224
|
/** Parent session the delegation was launched from. */
|
|
227
225
|
sessionID: string;
|
|
228
|
-
tool: '
|
|
229
|
-
/** Agent name (from the
|
|
226
|
+
tool: 'task';
|
|
227
|
+
/** Agent name (from the task input args). */
|
|
230
228
|
agent: string;
|
|
231
229
|
description: string;
|
|
232
|
-
/** Known
|
|
230
|
+
/** Known once the task completes and its output carries a marker. */
|
|
233
231
|
alias: string | null;
|
|
234
|
-
/** Child session id (parsed from the
|
|
232
|
+
/** Child session id (parsed from the task output, when present). */
|
|
235
233
|
taskID: string | null;
|
|
236
234
|
state: 'running' | 'completed' | 'error' | 'cancelled';
|
|
237
235
|
startedAt: number;
|
|
238
236
|
updatedAt: number | null;
|
|
239
|
-
/** True once a pantheon_delegation_read for this job has been observed. */
|
|
240
|
-
read: boolean;
|
|
241
237
|
};
|
|
242
238
|
/** Result of parsing one tool part into lifecycle-relevant fields. */
|
|
243
239
|
type ParsedDelegationToolPart = {
|
|
244
240
|
callID: string;
|
|
245
241
|
partID: string;
|
|
246
242
|
sessionID: string;
|
|
247
|
-
tool: '
|
|
248
|
-
/**
|
|
249
|
-
agent: string
|
|
243
|
+
tool: 'task';
|
|
244
|
+
/** Agent name from the task input args ('agent' fallback when absent). */
|
|
245
|
+
agent: string;
|
|
250
246
|
description: string;
|
|
251
247
|
status: 'pending' | 'running' | 'completed' | 'error';
|
|
252
248
|
alias: string | null;
|
|
@@ -256,9 +252,8 @@ type ParsedDelegationToolPart = {
|
|
|
256
252
|
};
|
|
257
253
|
/** Extract the tool name + args from a `message.part.updated` part and
|
|
258
254
|
* reduce it to what the panel needs. Returns null for anything that is
|
|
259
|
-
* not
|
|
260
|
-
*
|
|
261
|
-
* or is missing its callID. */
|
|
255
|
+
* not the native `task` subagent tool (parentID === caller mechanism —
|
|
256
|
+
* its children render `nat:`), or is missing its callID. */
|
|
262
257
|
declare function parseDelegationToolPart(part: DelegationToolPart, now?: number): ParsedDelegationToolPart | null;
|
|
263
258
|
/** Apply one tool part to the live map. Returns true when the map changed.
|
|
264
259
|
* Pure w.r.t. I/O — only mutates `map`. */
|
|
@@ -266,14 +261,14 @@ declare function reduceDelegationToolPart(map: Map<string, LiveDelegationEntry>,
|
|
|
266
261
|
/** Remove a live entry by part id (message.part.removed) or call id.
|
|
267
262
|
* Returns true when something was removed. */
|
|
268
263
|
declare function removeDelegationEntry(map: Map<string, LiveDelegationEntry>, partIDOrCallID: string): boolean;
|
|
269
|
-
/** Collect
|
|
264
|
+
/** Collect native task tool parts from a session's messages.
|
|
270
265
|
* Messages may carry their parts inline (duck-typed `msg.parts`); when
|
|
271
266
|
* they don't, the optional `getParts(messageID)` callback is used (the TUI
|
|
272
267
|
* SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
|
|
273
|
-
* child session with parentID = caller
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
268
|
+
* child session with parentID = caller, so its parts feed the live-map as
|
|
269
|
+
* the refresh signal (rows come from the children channel). Pure w.r.t.
|
|
270
|
+
* I/O — used by the mount re-scan to re-seed the live map after
|
|
271
|
+
* compaction/attach. */
|
|
277
272
|
declare function collectDelegationToolParts(messages: readonly {
|
|
278
273
|
id?: string;
|
|
279
274
|
parts?: unknown[];
|
|
@@ -284,7 +279,7 @@ declare function collectDelegationToolParts(messages: readonly {
|
|
|
284
279
|
* the map (0 on the second identical seed — idempotent, no extra bumps). */
|
|
285
280
|
declare function seedLiveDelegationMap(map: Map<string, LiveDelegationEntry>, parts: readonly DelegationToolPart[], now?: number): number;
|
|
286
281
|
/** Convert a live entry into the shared display shape. Alias falls back to
|
|
287
|
-
* a `live-<callID>` prefix while the
|
|
282
|
+
* a `live-<callID>` prefix while the task call has not completed yet. */
|
|
288
283
|
declare function toDelegationEntry(live: LiveDelegationEntry): DelegationEntry;
|
|
289
284
|
/** Combine the live channel with the md (historical) channel into one
|
|
290
285
|
* display list. Dedupes by (sessionID, alias) — aliases are per-parent-
|
|
@@ -467,7 +462,7 @@ declare function ceilingDelegationList(all: readonly DelegationEntry[], maxVisib
|
|
|
467
462
|
hiddenActive: number;
|
|
468
463
|
hiddenTerminal: number;
|
|
469
464
|
};
|
|
470
|
-
/** Split a display list into native task() rows vs
|
|
465
|
+
/** Split a display list into native task() rows vs pantheon report rows.
|
|
471
466
|
* Native = source 'children-only' (no delegate report); everything else counts
|
|
472
467
|
* as pantheon. Pure — powers the hooks.log line. */
|
|
473
468
|
declare function countDelegationSources(entries: readonly DelegationEntry[]): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KA07BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EAUA;;EAEA;EACA;EACA
|
|
1
|
+
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}
|
|
@@ -165,7 +165,7 @@ async function detectVersion(api) {
|
|
|
165
165
|
if (ver) return ver;
|
|
166
166
|
}
|
|
167
167
|
} catch {}
|
|
168
|
-
return "1.5.
|
|
168
|
+
return "1.5.2";
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
171
|
* usage-bar — AI subscription usage gauge for the opencode TUI.
|
|
@@ -980,7 +980,6 @@ function delegationActivity(entry) {
|
|
|
980
980
|
if (entry.state === "completed") return "completed";
|
|
981
981
|
if (entry.state === "error" || entry.state === "startup_failed" || entry.state === "startup_unknown") return "error";
|
|
982
982
|
if (entry.state === "cancelled") return "cancelled";
|
|
983
|
-
if (entry.read) return "reading";
|
|
984
983
|
if (entry.alias.startsWith("live-") && entry.taskID === void 0) return "delegating";
|
|
985
984
|
return "working";
|
|
986
985
|
}
|
|
@@ -989,7 +988,6 @@ function delegationActivityLabel(entry) {
|
|
|
989
988
|
switch (delegationActivity(entry)) {
|
|
990
989
|
case "delegating": return "DELEGATING";
|
|
991
990
|
case "working": return "WORKING";
|
|
992
|
-
case "reading": return "READING RESULT";
|
|
993
991
|
case "completed": return entry.timedOut ? "DONE (TIMED OUT)" : "DONE";
|
|
994
992
|
case "error": return "ERROR";
|
|
995
993
|
default: return "CANCELLED";
|
|
@@ -1129,7 +1127,6 @@ function mergeChildDelegationSources(children, live) {
|
|
|
1129
1127
|
if (existing.state !== "running") {
|
|
1130
1128
|
if (liveEntry.alias !== null && existing.alias !== incoming.alias) existing.alias = incoming.alias;
|
|
1131
1129
|
if (incoming.agent !== "agent" && existing.agent !== incoming.agent) existing.agent = incoming.agent;
|
|
1132
|
-
if (incoming.read && !existing.read) existing.read = true;
|
|
1133
1130
|
continue;
|
|
1134
1131
|
}
|
|
1135
1132
|
result[index] = {
|
|
@@ -1142,7 +1139,6 @@ function mergeChildDelegationSources(children, live) {
|
|
|
1142
1139
|
state: incoming.state,
|
|
1143
1140
|
startedAt: Math.min(existing.startedAt, incoming.startedAt),
|
|
1144
1141
|
updatedAt: incoming.updatedAt,
|
|
1145
|
-
read: incoming.read,
|
|
1146
1142
|
source: isNativeLive ? existing.source : "live"
|
|
1147
1143
|
};
|
|
1148
1144
|
}
|
|
@@ -1150,22 +1146,19 @@ function mergeChildDelegationSources(children, live) {
|
|
|
1150
1146
|
return result;
|
|
1151
1147
|
}
|
|
1152
1148
|
/** Duck-typed subset of a tool part (SDK v2 `ToolPart` / `ToolState`). */
|
|
1153
|
-
/** One live delegation tracked in-memory, keyed by the
|
|
1149
|
+
/** One live delegation tracked in-memory, keyed by the task callID. */
|
|
1154
1150
|
/** Result of parsing one tool part into lifecycle-relevant fields. */
|
|
1155
|
-
/** Alias in the
|
|
1151
|
+
/** Alias in the task output: "Delegated to apollo: [apo-1] (task …)". */
|
|
1156
1152
|
const DELEGATE_ALIAS_PATTERN = /\[([a-z]{2,8}-\d+)\]/i;
|
|
1157
|
-
/** Child task id in the
|
|
1153
|
+
/** Child task id in the task output: "(task ses_child_9)". */
|
|
1158
1154
|
const DELEGATE_TASKID_PATTERN = /\(task\s+([a-z0-9_]+)\)/i;
|
|
1159
|
-
/** Plain alias, as passed to pantheon_delegation_read input.id: "apo-1". */
|
|
1160
|
-
const READ_ALIAS_PATTERN = /^[a-z]{2,8}-\d+$/i;
|
|
1161
1155
|
/** Extract the tool name + args from a `message.part.updated` part and
|
|
1162
1156
|
* reduce it to what the panel needs. Returns null for anything that is
|
|
1163
|
-
* not
|
|
1164
|
-
*
|
|
1165
|
-
* or is missing its callID. */
|
|
1157
|
+
* not the native `task` subagent tool (parentID === caller mechanism —
|
|
1158
|
+
* its children render `nat:`), or is missing its callID. */
|
|
1166
1159
|
function parseDelegationToolPart(part, now = Date.now()) {
|
|
1167
1160
|
if (part.type !== "tool") return null;
|
|
1168
|
-
if (part.tool !== "
|
|
1161
|
+
if (part.tool !== "task") return null;
|
|
1169
1162
|
const callID = part.callID;
|
|
1170
1163
|
if (callID === void 0 || callID === "") return null;
|
|
1171
1164
|
const sessionID = part.sessionID ?? "";
|
|
@@ -1175,28 +1168,6 @@ function parseDelegationToolPart(part, now = Date.now()) {
|
|
|
1175
1168
|
const input = state.input ?? {};
|
|
1176
1169
|
const startedAt = state.time?.start ?? now;
|
|
1177
1170
|
const endAt = state.time?.end ?? null;
|
|
1178
|
-
if (part.tool === "pantheon_delegation_read") {
|
|
1179
|
-
const target = typeof input.id === "string" ? input.id : null;
|
|
1180
|
-
let alias = null;
|
|
1181
|
-
let taskID = null;
|
|
1182
|
-
if (target !== null) {
|
|
1183
|
-
if (READ_ALIAS_PATTERN.test(target)) alias = target;
|
|
1184
|
-
else if (target.startsWith("ses_")) taskID = target;
|
|
1185
|
-
}
|
|
1186
|
-
return {
|
|
1187
|
-
callID,
|
|
1188
|
-
partID: part.id ?? "",
|
|
1189
|
-
sessionID,
|
|
1190
|
-
tool: "pantheon_delegation_read",
|
|
1191
|
-
agent: null,
|
|
1192
|
-
description: "",
|
|
1193
|
-
status,
|
|
1194
|
-
alias,
|
|
1195
|
-
taskID,
|
|
1196
|
-
startedAt,
|
|
1197
|
-
endAt
|
|
1198
|
-
};
|
|
1199
|
-
}
|
|
1200
1171
|
const agent = typeof input.agent === "string" ? input.agent : typeof input.subagent_type === "string" ? input.subagent_type : "agent";
|
|
1201
1172
|
const description = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt.slice(0, 120) : "";
|
|
1202
1173
|
let alias = null;
|
|
@@ -1210,7 +1181,7 @@ function parseDelegationToolPart(part, now = Date.now()) {
|
|
|
1210
1181
|
callID,
|
|
1211
1182
|
partID: part.id ?? "",
|
|
1212
1183
|
sessionID,
|
|
1213
|
-
tool:
|
|
1184
|
+
tool: "task",
|
|
1214
1185
|
agent,
|
|
1215
1186
|
description,
|
|
1216
1187
|
status,
|
|
@@ -1220,38 +1191,11 @@ function parseDelegationToolPart(part, now = Date.now()) {
|
|
|
1220
1191
|
endAt
|
|
1221
1192
|
};
|
|
1222
1193
|
}
|
|
1223
|
-
/** Find a live entry by alias or taskID (read parts resolve by id). */
|
|
1224
|
-
function findLiveByTarget(map, alias, taskID) {
|
|
1225
|
-
if (alias === null && taskID === null) return void 0;
|
|
1226
|
-
for (const entry of map.values()) {
|
|
1227
|
-
if (alias !== null && entry.alias === alias) return entry;
|
|
1228
|
-
if (taskID !== null && entry.taskID === taskID) return entry;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
1194
|
/** Apply one tool part to the live map. Returns true when the map changed.
|
|
1232
1195
|
* Pure w.r.t. I/O — only mutates `map`. */
|
|
1233
1196
|
function reduceDelegationToolPart(map, part, now = Date.now()) {
|
|
1234
1197
|
const parsed = parseDelegationToolPart(part, now);
|
|
1235
1198
|
if (parsed === null) return false;
|
|
1236
|
-
if (parsed.tool === "pantheon_delegation_read") {
|
|
1237
|
-
const target = findLiveByTarget(map, parsed.alias, parsed.taskID);
|
|
1238
|
-
if (target === void 0) return false;
|
|
1239
|
-
let changed = false;
|
|
1240
|
-
if (!target.read) {
|
|
1241
|
-
target.read = true;
|
|
1242
|
-
changed = true;
|
|
1243
|
-
}
|
|
1244
|
-
if (parsed.status === "completed" && target.state === "running") {
|
|
1245
|
-
target.state = "completed";
|
|
1246
|
-
target.updatedAt = parsed.endAt ?? now;
|
|
1247
|
-
changed = true;
|
|
1248
|
-
} else if (parsed.status === "error" && target.state === "running") {
|
|
1249
|
-
target.state = "error";
|
|
1250
|
-
target.updatedAt = parsed.endAt ?? now;
|
|
1251
|
-
changed = true;
|
|
1252
|
-
}
|
|
1253
|
-
return changed;
|
|
1254
|
-
}
|
|
1255
1199
|
const existing = map.get(parsed.callID);
|
|
1256
1200
|
if (parsed.status === "error") {
|
|
1257
1201
|
if (existing !== void 0 && existing.state === "error" && existing.updatedAt === parsed.endAt) return false;
|
|
@@ -1260,14 +1204,13 @@ function reduceDelegationToolPart(map, part, now = Date.now()) {
|
|
|
1260
1204
|
partID: parsed.partID,
|
|
1261
1205
|
sessionID: parsed.sessionID,
|
|
1262
1206
|
tool: parsed.tool,
|
|
1263
|
-
agent: parsed.agent
|
|
1207
|
+
agent: parsed.agent,
|
|
1264
1208
|
description: parsed.description,
|
|
1265
1209
|
alias: existing?.alias ?? null,
|
|
1266
1210
|
taskID: existing?.taskID ?? null,
|
|
1267
1211
|
state: "error",
|
|
1268
1212
|
startedAt: existing?.startedAt ?? parsed.startedAt,
|
|
1269
|
-
updatedAt: parsed.endAt ?? now
|
|
1270
|
-
read: existing?.read ?? false
|
|
1213
|
+
updatedAt: parsed.endAt ?? now
|
|
1271
1214
|
});
|
|
1272
1215
|
return true;
|
|
1273
1216
|
}
|
|
@@ -1277,14 +1220,13 @@ function reduceDelegationToolPart(map, part, now = Date.now()) {
|
|
|
1277
1220
|
partID: parsed.partID,
|
|
1278
1221
|
sessionID: parsed.sessionID,
|
|
1279
1222
|
tool: parsed.tool,
|
|
1280
|
-
agent: parsed.agent
|
|
1223
|
+
agent: parsed.agent,
|
|
1281
1224
|
description: parsed.description,
|
|
1282
1225
|
alias: parsed.alias,
|
|
1283
1226
|
taskID: parsed.taskID,
|
|
1284
1227
|
state: "running",
|
|
1285
1228
|
startedAt: parsed.startedAt,
|
|
1286
|
-
updatedAt: null
|
|
1287
|
-
read: false
|
|
1229
|
+
updatedAt: null
|
|
1288
1230
|
});
|
|
1289
1231
|
return true;
|
|
1290
1232
|
}
|
|
@@ -1321,14 +1263,14 @@ function removeDelegationEntry(map, partIDOrCallID) {
|
|
|
1321
1263
|
}
|
|
1322
1264
|
return false;
|
|
1323
1265
|
}
|
|
1324
|
-
/** Collect
|
|
1266
|
+
/** Collect native task tool parts from a session's messages.
|
|
1325
1267
|
* Messages may carry their parts inline (duck-typed `msg.parts`); when
|
|
1326
1268
|
* they don't, the optional `getParts(messageID)` callback is used (the TUI
|
|
1327
1269
|
* SDK exposes `api.state.part(messageID)`). The native `task` tool spawns a
|
|
1328
|
-
* child session with parentID = caller
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1270
|
+
* child session with parentID = caller, so its parts feed the live-map as
|
|
1271
|
+
* the refresh signal (rows come from the children channel). Pure w.r.t.
|
|
1272
|
+
* I/O — used by the mount re-scan to re-seed the live map after
|
|
1273
|
+
* compaction/attach. */
|
|
1332
1274
|
function collectDelegationToolParts(messages, getParts) {
|
|
1333
1275
|
const out = [];
|
|
1334
1276
|
for (const msg of messages ?? []) {
|
|
@@ -1338,7 +1280,7 @@ function collectDelegationToolParts(messages, getParts) {
|
|
|
1338
1280
|
if (!parts) continue;
|
|
1339
1281
|
for (const raw of parts) {
|
|
1340
1282
|
const part = raw;
|
|
1341
|
-
if (part?.type === "tool" &&
|
|
1283
|
+
if (part?.type === "tool" && part.tool === "task") out.push(part);
|
|
1342
1284
|
}
|
|
1343
1285
|
}
|
|
1344
1286
|
return out;
|
|
@@ -1353,7 +1295,7 @@ function seedLiveDelegationMap(map, parts, now = Date.now()) {
|
|
|
1353
1295
|
return changed;
|
|
1354
1296
|
}
|
|
1355
1297
|
/** Convert a live entry into the shared display shape. Alias falls back to
|
|
1356
|
-
* a `live-<callID>` prefix while the
|
|
1298
|
+
* a `live-<callID>` prefix while the task call has not completed yet. */
|
|
1357
1299
|
function toDelegationEntry(live) {
|
|
1358
1300
|
return {
|
|
1359
1301
|
alias: live.alias ?? `live-${live.callID.slice(0, 8)}`,
|
|
@@ -1365,7 +1307,6 @@ function toDelegationEntry(live) {
|
|
|
1365
1307
|
updatedAt: live.updatedAt,
|
|
1366
1308
|
timedOut: false,
|
|
1367
1309
|
description: live.description,
|
|
1368
|
-
read: live.read,
|
|
1369
1310
|
source: "live"
|
|
1370
1311
|
};
|
|
1371
1312
|
}
|
|
@@ -1640,7 +1581,7 @@ function ceilingDelegationList(all, maxVisible = 8, now = Date.now()) {
|
|
|
1640
1581
|
hiddenTerminal
|
|
1641
1582
|
};
|
|
1642
1583
|
}
|
|
1643
|
-
/** Split a display list into native task() rows vs
|
|
1584
|
+
/** Split a display list into native task() rows vs pantheon report rows.
|
|
1644
1585
|
* Native = source 'children-only' (no delegate report); everything else counts
|
|
1645
1586
|
* as pantheon. Pure — powers the hooks.log line. */
|
|
1646
1587
|
function countDelegationSources(entries) {
|
|
@@ -1665,8 +1606,8 @@ function formatPanelLogLine(children, pantheon, native, md, events) {
|
|
|
1665
1606
|
* report still renders: description from its title, agent from the child
|
|
1666
1607
|
* itself (fallback 'agent'), state derived from its status, startedAt from
|
|
1667
1608
|
* time.created. A report-less child is a NATIVE task() child (every
|
|
1668
|
-
* child of the current session —
|
|
1669
|
-
*
|
|
1609
|
+
* child of the current session — the native `task()` tool — carries
|
|
1610
|
+
* parentID = caller), so it gets source 'children-only', a
|
|
1670
1611
|
* per-child alias 'native-<last4 of the child id>' (one identity per native
|
|
1671
1612
|
* row) instead of a report alias. The 'task nativa' description fallback
|
|
1672
1613
|
* keeps the row non-empty when the child carries no title.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pantheon-tui",
|
|
9
|
-
"version": "1.5.
|
|
9
|
+
"version": "1.5.2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": ">=1.14.0",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"rolldown-plugin-solid": "^0.2.1",
|
|
19
19
|
"tsdown": "^0.22.14"
|
|
20
20
|
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
23
|
+
},
|
|
21
24
|
"peerDependencies": {
|
|
22
25
|
"@opencode-ai/plugin": ">=1.14.0",
|
|
23
26
|
"@opencode-ai/sdk": ">=1.14.0",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.2",
|
|
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
|
+
"engines": {
|
|
8
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
"./tui": "./dist/tui.js",
|
|
9
12
|
"./server": "./dist/server.js"
|