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.
Files changed (60) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +192 -192
  3. package/dist/_version.d.ts +1 -1
  4. package/dist/_version.js +1 -1
  5. package/dist/core/color.d.ts +20 -0
  6. package/dist/core/color.js +68 -0
  7. package/dist/core/format.d.ts +5 -0
  8. package/dist/core/format.js +68 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.js +6 -0
  11. package/dist/core/kv.d.ts +20 -0
  12. package/dist/core/kv.js +30 -0
  13. package/dist/core/state-machine.d.ts +17 -0
  14. package/dist/core/state-machine.js +27 -0
  15. package/dist/core/types.d.ts +51 -0
  16. package/dist/core/types.js +2 -0
  17. package/dist/core/usage.d.ts +15 -0
  18. package/dist/core/usage.js +1 -0
  19. package/dist/index.js +148 -1484
  20. package/dist/panel/SubAgentPanel.d.ts +13 -0
  21. package/dist/panel/SubAgentPanel.js +1232 -0
  22. package/dist/panel/panel-api.d.ts +67 -0
  23. package/dist/panel/panel-api.js +1 -0
  24. package/dist/panel/store.d.ts +5 -0
  25. package/dist/panel/store.js +5 -0
  26. package/dist/tui.js +383 -289
  27. package/dist/v2/commands.d.ts +7 -0
  28. package/dist/v2/commands.js +263 -0
  29. package/dist/v2/index.d.ts +8 -0
  30. package/dist/v2/index.js +62 -0
  31. package/dist/v2/theme.d.ts +3 -0
  32. package/dist/v2/theme.js +12 -0
  33. package/dist/v2/types.d.ts +231 -0
  34. package/dist/v2/types.js +6 -0
  35. package/dist/v2/v2-panel-api.d.ts +12 -0
  36. package/dist/v2/v2-panel-api.js +345 -0
  37. package/dist/v2.js +3003 -0
  38. package/install.mjs +103 -103
  39. package/package.json +64 -63
  40. package/src/_version.ts +1 -1
  41. package/src/clipboard.ts +134 -134
  42. package/src/core/color.ts +70 -0
  43. package/src/core/format.ts +61 -0
  44. package/src/core/index.ts +6 -0
  45. package/src/core/kv.ts +36 -0
  46. package/src/core/state-machine.ts +46 -0
  47. package/src/core/types.ts +58 -0
  48. package/src/core/usage.ts +16 -0
  49. package/src/i18n.ts +261 -261
  50. package/src/index.tsx +124 -1750
  51. package/src/panel/SubAgentPanel.tsx +1460 -0
  52. package/src/panel/panel-api.ts +72 -0
  53. package/src/panel/store.ts +8 -0
  54. package/src/server.ts +10 -10
  55. package/src/v2/commands.ts +251 -0
  56. package/src/v2/index.tsx +98 -0
  57. package/src/v2/theme.ts +14 -0
  58. package/src/v2/types.ts +165 -0
  59. package/src/v2/v2-panel-api.ts +301 -0
  60. 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 }
@@ -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
@@ -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
+ }
@@ -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
+ }