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.
Files changed (59) hide show
  1. package/dist/_version.d.ts +1 -1
  2. package/dist/core/kv.d.ts +1 -0
  3. package/dist/core/types.d.ts +8 -0
  4. package/dist/i18n.d.ts +5 -0
  5. package/dist/panel/SubAgentPanel.d.ts +1 -0
  6. package/dist/panel/entry-map.d.ts +1 -0
  7. package/dist/panel/panel-api.d.ts +16 -0
  8. package/dist/tui.js +385 -218
  9. package/dist/v2/db.d.ts +14 -1
  10. package/dist/v2.js +473 -223
  11. package/package.json +17 -4
  12. package/dist/_version.js +0 -2
  13. package/dist/clipboard.js +0 -85
  14. package/dist/core/color.js +0 -68
  15. package/dist/core/format.js +0 -113
  16. package/dist/core/index.js +0 -6
  17. package/dist/core/kv.js +0 -63
  18. package/dist/core/state-machine.js +0 -27
  19. package/dist/core/types.js +0 -2
  20. package/dist/core/usage.js +0 -1
  21. package/dist/i18n.js +0 -333
  22. package/dist/index.js +0 -511
  23. package/dist/panel/SubAgentPanel.js +0 -1572
  24. package/dist/panel/entry-map.js +0 -99
  25. package/dist/panel/panel-api.js +0 -1
  26. package/dist/panel/session-routing.js +0 -4
  27. package/dist/panel/store.js +0 -5
  28. package/dist/server.js +0 -14
  29. package/dist/v2/commands.js +0 -420
  30. package/dist/v2/db.js +0 -267
  31. package/dist/v2/index.js +0 -83
  32. package/dist/v2/settings-menu.js +0 -225
  33. package/dist/v2/theme.js +0 -12
  34. package/dist/v2/types.js +0 -6
  35. package/dist/v2/v2-panel-api.js +0 -612
  36. package/src/_version.ts +0 -2
  37. package/src/clipboard.ts +0 -134
  38. package/src/core/color.ts +0 -70
  39. package/src/core/format.ts +0 -105
  40. package/src/core/index.ts +0 -6
  41. package/src/core/kv.ts +0 -77
  42. package/src/core/state-machine.ts +0 -46
  43. package/src/core/types.ts +0 -72
  44. package/src/core/usage.ts +0 -16
  45. package/src/i18n.ts +0 -345
  46. package/src/index.tsx +0 -564
  47. package/src/panel/SubAgentPanel.tsx +0 -1802
  48. package/src/panel/entry-map.ts +0 -115
  49. package/src/panel/panel-api.ts +0 -83
  50. package/src/panel/session-routing.ts +0 -10
  51. package/src/panel/store.ts +0 -8
  52. package/src/server.ts +0 -19
  53. package/src/v2/commands.ts +0 -408
  54. package/src/v2/db.ts +0 -274
  55. package/src/v2/index.tsx +0 -144
  56. package/src/v2/settings-menu.ts +0 -241
  57. package/src/v2/theme.ts +0 -14
  58. package/src/v2/types.ts +0 -167
  59. package/src/v2/v2-panel-api.ts +0 -515
package/src/v2/db.ts DELETED
@@ -1,274 +0,0 @@
1
- /**
2
- * 本地 opencode.db 只读索引(V2)。
3
- *
4
- * 宿主数据层只为当前 TUI 已加载的会话缓存消息:前台子代理(非 background)
5
- * 的 success 事件经常拿不到 metadata.sessionID,条目会一直停在 running,
6
- * model/tokens/cost 也无法回填。这些数据在宿主数据库里是完整的:
7
- * - 父会话 assistant 消息中,工具 part 的 state.metadata.sessionID 指向子会话(前台/后台都有);
8
- * - session_v2 保存子会话的 agent/model/tokens/cost 与 time_idle/idle_outcome。
9
- *
10
- * 本模块以只读方式查询该库作为补充数据源;bun:sqlite 不可用、库不存在或
11
- * 设置项 dbSync 关闭时静默禁用(回退宿主 API)。库结构属于宿主内部实现,
12
- * 任何读取失败都返回 undefined,绝不影响面板其余功能。
13
- */
14
-
15
- import { homedir } from "node:os"
16
-
17
- export interface ChildSessionInfo {
18
- id: string
19
- parentId?: string
20
- agent?: string
21
- title?: string
22
- model?: string
23
- cost?: number
24
- tokens?: number
25
- timeCreated?: number
26
- timeIdle?: number
27
- idleOutcome?: string
28
- }
29
-
30
- export interface SessionDbIndex {
31
- /** 数据库可用且设置项开启时为 true。 */
32
- enabled(): boolean
33
- /** call id → 子会话 id(前台/后台通用)。 */
34
- resolveCall(parentId: string, callId: string): string | undefined
35
- /** 无 call id 时的兜底:按 agent + 起始时间匹配子会话。 */
36
- matchChild(parentId: string, agent: string | undefined, startedAt: number | undefined): string | undefined
37
- /** 子会话汇总(model/tokens/cost/time_idle)。 */
38
- info(sid: string): ChildSessionInfo | undefined
39
- }
40
-
41
- type SqlRow = Record<string, unknown>
42
- interface SqlStatement {
43
- all(...params: unknown[]): SqlRow[]
44
- get(...params: unknown[]): SqlRow | undefined
45
- }
46
- interface SqlDatabase {
47
- query(sql: string): SqlStatement
48
- close?(): void
49
- }
50
-
51
- // 动态加载:仅 Bun 运行时可用。字符串变量使 tsc/esbuild 不做静态解析,
52
- // 打包产物保留运行时 import;Node(CI 测试)加载失败会静默禁用。
53
- const BUN_SQLITE = "bun:sqlite"
54
-
55
- /** 从宿主 model 字段提取模型 id。
56
- * 兼容三种形态:名字字符串、{ id } 对象,以及序列化后的 JSON 字符串
57
- * (session_v2.model 列存的就是 `{"id":...}` 文本)。 */
58
- export function modelIdOf(value: unknown): string | undefined {
59
- if (typeof value === "string") {
60
- const s = value.trim()
61
- if (s.length === 0) return undefined
62
- if (s.startsWith("{")) {
63
- try { return modelIdOf(JSON.parse(s)) } catch { return undefined }
64
- }
65
- return s
66
- }
67
- if (value && typeof value === "object") {
68
- const id = (value as { id?: unknown }).id
69
- if (id === undefined) return undefined
70
- const direct = modelIdOf(id)
71
- if (direct !== undefined) return direct
72
- const s = String(id)
73
- return s.length > 0 ? s : undefined
74
- }
75
- return undefined
76
- }
77
-
78
- /** 宿主 readSessionTokens 同语义:最后一条 assistant 消息的上下文规模。 */
79
- export function contextTokens(tokens: unknown): number | undefined {
80
- if (!tokens || typeof tokens !== "object") return undefined
81
- const t = tokens as Record<string, unknown>
82
- const cache = (t.cache && typeof t.cache === "object") ? (t.cache as Record<string, unknown>) : {}
83
- const sum =
84
- (Number(t.input) || 0) +
85
- (Number(t.output) || 0) +
86
- (Number(t.reasoning) || 0) +
87
- (Number(cache.read) || 0) +
88
- (Number(cache.write) || 0)
89
- return sum > 0 ? sum : undefined
90
- }
91
-
92
- /** 子会话终态:time_idle 存在才算结束;interrupted 视为取消。
93
- * 没有 time_idle 时不作判断(交给宿主实时状态)——避免时间阈值猜测。 */
94
- export function statusOfChild(info: ChildSessionInfo | undefined): "done" | "cancelled" | undefined {
95
- if (!info || info.timeIdle == null) return undefined
96
- return info.idleOutcome === "interrupted" ? "cancelled" : "done"
97
- }
98
-
99
- /** 定位宿主数据库(可用 OPENCODE_DB 覆盖,遵循 XDG_DATA_HOME)。 */
100
- export function findDbPath(
101
- env: Record<string, string | undefined> = process.env,
102
- home = safeHomedir(),
103
- ): string | undefined {
104
- if (env.OPENCODE_DB) return env.OPENCODE_DB
105
- const base = env.XDG_DATA_HOME && env.XDG_DATA_HOME.length > 0 ? env.XDG_DATA_HOME : `${home}/.local/share`
106
- if (!base || base === "/.local/share") return undefined
107
- return `${base}/opencode/opencode.db`
108
- }
109
-
110
- function safeHomedir(): string {
111
- try { return homedir() } catch { return "" }
112
- }
113
-
114
- const CALL_MAP_SQL = `
115
- SELECT json_extract(p.value, '$.id') AS call_id,
116
- json_extract(p.value, '$.state.metadata.sessionID') AS child_sid
117
- FROM session_message m, json_each(m.data, '$.content') p
118
- WHERE m.session_id = ? AND m.type = 'assistant'
119
- AND json_extract(p.value, '$.id') LIKE 'call_%'
120
- AND json_extract(p.value, '$.state.metadata.sessionID') IS NOT NULL`
121
-
122
- const CHILD_INFO_SQL = `
123
- SELECT parent_id, agent, title, model, cost,
124
- tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
125
- time_created, time_idle, idle_outcome
126
- FROM session_v2 WHERE id = ?`
127
-
128
- const LAST_ASSISTANT_SQL = `
129
- SELECT json_extract(data, '$.tokens.input') AS input,
130
- json_extract(data, '$.tokens.output') AS output,
131
- json_extract(data, '$.tokens.reasoning') AS reasoning,
132
- json_extract(data, '$.tokens.cache.read') AS cache_read,
133
- json_extract(data, '$.tokens.cache.write') AS cache_write,
134
- json_extract(data, '$.model.id') AS model_id
135
- FROM session_message
136
- WHERE session_id = ? AND type = 'assistant' AND json_extract(data, '$.tokens.output') > 0
137
- ORDER BY seq DESC LIMIT 1`
138
-
139
- const ASSISTANT_COST_SQL = `
140
- SELECT COUNT(*) AS n, SUM(COALESCE(json_extract(data, '$.cost'), 0)) AS total
141
- FROM session_message WHERE session_id = ? AND type = 'assistant'`
142
-
143
- const CHILDREN_SQL = `
144
- SELECT id, agent, time_created FROM session_v2 WHERE parent_id = ? ORDER BY time_created`
145
-
146
- const num = (v: unknown): number | undefined => {
147
- const n = Number(v)
148
- return Number.isFinite(n) ? n : undefined
149
- }
150
- const str = (v: unknown): string | undefined => (typeof v === "string" && v.length > 0 ? v : undefined)
151
-
152
- export function createSessionDbIndex(enabled: () => boolean): SessionDbIndex | undefined {
153
- const dbPath = findDbPath()
154
- if (!dbPath) return undefined
155
-
156
- let db: SqlDatabase | undefined
157
- let failed = false
158
- void (async () => {
159
- try {
160
- const mod = (await import(BUN_SQLITE)) as { Database?: new (p: string, o?: Record<string, unknown>) => SqlDatabase }
161
- const Database = mod?.Database
162
- if (!Database) { failed = true; return }
163
- db = new Database(dbPath, { readonly: true })
164
- try { db.query("pragma query_only = on").all() } catch {}
165
- } catch {
166
- failed = true
167
- db = undefined
168
- }
169
- })()
170
-
171
- const CALL_MAP_TTL_MS = 15000
172
- const CALL_MAP_RETRY_MS = 2000
173
- const INFO_TTL_MS = 5000
174
- const callMaps = new Map<string, { map: Map<string, string>; at: number }>()
175
- const infos = new Map<string, { info: ChildSessionInfo | undefined; at: number }>()
176
- const childrenCache = new Map<string, { list: SqlRow[]; at: number }>()
177
-
178
- const ready = (): boolean => {
179
- if (failed || db === undefined) return false
180
- try { return enabled() } catch { return false }
181
- }
182
-
183
- const buildCallMap = (parentId: string): Map<string, string> => {
184
- const now = Date.now()
185
- const prev = callMaps.get(parentId)
186
- if (prev && now - prev.at < CALL_MAP_RETRY_MS) return prev.map
187
- const map = new Map<string, string>()
188
- try {
189
- for (const row of db!.query(CALL_MAP_SQL).all(parentId)) {
190
- const id = str(row.call_id)
191
- const sid = str(row.child_sid)
192
- if (id && sid) map.set(id, sid)
193
- }
194
- } catch {}
195
- callMaps.set(parentId, { map, at: now })
196
- return map
197
- }
198
-
199
- const resolveCall = (parentId: string, callId: string): string | undefined => {
200
- if (!ready() || !parentId || !callId) return undefined
201
- const prev = callMaps.get(parentId)
202
- const map = prev && Date.now() - prev.at < CALL_MAP_TTL_MS ? prev.map : buildCallMap(parentId)
203
- return map.get(callId) ?? buildCallMap(parentId).get(callId)
204
- }
205
-
206
- const matchChild = (parentId: string, agent: string | undefined, startedAt: number | undefined): string | undefined => {
207
- if (!ready() || !parentId || startedAt === undefined) return undefined
208
- const now = Date.now()
209
- let cached = childrenCache.get(parentId)
210
- if (!cached || now - cached.at > CALL_MAP_TTL_MS) {
211
- let list: SqlRow[] = []
212
- try { list = db!.query(CHILDREN_SQL).all(parentId) } catch {}
213
- cached = { list, at: now }
214
- childrenCache.set(parentId, cached)
215
- }
216
- let best: string | undefined
217
- let bestAt = Number.POSITIVE_INFINITY
218
- for (const row of cached.list) {
219
- const sid = str(row.id)
220
- const t = num(row.time_created)
221
- if (!sid || t === undefined) continue
222
- if (t < startedAt - 2000) continue
223
- if (agent !== undefined && str(row.agent) !== agent) continue
224
- if (t < bestAt) { bestAt = t; best = sid }
225
- }
226
- return best
227
- }
228
-
229
- const info = (sid: string): ChildSessionInfo | undefined => {
230
- if (!sid) return undefined
231
- const hit = infos.get(sid)
232
- if (hit && Date.now() - hit.at < INFO_TTL_MS) return hit.info
233
- if (!ready()) return undefined
234
- let result: ChildSessionInfo | undefined
235
- try {
236
- const row = db!.query(CHILD_INFO_SQL).get(sid)
237
- if (row) {
238
- const last = db!.query(LAST_ASSISTANT_SQL).get(sid)
239
- let tokens = contextTokens(last ? {
240
- input: last.input, output: last.output, reasoning: last.reasoning,
241
- cache: { read: last.cache_read, write: last.cache_write },
242
- } : undefined)
243
- if (tokens === undefined) {
244
- const agg = (num(row.tokens_input) || 0) + (num(row.tokens_output) || 0) + (num(row.tokens_reasoning) || 0) +
245
- (num(row.tokens_cache_read) || 0) + (num(row.tokens_cache_write) || 0)
246
- if (agg > 0) tokens = agg
247
- }
248
- // cost 语义对齐宿主 readSessionCost:优先会话聚合值,
249
- // 0 时回退 assistant 消息求和(有消息即权威,0 表示确实免费)。
250
- let cost = num(row.cost)
251
- if (!(typeof cost === "number" && cost > 0)) {
252
- const aggCost = db!.query(ASSISTANT_COST_SQL).get(sid)
253
- if (aggCost && Number(aggCost.n) > 0) cost = Number(aggCost.total) || 0
254
- }
255
- result = {
256
- id: sid,
257
- parentId: str(row.parent_id),
258
- agent: str(row.agent),
259
- title: str(row.title),
260
- model: modelIdOf(last?.model_id) ?? modelIdOf(row.model),
261
- cost,
262
- tokens,
263
- timeCreated: num(row.time_created),
264
- timeIdle: num(row.time_idle),
265
- idleOutcome: str(row.idle_outcome),
266
- }
267
- }
268
- } catch { result = undefined }
269
- infos.set(sid, { info: result, at: Date.now() })
270
- return result
271
- }
272
-
273
- return { enabled: ready, resolveCall, matchChild, info }
274
- }
package/src/v2/index.tsx DELETED
@@ -1,144 +0,0 @@
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, TimeFormat } from "../core/types"
11
- import { TIME_FORMATS } from "../core/format"
12
- import { SETTING_KEYS } from "../core/kv"
13
- import { LANG_META, detectLang } from "../i18n"
14
-
15
- /** 命令层必须在 app 槽注册:侧栏隐藏(sidebar: auto)时命令仍需可用。 */
16
- function CommandRoot(props: {
17
- context: Context
18
- api: PanelApi
19
- signals: SharedSignals
20
- }) {
21
- props.context.keymap.layer(() => ({
22
- mode: "global" as const,
23
- commands: makeCommands(props.context, props.api, props.signals),
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
- }) {
35
- return (
36
- <SubAgentPanel
37
- api={props.api}
38
- theme={mapTheme(props.context.theme)}
39
- lang={props.signals.lang}
40
- maxEntries={props.signals.maxEntries}
41
- sortOrder={props.signals.sortOrder}
42
- scrollMode={props.signals.scrollMode}
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}
48
- sessionId={props.sessionID}
49
- />
50
- )
51
- }
52
-
53
- /** V2 入口:setup 创建共享信号 + PanelApi + 命令 layer + 侧边栏槽位。
54
- * 行为对齐 V1 tui()(信号初始值从 KV 恢复;侧边栏渲染共享 SubAgentPanel)。 */
55
- const mod: PluginModule & { server: () => Promise<Record<string, never>> } = {
56
- id: "opencode-subagent-magazine",
57
- setup(context: Context) {
58
- // settings 惰性 getter(闭包引用信号变量——运行时已赋值)
59
- let lang!: () => Lang
60
- let maxEntries!: () => number
61
- let sortOrder!: () => SortOrder
62
- let scrollMode!: () => ScrollMode
63
- const api = createPanelApi(context, {
64
- lang: () => lang(),
65
- maxEntries: () => maxEntries(),
66
- sortOrder: () => sortOrder(),
67
- scrollMode: () => scrollMode(),
68
- })
69
-
70
- // 信号初始值从 KV 恢复(对齐 V1 tui())
71
- const storedLang = String(api.kv.get(SETTING_KEYS.lang, ""))
72
- const initialLang: Lang =
73
- LANG_META.some((m) => m.code === storedLang) ? (storedLang as Lang) : detectLang()
74
- const [langSignal, setLang] = createSignal<Lang>(initialLang)
75
- const [maxSignal, setMaxEntries] = createSignal<number>(Number(api.kv.get(SETTING_KEYS.maxEntries, "10")) || 10)
76
- const [orderSignal, setSortOrder] = createSignal<SortOrder>(
77
- String(api.kv.get(SETTING_KEYS.order, "desc")) === "asc" ? "asc" : "desc",
78
- )
79
- const [scrollSignal, setScrollMode] = createSignal<ScrollMode>(
80
- String(api.kv.get(SETTING_KEYS.scrollMode, "wheel")) === "click" ? "click" : "wheel",
81
- )
82
- const [borderSignal, setBorderVisible] = createSignal<boolean>(
83
- (api.kv.get(SETTING_KEYS.border, false) as boolean) === true,
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
- )
101
- lang = langSignal
102
- maxEntries = maxSignal
103
- sortOrder = orderSignal
104
- scrollMode = scrollSignal
105
-
106
- const signals: SharedSignals = {
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: "",
114
- }
115
-
116
- // 命令层挂 app 槽:侧栏隐藏时斜杠命令仍需可用(见 CommandRoot)
117
- context.ui.slot({
118
- append: "app",
119
- render: () => (
120
- <CommandRoot context={context} api={api} signals={signals} />
121
- ),
122
- })
123
-
124
- // 侧边栏面板(共享 SubAgentPanel——V1/V2 同一组件)
125
- context.ui.slot({
126
- prepend: "sidebar.content",
127
- render: (props) => {
128
- signals.sessionId = String(props.sessionID ?? "")
129
- return (
130
- <PluginRoot
131
- context={context}
132
- api={api}
133
- signals={signals}
134
- sessionID={String(props.sessionID ?? "")}
135
- />
136
- )
137
- },
138
- })
139
- },
140
- // V1 server 空实现(兼容标记):v2 加载 setup,V1 检测需要 server 字段识别为插件
141
- server: async () => ({}),
142
- }
143
-
144
- export default mod
@@ -1,241 +0,0 @@
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/theme.ts DELETED
@@ -1,14 +0,0 @@
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
- }