opencode-subagent-magazine 1.6.1 → 1.7.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/src/v2/index.tsx CHANGED
@@ -7,22 +7,31 @@ import { makeCommands } from "./commands"
7
7
  import { mapTheme } from "./theme"
8
8
  import { SubAgentPanel } from "../panel/SubAgentPanel"
9
9
  import type { PanelApi } from "../panel/panel-api"
10
- import type { Lang, SortOrder, ScrollMode, SharedSignals } from "../core/types"
10
+ import type { Lang, SortOrder, ScrollMode, SharedSignals, TimeFormat } from "../core/types"
11
+ import { TIME_FORMATS } from "../core/format"
11
12
  import { SETTING_KEYS } from "../core/kv"
12
13
  import { LANG_META, detectLang } from "../i18n"
13
14
 
14
- /** 面板根组件:keymap.layer 必须在组件渲染上下文注册(setup 内调用报
15
- * Keymap.Provider missing),再渲染共享 SubAgentPanel。 */
16
- function PluginRoot(props: {
15
+ /** 命令层必须在 app 槽注册:侧栏隐藏(sidebar: auto)时命令仍需可用。 */
16
+ function CommandRoot(props: {
17
17
  context: Context
18
18
  api: PanelApi
19
19
  signals: SharedSignals
20
- sessionID: string
21
20
  }) {
22
21
  props.context.keymap.layer(() => ({
23
22
  mode: "global" as const,
24
23
  commands: makeCommands(props.context, props.api, props.signals),
25
24
  }))
25
+ return null
26
+ }
27
+
28
+ /** 面板根组件:渲染共享 SubAgentPanel(命令层见 CommandRoot)。 */
29
+ function PluginRoot(props: {
30
+ context: Context
31
+ api: PanelApi
32
+ signals: SharedSignals
33
+ sessionID: string
34
+ }) {
26
35
  return (
27
36
  <SubAgentPanel
28
37
  api={props.api}
@@ -32,6 +41,10 @@ function PluginRoot(props: {
32
41
  sortOrder={props.signals.sortOrder}
33
42
  scrollMode={props.signals.scrollMode}
34
43
  borderVisible={props.signals.borderVisible}
44
+ showEntryCost={props.signals.showEntryCost}
45
+ showEntryTime={props.signals.showEntryTime}
46
+ showEntryTokens={props.signals.showEntryTokens}
47
+ timeFormat={props.signals.timeFormat}
35
48
  sessionId={props.sessionID}
36
49
  />
37
50
  )
@@ -69,16 +82,45 @@ const mod: PluginModule & { server: () => Promise<Record<string, never>> } = {
69
82
  const [borderSignal, setBorderVisible] = createSignal<boolean>(
70
83
  (api.kv.get(SETTING_KEYS.border, false) as boolean) === true,
71
84
  )
85
+ const [showEntryCostSignal, setShowEntryCost] = createSignal<boolean>(
86
+ (api.kv.get(SETTING_KEYS.showEntryCost, false) as boolean) === true,
87
+ )
88
+ const [showEntryTimeSignal, setShowEntryTime] = createSignal<boolean>(
89
+ (api.kv.get(SETTING_KEYS.showEntryTime, true) as boolean) !== false,
90
+ )
91
+ const [showEntryTokensSignal, setShowEntryTokens] = createSignal<boolean>(
92
+ (api.kv.get(SETTING_KEYS.showEntryTokens, true) as boolean) !== false,
93
+ )
94
+ const storedTimeFormat = String(api.kv.get(SETTING_KEYS.timeFormat, "short"))
95
+ const [timeFormatSignal, setTimeFormat] = createSignal<TimeFormat>(
96
+ (TIME_FORMATS as readonly string[]).includes(storedTimeFormat) ? (storedTimeFormat as TimeFormat) : "short",
97
+ )
98
+ const [dbSyncSignal, setDbSync] = createSignal<boolean>(
99
+ (api.kv.get(SETTING_KEYS.dbSync, true) as boolean) !== false,
100
+ )
72
101
  lang = langSignal
73
102
  maxEntries = maxSignal
74
103
  sortOrder = orderSignal
75
104
  scrollMode = scrollSignal
76
105
 
77
106
  const signals: SharedSignals = {
78
- lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible: borderSignal, setBorderVisible, sessionId: "",
107
+ lang, setLang, maxEntries, setMaxEntries, sortOrder, setSortOrder, scrollMode, setScrollMode, borderVisible: borderSignal, setBorderVisible,
108
+ showEntryCost: showEntryCostSignal, setShowEntryCost,
109
+ showEntryTime: showEntryTimeSignal, setShowEntryTime,
110
+ showEntryTokens: showEntryTokensSignal, setShowEntryTokens,
111
+ timeFormat: timeFormatSignal, setTimeFormat,
112
+ dbSync: dbSyncSignal, setDbSync,
113
+ sessionId: "",
79
114
  }
80
115
 
81
- // 命令 layer(组件内注册——见 PluginRoot)
116
+ // 命令层挂 app 槽:侧栏隐藏时斜杠命令仍需可用(见 CommandRoot)
117
+ context.ui.slot({
118
+ append: "app",
119
+ render: () => (
120
+ <CommandRoot context={context} api={api} signals={signals} />
121
+ ),
122
+ })
123
+
82
124
  // 侧边栏面板(共享 SubAgentPanel——V1/V2 同一组件)
83
125
  context.ui.slot({
84
126
  prepend: "sidebar.content",
@@ -0,0 +1,241 @@
1
+ import type { Context } from "./types"
2
+ import type { PanelApi } from "../panel/panel-api"
3
+ import type { Lang, SortOrder, ScrollMode, SharedSignals, TimeFormat } from "../core/types"
4
+ import { SETTING_KEYS, readTTLDays } from "../core/kv"
5
+ import { TIME_FORMATS, TIME_FORMAT_SAMPLES } from "../core/format"
6
+ import { LANG_META, createT } from "../i18n"
7
+
8
+ /** 与侧栏设置共用的 TTL 周期:3/7/14/30 天或无限期。 */
9
+ const TTL_VALUES = [3, 7, 14, 30, 0]
10
+
11
+ /** 链式原生对话框之间的延迟,让宿主能关闭上一个对话框。 */
12
+ const nextTick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
13
+
14
+ /**
15
+ * 原生设置菜单(visual-cache 风格):用单个选择列表展示每项设置
16
+ * 及其当前值;选中某项会打开对应的编辑器,
17
+ * 随后菜单重新打开,直到用户关闭(Esc / Close)。
18
+ */
19
+ export function openSettingsMenu(context: Context, api: PanelApi, signals: SharedSignals): void {
20
+ runSettingsMenu(context, api, signals).catch((err) => {
21
+ try { context.ui.toast.show({ message: String(err), variant: "error" }) } catch {}
22
+ })
23
+ }
24
+
25
+ async function runSettingsMenu(context: Context, api: PanelApi, signals: SharedSignals): Promise<void> {
26
+ const t = createT(() => signals.lang())
27
+ const kv = api.kv
28
+ const onOff = (v: boolean) => (v ? t("settings.on") : t("settings.off"))
29
+ const langLabel = () => LANG_META.find((m) => m.code === signals.lang())?.label ?? signals.lang()
30
+ const ttlTitle = (d: number) => (d === 0 ? t("ttl.unlimited") : t(d === 3 ? "ttl.3d" : d === 7 ? "ttl.7d" : d === 14 ? "ttl.14d" : "ttl.30d"))
31
+ const ttlLabel = () => ttlTitle(readTTLDays(kv))
32
+ const orderLabel = () => (signals.sortOrder() === "desc" ? t("order.desc") : t("order.asc"))
33
+ const scrollLabel = () => (signals.scrollMode() === "wheel" ? t("scroll.wheel") : t("scroll.click"))
34
+ const toast = (item: string, value: string) =>
35
+ context.ui.toast.show({ message: t("settings.saved", { item, value }) })
36
+
37
+ while (true) {
38
+ const category = t("settings.section.general")
39
+ const choice = await context.ui.dialog.select<string>({
40
+ title: t("settings.title"),
41
+ options: [
42
+ { title: t("settings.lang"), value: "lang", description: langLabel(), category },
43
+ { title: t("settings.max"), value: "max", description: String(signals.maxEntries()), category },
44
+ { title: t("settings.order"), value: "order", description: orderLabel(), category },
45
+ { title: t("settings.scroll"), value: "scroll", description: scrollLabel(), category },
46
+ { title: t("settings.ttl"), value: "ttl", description: ttlLabel(), category },
47
+ { title: t("settings.border"), value: "border", description: onOff(signals.borderVisible()), category },
48
+ { title: t("settings.showEntryCost"), value: "cost", description: onOff(signals.showEntryCost()), category },
49
+ { title: t("settings.showEntryTime"), value: "time", description: onOff(signals.showEntryTime()), category },
50
+ { title: t("settings.timeFormat"), value: "timefmt", description: TIME_FORMAT_SAMPLES[signals.timeFormat()], category },
51
+ { title: t("settings.showEntryTokens"), value: "tokens", description: onOff(signals.showEntryTokens()), category },
52
+ { title: t("settings.dbSync"), value: "dbsync", description: onOff(signals.dbSync()), category },
53
+ { title: t("settings.close"), value: "close" },
54
+ ],
55
+ })
56
+ if (choice === undefined || choice === "close") return
57
+
58
+ // ── 语言 ──
59
+ if (choice === "lang") {
60
+ const picked = await context.ui.dialog.select<Lang>({
61
+ title: t("settings.lang"),
62
+ options: LANG_META.map((m) => ({ title: m.label, value: m.code })),
63
+ current: signals.lang(),
64
+ })
65
+ if (picked) {
66
+ signals.setLang(picked)
67
+ kv.set(SETTING_KEYS.lang, picked)
68
+ toast(t("settings.lang"), LANG_META.find((m) => m.code === picked)?.label ?? picked)
69
+ }
70
+ }
71
+
72
+ // ── 最大可见条目数 ──
73
+ if (choice === "max") {
74
+ const val = await context.ui.dialog.prompt({
75
+ title: t("settings.max"),
76
+ message: "1–50",
77
+ placeholder: String(signals.maxEntries()),
78
+ })
79
+ if (val !== undefined) {
80
+ const n = parseInt(val.trim(), 10)
81
+ if (Number.isNaN(n)) {
82
+ context.ui.toast.show({ message: t("settings.invalid"), variant: "warning" })
83
+ } else {
84
+ const clamped = Math.max(1, Math.min(50, n))
85
+ signals.setMaxEntries(clamped)
86
+ kv.set(SETTING_KEYS.maxEntries, clamped)
87
+ toast(t("settings.max"), String(clamped))
88
+ }
89
+ }
90
+ }
91
+
92
+ // ── 排序方式 ──
93
+ if (choice === "order") {
94
+ const picked = await context.ui.dialog.select<SortOrder>({
95
+ title: t("settings.order"),
96
+ options: [
97
+ { title: t("order.desc"), value: "desc" },
98
+ { title: t("order.asc"), value: "asc" },
99
+ ],
100
+ current: signals.sortOrder(),
101
+ })
102
+ if (picked) {
103
+ signals.setSortOrder(picked)
104
+ kv.set(SETTING_KEYS.order, picked)
105
+ toast(t("settings.order"), picked === "desc" ? t("order.desc") : t("order.asc"))
106
+ }
107
+ }
108
+
109
+ // ── 滚动模式 ──
110
+ if (choice === "scroll") {
111
+ const picked = await context.ui.dialog.select<ScrollMode>({
112
+ title: t("settings.scroll"),
113
+ options: [
114
+ { title: t("scroll.wheel"), value: "wheel" },
115
+ { title: t("scroll.click"), value: "click" },
116
+ ],
117
+ current: signals.scrollMode(),
118
+ })
119
+ if (picked) {
120
+ signals.setScrollMode(picked)
121
+ kv.set(SETTING_KEYS.scrollMode, picked)
122
+ toast(t("settings.scroll"), picked === "wheel" ? t("scroll.wheel") : t("scroll.click"))
123
+ }
124
+ }
125
+
126
+ // ── TTL / 保留期 ──
127
+ if (choice === "ttl") {
128
+ const picked = await context.ui.dialog.select<number>({
129
+ title: t("settings.ttl"),
130
+ options: TTL_VALUES.map((d) => ({ title: ttlTitle(d), value: d })),
131
+ current: readTTLDays(kv),
132
+ })
133
+ if (picked !== undefined) {
134
+ kv.set(SETTING_KEYS.ttlDays, String(picked))
135
+ toast(t("settings.ttl"), ttlTitle(picked))
136
+ }
137
+ }
138
+
139
+ // ── 边框 ──
140
+ if (choice === "border") {
141
+ const picked = await context.ui.dialog.select<boolean>({
142
+ title: t("settings.border"),
143
+ options: [
144
+ { title: t("settings.on"), value: true },
145
+ { title: t("settings.off"), value: false },
146
+ ],
147
+ current: signals.borderVisible(),
148
+ })
149
+ if (picked !== undefined) {
150
+ signals.setBorderVisible(picked)
151
+ kv.set(SETTING_KEYS.border, picked)
152
+ toast(t("settings.border"), onOff(picked))
153
+ }
154
+ }
155
+
156
+ // ── 单条目费用 ──
157
+ if (choice === "cost") {
158
+ const picked = await context.ui.dialog.select<boolean>({
159
+ title: t("settings.showEntryCost"),
160
+ options: [
161
+ { title: t("settings.on"), value: true },
162
+ { title: t("settings.off"), value: false },
163
+ ],
164
+ current: signals.showEntryCost(),
165
+ })
166
+ if (picked !== undefined) {
167
+ signals.setShowEntryCost(picked)
168
+ kv.set(SETTING_KEYS.showEntryCost, picked)
169
+ toast(t("settings.showEntryCost"), onOff(picked))
170
+ }
171
+ }
172
+
173
+ // ── 单条目时间 ──
174
+ if (choice === "time") {
175
+ const picked = await context.ui.dialog.select<boolean>({
176
+ title: t("settings.showEntryTime"),
177
+ options: [
178
+ { title: t("settings.on"), value: true },
179
+ { title: t("settings.off"), value: false },
180
+ ],
181
+ current: signals.showEntryTime(),
182
+ })
183
+ if (picked !== undefined) {
184
+ signals.setShowEntryTime(picked)
185
+ kv.set(SETTING_KEYS.showEntryTime, picked)
186
+ toast(t("settings.showEntryTime"), onOff(picked))
187
+ }
188
+ }
189
+
190
+ // ── 时间格式 ──
191
+ if (choice === "timefmt") {
192
+ const picked = await context.ui.dialog.select<TimeFormat>({
193
+ title: t("settings.timeFormat"),
194
+ options: TIME_FORMATS.map((f) => ({ title: `${f} — ${TIME_FORMAT_SAMPLES[f]}`, value: f })),
195
+ current: signals.timeFormat(),
196
+ })
197
+ if (picked) {
198
+ signals.setTimeFormat(picked)
199
+ kv.set(SETTING_KEYS.timeFormat, picked)
200
+ toast(t("settings.timeFormat"), TIME_FORMAT_SAMPLES[picked])
201
+ }
202
+ }
203
+
204
+ // ── 单条目 token 数 ──
205
+ if (choice === "tokens") {
206
+ const picked = await context.ui.dialog.select<boolean>({
207
+ title: t("settings.showEntryTokens"),
208
+ options: [
209
+ { title: t("settings.on"), value: true },
210
+ { title: t("settings.off"), value: false },
211
+ ],
212
+ current: signals.showEntryTokens(),
213
+ })
214
+ if (picked !== undefined) {
215
+ signals.setShowEntryTokens(picked)
216
+ kv.set(SETTING_KEYS.showEntryTokens, picked)
217
+ toast(t("settings.showEntryTokens"), onOff(picked))
218
+ }
219
+ }
220
+
221
+ // ── 本地数据库补全 ──
222
+ if (choice === "dbsync") {
223
+ const picked = await context.ui.dialog.select<boolean>({
224
+ title: t("settings.dbSync"),
225
+ options: [
226
+ { title: t("settings.on"), value: true },
227
+ { title: t("settings.off"), value: false },
228
+ ],
229
+ current: signals.dbSync(),
230
+ })
231
+ if (picked !== undefined) {
232
+ signals.setDbSync(picked)
233
+ kv.set(SETTING_KEYS.dbSync, picked)
234
+ toast(t("settings.dbSync"), onOff(picked))
235
+ }
236
+ }
237
+
238
+ // 让宿主先关闭编辑器,再重新打开菜单。
239
+ await nextTick()
240
+ }
241
+ }
package/src/v2/types.ts CHANGED
@@ -154,7 +154,9 @@ export interface Context {
154
154
  }
155
155
  }
156
156
  readonly keymap: {
157
- layer(input: () => { readonly commands?: readonly KeymapCommand[] }): void
157
+ layer(input: () => {
158
+ readonly commands?: readonly KeymapCommand[]
159
+ }): void
158
160
  shortcuts(id: string): readonly string[]
159
161
  }
160
162
  }