opencode-subagent-magazine 1.5.2 → 1.6.0-beta.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 +192 -192
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/color.d.ts +20 -0
- package/dist/core/color.js +68 -0
- package/dist/core/format.d.ts +5 -0
- package/dist/core/format.js +68 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +6 -0
- package/dist/core/kv.d.ts +20 -0
- package/dist/core/kv.js +30 -0
- package/dist/core/state-machine.d.ts +17 -0
- package/dist/core/state-machine.js +27 -0
- package/dist/core/types.d.ts +51 -0
- package/dist/core/types.js +2 -0
- package/dist/core/usage.d.ts +15 -0
- package/dist/core/usage.js +1 -0
- package/dist/index.js +148 -1484
- package/dist/panel/SubAgentPanel.d.ts +13 -0
- package/dist/panel/SubAgentPanel.js +1232 -0
- package/dist/panel/panel-api.d.ts +67 -0
- package/dist/panel/panel-api.js +1 -0
- package/dist/panel/store.d.ts +5 -0
- package/dist/panel/store.js +5 -0
- package/dist/tui.js +383 -289
- package/dist/v2/commands.d.ts +7 -0
- package/dist/v2/commands.js +263 -0
- package/dist/v2/index.d.ts +8 -0
- package/dist/v2/index.js +62 -0
- package/dist/v2/theme.d.ts +3 -0
- package/dist/v2/theme.js +12 -0
- package/dist/v2/types.d.ts +231 -0
- package/dist/v2/types.js +6 -0
- package/dist/v2/v2-panel-api.d.ts +12 -0
- package/dist/v2/v2-panel-api.js +345 -0
- package/dist/v2.js +3003 -0
- package/install.mjs +103 -103
- package/package.json +64 -63
- package/src/_version.ts +1 -1
- package/src/clipboard.ts +134 -134
- package/src/core/color.ts +70 -0
- package/src/core/format.ts +61 -0
- package/src/core/index.ts +6 -0
- package/src/core/kv.ts +36 -0
- package/src/core/state-machine.ts +46 -0
- package/src/core/types.ts +58 -0
- package/src/core/usage.ts +16 -0
- package/src/i18n.ts +261 -261
- package/src/index.tsx +124 -1750
- package/src/panel/SubAgentPanel.tsx +1460 -0
- package/src/panel/panel-api.ts +72 -0
- package/src/panel/store.ts +8 -0
- package/src/server.ts +10 -10
- package/src/v2/commands.ts +251 -0
- package/src/v2/index.tsx +98 -0
- package/src/v2/theme.ts +14 -0
- package/src/v2/types.ts +165 -0
- package/src/v2/v2-panel-api.ts +301 -0
- package/tui/index.js +11 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { KVApi } from "../core/kv"
|
|
2
|
+
import type { UsageReader, TodoStats } from "../core/usage"
|
|
3
|
+
import type { Lang, SortOrder, ScrollMode } from "../core/types"
|
|
4
|
+
|
|
5
|
+
/** Minimal session shape the panel reads from the host SDK. */
|
|
6
|
+
export interface SessionLike {
|
|
7
|
+
id?: string
|
|
8
|
+
parentID?: string
|
|
9
|
+
agent?: string
|
|
10
|
+
cost?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionStatusLike {
|
|
14
|
+
type: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Standardized panel events. Adapter layers (V1/V2) translate host events
|
|
19
|
+
* into these shapes so the panel component never touches host APIs directly.
|
|
20
|
+
*/
|
|
21
|
+
export type PanelEventType = "part.updated" | "message.updated" | "session.idle" | "session.error"
|
|
22
|
+
|
|
23
|
+
export interface PanelEvent {
|
|
24
|
+
type: PanelEventType
|
|
25
|
+
/** part payload for part.updated; session properties for idle/error. */
|
|
26
|
+
payload?: Record<string, unknown>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ToastOptions {
|
|
30
|
+
variant?: "success" | "warning" | "error"
|
|
31
|
+
title?: string
|
|
32
|
+
duration?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The full surface the SubAgentPanel component consumes.
|
|
37
|
+
* V1 and V2 each implement this against their host SDK; the panel is agnostic.
|
|
38
|
+
* Theme is slot-scoped in both hosts, so it is passed as a panel prop,
|
|
39
|
+
* not part of the API.
|
|
40
|
+
*/
|
|
41
|
+
export interface PanelApi {
|
|
42
|
+
kv: KVApi
|
|
43
|
+
usage: UsageReader
|
|
44
|
+
session: {
|
|
45
|
+
get(sid: string): SessionLike | undefined
|
|
46
|
+
status(sid: string): SessionStatusLike | undefined
|
|
47
|
+
/** Raw message list for a session (scan / error extraction). */
|
|
48
|
+
messages(sid: string): unknown[] | undefined
|
|
49
|
+
/** Raw parts of a message (scan). */
|
|
50
|
+
part(messageID: string): unknown[] | undefined
|
|
51
|
+
}
|
|
52
|
+
event: {
|
|
53
|
+
on(type: PanelEventType, cb: (e: PanelEvent) => void): () => void
|
|
54
|
+
}
|
|
55
|
+
client: {
|
|
56
|
+
abort(input: { sessionID: string }): Promise<void>
|
|
57
|
+
}
|
|
58
|
+
route: {
|
|
59
|
+
navigateSession(sessionID: string): void
|
|
60
|
+
}
|
|
61
|
+
ui: {
|
|
62
|
+
toast(message: string, opts?: ToastOptions): void
|
|
63
|
+
}
|
|
64
|
+
settings: {
|
|
65
|
+
lang: () => Lang
|
|
66
|
+
maxEntries: () => number
|
|
67
|
+
sortOrder: () => SortOrder
|
|
68
|
+
scrollMode: () => ScrollMode
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type { TodoStats }
|
|
@@ -0,0 +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)
|
package/src/server.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
-
|
|
3
|
-
const server: Plugin = async () => ({})
|
|
4
|
-
|
|
5
|
-
const mod: PluginModule = {
|
|
6
|
-
id: "opencode-subagent-magazine",
|
|
7
|
-
server,
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export default mod
|
|
1
|
+
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
const server: Plugin = async () => ({})
|
|
4
|
+
|
|
5
|
+
const mod: PluginModule = {
|
|
6
|
+
id: "opencode-subagent-magazine",
|
|
7
|
+
server,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default mod
|
|
@@ -0,0 +1,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
|
+
}
|
|
250
|
+
|
|
251
|
+
export { KV_PREFIX }
|
package/src/v2/index.tsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { createSignal } from "solid-js"
|
|
4
|
+
import type { Context, PluginModule } from "./types"
|
|
5
|
+
import { createPanelApi } from "./v2-panel-api"
|
|
6
|
+
import { makeCommands } from "./commands"
|
|
7
|
+
import { mapTheme } from "./theme"
|
|
8
|
+
import { SubAgentPanel } from "../panel/SubAgentPanel"
|
|
9
|
+
import type { PanelApi } from "../panel/panel-api"
|
|
10
|
+
import type { Lang, SortOrder, ScrollMode, SharedSignals } from "../core/types"
|
|
11
|
+
import { SETTING_KEYS } from "../core/kv"
|
|
12
|
+
import { LANG_META, detectLang } from "../i18n"
|
|
13
|
+
|
|
14
|
+
/** 面板根组件:keymap.layer 必须在组件渲染上下文注册(setup 内调用报
|
|
15
|
+
* Keymap.Provider missing),再渲染共享 SubAgentPanel。 */
|
|
16
|
+
function PluginRoot(props: {
|
|
17
|
+
context: Context
|
|
18
|
+
api: PanelApi
|
|
19
|
+
signals: SharedSignals
|
|
20
|
+
sessionID: string
|
|
21
|
+
}) {
|
|
22
|
+
props.context.keymap.layer(() => ({
|
|
23
|
+
mode: "global" as const,
|
|
24
|
+
commands: makeCommands(props.context, props.api, props.signals),
|
|
25
|
+
}))
|
|
26
|
+
return (
|
|
27
|
+
<SubAgentPanel
|
|
28
|
+
api={props.api}
|
|
29
|
+
theme={mapTheme(props.context.theme)}
|
|
30
|
+
lang={props.signals.lang}
|
|
31
|
+
maxEntries={props.signals.maxEntries}
|
|
32
|
+
sortOrder={props.signals.sortOrder}
|
|
33
|
+
scrollMode={props.signals.scrollMode}
|
|
34
|
+
sessionId={props.sessionID}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** V2 入口:setup 创建共享信号 + PanelApi + 命令 layer + 侧边栏槽位。
|
|
40
|
+
* 行为对齐 V1 tui()(信号初始值从 KV 恢复;侧边栏渲染共享 SubAgentPanel)。 */
|
|
41
|
+
const mod: PluginModule & { server: () => Promise<Record<string, never>> } = {
|
|
42
|
+
id: "opencode-subagent-magazine",
|
|
43
|
+
setup(context: Context) {
|
|
44
|
+
// settings 惰性 getter(闭包引用信号变量——运行时已赋值)
|
|
45
|
+
let lang!: () => Lang
|
|
46
|
+
let maxEntries!: () => number
|
|
47
|
+
let sortOrder!: () => SortOrder
|
|
48
|
+
let scrollMode!: () => ScrollMode
|
|
49
|
+
const api = createPanelApi(context, {
|
|
50
|
+
lang: () => lang(),
|
|
51
|
+
maxEntries: () => maxEntries(),
|
|
52
|
+
sortOrder: () => sortOrder(),
|
|
53
|
+
scrollMode: () => scrollMode(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// 信号初始值从 KV 恢复(对齐 V1 tui())
|
|
57
|
+
const storedLang = String(api.kv.get(SETTING_KEYS.lang, ""))
|
|
58
|
+
const initialLang: Lang =
|
|
59
|
+
LANG_META.some((m) => m.code === storedLang) ? (storedLang as Lang) : detectLang()
|
|
60
|
+
const [langSignal, setLang] = createSignal<Lang>(initialLang)
|
|
61
|
+
const [maxSignal, setMaxEntries] = createSignal<number>(Number(api.kv.get(SETTING_KEYS.maxEntries, "10")) || 10)
|
|
62
|
+
const [orderSignal, setSortOrder] = createSignal<SortOrder>(
|
|
63
|
+
String(api.kv.get(SETTING_KEYS.order, "desc")) === "asc" ? "asc" : "desc",
|
|
64
|
+
)
|
|
65
|
+
const [scrollSignal, setScrollMode] = createSignal<ScrollMode>(
|
|
66
|
+
String(api.kv.get(SETTING_KEYS.scrollMode, "wheel")) === "click" ? "click" : "wheel",
|
|
67
|
+
)
|
|
68
|
+
lang = langSignal
|
|
69
|
+
maxEntries = maxSignal
|
|
70
|
+
sortOrder = orderSignal
|
|
71
|
+
scrollMode = scrollSignal
|
|
72
|
+
|
|
73
|
+
const signals: SharedSignals = {
|
|
74
|
+
lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, sessionId: "",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 命令 layer(组件内注册——见 PluginRoot)
|
|
78
|
+
// 侧边栏面板(共享 SubAgentPanel——V1/V2 同一组件)
|
|
79
|
+
context.ui.slot({
|
|
80
|
+
prepend: "sidebar.content",
|
|
81
|
+
render: (props) => {
|
|
82
|
+
signals.sessionId = String(props.sessionID ?? "")
|
|
83
|
+
return (
|
|
84
|
+
<PluginRoot
|
|
85
|
+
context={context}
|
|
86
|
+
api={api}
|
|
87
|
+
signals={signals}
|
|
88
|
+
sessionID={String(props.sessionID ?? "")}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
// V1 server 空实现(兼容标记):v2 加载 setup,V1 检测需要 server 字段识别为插件
|
|
95
|
+
server: async () => ({}),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default mod
|
package/src/v2/theme.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Context } from "./types"
|
|
2
|
+
|
|
3
|
+
/** V2 theme → V1 形状映射(组件按 primary/text/textMuted/… 字段消费)。 */
|
|
4
|
+
export function mapTheme(theme: Context["theme"]): Record<string, unknown> {
|
|
5
|
+
return {
|
|
6
|
+
primary: theme.hue.interactive[300],
|
|
7
|
+
text: theme.text.default,
|
|
8
|
+
textMuted: theme.text.subdued,
|
|
9
|
+
success: theme.text.feedback.success.default,
|
|
10
|
+
warning: theme.text.feedback.warning.default,
|
|
11
|
+
error: theme.text.feedback.error.default,
|
|
12
|
+
border: theme.text.subdued,
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/v2/types.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 (opencode2) TUI plugin API — 最小本地类型(实验版)。
|
|
3
|
+
* 运行时由 opencode2 提供,此处仅用于本地类型检查;
|
|
4
|
+
* 结构对应 v2 分支 packages/plugin/src/tui/context.ts。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface App {
|
|
8
|
+
readonly version: string
|
|
9
|
+
readonly channel: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Theme {
|
|
13
|
+
readonly hue: {
|
|
14
|
+
readonly interactive: { readonly 300: string }
|
|
15
|
+
readonly accent: { readonly 500: string }
|
|
16
|
+
}
|
|
17
|
+
readonly text: {
|
|
18
|
+
readonly default: string
|
|
19
|
+
readonly subdued: string
|
|
20
|
+
readonly feedback: {
|
|
21
|
+
readonly success: { readonly default: string }
|
|
22
|
+
readonly error: { readonly default: string }
|
|
23
|
+
readonly warning: { readonly default: string }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TokenUsage {
|
|
29
|
+
input?: number
|
|
30
|
+
output?: number
|
|
31
|
+
reasoning?: number
|
|
32
|
+
cache?: { read?: number; write?: number }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MessageInfo {
|
|
36
|
+
readonly id: string
|
|
37
|
+
readonly type: string
|
|
38
|
+
readonly agent?: string
|
|
39
|
+
readonly model?: string
|
|
40
|
+
readonly time: { readonly created: number; readonly completed?: number }
|
|
41
|
+
readonly cost?: unknown
|
|
42
|
+
readonly tokens?: TokenUsage
|
|
43
|
+
readonly content?: unknown[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SessionInfo {
|
|
47
|
+
readonly id: string
|
|
48
|
+
readonly title?: string
|
|
49
|
+
readonly time?: { readonly created: number; readonly updated: number }
|
|
50
|
+
readonly model?: string
|
|
51
|
+
readonly agent?: string
|
|
52
|
+
readonly parentID?: string
|
|
53
|
+
readonly cost?: number
|
|
54
|
+
readonly tokens?: TokenUsage
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Data {
|
|
58
|
+
readonly session: {
|
|
59
|
+
get(sessionID: string): SessionInfo | undefined
|
|
60
|
+
list(): SessionInfo[]
|
|
61
|
+
cost(sessionID: string): number
|
|
62
|
+
status(sessionID: string): string
|
|
63
|
+
interrupt(sessionID: string): Promise<void>
|
|
64
|
+
readonly message: {
|
|
65
|
+
list(sessionID: string): MessageInfo[]
|
|
66
|
+
sync(sessionID: string): Promise<void>
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
readonly location: {
|
|
70
|
+
readonly provider: { list(location?: unknown): unknown[] }
|
|
71
|
+
readonly model: { list(location?: unknown): unknown[] }
|
|
72
|
+
readonly mcp: { readonly server: { list(location?: unknown): unknown[] } }
|
|
73
|
+
}
|
|
74
|
+
readonly on: (type: string, handler: (event: unknown) => void) => () => void
|
|
75
|
+
readonly listen: (handler: (event: unknown) => void) => () => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Storage {
|
|
79
|
+
store<Value extends object>(
|
|
80
|
+
key: string,
|
|
81
|
+
options: { readonly initial: Value },
|
|
82
|
+
): readonly [Value, (mutation: (draft: Value) => void) => Promise<void>]
|
|
83
|
+
memory<Value extends object>(
|
|
84
|
+
key: string,
|
|
85
|
+
options: { readonly initial: Value },
|
|
86
|
+
): readonly [Value, (mutation: (draft: Value) => void) => void]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type SlotClaim = {
|
|
90
|
+
readonly render: (input: Record<string, any>) => unknown
|
|
91
|
+
} & (
|
|
92
|
+
| { readonly append: string; readonly prepend?: never; readonly before?: never; readonly after?: never; readonly replace?: never }
|
|
93
|
+
| { readonly prepend: string; readonly append?: never; readonly before?: never; readonly after?: never; readonly replace?: never }
|
|
94
|
+
| { readonly before: string; readonly append?: never; readonly prepend?: never; readonly after?: never; readonly replace?: never }
|
|
95
|
+
| { readonly after: string; readonly append?: never; readonly prepend?: never; readonly before?: never; readonly replace?: never }
|
|
96
|
+
| { readonly replace: string; readonly append?: never; readonly prepend?: never; readonly before?: never; readonly after?: never }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
export interface KeymapCommand {
|
|
100
|
+
readonly id?: string
|
|
101
|
+
readonly title?: string
|
|
102
|
+
readonly description?: string
|
|
103
|
+
readonly group?: string
|
|
104
|
+
readonly palette?: true
|
|
105
|
+
readonly slash?: { readonly name: string; readonly aliases?: string[]; readonly arguments?: true }
|
|
106
|
+
readonly namespace?: string
|
|
107
|
+
readonly name?: string
|
|
108
|
+
readonly desc?: string
|
|
109
|
+
readonly category?: string
|
|
110
|
+
readonly slashName?: string
|
|
111
|
+
readonly slashAliases?: string[]
|
|
112
|
+
readonly run: (input?: string) => void | false | Promise<void>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 最小 client 面(运行时由 V2 提供——取消走 session.interrupt)。 */
|
|
116
|
+
export interface ClientLike {
|
|
117
|
+
readonly session: {
|
|
118
|
+
interrupt(input: { sessionID: string; continue?: boolean }): Promise<unknown>
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface Context {
|
|
123
|
+
readonly app: App
|
|
124
|
+
readonly options: Record<string, any>
|
|
125
|
+
readonly location: { readonly directory?: string } | undefined
|
|
126
|
+
readonly renderer: { readonly terminalWidth: number }
|
|
127
|
+
readonly theme: Theme
|
|
128
|
+
readonly data: Data
|
|
129
|
+
readonly client: ClientLike
|
|
130
|
+
readonly storage: Storage
|
|
131
|
+
readonly ui: {
|
|
132
|
+
slot(claim: SlotClaim): () => void
|
|
133
|
+
readonly toast: {
|
|
134
|
+
show(options: { readonly message: string; readonly title?: string; readonly variant?: string }): void
|
|
135
|
+
}
|
|
136
|
+
readonly dialog: {
|
|
137
|
+
prompt(options: { readonly title: string; readonly message?: string; readonly placeholder?: string }): Promise<string | undefined>
|
|
138
|
+
select<Value>(options: {
|
|
139
|
+
readonly title: string
|
|
140
|
+
readonly placeholder?: string
|
|
141
|
+
readonly options: readonly {
|
|
142
|
+
readonly title: string
|
|
143
|
+
readonly value: Value
|
|
144
|
+
readonly description?: string
|
|
145
|
+
readonly category?: string
|
|
146
|
+
readonly disabled?: boolean
|
|
147
|
+
}[]
|
|
148
|
+
readonly current?: Value
|
|
149
|
+
}): Promise<Value | undefined>
|
|
150
|
+
}
|
|
151
|
+
readonly router: {
|
|
152
|
+
current(): { readonly type?: string; readonly sessionID?: string; readonly params?: Record<string, unknown> }
|
|
153
|
+
navigate(route: { type: string; sessionID?: string; params?: Record<string, unknown> }): void
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
readonly keymap: {
|
|
157
|
+
layer(input: () => { readonly commands?: readonly KeymapCommand[] }): void
|
|
158
|
+
shortcuts(id: string): readonly string[]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type PluginModule = {
|
|
163
|
+
readonly id: string
|
|
164
|
+
readonly setup: (context: Context) => void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>
|
|
165
|
+
}
|