opencode-subagent-magazine 1.6.0-beta.3 → 1.6.0-beta.4
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 +3 -1
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/kv.d.ts +1 -0
- package/dist/core/kv.js +1 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +8 -0
- package/dist/index.js +17 -2
- package/dist/panel/SubAgentPanel.d.ts +1 -0
- package/dist/panel/SubAgentPanel.js +21 -9
- package/dist/tui.js +80 -27
- package/dist/v2/commands.js +13 -0
- package/dist/v2/index.js +3 -2
- package/dist/v2.js +76 -27
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- 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 -36
- package/src/core/state-machine.ts +46 -46
- package/src/core/types.ts +2 -0
- package/src/core/usage.ts +16 -16
- package/src/i18n.ts +8 -0
- package/src/index.tsx +472 -454
- package/src/panel/SubAgentPanel.tsx +27 -10
- package/src/panel/store.ts +8 -8
- package/src/v2/commands.ts +264 -251
- package/src/v2/index.tsx +102 -98
- package/src/v2/theme.ts +14 -14
- package/src/v2/types.ts +165 -165
- package/tui/index.js +11 -11
|
@@ -39,6 +39,7 @@ export function SubAgentPanel(props: {
|
|
|
39
39
|
maxEntries: () => number
|
|
40
40
|
sortOrder: () => SortOrder
|
|
41
41
|
scrollMode: () => ScrollMode
|
|
42
|
+
borderVisible: () => boolean
|
|
42
43
|
sessionId: string
|
|
43
44
|
}): JSX.Element {
|
|
44
45
|
const t = createT(() => props.lang())
|
|
@@ -1017,7 +1018,21 @@ export function SubAgentPanel(props: {
|
|
|
1017
1018
|
})
|
|
1018
1019
|
}
|
|
1019
1020
|
|
|
1020
|
-
|
|
1021
|
+
/** Horizontal space eaten by the panel border (1+1) and its padding (2+2). */
|
|
1022
|
+
const gutter = createMemo(() => (props.borderVisible() ? 6 : 0))
|
|
1023
|
+
|
|
1024
|
+
const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - gutter()))
|
|
1025
|
+
|
|
1026
|
+
// The border toggling shifts the box dimensions, which may not reliably
|
|
1027
|
+
// re-fire onSizeChange across (re)mount cycles; re-sync panelWidth with the
|
|
1028
|
+
// live box so the content width stays correct.
|
|
1029
|
+
createEffect(() => {
|
|
1030
|
+
props.borderVisible()
|
|
1031
|
+
if (boxEl && typeof boxEl.width === "number" && boxEl.width > 0) {
|
|
1032
|
+
const w = Math.max(20, boxEl.width)
|
|
1033
|
+
setPanelWidth((prev) => (prev === w ? prev : w))
|
|
1034
|
+
}
|
|
1035
|
+
})
|
|
1021
1036
|
|
|
1022
1037
|
// ── expanded detail right-align ──
|
|
1023
1038
|
const expandedMaxLabelW = createMemo(() => {
|
|
@@ -1030,7 +1045,7 @@ export function SubAgentPanel(props: {
|
|
|
1030
1045
|
|
|
1031
1046
|
const expandedPad = (label: string) => Math.max(0, expandedMaxLabelW() - visualWidth(label + ": "))
|
|
1032
1047
|
|
|
1033
|
-
const expandedValAvail = () => Math.max(6, panelWidth() - INDENT - expandedMaxLabelW())
|
|
1048
|
+
const expandedValAvail = () => Math.max(6, panelWidth() - gutter() - INDENT - expandedMaxLabelW())
|
|
1034
1049
|
|
|
1035
1050
|
// ── header parts for colored spans ──
|
|
1036
1051
|
const summaryParts = createMemo(() => {
|
|
@@ -1064,7 +1079,7 @@ export function SubAgentPanel(props: {
|
|
|
1064
1079
|
if (!open()) return false
|
|
1065
1080
|
const icon = "\u25bc"
|
|
1066
1081
|
const need = visualWidth(icon) + 1 + visualWidth(t("panel.title")) + versionW + summaryCols()
|
|
1067
|
-
return need <= panelWidth()
|
|
1082
|
+
return need <= panelWidth() - gutter()
|
|
1068
1083
|
})
|
|
1069
1084
|
|
|
1070
1085
|
const leftCols = createMemo(() => {
|
|
@@ -1076,17 +1091,19 @@ export function SubAgentPanel(props: {
|
|
|
1076
1091
|
|
|
1077
1092
|
const spacerCols = createMemo(() => {
|
|
1078
1093
|
if (!anyEntry()) return 0
|
|
1079
|
-
return Math.max(0, panelWidth() - leftCols() - summaryCols())
|
|
1094
|
+
return Math.max(0, panelWidth() - gutter() - leftCols() - summaryCols())
|
|
1080
1095
|
})
|
|
1081
1096
|
|
|
1082
1097
|
const valueCols = (label: string) =>
|
|
1083
|
-
Math.max(4, panelWidth() - INDENT - visualWidth(label + ": "))
|
|
1098
|
+
Math.max(4, panelWidth() - gutter() - INDENT - visualWidth(label + ": "))
|
|
1084
1099
|
|
|
1085
1100
|
// ── render ──
|
|
1086
1101
|
return (
|
|
1087
1102
|
<box
|
|
1088
|
-
border={
|
|
1089
|
-
|
|
1103
|
+
border={props.borderVisible()}
|
|
1104
|
+
{...(props.borderVisible() ? { borderColor: pal().border } : {})}
|
|
1105
|
+
paddingTop={0} paddingBottom={0}
|
|
1106
|
+
paddingLeft={props.borderVisible() ? 2 : 0} paddingRight={props.borderVisible() ? 2 : 0}
|
|
1090
1107
|
flexDirection="column" gap={0}
|
|
1091
1108
|
ref={boxEl}
|
|
1092
1109
|
onSizeChange={() => {
|
|
@@ -1223,7 +1240,7 @@ export function SubAgentPanel(props: {
|
|
|
1223
1240
|
if (tk) w += visualWidth(tk)
|
|
1224
1241
|
return w
|
|
1225
1242
|
}
|
|
1226
|
-
const labelAvail = () => Math.max(6, panelWidth() - LEFT_PAD - suffixW())
|
|
1243
|
+
const labelAvail = () => Math.max(6, panelWidth() - gutter() - LEFT_PAD - suffixW())
|
|
1227
1244
|
const labelText = () => {
|
|
1228
1245
|
const max = labelAvail()
|
|
1229
1246
|
const text = entry.title || entry.agent
|
|
@@ -1365,7 +1382,7 @@ export function SubAgentPanel(props: {
|
|
|
1365
1382
|
const cancelLabel = () => ` ${t("cancel.label")}`
|
|
1366
1383
|
const dismissLabel = () => ` ${t("dismiss.label")}`
|
|
1367
1384
|
const rightW = (isRunning ? visualWidth(dismissLabel()) : 0) + (isRunning && entry.sessionId ? visualWidth(cancelLabel()) : 0)
|
|
1368
|
-
const spacerW = () => Math.max(1, panelWidth() - openW() - rightW - 2)
|
|
1385
|
+
const spacerW = () => Math.max(1, panelWidth() - gutter() - openW() - rightW - 2)
|
|
1369
1386
|
return (
|
|
1370
1387
|
<box flexDirection="row">
|
|
1371
1388
|
<Show when={entry.sessionId}>
|
|
@@ -1422,7 +1439,7 @@ export function SubAgentPanel(props: {
|
|
|
1422
1439
|
const right = props.sortOrder() === "desc"
|
|
1423
1440
|
? `\u2191 ${t("scroll.top")}`
|
|
1424
1441
|
: `\u2193 ${t("scroll.bottom")}`
|
|
1425
|
-
const pad = showTop ? Math.max(1, panelWidth() - visualWidth(left) - visualWidth(right)) : 0
|
|
1442
|
+
const pad = showTop ? Math.max(1, panelWidth() - gutter() - visualWidth(left) - visualWidth(right)) : 0
|
|
1426
1443
|
return (
|
|
1427
1444
|
<box flexDirection="row">
|
|
1428
1445
|
<text
|
package/src/panel/store.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createSignal } from "solid-js"
|
|
2
|
-
import type { SubEntry } from "../core/types"
|
|
3
|
-
|
|
4
|
-
/** 模块级缓存:各 session 的 entry 状态独立存储,不随当前视图切换而清除。 */
|
|
5
|
-
export const globalEntryCache = new Map<string, Map<string, SubEntry>>()
|
|
6
|
-
|
|
7
|
-
/** 模块级刷新信号:外部(如斜杠命令)触发清除后 +1,组件 scan 依赖它以重扫。 */
|
|
8
|
-
export const [clearTick, setClearTick] = createSignal(0)
|
|
1
|
+
import { createSignal } from "solid-js"
|
|
2
|
+
import type { SubEntry } from "../core/types"
|
|
3
|
+
|
|
4
|
+
/** 模块级缓存:各 session 的 entry 状态独立存储,不随当前视图切换而清除。 */
|
|
5
|
+
export const globalEntryCache = new Map<string, Map<string, SubEntry>>()
|
|
6
|
+
|
|
7
|
+
/** 模块级刷新信号:外部(如斜杠命令)触发清除后 +1,组件 scan 依赖它以重扫。 */
|
|
8
|
+
export const [clearTick, setClearTick] = createSignal(0)
|
package/src/v2/commands.ts
CHANGED
|
@@ -1,251 +1,264 @@
|
|
|
1
|
-
import type { Context, KeymapCommand } from "./types"
|
|
2
|
-
import type { PanelApi } from "../panel/panel-api"
|
|
3
|
-
import type { Lang, SharedSignals, SubStatus } from "../core/types"
|
|
4
|
-
import { KV_PREFIX, SETTING_KEYS, loadSessionData, saveSessionData, readTTLDays } from "../core/kv"
|
|
5
|
-
import { PLUGIN_VERSION } from "../_version"
|
|
6
|
-
import { LANG_META, createT } from "../i18n"
|
|
7
|
-
import { globalEntryCache, setClearTick } from "../panel/store"
|
|
8
|
-
|
|
9
|
-
/** V2 命令(对齐 V1 的 9 个斜杠命令——promise 式对话框)。 */
|
|
10
|
-
export function makeCommands(context: Context, api: PanelApi, signals: SharedSignals): KeymapCommand[] {
|
|
11
|
-
const t = createT(() => signals.lang())
|
|
12
|
-
const kv = api.kv
|
|
13
|
-
const clampMax = (n: number) => Math.max(1, Math.min(50, n))
|
|
14
|
-
|
|
15
|
-
const resolveParent = (sid: string): { parentSid: string; isChild: boolean } => {
|
|
16
|
-
try {
|
|
17
|
-
const session = api.session.get(sid) as any
|
|
18
|
-
const parentID = session?.parentID as string | undefined
|
|
19
|
-
if (parentID) return { parentSid: parentID, isChild: true }
|
|
20
|
-
} catch {}
|
|
21
|
-
return { parentSid: sid, isChild: false }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return [
|
|
25
|
-
{
|
|
26
|
-
id: "opencode-subagent-magazine.subagent.lang",
|
|
27
|
-
title: "SubAgent Magazine: Language",
|
|
28
|
-
description: "Switch display language (中文 / English / 日本語 / 한국어)",
|
|
29
|
-
slash: { name: "subagent-lang" },
|
|
30
|
-
palette: true,
|
|
31
|
-
run: async () => {
|
|
32
|
-
const lang = await context.ui.dialog.select<Lang>({
|
|
33
|
-
title: "Language / 语言",
|
|
34
|
-
options: LANG_META.map((m) => ({ title: m.label, value: m.code })),
|
|
35
|
-
})
|
|
36
|
-
if (!lang) return
|
|
37
|
-
signals.setLang(lang)
|
|
38
|
-
kv.set(SETTING_KEYS.lang, lang)
|
|
39
|
-
api.ui.toast("Language: " + (LANG_META.find((m) => m.code === lang)?.label ?? lang))
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: "opencode-subagent-magazine.subagent.order",
|
|
44
|
-
title: "SubAgent Magazine: Sort Order",
|
|
45
|
-
description: "Set sub-agent entry sort order (desc / asc)",
|
|
46
|
-
slash: { name: "subagent-order" },
|
|
47
|
-
palette: true,
|
|
48
|
-
run: async () => {
|
|
49
|
-
const order = await context.ui.dialog.select<"desc" | "asc">({
|
|
50
|
-
title: "Sort Order / 排序方式",
|
|
51
|
-
options: [
|
|
52
|
-
{ title: t("order.desc"), value: "desc" },
|
|
53
|
-
{ title: t("order.asc"), value: "asc" },
|
|
54
|
-
],
|
|
55
|
-
})
|
|
56
|
-
if (!order) return
|
|
57
|
-
signals.setSortOrder(order)
|
|
58
|
-
kv.set(SETTING_KEYS.order, order)
|
|
59
|
-
api.ui.toast(order === "desc" ? t("order.desc") : t("order.asc"))
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "opencode-subagent-magazine.subagent.scroll",
|
|
64
|
-
title: "SubAgent Magazine: Scroll Mode",
|
|
65
|
-
description: "Set scroll mode (wheel / click)",
|
|
66
|
-
slash: { name: "subagent-scroll" },
|
|
67
|
-
palette: true,
|
|
68
|
-
run: async () => {
|
|
69
|
-
const mode = await context.ui.dialog.select<"wheel" | "click">({
|
|
70
|
-
title: "Scroll Mode / 滚动模式",
|
|
71
|
-
options: [
|
|
72
|
-
{ title: t("scroll.wheel"), value: "wheel" },
|
|
73
|
-
{ title: t("scroll.click"), value: "click" },
|
|
74
|
-
],
|
|
75
|
-
})
|
|
76
|
-
if (!mode) return
|
|
77
|
-
signals.setScrollMode(mode)
|
|
78
|
-
kv.set(SETTING_KEYS.scrollMode, mode)
|
|
79
|
-
api.ui.toast(mode === "wheel" ? t("scroll.wheel") : t("scroll.click"))
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
id: "opencode-subagent-magazine.subagent.max",
|
|
84
|
-
title: "SubAgent Magazine: Max Entries",
|
|
85
|
-
description: "Set max visible sub-agent entries in sidebar",
|
|
86
|
-
slash: { name: "subagent-max" },
|
|
87
|
-
palette: true,
|
|
88
|
-
run: async () => {
|
|
89
|
-
const val = await context.ui.dialog.prompt({
|
|
90
|
-
title: "Max Visible Entries",
|
|
91
|
-
message: "Number of entries to show in the sidebar (1–50)",
|
|
92
|
-
placeholder: String(signals.maxEntries()),
|
|
93
|
-
})
|
|
94
|
-
if (val === undefined) return
|
|
95
|
-
const n = clampMax(parseInt(val, 10) || 10)
|
|
96
|
-
signals.setMaxEntries(n)
|
|
97
|
-
kv.set(SETTING_KEYS.maxEntries, n)
|
|
98
|
-
api.ui.toast(`Max entries: ${n}`)
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
id: "opencode-subagent-magazine.subagent.version",
|
|
103
|
-
title: "SubAgent Magazine: Version",
|
|
104
|
-
description: "Show plugin version",
|
|
105
|
-
slash: { name: "subagent-version" },
|
|
106
|
-
palette: true,
|
|
107
|
-
run: () => {
|
|
108
|
-
api.ui.toast(`opencode-subagent-magazine v${PLUGIN_VERSION}`)
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
id: "opencode-subagent-magazine.subagent.session",
|
|
113
|
-
title: "SubAgent Magazine: Session",
|
|
114
|
-
description: "Show current session ID",
|
|
115
|
-
slash: { name: "subagent-session" },
|
|
116
|
-
palette: true,
|
|
117
|
-
run: () => {
|
|
118
|
-
api.ui.toast(`Session: ${signals.sessionId}`)
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
id: "opencode-subagent-magazine.subagent.clear-running",
|
|
123
|
-
title: "SubAgent Magazine: Clear Running",
|
|
124
|
-
description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
|
|
125
|
-
slash: { name: "subagent-clear-running" },
|
|
126
|
-
palette: true,
|
|
127
|
-
run: () => {
|
|
128
|
-
const sid = signals.sessionId
|
|
129
|
-
const entries = globalEntryCache.get(sid)
|
|
130
|
-
if (!entries || entries.size === 0) {
|
|
131
|
-
api.ui.toast(signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found")
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
let count = 0
|
|
135
|
-
for (const [, entry] of entries) {
|
|
136
|
-
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
137
|
-
entry.status = "done" as SubStatus
|
|
138
|
-
entry.endedAt = Date.now()
|
|
139
|
-
count++
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (count > 0) {
|
|
143
|
-
try {
|
|
144
|
-
const data = loadSessionData(kv)
|
|
145
|
-
const { parentSid, isChild } = resolveParent(sid)
|
|
146
|
-
if (isChild) {
|
|
147
|
-
if (!data[parentSid]) data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} }
|
|
148
|
-
if (!data[parentSid].children) data[parentSid].children = {}
|
|
149
|
-
if (!data[parentSid].children[sid]) data[parentSid].children[sid] = { scroll: 0, expanded: "", entries: [] }
|
|
150
|
-
data[parentSid].children[sid] = { ...data[parentSid].children[sid], entries: [...entries.values()] }
|
|
151
|
-
} else {
|
|
152
|
-
data[sid] = {
|
|
153
|
-
ts: Date.now(),
|
|
154
|
-
entries: [...entries.values()],
|
|
155
|
-
scroll: data[sid]?.scroll ?? 0,
|
|
156
|
-
expanded: data[sid]?.expanded ?? "",
|
|
157
|
-
children: data[sid]?.children ?? {},
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
saveSessionData(kv, data)
|
|
161
|
-
} catch {}
|
|
162
|
-
api.ui.toast(signals.lang() === "zh"
|
|
163
|
-
? `已标记 ${count} 个运行中的条目为完成`
|
|
164
|
-
: `Marked ${count} running entries as done`)
|
|
165
|
-
} else {
|
|
166
|
-
api.ui.toast(signals.lang() === "zh" ? "没有需要清理的运行中条目" : "No running entries to clear")
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
id: "opencode-subagent-magazine.subagent.ttl",
|
|
172
|
-
title: "SubAgent Magazine: TTL",
|
|
173
|
-
description: "Set session data retention period (days before auto-cleanup)",
|
|
174
|
-
slash: { name: "subagent-ttl" },
|
|
175
|
-
palette: true,
|
|
176
|
-
run: async () => {
|
|
177
|
-
const curDays = readTTLDays(kv)
|
|
178
|
-
const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`
|
|
179
|
-
const days = await context.ui.dialog.select<number>({
|
|
180
|
-
title: `${t("ttl.label")} (${curLabel})`,
|
|
181
|
-
options: [
|
|
182
|
-
{ title: t("ttl.3d"), value: 3 },
|
|
183
|
-
{ title: t("ttl.7d"), value: 7 },
|
|
184
|
-
{ title: t("ttl.14d"), value: 14 },
|
|
185
|
-
{ title: t("ttl.30d"), value: 30 },
|
|
186
|
-
{ title: t("ttl.unlimited"), value: 0 },
|
|
187
|
-
],
|
|
188
|
-
})
|
|
189
|
-
if (days === undefined) return
|
|
190
|
-
kv.set(SETTING_KEYS.ttlDays, String(days))
|
|
191
|
-
api.ui.toast(days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days }))
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
id: "opencode-subagent-magazine.subagent.clear-entries",
|
|
196
|
-
title: "SubAgent Magazine: Clear Entries",
|
|
197
|
-
description: "Delete all sub-agent records for the current session (cannot be undone)",
|
|
198
|
-
slash: { name: "subagent-clear-entries" },
|
|
199
|
-
palette: true,
|
|
200
|
-
run: async () => {
|
|
201
|
-
const sid = signals.sessionId
|
|
202
|
-
const sessionObj = api.session.get(sid)
|
|
203
|
-
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
204
|
-
const cached = globalEntryCache.get(sid)
|
|
205
|
-
let runningCount = 0
|
|
206
|
-
if (cached) {
|
|
207
|
-
for (const [, e] of cached) { if (e.status === "running") runningCount++ }
|
|
208
|
-
}
|
|
209
|
-
const msg = runningCount > 0 ? t("clear.prompt_running", { n: runningCount }) : t("clear.prompt")
|
|
210
|
-
const choice = await context.ui.dialog.select<"yes" | "no">({
|
|
211
|
-
title: t("clear.title"),
|
|
212
|
-
options: [
|
|
213
|
-
{ title: t("clear.title"), value: "yes" },
|
|
214
|
-
{ title: t("cancel.label"), value: "no" },
|
|
215
|
-
],
|
|
216
|
-
})
|
|
217
|
-
if (choice !== "yes") return
|
|
218
|
-
try {
|
|
219
|
-
const data = loadSessionData(kv)
|
|
220
|
-
let count = 0
|
|
221
|
-
if (parentID) {
|
|
222
|
-
if (data[parentID]?.children?.[sid]) {
|
|
223
|
-
const child = data[parentID].children[sid]
|
|
224
|
-
const ids = child.entries?.map((e) => e.id) ?? []
|
|
225
|
-
count = ids.length
|
|
226
|
-
child.entries = []
|
|
227
|
-
child.scroll = 0
|
|
228
|
-
child.expanded = ""
|
|
229
|
-
child.clearedIds = [...new Set([...(child.clearedIds ?? []), ...ids])]
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
count = data[sid]?.entries?.length ?? 0
|
|
233
|
-
if (data[sid]) {
|
|
234
|
-
const ids = data[sid].entries?.map((e) => e.id) ?? []
|
|
235
|
-
data[sid].entries = []
|
|
236
|
-
data[sid].scroll = 0
|
|
237
|
-
data[sid].expanded = ""
|
|
238
|
-
data[sid].clearedIds = [...new Set([...(data[sid].clearedIds ?? []), ...ids])]
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
saveSessionData(kv, data)
|
|
242
|
-
globalEntryCache.delete(sid)
|
|
243
|
-
setClearTick((v) => v + 1)
|
|
244
|
-
api.ui.toast(t("clear.done", { n: count }))
|
|
245
|
-
} catch {}
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1
|
+
import type { Context, KeymapCommand } from "./types"
|
|
2
|
+
import type { PanelApi } from "../panel/panel-api"
|
|
3
|
+
import type { Lang, SharedSignals, SubStatus } from "../core/types"
|
|
4
|
+
import { KV_PREFIX, SETTING_KEYS, loadSessionData, saveSessionData, readTTLDays } from "../core/kv"
|
|
5
|
+
import { PLUGIN_VERSION } from "../_version"
|
|
6
|
+
import { LANG_META, createT } from "../i18n"
|
|
7
|
+
import { globalEntryCache, setClearTick } from "../panel/store"
|
|
8
|
+
|
|
9
|
+
/** V2 命令(对齐 V1 的 9 个斜杠命令——promise 式对话框)。 */
|
|
10
|
+
export function makeCommands(context: Context, api: PanelApi, signals: SharedSignals): KeymapCommand[] {
|
|
11
|
+
const t = createT(() => signals.lang())
|
|
12
|
+
const kv = api.kv
|
|
13
|
+
const clampMax = (n: number) => Math.max(1, Math.min(50, n))
|
|
14
|
+
|
|
15
|
+
const resolveParent = (sid: string): { parentSid: string; isChild: boolean } => {
|
|
16
|
+
try {
|
|
17
|
+
const session = api.session.get(sid) as any
|
|
18
|
+
const parentID = session?.parentID as string | undefined
|
|
19
|
+
if (parentID) return { parentSid: parentID, isChild: true }
|
|
20
|
+
} catch {}
|
|
21
|
+
return { parentSid: sid, isChild: false }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
id: "opencode-subagent-magazine.subagent.lang",
|
|
27
|
+
title: "SubAgent Magazine: Language",
|
|
28
|
+
description: "Switch display language (中文 / English / 日本語 / 한국어)",
|
|
29
|
+
slash: { name: "subagent-lang" },
|
|
30
|
+
palette: true,
|
|
31
|
+
run: async () => {
|
|
32
|
+
const lang = await context.ui.dialog.select<Lang>({
|
|
33
|
+
title: "Language / 语言",
|
|
34
|
+
options: LANG_META.map((m) => ({ title: m.label, value: m.code })),
|
|
35
|
+
})
|
|
36
|
+
if (!lang) return
|
|
37
|
+
signals.setLang(lang)
|
|
38
|
+
kv.set(SETTING_KEYS.lang, lang)
|
|
39
|
+
api.ui.toast("Language: " + (LANG_META.find((m) => m.code === lang)?.label ?? lang))
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "opencode-subagent-magazine.subagent.order",
|
|
44
|
+
title: "SubAgent Magazine: Sort Order",
|
|
45
|
+
description: "Set sub-agent entry sort order (desc / asc)",
|
|
46
|
+
slash: { name: "subagent-order" },
|
|
47
|
+
palette: true,
|
|
48
|
+
run: async () => {
|
|
49
|
+
const order = await context.ui.dialog.select<"desc" | "asc">({
|
|
50
|
+
title: "Sort Order / 排序方式",
|
|
51
|
+
options: [
|
|
52
|
+
{ title: t("order.desc"), value: "desc" },
|
|
53
|
+
{ title: t("order.asc"), value: "asc" },
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
if (!order) return
|
|
57
|
+
signals.setSortOrder(order)
|
|
58
|
+
kv.set(SETTING_KEYS.order, order)
|
|
59
|
+
api.ui.toast(order === "desc" ? t("order.desc") : t("order.asc"))
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "opencode-subagent-magazine.subagent.scroll",
|
|
64
|
+
title: "SubAgent Magazine: Scroll Mode",
|
|
65
|
+
description: "Set scroll mode (wheel / click)",
|
|
66
|
+
slash: { name: "subagent-scroll" },
|
|
67
|
+
palette: true,
|
|
68
|
+
run: async () => {
|
|
69
|
+
const mode = await context.ui.dialog.select<"wheel" | "click">({
|
|
70
|
+
title: "Scroll Mode / 滚动模式",
|
|
71
|
+
options: [
|
|
72
|
+
{ title: t("scroll.wheel"), value: "wheel" },
|
|
73
|
+
{ title: t("scroll.click"), value: "click" },
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
if (!mode) return
|
|
77
|
+
signals.setScrollMode(mode)
|
|
78
|
+
kv.set(SETTING_KEYS.scrollMode, mode)
|
|
79
|
+
api.ui.toast(mode === "wheel" ? t("scroll.wheel") : t("scroll.click"))
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "opencode-subagent-magazine.subagent.max",
|
|
84
|
+
title: "SubAgent Magazine: Max Entries",
|
|
85
|
+
description: "Set max visible sub-agent entries in sidebar",
|
|
86
|
+
slash: { name: "subagent-max" },
|
|
87
|
+
palette: true,
|
|
88
|
+
run: async () => {
|
|
89
|
+
const val = await context.ui.dialog.prompt({
|
|
90
|
+
title: "Max Visible Entries",
|
|
91
|
+
message: "Number of entries to show in the sidebar (1–50)",
|
|
92
|
+
placeholder: String(signals.maxEntries()),
|
|
93
|
+
})
|
|
94
|
+
if (val === undefined) return
|
|
95
|
+
const n = clampMax(parseInt(val, 10) || 10)
|
|
96
|
+
signals.setMaxEntries(n)
|
|
97
|
+
kv.set(SETTING_KEYS.maxEntries, n)
|
|
98
|
+
api.ui.toast(`Max entries: ${n}`)
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "opencode-subagent-magazine.subagent.version",
|
|
103
|
+
title: "SubAgent Magazine: Version",
|
|
104
|
+
description: "Show plugin version",
|
|
105
|
+
slash: { name: "subagent-version" },
|
|
106
|
+
palette: true,
|
|
107
|
+
run: () => {
|
|
108
|
+
api.ui.toast(`opencode-subagent-magazine v${PLUGIN_VERSION}`)
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "opencode-subagent-magazine.subagent.session",
|
|
113
|
+
title: "SubAgent Magazine: Session",
|
|
114
|
+
description: "Show current session ID",
|
|
115
|
+
slash: { name: "subagent-session" },
|
|
116
|
+
palette: true,
|
|
117
|
+
run: () => {
|
|
118
|
+
api.ui.toast(`Session: ${signals.sessionId}`)
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "opencode-subagent-magazine.subagent.clear-running",
|
|
123
|
+
title: "SubAgent Magazine: Clear Running",
|
|
124
|
+
description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
|
|
125
|
+
slash: { name: "subagent-clear-running" },
|
|
126
|
+
palette: true,
|
|
127
|
+
run: () => {
|
|
128
|
+
const sid = signals.sessionId
|
|
129
|
+
const entries = globalEntryCache.get(sid)
|
|
130
|
+
if (!entries || entries.size === 0) {
|
|
131
|
+
api.ui.toast(signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found")
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
let count = 0
|
|
135
|
+
for (const [, entry] of entries) {
|
|
136
|
+
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
137
|
+
entry.status = "done" as SubStatus
|
|
138
|
+
entry.endedAt = Date.now()
|
|
139
|
+
count++
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (count > 0) {
|
|
143
|
+
try {
|
|
144
|
+
const data = loadSessionData(kv)
|
|
145
|
+
const { parentSid, isChild } = resolveParent(sid)
|
|
146
|
+
if (isChild) {
|
|
147
|
+
if (!data[parentSid]) data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} }
|
|
148
|
+
if (!data[parentSid].children) data[parentSid].children = {}
|
|
149
|
+
if (!data[parentSid].children[sid]) data[parentSid].children[sid] = { scroll: 0, expanded: "", entries: [] }
|
|
150
|
+
data[parentSid].children[sid] = { ...data[parentSid].children[sid], entries: [...entries.values()] }
|
|
151
|
+
} else {
|
|
152
|
+
data[sid] = {
|
|
153
|
+
ts: Date.now(),
|
|
154
|
+
entries: [...entries.values()],
|
|
155
|
+
scroll: data[sid]?.scroll ?? 0,
|
|
156
|
+
expanded: data[sid]?.expanded ?? "",
|
|
157
|
+
children: data[sid]?.children ?? {},
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
saveSessionData(kv, data)
|
|
161
|
+
} catch {}
|
|
162
|
+
api.ui.toast(signals.lang() === "zh"
|
|
163
|
+
? `已标记 ${count} 个运行中的条目为完成`
|
|
164
|
+
: `Marked ${count} running entries as done`)
|
|
165
|
+
} else {
|
|
166
|
+
api.ui.toast(signals.lang() === "zh" ? "没有需要清理的运行中条目" : "No running entries to clear")
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "opencode-subagent-magazine.subagent.ttl",
|
|
172
|
+
title: "SubAgent Magazine: TTL",
|
|
173
|
+
description: "Set session data retention period (days before auto-cleanup)",
|
|
174
|
+
slash: { name: "subagent-ttl" },
|
|
175
|
+
palette: true,
|
|
176
|
+
run: async () => {
|
|
177
|
+
const curDays = readTTLDays(kv)
|
|
178
|
+
const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`
|
|
179
|
+
const days = await context.ui.dialog.select<number>({
|
|
180
|
+
title: `${t("ttl.label")} (${curLabel})`,
|
|
181
|
+
options: [
|
|
182
|
+
{ title: t("ttl.3d"), value: 3 },
|
|
183
|
+
{ title: t("ttl.7d"), value: 7 },
|
|
184
|
+
{ title: t("ttl.14d"), value: 14 },
|
|
185
|
+
{ title: t("ttl.30d"), value: 30 },
|
|
186
|
+
{ title: t("ttl.unlimited"), value: 0 },
|
|
187
|
+
],
|
|
188
|
+
})
|
|
189
|
+
if (days === undefined) return
|
|
190
|
+
kv.set(SETTING_KEYS.ttlDays, String(days))
|
|
191
|
+
api.ui.toast(days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days }))
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "opencode-subagent-magazine.subagent.clear-entries",
|
|
196
|
+
title: "SubAgent Magazine: Clear Entries",
|
|
197
|
+
description: "Delete all sub-agent records for the current session (cannot be undone)",
|
|
198
|
+
slash: { name: "subagent-clear-entries" },
|
|
199
|
+
palette: true,
|
|
200
|
+
run: async () => {
|
|
201
|
+
const sid = signals.sessionId
|
|
202
|
+
const sessionObj = api.session.get(sid)
|
|
203
|
+
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
204
|
+
const cached = globalEntryCache.get(sid)
|
|
205
|
+
let runningCount = 0
|
|
206
|
+
if (cached) {
|
|
207
|
+
for (const [, e] of cached) { if (e.status === "running") runningCount++ }
|
|
208
|
+
}
|
|
209
|
+
const msg = runningCount > 0 ? t("clear.prompt_running", { n: runningCount }) : t("clear.prompt")
|
|
210
|
+
const choice = await context.ui.dialog.select<"yes" | "no">({
|
|
211
|
+
title: t("clear.title"),
|
|
212
|
+
options: [
|
|
213
|
+
{ title: t("clear.title"), value: "yes" },
|
|
214
|
+
{ title: t("cancel.label"), value: "no" },
|
|
215
|
+
],
|
|
216
|
+
})
|
|
217
|
+
if (choice !== "yes") return
|
|
218
|
+
try {
|
|
219
|
+
const data = loadSessionData(kv)
|
|
220
|
+
let count = 0
|
|
221
|
+
if (parentID) {
|
|
222
|
+
if (data[parentID]?.children?.[sid]) {
|
|
223
|
+
const child = data[parentID].children[sid]
|
|
224
|
+
const ids = child.entries?.map((e) => e.id) ?? []
|
|
225
|
+
count = ids.length
|
|
226
|
+
child.entries = []
|
|
227
|
+
child.scroll = 0
|
|
228
|
+
child.expanded = ""
|
|
229
|
+
child.clearedIds = [...new Set([...(child.clearedIds ?? []), ...ids])]
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
count = data[sid]?.entries?.length ?? 0
|
|
233
|
+
if (data[sid]) {
|
|
234
|
+
const ids = data[sid].entries?.map((e) => e.id) ?? []
|
|
235
|
+
data[sid].entries = []
|
|
236
|
+
data[sid].scroll = 0
|
|
237
|
+
data[sid].expanded = ""
|
|
238
|
+
data[sid].clearedIds = [...new Set([...(data[sid].clearedIds ?? []), ...ids])]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
saveSessionData(kv, data)
|
|
242
|
+
globalEntryCache.delete(sid)
|
|
243
|
+
setClearTick((v) => v + 1)
|
|
244
|
+
api.ui.toast(t("clear.done", { n: count }))
|
|
245
|
+
} catch {}
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "opencode-subagent-magazine.subagent.border",
|
|
250
|
+
title: "SubAgent Magazine: Border",
|
|
251
|
+
description: "Show or hide the panel border",
|
|
252
|
+
slash: { name: "subagent-border" },
|
|
253
|
+
palette: true,
|
|
254
|
+
run: () => {
|
|
255
|
+
const cur = Boolean(kv.get(SETTING_KEYS.border, false))
|
|
256
|
+
kv.set(SETTING_KEYS.border, !cur)
|
|
257
|
+
signals.setBorderVisible(!cur)
|
|
258
|
+
api.ui.toast(!cur ? t("borderShown") : t("borderHidden"))
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export { KV_PREFIX }
|