opencode-subagent-magazine 1.7.0 → 1.7.2
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/dist/_version.d.ts +1 -1
- package/dist/core/kv.d.ts +1 -0
- package/dist/core/types.d.ts +8 -0
- package/dist/i18n.d.ts +5 -0
- package/dist/panel/SubAgentPanel.d.ts +1 -0
- package/dist/panel/entry-map.d.ts +1 -0
- package/dist/panel/panel-api.d.ts +16 -0
- package/dist/tui.js +385 -218
- package/dist/v2/db.d.ts +14 -1
- package/dist/v2.js +473 -223
- package/package.json +17 -4
- package/dist/_version.js +0 -2
- package/dist/clipboard.js +0 -85
- package/dist/core/color.js +0 -68
- package/dist/core/format.js +0 -113
- package/dist/core/index.js +0 -6
- package/dist/core/kv.js +0 -63
- package/dist/core/state-machine.js +0 -27
- package/dist/core/types.js +0 -2
- package/dist/core/usage.js +0 -1
- package/dist/i18n.js +0 -333
- package/dist/index.js +0 -511
- package/dist/panel/SubAgentPanel.js +0 -1572
- package/dist/panel/entry-map.js +0 -99
- package/dist/panel/panel-api.js +0 -1
- package/dist/panel/session-routing.js +0 -4
- package/dist/panel/store.js +0 -5
- package/dist/server.js +0 -14
- package/dist/v2/commands.js +0 -420
- package/dist/v2/db.js +0 -267
- package/dist/v2/index.js +0 -83
- package/dist/v2/settings-menu.js +0 -225
- package/dist/v2/theme.js +0 -12
- package/dist/v2/types.js +0 -6
- package/dist/v2/v2-panel-api.js +0 -612
- package/src/_version.ts +0 -2
- package/src/clipboard.ts +0 -134
- package/src/core/color.ts +0 -70
- package/src/core/format.ts +0 -105
- package/src/core/index.ts +0 -6
- package/src/core/kv.ts +0 -77
- package/src/core/state-machine.ts +0 -46
- package/src/core/types.ts +0 -72
- package/src/core/usage.ts +0 -16
- package/src/i18n.ts +0 -345
- package/src/index.tsx +0 -564
- package/src/panel/SubAgentPanel.tsx +0 -1802
- package/src/panel/entry-map.ts +0 -115
- package/src/panel/panel-api.ts +0 -83
- package/src/panel/session-routing.ts +0 -10
- package/src/panel/store.ts +0 -8
- package/src/server.ts +0 -19
- package/src/v2/commands.ts +0 -408
- package/src/v2/db.ts +0 -274
- package/src/v2/index.tsx +0 -144
- package/src/v2/settings-menu.ts +0 -241
- package/src/v2/theme.ts +0 -14
- package/src/v2/types.ts +0 -167
- package/src/v2/v2-panel-api.ts +0 -515
package/src/panel/entry-map.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import type { SubEntry, SubStatus } from "../core/types"
|
|
2
|
-
|
|
3
|
-
export type SubEntryPatch = Omit<SubEntry, "startedAt" | "endedAt"> & {
|
|
4
|
-
startedAt?: number
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function findSubEntryKey(
|
|
8
|
-
entries: ReadonlyMap<string, SubEntry>,
|
|
9
|
-
id: string,
|
|
10
|
-
sessionId?: string,
|
|
11
|
-
) {
|
|
12
|
-
if (sessionId) {
|
|
13
|
-
for (const [key, entry] of entries) {
|
|
14
|
-
if (entry.sessionId === sessionId) return key
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return entries.has(id) ? id : undefined
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function upsertSubEntry(
|
|
21
|
-
entries: ReadonlyMap<string, SubEntry>,
|
|
22
|
-
partial: SubEntryPatch,
|
|
23
|
-
now = Date.now(),
|
|
24
|
-
) {
|
|
25
|
-
const key = findSubEntryKey(entries, partial.id, partial.sessionId) ?? partial.id
|
|
26
|
-
const existing = entries.get(key)
|
|
27
|
-
const next = new Map(entries)
|
|
28
|
-
if (key !== partial.id) next.delete(partial.id)
|
|
29
|
-
|
|
30
|
-
const ended = partial.status === "done" || partial.status === "error" || partial.status === "cancelled"
|
|
31
|
-
next.set(key, {
|
|
32
|
-
...(existing ?? { startedAt: now }),
|
|
33
|
-
...partial,
|
|
34
|
-
id: existing?.id ?? partial.id,
|
|
35
|
-
startedAt: existing?.startedAt || partial.startedAt || now,
|
|
36
|
-
endedAt: ended ? (existing?.endedAt || now) : undefined,
|
|
37
|
-
})
|
|
38
|
-
return next
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** running < cancel_requested < 终态(done/error/cancelled)。 */
|
|
42
|
-
function statusRank(status: SubStatus): number {
|
|
43
|
-
if (status === "running") return 0
|
|
44
|
-
if (status === "cancel_requested") return 1
|
|
45
|
-
return 2
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const isTerminalStatus = (status: SubStatus) => statusRank(status) === 2
|
|
49
|
-
|
|
50
|
-
function minDefined(a: number | undefined, b: number | undefined): number | undefined {
|
|
51
|
-
if (a === undefined) return b
|
|
52
|
-
if (b === undefined) return a
|
|
53
|
-
return Math.min(a, b)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function maxDefined(a: number | undefined, b: number | undefined): number | undefined {
|
|
57
|
-
if (a === undefined) return b
|
|
58
|
-
if (b === undefined) return a
|
|
59
|
-
return Math.max(a, b)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 合并同一子代理条目的两个视图,绝不回退状态。
|
|
64
|
-
*
|
|
65
|
-
* 持久化存储由多个 TUI 实例写入,每个写入者都可能
|
|
66
|
-
* 持有过期快照。终态始终优先于 running /
|
|
67
|
-
* cancel_requested,进度计数只增不减,描述性字段
|
|
68
|
-
* 则回退到仍保有它的一侧。
|
|
69
|
-
*/
|
|
70
|
-
export function mergeSubEntry(base: SubEntry, incoming: SubEntry): SubEntry {
|
|
71
|
-
const successor = statusRank(incoming.status) > statusRank(base.status) ? incoming : base
|
|
72
|
-
const status = successor.status
|
|
73
|
-
const ended = isTerminalStatus(status)
|
|
74
|
-
return {
|
|
75
|
-
...base,
|
|
76
|
-
...incoming,
|
|
77
|
-
id: base.id,
|
|
78
|
-
status,
|
|
79
|
-
sessionId: incoming.sessionId ?? base.sessionId,
|
|
80
|
-
startedAt: minDefined(base.startedAt, incoming.startedAt) ?? base.startedAt,
|
|
81
|
-
endedAt: ended ? (base.endedAt ?? incoming.endedAt ?? Date.now()) : undefined,
|
|
82
|
-
tokens: maxDefined(base.tokens, incoming.tokens),
|
|
83
|
-
cost: maxDefined(base.cost, incoming.cost),
|
|
84
|
-
model: incoming.model ?? base.model,
|
|
85
|
-
todoTotal: maxDefined(base.todoTotal, incoming.todoTotal),
|
|
86
|
-
todoDone: maxDefined(base.todoDone, incoming.todoDone),
|
|
87
|
-
error: incoming.error ?? base.error,
|
|
88
|
-
cancelRequestedAt: minDefined(base.cancelRequestedAt, incoming.cancelRequestedAt),
|
|
89
|
-
abortAccepted: base.abortAccepted || incoming.abortAccepted || undefined,
|
|
90
|
-
cancelReason: incoming.cancelReason ?? base.cancelReason,
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 合并两个条目集合,去重方式与 `upsertSubEntry` 相同
|
|
96
|
-
* (先子会话、后条目 id),冲突用 `mergeSubEntry` 合并。
|
|
97
|
-
* 所有持久化路径都走这里,避免并发实例以整表写入
|
|
98
|
-
* 互相丢弃对方的条目。
|
|
99
|
-
*/
|
|
100
|
-
export function mergeSubEntries(
|
|
101
|
-
base: Iterable<SubEntry>,
|
|
102
|
-
incoming: Iterable<SubEntry>,
|
|
103
|
-
): Map<string, SubEntry> {
|
|
104
|
-
const out = new Map<string, SubEntry>()
|
|
105
|
-
for (const entry of base) out.set(entry.id, entry)
|
|
106
|
-
for (const entry of incoming) {
|
|
107
|
-
const key = findSubEntryKey(out, entry.id, entry.sessionId)
|
|
108
|
-
if (!key) {
|
|
109
|
-
out.set(entry.id, entry)
|
|
110
|
-
continue
|
|
111
|
-
}
|
|
112
|
-
out.set(key, mergeSubEntry(out.get(key)!, entry))
|
|
113
|
-
}
|
|
114
|
-
return out
|
|
115
|
-
}
|
package/src/panel/panel-api.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
export type PanelEventScope = "session" | "global"
|
|
23
|
-
|
|
24
|
-
export interface PanelEvent {
|
|
25
|
-
type: PanelEventType
|
|
26
|
-
/** Global V2 events need explicit session ownership checks. */
|
|
27
|
-
scope?: PanelEventScope
|
|
28
|
-
/** part payload for part.updated; session properties for idle/error. */
|
|
29
|
-
payload?: Record<string, unknown>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ToastOptions {
|
|
33
|
-
variant?: "success" | "warning" | "error"
|
|
34
|
-
title?: string
|
|
35
|
-
duration?: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The full surface the SubAgentPanel component consumes.
|
|
40
|
-
* V1 and V2 each implement this against their host SDK; the panel is agnostic.
|
|
41
|
-
* Theme is slot-scoped in both hosts, so it is passed as a panel prop,
|
|
42
|
-
* not part of the API.
|
|
43
|
-
*/
|
|
44
|
-
export interface PanelApi {
|
|
45
|
-
kv: KVApi
|
|
46
|
-
usage: UsageReader
|
|
47
|
-
session: {
|
|
48
|
-
get(sid: string): SessionLike | undefined
|
|
49
|
-
status(sid: string): SessionStatusLike | undefined
|
|
50
|
-
/** Raw message list for a session (scan / error extraction). */
|
|
51
|
-
messages(sid: string): unknown[] | undefined
|
|
52
|
-
/** Raw parts of a message (scan). */
|
|
53
|
-
part(messageID: string): unknown[] | undefined
|
|
54
|
-
/** Optional host-side fallback: map a subagent tool call to its child session
|
|
55
|
-
* when live events/history lack the link (V2 reads the local database). */
|
|
56
|
-
resolveChild?(input: {
|
|
57
|
-
parentId: string
|
|
58
|
-
callId?: string
|
|
59
|
-
agent?: string
|
|
60
|
-
startedAt?: number
|
|
61
|
-
}): string | undefined
|
|
62
|
-
}
|
|
63
|
-
event: {
|
|
64
|
-
on(type: PanelEventType, cb: (e: PanelEvent) => void): () => void
|
|
65
|
-
}
|
|
66
|
-
client: {
|
|
67
|
-
abort(input: { sessionID: string }): Promise<void>
|
|
68
|
-
}
|
|
69
|
-
route: {
|
|
70
|
-
navigateSession(sessionID: string): void
|
|
71
|
-
}
|
|
72
|
-
ui: {
|
|
73
|
-
toast(message: string, opts?: ToastOptions): void
|
|
74
|
-
}
|
|
75
|
-
settings: {
|
|
76
|
-
lang: () => Lang
|
|
77
|
-
maxEntries: () => number
|
|
78
|
-
sortOrder: () => SortOrder
|
|
79
|
-
scrollMode: () => ScrollMode
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export type { TodoStats }
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { SessionLike } from "./panel-api"
|
|
2
|
-
|
|
3
|
-
/** Whether an end event belongs to a child tracked by the current panel. */
|
|
4
|
-
export function isDirectChildSession(
|
|
5
|
-
currentSessionId: string,
|
|
6
|
-
eventSessionId: string,
|
|
7
|
-
eventSession: SessionLike | undefined,
|
|
8
|
-
): boolean {
|
|
9
|
-
return eventSessionId !== currentSessionId && eventSession?.parentID === currentSessionId
|
|
10
|
-
}
|
package/src/panel/store.ts
DELETED
|
@@ -1,8 +0,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/server.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
|
2
|
-
|
|
3
|
-
const server: Plugin = async () => ({})
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* V2 (opencode2) requires the default export to expose an `id` plus a
|
|
7
|
-
* `setup` (or `effect`) function. This plugin has no server-side behavior,
|
|
8
|
-
* so an empty setup keeps the module schema-valid and active on V2 while the
|
|
9
|
-
* `server` field stays for V1 detection.
|
|
10
|
-
*/
|
|
11
|
-
const setup = async () => {}
|
|
12
|
-
|
|
13
|
-
const mod: PluginModule & { setup: () => Promise<void> } = {
|
|
14
|
-
id: "opencode-subagent-magazine",
|
|
15
|
-
server,
|
|
16
|
-
setup,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export default mod
|
package/src/v2/commands.ts
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import type { Context, KeymapCommand } from "./types"
|
|
2
|
-
import type { PanelApi } from "../panel/panel-api"
|
|
3
|
-
import type { Lang, SharedSignals, SubStatus, TimeFormat } from "../core/types"
|
|
4
|
-
import { TIME_FORMATS, TIME_FORMAT_SAMPLES } from "../core/format"
|
|
5
|
-
import { KV_PREFIX, SETTING_KEYS, updateSessionData, readTTLDays } from "../core/kv"
|
|
6
|
-
import { PLUGIN_VERSION } from "../_version"
|
|
7
|
-
import { LANG_META, createT } from "../i18n"
|
|
8
|
-
import { globalEntryCache, setClearTick } from "../panel/store"
|
|
9
|
-
import { mergeSubEntries } from "../panel/entry-map"
|
|
10
|
-
import { openSettingsMenu } from "./settings-menu"
|
|
11
|
-
|
|
12
|
-
/** V2 命令(对齐 V1 的 9 个斜杠命令——promise 式对话框)。 */
|
|
13
|
-
export function makeCommands(
|
|
14
|
-
context: Context,
|
|
15
|
-
api: PanelApi,
|
|
16
|
-
signals: SharedSignals,
|
|
17
|
-
): KeymapCommand[] {
|
|
18
|
-
const t = createT(() => signals.lang())
|
|
19
|
-
const kv = api.kv
|
|
20
|
-
const clampMax = (n: number) => Math.max(1, Math.min(50, n))
|
|
21
|
-
const onOff = (v: boolean) => (v ? t("settings.on") : t("settings.off"))
|
|
22
|
-
|
|
23
|
-
const resolveParent = (sid: string): { parentSid: string; isChild: boolean } => {
|
|
24
|
-
try {
|
|
25
|
-
const session = api.session.get(sid) as any
|
|
26
|
-
const parentID = session?.parentID as string | undefined
|
|
27
|
-
if (parentID) return { parentSid: parentID, isChild: true }
|
|
28
|
-
} catch {}
|
|
29
|
-
return { parentSid: sid, isChild: false }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return [
|
|
33
|
-
{
|
|
34
|
-
id: "opencode-subagent-magazine.subagent.lang",
|
|
35
|
-
title: "SubAgent Magazine: Language",
|
|
36
|
-
description: "Switch display language (中文 / English / 日本語 / 한국어)",
|
|
37
|
-
slash: { name: "subagent-lang" },
|
|
38
|
-
palette: true,
|
|
39
|
-
run: async () => {
|
|
40
|
-
const lang = await context.ui.dialog.select<Lang>({
|
|
41
|
-
title: "Language / 语言",
|
|
42
|
-
options: LANG_META.map((m) => ({ title: m.label, value: m.code })),
|
|
43
|
-
})
|
|
44
|
-
if (!lang) return
|
|
45
|
-
signals.setLang(lang)
|
|
46
|
-
kv.set(SETTING_KEYS.lang, lang)
|
|
47
|
-
api.ui.toast("Language: " + (LANG_META.find((m) => m.code === lang)?.label ?? lang))
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "opencode-subagent-magazine.subagent.order",
|
|
52
|
-
title: "SubAgent Magazine: Sort Order",
|
|
53
|
-
description: "Set sub-agent entry sort order (desc / asc)",
|
|
54
|
-
slash: { name: "subagent-order" },
|
|
55
|
-
palette: true,
|
|
56
|
-
run: async () => {
|
|
57
|
-
const order = await context.ui.dialog.select<"desc" | "asc">({
|
|
58
|
-
title: "Sort Order / 排序方式",
|
|
59
|
-
options: [
|
|
60
|
-
{ title: t("order.desc"), value: "desc" },
|
|
61
|
-
{ title: t("order.asc"), value: "asc" },
|
|
62
|
-
],
|
|
63
|
-
})
|
|
64
|
-
if (!order) return
|
|
65
|
-
signals.setSortOrder(order)
|
|
66
|
-
kv.set(SETTING_KEYS.order, order)
|
|
67
|
-
api.ui.toast(order === "desc" ? t("order.desc") : t("order.asc"))
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: "opencode-subagent-magazine.subagent.scroll",
|
|
72
|
-
title: "SubAgent Magazine: Scroll Mode",
|
|
73
|
-
description: "Set scroll mode (wheel / click)",
|
|
74
|
-
slash: { name: "subagent-scroll" },
|
|
75
|
-
palette: true,
|
|
76
|
-
run: async () => {
|
|
77
|
-
const mode = await context.ui.dialog.select<"wheel" | "click">({
|
|
78
|
-
title: "Scroll Mode / 滚动模式",
|
|
79
|
-
options: [
|
|
80
|
-
{ title: t("scroll.wheel"), value: "wheel" },
|
|
81
|
-
{ title: t("scroll.click"), value: "click" },
|
|
82
|
-
],
|
|
83
|
-
})
|
|
84
|
-
if (!mode) return
|
|
85
|
-
signals.setScrollMode(mode)
|
|
86
|
-
kv.set(SETTING_KEYS.scrollMode, mode)
|
|
87
|
-
api.ui.toast(mode === "wheel" ? t("scroll.wheel") : t("scroll.click"))
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: "opencode-subagent-magazine.subagent.max",
|
|
92
|
-
title: "SubAgent Magazine: Max Entries",
|
|
93
|
-
description: "Set max visible sub-agent entries in sidebar",
|
|
94
|
-
slash: { name: "subagent-max" },
|
|
95
|
-
palette: true,
|
|
96
|
-
run: async () => {
|
|
97
|
-
const val = await context.ui.dialog.prompt({
|
|
98
|
-
title: "Max Visible Entries",
|
|
99
|
-
message: "Number of entries to show in the sidebar (1–50)",
|
|
100
|
-
placeholder: String(signals.maxEntries()),
|
|
101
|
-
})
|
|
102
|
-
if (val === undefined) return
|
|
103
|
-
const n = clampMax(parseInt(val, 10) || 10)
|
|
104
|
-
signals.setMaxEntries(n)
|
|
105
|
-
kv.set(SETTING_KEYS.maxEntries, n)
|
|
106
|
-
api.ui.toast(`Max entries: ${n}`)
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: "opencode-subagent-magazine.subagent.config",
|
|
111
|
-
title: "SubAgent Magazine: Settings",
|
|
112
|
-
description: "Show current settings (change each via its own command)",
|
|
113
|
-
slash: { name: "subagent-config" },
|
|
114
|
-
palette: true,
|
|
115
|
-
run: () => {
|
|
116
|
-
const ttl = readTTLDays(kv)
|
|
117
|
-
const langLabel = LANG_META.find((m) => m.code === signals.lang())?.label ?? signals.lang()
|
|
118
|
-
const message = [
|
|
119
|
-
`${t("settings.lang")}: ${langLabel}`,
|
|
120
|
-
`${t("settings.max")}: ${signals.maxEntries()}`,
|
|
121
|
-
`${t("settings.order")}: ${signals.sortOrder() === "desc" ? t("order.desc") : t("order.asc")}`,
|
|
122
|
-
`${t("settings.scroll")}: ${signals.scrollMode() === "wheel" ? t("scroll.wheel") : t("scroll.click")}`,
|
|
123
|
-
`${t("settings.ttl")}: ${ttl === 0 ? t("ttl.unlimited") : `${ttl}d`}`,
|
|
124
|
-
`${t("settings.border")}: ${onOff(signals.borderVisible())}`,
|
|
125
|
-
`${t("settings.showEntryCost")}: ${onOff(signals.showEntryCost())}`,
|
|
126
|
-
`${t("settings.showEntryTime")}: ${onOff(signals.showEntryTime())}`,
|
|
127
|
-
`${t("settings.showEntryTokens")}: ${onOff(signals.showEntryTokens())}`,
|
|
128
|
-
`${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[signals.timeFormat()]}`,
|
|
129
|
-
`${t("settings.dbSync")}: ${onOff(signals.dbSync())}`,
|
|
130
|
-
].join("\n")
|
|
131
|
-
context.ui.toast.show({ title: t("settings.title"), message })
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
id: "opencode-subagent-magazine.subagent.sections",
|
|
136
|
-
title: "SubAgent Magazine: Settings Menu",
|
|
137
|
-
description: "Open the interactive settings menu (Esc to close)",
|
|
138
|
-
slash: { name: "subagent-sections" },
|
|
139
|
-
palette: true,
|
|
140
|
-
run: () => {
|
|
141
|
-
openSettingsMenu(context, api, signals)
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
id: "opencode-subagent-magazine.subagent.cost",
|
|
146
|
-
title: "SubAgent Magazine: Toggle Entry Cost",
|
|
147
|
-
description: "Show or hide the per-sub-agent cost in the sidebar",
|
|
148
|
-
slash: { name: "subagent-cost" },
|
|
149
|
-
palette: true,
|
|
150
|
-
run: () => {
|
|
151
|
-
const v = !signals.showEntryCost()
|
|
152
|
-
signals.setShowEntryCost(v)
|
|
153
|
-
kv.set(SETTING_KEYS.showEntryCost, v)
|
|
154
|
-
api.ui.toast(`${t("settings.showEntryCost")}: ${onOff(v)}`)
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
id: "opencode-subagent-magazine.subagent.time",
|
|
159
|
-
title: "SubAgent Magazine: Toggle Entry Time",
|
|
160
|
-
description: "Show or hide the elapsed time in the sidebar list",
|
|
161
|
-
slash: { name: "subagent-time" },
|
|
162
|
-
palette: true,
|
|
163
|
-
run: () => {
|
|
164
|
-
const v = !signals.showEntryTime()
|
|
165
|
-
signals.setShowEntryTime(v)
|
|
166
|
-
kv.set(SETTING_KEYS.showEntryTime, v)
|
|
167
|
-
api.ui.toast(`${t("settings.showEntryTime")}: ${onOff(v)}`)
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
id: "opencode-subagent-magazine.subagent.time-format",
|
|
172
|
-
title: "SubAgent Magazine: Time Format",
|
|
173
|
-
description: "Set how elapsed time is displayed (short / decimal / clock / compact / seconds)",
|
|
174
|
-
slash: { name: "subagent-time-format" },
|
|
175
|
-
palette: true,
|
|
176
|
-
run: async () => {
|
|
177
|
-
const picked = await context.ui.dialog.select<TimeFormat>({
|
|
178
|
-
title: t("settings.timeFormat"),
|
|
179
|
-
options: TIME_FORMATS.map((f) => ({ title: `${f} — ${TIME_FORMAT_SAMPLES[f]}`, value: f })),
|
|
180
|
-
current: signals.timeFormat(),
|
|
181
|
-
})
|
|
182
|
-
if (!picked) return
|
|
183
|
-
signals.setTimeFormat(picked)
|
|
184
|
-
kv.set(SETTING_KEYS.timeFormat, picked)
|
|
185
|
-
api.ui.toast(`${t("settings.timeFormat")}: ${TIME_FORMAT_SAMPLES[picked]}`)
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
id: "opencode-subagent-magazine.subagent.tokens",
|
|
190
|
-
title: "SubAgent Magazine: Toggle Entry Tokens",
|
|
191
|
-
description: "Show or hide the token count in the sidebar list",
|
|
192
|
-
slash: { name: "subagent-tokens" },
|
|
193
|
-
palette: true,
|
|
194
|
-
run: () => {
|
|
195
|
-
const v = !signals.showEntryTokens()
|
|
196
|
-
signals.setShowEntryTokens(v)
|
|
197
|
-
kv.set(SETTING_KEYS.showEntryTokens, v)
|
|
198
|
-
api.ui.toast(`${t("settings.showEntryTokens")}: ${onOff(v)}`)
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
id: "opencode-subagent-magazine.subagent.db",
|
|
203
|
-
title: "SubAgent Magazine: Toggle Local DB",
|
|
204
|
-
description: "Enrich sub-agent entries from the local OpenCode database (V2)",
|
|
205
|
-
slash: { name: "subagent-db" },
|
|
206
|
-
palette: true,
|
|
207
|
-
run: () => {
|
|
208
|
-
const v = !signals.dbSync()
|
|
209
|
-
signals.setDbSync(v)
|
|
210
|
-
kv.set(SETTING_KEYS.dbSync, v)
|
|
211
|
-
api.ui.toast(`${t("settings.dbSync")}: ${onOff(v)}`)
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
id: "opencode-subagent-magazine.subagent.version",
|
|
216
|
-
title: "SubAgent Magazine: Version",
|
|
217
|
-
description: "Show plugin version",
|
|
218
|
-
slash: { name: "subagent-version" },
|
|
219
|
-
palette: true,
|
|
220
|
-
run: () => {
|
|
221
|
-
api.ui.toast(`opencode-subagent-magazine v${PLUGIN_VERSION}`)
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
id: "opencode-subagent-magazine.subagent.session",
|
|
226
|
-
title: "SubAgent Magazine: Session",
|
|
227
|
-
description: "Show current session ID",
|
|
228
|
-
slash: { name: "subagent-session" },
|
|
229
|
-
palette: true,
|
|
230
|
-
run: () => {
|
|
231
|
-
api.ui.toast(`Session: ${signals.sessionId}`)
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: "opencode-subagent-magazine.subagent.clear-running",
|
|
236
|
-
title: "SubAgent Magazine: Clear Running",
|
|
237
|
-
description: "Mark all running sub-agent entries as done (for stuck/zombie entries)",
|
|
238
|
-
slash: { name: "subagent-clear-running" },
|
|
239
|
-
palette: true,
|
|
240
|
-
run: () => {
|
|
241
|
-
const sid = signals.sessionId
|
|
242
|
-
const entries = globalEntryCache.get(sid)
|
|
243
|
-
if (!entries || entries.size === 0) {
|
|
244
|
-
api.ui.toast(signals.lang() === "zh" ? "暂无子代理条目" : "No sub-agent entries found")
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
|
-
let count = 0
|
|
248
|
-
for (const [, entry] of entries) {
|
|
249
|
-
if (entry.status === "running" || entry.status === "cancel_requested") {
|
|
250
|
-
entry.status = "done" as SubStatus
|
|
251
|
-
entry.endedAt = Date.now()
|
|
252
|
-
count++
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (count > 0) {
|
|
256
|
-
const ok = () => api.ui.toast(signals.lang() === "zh"
|
|
257
|
-
? `已标记 ${count} 个运行中的条目为完成`
|
|
258
|
-
: `Marked ${count} running entries as done`)
|
|
259
|
-
const fail = () => {
|
|
260
|
-
try { api.ui.toast(t("clear.failed")) } catch {}
|
|
261
|
-
}
|
|
262
|
-
try {
|
|
263
|
-
const { parentSid, isChild } = resolveParent(sid)
|
|
264
|
-
const done = updateSessionData(kv, (data) => {
|
|
265
|
-
// 落定前先把(可能过期的)模块缓存与持久化记录
|
|
266
|
-
// merge:其他实例可能有本实例从未见过的条目,
|
|
267
|
-
// 这次写入不能把它们丢掉。
|
|
268
|
-
if (isChild) {
|
|
269
|
-
if (!data[parentSid]) data[parentSid] = { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} }
|
|
270
|
-
if (!data[parentSid].children) data[parentSid].children = {}
|
|
271
|
-
const prev = data[parentSid].children[sid]?.entries ?? []
|
|
272
|
-
const merged = [...mergeSubEntries(prev, entries.values()).values()].map((e) =>
|
|
273
|
-
e.status === "running" || e.status === "cancel_requested"
|
|
274
|
-
? { ...e, status: "done" as SubStatus, endedAt: e.endedAt ?? Date.now() }
|
|
275
|
-
: e
|
|
276
|
-
)
|
|
277
|
-
data[parentSid].children[sid] = {
|
|
278
|
-
...(data[parentSid].children[sid] ?? { scroll: 0, expanded: "" }),
|
|
279
|
-
entries: merged,
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
const rec = data[parentSid] ?? { ts: Date.now(), entries: [], scroll: 0, expanded: "", children: {} }
|
|
283
|
-
const merged = [...mergeSubEntries(rec.entries ?? [], entries.values()).values()].map((e) =>
|
|
284
|
-
e.status === "running" || e.status === "cancel_requested"
|
|
285
|
-
? { ...e, status: "done" as SubStatus, endedAt: e.endedAt ?? Date.now() }
|
|
286
|
-
: e
|
|
287
|
-
)
|
|
288
|
-
data[parentSid] = { ...rec, ts: Date.now(), entries: merged, children: rec.children ?? {} }
|
|
289
|
-
}
|
|
290
|
-
})
|
|
291
|
-
if (done && typeof (done as Promise<void>).then === "function") {
|
|
292
|
-
void (done as Promise<void>).then(ok, fail)
|
|
293
|
-
} else {
|
|
294
|
-
ok()
|
|
295
|
-
}
|
|
296
|
-
} catch {
|
|
297
|
-
fail()
|
|
298
|
-
}
|
|
299
|
-
} else {
|
|
300
|
-
api.ui.toast(signals.lang() === "zh" ? "没有需要清理的运行中条目" : "No running entries to clear")
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
id: "opencode-subagent-magazine.subagent.ttl",
|
|
306
|
-
title: "SubAgent Magazine: TTL",
|
|
307
|
-
description: "Set session data retention period (days before auto-cleanup)",
|
|
308
|
-
slash: { name: "subagent-ttl" },
|
|
309
|
-
palette: true,
|
|
310
|
-
run: async () => {
|
|
311
|
-
const curDays = readTTLDays(kv)
|
|
312
|
-
const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`
|
|
313
|
-
const days = await context.ui.dialog.select<number>({
|
|
314
|
-
title: `${t("ttl.label")} (${curLabel})`,
|
|
315
|
-
options: [
|
|
316
|
-
{ title: t("ttl.3d"), value: 3 },
|
|
317
|
-
{ title: t("ttl.7d"), value: 7 },
|
|
318
|
-
{ title: t("ttl.14d"), value: 14 },
|
|
319
|
-
{ title: t("ttl.30d"), value: 30 },
|
|
320
|
-
{ title: t("ttl.unlimited"), value: 0 },
|
|
321
|
-
],
|
|
322
|
-
})
|
|
323
|
-
if (days === undefined) return
|
|
324
|
-
kv.set(SETTING_KEYS.ttlDays, String(days))
|
|
325
|
-
api.ui.toast(days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days }))
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
id: "opencode-subagent-magazine.subagent.clear-entries",
|
|
330
|
-
title: "SubAgent Magazine: Clear Entries",
|
|
331
|
-
description: "Delete all sub-agent records for the current session (cannot be undone)",
|
|
332
|
-
slash: { name: "subagent-clear-entries" },
|
|
333
|
-
palette: true,
|
|
334
|
-
run: async () => {
|
|
335
|
-
const sid = signals.sessionId
|
|
336
|
-
const sessionObj = api.session.get(sid)
|
|
337
|
-
const parentID = (sessionObj as any)?.parentID as string | undefined
|
|
338
|
-
const cached = globalEntryCache.get(sid)
|
|
339
|
-
let runningCount = 0
|
|
340
|
-
if (cached) {
|
|
341
|
-
for (const [, e] of cached) { if (e.status === "running") runningCount++ }
|
|
342
|
-
}
|
|
343
|
-
const msg = runningCount > 0 ? t("clear.prompt_running", { n: runningCount }) : t("clear.prompt")
|
|
344
|
-
const choice = await context.ui.dialog.select<"yes" | "no">({
|
|
345
|
-
title: t("clear.title"),
|
|
346
|
-
options: [
|
|
347
|
-
{ title: t("clear.title"), value: "yes" },
|
|
348
|
-
{ title: t("cancel.label"), value: "no" },
|
|
349
|
-
],
|
|
350
|
-
})
|
|
351
|
-
if (choice !== "yes") return
|
|
352
|
-
try {
|
|
353
|
-
let count = 0
|
|
354
|
-
const finish = () => {
|
|
355
|
-
globalEntryCache.delete(sid)
|
|
356
|
-
setClearTick((v) => v + 1)
|
|
357
|
-
api.ui.toast(t("clear.done", { n: count }))
|
|
358
|
-
}
|
|
359
|
-
const done = updateSessionData(kv, (data) => {
|
|
360
|
-
if (parentID) {
|
|
361
|
-
const child = data[parentID]?.children?.[sid]
|
|
362
|
-
if (child) {
|
|
363
|
-
const ids = child.entries?.map((e) => e.id) ?? []
|
|
364
|
-
count = ids.length
|
|
365
|
-
child.entries = []
|
|
366
|
-
child.scroll = 0
|
|
367
|
-
child.expanded = ""
|
|
368
|
-
child.clearedIds = [...new Set([...(child.clearedIds ?? []), ...ids])]
|
|
369
|
-
}
|
|
370
|
-
} else {
|
|
371
|
-
const rec = data[sid]
|
|
372
|
-
if (rec) {
|
|
373
|
-
const ids = rec.entries?.map((e) => e.id) ?? []
|
|
374
|
-
count = ids.length
|
|
375
|
-
rec.entries = []
|
|
376
|
-
rec.scroll = 0
|
|
377
|
-
rec.expanded = ""
|
|
378
|
-
rec.clearedIds = [...new Set([...(rec.clearedIds ?? []), ...ids])]
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
})
|
|
382
|
-
if (done && typeof (done as Promise<void>).then === "function") {
|
|
383
|
-
void (done as Promise<void>).then(finish, () => {
|
|
384
|
-
try { api.ui.toast(t("clear.failed")) } catch {}
|
|
385
|
-
})
|
|
386
|
-
} else {
|
|
387
|
-
finish()
|
|
388
|
-
}
|
|
389
|
-
} catch {}
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
id: "opencode-subagent-magazine.subagent.border",
|
|
394
|
-
title: "SubAgent Magazine: Border",
|
|
395
|
-
description: "Show or hide the panel border",
|
|
396
|
-
slash: { name: "subagent-border" },
|
|
397
|
-
palette: true,
|
|
398
|
-
run: () => {
|
|
399
|
-
const cur = Boolean(kv.get(SETTING_KEYS.border, false))
|
|
400
|
-
kv.set(SETTING_KEYS.border, !cur)
|
|
401
|
-
signals.setBorderVisible(!cur)
|
|
402
|
-
api.ui.toast(!cur ? t("borderShown") : t("borderHidden"))
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
]
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
export { KV_PREFIX }
|