opencode-subagent-magazine 1.7.0 → 1.7.1

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.
@@ -121,6 +121,7 @@ export function makeCommands(context, api, signals) {
121
121
  `${t("settings.showEntryCost")}: ${onOff(signals.showEntryCost())}`,
122
122
  `${t("settings.showEntryTime")}: ${onOff(signals.showEntryTime())}`,
123
123
  `${t("settings.showEntryTokens")}: ${onOff(signals.showEntryTokens())}`,
124
+ `${t("settings.showOrigin")}: ${onOff(signals.showOrigin())}`,
124
125
  `${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[signals.timeFormat()]}`,
125
126
  `${t("settings.dbSync")}: ${onOff(signals.dbSync())}`,
126
127
  ].join("\n");
@@ -195,6 +196,19 @@ export function makeCommands(context, api, signals) {
195
196
  api.ui.toast(`${t("settings.showEntryTokens")}: ${onOff(v)}`);
196
197
  },
197
198
  },
199
+ {
200
+ id: "opencode-subagent-magazine.subagent.origin",
201
+ title: "SubAgent Magazine: Toggle Origin Indicators",
202
+ description: "Show or hide origin markers (⇢ background / ↳ spawned) in the sidebar list",
203
+ slash: { name: "subagent-origin" },
204
+ palette: true,
205
+ run: () => {
206
+ const v = !signals.showOrigin();
207
+ signals.setShowOrigin(v);
208
+ kv.set(SETTING_KEYS.showOrigin, v);
209
+ api.ui.toast(`${t("settings.showOrigin")}: ${onOff(v)}`);
210
+ },
211
+ },
198
212
  {
199
213
  id: "opencode-subagent-magazine.subagent.db",
200
214
  title: "SubAgent Magazine: Toggle Local DB",
package/dist/v2/db.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface ChildSessionInfo {
22
22
  timeCreated?: number;
23
23
  timeIdle?: number;
24
24
  idleOutcome?: string;
25
+ /** 仍在运行:最后一条消息晚于 time_idle(会话被恢复时 time_idle 不会被清空)。 */
26
+ active?: boolean;
25
27
  }
26
28
  export interface SessionDbIndex {
27
29
  /** 数据库可用且设置项开启时为 true。 */
@@ -32,6 +34,8 @@ export interface SessionDbIndex {
32
34
  matchChild(parentId: string, agent: string | undefined, startedAt: number | undefined): string | undefined;
33
35
  /** 子会话汇总(model/tokens/cost/time_idle)。 */
34
36
  info(sid: string): ChildSessionInfo | undefined;
37
+ /** 该会话派生的子会话(spawn),用于补全没有工具条目的衍生会话。 */
38
+ children(parentId: string): ChildSessionInfo[] | undefined;
35
39
  }
36
40
  /** 从宿主 model 字段提取模型 id。
37
41
  * 兼容三种形态:名字字符串、{ id } 对象,以及序列化后的 JSON 字符串
@@ -39,7 +43,16 @@ export interface SessionDbIndex {
39
43
  export declare function modelIdOf(value: unknown): string | undefined;
40
44
  /** 宿主 readSessionTokens 同语义:最后一条 assistant 消息的上下文规模。 */
41
45
  export declare function contextTokens(tokens: unknown): number | undefined;
42
- /** 子会话终态:time_idle 存在才算结束;interrupted 视为取消。
46
+ /** 子会话是否仍在运行。
47
+ * 恢复过的会话会保留旧的 time_idle(host 不清空),因此以
48
+ * 「最后一条消息是否晚于 time_idle」为准;最后一条是 idle
49
+ * 消息则视为已停止。 */
50
+ export declare function isSessionActive(input: {
51
+ timeIdle?: number;
52
+ lastMessageType?: string;
53
+ lastMessageAt?: number;
54
+ }): boolean;
55
+ /** 子会话终态:time_idle 存在且没有恢复活动才算结束;interrupted 视为取消。
43
56
  * 没有 time_idle 时不作判断(交给宿主实时状态)——避免时间阈值猜测。 */
44
57
  export declare function statusOfChild(info: ChildSessionInfo | undefined): "done" | "cancelled" | undefined;
45
58
  /** 定位宿主数据库(可用 OPENCODE_DB 覆盖,遵循 XDG_DATA_HOME)。 */
package/dist/v2/db.js CHANGED
@@ -58,10 +58,28 @@ export function contextTokens(tokens) {
58
58
  (Number(cache.write) || 0);
59
59
  return sum > 0 ? sum : undefined;
60
60
  }
61
- /** 子会话终态:time_idle 存在才算结束;interrupted 视为取消。
61
+ /** 子会话是否仍在运行。
62
+ * 恢复过的会话会保留旧的 time_idle(host 不清空),因此以
63
+ * 「最后一条消息是否晚于 time_idle」为准;最后一条是 idle
64
+ * 消息则视为已停止。 */
65
+ export function isSessionActive(input) {
66
+ const { timeIdle, lastMessageType, lastMessageAt } = input;
67
+ if (lastMessageType === "idle")
68
+ return false;
69
+ if (lastMessageAt === undefined)
70
+ return false;
71
+ if (timeIdle === undefined)
72
+ return true;
73
+ return lastMessageAt > timeIdle;
74
+ }
75
+ /** 子会话终态:time_idle 存在且没有恢复活动才算结束;interrupted 视为取消。
62
76
  * 没有 time_idle 时不作判断(交给宿主实时状态)——避免时间阈值猜测。 */
63
77
  export function statusOfChild(info) {
64
- if (!info || info.timeIdle == null)
78
+ if (!info)
79
+ return undefined;
80
+ if (info.active === true)
81
+ return undefined;
82
+ if (info.timeIdle == null)
65
83
  return undefined;
66
84
  return info.idleOutcome === "interrupted" ? "cancelled" : "done";
67
85
  }
@@ -104,11 +122,19 @@ const LAST_ASSISTANT_SQL = `
104
122
  FROM session_message
105
123
  WHERE session_id = ? AND type = 'assistant' AND json_extract(data, '$.tokens.output') > 0
106
124
  ORDER BY seq DESC LIMIT 1`;
125
+ const LAST_MESSAGE_SQL = `
126
+ SELECT type, COALESCE(time_updated, time_created) AS at
127
+ FROM session_message WHERE session_id = ? ORDER BY seq DESC LIMIT 1`;
107
128
  const ASSISTANT_COST_SQL = `
108
129
  SELECT COUNT(*) AS n, SUM(COALESCE(json_extract(data, '$.cost'), 0)) AS total
109
130
  FROM session_message WHERE session_id = ? AND type = 'assistant'`;
110
131
  const CHILDREN_SQL = `
111
- SELECT id, agent, time_created FROM session_v2 WHERE parent_id = ? ORDER BY time_created`;
132
+ SELECT id, agent, title, model, cost,
133
+ tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
134
+ time_created, time_idle, idle_outcome
135
+ FROM session_v2
136
+ WHERE parent_id = ? AND (fork_session_id IS NULL OR fork_session_id = '')
137
+ ORDER BY time_created`;
112
138
  const num = (v) => {
113
139
  const n = Number(v);
114
140
  return Number.isFinite(n) ? n : undefined;
@@ -225,6 +251,7 @@ export function createSessionDbIndex(enabled) {
225
251
  const row = db.query(CHILD_INFO_SQL).get(sid);
226
252
  if (row) {
227
253
  const last = db.query(LAST_ASSISTANT_SQL).get(sid);
254
+ const lastMsg = db.query(LAST_MESSAGE_SQL).get(sid);
228
255
  let tokens = contextTokens(last ? {
229
256
  input: last.input, output: last.output, reasoning: last.reasoning,
230
257
  cache: { read: last.cache_read, write: last.cache_write },
@@ -254,6 +281,11 @@ export function createSessionDbIndex(enabled) {
254
281
  timeCreated: num(row.time_created),
255
282
  timeIdle: num(row.time_idle),
256
283
  idleOutcome: str(row.idle_outcome),
284
+ active: isSessionActive({
285
+ timeIdle: num(row.time_idle),
286
+ lastMessageType: str(lastMsg?.type),
287
+ lastMessageAt: num(lastMsg?.at),
288
+ }),
257
289
  };
258
290
  }
259
291
  }
@@ -263,5 +295,47 @@ export function createSessionDbIndex(enabled) {
263
295
  infos.set(sid, { info: result, at: Date.now() });
264
296
  return result;
265
297
  };
266
- return { enabled: ready, resolveCall, matchChild, info };
298
+ const children = (parentId) => {
299
+ if (!ready() || !parentId)
300
+ return undefined;
301
+ const now = Date.now();
302
+ let cached = childrenCache.get(parentId);
303
+ if (!cached || now - cached.at > CALL_MAP_TTL_MS) {
304
+ let list = [];
305
+ try {
306
+ list = db.query(CHILDREN_SQL).all(parentId);
307
+ }
308
+ catch { }
309
+ cached = { list, at: now };
310
+ childrenCache.set(parentId, cached);
311
+ }
312
+ const out = [];
313
+ for (const row of cached.list) {
314
+ const sid = str(row.id);
315
+ if (!sid)
316
+ continue;
317
+ const full = info(sid);
318
+ if (full) {
319
+ out.push(full);
320
+ continue;
321
+ }
322
+ // info 缓存未命中时不丢字段:用列表行自身的汇总兜底。
323
+ const agg = (num(row.tokens_input) || 0) + (num(row.tokens_output) || 0) + (num(row.tokens_reasoning) || 0) +
324
+ (num(row.tokens_cache_read) || 0) + (num(row.tokens_cache_write) || 0);
325
+ out.push({
326
+ id: sid,
327
+ parentId,
328
+ agent: str(row.agent),
329
+ title: str(row.title),
330
+ model: modelIdOf(row.model),
331
+ cost: num(row.cost),
332
+ tokens: agg > 0 ? agg : undefined,
333
+ timeCreated: num(row.time_created),
334
+ timeIdle: num(row.time_idle),
335
+ idleOutcome: str(row.idle_outcome),
336
+ });
337
+ }
338
+ return out;
339
+ };
340
+ return { enabled: ready, resolveCall, matchChild, info, children };
267
341
  }
package/dist/v2/index.js CHANGED
@@ -18,7 +18,7 @@ function CommandRoot(props) {
18
18
  }
19
19
  /** 面板根组件:渲染共享 SubAgentPanel(命令层见 CommandRoot)。 */
20
20
  function PluginRoot(props) {
21
- return (_jsx(SubAgentPanel, { api: props.api, theme: mapTheme(props.context.theme), lang: props.signals.lang, maxEntries: props.signals.maxEntries, sortOrder: props.signals.sortOrder, scrollMode: props.signals.scrollMode, borderVisible: props.signals.borderVisible, showEntryCost: props.signals.showEntryCost, showEntryTime: props.signals.showEntryTime, showEntryTokens: props.signals.showEntryTokens, timeFormat: props.signals.timeFormat, sessionId: props.sessionID }));
21
+ return (_jsx(SubAgentPanel, { api: props.api, theme: mapTheme(props.context.theme), lang: props.signals.lang, maxEntries: props.signals.maxEntries, sortOrder: props.signals.sortOrder, scrollMode: props.signals.scrollMode, borderVisible: props.signals.borderVisible, showEntryCost: props.signals.showEntryCost, showEntryTime: props.signals.showEntryTime, showEntryTokens: props.signals.showEntryTokens, timeFormat: props.signals.timeFormat, showOrigin: props.signals.showOrigin, sessionId: props.sessionID }));
22
22
  }
23
23
  /** V2 入口:setup 创建共享信号 + PanelApi + 命令 layer + 侧边栏槽位。
24
24
  * 行为对齐 V1 tui()(信号初始值从 KV 恢复;侧边栏渲染共享 SubAgentPanel)。 */
@@ -49,6 +49,7 @@ const mod = {
49
49
  const [showEntryTokensSignal, setShowEntryTokens] = createSignal(api.kv.get(SETTING_KEYS.showEntryTokens, true) !== false);
50
50
  const storedTimeFormat = String(api.kv.get(SETTING_KEYS.timeFormat, "short"));
51
51
  const [timeFormatSignal, setTimeFormat] = createSignal(TIME_FORMATS.includes(storedTimeFormat) ? storedTimeFormat : "short");
52
+ const [showOriginSignal, setShowOrigin] = createSignal(api.kv.get(SETTING_KEYS.showOrigin, false) === true);
52
53
  const [dbSyncSignal, setDbSync] = createSignal(api.kv.get(SETTING_KEYS.dbSync, true) !== false);
53
54
  lang = langSignal;
54
55
  maxEntries = maxSignal;
@@ -60,6 +61,7 @@ const mod = {
60
61
  showEntryTime: showEntryTimeSignal, setShowEntryTime,
61
62
  showEntryTokens: showEntryTokensSignal, setShowEntryTokens,
62
63
  timeFormat: timeFormatSignal, setTimeFormat,
64
+ showOrigin: showOriginSignal, setShowOrigin,
63
65
  dbSync: dbSyncSignal, setDbSync,
64
66
  sessionId: "",
65
67
  };
@@ -43,6 +43,7 @@ async function runSettingsMenu(context, api, signals) {
43
43
  { title: t("settings.showEntryTime"), value: "time", description: onOff(signals.showEntryTime()), category },
44
44
  { title: t("settings.timeFormat"), value: "timefmt", description: TIME_FORMAT_SAMPLES[signals.timeFormat()], category },
45
45
  { title: t("settings.showEntryTokens"), value: "tokens", description: onOff(signals.showEntryTokens()), category },
46
+ { title: t("settings.showOrigin"), value: "origin", description: onOff(signals.showOrigin()), category },
46
47
  { title: t("settings.dbSync"), value: "dbsync", description: onOff(signals.dbSync()), category },
47
48
  { title: t("settings.close"), value: "close" },
48
49
  ],
@@ -203,6 +204,22 @@ async function runSettingsMenu(context, api, signals) {
203
204
  toast(t("settings.showEntryTokens"), onOff(picked));
204
205
  }
205
206
  }
207
+ // ── Origin indicators ──
208
+ if (choice === "origin") {
209
+ const picked = await context.ui.dialog.select({
210
+ title: t("settings.showOrigin"),
211
+ options: [
212
+ { title: t("settings.on"), value: true },
213
+ { title: t("settings.off"), value: false },
214
+ ],
215
+ current: signals.showOrigin(),
216
+ });
217
+ if (picked !== undefined) {
218
+ signals.setShowOrigin(picked);
219
+ kv.set(SETTING_KEYS.showOrigin, picked);
220
+ toast(t("settings.showOrigin"), onOff(picked));
221
+ }
222
+ }
206
223
  // ── 本地数据库补全 ──
207
224
  if (choice === "dbsync") {
208
225
  const picked = await context.ui.dialog.select({
@@ -502,9 +502,12 @@ export function createPanelApi(context, settings) {
502
502
  }
503
503
  catch { }
504
504
  // 宿主没有该子会话的实时状态(未加载缓存)时用本地库终态兜底:
505
- // time_idle 存在即已结束;不存在则不猜测(交给事件/扫描)。
505
+ // time_idle 存在且没有恢复活动即已结束;仍在运行则回报 busy。
506
506
  try {
507
- if (statusOfChild(dbIndex?.info(sid)) !== undefined)
507
+ const info = dbIndex?.info(sid);
508
+ if (info?.active === true)
509
+ return { type: "busy" };
510
+ if (statusOfChild(info) !== undefined)
508
511
  return { type: "idle" };
509
512
  }
510
513
  catch { }
@@ -539,6 +542,14 @@ export function createPanelApi(context, settings) {
539
542
  return undefined;
540
543
  }
541
544
  },
545
+ listChildren: (parentId) => {
546
+ try {
547
+ return dbIndex?.enabled() ? dbIndex.children(parentId) : undefined;
548
+ }
549
+ catch {
550
+ return undefined;
551
+ }
552
+ },
542
553
  },
543
554
  event: {
544
555
  on: (type, cb) => {