opencode-subagent-magazine 1.6.0 → 1.7.0

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 (44) hide show
  1. package/README.md +12 -1
  2. package/dist/_version.d.ts +1 -1
  3. package/dist/_version.js +1 -1
  4. package/dist/core/format.d.ts +7 -0
  5. package/dist/core/format.js +45 -0
  6. package/dist/core/kv.d.ts +21 -1
  7. package/dist/core/kv.js +33 -1
  8. package/dist/core/types.d.ts +12 -0
  9. package/dist/i18n.d.ts +19 -0
  10. package/dist/i18n.js +76 -0
  11. package/dist/index.js +73 -2
  12. package/dist/panel/SubAgentPanel.d.ts +5 -1
  13. package/dist/panel/SubAgentPanel.js +568 -261
  14. package/dist/panel/entry-map.d.ts +16 -0
  15. package/dist/panel/entry-map.js +74 -0
  16. package/dist/panel/panel-api.d.ts +8 -0
  17. package/dist/server.d.ts +3 -1
  18. package/dist/server.js +8 -0
  19. package/dist/tui.js +927 -431
  20. package/dist/v2/commands.js +188 -44
  21. package/dist/v2/db.d.ts +47 -0
  22. package/dist/v2/db.js +267 -0
  23. package/dist/v2/index.js +26 -6
  24. package/dist/v2/settings-menu.d.ts +9 -0
  25. package/dist/v2/settings-menu.js +225 -0
  26. package/dist/v2/v2-panel-api.js +304 -44
  27. package/dist/v2.js +1741 -560
  28. package/package.json +1 -1
  29. package/src/_version.ts +1 -1
  30. package/src/core/format.ts +44 -0
  31. package/src/core/kv.ts +42 -2
  32. package/src/core/types.ts +12 -0
  33. package/src/i18n.ts +76 -0
  34. package/src/index.tsx +93 -2
  35. package/src/panel/SubAgentPanel.tsx +541 -233
  36. package/src/panel/entry-map.ts +77 -1
  37. package/src/panel/panel-api.ts +8 -0
  38. package/src/server.ts +10 -1
  39. package/src/v2/commands.ts +187 -43
  40. package/src/v2/db.ts +274 -0
  41. package/src/v2/index.tsx +49 -7
  42. package/src/v2/settings-menu.ts +241 -0
  43. package/src/v2/types.ts +3 -1
  44. package/src/v2/v2-panel-api.ts +241 -33
package/dist/v2.js CHANGED
@@ -2,6 +2,274 @@
2
2
  import { createComponent as _$createComponent2 } from "@opentui/solid";
3
3
  import { createSignal as createSignal3 } from "solid-js";
4
4
 
5
+ // src/core/kv.ts
6
+ var KV_PREFIX = "subagent_magazine";
7
+ var SESSION_DATA_KEY = `${KV_PREFIX}.session_data`;
8
+ var SETTING_KEYS = {
9
+ lang: `${KV_PREFIX}.lang`,
10
+ maxEntries: `${KV_PREFIX}.max_entries`,
11
+ order: `${KV_PREFIX}.order`,
12
+ scrollMode: `${KV_PREFIX}.scroll_mode`,
13
+ open: `${KV_PREFIX}.open`,
14
+ ttlDays: `${KV_PREFIX}.ttl_days`,
15
+ border: `${KV_PREFIX}.border`,
16
+ showEntryCost: `${KV_PREFIX}.show_entry_cost`,
17
+ showEntryTime: `${KV_PREFIX}.show_entry_time`,
18
+ showEntryTokens: `${KV_PREFIX}.show_entry_tokens`,
19
+ timeFormat: `${KV_PREFIX}.time_format`,
20
+ dbSync: `${KV_PREFIX}.db_sync`
21
+ };
22
+ function loadSessionData(kv) {
23
+ try {
24
+ const raw = kv.get(SESSION_DATA_KEY, "{}");
25
+ return JSON.parse(String(raw));
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+ function updateSessionData(kv, mutator) {
31
+ try {
32
+ if (typeof kv.update === "function") {
33
+ return kv.update(SESSION_DATA_KEY, (raw) => {
34
+ const data2 = parseSessionData(raw);
35
+ mutator(data2);
36
+ return JSON.stringify(data2);
37
+ });
38
+ }
39
+ const data = loadSessionData(kv);
40
+ mutator(data);
41
+ kv.set(SESSION_DATA_KEY, JSON.stringify(data));
42
+ } catch {
43
+ }
44
+ }
45
+ function parseSessionData(raw) {
46
+ try {
47
+ return JSON.parse(String(raw ?? "{}"));
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+ function readTTLDays(kv) {
53
+ const ttlDaysRaw = parseInt(String(kv.get(SETTING_KEYS.ttlDays, "3")), 10);
54
+ return Number.isNaN(ttlDaysRaw) ? 3 : ttlDaysRaw;
55
+ }
56
+
57
+ // src/v2/db.ts
58
+ import { homedir } from "node:os";
59
+ var BUN_SQLITE = "bun:sqlite";
60
+ function modelIdOf(value) {
61
+ if (typeof value === "string") {
62
+ const s = value.trim();
63
+ if (s.length === 0) return void 0;
64
+ if (s.startsWith("{")) {
65
+ try {
66
+ return modelIdOf(JSON.parse(s));
67
+ } catch {
68
+ return void 0;
69
+ }
70
+ }
71
+ return s;
72
+ }
73
+ if (value && typeof value === "object") {
74
+ const id = value.id;
75
+ if (id === void 0) return void 0;
76
+ const direct = modelIdOf(id);
77
+ if (direct !== void 0) return direct;
78
+ const s = String(id);
79
+ return s.length > 0 ? s : void 0;
80
+ }
81
+ return void 0;
82
+ }
83
+ function contextTokens(tokens) {
84
+ if (!tokens || typeof tokens !== "object") return void 0;
85
+ const t = tokens;
86
+ const cache = t.cache && typeof t.cache === "object" ? t.cache : {};
87
+ const sum = (Number(t.input) || 0) + (Number(t.output) || 0) + (Number(t.reasoning) || 0) + (Number(cache.read) || 0) + (Number(cache.write) || 0);
88
+ return sum > 0 ? sum : void 0;
89
+ }
90
+ function statusOfChild(info) {
91
+ if (!info || info.timeIdle == null) return void 0;
92
+ return info.idleOutcome === "interrupted" ? "cancelled" : "done";
93
+ }
94
+ function findDbPath(env = process.env, home = safeHomedir()) {
95
+ if (env.OPENCODE_DB) return env.OPENCODE_DB;
96
+ const base = env.XDG_DATA_HOME && env.XDG_DATA_HOME.length > 0 ? env.XDG_DATA_HOME : `${home}/.local/share`;
97
+ if (!base || base === "/.local/share") return void 0;
98
+ return `${base}/opencode/opencode.db`;
99
+ }
100
+ function safeHomedir() {
101
+ try {
102
+ return homedir();
103
+ } catch {
104
+ return "";
105
+ }
106
+ }
107
+ var CALL_MAP_SQL = `
108
+ SELECT json_extract(p.value, '$.id') AS call_id,
109
+ json_extract(p.value, '$.state.metadata.sessionID') AS child_sid
110
+ FROM session_message m, json_each(m.data, '$.content') p
111
+ WHERE m.session_id = ? AND m.type = 'assistant'
112
+ AND json_extract(p.value, '$.id') LIKE 'call_%'
113
+ AND json_extract(p.value, '$.state.metadata.sessionID') IS NOT NULL`;
114
+ var CHILD_INFO_SQL = `
115
+ SELECT parent_id, agent, title, model, cost,
116
+ tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
117
+ time_created, time_idle, idle_outcome
118
+ FROM session_v2 WHERE id = ?`;
119
+ var LAST_ASSISTANT_SQL = `
120
+ SELECT json_extract(data, '$.tokens.input') AS input,
121
+ json_extract(data, '$.tokens.output') AS output,
122
+ json_extract(data, '$.tokens.reasoning') AS reasoning,
123
+ json_extract(data, '$.tokens.cache.read') AS cache_read,
124
+ json_extract(data, '$.tokens.cache.write') AS cache_write,
125
+ json_extract(data, '$.model.id') AS model_id
126
+ FROM session_message
127
+ WHERE session_id = ? AND type = 'assistant' AND json_extract(data, '$.tokens.output') > 0
128
+ ORDER BY seq DESC LIMIT 1`;
129
+ var ASSISTANT_COST_SQL = `
130
+ SELECT COUNT(*) AS n, SUM(COALESCE(json_extract(data, '$.cost'), 0)) AS total
131
+ FROM session_message WHERE session_id = ? AND type = 'assistant'`;
132
+ var CHILDREN_SQL = `
133
+ SELECT id, agent, time_created FROM session_v2 WHERE parent_id = ? ORDER BY time_created`;
134
+ var num = (v) => {
135
+ const n = Number(v);
136
+ return Number.isFinite(n) ? n : void 0;
137
+ };
138
+ var str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
139
+ function createSessionDbIndex(enabled) {
140
+ const dbPath = findDbPath();
141
+ if (!dbPath) return void 0;
142
+ let db;
143
+ let failed = false;
144
+ void (async () => {
145
+ try {
146
+ const mod2 = await import(BUN_SQLITE);
147
+ const Database = mod2?.Database;
148
+ if (!Database) {
149
+ failed = true;
150
+ return;
151
+ }
152
+ db = new Database(dbPath, { readonly: true });
153
+ try {
154
+ db.query("pragma query_only = on").all();
155
+ } catch {
156
+ }
157
+ } catch {
158
+ failed = true;
159
+ db = void 0;
160
+ }
161
+ })();
162
+ const CALL_MAP_TTL_MS = 15e3;
163
+ const CALL_MAP_RETRY_MS = 2e3;
164
+ const INFO_TTL_MS = 5e3;
165
+ const callMaps = /* @__PURE__ */ new Map();
166
+ const infos = /* @__PURE__ */ new Map();
167
+ const childrenCache = /* @__PURE__ */ new Map();
168
+ const ready = () => {
169
+ if (failed || db === void 0) return false;
170
+ try {
171
+ return enabled();
172
+ } catch {
173
+ return false;
174
+ }
175
+ };
176
+ const buildCallMap = (parentId) => {
177
+ const now = Date.now();
178
+ const prev = callMaps.get(parentId);
179
+ if (prev && now - prev.at < CALL_MAP_RETRY_MS) return prev.map;
180
+ const map = /* @__PURE__ */ new Map();
181
+ try {
182
+ for (const row of db.query(CALL_MAP_SQL).all(parentId)) {
183
+ const id = str(row.call_id);
184
+ const sid = str(row.child_sid);
185
+ if (id && sid) map.set(id, sid);
186
+ }
187
+ } catch {
188
+ }
189
+ callMaps.set(parentId, { map, at: now });
190
+ return map;
191
+ };
192
+ const resolveCall = (parentId, callId) => {
193
+ if (!ready() || !parentId || !callId) return void 0;
194
+ const prev = callMaps.get(parentId);
195
+ const map = prev && Date.now() - prev.at < CALL_MAP_TTL_MS ? prev.map : buildCallMap(parentId);
196
+ return map.get(callId) ?? buildCallMap(parentId).get(callId);
197
+ };
198
+ const matchChild = (parentId, agent, startedAt) => {
199
+ if (!ready() || !parentId || startedAt === void 0) return void 0;
200
+ const now = Date.now();
201
+ let cached = childrenCache.get(parentId);
202
+ if (!cached || now - cached.at > CALL_MAP_TTL_MS) {
203
+ let list = [];
204
+ try {
205
+ list = db.query(CHILDREN_SQL).all(parentId);
206
+ } catch {
207
+ }
208
+ cached = { list, at: now };
209
+ childrenCache.set(parentId, cached);
210
+ }
211
+ let best;
212
+ let bestAt = Number.POSITIVE_INFINITY;
213
+ for (const row of cached.list) {
214
+ const sid = str(row.id);
215
+ const t = num(row.time_created);
216
+ if (!sid || t === void 0) continue;
217
+ if (t < startedAt - 2e3) continue;
218
+ if (agent !== void 0 && str(row.agent) !== agent) continue;
219
+ if (t < bestAt) {
220
+ bestAt = t;
221
+ best = sid;
222
+ }
223
+ }
224
+ return best;
225
+ };
226
+ const info = (sid) => {
227
+ if (!sid) return void 0;
228
+ const hit = infos.get(sid);
229
+ if (hit && Date.now() - hit.at < INFO_TTL_MS) return hit.info;
230
+ if (!ready()) return void 0;
231
+ let result;
232
+ try {
233
+ const row = db.query(CHILD_INFO_SQL).get(sid);
234
+ if (row) {
235
+ const last = db.query(LAST_ASSISTANT_SQL).get(sid);
236
+ let tokens = contextTokens(last ? {
237
+ input: last.input,
238
+ output: last.output,
239
+ reasoning: last.reasoning,
240
+ cache: { read: last.cache_read, write: last.cache_write }
241
+ } : void 0);
242
+ if (tokens === void 0) {
243
+ const agg = (num(row.tokens_input) || 0) + (num(row.tokens_output) || 0) + (num(row.tokens_reasoning) || 0) + (num(row.tokens_cache_read) || 0) + (num(row.tokens_cache_write) || 0);
244
+ if (agg > 0) tokens = agg;
245
+ }
246
+ let cost = num(row.cost);
247
+ if (!(typeof cost === "number" && cost > 0)) {
248
+ const aggCost = db.query(ASSISTANT_COST_SQL).get(sid);
249
+ if (aggCost && Number(aggCost.n) > 0) cost = Number(aggCost.total) || 0;
250
+ }
251
+ result = {
252
+ id: sid,
253
+ parentId: str(row.parent_id),
254
+ agent: str(row.agent),
255
+ title: str(row.title),
256
+ model: modelIdOf(last?.model_id) ?? modelIdOf(row.model),
257
+ cost,
258
+ tokens,
259
+ timeCreated: num(row.time_created),
260
+ timeIdle: num(row.time_idle),
261
+ idleOutcome: str(row.idle_outcome)
262
+ };
263
+ }
264
+ } catch {
265
+ result = void 0;
266
+ }
267
+ infos.set(sid, { info: result, at: Date.now() });
268
+ return result;
269
+ };
270
+ return { enabled: ready, resolveCall, matchChild, info };
271
+ }
272
+
5
273
  // src/v2/v2-panel-api.ts
6
274
  var SUBAGENT_TOOL_V2 = "subagent";
7
275
  function normalizeMeta(meta) {
@@ -18,7 +286,11 @@ function toV1Part(p) {
18
286
  if (st.output === void 0 && Array.isArray(st.content)) {
19
287
  st.output = st.content.map((c) => c.type === "text" ? c.text : c.type === "file" ? String(c.path ?? c.filename ?? "") : JSON.stringify(c)).filter((t) => typeof t === "string" && t.length > 0).join("\n");
20
288
  }
21
- return { type: "tool", tool: p.tool ?? p.name, state: st, subagent_type: p.subagent_type, metadata: normalizeMeta(p.metadata) };
289
+ const meta = normalizeMeta({
290
+ ...st.metadata ?? {},
291
+ ...p.metadata ?? {}
292
+ });
293
+ return { type: "tool", id: p.id, tool: p.tool ?? p.name, state: st, subagent_type: p.subagent_type, metadata: meta };
22
294
  }
23
295
  if (p.type === "text") return { type: "text", text: String(p.text ?? "") };
24
296
  if (p.type === "file") return { type: "file", source: p.source ?? {} };
@@ -55,7 +327,60 @@ function createPanelApi(context, settings) {
55
327
  d.value = value;
56
328
  });
57
329
  };
330
+ const kvUpdate = (key, updater) => {
331
+ let entry = kvStore.get(key);
332
+ if (!entry) {
333
+ const created = context.storage.store(`subagent_magazine.${key}`, {
334
+ initial: { value: void 0 }
335
+ });
336
+ entry = created;
337
+ kvStore.set(key, entry);
338
+ }
339
+ const [, mutate] = entry;
340
+ return mutate((d) => {
341
+ d.value = updater(d.value);
342
+ });
343
+ };
344
+ const dbIndex = createSessionDbIndex(() => kvGet(SETTING_KEYS.dbSync, true) !== false);
345
+ const withResolvedSid = (data, meta) => {
346
+ if (meta.sessionID !== void 0) return meta;
347
+ try {
348
+ const parent = data.sessionID !== void 0 ? String(data.sessionID) : "";
349
+ const callId = data.id !== void 0 ? String(data.id) : "";
350
+ const sid = parent && callId ? dbIndex?.resolveCall(parent, callId) : void 0;
351
+ if (sid) return normalizeMeta({ ...meta, sessionID: sid });
352
+ } catch {
353
+ }
354
+ return meta;
355
+ };
58
356
  const isSubagentTool = (name) => name === SUBAGENT_TOOL_V2 || name === "task" || name === "delegate" || name === "call_omo_agent";
357
+ const findToolPart = (data) => {
358
+ const sid = data.sessionID;
359
+ const msgID = data.assistantMessageID;
360
+ const partID = data.id;
361
+ if (sid === void 0 || msgID === void 0 || partID === void 0) return void 0;
362
+ try {
363
+ const raw = context.data.session.message.list(String(sid));
364
+ if (!raw) return void 0;
365
+ for (let i = raw.length - 1; i >= 0; i--) {
366
+ const m = raw[i];
367
+ if (!m || String(m.id) !== String(msgID)) continue;
368
+ const content = Array.isArray(m.content) ? m.content : [];
369
+ for (const p of content) {
370
+ const pp = p;
371
+ if (pp && pp.type === "tool" && String(pp.id) === String(partID)) {
372
+ const st = pp.state ?? {};
373
+ const input = st.input && typeof st.input === "object" ? st.input : void 0;
374
+ const name = pp.name ?? pp.tool;
375
+ return { name: name !== void 0 ? String(name) : void 0, input };
376
+ }
377
+ }
378
+ break;
379
+ }
380
+ } catch {
381
+ }
382
+ return void 0;
383
+ };
59
384
  const toolEventToPart = (event) => {
60
385
  const type = event.type;
61
386
  const data = event.data;
@@ -73,20 +398,29 @@ function createPanelApi(context, settings) {
73
398
  return { type: "tool", tool: normName(String(data.name)), id: String(data.id), state: { status: "pending", input: {} } };
74
399
  }
75
400
  if (type === "session.tool.called") {
76
- const name = data.name ?? info?.name;
401
+ let name = data.name ?? info?.name;
402
+ let input = data.input ?? info?.input;
403
+ if (!name) {
404
+ const resolved = findToolPart(data);
405
+ if (resolved?.name) {
406
+ name = resolved.name;
407
+ if (input === void 0) input = resolved.input;
408
+ }
409
+ }
77
410
  if (name && !isSubagentTool(name)) return void 0;
78
411
  if (!name) return void 0;
79
- if (key) toolInfo.set(key, { name: String(name), input: data.input ?? {} });
412
+ if (key) toolInfo.set(key, { name: String(name), input: input ?? {} });
80
413
  return {
81
414
  type: "tool",
82
415
  tool: normName(String(name)),
83
416
  id: String(data.id),
84
- subagent_type: agentOf(data.input),
85
- state: { status: "running", input: data.input ?? {}, metadata: {} }
417
+ subagent_type: agentOf(input),
418
+ state: { status: "running", input: input ?? {}, metadata: withResolvedSid(data, {}) }
86
419
  };
87
420
  }
88
421
  if (type === "session.tool.progress") {
89
- const name = data.name ?? info?.name;
422
+ let name = data.name ?? info?.name;
423
+ if (!name) name = findToolPart(data)?.name;
90
424
  if (name && !isSubagentTool(name)) return void 0;
91
425
  if (!name) return void 0;
92
426
  return {
@@ -94,24 +428,42 @@ function createPanelApi(context, settings) {
94
428
  tool: normName(String(name)),
95
429
  id: String(data.id),
96
430
  subagent_type: agentOf(info?.input),
97
- state: { status: "running", input: info?.input ?? {}, metadata: normalizeMeta(data.metadata) }
431
+ state: { status: "running", input: info?.input ?? {}, metadata: withResolvedSid(data, normalizeMeta(data.metadata)) }
98
432
  };
99
433
  }
100
434
  if (type === "session.tool.success" || type === "session.tool.failed") {
101
- const meta = normalizeMeta(data.metadata);
102
- if (meta.sessionID === void 0) return void 0;
435
+ const eventMeta = normalizeMeta(data.metadata);
436
+ const meta = withResolvedSid(data, eventMeta);
103
437
  if (type === "session.tool.failed" && meta.status === "running") return void 0;
104
- const name = data.name ?? info?.name;
438
+ let name = data.name ?? info?.name;
439
+ let input = info?.input ?? data.input;
440
+ if (!name || input === void 0) {
441
+ const resolved = findToolPart(data);
442
+ if (!name && resolved?.name) name = resolved.name;
443
+ if (input === void 0 && resolved?.input) input = resolved.input;
444
+ }
105
445
  if (name && !isSubagentTool(name)) return void 0;
106
- const input = info?.input ?? data.input ?? {};
446
+ if (key && name) toolInfo.set(key, { name: String(name), input: input ?? info?.input ?? {} });
447
+ const finalInput = input ?? {};
448
+ if (meta.sessionID === void 0 && (finalInput.background === true || finalInput.run_in_background === true)) {
449
+ return void 0;
450
+ }
451
+ let status = type === "session.tool.failed" ? "error" : "completed";
452
+ if (meta.sessionID !== void 0 && meta.sessionID !== eventMeta.sessionID) {
453
+ try {
454
+ const childInfo = dbIndex?.info(String(meta.sessionID));
455
+ if (childInfo && statusOfChild(childInfo) === void 0) status = "running";
456
+ } catch {
457
+ }
458
+ }
107
459
  return {
108
460
  type: "tool",
109
461
  tool: normName(String(name ?? "task")),
110
462
  id: String(data.id),
111
- subagent_type: agentOf(input),
463
+ subagent_type: agentOf(finalInput),
112
464
  state: {
113
- status: type === "session.tool.failed" ? "error" : "completed",
114
- input,
465
+ status,
466
+ input: finalInput,
115
467
  metadata: meta
116
468
  }
117
469
  };
@@ -133,14 +485,55 @@ function createPanelApi(context, settings) {
133
485
  }
134
486
  return out;
135
487
  };
488
+ const usageCache = /* @__PURE__ */ new Map();
489
+ const syncRequestedAt = /* @__PURE__ */ new Map();
490
+ const USAGE_CACHE_TTL_MS = 5e3;
491
+ const SYNC_THROTTLE_MS = 3e4;
492
+ const rememberUsage = (sid, patch) => {
493
+ const prev = usageCache.get(sid);
494
+ usageCache.set(sid, { ...prev, ...patch, at: Date.now() });
495
+ };
496
+ const cachedUsage = (sid) => {
497
+ const hit = usageCache.get(sid);
498
+ if (!hit) return void 0;
499
+ if (Date.now() - hit.at > USAGE_CACHE_TTL_MS) return void 0;
500
+ return hit;
501
+ };
502
+ const requestMessageSync = (sid) => {
503
+ const last = syncRequestedAt.get(sid) ?? 0;
504
+ if (Date.now() - last < SYNC_THROTTLE_MS) return;
505
+ syncRequestedAt.set(sid, Date.now());
506
+ try {
507
+ const sync = context.data.session.message.sync;
508
+ if (typeof sync === "function") void sync.call(context.data.session.message, sid).catch(() => {
509
+ });
510
+ } catch {
511
+ }
512
+ };
513
+ const listedSession = (sid) => {
514
+ try {
515
+ return context.data.session.list()?.find((s) => s.id === sid);
516
+ } catch {
517
+ return void 0;
518
+ }
519
+ };
136
520
  return {
137
521
  kv: {
138
522
  get: kvGet,
139
- set: kvSet
523
+ set: kvSet,
524
+ update: kvUpdate
140
525
  },
141
526
  usage: {
142
527
  readSessionTokens: (sid) => {
143
528
  if (!sid) return void 0;
529
+ try {
530
+ const dbTokens = dbIndex?.info(sid)?.tokens;
531
+ if (dbTokens !== void 0) {
532
+ rememberUsage(sid, { tokens: dbTokens });
533
+ return dbTokens;
534
+ }
535
+ } catch {
536
+ }
144
537
  try {
145
538
  const msgs = context.data.session.message.list(sid);
146
539
  if (msgs) {
@@ -150,9 +543,15 @@ function createPanelApi(context, settings) {
150
543
  const t = m.tokens;
151
544
  if (!t || !(Number(t.output) > 0)) continue;
152
545
  const ctx = (Number(t.input) || 0) + (Number(t.output) || 0) + (Number(t.reasoning) || 0) + (Number(t.cache?.read) || 0) + (Number(t.cache?.write) || 0);
153
- if (ctx > 0) return ctx;
546
+ if (ctx > 0) {
547
+ rememberUsage(sid, { tokens: ctx });
548
+ return ctx;
549
+ }
154
550
  }
155
551
  }
552
+ const cached = cachedUsage(sid)?.tokens;
553
+ if (cached !== void 0) return cached;
554
+ requestMessageSync(sid);
156
555
  return void 0;
157
556
  } catch {
158
557
  return void 0;
@@ -160,33 +559,91 @@ function createPanelApi(context, settings) {
160
559
  },
161
560
  readSessionCost: (sid) => {
162
561
  if (!sid) return void 0;
562
+ try {
563
+ const dbCost = dbIndex?.info(sid)?.cost;
564
+ if (dbCost !== void 0) {
565
+ rememberUsage(sid, { cost: dbCost });
566
+ return dbCost;
567
+ }
568
+ } catch {
569
+ }
570
+ try {
571
+ const direct = context.data.session.cost(sid);
572
+ if (typeof direct === "number" && direct > 0) {
573
+ rememberUsage(sid, { cost: direct });
574
+ return direct;
575
+ }
576
+ } catch {
577
+ }
163
578
  try {
164
579
  const session = context.data.session.get(sid);
165
- if (session?.cost != null && session.cost > 0) return session.cost;
580
+ if (session?.cost != null && session.cost > 0) {
581
+ rememberUsage(sid, { cost: session.cost });
582
+ return session.cost;
583
+ }
584
+ const listed = listedSession(sid);
585
+ if (listed?.cost != null && listed.cost > 0) {
586
+ rememberUsage(sid, { cost: listed.cost });
587
+ return listed.cost;
588
+ }
166
589
  const msgs = context.data.session.message.list(sid);
167
- if (!msgs) return void 0;
168
- let total = 0;
169
- for (const m of msgs) {
170
- if (m.type === "assistant" && typeof m.cost === "number") total += m.cost;
590
+ if (Array.isArray(msgs) && msgs.length > 0) {
591
+ let total = 0;
592
+ let sawAssistant = false;
593
+ for (const m of msgs) {
594
+ if (m.type !== "assistant") continue;
595
+ sawAssistant = true;
596
+ if (typeof m.cost === "number") total += m.cost;
597
+ }
598
+ if (sawAssistant) {
599
+ rememberUsage(sid, { cost: total });
600
+ return total;
601
+ }
171
602
  }
172
- return total > 0 ? total : void 0;
603
+ const cached = cachedUsage(sid)?.cost;
604
+ if (cached !== void 0) return cached;
605
+ requestMessageSync(sid);
606
+ return void 0;
173
607
  } catch {
174
608
  return void 0;
175
609
  }
176
610
  },
177
611
  readSessionModel: (sid) => {
178
612
  if (!sid) return void 0;
613
+ try {
614
+ const dbModel = dbIndex?.info(sid)?.model;
615
+ if (dbModel !== void 0) {
616
+ rememberUsage(sid, { model: dbModel });
617
+ return dbModel;
618
+ }
619
+ } catch {
620
+ }
179
621
  try {
180
622
  const msgs = context.data.session.message.list(sid);
181
623
  if (msgs) {
182
624
  for (let i = msgs.length - 1; i >= 0; i--) {
183
625
  const m = msgs[i];
184
626
  if (m.type !== "assistant") continue;
185
- const model = m.model;
186
- if (typeof model === "string") return model;
187
- if (model?.id) return String(model.id);
627
+ const id = modelIdOf(m.model);
628
+ if (id) {
629
+ rememberUsage(sid, { model: id });
630
+ return id;
631
+ }
188
632
  }
189
633
  }
634
+ const sessionModel = modelIdOf(context.data.session.get(sid)?.model);
635
+ if (sessionModel) {
636
+ rememberUsage(sid, { model: sessionModel });
637
+ return sessionModel;
638
+ }
639
+ const listedModel = modelIdOf(listedSession(sid)?.model);
640
+ if (listedModel) {
641
+ rememberUsage(sid, { model: listedModel });
642
+ return listedModel;
643
+ }
644
+ const cached = cachedUsage(sid)?.model;
645
+ if (cached !== void 0) return cached;
646
+ requestMessageSync(sid);
190
647
  return void 0;
191
648
  } catch {
192
649
  return void 0;
@@ -198,7 +655,14 @@ function createPanelApi(context, settings) {
198
655
  session: {
199
656
  get: (sid) => {
200
657
  try {
201
- return context.data.session.get(sid);
658
+ const direct = context.data.session.get(sid);
659
+ if (direct) return direct;
660
+ const list = context.data.session.list?.();
661
+ if (Array.isArray(list)) {
662
+ const found = list.find((s) => s?.id === sid);
663
+ if (found) return found;
664
+ }
665
+ return void 0;
202
666
  } catch {
203
667
  return void 0;
204
668
  }
@@ -206,11 +670,17 @@ function createPanelApi(context, settings) {
206
670
  status: (sid) => {
207
671
  try {
208
672
  const st = context.data.session.status(sid);
209
- const type = st === "running" ? "busy" : st ?? "";
210
- return { type };
673
+ if (st) {
674
+ const type = st === "running" ? "busy" : st;
675
+ return { type };
676
+ }
211
677
  } catch {
212
- return void 0;
213
678
  }
679
+ try {
680
+ if (statusOfChild(dbIndex?.info(sid)) !== void 0) return { type: "idle" };
681
+ } catch {
682
+ }
683
+ return void 0;
214
684
  },
215
685
  messages: (sid) => {
216
686
  try {
@@ -229,6 +699,15 @@ function createPanelApi(context, settings) {
229
699
  return [];
230
700
  }
231
701
  return msg.content.map((p) => toV1Part(p));
702
+ },
703
+ resolveChild: (input) => {
704
+ try {
705
+ const sid = input.callId ? dbIndex?.resolveCall(input.parentId, input.callId) : void 0;
706
+ if (sid) return sid;
707
+ return dbIndex?.matchChild(input.parentId, input.agent, input.startedAt);
708
+ } catch {
709
+ return void 0;
710
+ }
232
711
  }
233
712
  },
234
713
  event: {
@@ -239,9 +718,11 @@ function createPanelApi(context, settings) {
239
718
  for (const evt of ["session.tool.input.started", "session.tool.called", "session.tool.progress", "session.tool.success", "session.tool.failed"]) {
240
719
  unsubs.push(context.data.on(evt, (e) => {
241
720
  const part = toolEventToPart(e);
242
- if (!part) return;
243
- const evt2 = e.data;
244
- const sid = evt2?.sessionID !== void 0 ? String(evt2.sessionID) : void 0;
721
+ const evtData = e.data;
722
+ if (!part) {
723
+ return;
724
+ }
725
+ const sid = evtData?.sessionID !== void 0 ? String(evtData.sessionID) : void 0;
245
726
  if (!sid) return;
246
727
  cb({ type, scope: "global", payload: { part, sessionID: sid } });
247
728
  }));
@@ -298,39 +779,90 @@ function createPanelApi(context, settings) {
298
779
  };
299
780
  }
300
781
 
301
- // src/core/kv.ts
302
- var KV_PREFIX = "subagent_magazine";
303
- var SESSION_DATA_KEY = `${KV_PREFIX}.session_data`;
304
- var SETTING_KEYS = {
305
- lang: `${KV_PREFIX}.lang`,
306
- maxEntries: `${KV_PREFIX}.max_entries`,
307
- order: `${KV_PREFIX}.order`,
308
- scrollMode: `${KV_PREFIX}.scroll_mode`,
309
- open: `${KV_PREFIX}.open`,
310
- ttlDays: `${KV_PREFIX}.ttl_days`,
311
- border: `${KV_PREFIX}.border`
312
- };
313
- function loadSessionData(kv) {
314
- try {
315
- const raw = kv.get(SESSION_DATA_KEY, "{}");
316
- return JSON.parse(String(raw));
317
- } catch {
318
- return {};
782
+ // src/core/format.ts
783
+ function charColumns(c) {
784
+ const code = c.codePointAt(0) ?? 0;
785
+ if (code < 32) return 0;
786
+ if (code < 127) return 1;
787
+ if (code < 160) return 0;
788
+ if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 128591 || code >= 131072 && code <= 262141)
789
+ return 2;
790
+ return 1;
791
+ }
792
+ function visualWidth(s) {
793
+ let w = 0;
794
+ for (const c of s) w += charColumns(c);
795
+ return w;
796
+ }
797
+ function truncate(text, maxCols) {
798
+ if (visualWidth(text) <= maxCols) return text;
799
+ let cols = 0;
800
+ let i = 0;
801
+ for (const c of text) {
802
+ const w = charColumns(c);
803
+ if (cols + w > maxCols - 1) break;
804
+ cols += w;
805
+ i += c.length;
319
806
  }
807
+ return text.slice(0, i) + "\u2026";
320
808
  }
321
- function saveSessionData(kv, data) {
322
- try {
323
- kv.set(SESSION_DATA_KEY, JSON.stringify(data));
324
- } catch {
809
+ function fmtDurationShort(ms, running) {
810
+ if (running && ms < 2e3) return "";
811
+ if (ms < 1e3) return (ms / 1e3).toFixed(2) + "s";
812
+ if (ms < 6e4) return (ms / 1e3).toFixed(2) + "s";
813
+ const m = Math.floor(ms / 6e4);
814
+ const s = Math.round(ms % 6e4 / 1e3);
815
+ return `${m}m${s}s`;
816
+ }
817
+ var TIME_FORMATS = ["short", "decimal", "clock", "compact", "seconds"];
818
+ var TIME_FORMAT_SAMPLES = {
819
+ decimal: "45.3s \xB7 9.5m \xB7 1.9h",
820
+ short: "45.32s \xB7 112m35s",
821
+ clock: "0:45 \xB7 1:52:35",
822
+ compact: "45s \xB7 1h52m",
823
+ seconds: "45s \xB7 6755s"
824
+ };
825
+ function fmtDuration(ms, running, mode = "short") {
826
+ if (running && ms < 2e3) return "";
827
+ const t = Number.isFinite(ms) && ms > 0 ? ms : 0;
828
+ switch (mode) {
829
+ case "short":
830
+ return fmtDurationShort(t, false);
831
+ case "compact": {
832
+ if (t < 6e4) return `${Math.floor(t / 1e3)}s`;
833
+ if (t < 36e5) return `${Math.floor(t / 6e4)}m${Math.floor(t % 6e4 / 1e3)}s`;
834
+ return `${Math.floor(t / 36e5)}h${Math.floor(t % 36e5 / 6e4)}m`;
835
+ }
836
+ case "clock": {
837
+ const s = Math.floor(t % 6e4 / 1e3);
838
+ if (t < 36e5) return `${Math.floor(t / 6e4)}:${String(s).padStart(2, "0")}`;
839
+ const m = Math.floor(t % 36e5 / 6e4);
840
+ return `${Math.floor(t / 36e5)}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
841
+ }
842
+ case "seconds":
843
+ return `${Math.floor(t / 1e3)}s`;
844
+ case "decimal":
845
+ default: {
846
+ if (t < 59950) return `${(t / 1e3).toFixed(1)}s`;
847
+ if (t < 3597e3) return `${(t / 6e4).toFixed(1)}m`;
848
+ return `${(t / 36e5).toFixed(1)}h`;
849
+ }
325
850
  }
326
851
  }
327
- function readTTLDays(kv) {
328
- const ttlDaysRaw = parseInt(String(kv.get(SETTING_KEYS.ttlDays, "3")), 10);
329
- return Number.isNaN(ttlDaysRaw) ? 3 : ttlDaysRaw;
852
+ function fmtTokens(n) {
853
+ if (n < 1e3) return `${n}`;
854
+ if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
855
+ return `${(n / 1e6).toFixed(1)}M`;
856
+ }
857
+ function safeErrorMsg(err) {
858
+ if (!err) return "";
859
+ if (typeof err === "string") return err;
860
+ if (typeof err === "object") return String(err.message || err.code || "");
861
+ return "";
330
862
  }
331
863
 
332
864
  // src/_version.ts
333
- var PLUGIN_VERSION = "1.6.0";
865
+ var PLUGIN_VERSION = "1.7.0";
334
866
 
335
867
  // src/i18n.ts
336
868
  var ZH_T = {
@@ -374,6 +906,7 @@ var ZH_T = {
374
906
  "clear.prompt": "\u786E\u5B9A\u6E05\u9664\u5F53\u524D\u4F1A\u8BDD\u6240\u6709\u5B50\u4EE3\u7406\u8BB0\u5F55\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002",
375
907
  "clear.prompt_running": "\u5F53\u524D\u6709 {n} \u4E2A\u8FD0\u884C\u4E2D\u7684\u5B50\u4EE3\u7406\uFF0C\u6E05\u9664\u540E\u5C06\u4E0D\u53EF\u6062\u590D\u3002\u786E\u5B9A\u7EE7\u7EED\uFF1F",
376
908
  "clear.done": "\u5DF2\u6E05\u9664 {n} \u6761\u5B50\u4EE3\u7406\u8BB0\u5F55",
909
+ "clear.failed": "\u6E05\u9664\u5931\u8D25",
377
910
  "clear.empty": "\u5F53\u524D\u4F1A\u8BDD\u65E0\u5B50\u4EE3\u7406\u8BB0\u5F55",
378
911
  "cancel.no_session": "\u5B50\u4F1A\u8BDD ID \u4E0D\u53EF\u7528",
379
912
  "cancel.not_child": "\u76EE\u6807\u4E0D\u662F\u5B50\u4F1A\u8BDD",
@@ -384,7 +917,25 @@ var ZH_T = {
384
917
  "cancel.sent": "\u5DF2\u53D1\u9001\u53D6\u6D88\u6307\u4EE4",
385
918
  "cancel.failed": "\u53D6\u6D88\u5931\u8D25",
386
919
  "borderShown": "\u9762\u677F\u8FB9\u6846 \u5DF2\u663E\u793A",
387
- "borderHidden": "\u9762\u677F\u8FB9\u6846 \u5DF2\u9690\u85CF"
920
+ "borderHidden": "\u9762\u677F\u8FB9\u6846 \u5DF2\u9690\u85CF",
921
+ "settings.title": "SubAgent Magazine \u2014 \u8BBE\u7F6E",
922
+ "settings.section.general": "\u901A\u7528",
923
+ "settings.lang": "\u8BED\u8A00",
924
+ "settings.max": "\u6700\u5927\u6761\u76EE\u6570",
925
+ "settings.order": "\u6392\u5E8F\u65B9\u5F0F",
926
+ "settings.scroll": "\u6EDA\u52A8\u6A21\u5F0F",
927
+ "settings.ttl": "\u4FDD\u7559\u671F\u9650",
928
+ "settings.border": "\u9762\u677F\u8FB9\u6846",
929
+ "settings.showEntryCost": "\u6BCF\u6761\u5B50\u4EE3\u7406\u663E\u793A\u8D39\u7528",
930
+ "settings.showEntryTime": "\u5217\u8868\u4E2D\u663E\u793A\u8017\u65F6",
931
+ "settings.showEntryTokens": "\u5217\u8868\u4E2D\u663E\u793A Token \u6570",
932
+ "settings.timeFormat": "\u65F6\u95F4\u683C\u5F0F",
933
+ "settings.dbSync": "\u672C\u5730\u6570\u636E\u5E93\u8865\u5168",
934
+ "settings.on": "\u5F00",
935
+ "settings.off": "\u5173",
936
+ "settings.saved": "\u5DF2\u4FDD\u5B58 {item}\uFF1A{value}",
937
+ "settings.invalid": "\u65E0\u6548\u7684\u6570\u503C",
938
+ "settings.close": "\u5173\u95ED"
388
939
  };
389
940
  var EN_T = {
390
941
  "panel.title": "SubAgent",
@@ -427,6 +978,7 @@ var EN_T = {
427
978
  "clear.prompt": "Clear all sub-agent records for this session? This cannot be undone.",
428
979
  "clear.prompt_running": "{n} sub-agent(s) are still running. Clearing will discard them permanently. Continue?",
429
980
  "clear.done": "Cleared {n} sub-agent record(s)",
981
+ "clear.failed": "Clear failed",
430
982
  "clear.empty": "No sub-agent records in this session",
431
983
  "cancel.no_session": "Child session ID is unavailable",
432
984
  "cancel.not_child": "Target is not a child session",
@@ -437,7 +989,25 @@ var EN_T = {
437
989
  "cancel.sent": "Cancel instruction sent",
438
990
  "cancel.failed": "Cancellation failed",
439
991
  "borderShown": "Panel border shown",
440
- "borderHidden": "Panel border hidden"
992
+ "borderHidden": "Panel border hidden",
993
+ "settings.title": "SubAgent Magazine \u2014 Settings",
994
+ "settings.section.general": "General",
995
+ "settings.lang": "Language",
996
+ "settings.max": "Max entries",
997
+ "settings.order": "Sort order",
998
+ "settings.scroll": "Scroll mode",
999
+ "settings.ttl": "Retention",
1000
+ "settings.border": "Panel border",
1001
+ "settings.showEntryCost": "Cost per sub-agent",
1002
+ "settings.showEntryTime": "Elapsed time in list",
1003
+ "settings.showEntryTokens": "Token count in list",
1004
+ "settings.timeFormat": "Time format",
1005
+ "settings.dbSync": "Local database enrichment",
1006
+ "settings.on": "on",
1007
+ "settings.off": "off",
1008
+ "settings.saved": "Saved {item}: {value}",
1009
+ "settings.invalid": "Invalid value",
1010
+ "settings.close": "Close"
441
1011
  };
442
1012
  var JA_T = {
443
1013
  "panel.title": "\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8",
@@ -480,6 +1050,7 @@ var JA_T = {
480
1050
  "clear.prompt": "\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u5168\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u8A18\u9332\u3092\u524A\u9664\u3057\u307E\u3059\u304B\uFF1F\u3053\u306E\u64CD\u4F5C\u306F\u5143\u306B\u623B\u305B\u307E\u305B\u3093\u3002",
481
1051
  "clear.prompt_running": "\u5B9F\u884C\u4E2D\u306E\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u304C {n} \u500B\u3042\u308A\u307E\u3059\u3002\u524A\u9664\u3059\u308B\u3068\u5FA9\u5143\u3067\u304D\u307E\u305B\u3093\u3002\u7D9A\u884C\u3057\u307E\u3059\u304B\uFF1F",
482
1052
  "clear.done": "{n} \u4EF6\u306E\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u8A18\u9332\u3092\u524A\u9664\u3057\u307E\u3057\u305F",
1053
+ "clear.failed": "\u30AF\u30EA\u30A2\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
483
1054
  "clear.empty": "\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306B\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u8A18\u9332\u306F\u3042\u308A\u307E\u305B\u3093",
484
1055
  "cancel.no_session": "\u5B50\u30BB\u30C3\u30B7\u30E7\u30F3 ID \u304C\u5229\u7528\u3067\u304D\u307E\u305B\u3093",
485
1056
  "cancel.not_child": "\u5BFE\u8C61\u306F\u5B50\u30BB\u30C3\u30B7\u30E7\u30F3\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
@@ -490,7 +1061,25 @@ var JA_T = {
490
1061
  "cancel.sent": "\u30AD\u30E3\u30F3\u30BB\u30EB\u6307\u793A\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F",
491
1062
  "cancel.failed": "\u30AD\u30E3\u30F3\u30BB\u30EB\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
492
1063
  "borderShown": "\u30D1\u30CD\u30EB\u67A0\u7DDA \u8868\u793A",
493
- "borderHidden": "\u30D1\u30CD\u30EB\u67A0\u7DDA \u975E\u8868\u793A"
1064
+ "borderHidden": "\u30D1\u30CD\u30EB\u67A0\u7DDA \u975E\u8868\u793A",
1065
+ "settings.title": "SubAgent Magazine \u2014 \u8A2D\u5B9A",
1066
+ "settings.section.general": "\u4E00\u822C",
1067
+ "settings.lang": "\u8A00\u8A9E",
1068
+ "settings.max": "\u6700\u5927\u8868\u793A\u6570",
1069
+ "settings.order": "\u4E26\u3073\u9806",
1070
+ "settings.scroll": "\u30B9\u30AF\u30ED\u30FC\u30EB",
1071
+ "settings.ttl": "\u4FDD\u6301\u671F\u9593",
1072
+ "settings.border": "\u30D1\u30CD\u30EB\u67A0",
1073
+ "settings.showEntryCost": "\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u6BCE\u306E\u8CBB\u7528",
1074
+ "settings.showEntryTime": "\u4E00\u89A7\u306B\u6240\u8981\u6642\u9593\u3092\u8868\u793A",
1075
+ "settings.showEntryTokens": "\u4E00\u89A7\u306B\u30C8\u30FC\u30AF\u30F3\u6570\u3092\u8868\u793A",
1076
+ "settings.timeFormat": "\u6642\u9593\u5F62\u5F0F",
1077
+ "settings.dbSync": "\u30ED\u30FC\u30AB\u30EBDB\u88DC\u5B8C",
1078
+ "settings.on": "\u30AA\u30F3",
1079
+ "settings.off": "\u30AA\u30D5",
1080
+ "settings.saved": "\u4FDD\u5B58\u3057\u307E\u3057\u305F {item}\uFF1A{value}",
1081
+ "settings.invalid": "\u7121\u52B9\u306A\u5024\u3067\u3059",
1082
+ "settings.close": "\u9589\u3058\u308B"
494
1083
  };
495
1084
  var KO_T = {
496
1085
  "panel.title": "\uC11C\uBE0C \uC5D0\uC774\uC804\uD2B8",
@@ -533,6 +1122,7 @@ var KO_T = {
533
1122
  "clear.prompt": "\uC774 \uC138\uC158\uC758 \uBAA8\uB4E0 \uC11C\uBE0C \uC5D0\uC774\uC804\uD2B8 \uAE30\uB85D\uC744 \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \uC774 \uC791\uC5C5\uC740 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
534
1123
  "clear.prompt_running": "\uC2E4\uD589 \uC911\uC778 \uC11C\uBE0C \uC5D0\uC774\uC804\uD2B8\uAC00 {n}\uAC1C \uC788\uC2B5\uB2C8\uB2E4. \uC0AD\uC81C\uD558\uBA74 \uBCF5\uAD6C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACC4\uC18D\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
535
1124
  "clear.done": "\uC11C\uBE0C \uC5D0\uC774\uC804\uD2B8 \uAE30\uB85D {n}\uAC1C\uB97C \uC0AD\uC81C\uD588\uC2B5\uB2C8\uB2E4",
1125
+ "clear.failed": "\uC9C0\uC6B0\uAE30\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
536
1126
  "clear.empty": "\uC774 \uC138\uC158\uC5D0 \uC11C\uBE0C \uC5D0\uC774\uC804\uD2B8 \uAE30\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4",
537
1127
  "cancel.no_session": "\uD558\uC704 \uC138\uC158 ID\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
538
1128
  "cancel.not_child": "\uB300\uC0C1\uC774 \uD558\uC704 \uC138\uC158\uC774 \uC544\uB2D9\uB2C8\uB2E4",
@@ -543,7 +1133,25 @@ var KO_T = {
543
1133
  "cancel.sent": "\uCDE8\uC18C \uC9C0\uC2DC\uB97C \uBCF4\uB0C8\uC2B5\uB2C8\uB2E4",
544
1134
  "cancel.failed": "\uCDE8\uC18C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4",
545
1135
  "borderShown": "\uD328\uB110 \uD14C\uB450\uB9AC \uD45C\uC2DC",
546
- "borderHidden": "\uD328\uB110 \uD14C\uB450\uB9AC \uC228\uAE40"
1136
+ "borderHidden": "\uD328\uB110 \uD14C\uB450\uB9AC \uC228\uAE40",
1137
+ "settings.title": "SubAgent Magazine \u2014 \uC124\uC815",
1138
+ "settings.section.general": "\uC77C\uBC18",
1139
+ "settings.lang": "\uC5B8\uC5B4",
1140
+ "settings.max": "\uCD5C\uB300 \uD56D\uBAA9 \uC218",
1141
+ "settings.order": "\uC815\uB82C \uC21C\uC11C",
1142
+ "settings.scroll": "\uC2A4\uD06C\uB864 \uBAA8\uB4DC",
1143
+ "settings.ttl": "\uBCF4\uC874 \uAE30\uAC04",
1144
+ "settings.border": "\uD328\uB110 \uD14C\uB450\uB9AC",
1145
+ "settings.showEntryCost": "\uD558\uC704 \uC5D0\uC774\uC804\uD2B8\uBCC4 \uBE44\uC6A9",
1146
+ "settings.showEntryTime": "\uBAA9\uB85D\uC5D0 \uC18C\uC694 \uC2DC\uAC04 \uD45C\uC2DC",
1147
+ "settings.showEntryTokens": "\uBAA9\uB85D\uC5D0 \uD1A0\uD070 \uC218 \uD45C\uC2DC",
1148
+ "settings.timeFormat": "\uC2DC\uAC04 \uD615\uC2DD",
1149
+ "settings.dbSync": "\uB85C\uCEEC DB \uBCF4\uC644",
1150
+ "settings.on": "\uCF2C",
1151
+ "settings.off": "\uB054",
1152
+ "settings.saved": "\uC800\uC7A5\uB428 {item}: {value}",
1153
+ "settings.invalid": "\uC798\uBABB\uB41C \uAC12",
1154
+ "settings.close": "\uB2EB\uAE30"
547
1155
  };
548
1156
  var LANGS = { zh: ZH_T, en: EN_T, ja: JA_T, ko: KO_T };
549
1157
  var LANG_META = [
@@ -579,11 +1187,292 @@ import { createSignal } from "solid-js";
579
1187
  var globalEntryCache = /* @__PURE__ */ new Map();
580
1188
  var [clearTick, setClearTick] = createSignal(0);
581
1189
 
1190
+ // src/panel/entry-map.ts
1191
+ function findSubEntryKey(entries, id, sessionId) {
1192
+ if (sessionId) {
1193
+ for (const [key, entry] of entries) {
1194
+ if (entry.sessionId === sessionId) return key;
1195
+ }
1196
+ }
1197
+ return entries.has(id) ? id : void 0;
1198
+ }
1199
+ function upsertSubEntry(entries, partial, now = Date.now()) {
1200
+ const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id;
1201
+ const existing = entries.get(key);
1202
+ const next = new Map(entries);
1203
+ if (key !== partial.id) next.delete(partial.id);
1204
+ const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled";
1205
+ next.set(key, {
1206
+ ...existing ?? { startedAt: now },
1207
+ ...partial,
1208
+ id: existing?.id ?? partial.id,
1209
+ startedAt: existing?.startedAt || partial.startedAt || now,
1210
+ endedAt: ended ? existing?.endedAt || now : void 0
1211
+ });
1212
+ return next;
1213
+ }
1214
+ function statusRank(status) {
1215
+ if (status === "running") return 0;
1216
+ if (status === "cancel_requested") return 1;
1217
+ return 2;
1218
+ }
1219
+ var isTerminalStatus = (status) => statusRank(status) === 2;
1220
+ function minDefined(a, b) {
1221
+ if (a === void 0) return b;
1222
+ if (b === void 0) return a;
1223
+ return Math.min(a, b);
1224
+ }
1225
+ function maxDefined(a, b) {
1226
+ if (a === void 0) return b;
1227
+ if (b === void 0) return a;
1228
+ return Math.max(a, b);
1229
+ }
1230
+ function mergeSubEntry(base, incoming) {
1231
+ const successor = statusRank(incoming.status) > statusRank(base.status) ? incoming : base;
1232
+ const status = successor.status;
1233
+ const ended = isTerminalStatus(status);
1234
+ return {
1235
+ ...base,
1236
+ ...incoming,
1237
+ id: base.id,
1238
+ status,
1239
+ sessionId: incoming.sessionId ?? base.sessionId,
1240
+ startedAt: minDefined(base.startedAt, incoming.startedAt) ?? base.startedAt,
1241
+ endedAt: ended ? base.endedAt ?? incoming.endedAt ?? Date.now() : void 0,
1242
+ tokens: maxDefined(base.tokens, incoming.tokens),
1243
+ cost: maxDefined(base.cost, incoming.cost),
1244
+ model: incoming.model ?? base.model,
1245
+ todoTotal: maxDefined(base.todoTotal, incoming.todoTotal),
1246
+ todoDone: maxDefined(base.todoDone, incoming.todoDone),
1247
+ error: incoming.error ?? base.error,
1248
+ cancelRequestedAt: minDefined(base.cancelRequestedAt, incoming.cancelRequestedAt),
1249
+ abortAccepted: base.abortAccepted || incoming.abortAccepted || void 0,
1250
+ cancelReason: incoming.cancelReason ?? base.cancelReason
1251
+ };
1252
+ }
1253
+ function mergeSubEntries(base, incoming) {
1254
+ const out = /* @__PURE__ */ new Map();
1255
+ for (const entry of base) out.set(entry.id, entry);
1256
+ for (const entry of incoming) {
1257
+ const key = findSubEntryKey(out, entry.id, entry.sessionId);
1258
+ if (!key) {
1259
+ out.set(entry.id, entry);
1260
+ continue;
1261
+ }
1262
+ out.set(key, mergeSubEntry(out.get(key), entry));
1263
+ }
1264
+ return out;
1265
+ }
1266
+
1267
+ // src/v2/settings-menu.ts
1268
+ var TTL_VALUES = [3, 7, 14, 30, 0];
1269
+ var nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
1270
+ function openSettingsMenu(context, api, signals) {
1271
+ runSettingsMenu(context, api, signals).catch((err) => {
1272
+ try {
1273
+ context.ui.toast.show({ message: String(err), variant: "error" });
1274
+ } catch {
1275
+ }
1276
+ });
1277
+ }
1278
+ async function runSettingsMenu(context, api, signals) {
1279
+ const t = createT(() => signals.lang());
1280
+ const kv = api.kv;
1281
+ const onOff = (v) => v ? t("settings.on") : t("settings.off");
1282
+ const langLabel = () => LANG_META.find((m) => m.code === signals.lang())?.label ?? signals.lang();
1283
+ const ttlTitle = (d) => d === 0 ? t("ttl.unlimited") : t(d === 3 ? "ttl.3d" : d === 7 ? "ttl.7d" : d === 14 ? "ttl.14d" : "ttl.30d");
1284
+ const ttlLabel = () => ttlTitle(readTTLDays(kv));
1285
+ const orderLabel = () => signals.sortOrder() === "desc" ? t("order.desc") : t("order.asc");
1286
+ const scrollLabel = () => signals.scrollMode() === "wheel" ? t("scroll.wheel") : t("scroll.click");
1287
+ const toast = (item, value) => context.ui.toast.show({ message: t("settings.saved", { item, value }) });
1288
+ while (true) {
1289
+ const category = t("settings.section.general");
1290
+ const choice = await context.ui.dialog.select({
1291
+ title: t("settings.title"),
1292
+ options: [
1293
+ { title: t("settings.lang"), value: "lang", description: langLabel(), category },
1294
+ { title: t("settings.max"), value: "max", description: String(signals.maxEntries()), category },
1295
+ { title: t("settings.order"), value: "order", description: orderLabel(), category },
1296
+ { title: t("settings.scroll"), value: "scroll", description: scrollLabel(), category },
1297
+ { title: t("settings.ttl"), value: "ttl", description: ttlLabel(), category },
1298
+ { title: t("settings.border"), value: "border", description: onOff(signals.borderVisible()), category },
1299
+ { title: t("settings.showEntryCost"), value: "cost", description: onOff(signals.showEntryCost()), category },
1300
+ { title: t("settings.showEntryTime"), value: "time", description: onOff(signals.showEntryTime()), category },
1301
+ { title: t("settings.timeFormat"), value: "timefmt", description: TIME_FORMAT_SAMPLES[signals.timeFormat()], category },
1302
+ { title: t("settings.showEntryTokens"), value: "tokens", description: onOff(signals.showEntryTokens()), category },
1303
+ { title: t("settings.dbSync"), value: "dbsync", description: onOff(signals.dbSync()), category },
1304
+ { title: t("settings.close"), value: "close" }
1305
+ ]
1306
+ });
1307
+ if (choice === void 0 || choice === "close") return;
1308
+ if (choice === "lang") {
1309
+ const picked = await context.ui.dialog.select({
1310
+ title: t("settings.lang"),
1311
+ options: LANG_META.map((m) => ({ title: m.label, value: m.code })),
1312
+ current: signals.lang()
1313
+ });
1314
+ if (picked) {
1315
+ signals.setLang(picked);
1316
+ kv.set(SETTING_KEYS.lang, picked);
1317
+ toast(t("settings.lang"), LANG_META.find((m) => m.code === picked)?.label ?? picked);
1318
+ }
1319
+ }
1320
+ if (choice === "max") {
1321
+ const val = await context.ui.dialog.prompt({
1322
+ title: t("settings.max"),
1323
+ message: "1\u201350",
1324
+ placeholder: String(signals.maxEntries())
1325
+ });
1326
+ if (val !== void 0) {
1327
+ const n = parseInt(val.trim(), 10);
1328
+ if (Number.isNaN(n)) {
1329
+ context.ui.toast.show({ message: t("settings.invalid"), variant: "warning" });
1330
+ } else {
1331
+ const clamped = Math.max(1, Math.min(50, n));
1332
+ signals.setMaxEntries(clamped);
1333
+ kv.set(SETTING_KEYS.maxEntries, clamped);
1334
+ toast(t("settings.max"), String(clamped));
1335
+ }
1336
+ }
1337
+ }
1338
+ if (choice === "order") {
1339
+ const picked = await context.ui.dialog.select({
1340
+ title: t("settings.order"),
1341
+ options: [
1342
+ { title: t("order.desc"), value: "desc" },
1343
+ { title: t("order.asc"), value: "asc" }
1344
+ ],
1345
+ current: signals.sortOrder()
1346
+ });
1347
+ if (picked) {
1348
+ signals.setSortOrder(picked);
1349
+ kv.set(SETTING_KEYS.order, picked);
1350
+ toast(t("settings.order"), picked === "desc" ? t("order.desc") : t("order.asc"));
1351
+ }
1352
+ }
1353
+ if (choice === "scroll") {
1354
+ const picked = await context.ui.dialog.select({
1355
+ title: t("settings.scroll"),
1356
+ options: [
1357
+ { title: t("scroll.wheel"), value: "wheel" },
1358
+ { title: t("scroll.click"), value: "click" }
1359
+ ],
1360
+ current: signals.scrollMode()
1361
+ });
1362
+ if (picked) {
1363
+ signals.setScrollMode(picked);
1364
+ kv.set(SETTING_KEYS.scrollMode, picked);
1365
+ toast(t("settings.scroll"), picked === "wheel" ? t("scroll.wheel") : t("scroll.click"));
1366
+ }
1367
+ }
1368
+ if (choice === "ttl") {
1369
+ const picked = await context.ui.dialog.select({
1370
+ title: t("settings.ttl"),
1371
+ options: TTL_VALUES.map((d) => ({ title: ttlTitle(d), value: d })),
1372
+ current: readTTLDays(kv)
1373
+ });
1374
+ if (picked !== void 0) {
1375
+ kv.set(SETTING_KEYS.ttlDays, String(picked));
1376
+ toast(t("settings.ttl"), ttlTitle(picked));
1377
+ }
1378
+ }
1379
+ if (choice === "border") {
1380
+ const picked = await context.ui.dialog.select({
1381
+ title: t("settings.border"),
1382
+ options: [
1383
+ { title: t("settings.on"), value: true },
1384
+ { title: t("settings.off"), value: false }
1385
+ ],
1386
+ current: signals.borderVisible()
1387
+ });
1388
+ if (picked !== void 0) {
1389
+ signals.setBorderVisible(picked);
1390
+ kv.set(SETTING_KEYS.border, picked);
1391
+ toast(t("settings.border"), onOff(picked));
1392
+ }
1393
+ }
1394
+ if (choice === "cost") {
1395
+ const picked = await context.ui.dialog.select({
1396
+ title: t("settings.showEntryCost"),
1397
+ options: [
1398
+ { title: t("settings.on"), value: true },
1399
+ { title: t("settings.off"), value: false }
1400
+ ],
1401
+ current: signals.showEntryCost()
1402
+ });
1403
+ if (picked !== void 0) {
1404
+ signals.setShowEntryCost(picked);
1405
+ kv.set(SETTING_KEYS.showEntryCost, picked);
1406
+ toast(t("settings.showEntryCost"), onOff(picked));
1407
+ }
1408
+ }
1409
+ if (choice === "time") {
1410
+ const picked = await context.ui.dialog.select({
1411
+ title: t("settings.showEntryTime"),
1412
+ options: [
1413
+ { title: t("settings.on"), value: true },
1414
+ { title: t("settings.off"), value: false }
1415
+ ],
1416
+ current: signals.showEntryTime()
1417
+ });
1418
+ if (picked !== void 0) {
1419
+ signals.setShowEntryTime(picked);
1420
+ kv.set(SETTING_KEYS.showEntryTime, picked);
1421
+ toast(t("settings.showEntryTime"), onOff(picked));
1422
+ }
1423
+ }
1424
+ if (choice === "timefmt") {
1425
+ const picked = await context.ui.dialog.select({
1426
+ title: t("settings.timeFormat"),
1427
+ options: TIME_FORMATS.map((f) => ({ title: `${f} \u2014 ${TIME_FORMAT_SAMPLES[f]}`, value: f })),
1428
+ current: signals.timeFormat()
1429
+ });
1430
+ if (picked) {
1431
+ signals.setTimeFormat(picked);
1432
+ kv.set(SETTING_KEYS.timeFormat, picked);
1433
+ toast(t("settings.timeFormat"), TIME_FORMAT_SAMPLES[picked]);
1434
+ }
1435
+ }
1436
+ if (choice === "tokens") {
1437
+ const picked = await context.ui.dialog.select({
1438
+ title: t("settings.showEntryTokens"),
1439
+ options: [
1440
+ { title: t("settings.on"), value: true },
1441
+ { title: t("settings.off"), value: false }
1442
+ ],
1443
+ current: signals.showEntryTokens()
1444
+ });
1445
+ if (picked !== void 0) {
1446
+ signals.setShowEntryTokens(picked);
1447
+ kv.set(SETTING_KEYS.showEntryTokens, picked);
1448
+ toast(t("settings.showEntryTokens"), onOff(picked));
1449
+ }
1450
+ }
1451
+ if (choice === "dbsync") {
1452
+ const picked = await context.ui.dialog.select({
1453
+ title: t("settings.dbSync"),
1454
+ options: [
1455
+ { title: t("settings.on"), value: true },
1456
+ { title: t("settings.off"), value: false }
1457
+ ],
1458
+ current: signals.dbSync()
1459
+ });
1460
+ if (picked !== void 0) {
1461
+ signals.setDbSync(picked);
1462
+ kv.set(SETTING_KEYS.dbSync, picked);
1463
+ toast(t("settings.dbSync"), onOff(picked));
1464
+ }
1465
+ }
1466
+ await nextTick();
1467
+ }
1468
+ }
1469
+
582
1470
  // src/v2/commands.ts
583
1471
  function makeCommands(context, api, signals) {
584
1472
  const t = createT(() => signals.lang());
585
1473
  const kv = api.kv;
586
1474
  const clampMax = (n) => Math.max(1, Math.min(50, n));
1475
+ const onOff = (v) => v ? t("settings.on") : t("settings.off");
587
1476
  const resolveParent = (sid) => {
588
1477
  try {
589
1478
  const session = api.session.get(sid);
@@ -670,6 +1559,111 @@ function makeCommands(context, api, signals) {
670
1559
  api.ui.toast(`Max entries: ${n}`);
671
1560
  }
672
1561
  },
1562
+ {
1563
+ id: "opencode-subagent-magazine.subagent.config",
1564
+ title: "SubAgent Magazine: Settings",
1565
+ description: "Show current settings (change each via its own command)",
1566
+ slash: { name: "subagent-config" },
1567
+ palette: true,
1568
+ run: () => {
1569
+ const ttl = readTTLDays(kv);
1570
+ const langLabel = LANG_META.find((m) => m.code === signals.lang())?.label ?? signals.lang();
1571
+ const message = [
1572
+ `${t("settings.lang")}: ${langLabel}`,
1573
+ `${t("settings.max")}: ${signals.maxEntries()}`,
1574
+ `${t("settings.order")}: ${signals.sortOrder() === "desc" ? t("order.desc") : t("order.asc")}`,
1575
+ `${t("settings.scroll")}: ${signals.scrollMode() === "wheel" ? t("scroll.wheel") : t("scroll.click")}`,
1576
+ `${t("settings.ttl")}: ${ttl === 0 ? t("ttl.unlimited") : `${ttl}d`}`,
1577
+ `${t("settings.border")}: ${onOff(signals.borderVisible())}`,
1578
+ `${t("settings.showEntryCost")}: ${onOff(signals.showEntryCost())}`,
1579
+ `${t("settings.showEntryTime")}: ${onOff(signals.showEntryTime())}`,
1580
+ `${t("settings.showEntryTokens")}: ${onOff(signals.showEntryTokens())}`,
1581
+ `${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[signals.timeFormat()]}`,
1582
+ `${t("settings.dbSync")}: ${onOff(signals.dbSync())}`
1583
+ ].join("\n");
1584
+ context.ui.toast.show({ title: t("settings.title"), message });
1585
+ }
1586
+ },
1587
+ {
1588
+ id: "opencode-subagent-magazine.subagent.sections",
1589
+ title: "SubAgent Magazine: Settings Menu",
1590
+ description: "Open the interactive settings menu (Esc to close)",
1591
+ slash: { name: "subagent-sections" },
1592
+ palette: true,
1593
+ run: () => {
1594
+ openSettingsMenu(context, api, signals);
1595
+ }
1596
+ },
1597
+ {
1598
+ id: "opencode-subagent-magazine.subagent.cost",
1599
+ title: "SubAgent Magazine: Toggle Entry Cost",
1600
+ description: "Show or hide the per-sub-agent cost in the sidebar",
1601
+ slash: { name: "subagent-cost" },
1602
+ palette: true,
1603
+ run: () => {
1604
+ const v = !signals.showEntryCost();
1605
+ signals.setShowEntryCost(v);
1606
+ kv.set(SETTING_KEYS.showEntryCost, v);
1607
+ api.ui.toast(`${t("settings.showEntryCost")}: ${onOff(v)}`);
1608
+ }
1609
+ },
1610
+ {
1611
+ id: "opencode-subagent-magazine.subagent.time",
1612
+ title: "SubAgent Magazine: Toggle Entry Time",
1613
+ description: "Show or hide the elapsed time in the sidebar list",
1614
+ slash: { name: "subagent-time" },
1615
+ palette: true,
1616
+ run: () => {
1617
+ const v = !signals.showEntryTime();
1618
+ signals.setShowEntryTime(v);
1619
+ kv.set(SETTING_KEYS.showEntryTime, v);
1620
+ api.ui.toast(`${t("settings.showEntryTime")}: ${onOff(v)}`);
1621
+ }
1622
+ },
1623
+ {
1624
+ id: "opencode-subagent-magazine.subagent.time-format",
1625
+ title: "SubAgent Magazine: Time Format",
1626
+ description: "Set how elapsed time is displayed (short / decimal / clock / compact / seconds)",
1627
+ slash: { name: "subagent-time-format" },
1628
+ palette: true,
1629
+ run: async () => {
1630
+ const picked = await context.ui.dialog.select({
1631
+ title: t("settings.timeFormat"),
1632
+ options: TIME_FORMATS.map((f) => ({ title: `${f} \u2014 ${TIME_FORMAT_SAMPLES[f]}`, value: f })),
1633
+ current: signals.timeFormat()
1634
+ });
1635
+ if (!picked) return;
1636
+ signals.setTimeFormat(picked);
1637
+ kv.set(SETTING_KEYS.timeFormat, picked);
1638
+ api.ui.toast(`${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[picked]}`);
1639
+ }
1640
+ },
1641
+ {
1642
+ id: "opencode-subagent-magazine.subagent.tokens",
1643
+ title: "SubAgent Magazine: Toggle Entry Tokens",
1644
+ description: "Show or hide the token count in the sidebar list",
1645
+ slash: { name: "subagent-tokens" },
1646
+ palette: true,
1647
+ run: () => {
1648
+ const v = !signals.showEntryTokens();
1649
+ signals.setShowEntryTokens(v);
1650
+ kv.set(SETTING_KEYS.showEntryTokens, v);
1651
+ api.ui.toast(`${t("settings.showEntryTokens")}: ${onOff(v)}`);
1652
+ }
1653
+ },
1654
+ {
1655
+ id: "opencode-subagent-magazine.subagent.db",
1656
+ title: "SubAgent Magazine: Toggle Local DB",
1657
+ description: "Enrich sub-agent entries from the local OpenCode database (V2)",
1658
+ slash: { name: "subagent-db" },
1659
+ palette: true,
1660
+ run: () => {
1661
+ const v = !signals.dbSync();
1662
+ signals.setDbSync(v);
1663
+ kv.set(SETTING_KEYS.dbSync, v);
1664
+ api.ui.toast(`${t("settings.dbSync")}: ${onOff(v)}`);
1665
+ }
1666
+ },
673
1667
  {
674
1668
  id: "opencode-subagent-magazine.subagent.version",
675
1669
  title: "SubAgent Magazine: Version",
@@ -712,27 +1706,43 @@ function makeCommands(context, api, signals) {
712
1706
  }
713
1707
  }
714
1708
  if (count > 0) {
1709
+ const ok = () => api.ui.toast(signals.lang() === "zh" ? `\u5DF2\u6807\u8BB0 ${count} \u4E2A\u8FD0\u884C\u4E2D\u7684\u6761\u76EE\u4E3A\u5B8C\u6210` : `Marked ${count} running entries as done`);
1710
+ const fail = () => {
1711
+ try {
1712
+ api.ui.toast(t("clear.failed"));
1713
+ } catch {
1714
+ }
1715
+ };
715
1716
  try {
716
- const data = loadSessionData(kv);
717
1717
  const { parentSid, isChild } = resolveParent(sid);
718
- if (isChild) {
719
- if (!data[parentSid]) data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
720
- if (!data[parentSid].children) data[parentSid].children = {};
721
- if (!data[parentSid].children[sid]) data[parentSid].children[sid] = { scroll: 0, expanded: "", entries: [] };
722
- data[parentSid].children[sid] = { ...data[parentSid].children[sid], entries: [...entries.values()] };
1718
+ const done = updateSessionData(kv, (data) => {
1719
+ if (isChild) {
1720
+ if (!data[parentSid]) data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
1721
+ if (!data[parentSid].children) data[parentSid].children = {};
1722
+ const prev = data[parentSid].children[sid]?.entries ?? [];
1723
+ const merged = [...mergeSubEntries(prev, entries.values()).values()].map(
1724
+ (e) => e.status === "running" || e.status === "cancel_requested" ? { ...e, status: "done", endedAt: e.endedAt ?? Date.now() } : e
1725
+ );
1726
+ data[parentSid].children[sid] = {
1727
+ ...data[parentSid].children[sid] ?? { scroll: 0, expanded: "" },
1728
+ entries: merged
1729
+ };
1730
+ } else {
1731
+ const rec = data[parentSid] ?? { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
1732
+ const merged = [...mergeSubEntries(rec.entries ?? [], entries.values()).values()].map(
1733
+ (e) => e.status === "running" || e.status === "cancel_requested" ? { ...e, status: "done", endedAt: e.endedAt ?? Date.now() } : e
1734
+ );
1735
+ data[parentSid] = { ...rec, ts: Date.now(), entries: merged, children: rec.children ?? {} };
1736
+ }
1737
+ });
1738
+ if (done && typeof done.then === "function") {
1739
+ void done.then(ok, fail);
723
1740
  } else {
724
- data[sid] = {
725
- ts: Date.now(),
726
- entries: [...entries.values()],
727
- scroll: data[sid]?.scroll ?? 0,
728
- expanded: data[sid]?.expanded ?? "",
729
- children: data[sid]?.children ?? {}
730
- };
1741
+ ok();
731
1742
  }
732
- saveSessionData(kv, data);
733
1743
  } catch {
1744
+ fail();
734
1745
  }
735
- api.ui.toast(signals.lang() === "zh" ? `\u5DF2\u6807\u8BB0 ${count} \u4E2A\u8FD0\u884C\u4E2D\u7684\u6761\u76EE\u4E3A\u5B8C\u6210` : `Marked ${count} running entries as done`);
736
1746
  } else {
737
1747
  api.ui.toast(signals.lang() === "zh" ? "\u6CA1\u6709\u9700\u8981\u6E05\u7406\u7684\u8FD0\u884C\u4E2D\u6761\u76EE" : "No running entries to clear");
738
1748
  }
@@ -789,32 +1799,45 @@ function makeCommands(context, api, signals) {
789
1799
  });
790
1800
  if (choice !== "yes") return;
791
1801
  try {
792
- const data = loadSessionData(kv);
793
1802
  let count = 0;
794
- if (parentID) {
795
- if (data[parentID]?.children?.[sid]) {
796
- const child = data[parentID].children[sid];
797
- const ids = child.entries?.map((e) => e.id) ?? [];
798
- count = ids.length;
799
- child.entries = [];
800
- child.scroll = 0;
801
- child.expanded = "";
802
- child.clearedIds = [.../* @__PURE__ */ new Set([...child.clearedIds ?? [], ...ids])];
1803
+ const finish = () => {
1804
+ globalEntryCache.delete(sid);
1805
+ setClearTick((v) => v + 1);
1806
+ api.ui.toast(t("clear.done", { n: count }));
1807
+ };
1808
+ const done = updateSessionData(kv, (data) => {
1809
+ if (parentID) {
1810
+ const child = data[parentID]?.children?.[sid];
1811
+ if (child) {
1812
+ const ids = child.entries?.map((e) => e.id) ?? [];
1813
+ count = ids.length;
1814
+ child.entries = [];
1815
+ child.scroll = 0;
1816
+ child.expanded = "";
1817
+ child.clearedIds = [.../* @__PURE__ */ new Set([...child.clearedIds ?? [], ...ids])];
1818
+ }
1819
+ } else {
1820
+ const rec = data[sid];
1821
+ if (rec) {
1822
+ const ids = rec.entries?.map((e) => e.id) ?? [];
1823
+ count = ids.length;
1824
+ rec.entries = [];
1825
+ rec.scroll = 0;
1826
+ rec.expanded = "";
1827
+ rec.clearedIds = [.../* @__PURE__ */ new Set([...rec.clearedIds ?? [], ...ids])];
1828
+ }
803
1829
  }
1830
+ });
1831
+ if (done && typeof done.then === "function") {
1832
+ void done.then(finish, () => {
1833
+ try {
1834
+ api.ui.toast(t("clear.failed"));
1835
+ } catch {
1836
+ }
1837
+ });
804
1838
  } else {
805
- count = data[sid]?.entries?.length ?? 0;
806
- if (data[sid]) {
807
- const ids = data[sid].entries?.map((e) => e.id) ?? [];
808
- data[sid].entries = [];
809
- data[sid].scroll = 0;
810
- data[sid].expanded = "";
811
- data[sid].clearedIds = [.../* @__PURE__ */ new Set([...data[sid].clearedIds ?? [], ...ids])];
812
- }
1839
+ finish();
813
1840
  }
814
- saveSessionData(kv, data);
815
- globalEntryCache.delete(sid);
816
- setClearTick((v) => v + 1);
817
- api.ui.toast(t("clear.done", { n: count }));
818
1841
  } catch {
819
1842
  }
820
1843
  }
@@ -961,53 +1984,6 @@ async function copyText(text) {
961
1984
  // src/core/types.ts
962
1985
  var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["task", "subagent", "delegate", "call_omo_agent"]);
963
1986
 
964
- // src/core/format.ts
965
- function charColumns(c) {
966
- const code = c.codePointAt(0) ?? 0;
967
- if (code < 32) return 0;
968
- if (code < 127) return 1;
969
- if (code < 160) return 0;
970
- if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 128591 || code >= 131072 && code <= 262141)
971
- return 2;
972
- return 1;
973
- }
974
- function visualWidth(s) {
975
- let w = 0;
976
- for (const c of s) w += charColumns(c);
977
- return w;
978
- }
979
- function truncate(text, maxCols) {
980
- if (visualWidth(text) <= maxCols) return text;
981
- let cols = 0;
982
- let i = 0;
983
- for (const c of text) {
984
- const w = charColumns(c);
985
- if (cols + w > maxCols - 1) break;
986
- cols += w;
987
- i += c.length;
988
- }
989
- return text.slice(0, i) + "\u2026";
990
- }
991
- function fmtDurationShort(ms, running) {
992
- if (running && ms < 2e3) return "";
993
- if (ms < 1e3) return (ms / 1e3).toFixed(2) + "s";
994
- if (ms < 6e4) return (ms / 1e3).toFixed(2) + "s";
995
- const m = Math.floor(ms / 6e4);
996
- const s = Math.round(ms % 6e4 / 1e3);
997
- return `${m}m${s}s`;
998
- }
999
- function fmtTokens(n) {
1000
- if (n < 1e3) return `${n}`;
1001
- if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
1002
- return `${(n / 1e6).toFixed(1)}M`;
1003
- }
1004
- function safeErrorMsg(err) {
1005
- if (!err) return "";
1006
- if (typeof err === "string") return err;
1007
- if (typeof err === "object") return String(err.message || err.code || "");
1008
- return "";
1009
- }
1010
-
1011
1987
  // src/core/color.ts
1012
1988
  function rgb(raw) {
1013
1989
  if (typeof raw === "string" && raw.startsWith("#")) {
@@ -1063,52 +2039,38 @@ function dimColor(hex, factor = 0.5) {
1063
2039
  const r = Math.round(c.r * factor);
1064
2040
  const g = Math.round(c.g * factor);
1065
2041
  const b = Math.round(c.b * factor);
1066
- return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
1067
- }
1068
- var FALLBACK = {
1069
- primary: "#8B9DAF",
1070
- text: "#C5C5BB",
1071
- muted: "#7A7A72",
1072
- success: "#9CAF8B",
1073
- warning: "#C5B88D",
1074
- error: "#B08A8A",
1075
- border: "#6B6B63"
1076
- };
1077
- var MAX_SAT = 0.28;
1078
-
1079
- // src/panel/session-routing.ts
1080
- function isDirectChildSession(currentSessionId, eventSessionId, eventSession) {
1081
- return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId;
1082
- }
1083
-
1084
- // src/panel/entry-map.ts
1085
- function findSubEntryKey(entries, id, sessionId) {
1086
- if (sessionId) {
1087
- for (const [key, entry] of entries) {
1088
- if (entry.sessionId === sessionId) return key;
1089
- }
1090
- }
1091
- return entries.has(id) ? id : void 0;
1092
- }
1093
- function upsertSubEntry(entries, partial, now = Date.now()) {
1094
- const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id;
1095
- const existing = entries.get(key);
1096
- const next = new Map(entries);
1097
- if (key !== partial.id) next.delete(partial.id);
1098
- const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled";
1099
- next.set(key, {
1100
- ...existing ?? { startedAt: now },
1101
- ...partial,
1102
- id: existing?.id ?? partial.id,
1103
- startedAt: existing?.startedAt || partial.startedAt || now,
1104
- endedAt: ended ? existing?.endedAt || now : void 0
1105
- });
1106
- return next;
2042
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
2043
+ }
2044
+ var FALLBACK = {
2045
+ primary: "#8B9DAF",
2046
+ text: "#C5C5BB",
2047
+ muted: "#7A7A72",
2048
+ success: "#9CAF8B",
2049
+ warning: "#C5B88D",
2050
+ error: "#B08A8A",
2051
+ border: "#6B6B63"
2052
+ };
2053
+ var MAX_SAT = 0.28;
2054
+
2055
+ // src/panel/session-routing.ts
2056
+ function isDirectChildSession(currentSessionId, eventSessionId, eventSession) {
2057
+ return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId;
1107
2058
  }
1108
2059
 
1109
2060
  // src/panel/SubAgentPanel.tsx
1110
2061
  var LEFT_PAD = 4;
1111
2062
  var INDENT = 2;
2063
+ function partMetadata(part, st) {
2064
+ const partMeta = part?.metadata;
2065
+ if (partMeta && Object.keys(partMeta).length > 0) return partMeta;
2066
+ const stateMeta = st?.metadata;
2067
+ return stateMeta && Object.keys(stateMeta).length > 0 ? stateMeta : void 0;
2068
+ }
2069
+ function validModel(model) {
2070
+ if (typeof model !== "string") return false;
2071
+ const s = model.trim();
2072
+ return s.length > 0 && s !== "[object Object]" && !s.startsWith("{");
2073
+ }
1112
2074
  function SubAgentPanel(props) {
1113
2075
  const t = createT(() => props.lang());
1114
2076
  const SESSION_DATA_KEY2 = `${KV_PREFIX}.session_data`;
@@ -1123,11 +2085,32 @@ function SubAgentPanel(props) {
1123
2085
  return {};
1124
2086
  }
1125
2087
  };
1126
- const saveSessionData2 = (data) => {
1127
- try {
1128
- props.api.kv.set(SESSION_DATA_KEY2, JSON.stringify(data));
1129
- } catch {
1130
- }
2088
+ const mutateSessionData = (mutator) => {
2089
+ void Promise.resolve(updateSessionData(props.api.kv, mutator)).catch(() => {
2090
+ });
2091
+ };
2092
+ const ensureRootRecord = (data, sid) => {
2093
+ const rec = data[sid];
2094
+ if (rec) return rec;
2095
+ const created = {
2096
+ ts: Date.now(),
2097
+ entries: [],
2098
+ scroll: 0,
2099
+ expanded: "",
2100
+ children: {}
2101
+ };
2102
+ data[sid] = created;
2103
+ return created;
2104
+ };
2105
+ const ensureChildRecord = (data, parentSid, sid) => {
2106
+ const parent = ensureRootRecord(data, parentSid);
2107
+ if (!parent.children) parent.children = {};
2108
+ if (!parent.children[sid]) parent.children[sid] = {
2109
+ scroll: 0,
2110
+ expanded: "",
2111
+ entries: []
2112
+ };
2113
+ return parent.children[sid];
1131
2114
  };
1132
2115
  const resolveParent = (sid) => {
1133
2116
  try {
@@ -1169,129 +2152,71 @@ function SubAgentPanel(props) {
1169
2152
  clearTimeout(persistTimer);
1170
2153
  persistTimer = setTimeout(() => {
1171
2154
  try {
1172
- const data = loadSessionData2();
2155
+ mutateSessionData((data) => {
2156
+ const {
2157
+ parentSid,
2158
+ isChild
2159
+ } = resolveParent(sid);
2160
+ if (isChild) {
2161
+ const parent = ensureRootRecord(data, parentSid);
2162
+ const child = ensureChildRecord(data, parentSid, sid);
2163
+ child.entries = [...mergeSubEntries(child.entries ?? [], entries2.values()).values()];
2164
+ parent.ts = Date.now();
2165
+ } else {
2166
+ const rec = ensureRootRecord(data, sid);
2167
+ rec.ts = Date.now();
2168
+ rec.entries = [...mergeSubEntries(rec.entries ?? [], entries2.values()).values()];
2169
+ }
2170
+ });
2171
+ } catch {
2172
+ }
2173
+ }, 200);
2174
+ };
2175
+ const persistScroll = (sid, scroll) => {
2176
+ try {
2177
+ mutateSessionData((data) => {
1173
2178
  const {
1174
2179
  parentSid,
1175
2180
  isChild
1176
2181
  } = resolveParent(sid);
1177
2182
  if (isChild) {
1178
- if (!data[parentSid]) data[parentSid] = {
1179
- ts: Date.now(),
1180
- entries: [],
1181
- scroll: 0,
1182
- expanded: "",
1183
- children: {}
1184
- };
1185
- if (!data[parentSid].children) data[parentSid].children = {};
1186
- if (!data[parentSid].children[sid]) data[parentSid].children[sid] = {
1187
- scroll: 0,
1188
- expanded: "",
1189
- entries: []
1190
- };
1191
- data[parentSid].children[sid] = {
1192
- ...data[parentSid].children[sid],
1193
- entries: [...entries2.values()]
1194
- };
2183
+ ensureChildRecord(data, parentSid, sid).scroll = scroll;
1195
2184
  } else {
1196
- data[sid] = {
1197
- ...data[sid],
1198
- ts: Date.now(),
1199
- entries: [...entries2.values()],
1200
- children: data[sid]?.children ?? {}
1201
- };
2185
+ const rec = ensureRootRecord(data, sid);
2186
+ rec.ts = Date.now();
2187
+ rec.scroll = scroll;
1202
2188
  }
1203
- saveSessionData2(data);
1204
- } catch {
1205
- }
1206
- }, 200);
1207
- };
1208
- const persistScroll = (sid, scroll) => {
1209
- try {
1210
- const data = loadSessionData2();
1211
- const {
1212
- parentSid,
1213
- isChild
1214
- } = resolveParent(sid);
1215
- if (isChild) {
1216
- if (!data[parentSid]) data[parentSid] = {
1217
- ts: Date.now(),
1218
- entries: [],
1219
- scroll: 0,
1220
- expanded: "",
1221
- children: {}
1222
- };
1223
- if (!data[parentSid].children) data[parentSid].children = {};
1224
- if (!data[parentSid].children[sid]) data[parentSid].children[sid] = {
1225
- scroll: 0,
1226
- expanded: "",
1227
- entries: []
1228
- };
1229
- data[parentSid].children[sid] = {
1230
- ...data[parentSid].children[sid],
1231
- scroll
1232
- };
1233
- } else {
1234
- data[sid] = {
1235
- ...data[sid],
1236
- ts: Date.now(),
1237
- scroll,
1238
- children: data[sid]?.children ?? {}
1239
- };
1240
- }
1241
- saveSessionData2(data);
2189
+ });
1242
2190
  } catch {
1243
2191
  }
1244
2192
  };
1245
2193
  const persistExpanded = (sid, expanded2) => {
1246
2194
  try {
1247
- const data = loadSessionData2();
1248
- const {
1249
- parentSid,
1250
- isChild
1251
- } = resolveParent(sid);
1252
- if (isChild) {
1253
- if (!data[parentSid]) data[parentSid] = {
1254
- ts: Date.now(),
1255
- entries: [],
1256
- scroll: 0,
1257
- expanded: "",
1258
- children: {}
1259
- };
1260
- if (!data[parentSid].children) data[parentSid].children = {};
1261
- if (!data[parentSid].children[sid]) data[parentSid].children[sid] = {
1262
- scroll: 0,
1263
- expanded: "",
1264
- entries: []
1265
- };
1266
- data[parentSid].children[sid] = {
1267
- ...data[parentSid].children[sid],
1268
- expanded: expanded2
1269
- };
1270
- } else {
1271
- data[sid] = {
1272
- ...data[sid],
1273
- ts: Date.now(),
1274
- expanded: expanded2,
1275
- children: data[sid]?.children ?? {}
1276
- };
1277
- }
1278
- saveSessionData2(data);
2195
+ mutateSessionData((data) => {
2196
+ const {
2197
+ parentSid,
2198
+ isChild
2199
+ } = resolveParent(sid);
2200
+ if (isChild) {
2201
+ ensureChildRecord(data, parentSid, sid).expanded = expanded2;
2202
+ } else {
2203
+ const rec = ensureRootRecord(data, sid);
2204
+ rec.ts = Date.now();
2205
+ rec.expanded = expanded2;
2206
+ }
2207
+ });
1279
2208
  } catch {
1280
2209
  }
1281
2210
  };
1282
2211
  const cleanupOldSessions = () => {
1283
2212
  if (ttlDays <= 0) return;
1284
2213
  try {
1285
- const data = loadSessionData2();
1286
2214
  const cutoff = Date.now() - TTL_MS;
1287
- let changed = false;
1288
- for (const sid of Object.keys(data)) {
1289
- if (data[sid].ts < cutoff) {
1290
- delete data[sid];
1291
- changed = true;
2215
+ mutateSessionData((data) => {
2216
+ for (const sid of Object.keys(data)) {
2217
+ if (data[sid].ts < cutoff) delete data[sid];
1292
2218
  }
1293
- }
1294
- if (changed) saveSessionData2(data);
2219
+ });
1295
2220
  } catch {
1296
2221
  }
1297
2222
  };
@@ -1311,38 +2236,22 @@ function SubAgentPanel(props) {
1311
2236
  if (needsImmediateFlush) {
1312
2237
  clearTimeout(persistTimer);
1313
2238
  try {
1314
- const data = loadSessionData2();
1315
- const {
1316
- parentSid,
1317
- isChild
1318
- } = resolveParent(props.sessionId);
1319
- if (isChild) {
1320
- if (!data[parentSid]) data[parentSid] = {
1321
- ts: Date.now(),
1322
- entries: [],
1323
- scroll: 0,
1324
- expanded: "",
1325
- children: {}
1326
- };
1327
- if (!data[parentSid].children) data[parentSid].children = {};
1328
- if (!data[parentSid].children[props.sessionId]) data[parentSid].children[props.sessionId] = {
1329
- scroll: 0,
1330
- expanded: "",
1331
- entries: []
1332
- };
1333
- data[parentSid].children[props.sessionId] = {
1334
- ...data[parentSid].children[props.sessionId],
1335
- entries: [...next.values()]
1336
- };
1337
- } else {
1338
- data[props.sessionId] = {
1339
- ...data[props.sessionId],
1340
- ts: Date.now(),
1341
- entries: [...next.values()],
1342
- children: data[props.sessionId]?.children ?? {}
1343
- };
1344
- }
1345
- saveSessionData2(data);
2239
+ mutateSessionData((data) => {
2240
+ const {
2241
+ parentSid,
2242
+ isChild
2243
+ } = resolveParent(props.sessionId);
2244
+ if (isChild) {
2245
+ const parent = ensureRootRecord(data, parentSid);
2246
+ const child = ensureChildRecord(data, parentSid, props.sessionId);
2247
+ child.entries = [...mergeSubEntries(child.entries ?? [], next.values()).values()];
2248
+ parent.ts = Date.now();
2249
+ } else {
2250
+ const rec = ensureRootRecord(data, props.sessionId);
2251
+ rec.ts = Date.now();
2252
+ rec.entries = [...mergeSubEntries(rec.entries ?? [], next.values()).values()];
2253
+ }
2254
+ });
1346
2255
  } catch {
1347
2256
  }
1348
2257
  } else {
@@ -1510,7 +2419,9 @@ function SubAgentPanel(props) {
1510
2419
  const part = event.payload?.part;
1511
2420
  if (!part) return;
1512
2421
  const eventSid = event.payload?.sessionID !== void 0 ? String(event.payload.sessionID) : void 0;
1513
- if (event.scope === "global" && eventSid !== props.sessionId) return;
2422
+ if (event.scope === "global" && eventSid !== props.sessionId) {
2423
+ return;
2424
+ }
1514
2425
  if (part.type === "subtask") {
1515
2426
  const agent = String(part.agent ?? "?");
1516
2427
  const prompt = String(part.prompt ?? "");
@@ -1539,7 +2450,7 @@ function SubAgentPanel(props) {
1539
2450
  if (rawStatus === "error") {
1540
2451
  const id2 = `tool:${String(part.id ?? "")}`;
1541
2452
  if (!part.id) return;
1542
- const stMeta2 = st?.metadata;
2453
+ const stMeta2 = partMetadata(part, st);
1543
2454
  const errorSid = stMeta2?.session_id !== void 0 ? String(stMeta2.session_id) : stMeta2?.sessionId !== void 0 ? String(stMeta2.sessionId) : void 0;
1544
2455
  const key = findSubEntryKey(entryMap(), id2, errorSid);
1545
2456
  const existing = key ? entryMap().get(key) : void 0;
@@ -1559,7 +2470,7 @@ function SubAgentPanel(props) {
1559
2470
  let status = "running";
1560
2471
  if (rawStatus === "completed") status = "done";
1561
2472
  if ((input?.run_in_background === true || input?.background === true) && status === "done") {
1562
- const stMetaCheck = st?.metadata;
2473
+ const stMetaCheck = partMetadata(part, st);
1563
2474
  const hasChild = stMetaCheck?.session_id !== void 0 || stMetaCheck?.sessionId !== void 0;
1564
2475
  if (hasChild) status = "running";
1565
2476
  }
@@ -1568,7 +2479,7 @@ function SubAgentPanel(props) {
1568
2479
  const desc = input?.description !== void 0 ? String(input.description) : "";
1569
2480
  const title = desc || truncate(prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim(), 40);
1570
2481
  const id = `tool:${String(part.id ?? crypto.randomUUID())}`;
1571
- const stMeta = st?.metadata;
2482
+ const stMeta = partMetadata(part, st);
1572
2483
  const subSid = stMeta?.session_id !== void 0 ? String(stMeta.session_id) : stMeta?.sessionId !== void 0 ? String(stMeta.sessionId) : void 0;
1573
2484
  upsertEntry({
1574
2485
  id,
@@ -1758,28 +2669,37 @@ function SubAgentPanel(props) {
1758
2669
  if (!found) {
1759
2670
  const data = loadSessionData2();
1760
2671
  const rec = data[parentSid];
1761
- if (rec?.entries) {
2672
+ if (rec?.entries?.length) {
1762
2673
  const fallbackMap = new Map(rec.entries.map((e) => [e.id, e]));
1763
2674
  found = tryMatchAndUpdate(fallbackMap, sid, status, nowTs);
1764
2675
  if (found) {
1765
- data[parentSid] = {
1766
- ...rec,
1767
- ts: nowTs,
1768
- entries: [...fallbackMap.values()]
1769
- };
1770
- saveSessionData2(data);
1771
2676
  globalEntryCache.set(parentSid, fallbackMap);
2677
+ mutateSessionData((latest) => {
2678
+ const latestRec = latest[parentSid];
2679
+ latest[parentSid] = {
2680
+ ...latestRec ?? rec,
2681
+ ts: nowTs,
2682
+ entries: [...mergeSubEntries(latestRec?.entries ?? [], fallbackMap.values()).values()]
2683
+ };
2684
+ });
1772
2685
  }
1773
2686
  }
1774
2687
  }
1775
2688
  if (found && parentCache) {
1776
- const data = loadSessionData2();
1777
- data[parentSid] = {
1778
- ...data[parentSid],
1779
- ts: nowTs,
1780
- entries: [...parentCache.values()]
1781
- };
1782
- saveSessionData2(data);
2689
+ mutateSessionData((data) => {
2690
+ const rec = data[parentSid];
2691
+ data[parentSid] = {
2692
+ ...rec ?? {
2693
+ entries: [],
2694
+ scroll: 0,
2695
+ expanded: "",
2696
+ children: {},
2697
+ ts: nowTs
2698
+ },
2699
+ ts: nowTs,
2700
+ entries: [...mergeSubEntries(rec?.entries ?? [], parentCache.values()).values()]
2701
+ };
2702
+ });
1783
2703
  }
1784
2704
  }
1785
2705
  } catch {
@@ -1818,52 +2738,357 @@ function SubAgentPanel(props) {
1818
2738
  }, 150);
1819
2739
  };
1820
2740
  const bump = () => setRenderTick((v) => v + 1);
2741
+ const settleIdleEntries = (prev) => {
2742
+ let changed = false;
2743
+ const next = new Map(prev);
2744
+ for (const [id, entry] of next) {
2745
+ if (entry.status !== "running" && entry.status !== "cancel_requested") continue;
2746
+ let sid = entry.sessionId;
2747
+ if (!sid) {
2748
+ try {
2749
+ const callId = String(entry.id).replace(/^tool:/, "");
2750
+ sid = props.api.session.resolveChild?.({
2751
+ parentId: props.sessionId,
2752
+ callId,
2753
+ agent: entry.agent,
2754
+ startedAt: entry.startedAt
2755
+ });
2756
+ } catch {
2757
+ }
2758
+ if (!sid) continue;
2759
+ next.set(id, {
2760
+ ...entry,
2761
+ sessionId: sid
2762
+ });
2763
+ changed = true;
2764
+ }
2765
+ try {
2766
+ const st = props.api.session.status(sid);
2767
+ if (!st || st.type !== "idle") continue;
2768
+ const tokens = props.api.usage.readSessionTokens(sid);
2769
+ const cost = props.api.usage.readSessionCost(sid);
2770
+ const finalStatus = entry.status === "cancel_requested" && entry.abortAccepted ? "cancelled" : "done";
2771
+ next.set(id, {
2772
+ ...entry,
2773
+ sessionId: sid,
2774
+ status: finalStatus,
2775
+ endedAt: Date.now(),
2776
+ tokens: tokens ?? entry.tokens,
2777
+ cost: cost ?? entry.cost
2778
+ });
2779
+ changed = true;
2780
+ } catch {
2781
+ }
2782
+ }
2783
+ return changed ? next : prev;
2784
+ };
2785
+ const reconcileIdleEntries = () => {
2786
+ try {
2787
+ const before = entryMap();
2788
+ const after = settleIdleEntries(before);
2789
+ if (after !== before) setEntryMap(after);
2790
+ } catch {
2791
+ }
2792
+ };
2793
+ const runSessionScan = (sid, replace) => {
2794
+ let msgs;
2795
+ try {
2796
+ msgs = props.api.session.messages(sid);
2797
+ } catch {
2798
+ }
2799
+ if (!msgs || msgs.length === 0) return false;
2800
+ setEntryMapRaw((prev) => {
2801
+ const next = replace ? new Map(globalEntryCache.get(sid) ?? loadEntries(sid)) : new Map(prev);
2802
+ const {
2803
+ parentSid: scanPSid,
2804
+ isChild: scanChild
2805
+ } = resolveParent(sid);
2806
+ const scanRec = loadSessionData2()[scanPSid];
2807
+ const clearedIds = new Set(scanChild ? scanRec?.children?.[sid]?.clearedIds : scanRec?.clearedIds);
2808
+ try {
2809
+ for (const msg of msgs) {
2810
+ const parts = props.api.session.part(msg.id) ?? [];
2811
+ for (const partRaw of parts) {
2812
+ const part = partRaw;
2813
+ if (part.type === "tool") {
2814
+ const tool = String(part.tool ?? "");
2815
+ if (!SUBAGENT_TOOLS.has(tool)) continue;
2816
+ const id = `tool:${String(part.id ?? "")}`;
2817
+ if (!part.id) continue;
2818
+ const st = part.state;
2819
+ const rawStatus = String(st?.status ?? "");
2820
+ const scanInput = st?.input;
2821
+ const scanMeta = partMetadata(part, st);
2822
+ let scanSubSid = scanMeta?.session_id !== void 0 ? String(scanMeta.session_id) : scanMeta?.sessionId !== void 0 ? String(scanMeta.sessionId) : scanMeta?.sessionID !== void 0 ? String(scanMeta.sessionID) : void 0;
2823
+ if (!scanSubSid) {
2824
+ try {
2825
+ scanSubSid = props.api.session.resolveChild?.({
2826
+ parentId: sid,
2827
+ callId: String(part.id)
2828
+ });
2829
+ } catch {
2830
+ }
2831
+ }
2832
+ const existingKey = findSubEntryKey(next, id, scanSubSid);
2833
+ const exists = existingKey ? next.get(existingKey) : void 0;
2834
+ if (!exists && clearedIds.has(id)) continue;
2835
+ if ((rawStatus === "pending" || rawStatus === "") && !exists) continue;
2836
+ if (rawStatus === "error") {
2837
+ if (exists && exists.status === "running") {
2838
+ next.set(existingKey ?? id, {
2839
+ ...exists,
2840
+ status: "error",
2841
+ endedAt: Date.now()
2842
+ });
2843
+ }
2844
+ continue;
2845
+ }
2846
+ const settled = exists !== void 0 && exists.status !== "running" && exists.status !== "cancel_requested";
2847
+ const needsRepair = settled && (exists.sessionId === void 0 || exists.tokens === void 0 || exists.cost === void 0 || !validModel(exists.model));
2848
+ if (settled && !needsRepair) continue;
2849
+ let status = "running";
2850
+ if (rawStatus === "completed") status = "done";
2851
+ if ((scanInput?.run_in_background === true || scanInput?.background === true) && status === "done") {
2852
+ if (scanSubSid !== void 0) status = "running";
2853
+ }
2854
+ if (status === "done" && scanSubSid) {
2855
+ try {
2856
+ const childStatus = props.api.session.status(scanSubSid);
2857
+ if (childStatus && childStatus.type !== "idle") status = "running";
2858
+ } catch {
2859
+ }
2860
+ }
2861
+ if (settled) {
2862
+ status = exists.status;
2863
+ } else if (exists && status === "running") {
2864
+ if (!rawStatus) {
2865
+ const msgTokens = msg?.tokens;
2866
+ if (msgTokens && (Number(msgTokens.input) > 0 || Number(msgTokens.output) > 0)) {
2867
+ status = "done";
2868
+ } else {
2869
+ continue;
2870
+ }
2871
+ } else {
2872
+ continue;
2873
+ }
2874
+ }
2875
+ const agent = String(part.subagent_type ?? scanInput?.agent ?? scanInput?.subagent_type ?? tool);
2876
+ const prompt = String(scanInput?.prompt ?? part.description ?? "");
2877
+ const desc = scanInput?.description !== void 0 ? String(scanInput.description) : "";
2878
+ const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
2879
+ let tokens;
2880
+ if (scanSubSid) tokens = props.api.usage.readSessionTokens(scanSubSid);
2881
+ let cost;
2882
+ if (scanSubSid) cost = props.api.usage.readSessionCost(scanSubSid);
2883
+ let model;
2884
+ if (scanSubSid) model = props.api.usage.readSessionModel(scanSubSid);
2885
+ const entryKey = existingKey ?? id;
2886
+ next.set(entryKey, {
2887
+ ...exists ?? {
2888
+ id
2889
+ },
2890
+ title: title || exists?.title || "",
2891
+ agent: exists?.agent ?? agent,
2892
+ prompt: prompt || exists?.prompt || "",
2893
+ // Preserve existing values (from handleSessionEnd / KV) — scan must not overwrite
2894
+ tokens: exists?.tokens ?? tokens,
2895
+ cost: exists?.cost ?? cost,
2896
+ model: validModel(exists?.model) ? exists.model : model,
2897
+ sessionId: exists?.sessionId ?? scanSubSid,
2898
+ status,
2899
+ startedAt: exists?.startedAt || Date.now(),
2900
+ endedAt: exists?.endedAt ?? (status === "done" ? Date.now() : void 0)
2901
+ });
2902
+ if (entryKey !== id) next.delete(id);
2903
+ }
2904
+ }
2905
+ }
2906
+ } catch {
2907
+ }
2908
+ return next;
2909
+ });
2910
+ setEntryMapRaw((prev) => settleIdleEntries(prev));
2911
+ const snapshot = entryMap();
2912
+ if (snapshot.size > 0) {
2913
+ globalEntryCache.set(sid, new Map(snapshot));
2914
+ persistEntries(sid, snapshot);
2915
+ }
2916
+ return true;
2917
+ };
2918
+ let rescanTimer;
2919
+ const missingNewestToolPart = () => {
2920
+ try {
2921
+ const msgs = props.api.session.messages(props.sessionId);
2922
+ if (!msgs || msgs.length === 0) return false;
2923
+ const map = entryMap();
2924
+ const from = Math.max(0, msgs.length - 6);
2925
+ for (let i = msgs.length - 1; i >= from; i--) {
2926
+ const parts = props.api.session.part(msgs[i]?.id) ?? [];
2927
+ for (const part of parts) {
2928
+ if (!part || part.type !== "tool") continue;
2929
+ if (!SUBAGENT_TOOLS.has(String(part.tool ?? ""))) continue;
2930
+ const partId = String(part.id ?? "");
2931
+ if (!partId) continue;
2932
+ const st = part.state;
2933
+ const rawStatus = String(st?.status ?? "");
2934
+ if (rawStatus !== "running" && rawStatus !== "completed") continue;
2935
+ const id = `tool:${partId}`;
2936
+ const meta = partMetadata(part, st);
2937
+ const subSid = meta?.session_id !== void 0 ? String(meta.session_id) : meta?.sessionId !== void 0 ? String(meta.sessionId) : void 0;
2938
+ const key = findSubEntryKey(map, id, subSid);
2939
+ const entry = key ? map.get(key) : void 0;
2940
+ if (!entry) return true;
2941
+ const input = st?.input;
2942
+ const isBackground = input?.background === true || input?.run_in_background === true;
2943
+ const stillRunning = entry.status === "running" || entry.status === "cancel_requested";
2944
+ if (rawStatus === "completed" && !isBackground && stillRunning) return true;
2945
+ }
2946
+ }
2947
+ } catch {
2948
+ }
2949
+ return false;
2950
+ };
2951
+ const scheduleRescan = () => {
2952
+ if (disposed) return;
2953
+ clearTimeout(rescanTimer);
2954
+ rescanTimer = setTimeout(() => {
2955
+ if (disposed) return;
2956
+ untrack(() => {
2957
+ if (!missingNewestToolPart()) return;
2958
+ runSessionScan(props.sessionId, false);
2959
+ bump();
2960
+ });
2961
+ }, 400);
2962
+ };
2963
+ let enrichCursor = 0;
1821
2964
  onMount(() => {
1822
2965
  const clock = setInterval(() => {
1823
2966
  setNow(Date.now());
1824
2967
  bump();
1825
2968
  }, 100);
2969
+ let tick = 0;
1826
2970
  const tokenTimer = setInterval(() => {
2971
+ tick++;
1827
2972
  untrack(() => {
2973
+ let enriched;
2974
+ let attempted = 0;
1828
2975
  setEntryMapRaw((prev) => {
1829
2976
  let changed = false;
1830
2977
  const next = new Map(prev);
2978
+ const candidates = [];
1831
2979
  for (const [id, entry] of next) {
1832
- if (entry.status === "running" && entry.sessionId) {
2980
+ const running = entry.status === "running" || entry.status === "cancel_requested";
2981
+ const missing = entry.tokens === void 0 || entry.cost === void 0 || !validModel(entry.model);
2982
+ if (entry.sessionId && !running && !missing) continue;
2983
+ candidates.push(id);
2984
+ }
2985
+ if (candidates.length > 0) {
2986
+ const start = enrichCursor % candidates.length;
2987
+ let budget = 4;
2988
+ for (let k = 0; k < candidates.length && budget > 0; k++) {
2989
+ const id = candidates[(start + k) % candidates.length];
2990
+ let entry = next.get(id);
2991
+ if (!entry) continue;
2992
+ if (!entry.sessionId) {
2993
+ try {
2994
+ const callId = String(entry.id).replace(/^tool:/, "");
2995
+ const sid = props.api.session.resolveChild?.({
2996
+ parentId: props.sessionId,
2997
+ callId,
2998
+ agent: entry.agent,
2999
+ startedAt: entry.startedAt
3000
+ });
3001
+ if (sid) {
3002
+ entry = {
3003
+ ...entry,
3004
+ sessionId: sid
3005
+ };
3006
+ next.set(id, entry);
3007
+ changed = true;
3008
+ }
3009
+ } catch {
3010
+ }
3011
+ }
3012
+ if (!entry.sessionId) continue;
3013
+ const childSid = entry.sessionId;
1833
3014
  let isChild = false;
1834
3015
  try {
1835
- const s = props.api.session.get(entry.sessionId);
1836
- isChild = s?.parentID === props.sessionId;
3016
+ const s = props.api.session.get(childSid);
3017
+ isChild = !s || s.parentID === props.sessionId;
1837
3018
  } catch {
3019
+ isChild = true;
1838
3020
  }
1839
3021
  if (!isChild) continue;
1840
- const total = props.api.usage.readSessionTokens(entry.sessionId);
1841
- const todo = props.api.usage.readSessionTodo(entry.sessionId);
1842
- const model = entry.model ?? props.api.usage.readSessionModel(entry.sessionId);
3022
+ budget--;
3023
+ attempted++;
3024
+ const running = entry.status === "running" || entry.status === "cancel_requested";
3025
+ let entryChanged = false;
1843
3026
  const nextEntry = {
1844
3027
  ...entry
1845
3028
  };
1846
- if (total !== void 0 && total !== entry.tokens) {
1847
- nextEntry.tokens = total;
1848
- changed = true;
3029
+ if (running || entry.tokens === void 0) {
3030
+ const total = props.api.usage.readSessionTokens(childSid);
3031
+ if (total !== void 0 && total !== entry.tokens) {
3032
+ nextEntry.tokens = total;
3033
+ entryChanged = true;
3034
+ }
3035
+ }
3036
+ if (running || entry.cost === void 0) {
3037
+ const cost = props.api.usage.readSessionCost(childSid);
3038
+ if (cost !== void 0 && cost !== entry.cost) {
3039
+ nextEntry.cost = cost;
3040
+ entryChanged = true;
3041
+ }
1849
3042
  }
1850
- if (todo !== void 0) {
1851
- if (todo.total !== entry.todoTotal || todo.done !== entry.todoDone) {
3043
+ if (running || !validModel(entry.model)) {
3044
+ const model = props.api.usage.readSessionModel(childSid);
3045
+ if (model && model !== entry.model) {
3046
+ nextEntry.model = model;
3047
+ entryChanged = true;
3048
+ }
3049
+ }
3050
+ if (running) {
3051
+ const todo = props.api.usage.readSessionTodo(childSid);
3052
+ if (todo !== void 0 && (todo.total !== entry.todoTotal || todo.done !== entry.todoDone)) {
1852
3053
  nextEntry.todoTotal = todo.total;
1853
3054
  nextEntry.todoDone = todo.done;
1854
- changed = true;
3055
+ entryChanged = true;
1855
3056
  }
1856
3057
  }
1857
- if (model && !entry.model) {
1858
- nextEntry.model = model;
3058
+ if (entryChanged) {
3059
+ next.set(id, nextEntry);
1859
3060
  changed = true;
1860
3061
  }
1861
- if (changed) next.set(id, nextEntry);
1862
3062
  }
3063
+ enrichCursor = (start + Math.max(1, attempted)) % candidates.length;
1863
3064
  }
1864
- return changed ? next : prev;
3065
+ if (!changed) return prev;
3066
+ enriched = next;
3067
+ return next;
1865
3068
  });
3069
+ if (enriched) {
3070
+ globalEntryCache.set(props.sessionId, new Map(enriched));
3071
+ persistEntries(props.sessionId, enriched);
3072
+ }
1866
3073
  });
3074
+ reconcileIdleEntries();
3075
+ if (tick % 4 === 0) {
3076
+ untrack(() => {
3077
+ if (missingNewestToolPart()) {
3078
+ runSessionScan(props.sessionId, false);
3079
+ bump();
3080
+ }
3081
+ });
3082
+ }
3083
+ if (tick % 60 === 0) {
3084
+ untrack(() => {
3085
+ const hasRunning = entryList().some((e) => e.status === "running" || e.status === "cancel_requested");
3086
+ if (hasRunning) {
3087
+ runSessionScan(props.sessionId, false);
3088
+ bump();
3089
+ }
3090
+ });
3091
+ }
1867
3092
  bump();
1868
3093
  }, 500);
1869
3094
  bump();
@@ -1871,7 +3096,10 @@ function SubAgentPanel(props) {
1871
3096
  handlePartUpdated(e);
1872
3097
  bump();
1873
3098
  });
1874
- const unsubMsg = props.api.event.on("message.updated", () => bump());
3099
+ const unsubMsg = props.api.event.on("message.updated", () => {
3100
+ bump();
3101
+ scheduleRescan();
3102
+ });
1875
3103
  const unsubIdle = props.api.event.on("session.idle", (e) => {
1876
3104
  handleSessionEnd(e, "done");
1877
3105
  bump();
@@ -1882,6 +3110,7 @@ function SubAgentPanel(props) {
1882
3110
  });
1883
3111
  onCleanup(() => {
1884
3112
  disposed = true;
3113
+ clearTimeout(rescanTimer);
1885
3114
  clearInterval(clock);
1886
3115
  clearInterval(tokenTimer);
1887
3116
  unsubPart();
@@ -1899,7 +3128,9 @@ function SubAgentPanel(props) {
1899
3128
  const tick = clearTick();
1900
3129
  const forceReload = tick !== lastTick && !switched;
1901
3130
  lastTick = tick;
1902
- const t2 = setTimeout(() => {
3131
+ let attempts = 0;
3132
+ let retryTimer;
3133
+ const run = () => {
1903
3134
  untrack(() => {
1904
3135
  if (switched) {
1905
3136
  const {
@@ -1910,128 +3141,25 @@ function SubAgentPanel(props) {
1910
3141
  const saved = isChild ? data[parentSid]?.children?.[sid]?.scroll ?? 0 : data[sid]?.scroll ?? 0;
1911
3142
  setScrollOffset(saved);
1912
3143
  if (!isChild && data[sid]?.entries?.length) {
1913
- data[sid].ts = Date.now();
1914
- saveSessionData2(data);
3144
+ void Promise.resolve(updateSessionData(props.api.kv, (latest) => {
3145
+ if (latest[sid]?.entries?.length) latest[sid].ts = Date.now();
3146
+ })).catch(() => {
3147
+ });
1915
3148
  }
1916
3149
  }
1917
- setEntryMapRaw((prev) => {
1918
- const next = switched || forceReload ? new Map(globalEntryCache.get(sid) ?? loadEntries(sid)) : new Map(prev);
1919
- const {
1920
- parentSid: scanPSid,
1921
- isChild: scanChild
1922
- } = resolveParent(sid);
1923
- const scanRec = loadSessionData2()[scanPSid];
1924
- const clearedIds = new Set(scanChild ? scanRec?.children?.[sid]?.clearedIds : scanRec?.clearedIds);
1925
- try {
1926
- const msgs = props.api.session.messages(sid);
1927
- if (msgs && msgs.length) {
1928
- for (const msg of msgs) {
1929
- const parts = props.api.session.part(msg.id) ?? [];
1930
- for (const partRaw of parts) {
1931
- const part = partRaw;
1932
- if (part.type === "tool") {
1933
- const tool = String(part.tool ?? "");
1934
- if (!SUBAGENT_TOOLS.has(tool)) continue;
1935
- const id = `tool:${String(part.id ?? "")}`;
1936
- if (!part.id) continue;
1937
- const st = part.state;
1938
- const rawStatus = String(st?.status ?? "");
1939
- const scanStMeta = st?.metadata;
1940
- const scanSubSid = scanStMeta?.session_id !== void 0 ? String(scanStMeta.session_id) : scanStMeta?.sessionId !== void 0 ? String(scanStMeta.sessionId) : void 0;
1941
- const existingKey = findSubEntryKey(next, id, scanSubSid);
1942
- const exists = existingKey ? next.get(existingKey) : void 0;
1943
- if (!exists && clearedIds.has(id)) continue;
1944
- if ((rawStatus === "pending" || rawStatus === "") && !exists) continue;
1945
- if (rawStatus === "error") {
1946
- if (exists && exists.status === "running") {
1947
- next.set(existingKey ?? id, {
1948
- ...exists,
1949
- status: "error",
1950
- endedAt: Date.now()
1951
- });
1952
- }
1953
- continue;
1954
- }
1955
- let status = "running";
1956
- if (rawStatus === "completed") status = "done";
1957
- if ((st?.input?.run_in_background === true || st?.input?.background === true) && status === "done") {
1958
- const scanStMeta2 = st?.metadata;
1959
- const scanHasChild = scanStMeta2?.session_id !== void 0 || scanStMeta2?.sessionId !== void 0;
1960
- if (scanHasChild) status = "running";
1961
- }
1962
- if (exists && existingKey === id && exists.status !== "running" && exists.status !== "cancel_requested") continue;
1963
- if (exists && status === "running") {
1964
- if (!rawStatus) {
1965
- const msgTokens = msg?.tokens;
1966
- if (msgTokens && (Number(msgTokens.input) > 0 || Number(msgTokens.output) > 0)) {
1967
- status = "done";
1968
- } else if (Date.now() - exists.startedAt > 30 * 60 * 1e3) {
1969
- status = "done";
1970
- } else {
1971
- continue;
1972
- }
1973
- } else {
1974
- continue;
1975
- }
1976
- }
1977
- const input = st?.input;
1978
- const agent = String(part.subagent_type ?? input?.agent ?? input?.subagent_type ?? tool);
1979
- const prompt = String(input?.prompt ?? part.description ?? "");
1980
- const desc = input?.description !== void 0 ? String(input.description) : "";
1981
- const title = desc || truncate(prompt.replace(/\n/g, " ").trim(), 40);
1982
- let tokens;
1983
- if (scanSubSid) tokens = props.api.usage.readSessionTokens(scanSubSid);
1984
- const ended = status === "done";
1985
- const entryKey = existingKey ?? id;
1986
- next.set(entryKey, {
1987
- id: exists?.id ?? id,
1988
- title,
1989
- agent,
1990
- prompt,
1991
- // Preserve existing values (from handleSessionEnd / KV) — scan must not overwrite
1992
- tokens: exists?.tokens ?? tokens,
1993
- sessionId: exists?.sessionId ?? scanSubSid,
1994
- status,
1995
- startedAt: exists?.startedAt || Date.now(),
1996
- endedAt: ended ? exists?.endedAt || Date.now() : void 0
1997
- });
1998
- if (entryKey !== id) next.delete(id);
1999
- }
2000
- }
2001
- }
2002
- }
2003
- } catch {
2004
- }
2005
- return next;
2006
- });
2007
- setEntryMapRaw((prev) => {
2008
- let changed = false;
2009
- const next = new Map(prev);
2010
- for (const [id, entry] of next) {
2011
- if (entry.status !== "running" && entry.status !== "cancel_requested" || !entry.sessionId) continue;
2012
- try {
2013
- const st = props.api.session.status(entry.sessionId);
2014
- if (!st || st.type !== "idle") continue;
2015
- const tokens = props.api.usage.readSessionTokens(entry.sessionId);
2016
- const cost = props.api.usage.readSessionCost(entry.sessionId);
2017
- const finalStatus = entry.status === "cancel_requested" && entry.abortAccepted ? "cancelled" : "done";
2018
- next.set(id, {
2019
- ...entry,
2020
- status: finalStatus,
2021
- endedAt: Date.now(),
2022
- tokens: tokens ?? entry.tokens,
2023
- cost: cost ?? entry.cost
2024
- });
2025
- changed = true;
2026
- } catch {
2027
- }
2028
- }
2029
- return changed ? next : prev;
2030
- });
2031
- bump();
3150
+ const ok = runSessionScan(sid, switched || forceReload);
3151
+ if (!ok && attempts < 4) {
3152
+ attempts++;
3153
+ retryTimer = setTimeout(run, 400);
3154
+ }
2032
3155
  });
2033
- }, 150);
2034
- onCleanup(() => clearTimeout(t2));
3156
+ bump();
3157
+ };
3158
+ const t2 = setTimeout(run, 150);
3159
+ onCleanup(() => {
3160
+ clearTimeout(t2);
3161
+ if (retryTimer) clearTimeout(retryTimer);
3162
+ });
2035
3163
  });
2036
3164
  const pal = createMemo(() => {
2037
3165
  const th = props.theme;
@@ -2410,14 +3538,17 @@ function SubAgentPanel(props) {
2410
3538
  return "#" + [r, g, bl].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
2411
3539
  };
2412
3540
  const timeColor = () => isActiveRunning ? pal().warning : isError ? pal().error : pal().muted;
2413
- const tokenText = () => !isExpanded() && entry.tokens !== void 0 && entry.tokens > 0 ? ` ${fmtTokens(entry.tokens)}` : "";
2414
- const timeText = () => !isExpanded() && (elapsed() >= 2e3 || entry.endedAt !== void 0) ? fmtDurationShort(elapsed(), isActiveRunning) : "";
3541
+ const tokenText = () => !isExpanded() && props.showEntryTokens() && entry.tokens !== void 0 && entry.tokens > 0 ? ` ${fmtTokens(entry.tokens)}` : "";
3542
+ const costText = () => !isExpanded() && props.showEntryCost() && entry.cost !== void 0 && entry.cost > 0 ? ` $${entry.cost.toFixed(4)}` : "";
3543
+ const timeText = () => !isExpanded() && props.showEntryTime() && (elapsed() >= 2e3 || entry.endedAt !== void 0) ? fmtDuration(elapsed(), isActiveRunning, props.timeFormat()) : "";
2415
3544
  const suffixW = () => {
2416
3545
  let w = 0;
2417
3546
  const t2 = timeText();
2418
3547
  if (t2) w += 1 + visualWidth(t2);
2419
3548
  const tk = tokenText();
2420
3549
  if (tk) w += visualWidth(tk);
3550
+ const c = costText();
3551
+ if (c) w += visualWidth(c);
2421
3552
  return w;
2422
3553
  };
2423
3554
  const labelAvail = () => Math.max(6, panelWidth() - gutter() - LEFT_PAD - suffixW());
@@ -2461,6 +3592,17 @@ function SubAgentPanel(props) {
2461
3592
  return _el$83;
2462
3593
  })() : null;
2463
3594
  })(), null);
3595
+ _$insert(_el$25, (() => {
3596
+ var _c$4 = _$memo(() => !!costText());
3597
+ return () => _c$4() ? (() => {
3598
+ var _el$84 = _$createElement("span");
3599
+ _$insert(_el$84, costText);
3600
+ _$effect((_$p) => _$setProp(_el$84, "style", {
3601
+ fg: pal().warning
3602
+ }, _$p));
3603
+ return _el$84;
3604
+ })() : null;
3605
+ })(), null);
2464
3606
  _$effect((_p$) => {
2465
3607
  var _v$3 = {
2466
3608
  fg: pal().muted
@@ -2553,7 +3695,7 @@ function SubAgentPanel(props) {
2553
3695
  _$insertNode(_el$45, _el$46);
2554
3696
  _$insert(_el$45, () => t("time.label"), _el$46);
2555
3697
  _$insert(_el$47, () => " ".repeat(expandedPad(t("time.label"))));
2556
- _$insert(_el$48, () => fmtDurationShort(elapsed(), isActiveRunning));
3698
+ _$insert(_el$48, () => fmtDuration(elapsed(), isActiveRunning, props.timeFormat()));
2557
3699
  _$effect((_p$) => {
2558
3700
  var _v$10 = {
2559
3701
  fg: pal().primary
@@ -2647,16 +3789,16 @@ function SubAgentPanel(props) {
2647
3789
  return (() => {
2648
3790
  const cost = entry.cost;
2649
3791
  return (() => {
2650
- var _el$84 = _$createElement("text"), _el$85 = _$createTextNode(` `), _el$86 = _$createElement("span"), _el$87 = _$createTextNode(`: `), _el$88 = _$createElement("span"), _el$89 = _$createElement("span"), _el$90 = _$createTextNode(`$`);
2651
- _$insertNode(_el$84, _el$85);
2652
- _$insertNode(_el$84, _el$86);
2653
- _$insertNode(_el$84, _el$88);
2654
- _$insertNode(_el$84, _el$89);
2655
- _$insertNode(_el$86, _el$87);
2656
- _$insert(_el$86, () => t("cost.label"), _el$87);
2657
- _$insert(_el$88, () => " ".repeat(expandedPad(t("cost.label"))));
2658
- _$insertNode(_el$89, _el$90);
2659
- _$insert(_el$89, () => cost.toFixed(4), null);
3792
+ var _el$85 = _$createElement("text"), _el$86 = _$createTextNode(` `), _el$87 = _$createElement("span"), _el$88 = _$createTextNode(`: `), _el$89 = _$createElement("span"), _el$90 = _$createElement("span"), _el$91 = _$createTextNode(`$`);
3793
+ _$insertNode(_el$85, _el$86);
3794
+ _$insertNode(_el$85, _el$87);
3795
+ _$insertNode(_el$85, _el$89);
3796
+ _$insertNode(_el$85, _el$90);
3797
+ _$insertNode(_el$87, _el$88);
3798
+ _$insert(_el$87, () => t("cost.label"), _el$88);
3799
+ _$insert(_el$89, () => " ".repeat(expandedPad(t("cost.label"))));
3800
+ _$insertNode(_el$90, _el$91);
3801
+ _$insert(_el$90, () => cost.toFixed(4), null);
2660
3802
  _$effect((_p$) => {
2661
3803
  var _v$29 = {
2662
3804
  fg: pal().primary
@@ -2665,22 +3807,22 @@ function SubAgentPanel(props) {
2665
3807
  }, _v$31 = {
2666
3808
  fg: pal().muted
2667
3809
  };
2668
- _v$29 !== _p$.e && (_p$.e = _$setProp(_el$86, "style", _v$29, _p$.e));
2669
- _v$30 !== _p$.t && (_p$.t = _$setProp(_el$88, "style", _v$30, _p$.t));
2670
- _v$31 !== _p$.a && (_p$.a = _$setProp(_el$89, "style", _v$31, _p$.a));
3810
+ _v$29 !== _p$.e && (_p$.e = _$setProp(_el$87, "style", _v$29, _p$.e));
3811
+ _v$30 !== _p$.t && (_p$.t = _$setProp(_el$89, "style", _v$30, _p$.t));
3812
+ _v$31 !== _p$.a && (_p$.a = _$setProp(_el$90, "style", _v$31, _p$.a));
2671
3813
  return _p$;
2672
3814
  }, {
2673
3815
  e: void 0,
2674
3816
  t: void 0,
2675
3817
  a: void 0
2676
3818
  });
2677
- return _el$84;
3819
+ return _el$85;
2678
3820
  })();
2679
3821
  })();
2680
3822
  }
2681
3823
  }), _$createComponent(Show, {
2682
3824
  get when() {
2683
- return entry.model;
3825
+ return _$memo(() => !!validModel(entry.model))() ? entry.model : void 0;
2684
3826
  },
2685
3827
  get children() {
2686
3828
  var _el$61 = _$createElement("text"), _el$62 = _$createTextNode(` `), _el$63 = _$createElement("span"), _el$64 = _$createTextNode(`: `), _el$65 = _$createElement("span"), _el$66 = _$createElement("span");
@@ -2819,68 +3961,68 @@ ${t("session.toast.copy_failed")}`, {
2819
3961
  const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0);
2820
3962
  const spacerW = () => Math.max(1, panelWidth() - gutter() - openW() - rightW - 2);
2821
3963
  return (() => {
2822
- var _el$91 = _$createElement("box"), _el$95 = _$createElement("text");
2823
- _$insertNode(_el$91, _el$95);
2824
- _$setProp(_el$91, "flexDirection", "row");
2825
- _$insert(_el$91, _$createComponent(Show, {
3964
+ var _el$92 = _$createElement("box"), _el$96 = _$createElement("text");
3965
+ _$insertNode(_el$92, _el$96);
3966
+ _$setProp(_el$92, "flexDirection", "row");
3967
+ _$insert(_el$92, _$createComponent(Show, {
2826
3968
  get when() {
2827
3969
  return entry.sessionId;
2828
3970
  },
2829
3971
  get children() {
2830
- var _el$92 = _$createElement("text"), _el$93 = _$createElement("span"), _el$94 = _$createElement("span");
2831
- _$insertNode(_el$92, _el$93);
2832
- _$insertNode(_el$92, _el$94);
2833
- _$setProp(_el$92, "onMouseOver", () => setHoveredOpen(entry.id));
2834
- _$setProp(_el$92, "onMouseOut", () => setHoveredOpen(void 0));
2835
- _$setProp(_el$92, "onMouseUp", () => {
3972
+ var _el$93 = _$createElement("text"), _el$94 = _$createElement("span"), _el$95 = _$createElement("span");
3973
+ _$insertNode(_el$93, _el$94);
3974
+ _$insertNode(_el$93, _el$95);
3975
+ _$setProp(_el$93, "onMouseOver", () => setHoveredOpen(entry.id));
3976
+ _$setProp(_el$93, "onMouseOut", () => setHoveredOpen(void 0));
3977
+ _$setProp(_el$93, "onMouseUp", () => {
2836
3978
  if (entry.sessionId) {
2837
3979
  props.api.route.navigateSession(entry.sessionId);
2838
3980
  }
2839
3981
  });
2840
- _$insert(_el$93, openPrefix);
2841
- _$insert(_el$94, () => t("open.label"));
3982
+ _$insert(_el$94, openPrefix);
3983
+ _$insert(_el$95, () => t("open.label"));
2842
3984
  _$effect((_p$) => {
2843
3985
  var _v$32 = {
2844
3986
  fg: hoveredOpen() === entry.id ? pal().warning : pal().primary
2845
3987
  }, _v$33 = {
2846
3988
  fg: hoveredOpen() === entry.id ? pal().warning : pal().primary
2847
3989
  };
2848
- _v$32 !== _p$.e && (_p$.e = _$setProp(_el$93, "style", _v$32, _p$.e));
2849
- _v$33 !== _p$.t && (_p$.t = _$setProp(_el$94, "style", _v$33, _p$.t));
3990
+ _v$32 !== _p$.e && (_p$.e = _$setProp(_el$94, "style", _v$32, _p$.e));
3991
+ _v$33 !== _p$.t && (_p$.t = _$setProp(_el$95, "style", _v$33, _p$.t));
2850
3992
  return _p$;
2851
3993
  }, {
2852
3994
  e: void 0,
2853
3995
  t: void 0
2854
3996
  });
2855
- return _el$92;
3997
+ return _el$93;
2856
3998
  }
2857
- }), _el$95);
2858
- _$insert(_el$95, () => " ".repeat(spacerW()));
2859
- _$insert(_el$91, _$createComponent(Show, {
3999
+ }), _el$96);
4000
+ _$insert(_el$96, () => " ".repeat(spacerW()));
4001
+ _$insert(_el$92, _$createComponent(Show, {
2860
4002
  get when() {
2861
4003
  return isRunning && entry.sessionId;
2862
4004
  },
2863
4005
  get children() {
2864
- var _el$96 = _$createElement("text"), _el$97 = _$createElement("span");
2865
- _$insertNode(_el$96, _el$97);
2866
- _$setProp(_el$96, "onMouseOver", () => setHoveredCancel(entry.id));
2867
- _$setProp(_el$96, "onMouseOut", () => setHoveredCancel(void 0));
2868
- _$setProp(_el$96, "onMouseUp", () => cancelEntry(entry));
2869
- _$insert(_el$97, cancelLabel);
2870
- _$effect((_$p) => _$setProp(_el$97, "style", {
4006
+ var _el$97 = _$createElement("text"), _el$98 = _$createElement("span");
4007
+ _$insertNode(_el$97, _el$98);
4008
+ _$setProp(_el$97, "onMouseOver", () => setHoveredCancel(entry.id));
4009
+ _$setProp(_el$97, "onMouseOut", () => setHoveredCancel(void 0));
4010
+ _$setProp(_el$97, "onMouseUp", () => cancelEntry(entry));
4011
+ _$insert(_el$98, cancelLabel);
4012
+ _$effect((_$p) => _$setProp(_el$98, "style", {
2871
4013
  fg: hoveredCancel() === entry.id ? pal().warning : pal().error
2872
4014
  }, _$p));
2873
- return _el$96;
4015
+ return _el$97;
2874
4016
  }
2875
4017
  }), null);
2876
- _$insert(_el$91, _$createComponent(Show, {
4018
+ _$insert(_el$92, _$createComponent(Show, {
2877
4019
  when: isRunning,
2878
4020
  get children() {
2879
- var _el$98 = _$createElement("text"), _el$99 = _$createElement("span");
2880
- _$insertNode(_el$98, _el$99);
2881
- _$setProp(_el$98, "onMouseOver", () => setHoveredDismiss(entry.id));
2882
- _$setProp(_el$98, "onMouseOut", () => setHoveredDismiss(void 0));
2883
- _$setProp(_el$98, "onMouseUp", () => {
4021
+ var _el$99 = _$createElement("text"), _el$100 = _$createElement("span");
4022
+ _$insertNode(_el$99, _el$100);
4023
+ _$setProp(_el$99, "onMouseOver", () => setHoveredDismiss(entry.id));
4024
+ _$setProp(_el$99, "onMouseOut", () => setHoveredDismiss(void 0));
4025
+ _$setProp(_el$99, "onMouseUp", () => {
2884
4026
  upsertEntry({
2885
4027
  id: entry.id,
2886
4028
  title: entry.title,
@@ -2889,17 +4031,17 @@ ${t("session.toast.copy_failed")}`, {
2889
4031
  status: "done"
2890
4032
  });
2891
4033
  });
2892
- _$insert(_el$99, dismissLabel);
2893
- _$effect((_$p) => _$setProp(_el$99, "style", {
4034
+ _$insert(_el$100, dismissLabel);
4035
+ _$effect((_$p) => _$setProp(_el$100, "style", {
2894
4036
  fg: hoveredDismiss() === entry.id ? pal().warning : pal().muted
2895
4037
  }, _$p));
2896
- return _el$98;
4038
+ return _el$99;
2897
4039
  }
2898
4040
  }), null);
2899
- _$effect((_$p) => _$setProp(_el$95, "style", {
4041
+ _$effect((_$p) => _$setProp(_el$96, "style", {
2900
4042
  fg: pal().muted
2901
4043
  }, _$p));
2902
- return _el$91;
4044
+ return _el$92;
2903
4045
  })();
2904
4046
  })();
2905
4047
  }
@@ -2920,13 +4062,13 @@ ${t("session.toast.copy_failed")}`, {
2920
4062
  const right = props.sortOrder() === "desc" ? `\u2191 ${t("scroll.top")}` : `\u2193 ${t("scroll.bottom")}`;
2921
4063
  const pad = showTop ? Math.max(1, panelWidth() - gutter() - visualWidth(left) - visualWidth(right)) : 0;
2922
4064
  return (() => {
2923
- var _el$100 = _$createElement("box"), _el$101 = _$createElement("text"), _el$102 = _$createElement("span");
2924
- _$insertNode(_el$100, _el$101);
2925
- _$setProp(_el$100, "flexDirection", "row");
4065
+ var _el$101 = _$createElement("box"), _el$102 = _$createElement("text"), _el$103 = _$createElement("span");
2926
4066
  _$insertNode(_el$101, _el$102);
2927
- _$setProp(_el$101, "onMouseOver", () => showMore && setHoveredMoreBelow(true));
2928
- _$setProp(_el$101, "onMouseOut", () => setHoveredMoreBelow(false));
2929
- _$setProp(_el$101, "onMouseUp", () => {
4067
+ _$setProp(_el$101, "flexDirection", "row");
4068
+ _$insertNode(_el$102, _el$103);
4069
+ _$setProp(_el$102, "onMouseOver", () => showMore && setHoveredMoreBelow(true));
4070
+ _$setProp(_el$102, "onMouseOut", () => setHoveredMoreBelow(false));
4071
+ _$setProp(_el$102, "onMouseUp", () => {
2930
4072
  if (!showMore) return;
2931
4073
  const total = entryList().length;
2932
4074
  const m = max();
@@ -2938,20 +4080,20 @@ ${t("session.toast.copy_failed")}`, {
2938
4080
  }
2939
4081
  setHoveredMoreBelow(false);
2940
4082
  });
2941
- _$insert(_el$102, left);
2942
- _$insert(_el$100, showTop ? [(() => {
2943
- var _el$103 = _$createElement("text");
2944
- _$insert(_el$103, () => " ".repeat(pad));
2945
- _$effect((_$p) => _$setProp(_el$103, "style", {
4083
+ _$insert(_el$103, left);
4084
+ _$insert(_el$101, showTop ? [(() => {
4085
+ var _el$104 = _$createElement("text");
4086
+ _$insert(_el$104, () => " ".repeat(pad));
4087
+ _$effect((_$p) => _$setProp(_el$104, "style", {
2946
4088
  fg: pal().muted
2947
4089
  }, _$p));
2948
- return _el$103;
4090
+ return _el$104;
2949
4091
  })(), (() => {
2950
- var _el$104 = _$createElement("text"), _el$105 = _$createElement("span");
2951
- _$insertNode(_el$104, _el$105);
2952
- _$setProp(_el$104, "onMouseOver", () => setHoveredTop(true));
2953
- _$setProp(_el$104, "onMouseOut", () => setHoveredTop(false));
2954
- _$setProp(_el$104, "onMouseUp", () => {
4092
+ var _el$105 = _$createElement("text"), _el$106 = _$createElement("span");
4093
+ _$insertNode(_el$105, _el$106);
4094
+ _$setProp(_el$105, "onMouseOver", () => setHoveredTop(true));
4095
+ _$setProp(_el$105, "onMouseOut", () => setHoveredTop(false));
4096
+ _$setProp(_el$105, "onMouseUp", () => {
2955
4097
  const total = entryList().length;
2956
4098
  const m = max();
2957
4099
  if (props.sortOrder() === "desc") {
@@ -2961,16 +4103,16 @@ ${t("session.toast.copy_failed")}`, {
2961
4103
  }
2962
4104
  setHoveredTop(false);
2963
4105
  });
2964
- _$insert(_el$105, right);
2965
- _$effect((_$p) => _$setProp(_el$105, "style", {
4106
+ _$insert(_el$106, right);
4107
+ _$effect((_$p) => _$setProp(_el$106, "style", {
2966
4108
  fg: hoveredTop() ? pal().warning : pal().muted
2967
4109
  }, _$p));
2968
- return _el$104;
4110
+ return _el$105;
2969
4111
  })()] : null, null);
2970
- _$effect((_$p) => _$setProp(_el$102, "style", {
4112
+ _$effect((_$p) => _$setProp(_el$103, "style", {
2971
4113
  fg: showMore && hoveredMoreBelow() ? pal().warning : pal().muted
2972
4114
  }, _$p));
2973
- return _el$100;
4115
+ return _el$101;
2974
4116
  })();
2975
4117
  })();
2976
4118
  }
@@ -2998,11 +4140,14 @@ ${t("session.toast.copy_failed")}`, {
2998
4140
  }
2999
4141
 
3000
4142
  // src/v2/index.tsx
3001
- function PluginRoot(props) {
4143
+ function CommandRoot(props) {
3002
4144
  props.context.keymap.layer(() => ({
3003
4145
  mode: "global",
3004
4146
  commands: makeCommands(props.context, props.api, props.signals)
3005
4147
  }));
4148
+ return null;
4149
+ }
4150
+ function PluginRoot(props) {
3006
4151
  return _$createComponent2(SubAgentPanel, {
3007
4152
  get api() {
3008
4153
  return props.api;
@@ -3025,6 +4170,18 @@ function PluginRoot(props) {
3025
4170
  get borderVisible() {
3026
4171
  return props.signals.borderVisible;
3027
4172
  },
4173
+ get showEntryCost() {
4174
+ return props.signals.showEntryCost;
4175
+ },
4176
+ get showEntryTime() {
4177
+ return props.signals.showEntryTime;
4178
+ },
4179
+ get showEntryTokens() {
4180
+ return props.signals.showEntryTokens;
4181
+ },
4182
+ get timeFormat() {
4183
+ return props.signals.timeFormat;
4184
+ },
3028
4185
  get sessionId() {
3029
4186
  return props.sessionID;
3030
4187
  }
@@ -3050,6 +4207,12 @@ var mod = {
3050
4207
  const [orderSignal, setSortOrder] = createSignal3(String(api.kv.get(SETTING_KEYS.order, "desc")) === "asc" ? "asc" : "desc");
3051
4208
  const [scrollSignal, setScrollMode] = createSignal3(String(api.kv.get(SETTING_KEYS.scrollMode, "wheel")) === "click" ? "click" : "wheel");
3052
4209
  const [borderSignal, setBorderVisible] = createSignal3(api.kv.get(SETTING_KEYS.border, false) === true);
4210
+ const [showEntryCostSignal, setShowEntryCost] = createSignal3(api.kv.get(SETTING_KEYS.showEntryCost, false) === true);
4211
+ const [showEntryTimeSignal, setShowEntryTime] = createSignal3(api.kv.get(SETTING_KEYS.showEntryTime, true) !== false);
4212
+ const [showEntryTokensSignal, setShowEntryTokens] = createSignal3(api.kv.get(SETTING_KEYS.showEntryTokens, true) !== false);
4213
+ const storedTimeFormat = String(api.kv.get(SETTING_KEYS.timeFormat, "short"));
4214
+ const [timeFormatSignal, setTimeFormat] = createSignal3(TIME_FORMATS.includes(storedTimeFormat) ? storedTimeFormat : "short");
4215
+ const [dbSyncSignal, setDbSync] = createSignal3(api.kv.get(SETTING_KEYS.dbSync, true) !== false);
3053
4216
  lang = langSignal;
3054
4217
  maxEntries = maxSignal;
3055
4218
  sortOrder = orderSignal;
@@ -3065,8 +4228,26 @@ var mod = {
3065
4228
  setScrollMode,
3066
4229
  borderVisible: borderSignal,
3067
4230
  setBorderVisible,
4231
+ showEntryCost: showEntryCostSignal,
4232
+ setShowEntryCost,
4233
+ showEntryTime: showEntryTimeSignal,
4234
+ setShowEntryTime,
4235
+ showEntryTokens: showEntryTokensSignal,
4236
+ setShowEntryTokens,
4237
+ timeFormat: timeFormatSignal,
4238
+ setTimeFormat,
4239
+ dbSync: dbSyncSignal,
4240
+ setDbSync,
3068
4241
  sessionId: ""
3069
4242
  };
4243
+ context.ui.slot({
4244
+ append: "app",
4245
+ render: () => _$createComponent2(CommandRoot, {
4246
+ context,
4247
+ api,
4248
+ signals
4249
+ })
4250
+ });
3070
4251
  context.ui.slot({
3071
4252
  prepend: "sidebar.content",
3072
4253
  render: (props) => {