opencode-subagent-magazine 1.6.1 → 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.
- 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 +21 -1
- package/dist/core/kv.js +33 -1
- package/dist/core/types.d.ts +12 -0
- package/dist/i18n.d.ts +19 -0
- package/dist/i18n.js +76 -0
- package/dist/index.js +73 -2
- package/dist/panel/SubAgentPanel.d.ts +5 -1
- package/dist/panel/SubAgentPanel.js +568 -261
- package/dist/panel/entry-map.d.ts +16 -0
- package/dist/panel/entry-map.js +74 -0
- package/dist/panel/panel-api.d.ts +8 -0
- package/dist/tui.js +927 -431
- package/dist/v2/commands.js +188 -44
- package/dist/v2/db.d.ts +47 -0
- package/dist/v2/db.js +267 -0
- package/dist/v2/index.js +26 -6
- package/dist/v2/settings-menu.d.ts +9 -0
- package/dist/v2/settings-menu.js +225 -0
- package/dist/v2/v2-panel-api.js +304 -44
- package/dist/v2.js +1741 -560
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/core/format.ts +44 -0
- package/src/core/kv.ts +42 -2
- package/src/core/types.ts +12 -0
- package/src/i18n.ts +76 -0
- package/src/index.tsx +93 -2
- package/src/panel/SubAgentPanel.tsx +541 -233
- package/src/panel/entry-map.ts +77 -1
- package/src/panel/panel-api.ts +8 -0
- package/src/v2/commands.ts +187 -43
- package/src/v2/db.ts +274 -0
- package/src/v2/index.tsx +49 -7
- package/src/v2/settings-menu.ts +241 -0
- package/src/v2/types.ts +3 -1
- package/src/v2/v2-panel-api.ts +241 -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,112 @@ 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.timeFormat")}: ${TIME_FORMAT_SAMPLES[signals.timeFormat()]}`,
|
|
125
|
+
`${t("settings.dbSync")}: ${onOff(signals.dbSync())}`,
|
|
126
|
+
].join("\n");
|
|
127
|
+
context.ui.toast.show({ title: t("settings.title"), message });
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "opencode-subagent-magazine.subagent.sections",
|
|
132
|
+
title: "SubAgent Magazine: Settings Menu",
|
|
133
|
+
description: "Open the interactive settings menu (Esc to close)",
|
|
134
|
+
slash: { name: "subagent-sections" },
|
|
135
|
+
palette: true,
|
|
136
|
+
run: () => {
|
|
137
|
+
openSettingsMenu(context, api, signals);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "opencode-subagent-magazine.subagent.cost",
|
|
142
|
+
title: "SubAgent Magazine: Toggle Entry Cost",
|
|
143
|
+
description: "Show or hide the per-sub-agent cost in the sidebar",
|
|
144
|
+
slash: { name: "subagent-cost" },
|
|
145
|
+
palette: true,
|
|
146
|
+
run: () => {
|
|
147
|
+
const v = !signals.showEntryCost();
|
|
148
|
+
signals.setShowEntryCost(v);
|
|
149
|
+
kv.set(SETTING_KEYS.showEntryCost, v);
|
|
150
|
+
api.ui.toast(`${t("settings.showEntryCost")}: ${onOff(v)}`);
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "opencode-subagent-magazine.subagent.time",
|
|
155
|
+
title: "SubAgent Magazine: Toggle Entry Time",
|
|
156
|
+
description: "Show or hide the elapsed time in the sidebar list",
|
|
157
|
+
slash: { name: "subagent-time" },
|
|
158
|
+
palette: true,
|
|
159
|
+
run: () => {
|
|
160
|
+
const v = !signals.showEntryTime();
|
|
161
|
+
signals.setShowEntryTime(v);
|
|
162
|
+
kv.set(SETTING_KEYS.showEntryTime, v);
|
|
163
|
+
api.ui.toast(`${t("settings.showEntryTime")}: ${onOff(v)}`);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "opencode-subagent-magazine.subagent.time-format",
|
|
168
|
+
title: "SubAgent Magazine: Time Format",
|
|
169
|
+
description: "Set how elapsed time is displayed (short / decimal / clock / compact / seconds)",
|
|
170
|
+
slash: { name: "subagent-time-format" },
|
|
171
|
+
palette: true,
|
|
172
|
+
run: async () => {
|
|
173
|
+
const picked = await context.ui.dialog.select({
|
|
174
|
+
title: t("settings.timeFormat"),
|
|
175
|
+
options: TIME_FORMATS.map((f) => ({ title: `${f} — ${TIME_FORMAT_SAMPLES[f]}`, value: f })),
|
|
176
|
+
current: signals.timeFormat(),
|
|
177
|
+
});
|
|
178
|
+
if (!picked)
|
|
179
|
+
return;
|
|
180
|
+
signals.setTimeFormat(picked);
|
|
181
|
+
kv.set(SETTING_KEYS.timeFormat, picked);
|
|
182
|
+
api.ui.toast(`${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[picked]}`);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "opencode-subagent-magazine.subagent.tokens",
|
|
187
|
+
title: "SubAgent Magazine: Toggle Entry Tokens",
|
|
188
|
+
description: "Show or hide the token count in the sidebar list",
|
|
189
|
+
slash: { name: "subagent-tokens" },
|
|
190
|
+
palette: true,
|
|
191
|
+
run: () => {
|
|
192
|
+
const v = !signals.showEntryTokens();
|
|
193
|
+
signals.setShowEntryTokens(v);
|
|
194
|
+
kv.set(SETTING_KEYS.showEntryTokens, v);
|
|
195
|
+
api.ui.toast(`${t("settings.showEntryTokens")}: ${onOff(v)}`);
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "opencode-subagent-magazine.subagent.db",
|
|
200
|
+
title: "SubAgent Magazine: Toggle Local DB",
|
|
201
|
+
description: "Enrich sub-agent entries from the local OpenCode database (V2)",
|
|
202
|
+
slash: { name: "subagent-db" },
|
|
203
|
+
palette: true,
|
|
204
|
+
run: () => {
|
|
205
|
+
const v = !signals.dbSync();
|
|
206
|
+
signals.setDbSync(v);
|
|
207
|
+
kv.set(SETTING_KEYS.dbSync, v);
|
|
208
|
+
api.ui.toast(`${t("settings.dbSync")}: ${onOff(v)}`);
|
|
209
|
+
},
|
|
210
|
+
},
|
|
101
211
|
{
|
|
102
212
|
id: "opencode-subagent-magazine.subagent.version",
|
|
103
213
|
title: "SubAgent Magazine: Version",
|
|
@@ -140,33 +250,53 @@ export function makeCommands(context, api, signals) {
|
|
|
140
250
|
}
|
|
141
251
|
}
|
|
142
252
|
if (count > 0) {
|
|
253
|
+
const ok = () => api.ui.toast(signals.lang() === "zh"
|
|
254
|
+
? `已标记 ${count} 个运行中的条目为完成`
|
|
255
|
+
: `Marked ${count} running entries as done`);
|
|
256
|
+
const fail = () => {
|
|
257
|
+
try {
|
|
258
|
+
api.ui.toast(t("clear.failed"));
|
|
259
|
+
}
|
|
260
|
+
catch { }
|
|
261
|
+
};
|
|
143
262
|
try {
|
|
144
|
-
const data = loadSessionData(kv);
|
|
145
263
|
const { parentSid, isChild } = resolveParent(sid);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
264
|
+
const done = updateSessionData(kv, (data) => {
|
|
265
|
+
// 落定前先把(可能过期的)模块缓存与持久化记录
|
|
266
|
+
// merge:其他实例可能有本实例从未见过的条目,
|
|
267
|
+
// 这次写入不能把它们丢掉。
|
|
268
|
+
if (isChild) {
|
|
269
|
+
if (!data[parentSid])
|
|
270
|
+
data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
|
|
271
|
+
if (!data[parentSid].children)
|
|
272
|
+
data[parentSid].children = {};
|
|
273
|
+
const prev = data[parentSid].children[sid]?.entries ?? [];
|
|
274
|
+
const merged = [...mergeSubEntries(prev, entries.values()).values()].map((e) => e.status === "running" || e.status === "cancel_requested"
|
|
275
|
+
? { ...e, status: "done", endedAt: e.endedAt ?? Date.now() }
|
|
276
|
+
: e);
|
|
277
|
+
data[parentSid].children[sid] = {
|
|
278
|
+
...(data[parentSid].children[sid] ?? { scroll: 0, expanded: "" }),
|
|
279
|
+
entries: merged,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const rec = data[parentSid] ?? { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} };
|
|
284
|
+
const merged = [...mergeSubEntries(rec.entries ?? [], entries.values()).values()].map((e) => e.status === "running" || e.status === "cancel_requested"
|
|
285
|
+
? { ...e, status: "done", endedAt: e.endedAt ?? Date.now() }
|
|
286
|
+
: e);
|
|
287
|
+
data[parentSid] = { ...rec, ts: Date.now(), entries: merged, children: rec.children ?? {} };
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
if (done && typeof done.then === "function") {
|
|
291
|
+
void done.then(ok, fail);
|
|
154
292
|
}
|
|
155
293
|
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
|
-
};
|
|
294
|
+
ok();
|
|
163
295
|
}
|
|
164
|
-
saveSessionData(kv, data);
|
|
165
296
|
}
|
|
166
|
-
catch {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
: `Marked ${count} running entries as done`);
|
|
297
|
+
catch {
|
|
298
|
+
fail();
|
|
299
|
+
}
|
|
170
300
|
}
|
|
171
301
|
else {
|
|
172
302
|
api.ui.toast(signals.lang() === "zh" ? "没有需要清理的运行中条目" : "No running entries to clear");
|
|
@@ -227,33 +357,47 @@ export function makeCommands(context, api, signals) {
|
|
|
227
357
|
if (choice !== "yes")
|
|
228
358
|
return;
|
|
229
359
|
try {
|
|
230
|
-
const data = loadSessionData(kv);
|
|
231
360
|
let count = 0;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
child
|
|
240
|
-
|
|
361
|
+
const finish = () => {
|
|
362
|
+
globalEntryCache.delete(sid);
|
|
363
|
+
setClearTick((v) => v + 1);
|
|
364
|
+
api.ui.toast(t("clear.done", { n: count }));
|
|
365
|
+
};
|
|
366
|
+
const done = updateSessionData(kv, (data) => {
|
|
367
|
+
if (parentID) {
|
|
368
|
+
const child = data[parentID]?.children?.[sid];
|
|
369
|
+
if (child) {
|
|
370
|
+
const ids = child.entries?.map((e) => e.id) ?? [];
|
|
371
|
+
count = ids.length;
|
|
372
|
+
child.entries = [];
|
|
373
|
+
child.scroll = 0;
|
|
374
|
+
child.expanded = "";
|
|
375
|
+
child.clearedIds = [...new Set([...(child.clearedIds ?? []), ...ids])];
|
|
376
|
+
}
|
|
241
377
|
}
|
|
378
|
+
else {
|
|
379
|
+
const rec = data[sid];
|
|
380
|
+
if (rec) {
|
|
381
|
+
const ids = rec.entries?.map((e) => e.id) ?? [];
|
|
382
|
+
count = ids.length;
|
|
383
|
+
rec.entries = [];
|
|
384
|
+
rec.scroll = 0;
|
|
385
|
+
rec.expanded = "";
|
|
386
|
+
rec.clearedIds = [...new Set([...(rec.clearedIds ?? []), ...ids])];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
if (done && typeof done.then === "function") {
|
|
391
|
+
void done.then(finish, () => {
|
|
392
|
+
try {
|
|
393
|
+
api.ui.toast(t("clear.failed"));
|
|
394
|
+
}
|
|
395
|
+
catch { }
|
|
396
|
+
});
|
|
242
397
|
}
|
|
243
398
|
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
|
-
}
|
|
399
|
+
finish();
|
|
252
400
|
}
|
|
253
|
-
saveSessionData(kv, data);
|
|
254
|
-
globalEntryCache.delete(sid);
|
|
255
|
-
setClearTick((v) => v + 1);
|
|
256
|
-
api.ui.toast(t("clear.done", { n: count }));
|
|
257
401
|
}
|
|
258
402
|
catch { }
|
|
259
403
|
},
|
package/dist/v2/db.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
|
26
|
+
export interface SessionDbIndex {
|
|
27
|
+
/** 数据库可用且设置项开启时为 true。 */
|
|
28
|
+
enabled(): boolean;
|
|
29
|
+
/** call id → 子会话 id(前台/后台通用)。 */
|
|
30
|
+
resolveCall(parentId: string, callId: string): string | undefined;
|
|
31
|
+
/** 无 call id 时的兜底:按 agent + 起始时间匹配子会话。 */
|
|
32
|
+
matchChild(parentId: string, agent: string | undefined, startedAt: number | undefined): string | undefined;
|
|
33
|
+
/** 子会话汇总(model/tokens/cost/time_idle)。 */
|
|
34
|
+
info(sid: string): ChildSessionInfo | undefined;
|
|
35
|
+
}
|
|
36
|
+
/** 从宿主 model 字段提取模型 id。
|
|
37
|
+
* 兼容三种形态:名字字符串、{ id } 对象,以及序列化后的 JSON 字符串
|
|
38
|
+
* (session_v2.model 列存的就是 `{"id":...}` 文本)。 */
|
|
39
|
+
export declare function modelIdOf(value: unknown): string | undefined;
|
|
40
|
+
/** 宿主 readSessionTokens 同语义:最后一条 assistant 消息的上下文规模。 */
|
|
41
|
+
export declare function contextTokens(tokens: unknown): number | undefined;
|
|
42
|
+
/** 子会话终态:time_idle 存在才算结束;interrupted 视为取消。
|
|
43
|
+
* 没有 time_idle 时不作判断(交给宿主实时状态)——避免时间阈值猜测。 */
|
|
44
|
+
export declare function statusOfChild(info: ChildSessionInfo | undefined): "done" | "cancelled" | undefined;
|
|
45
|
+
/** 定位宿主数据库(可用 OPENCODE_DB 覆盖,遵循 XDG_DATA_HOME)。 */
|
|
46
|
+
export declare function findDbPath(env?: Record<string, string | undefined>, home?: string): string | undefined;
|
|
47
|
+
export declare function createSessionDbIndex(enabled: () => boolean): SessionDbIndex | undefined;
|
package/dist/v2/db.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
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
|
+
import { homedir } from "node:os";
|
|
15
|
+
// 动态加载:仅 Bun 运行时可用。字符串变量使 tsc/esbuild 不做静态解析,
|
|
16
|
+
// 打包产物保留运行时 import;Node(CI 测试)加载失败会静默禁用。
|
|
17
|
+
const BUN_SQLITE = "bun:sqlite";
|
|
18
|
+
/** 从宿主 model 字段提取模型 id。
|
|
19
|
+
* 兼容三种形态:名字字符串、{ id } 对象,以及序列化后的 JSON 字符串
|
|
20
|
+
* (session_v2.model 列存的就是 `{"id":...}` 文本)。 */
|
|
21
|
+
export function modelIdOf(value) {
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
const s = value.trim();
|
|
24
|
+
if (s.length === 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (s.startsWith("{")) {
|
|
27
|
+
try {
|
|
28
|
+
return modelIdOf(JSON.parse(s));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
if (value && typeof value === "object") {
|
|
37
|
+
const id = value.id;
|
|
38
|
+
if (id === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
const direct = modelIdOf(id);
|
|
41
|
+
if (direct !== undefined)
|
|
42
|
+
return direct;
|
|
43
|
+
const s = String(id);
|
|
44
|
+
return s.length > 0 ? s : undefined;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/** 宿主 readSessionTokens 同语义:最后一条 assistant 消息的上下文规模。 */
|
|
49
|
+
export function contextTokens(tokens) {
|
|
50
|
+
if (!tokens || typeof tokens !== "object")
|
|
51
|
+
return undefined;
|
|
52
|
+
const t = tokens;
|
|
53
|
+
const cache = (t.cache && typeof t.cache === "object") ? t.cache : {};
|
|
54
|
+
const sum = (Number(t.input) || 0) +
|
|
55
|
+
(Number(t.output) || 0) +
|
|
56
|
+
(Number(t.reasoning) || 0) +
|
|
57
|
+
(Number(cache.read) || 0) +
|
|
58
|
+
(Number(cache.write) || 0);
|
|
59
|
+
return sum > 0 ? sum : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** 子会话终态:time_idle 存在才算结束;interrupted 视为取消。
|
|
62
|
+
* 没有 time_idle 时不作判断(交给宿主实时状态)——避免时间阈值猜测。 */
|
|
63
|
+
export function statusOfChild(info) {
|
|
64
|
+
if (!info || info.timeIdle == null)
|
|
65
|
+
return undefined;
|
|
66
|
+
return info.idleOutcome === "interrupted" ? "cancelled" : "done";
|
|
67
|
+
}
|
|
68
|
+
/** 定位宿主数据库(可用 OPENCODE_DB 覆盖,遵循 XDG_DATA_HOME)。 */
|
|
69
|
+
export function findDbPath(env = process.env, home = safeHomedir()) {
|
|
70
|
+
if (env.OPENCODE_DB)
|
|
71
|
+
return env.OPENCODE_DB;
|
|
72
|
+
const base = env.XDG_DATA_HOME && env.XDG_DATA_HOME.length > 0 ? env.XDG_DATA_HOME : `${home}/.local/share`;
|
|
73
|
+
if (!base || base === "/.local/share")
|
|
74
|
+
return undefined;
|
|
75
|
+
return `${base}/opencode/opencode.db`;
|
|
76
|
+
}
|
|
77
|
+
function safeHomedir() {
|
|
78
|
+
try {
|
|
79
|
+
return homedir();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const CALL_MAP_SQL = `
|
|
86
|
+
SELECT json_extract(p.value, '$.id') AS call_id,
|
|
87
|
+
json_extract(p.value, '$.state.metadata.sessionID') AS child_sid
|
|
88
|
+
FROM session_message m, json_each(m.data, '$.content') p
|
|
89
|
+
WHERE m.session_id = ? AND m.type = 'assistant'
|
|
90
|
+
AND json_extract(p.value, '$.id') LIKE 'call_%'
|
|
91
|
+
AND json_extract(p.value, '$.state.metadata.sessionID') IS NOT NULL`;
|
|
92
|
+
const CHILD_INFO_SQL = `
|
|
93
|
+
SELECT parent_id, agent, title, model, cost,
|
|
94
|
+
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
|
95
|
+
time_created, time_idle, idle_outcome
|
|
96
|
+
FROM session_v2 WHERE id = ?`;
|
|
97
|
+
const LAST_ASSISTANT_SQL = `
|
|
98
|
+
SELECT json_extract(data, '$.tokens.input') AS input,
|
|
99
|
+
json_extract(data, '$.tokens.output') AS output,
|
|
100
|
+
json_extract(data, '$.tokens.reasoning') AS reasoning,
|
|
101
|
+
json_extract(data, '$.tokens.cache.read') AS cache_read,
|
|
102
|
+
json_extract(data, '$.tokens.cache.write') AS cache_write,
|
|
103
|
+
json_extract(data, '$.model.id') AS model_id
|
|
104
|
+
FROM session_message
|
|
105
|
+
WHERE session_id = ? AND type = 'assistant' AND json_extract(data, '$.tokens.output') > 0
|
|
106
|
+
ORDER BY seq DESC LIMIT 1`;
|
|
107
|
+
const ASSISTANT_COST_SQL = `
|
|
108
|
+
SELECT COUNT(*) AS n, SUM(COALESCE(json_extract(data, '$.cost'), 0)) AS total
|
|
109
|
+
FROM session_message WHERE session_id = ? AND type = 'assistant'`;
|
|
110
|
+
const CHILDREN_SQL = `
|
|
111
|
+
SELECT id, agent, time_created FROM session_v2 WHERE parent_id = ? ORDER BY time_created`;
|
|
112
|
+
const num = (v) => {
|
|
113
|
+
const n = Number(v);
|
|
114
|
+
return Number.isFinite(n) ? n : undefined;
|
|
115
|
+
};
|
|
116
|
+
const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
|
|
117
|
+
export function createSessionDbIndex(enabled) {
|
|
118
|
+
const dbPath = findDbPath();
|
|
119
|
+
if (!dbPath)
|
|
120
|
+
return undefined;
|
|
121
|
+
let db;
|
|
122
|
+
let failed = false;
|
|
123
|
+
void (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const mod = (await import(BUN_SQLITE));
|
|
126
|
+
const Database = mod?.Database;
|
|
127
|
+
if (!Database) {
|
|
128
|
+
failed = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
db = new Database(dbPath, { readonly: true });
|
|
132
|
+
try {
|
|
133
|
+
db.query("pragma query_only = on").all();
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
failed = true;
|
|
139
|
+
db = undefined;
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
const CALL_MAP_TTL_MS = 15000;
|
|
143
|
+
const CALL_MAP_RETRY_MS = 2000;
|
|
144
|
+
const INFO_TTL_MS = 5000;
|
|
145
|
+
const callMaps = new Map();
|
|
146
|
+
const infos = new Map();
|
|
147
|
+
const childrenCache = new Map();
|
|
148
|
+
const ready = () => {
|
|
149
|
+
if (failed || db === undefined)
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
return enabled();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const buildCallMap = (parentId) => {
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const prev = callMaps.get(parentId);
|
|
161
|
+
if (prev && now - prev.at < CALL_MAP_RETRY_MS)
|
|
162
|
+
return prev.map;
|
|
163
|
+
const map = new Map();
|
|
164
|
+
try {
|
|
165
|
+
for (const row of db.query(CALL_MAP_SQL).all(parentId)) {
|
|
166
|
+
const id = str(row.call_id);
|
|
167
|
+
const sid = str(row.child_sid);
|
|
168
|
+
if (id && sid)
|
|
169
|
+
map.set(id, sid);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
callMaps.set(parentId, { map, at: now });
|
|
174
|
+
return map;
|
|
175
|
+
};
|
|
176
|
+
const resolveCall = (parentId, callId) => {
|
|
177
|
+
if (!ready() || !parentId || !callId)
|
|
178
|
+
return undefined;
|
|
179
|
+
const prev = callMaps.get(parentId);
|
|
180
|
+
const map = prev && Date.now() - prev.at < CALL_MAP_TTL_MS ? prev.map : buildCallMap(parentId);
|
|
181
|
+
return map.get(callId) ?? buildCallMap(parentId).get(callId);
|
|
182
|
+
};
|
|
183
|
+
const matchChild = (parentId, agent, startedAt) => {
|
|
184
|
+
if (!ready() || !parentId || startedAt === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
let cached = childrenCache.get(parentId);
|
|
188
|
+
if (!cached || now - cached.at > CALL_MAP_TTL_MS) {
|
|
189
|
+
let list = [];
|
|
190
|
+
try {
|
|
191
|
+
list = db.query(CHILDREN_SQL).all(parentId);
|
|
192
|
+
}
|
|
193
|
+
catch { }
|
|
194
|
+
cached = { list, at: now };
|
|
195
|
+
childrenCache.set(parentId, cached);
|
|
196
|
+
}
|
|
197
|
+
let best;
|
|
198
|
+
let bestAt = Number.POSITIVE_INFINITY;
|
|
199
|
+
for (const row of cached.list) {
|
|
200
|
+
const sid = str(row.id);
|
|
201
|
+
const t = num(row.time_created);
|
|
202
|
+
if (!sid || t === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
if (t < startedAt - 2000)
|
|
205
|
+
continue;
|
|
206
|
+
if (agent !== undefined && str(row.agent) !== agent)
|
|
207
|
+
continue;
|
|
208
|
+
if (t < bestAt) {
|
|
209
|
+
bestAt = t;
|
|
210
|
+
best = sid;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return best;
|
|
214
|
+
};
|
|
215
|
+
const info = (sid) => {
|
|
216
|
+
if (!sid)
|
|
217
|
+
return undefined;
|
|
218
|
+
const hit = infos.get(sid);
|
|
219
|
+
if (hit && Date.now() - hit.at < INFO_TTL_MS)
|
|
220
|
+
return hit.info;
|
|
221
|
+
if (!ready())
|
|
222
|
+
return undefined;
|
|
223
|
+
let result;
|
|
224
|
+
try {
|
|
225
|
+
const row = db.query(CHILD_INFO_SQL).get(sid);
|
|
226
|
+
if (row) {
|
|
227
|
+
const last = db.query(LAST_ASSISTANT_SQL).get(sid);
|
|
228
|
+
let tokens = contextTokens(last ? {
|
|
229
|
+
input: last.input, output: last.output, reasoning: last.reasoning,
|
|
230
|
+
cache: { read: last.cache_read, write: last.cache_write },
|
|
231
|
+
} : undefined);
|
|
232
|
+
if (tokens === undefined) {
|
|
233
|
+
const agg = (num(row.tokens_input) || 0) + (num(row.tokens_output) || 0) + (num(row.tokens_reasoning) || 0) +
|
|
234
|
+
(num(row.tokens_cache_read) || 0) + (num(row.tokens_cache_write) || 0);
|
|
235
|
+
if (agg > 0)
|
|
236
|
+
tokens = agg;
|
|
237
|
+
}
|
|
238
|
+
// cost 语义对齐宿主 readSessionCost:优先会话聚合值,
|
|
239
|
+
// 0 时回退 assistant 消息求和(有消息即权威,0 表示确实免费)。
|
|
240
|
+
let cost = num(row.cost);
|
|
241
|
+
if (!(typeof cost === "number" && cost > 0)) {
|
|
242
|
+
const aggCost = db.query(ASSISTANT_COST_SQL).get(sid);
|
|
243
|
+
if (aggCost && Number(aggCost.n) > 0)
|
|
244
|
+
cost = Number(aggCost.total) || 0;
|
|
245
|
+
}
|
|
246
|
+
result = {
|
|
247
|
+
id: sid,
|
|
248
|
+
parentId: str(row.parent_id),
|
|
249
|
+
agent: str(row.agent),
|
|
250
|
+
title: str(row.title),
|
|
251
|
+
model: modelIdOf(last?.model_id) ?? modelIdOf(row.model),
|
|
252
|
+
cost,
|
|
253
|
+
tokens,
|
|
254
|
+
timeCreated: num(row.time_created),
|
|
255
|
+
timeIdle: num(row.time_idle),
|
|
256
|
+
idleOutcome: str(row.idle_outcome),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
result = undefined;
|
|
262
|
+
}
|
|
263
|
+
infos.set(sid, { info: result, at: Date.now() });
|
|
264
|
+
return result;
|
|
265
|
+
};
|
|
266
|
+
return { enabled: ready, resolveCall, matchChild, info };
|
|
267
|
+
}
|
package/dist/v2/index.js
CHANGED
|
@@ -5,16 +5,20 @@ import { createPanelApi } from "./v2-panel-api";
|
|
|
5
5
|
import { makeCommands } from "./commands";
|
|
6
6
|
import { mapTheme } from "./theme";
|
|
7
7
|
import { SubAgentPanel } from "../panel/SubAgentPanel";
|
|
8
|
+
import { TIME_FORMATS } from "../core/format";
|
|
8
9
|
import { SETTING_KEYS } from "../core/kv";
|
|
9
10
|
import { LANG_META, detectLang } from "../i18n";
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
function PluginRoot(props) {
|
|
11
|
+
/** 命令层必须在 app 槽注册:侧栏隐藏(sidebar: auto)时命令仍需可用。 */
|
|
12
|
+
function CommandRoot(props) {
|
|
13
13
|
props.context.keymap.layer(() => ({
|
|
14
14
|
mode: "global",
|
|
15
15
|
commands: makeCommands(props.context, props.api, props.signals),
|
|
16
16
|
}));
|
|
17
|
-
return
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
/** 面板根组件:渲染共享 SubAgentPanel(命令层见 CommandRoot)。 */
|
|
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 }));
|
|
18
22
|
}
|
|
19
23
|
/** V2 入口:setup 创建共享信号 + PanelApi + 命令 layer + 侧边栏槽位。
|
|
20
24
|
* 行为对齐 V1 tui()(信号初始值从 KV 恢复;侧边栏渲染共享 SubAgentPanel)。 */
|
|
@@ -40,14 +44,30 @@ const mod = {
|
|
|
40
44
|
const [orderSignal, setSortOrder] = createSignal(String(api.kv.get(SETTING_KEYS.order, "desc")) === "asc" ? "asc" : "desc");
|
|
41
45
|
const [scrollSignal, setScrollMode] = createSignal(String(api.kv.get(SETTING_KEYS.scrollMode, "wheel")) === "click" ? "click" : "wheel");
|
|
42
46
|
const [borderSignal, setBorderVisible] = createSignal(api.kv.get(SETTING_KEYS.border, false) === true);
|
|
47
|
+
const [showEntryCostSignal, setShowEntryCost] = createSignal(api.kv.get(SETTING_KEYS.showEntryCost, false) === true);
|
|
48
|
+
const [showEntryTimeSignal, setShowEntryTime] = createSignal(api.kv.get(SETTING_KEYS.showEntryTime, true) !== false);
|
|
49
|
+
const [showEntryTokensSignal, setShowEntryTokens] = createSignal(api.kv.get(SETTING_KEYS.showEntryTokens, true) !== false);
|
|
50
|
+
const storedTimeFormat = String(api.kv.get(SETTING_KEYS.timeFormat, "short"));
|
|
51
|
+
const [timeFormatSignal, setTimeFormat] = createSignal(TIME_FORMATS.includes(storedTimeFormat) ? storedTimeFormat : "short");
|
|
52
|
+
const [dbSyncSignal, setDbSync] = createSignal(api.kv.get(SETTING_KEYS.dbSync, true) !== false);
|
|
43
53
|
lang = langSignal;
|
|
44
54
|
maxEntries = maxSignal;
|
|
45
55
|
sortOrder = orderSignal;
|
|
46
56
|
scrollMode = scrollSignal;
|
|
47
57
|
const signals = {
|
|
48
|
-
lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible: borderSignal, setBorderVisible,
|
|
58
|
+
lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible: borderSignal, setBorderVisible,
|
|
59
|
+
showEntryCost: showEntryCostSignal, setShowEntryCost,
|
|
60
|
+
showEntryTime: showEntryTimeSignal, setShowEntryTime,
|
|
61
|
+
showEntryTokens: showEntryTokensSignal, setShowEntryTokens,
|
|
62
|
+
timeFormat: timeFormatSignal, setTimeFormat,
|
|
63
|
+
dbSync: dbSyncSignal, setDbSync,
|
|
64
|
+
sessionId: "",
|
|
49
65
|
};
|
|
50
|
-
//
|
|
66
|
+
// 命令层挂 app 槽:侧栏隐藏时斜杠命令仍需可用(见 CommandRoot)
|
|
67
|
+
context.ui.slot({
|
|
68
|
+
append: "app",
|
|
69
|
+
render: () => (_jsx(CommandRoot, { context: context, api: api, signals: signals })),
|
|
70
|
+
});
|
|
51
71
|
// 侧边栏面板(共享 SubAgentPanel——V1/V2 同一组件)
|
|
52
72
|
context.ui.slot({
|
|
53
73
|
prepend: "sidebar.content",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Context } from "./types";
|
|
2
|
+
import type { PanelApi } from "../panel/panel-api";
|
|
3
|
+
import type { SharedSignals } from "../core/types";
|
|
4
|
+
/**
|
|
5
|
+
* 原生设置菜单(visual-cache 风格):用单个选择列表展示每项设置
|
|
6
|
+
* 及其当前值;选中某项会打开对应的编辑器,
|
|
7
|
+
* 随后菜单重新打开,直到用户关闭(Esc / Close)。
|
|
8
|
+
*/
|
|
9
|
+
export declare function openSettingsMenu(context: Context, api: PanelApi, signals: SharedSignals): void;
|