opencode-subagent-magazine 1.6.1 → 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.
- package/README.md +12 -1
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/format.d.ts +7 -0
- package/dist/core/format.js +45 -0
- package/dist/core/kv.d.ts +22 -1
- package/dist/core/kv.js +34 -1
- package/dist/core/types.d.ts +20 -0
- package/dist/core/types.js +7 -0
- package/dist/i18n.d.ts +24 -0
- package/dist/i18n.js +96 -0
- package/dist/index.js +90 -2
- package/dist/panel/SubAgentPanel.d.ts +6 -1
- package/dist/panel/SubAgentPanel.js +645 -266
- package/dist/panel/entry-map.d.ts +17 -0
- package/dist/panel/entry-map.js +75 -1
- package/dist/panel/panel-api.d.ts +24 -0
- package/dist/tui.js +1230 -567
- package/dist/v2/commands.js +202 -44
- package/dist/v2/db.d.ts +60 -0
- package/dist/v2/db.js +341 -0
- package/dist/v2/index.js +28 -6
- package/dist/v2/settings-menu.d.ts +9 -0
- package/dist/v2/settings-menu.js +242 -0
- package/dist/v2/v2-panel-api.js +315 -44
- package/dist/v2.js +2139 -708
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/core/format.ts +44 -0
- package/src/core/kv.ts +43 -2
- package/src/core/types.ts +25 -0
- package/src/i18n.ts +96 -0
- package/src/index.tsx +113 -2
- package/src/panel/SubAgentPanel.tsx +621 -238
- package/src/panel/entry-map.ts +79 -2
- package/src/panel/panel-api.ts +25 -0
- package/src/v2/commands.ts +201 -43
- package/src/v2/db.ts +346 -0
- package/src/v2/index.tsx +54 -7
- package/src/v2/settings-menu.ts +259 -0
- package/src/v2/types.ts +3 -1
- package/src/v2/v2-panel-api.ts +246 -33
package/dist/v2/commands.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TIME_FORMATS, TIME_FORMAT_SAMPLES } from "../core/format";
|
|
2
|
+
import { KV_PREFIX, SETTING_KEYS, updateSessionData, readTTLDays } from "../core/kv";
|
|
2
3
|
import { PLUGIN_VERSION } from "../_version";
|
|
3
4
|
import { LANG_META, createT } from "../i18n";
|
|
4
5
|
import { globalEntryCache, setClearTick } from "../panel/store";
|
|
6
|
+
import { mergeSubEntries } from "../panel/entry-map";
|
|
7
|
+
import { openSettingsMenu } from "./settings-menu";
|
|
5
8
|
/** V2 命令(对齐 V1 的 9 个斜杠命令——promise 式对话框)。 */
|
|
6
9
|
export function makeCommands(context, api, signals) {
|
|
7
10
|
const t = createT(() => signals.lang());
|
|
8
11
|
const kv = api.kv;
|
|
9
12
|
const clampMax = (n) => Math.max(1, Math.min(50, n));
|
|
13
|
+
const onOff = (v) => (v ? t("settings.on") : t("settings.off"));
|
|
10
14
|
const resolveParent = (sid) => {
|
|
11
15
|
try {
|
|
12
16
|
const session = api.session.get(sid);
|
|
@@ -98,6 +102,126 @@ export function makeCommands(context, api, signals) {
|
|
|
98
102
|
api.ui.toast(`Max entries: ${n}`);
|
|
99
103
|
},
|
|
100
104
|
},
|
|
105
|
+
{
|
|
106
|
+
id: "opencode-subagent-magazine.subagent.config",
|
|
107
|
+
title: "SubAgent Magazine: Settings",
|
|
108
|
+
description: "Show current settings (change each via its own command)",
|
|
109
|
+
slash: { name: "subagent-config" },
|
|
110
|
+
palette: true,
|
|
111
|
+
run: () => {
|
|
112
|
+
const ttl = readTTLDays(kv);
|
|
113
|
+
const langLabel = LANG_META.find((m) => m.code === signals.lang())?.label ?? signals.lang();
|
|
114
|
+
const message = [
|
|
115
|
+
`${t("settings.lang")}: ${langLabel}`,
|
|
116
|
+
`${t("settings.max")}: ${signals.maxEntries()}`,
|
|
117
|
+
`${t("settings.order")}: ${signals.sortOrder() === "desc" ? t("order.desc") : t("order.asc")}`,
|
|
118
|
+
`${t("settings.scroll")}: ${signals.scrollMode() === "wheel" ? t("scroll.wheel") : t("scroll.click")}`,
|
|
119
|
+
`${t("settings.ttl")}: ${ttl === 0 ? t("ttl.unlimited") : `${ttl}d`}`,
|
|
120
|
+
`${t("settings.border")}: ${onOff(signals.borderVisible())}`,
|
|
121
|
+
`${t("settings.showEntryCost")}: ${onOff(signals.showEntryCost())}`,
|
|
122
|
+
`${t("settings.showEntryTime")}: ${onOff(signals.showEntryTime())}`,
|
|
123
|
+
`${t("settings.showEntryTokens")}: ${onOff(signals.showEntryTokens())}`,
|
|
124
|
+
`${t("settings.showOrigin")}: ${onOff(signals.showOrigin())}`,
|
|
125
|
+
`${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[signals.timeFormat()]}`,
|
|
126
|
+
`${t("settings.dbSync")}: ${onOff(signals.dbSync())}`,
|
|
127
|
+
].join("\n");
|
|
128
|
+
context.ui.toast.show({ title: t("settings.title"), message });
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "opencode-subagent-magazine.subagent.sections",
|
|
133
|
+
title: "SubAgent Magazine: Settings Menu",
|
|
134
|
+
description: "Open the interactive settings menu (Esc to close)",
|
|
135
|
+
slash: { name: "subagent-sections" },
|
|
136
|
+
palette: true,
|
|
137
|
+
run: () => {
|
|
138
|
+
openSettingsMenu(context, api, signals);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "opencode-subagent-magazine.subagent.cost",
|
|
143
|
+
title: "SubAgent Magazine: Toggle Entry Cost",
|
|
144
|
+
description: "Show or hide the per-sub-agent cost in the sidebar",
|
|
145
|
+
slash: { name: "subagent-cost" },
|
|
146
|
+
palette: true,
|
|
147
|
+
run: () => {
|
|
148
|
+
const v = !signals.showEntryCost();
|
|
149
|
+
signals.setShowEntryCost(v);
|
|
150
|
+
kv.set(SETTING_KEYS.showEntryCost, v);
|
|
151
|
+
api.ui.toast(`${t("settings.showEntryCost")}: ${onOff(v)}`);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "opencode-subagent-magazine.subagent.time",
|
|
156
|
+
title: "SubAgent Magazine: Toggle Entry Time",
|
|
157
|
+
description: "Show or hide the elapsed time in the sidebar list",
|
|
158
|
+
slash: { name: "subagent-time" },
|
|
159
|
+
palette: true,
|
|
160
|
+
run: () => {
|
|
161
|
+
const v = !signals.showEntryTime();
|
|
162
|
+
signals.setShowEntryTime(v);
|
|
163
|
+
kv.set(SETTING_KEYS.showEntryTime, v);
|
|
164
|
+
api.ui.toast(`${t("settings.showEntryTime")}: ${onOff(v)}`);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "opencode-subagent-magazine.subagent.time-format",
|
|
169
|
+
title: "SubAgent Magazine: Time Format",
|
|
170
|
+
description: "Set how elapsed time is displayed (short / decimal / clock / compact / seconds)",
|
|
171
|
+
slash: { name: "subagent-time-format" },
|
|
172
|
+
palette: true,
|
|
173
|
+
run: async () => {
|
|
174
|
+
const picked = await context.ui.dialog.select({
|
|
175
|
+
title: t("settings.timeFormat"),
|
|
176
|
+
options: TIME_FORMATS.map((f) => ({ title: `${f} — ${TIME_FORMAT_SAMPLES[f]}`, value: f })),
|
|
177
|
+
current: signals.timeFormat(),
|
|
178
|
+
});
|
|
179
|
+
if (!picked)
|
|
180
|
+
return;
|
|
181
|
+
signals.setTimeFormat(picked);
|
|
182
|
+
kv.set(SETTING_KEYS.timeFormat, picked);
|
|
183
|
+
api.ui.toast(`${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[picked]}`);
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "opencode-subagent-magazine.subagent.tokens",
|
|
188
|
+
title: "SubAgent Magazine: Toggle Entry Tokens",
|
|
189
|
+
description: "Show or hide the token count in the sidebar list",
|
|
190
|
+
slash: { name: "subagent-tokens" },
|
|
191
|
+
palette: true,
|
|
192
|
+
run: () => {
|
|
193
|
+
const v = !signals.showEntryTokens();
|
|
194
|
+
signals.setShowEntryTokens(v);
|
|
195
|
+
kv.set(SETTING_KEYS.showEntryTokens, v);
|
|
196
|
+
api.ui.toast(`${t("settings.showEntryTokens")}: ${onOff(v)}`);
|
|
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
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "opencode-subagent-magazine.subagent.db",
|
|
214
|
+
title: "SubAgent Magazine: Toggle Local DB",
|
|
215
|
+
description: "Enrich sub-agent entries from the local OpenCode database (V2)",
|
|
216
|
+
slash: { name: "subagent-db" },
|
|
217
|
+
palette: true,
|
|
218
|
+
run: () => {
|
|
219
|
+
const v = !signals.dbSync();
|
|
220
|
+
signals.setDbSync(v);
|
|
221
|
+
kv.set(SETTING_KEYS.dbSync, v);
|
|
222
|
+
api.ui.toast(`${t("settings.dbSync")}: ${onOff(v)}`);
|
|
223
|
+
},
|
|
224
|
+
},
|
|
101
225
|
{
|
|
102
226
|
id: "opencode-subagent-magazine.subagent.version",
|
|
103
227
|
title: "SubAgent Magazine: Version",
|
|
@@ -140,33 +264,53 @@ export function makeCommands(context, api, signals) {
|
|
|
140
264
|
}
|
|
141
265
|
}
|
|
142
266
|
if (count > 0) {
|
|
267
|
+
const ok = () => api.ui.toast(signals.lang() === "zh"
|
|
268
|
+
? `已标记 ${count} 个运行中的条目为完成`
|
|
269
|
+
: `Marked ${count} running entries as done`);
|
|
270
|
+
const fail = () => {
|
|
271
|
+
try {
|
|
272
|
+
api.ui.toast(t("clear.failed"));
|
|
273
|
+
}
|
|
274
|
+
catch { }
|
|
275
|
+
};
|
|
143
276
|
try {
|
|
144
|
-
const data = loadSessionData(kv);
|
|
145
277
|
const { parentSid, isChild } = resolveParent(sid);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
278
|
+
const done = updateSessionData(kv, (data) => {
|
|
279
|
+
// 落定前先把(可能过期的)模块缓存与持久化记录
|
|
280
|
+
// merge:其他实例可能有本实例从未见过的条目,
|
|
281
|
+
// 这次写入不能把它们丢掉。
|
|
282
|
+
if (isChild) {
|
|
283
|
+
if (!data[parentSid])
|
|
284
|
+
data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
|
|
285
|
+
if (!data[parentSid].children)
|
|
286
|
+
data[parentSid].children = {};
|
|
287
|
+
const prev = data[parentSid].children[sid]?.entries ?? [];
|
|
288
|
+
const merged = [...mergeSubEntries(prev, entries.values()).values()].map((e) => e.status === "running" || e.status === "cancel_requested"
|
|
289
|
+
? { ...e, status: "done", endedAt: e.endedAt ?? Date.now() }
|
|
290
|
+
: e);
|
|
291
|
+
data[parentSid].children[sid] = {
|
|
292
|
+
...(data[parentSid].children[sid] ?? { scroll: 0, expanded: "" }),
|
|
293
|
+
entries: merged,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
const rec = data[parentSid] ?? { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
|
|
298
|
+
const merged = [...mergeSubEntries(rec.entries ?? [], entries.values()).values()].map((e) => e.status === "running" || e.status === "cancel_requested"
|
|
299
|
+
? { ...e, status: "done", endedAt: e.endedAt ?? Date.now() }
|
|
300
|
+
: e);
|
|
301
|
+
data[parentSid] = { ...rec, ts: Date.now(), entries: merged, children: rec.children ?? {} };
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
if (done && typeof done.then === "function") {
|
|
305
|
+
void done.then(ok, fail);
|
|
154
306
|
}
|
|
155
307
|
else {
|
|
156
|
-
|
|
157
|
-
ts: Date.now(),
|
|
158
|
-
entries: [...entries.values()],
|
|
159
|
-
scroll: data[sid]?.scroll ?? 0,
|
|
160
|
-
expanded: data[sid]?.expanded ?? "",
|
|
161
|
-
children: data[sid]?.children ?? {},
|
|
162
|
-
};
|
|
308
|
+
ok();
|
|
163
309
|
}
|
|
164
|
-
saveSessionData(kv, data);
|
|
165
310
|
}
|
|
166
|
-
catch {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
: `Marked ${count} running entries as done`);
|
|
311
|
+
catch {
|
|
312
|
+
fail();
|
|
313
|
+
}
|
|
170
314
|
}
|
|
171
315
|
else {
|
|
172
316
|
api.ui.toast(signals.lang() === "zh" ? "没有需要清理的运行中条目" : "No running entries to clear");
|
|
@@ -227,33 +371,47 @@ export function makeCommands(context, api, signals) {
|
|
|
227
371
|
if (choice !== "yes")
|
|
228
372
|
return;
|
|
229
373
|
try {
|
|
230
|
-
const data = loadSessionData(kv);
|
|
231
374
|
let count = 0;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
child
|
|
240
|
-
|
|
375
|
+
const finish = () => {
|
|
376
|
+
globalEntryCache.delete(sid);
|
|
377
|
+
setClearTick((v) => v + 1);
|
|
378
|
+
api.ui.toast(t("clear.done", { n: count }));
|
|
379
|
+
};
|
|
380
|
+
const done = updateSessionData(kv, (data) => {
|
|
381
|
+
if (parentID) {
|
|
382
|
+
const child = data[parentID]?.children?.[sid];
|
|
383
|
+
if (child) {
|
|
384
|
+
const ids = child.entries?.map((e) => e.id) ?? [];
|
|
385
|
+
count = ids.length;
|
|
386
|
+
child.entries = [];
|
|
387
|
+
child.scroll = 0;
|
|
388
|
+
child.expanded = "";
|
|
389
|
+
child.clearedIds = [...new Set([...(child.clearedIds ?? []), ...ids])];
|
|
390
|
+
}
|
|
241
391
|
}
|
|
392
|
+
else {
|
|
393
|
+
const rec = data[sid];
|
|
394
|
+
if (rec) {
|
|
395
|
+
const ids = rec.entries?.map((e) => e.id) ?? [];
|
|
396
|
+
count = ids.length;
|
|
397
|
+
rec.entries = [];
|
|
398
|
+
rec.scroll = 0;
|
|
399
|
+
rec.expanded = "";
|
|
400
|
+
rec.clearedIds = [...new Set([...(rec.clearedIds ?? []), ...ids])];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
if (done && typeof done.then === "function") {
|
|
405
|
+
void done.then(finish, () => {
|
|
406
|
+
try {
|
|
407
|
+
api.ui.toast(t("clear.failed"));
|
|
408
|
+
}
|
|
409
|
+
catch { }
|
|
410
|
+
});
|
|
242
411
|
}
|
|
243
412
|
else {
|
|
244
|
-
|
|
245
|
-
if (data[sid]) {
|
|
246
|
-
const ids = data[sid].entries?.map((e) => e.id) ?? [];
|
|
247
|
-
data[sid].entries = [];
|
|
248
|
-
data[sid].scroll = 0;
|
|
249
|
-
data[sid].expanded = "";
|
|
250
|
-
data[sid].clearedIds = [...new Set([...(data[sid].clearedIds ?? []), ...ids])];
|
|
251
|
-
}
|
|
413
|
+
finish();
|
|
252
414
|
}
|
|
253
|
-
saveSessionData(kv, data);
|
|
254
|
-
globalEntryCache.delete(sid);
|
|
255
|
-
setClearTick((v) => v + 1);
|
|
256
|
-
api.ui.toast(t("clear.done", { n: count }));
|
|
257
415
|
}
|
|
258
416
|
catch { }
|
|
259
417
|
},
|
package/dist/v2/db.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 opencode.db 只读索引(V2)。
|
|
3
|
+
*
|
|
4
|
+
* 宿主数据层只为当前 TUI 已加载的会话缓存消息:前台子代理(非 background)
|
|
5
|
+
* 的 success 事件经常拿不到 metadata.sessionID,条目会一直停在 running,
|
|
6
|
+
* model/tokens/cost 也无法回填。这些数据在宿主数据库里是完整的:
|
|
7
|
+
* - 父会话 assistant 消息中,工具 part 的 state.metadata.sessionID 指向子会话(前台/后台都有);
|
|
8
|
+
* - session_v2 保存子会话的 agent/model/tokens/cost 与 time_idle/idle_outcome。
|
|
9
|
+
*
|
|
10
|
+
* 本模块以只读方式查询该库作为补充数据源;bun:sqlite 不可用、库不存在或
|
|
11
|
+
* 设置项 dbSync 关闭时静默禁用(回退宿主 API)。库结构属于宿主内部实现,
|
|
12
|
+
* 任何读取失败都返回 undefined,绝不影响面板其余功能。
|
|
13
|
+
*/
|
|
14
|
+
export interface ChildSessionInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
parentId?: string;
|
|
17
|
+
agent?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
cost?: number;
|
|
21
|
+
tokens?: number;
|
|
22
|
+
timeCreated?: number;
|
|
23
|
+
timeIdle?: number;
|
|
24
|
+
idleOutcome?: string;
|
|
25
|
+
/** 仍在运行:最后一条消息晚于 time_idle(会话被恢复时 time_idle 不会被清空)。 */
|
|
26
|
+
active?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface SessionDbIndex {
|
|
29
|
+
/** 数据库可用且设置项开启时为 true。 */
|
|
30
|
+
enabled(): boolean;
|
|
31
|
+
/** call id → 子会话 id(前台/后台通用)。 */
|
|
32
|
+
resolveCall(parentId: string, callId: string): string | undefined;
|
|
33
|
+
/** 无 call id 时的兜底:按 agent + 起始时间匹配子会话。 */
|
|
34
|
+
matchChild(parentId: string, agent: string | undefined, startedAt: number | undefined): string | undefined;
|
|
35
|
+
/** 子会话汇总(model/tokens/cost/time_idle)。 */
|
|
36
|
+
info(sid: string): ChildSessionInfo | undefined;
|
|
37
|
+
/** 该会话派生的子会话(spawn),用于补全没有工具条目的衍生会话。 */
|
|
38
|
+
children(parentId: string): ChildSessionInfo[] | undefined;
|
|
39
|
+
}
|
|
40
|
+
/** 从宿主 model 字段提取模型 id。
|
|
41
|
+
* 兼容三种形态:名字字符串、{ id } 对象,以及序列化后的 JSON 字符串
|
|
42
|
+
* (session_v2.model 列存的就是 `{"id":...}` 文本)。 */
|
|
43
|
+
export declare function modelIdOf(value: unknown): string | undefined;
|
|
44
|
+
/** 宿主 readSessionTokens 同语义:最后一条 assistant 消息的上下文规模。 */
|
|
45
|
+
export declare function contextTokens(tokens: unknown): number | undefined;
|
|
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 视为取消。
|
|
56
|
+
* 没有 time_idle 时不作判断(交给宿主实时状态)——避免时间阈值猜测。 */
|
|
57
|
+
export declare function statusOfChild(info: ChildSessionInfo | undefined): "done" | "cancelled" | undefined;
|
|
58
|
+
/** 定位宿主数据库(可用 OPENCODE_DB 覆盖,遵循 XDG_DATA_HOME)。 */
|
|
59
|
+
export declare function findDbPath(env?: Record<string, string | undefined>, home?: string): string | undefined;
|
|
60
|
+
export declare function createSessionDbIndex(enabled: () => boolean): SessionDbIndex | undefined;
|