opencode-subagent-magazine 1.6.0-beta.4 → 1.6.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/LICENSE +21 -21
- package/README.md +230 -194
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/tui.js +1 -1
- package/dist/v2.js +1 -1
- package/install.mjs +103 -103
- package/package.json +68 -65
- package/src/_version.ts +1 -1
- package/src/clipboard.ts +134 -134
- package/src/core/color.ts +70 -70
- package/src/core/format.ts +61 -61
- package/src/core/index.ts +6 -6
- package/src/core/kv.ts +37 -37
- package/src/core/state-machine.ts +46 -46
- package/src/core/types.ts +60 -60
- package/src/core/usage.ts +16 -16
- package/src/i18n.ts +269 -269
- package/src/index.tsx +472 -472
- package/src/panel/SubAgentPanel.tsx +1494 -1494
- package/src/panel/entry-map.ts +39 -39
- package/src/panel/panel-api.ts +75 -75
- package/src/panel/session-routing.ts +10 -10
- package/src/panel/store.ts +8 -8
- package/src/server.ts +10 -10
- package/src/v2/commands.ts +264 -264
- package/src/v2/index.tsx +102 -102
- package/src/v2/theme.ts +14 -14
- package/src/v2/types.ts +165 -165
- package/src/v2/v2-panel-api.ts +307 -307
- package/tui/index.js +11 -11
package/src/index.tsx
CHANGED
|
@@ -1,473 +1,473 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
|
|
3
|
-
import type { JSX } from "@opentui/solid"
|
|
4
|
-
import type {
|
|
5
|
-
TuiPlugin,
|
|
6
|
-
TuiPluginApi,
|
|
7
|
-
TuiSlotContext,
|
|
8
|
-
TuiSlotPlugin,
|
|
9
|
-
TuiPluginModule,
|
|
10
|
-
} from "@opencode-ai/plugin/tui"
|
|
11
|
-
import { createSignal } from "solid-js"
|
|
12
|
-
import { PLUGIN_VERSION } from "./_version"
|
|
13
|
-
import { LANG_META, createT, detectLang } from "./i18n"
|
|
14
|
-
import type { Lang, SortOrder, ScrollMode, SubStatus, SharedSignals } from "./core/types"
|
|
15
|
-
import { KV_PREFIX } from "./core/kv"
|
|
16
|
-
import type { PanelApi } from "./panel/panel-api"
|
|
17
|
-
import { SubAgentPanel } from "./panel/SubAgentPanel"
|
|
18
|
-
import { globalEntryCache, setClearTick } from "./panel/store"
|
|
19
|
-
|
|
20
|
-
// Plugin entry
|
|
21
|
-
// ===================================================================
|
|
22
|
-
|
|
23
|
-
function createSidebarSlot(api: TuiPluginApi, panelApi: PanelApi, sig: SharedSignals): TuiSlotPlugin {
|
|
24
|
-
return {
|
|
25
|
-
order: 60,
|
|
26
|
-
slots: {
|
|
27
|
-
sidebar_content(ctx: TuiSlotContext, input: { session_id: string }): JSX.Element {
|
|
28
|
-
sig.sessionId = input.session_id
|
|
29
|
-
return (
|
|
30
|
-
<SubAgentPanel
|
|
31
|
-
api={panelApi}
|
|
32
|
-
theme={ctx.theme.current as Record<string, unknown>}
|
|
33
|
-
lang={sig.lang}
|
|
34
|
-
maxEntries={sig.maxEntries}
|
|
35
|
-
sortOrder={sig.sortOrder}
|
|
36
|
-
scrollMode={sig.scrollMode}
|
|
37
|
-
borderVisible={sig.borderVisible}
|
|
38
|
-
sessionId={input.session_id}
|
|
39
|
-
/>
|
|
40
|
-
)
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
47
|
-
// ── language ──
|
|
48
|
-
const stored = String(api.kv.get(`${KV_PREFIX}.lang`, ""))
|
|
49
|
-
const initialLang: Lang =
|
|
50
|
-
LANG_META.some((m) => m.code === stored) ? (stored as Lang) : detectLang()
|
|
51
|
-
const [lang, setLang] = createSignal<Lang>(initialLang)
|
|
52
|
-
const [maxEntries, setMaxEntries] = createSignal(
|
|
53
|
-
parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10
|
|
54
|
-
)
|
|
55
|
-
const [sortOrder, setSortOrder] = createSignal<SortOrder>(
|
|
56
|
-
String(api.kv.get(`${KV_PREFIX}.order`, "desc")) === "asc" ? "asc" : "desc"
|
|
57
|
-
)
|
|
58
|
-
const [scrollMode, setScrollMode] = createSignal<ScrollMode>(
|
|
59
|
-
String(api.kv.get(`${KV_PREFIX}.scroll_mode`, "wheel")) === "click" ? "click" : "wheel"
|
|
60
|
-
)
|
|
61
|
-
const [borderVisible, setBorderVisible] = createSignal<boolean>(
|
|
62
|
-
(api.kv.get(`${KV_PREFIX}.border`, false) as boolean) === true
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible, setBorderVisible, sessionId: "" }
|
|
66
|
-
|
|
67
|
-
// ── V1 PanelApi adapter: wraps the V1 host API into the shared panel contract ──
|
|
68
|
-
const v1Api: PanelApi = {
|
|
69
|
-
kv: api.kv as any,
|
|
70
|
-
usage: {
|
|
71
|
-
readSessionTokens: (sid: string): number | undefined => {
|
|
72
|
-
if (!sid) return undefined
|
|
73
|
-
try {
|
|
74
|
-
const msgs = api.state.session.messages(sid)
|
|
75
|
-
if (msgs) {
|
|
76
|
-
for (let i = (msgs as any[]).length - 1; i >= 0; i--) {
|
|
77
|
-
const m = (msgs as any[])[i]
|
|
78
|
-
if (m.role !== "assistant") continue
|
|
79
|
-
const t = m.tokens
|
|
80
|
-
if (!t || !(Number(t.output) > 0)) continue
|
|
81
|
-
const ctx =
|
|
82
|
-
(Number(t.input) || 0) +
|
|
83
|
-
(Number(t.output) || 0) +
|
|
84
|
-
(Number(t.reasoning) || 0) +
|
|
85
|
-
(Number(t.cache?.read) || 0) +
|
|
86
|
-
(Number(t.cache?.write) || 0)
|
|
87
|
-
if (ctx > 0) return ctx
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return undefined
|
|
91
|
-
} catch {
|
|
92
|
-
return undefined
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
readSessionCost: (sid: string): number | undefined => {
|
|
96
|
-
if (!sid) return undefined
|
|
97
|
-
try {
|
|
98
|
-
const session = api.state.session.get(sid)
|
|
99
|
-
if (session?.cost != null && session.cost > 0) return session.cost
|
|
100
|
-
const msgs = api.state.session.messages(sid)
|
|
101
|
-
if (!msgs) return undefined
|
|
102
|
-
let total = 0
|
|
103
|
-
for (const m of msgs as any[]) {
|
|
104
|
-
if (m.role === "assistant" && typeof m.cost === "number") total += m.cost
|
|
105
|
-
}
|
|
106
|
-
return total > 0 ? total : undefined
|
|
107
|
-
} catch {
|
|
108
|
-
return undefined
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
readSessionModel: (sid: string): string | undefined => {
|
|
112
|
-
if (!sid) return undefined
|
|
113
|
-
try {
|
|
114
|
-
const msgs = api.state.session.messages(sid)
|
|
115
|
-
if (msgs) {
|
|
116
|
-
for (let i = (msgs as any[]).length - 1; i >= 0; i--) {
|
|
117
|
-
const m = (msgs as any[])[i]
|
|
118
|
-
if (m.role === "assistant" && m.modelID) return String(m.modelID)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return undefined
|
|
122
|
-
} catch {
|
|
123
|
-
return undefined
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
readSessionTodo: (sid: string): { total: number; done: number } | undefined => {
|
|
127
|
-
if (!sid) return undefined
|
|
128
|
-
try {
|
|
129
|
-
const todos = api.state.session.todo(sid)
|
|
130
|
-
if (!todos || todos.length === 0) return undefined
|
|
131
|
-
let done = 0
|
|
132
|
-
for (const t of todos) {
|
|
133
|
-
if (t.status === "completed" || t.status === "cancelled") done++
|
|
134
|
-
}
|
|
135
|
-
return { total: todos.length, done }
|
|
136
|
-
} catch {
|
|
137
|
-
return undefined
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
session: {
|
|
142
|
-
get: (sid) => { try { return api.state.session.get(sid) as any } catch { return undefined } },
|
|
143
|
-
status: (sid) => { try { return api.state.session.status(sid) as any } catch { return undefined } },
|
|
144
|
-
messages: (sid) => { try { return api.state.session.messages(sid) as any[] } catch { return undefined } },
|
|
145
|
-
part: (messageID) => { try { return api.state.part(messageID) as any[] } catch { return undefined } },
|
|
146
|
-
},
|
|
147
|
-
event: {
|
|
148
|
-
on: (type, cb) => {
|
|
149
|
-
switch (type) {
|
|
150
|
-
case "part.updated":
|
|
151
|
-
return api.event.on("message.part.updated", (e) =>
|
|
152
|
-
cb({ type, payload: { part: (e as any).properties?.part } }))
|
|
153
|
-
case "message.updated":
|
|
154
|
-
return api.event.on("message.updated", () => cb({ type }))
|
|
155
|
-
case "session.idle":
|
|
156
|
-
return api.event.on("session.idle", (e) => cb({ type, payload: (e as any).properties }))
|
|
157
|
-
case "session.error":
|
|
158
|
-
return api.event.on("session.error", (e) => cb({ type, payload: (e as any).properties }))
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
client: {
|
|
163
|
-
abort: (input) => api.client.session.abort(input).then(() => {}),
|
|
164
|
-
},
|
|
165
|
-
route: {
|
|
166
|
-
navigateSession: (sessionID) => api.route.navigate("session", { sessionID }),
|
|
167
|
-
},
|
|
168
|
-
ui: {
|
|
169
|
-
toast: (message, opts) => api.ui.toast({ ...opts, message } as any),
|
|
170
|
-
},
|
|
171
|
-
settings: {
|
|
172
|
-
lang: () => lang(),
|
|
173
|
-
maxEntries: () => maxEntries(),
|
|
174
|
-
sortOrder: () => sortOrder(),
|
|
175
|
-
scrollMode: () => scrollMode(),
|
|
176
|
-
},
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
api.slots.register(createSidebarSlot(api, v1Api, signals))
|
|
180
|
-
|
|
181
|
-
// ── slash command: /subagent-lang ──
|
|
182
|
-
api.command?.register(() => [
|
|
183
|
-
{
|
|
184
|
-
title: "SubAgent Magazine: Language",
|
|
185
|
-
value: "subagent-lang",
|
|
186
|
-
description: "Switch display language (中文 / English)",
|
|
187
|
-
slash: { name: "subagent-lang" },
|
|
188
|
-
onSelect: (dialog) => {
|
|
189
|
-
dialog?.replace(() => (
|
|
190
|
-
<api.ui.DialogSelect
|
|
191
|
-
title="Language / 语言"
|
|
192
|
-
options={LANG_META.map((m) => ({ title: m.label, value: m.code }))}
|
|
193
|
-
onSelect={(opt) => {
|
|
194
|
-
const l = opt.value as Lang
|
|
195
|
-
setLang(l)
|
|
196
|
-
api.kv.set(`${KV_PREFIX}.lang`, l)
|
|
197
|
-
api.ui.toast({
|
|
198
|
-
message: "Language: " + (LANG_META.find((m) => m.code === l)?.label ?? l),
|
|
199
|
-
})
|
|
200
|
-
dialog?.clear()
|
|
201
|
-
}}
|
|
202
|
-
/>
|
|
203
|
-
))
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
title: "SubAgent Magazine: Sort Order",
|
|
208
|
-
value: "subagent-order",
|
|
209
|
-
description: "Set sub-agent entry sort order (desc / asc)",
|
|
210
|
-
slash: { name: "subagent-order" },
|
|
211
|
-
onSelect: (dialog) => {
|
|
212
|
-
const t = createT(() => lang())
|
|
213
|
-
dialog?.replace(() => (
|
|
214
|
-
<api.ui.DialogSelect
|
|
215
|
-
title="Sort Order / 排序方式"
|
|
216
|
-
options={[
|
|
217
|
-
{ title: t("order.desc"), value: "desc" },
|
|
218
|
-
{ title: t("order.asc"), value: "asc" },
|
|
219
|
-
]}
|
|
220
|
-
onSelect={(opt) => {
|
|
221
|
-
const o = opt.value as SortOrder
|
|
222
|
-
setSortOrder(o)
|
|
223
|
-
api.kv.set(`${KV_PREFIX}.order`, o)
|
|
224
|
-
api.ui.toast({
|
|
225
|
-
message: o === "desc" ? t("order.desc") : t("order.asc"),
|
|
226
|
-
})
|
|
227
|
-
dialog?.clear()
|
|
228
|
-
}}
|
|
229
|
-
/>
|
|
230
|
-
))
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
title: "SubAgent Magazine: Scroll Mode",
|
|
235
|
-
value: "subagent-scroll",
|
|
236
|
-
description: "Set scroll mode (wheel / click)",
|
|
237
|
-
slash: { name: "subagent-scroll" },
|
|
238
|
-
onSelect: (dialog) => {
|
|
239
|
-
const t = createT(() => lang())
|
|
240
|
-
dialog?.replace(() => (
|
|
241
|
-
<api.ui.DialogSelect
|
|
242
|
-
title="Scroll Mode / 滚动模式"
|
|
243
|
-
options={[
|
|
244
|
-
{ title: t("scroll.wheel"), value: "wheel" },
|
|
245
|
-
{ title: t("scroll.click"), value: "click" },
|
|
246
|
-
]}
|
|
247
|
-
onSelect={(opt) => {
|
|
248
|
-
const m = opt.value as ScrollMode
|
|
249
|
-
setScrollMode(m)
|
|
250
|
-
api.kv.set(`${KV_PREFIX}.scroll_mode`, m)
|
|
251
|
-
api.ui.toast({
|
|
252
|
-
message: m === "wheel" ? t("scroll.wheel") : t("scroll.click"),
|
|
253
|
-
})
|
|
254
|
-
dialog?.clear()
|
|
255
|
-
}}
|
|
256
|
-
/>
|
|
257
|
-
))
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
title: "SubAgent Magazine: Max Entries",
|
|
262
|
-
value: "subagent-max",
|
|
263
|
-
description: "Set max visible sub-agent entries in sidebar",
|
|
264
|
-
slash: { name: "subagent-max" },
|
|
265
|
-
onSelect: (dialog) => {
|
|
266
|
-
dialog?.replace(() => (
|
|
267
|
-
<api.ui.DialogPrompt
|
|
268
|
-
title="Max Visible Entries"
|
|
269
|
-
description={() => (
|
|
270
|
-
<text>Number of entries to show in the sidebar (1–50)</text>
|
|
271
|
-
)}
|
|
272
|
-
value={String(maxEntries())}
|
|
273
|
-
onConfirm={(val) => {
|
|
274
|
-
const n = Math.max(1, Math.min(50, parseInt(val, 10) || 10))
|
|
275
|
-
setMaxEntries(n)
|
|
276
|
-
api.kv.set(`${KV_PREFIX}.max_entries`, n)
|
|
277
|
-
api.ui.toast({ message: `Max entries: ${n}` })
|
|
278
|
-
dialog?.clear()
|
|
279
|
-
}}
|
|
280
|
-
/>
|
|
281
|
-
))
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
title: "SubAgent Magazine: Version",
|
|
286
|
-
value: "subagent-version",
|
|
287
|
-
description: "Show plugin version",
|
|
288
|
-
slash: { name: "subagent-version" },
|
|
289
|
-
onSelect: (dialog) => {
|
|
290
|
-
api.ui.toast({ message: `opencode-subagent-magazine v${PLUGIN_VERSION}` })
|
|
291
|
-
dialog?.clear()
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
title: "SubAgent Magazine: Session",
|
|
296
|
-
value: "subagent-session",
|
|
297
|
-
description: "Show current session ID",
|
|
298
|
-
slash: { name: "subagent-session" },
|
|
299
|
-
onSelect: (dialog) => {
|
|
300
|
-
api.ui.toast({ message: `Session: ${signals.sessionId}` })
|
|
301
|
-
dialog?.clear()
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
title: "SubAgent Magazine: Clear Running",
|
|
306
|
-
value: "subagent-clear-running",
|
|
307
|
-
description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
|
|
308
|
-
slash: { name: "subagent-clear-running" },
|
|
309
|
-
onSelect: (dialog) => {
|
|
310
|
-
const sid = signals.sessionId
|
|
311
|
-
const entries = globalEntryCache.get(sid)
|
|
312
|
-
if (!entries || entries.size === 0) {
|
|
313
|
-
const msg = signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found"
|
|
314
|
-
api.ui.toast({ message: msg })
|
|
315
|
-
dialog?.clear()
|
|
316
|
-
return
|
|
317
|
-
}
|
|
318
|
-
let count = 0
|
|
319
|
-
for (const [, entry] of entries) {
|
|
320
|
-
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
321
|
-
entry.status = "done" as SubStatus
|
|
322
|
-
entry.endedAt = Date.now()
|
|
323
|
-
count++
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (count > 0) {
|
|
327
|
-
// 立即写 KV
|
|
328
|
-
try {
|
|
329
|
-
const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")))
|
|
330
|
-
const sessionObj = api.state.session.get(sid)
|
|
331
|
-
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
332
|
-
if (parentID) {
|
|
333
|
-
if (!data[parentID]) data[parentID] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} }
|
|
334
|
-
if (!data[parentID].children) data[parentID].children = {}
|
|
335
|
-
if (!data[parentID].children[sid]) data[parentID].children[sid] = { scroll: 0, expanded: "", entries: [] }
|
|
336
|
-
data[parentID].children[sid] = { ...data[parentID].children[sid], entries: [...entries.values()] }
|
|
337
|
-
} else {
|
|
338
|
-
data[sid] = {
|
|
339
|
-
ts: Date.now(),
|
|
340
|
-
entries: [...entries.values()],
|
|
341
|
-
scroll: data[sid]?.scroll ?? 0,
|
|
342
|
-
expanded: data[sid]?.expanded ?? "",
|
|
343
|
-
children: data[sid]?.children ?? {},
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
|
|
347
|
-
} catch {}
|
|
348
|
-
const msg = signals.lang() === "zh"
|
|
349
|
-
? `已标记 ${count} 个运行中的条目为完成`
|
|
350
|
-
: `Marked ${count} running entries as done`
|
|
351
|
-
api.ui.toast({ message: msg })
|
|
352
|
-
} else {
|
|
353
|
-
const msg = signals.lang() === "zh"
|
|
354
|
-
? "没有需要清理的运行中条目"
|
|
355
|
-
: "No running entries to clear"
|
|
356
|
-
api.ui.toast({ message: msg })
|
|
357
|
-
}
|
|
358
|
-
dialog?.clear()
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
title: "SubAgent Magazine: TTL",
|
|
363
|
-
value: "subagent-ttl",
|
|
364
|
-
description: "Set session data retention period (days before auto-cleanup)",
|
|
365
|
-
slash: { name: "subagent-ttl" },
|
|
366
|
-
onSelect: (dialog) => {
|
|
367
|
-
const t = createT(() => signals.lang())
|
|
368
|
-
const curRaw = parseInt(String(api.kv.get(`${KV_PREFIX}.ttl_days`, "3")), 10)
|
|
369
|
-
const curDays = Number.isNaN(curRaw) ? 3 : curRaw
|
|
370
|
-
const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`
|
|
371
|
-
dialog?.replace(() => (
|
|
372
|
-
<api.ui.DialogSelect
|
|
373
|
-
title={`${t("ttl.label")} (${curLabel})`}
|
|
374
|
-
options={[
|
|
375
|
-
{ title: t("ttl.3d"), value: "3" },
|
|
376
|
-
{ title: t("ttl.7d"), value: "7" },
|
|
377
|
-
{ title: t("ttl.14d"), value: "14" },
|
|
378
|
-
{ title: t("ttl.30d"), value: "30" },
|
|
379
|
-
{ title: t("ttl.unlimited"), value: "0" },
|
|
380
|
-
]}
|
|
381
|
-
onSelect={(opt) => {
|
|
382
|
-
const days = parseInt(opt.value, 10)
|
|
383
|
-
api.kv.set(`${KV_PREFIX}.ttl_days`, String(days))
|
|
384
|
-
const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days })
|
|
385
|
-
api.ui.toast({ message: msg })
|
|
386
|
-
dialog?.clear()
|
|
387
|
-
}}
|
|
388
|
-
/>
|
|
389
|
-
))
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
title: "SubAgent Magazine: Clear Entries",
|
|
394
|
-
value: "subagent-clear-entries",
|
|
395
|
-
description: "Delete all sub-agent records for the current session (cannot be undone)",
|
|
396
|
-
slash: { name: "subagent-clear-entries" },
|
|
397
|
-
onSelect: (dialog) => {
|
|
398
|
-
const t = createT(() => signals.lang())
|
|
399
|
-
const sid = signals.sessionId
|
|
400
|
-
const sessionObj = api.state.session.get(sid)
|
|
401
|
-
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
402
|
-
// 检查是否存在运行中的条目
|
|
403
|
-
const cached = globalEntryCache.get(sid)
|
|
404
|
-
let runningCount = 0
|
|
405
|
-
if (cached) {
|
|
406
|
-
for (const [, e] of cached) { if (e.status === "running") runningCount++ }
|
|
407
|
-
}
|
|
408
|
-
const msg = runningCount > 0
|
|
409
|
-
? t("clear.prompt_running", { n: runningCount })
|
|
410
|
-
: t("clear.prompt")
|
|
411
|
-
dialog?.replace(() => (
|
|
412
|
-
<api.ui.DialogConfirm
|
|
413
|
-
title={t("clear.title")}
|
|
414
|
-
message={msg}
|
|
415
|
-
onConfirm={() => {
|
|
416
|
-
try {
|
|
417
|
-
const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")))
|
|
418
|
-
let count = 0
|
|
419
|
-
if (parentID) {
|
|
420
|
-
if (data[parentID]?.children?.[sid]) {
|
|
421
|
-
const child = data[parentID].children[sid]
|
|
422
|
-
const ids = child.entries?.map((e: any) => e.id) ?? []
|
|
423
|
-
count = ids.length
|
|
424
|
-
child.entries = []
|
|
425
|
-
child.scroll = 0
|
|
426
|
-
child.expanded = ""
|
|
427
|
-
child.clearedIds = [...new Set([...(child.clearedIds ?? []), ...ids])]
|
|
428
|
-
}
|
|
429
|
-
} else {
|
|
430
|
-
count = data[sid]?.entries?.length ?? 0
|
|
431
|
-
if (data[sid]) {
|
|
432
|
-
const ids = data[sid].entries?.map((e: any) => e.id) ?? []
|
|
433
|
-
data[sid].entries = []
|
|
434
|
-
data[sid].scroll = 0
|
|
435
|
-
data[sid].expanded = ""
|
|
436
|
-
data[sid].clearedIds = [...new Set([...(data[sid].clearedIds ?? []), ...ids])]
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
|
|
440
|
-
globalEntryCache.delete(sid)
|
|
441
|
-
setClearTick((v) => v + 1)
|
|
442
|
-
const msg = t("clear.done", { n: count })
|
|
443
|
-
api.ui.toast({ message: msg })
|
|
444
|
-
} catch {}
|
|
445
|
-
dialog?.clear()
|
|
446
|
-
}}
|
|
447
|
-
/>
|
|
448
|
-
))
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
title: "SubAgent Magazine: Border",
|
|
453
|
-
value: "subagent-border",
|
|
454
|
-
description: "Show or hide the panel border",
|
|
455
|
-
slash: { name: "subagent-border" },
|
|
456
|
-
onSelect: (dialog) => {
|
|
457
|
-
const t = createT(() => signals.lang())
|
|
458
|
-
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, false))
|
|
459
|
-
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
460
|
-
signals.setBorderVisible(!cur)
|
|
461
|
-
api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") })
|
|
462
|
-
dialog?.clear()
|
|
463
|
-
},
|
|
464
|
-
},
|
|
465
|
-
])
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const mod: TuiPluginModule & { id: string } = {
|
|
469
|
-
id: "opencode-subagent-magazine",
|
|
470
|
-
tui,
|
|
471
|
-
}
|
|
472
|
-
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "@opentui/solid"
|
|
4
|
+
import type {
|
|
5
|
+
TuiPlugin,
|
|
6
|
+
TuiPluginApi,
|
|
7
|
+
TuiSlotContext,
|
|
8
|
+
TuiSlotPlugin,
|
|
9
|
+
TuiPluginModule,
|
|
10
|
+
} from "@opencode-ai/plugin/tui"
|
|
11
|
+
import { createSignal } from "solid-js"
|
|
12
|
+
import { PLUGIN_VERSION } from "./_version"
|
|
13
|
+
import { LANG_META, createT, detectLang } from "./i18n"
|
|
14
|
+
import type { Lang, SortOrder, ScrollMode, SubStatus, SharedSignals } from "./core/types"
|
|
15
|
+
import { KV_PREFIX } from "./core/kv"
|
|
16
|
+
import type { PanelApi } from "./panel/panel-api"
|
|
17
|
+
import { SubAgentPanel } from "./panel/SubAgentPanel"
|
|
18
|
+
import { globalEntryCache, setClearTick } from "./panel/store"
|
|
19
|
+
|
|
20
|
+
// Plugin entry
|
|
21
|
+
// ===================================================================
|
|
22
|
+
|
|
23
|
+
function createSidebarSlot(api: TuiPluginApi, panelApi: PanelApi, sig: SharedSignals): TuiSlotPlugin {
|
|
24
|
+
return {
|
|
25
|
+
order: 60,
|
|
26
|
+
slots: {
|
|
27
|
+
sidebar_content(ctx: TuiSlotContext, input: { session_id: string }): JSX.Element {
|
|
28
|
+
sig.sessionId = input.session_id
|
|
29
|
+
return (
|
|
30
|
+
<SubAgentPanel
|
|
31
|
+
api={panelApi}
|
|
32
|
+
theme={ctx.theme.current as Record<string, unknown>}
|
|
33
|
+
lang={sig.lang}
|
|
34
|
+
maxEntries={sig.maxEntries}
|
|
35
|
+
sortOrder={sig.sortOrder}
|
|
36
|
+
scrollMode={sig.scrollMode}
|
|
37
|
+
borderVisible={sig.borderVisible}
|
|
38
|
+
sessionId={input.session_id}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
47
|
+
// ── language ──
|
|
48
|
+
const stored = String(api.kv.get(`${KV_PREFIX}.lang`, ""))
|
|
49
|
+
const initialLang: Lang =
|
|
50
|
+
LANG_META.some((m) => m.code === stored) ? (stored as Lang) : detectLang()
|
|
51
|
+
const [lang, setLang] = createSignal<Lang>(initialLang)
|
|
52
|
+
const [maxEntries, setMaxEntries] = createSignal(
|
|
53
|
+
parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10
|
|
54
|
+
)
|
|
55
|
+
const [sortOrder, setSortOrder] = createSignal<SortOrder>(
|
|
56
|
+
String(api.kv.get(`${KV_PREFIX}.order`, "desc")) === "asc" ? "asc" : "desc"
|
|
57
|
+
)
|
|
58
|
+
const [scrollMode, setScrollMode] = createSignal<ScrollMode>(
|
|
59
|
+
String(api.kv.get(`${KV_PREFIX}.scroll_mode`, "wheel")) === "click" ? "click" : "wheel"
|
|
60
|
+
)
|
|
61
|
+
const [borderVisible, setBorderVisible] = createSignal<boolean>(
|
|
62
|
+
(api.kv.get(`${KV_PREFIX}.border`, false) as boolean) === true
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const signals: SharedSignals = { lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible, setBorderVisible, sessionId: "" }
|
|
66
|
+
|
|
67
|
+
// ── V1 PanelApi adapter: wraps the V1 host API into the shared panel contract ──
|
|
68
|
+
const v1Api: PanelApi = {
|
|
69
|
+
kv: api.kv as any,
|
|
70
|
+
usage: {
|
|
71
|
+
readSessionTokens: (sid: string): number | undefined => {
|
|
72
|
+
if (!sid) return undefined
|
|
73
|
+
try {
|
|
74
|
+
const msgs = api.state.session.messages(sid)
|
|
75
|
+
if (msgs) {
|
|
76
|
+
for (let i = (msgs as any[]).length - 1; i >= 0; i--) {
|
|
77
|
+
const m = (msgs as any[])[i]
|
|
78
|
+
if (m.role !== "assistant") continue
|
|
79
|
+
const t = m.tokens
|
|
80
|
+
if (!t || !(Number(t.output) > 0)) continue
|
|
81
|
+
const ctx =
|
|
82
|
+
(Number(t.input) || 0) +
|
|
83
|
+
(Number(t.output) || 0) +
|
|
84
|
+
(Number(t.reasoning) || 0) +
|
|
85
|
+
(Number(t.cache?.read) || 0) +
|
|
86
|
+
(Number(t.cache?.write) || 0)
|
|
87
|
+
if (ctx > 0) return ctx
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
readSessionCost: (sid: string): number | undefined => {
|
|
96
|
+
if (!sid) return undefined
|
|
97
|
+
try {
|
|
98
|
+
const session = api.state.session.get(sid)
|
|
99
|
+
if (session?.cost != null && session.cost > 0) return session.cost
|
|
100
|
+
const msgs = api.state.session.messages(sid)
|
|
101
|
+
if (!msgs) return undefined
|
|
102
|
+
let total = 0
|
|
103
|
+
for (const m of msgs as any[]) {
|
|
104
|
+
if (m.role === "assistant" && typeof m.cost === "number") total += m.cost
|
|
105
|
+
}
|
|
106
|
+
return total > 0 ? total : undefined
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
readSessionModel: (sid: string): string | undefined => {
|
|
112
|
+
if (!sid) return undefined
|
|
113
|
+
try {
|
|
114
|
+
const msgs = api.state.session.messages(sid)
|
|
115
|
+
if (msgs) {
|
|
116
|
+
for (let i = (msgs as any[]).length - 1; i >= 0; i--) {
|
|
117
|
+
const m = (msgs as any[])[i]
|
|
118
|
+
if (m.role === "assistant" && m.modelID) return String(m.modelID)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
readSessionTodo: (sid: string): { total: number; done: number } | undefined => {
|
|
127
|
+
if (!sid) return undefined
|
|
128
|
+
try {
|
|
129
|
+
const todos = api.state.session.todo(sid)
|
|
130
|
+
if (!todos || todos.length === 0) return undefined
|
|
131
|
+
let done = 0
|
|
132
|
+
for (const t of todos) {
|
|
133
|
+
if (t.status === "completed" || t.status === "cancelled") done++
|
|
134
|
+
}
|
|
135
|
+
return { total: todos.length, done }
|
|
136
|
+
} catch {
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
session: {
|
|
142
|
+
get: (sid) => { try { return api.state.session.get(sid) as any } catch { return undefined } },
|
|
143
|
+
status: (sid) => { try { return api.state.session.status(sid) as any } catch { return undefined } },
|
|
144
|
+
messages: (sid) => { try { return api.state.session.messages(sid) as any[] } catch { return undefined } },
|
|
145
|
+
part: (messageID) => { try { return api.state.part(messageID) as any[] } catch { return undefined } },
|
|
146
|
+
},
|
|
147
|
+
event: {
|
|
148
|
+
on: (type, cb) => {
|
|
149
|
+
switch (type) {
|
|
150
|
+
case "part.updated":
|
|
151
|
+
return api.event.on("message.part.updated", (e) =>
|
|
152
|
+
cb({ type, payload: { part: (e as any).properties?.part } }))
|
|
153
|
+
case "message.updated":
|
|
154
|
+
return api.event.on("message.updated", () => cb({ type }))
|
|
155
|
+
case "session.idle":
|
|
156
|
+
return api.event.on("session.idle", (e) => cb({ type, payload: (e as any).properties }))
|
|
157
|
+
case "session.error":
|
|
158
|
+
return api.event.on("session.error", (e) => cb({ type, payload: (e as any).properties }))
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
client: {
|
|
163
|
+
abort: (input) => api.client.session.abort(input).then(() => {}),
|
|
164
|
+
},
|
|
165
|
+
route: {
|
|
166
|
+
navigateSession: (sessionID) => api.route.navigate("session", { sessionID }),
|
|
167
|
+
},
|
|
168
|
+
ui: {
|
|
169
|
+
toast: (message, opts) => api.ui.toast({ ...opts, message } as any),
|
|
170
|
+
},
|
|
171
|
+
settings: {
|
|
172
|
+
lang: () => lang(),
|
|
173
|
+
maxEntries: () => maxEntries(),
|
|
174
|
+
sortOrder: () => sortOrder(),
|
|
175
|
+
scrollMode: () => scrollMode(),
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
api.slots.register(createSidebarSlot(api, v1Api, signals))
|
|
180
|
+
|
|
181
|
+
// ── slash command: /subagent-lang ──
|
|
182
|
+
api.command?.register(() => [
|
|
183
|
+
{
|
|
184
|
+
title: "SubAgent Magazine: Language",
|
|
185
|
+
value: "subagent-lang",
|
|
186
|
+
description: "Switch display language (中文 / English)",
|
|
187
|
+
slash: { name: "subagent-lang" },
|
|
188
|
+
onSelect: (dialog) => {
|
|
189
|
+
dialog?.replace(() => (
|
|
190
|
+
<api.ui.DialogSelect
|
|
191
|
+
title="Language / 语言"
|
|
192
|
+
options={LANG_META.map((m) => ({ title: m.label, value: m.code }))}
|
|
193
|
+
onSelect={(opt) => {
|
|
194
|
+
const l = opt.value as Lang
|
|
195
|
+
setLang(l)
|
|
196
|
+
api.kv.set(`${KV_PREFIX}.lang`, l)
|
|
197
|
+
api.ui.toast({
|
|
198
|
+
message: "Language: " + (LANG_META.find((m) => m.code === l)?.label ?? l),
|
|
199
|
+
})
|
|
200
|
+
dialog?.clear()
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
))
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
title: "SubAgent Magazine: Sort Order",
|
|
208
|
+
value: "subagent-order",
|
|
209
|
+
description: "Set sub-agent entry sort order (desc / asc)",
|
|
210
|
+
slash: { name: "subagent-order" },
|
|
211
|
+
onSelect: (dialog) => {
|
|
212
|
+
const t = createT(() => lang())
|
|
213
|
+
dialog?.replace(() => (
|
|
214
|
+
<api.ui.DialogSelect
|
|
215
|
+
title="Sort Order / 排序方式"
|
|
216
|
+
options={[
|
|
217
|
+
{ title: t("order.desc"), value: "desc" },
|
|
218
|
+
{ title: t("order.asc"), value: "asc" },
|
|
219
|
+
]}
|
|
220
|
+
onSelect={(opt) => {
|
|
221
|
+
const o = opt.value as SortOrder
|
|
222
|
+
setSortOrder(o)
|
|
223
|
+
api.kv.set(`${KV_PREFIX}.order`, o)
|
|
224
|
+
api.ui.toast({
|
|
225
|
+
message: o === "desc" ? t("order.desc") : t("order.asc"),
|
|
226
|
+
})
|
|
227
|
+
dialog?.clear()
|
|
228
|
+
}}
|
|
229
|
+
/>
|
|
230
|
+
))
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
title: "SubAgent Magazine: Scroll Mode",
|
|
235
|
+
value: "subagent-scroll",
|
|
236
|
+
description: "Set scroll mode (wheel / click)",
|
|
237
|
+
slash: { name: "subagent-scroll" },
|
|
238
|
+
onSelect: (dialog) => {
|
|
239
|
+
const t = createT(() => lang())
|
|
240
|
+
dialog?.replace(() => (
|
|
241
|
+
<api.ui.DialogSelect
|
|
242
|
+
title="Scroll Mode / 滚动模式"
|
|
243
|
+
options={[
|
|
244
|
+
{ title: t("scroll.wheel"), value: "wheel" },
|
|
245
|
+
{ title: t("scroll.click"), value: "click" },
|
|
246
|
+
]}
|
|
247
|
+
onSelect={(opt) => {
|
|
248
|
+
const m = opt.value as ScrollMode
|
|
249
|
+
setScrollMode(m)
|
|
250
|
+
api.kv.set(`${KV_PREFIX}.scroll_mode`, m)
|
|
251
|
+
api.ui.toast({
|
|
252
|
+
message: m === "wheel" ? t("scroll.wheel") : t("scroll.click"),
|
|
253
|
+
})
|
|
254
|
+
dialog?.clear()
|
|
255
|
+
}}
|
|
256
|
+
/>
|
|
257
|
+
))
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
title: "SubAgent Magazine: Max Entries",
|
|
262
|
+
value: "subagent-max",
|
|
263
|
+
description: "Set max visible sub-agent entries in sidebar",
|
|
264
|
+
slash: { name: "subagent-max" },
|
|
265
|
+
onSelect: (dialog) => {
|
|
266
|
+
dialog?.replace(() => (
|
|
267
|
+
<api.ui.DialogPrompt
|
|
268
|
+
title="Max Visible Entries"
|
|
269
|
+
description={() => (
|
|
270
|
+
<text>Number of entries to show in the sidebar (1–50)</text>
|
|
271
|
+
)}
|
|
272
|
+
value={String(maxEntries())}
|
|
273
|
+
onConfirm={(val) => {
|
|
274
|
+
const n = Math.max(1, Math.min(50, parseInt(val, 10) || 10))
|
|
275
|
+
setMaxEntries(n)
|
|
276
|
+
api.kv.set(`${KV_PREFIX}.max_entries`, n)
|
|
277
|
+
api.ui.toast({ message: `Max entries: ${n}` })
|
|
278
|
+
dialog?.clear()
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
))
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
title: "SubAgent Magazine: Version",
|
|
286
|
+
value: "subagent-version",
|
|
287
|
+
description: "Show plugin version",
|
|
288
|
+
slash: { name: "subagent-version" },
|
|
289
|
+
onSelect: (dialog) => {
|
|
290
|
+
api.ui.toast({ message: `opencode-subagent-magazine v${PLUGIN_VERSION}` })
|
|
291
|
+
dialog?.clear()
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
title: "SubAgent Magazine: Session",
|
|
296
|
+
value: "subagent-session",
|
|
297
|
+
description: "Show current session ID",
|
|
298
|
+
slash: { name: "subagent-session" },
|
|
299
|
+
onSelect: (dialog) => {
|
|
300
|
+
api.ui.toast({ message: `Session: ${signals.sessionId}` })
|
|
301
|
+
dialog?.clear()
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
title: "SubAgent Magazine: Clear Running",
|
|
306
|
+
value: "subagent-clear-running",
|
|
307
|
+
description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
|
|
308
|
+
slash: { name: "subagent-clear-running" },
|
|
309
|
+
onSelect: (dialog) => {
|
|
310
|
+
const sid = signals.sessionId
|
|
311
|
+
const entries = globalEntryCache.get(sid)
|
|
312
|
+
if (!entries || entries.size === 0) {
|
|
313
|
+
const msg = signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found"
|
|
314
|
+
api.ui.toast({ message: msg })
|
|
315
|
+
dialog?.clear()
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
let count = 0
|
|
319
|
+
for (const [, entry] of entries) {
|
|
320
|
+
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
321
|
+
entry.status = "done" as SubStatus
|
|
322
|
+
entry.endedAt = Date.now()
|
|
323
|
+
count++
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (count > 0) {
|
|
327
|
+
// 立即写 KV
|
|
328
|
+
try {
|
|
329
|
+
const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")))
|
|
330
|
+
const sessionObj = api.state.session.get(sid)
|
|
331
|
+
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
332
|
+
if (parentID) {
|
|
333
|
+
if (!data[parentID]) data[parentID] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} }
|
|
334
|
+
if (!data[parentID].children) data[parentID].children = {}
|
|
335
|
+
if (!data[parentID].children[sid]) data[parentID].children[sid] = { scroll: 0, expanded: "", entries: [] }
|
|
336
|
+
data[parentID].children[sid] = { ...data[parentID].children[sid], entries: [...entries.values()] }
|
|
337
|
+
} else {
|
|
338
|
+
data[sid] = {
|
|
339
|
+
ts: Date.now(),
|
|
340
|
+
entries: [...entries.values()],
|
|
341
|
+
scroll: data[sid]?.scroll ?? 0,
|
|
342
|
+
expanded: data[sid]?.expanded ?? "",
|
|
343
|
+
children: data[sid]?.children ?? {},
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
|
|
347
|
+
} catch {}
|
|
348
|
+
const msg = signals.lang() === "zh"
|
|
349
|
+
? `已标记 ${count} 个运行中的条目为完成`
|
|
350
|
+
: `Marked ${count} running entries as done`
|
|
351
|
+
api.ui.toast({ message: msg })
|
|
352
|
+
} else {
|
|
353
|
+
const msg = signals.lang() === "zh"
|
|
354
|
+
? "没有需要清理的运行中条目"
|
|
355
|
+
: "No running entries to clear"
|
|
356
|
+
api.ui.toast({ message: msg })
|
|
357
|
+
}
|
|
358
|
+
dialog?.clear()
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
title: "SubAgent Magazine: TTL",
|
|
363
|
+
value: "subagent-ttl",
|
|
364
|
+
description: "Set session data retention period (days before auto-cleanup)",
|
|
365
|
+
slash: { name: "subagent-ttl" },
|
|
366
|
+
onSelect: (dialog) => {
|
|
367
|
+
const t = createT(() => signals.lang())
|
|
368
|
+
const curRaw = parseInt(String(api.kv.get(`${KV_PREFIX}.ttl_days`, "3")), 10)
|
|
369
|
+
const curDays = Number.isNaN(curRaw) ? 3 : curRaw
|
|
370
|
+
const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`
|
|
371
|
+
dialog?.replace(() => (
|
|
372
|
+
<api.ui.DialogSelect
|
|
373
|
+
title={`${t("ttl.label")} (${curLabel})`}
|
|
374
|
+
options={[
|
|
375
|
+
{ title: t("ttl.3d"), value: "3" },
|
|
376
|
+
{ title: t("ttl.7d"), value: "7" },
|
|
377
|
+
{ title: t("ttl.14d"), value: "14" },
|
|
378
|
+
{ title: t("ttl.30d"), value: "30" },
|
|
379
|
+
{ title: t("ttl.unlimited"), value: "0" },
|
|
380
|
+
]}
|
|
381
|
+
onSelect={(opt) => {
|
|
382
|
+
const days = parseInt(opt.value, 10)
|
|
383
|
+
api.kv.set(`${KV_PREFIX}.ttl_days`, String(days))
|
|
384
|
+
const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days })
|
|
385
|
+
api.ui.toast({ message: msg })
|
|
386
|
+
dialog?.clear()
|
|
387
|
+
}}
|
|
388
|
+
/>
|
|
389
|
+
))
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
title: "SubAgent Magazine: Clear Entries",
|
|
394
|
+
value: "subagent-clear-entries",
|
|
395
|
+
description: "Delete all sub-agent records for the current session (cannot be undone)",
|
|
396
|
+
slash: { name: "subagent-clear-entries" },
|
|
397
|
+
onSelect: (dialog) => {
|
|
398
|
+
const t = createT(() => signals.lang())
|
|
399
|
+
const sid = signals.sessionId
|
|
400
|
+
const sessionObj = api.state.session.get(sid)
|
|
401
|
+
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
402
|
+
// 检查是否存在运行中的条目
|
|
403
|
+
const cached = globalEntryCache.get(sid)
|
|
404
|
+
let runningCount = 0
|
|
405
|
+
if (cached) {
|
|
406
|
+
for (const [, e] of cached) { if (e.status === "running") runningCount++ }
|
|
407
|
+
}
|
|
408
|
+
const msg = runningCount > 0
|
|
409
|
+
? t("clear.prompt_running", { n: runningCount })
|
|
410
|
+
: t("clear.prompt")
|
|
411
|
+
dialog?.replace(() => (
|
|
412
|
+
<api.ui.DialogConfirm
|
|
413
|
+
title={t("clear.title")}
|
|
414
|
+
message={msg}
|
|
415
|
+
onConfirm={() => {
|
|
416
|
+
try {
|
|
417
|
+
const data = JSON.parse(String(api.kv.get(`${KV_PREFIX}.session_data`, "{}")))
|
|
418
|
+
let count = 0
|
|
419
|
+
if (parentID) {
|
|
420
|
+
if (data[parentID]?.children?.[sid]) {
|
|
421
|
+
const child = data[parentID].children[sid]
|
|
422
|
+
const ids = child.entries?.map((e: any) => e.id) ?? []
|
|
423
|
+
count = ids.length
|
|
424
|
+
child.entries = []
|
|
425
|
+
child.scroll = 0
|
|
426
|
+
child.expanded = ""
|
|
427
|
+
child.clearedIds = [...new Set([...(child.clearedIds ?? []), ...ids])]
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
count = data[sid]?.entries?.length ?? 0
|
|
431
|
+
if (data[sid]) {
|
|
432
|
+
const ids = data[sid].entries?.map((e: any) => e.id) ?? []
|
|
433
|
+
data[sid].entries = []
|
|
434
|
+
data[sid].scroll = 0
|
|
435
|
+
data[sid].expanded = ""
|
|
436
|
+
data[sid].clearedIds = [...new Set([...(data[sid].clearedIds ?? []), ...ids])]
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
|
|
440
|
+
globalEntryCache.delete(sid)
|
|
441
|
+
setClearTick((v) => v + 1)
|
|
442
|
+
const msg = t("clear.done", { n: count })
|
|
443
|
+
api.ui.toast({ message: msg })
|
|
444
|
+
} catch {}
|
|
445
|
+
dialog?.clear()
|
|
446
|
+
}}
|
|
447
|
+
/>
|
|
448
|
+
))
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
title: "SubAgent Magazine: Border",
|
|
453
|
+
value: "subagent-border",
|
|
454
|
+
description: "Show or hide the panel border",
|
|
455
|
+
slash: { name: "subagent-border" },
|
|
456
|
+
onSelect: (dialog) => {
|
|
457
|
+
const t = createT(() => signals.lang())
|
|
458
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, false))
|
|
459
|
+
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
460
|
+
signals.setBorderVisible(!cur)
|
|
461
|
+
api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") })
|
|
462
|
+
dialog?.clear()
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
])
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const mod: TuiPluginModule & { id: string } = {
|
|
469
|
+
id: "opencode-subagent-magazine",
|
|
470
|
+
tui,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
473
|
export default mod
|