opencode-subagent-magazine 1.5.0 → 1.5.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/src/i18n.ts ADDED
@@ -0,0 +1,261 @@
1
+ // ---------------------------------------------------------------------------
2
+ // i18n — centralized translations (aligned with opencode-visual-cache).
3
+ // Add a language by appending a table that satisfies `Translation`; the
4
+ // compiler enforces key completeness.
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type LangCode = "zh" | "en" | "ja" | "ko"
8
+
9
+ const ZH_T = {
10
+ "panel.title": "子代理",
11
+ "status.none": "暂无子代理",
12
+ "agent.label": "代理",
13
+ "status.label": "状态",
14
+ "time.label": "耗时",
15
+ "tokens.label": "上下文",
16
+ "error.label": "错误",
17
+ "model.label": "模型",
18
+ "todo.label": "进度",
19
+ "session.label": "会话 ID",
20
+ "session.toast.copy": "可手动复制上方 ID",
21
+ "session.toast.copied": "会话 ID 已复制",
22
+ "session.toast.copy_failed": "无法访问系统剪贴板,请手动复制上方 ID",
23
+ "open.label": "进入会话",
24
+ "cost.label": "费用",
25
+ "scroll.more": "更多",
26
+ "scroll.top": "回顶",
27
+ "scroll.bottom": "回底",
28
+ "dismiss.label": "标记完成",
29
+ "cancel.label": "取消",
30
+ "status.running": "运行中",
31
+ "status.done": "已完成",
32
+ "status.cancelled": "已取消",
33
+ "status.error": "错误",
34
+ "order.desc": "降序(最新在前)",
35
+ "order.asc": "升序(最早在前)",
36
+ "scroll.wheel": "滚轮翻页",
37
+ "scroll.click": "点击翻页",
38
+ "ttl.label": "清理周期",
39
+ "ttl.3d": "3 天",
40
+ "ttl.7d": "7 天",
41
+ "ttl.14d": "14 天",
42
+ "ttl.30d": "30 天",
43
+ "ttl.unlimited": "无期限",
44
+ "ttl.toast": "清理周期已设为 {n} 天",
45
+ "ttl.toast_unlimited": "清理周期已设为无期限",
46
+ "clear.title": "确认清除",
47
+ "clear.prompt": "确定清除当前会话所有子代理记录?此操作不可撤销。",
48
+ "clear.prompt_running": "当前有 {n} 个运行中的子代理,清除后将不可恢复。确定继续?",
49
+ "clear.done": "已清除 {n} 条子代理记录",
50
+ "clear.empty": "当前会话无子代理记录",
51
+ "cancel.no_session": "子会话 ID 不可用",
52
+ "cancel.not_child": "目标不是子会话",
53
+ "cancel.read_error": "无法读取会话信息",
54
+ "cancel.outside_tree": "目标不在当前监控会话树中",
55
+ "cancel.already_ended": "会话已结束,无需取消",
56
+ "cancel.status_error": "无法查询会话状态",
57
+ "cancel.sent": "已发送取消指令",
58
+ "cancel.failed": "取消失败",
59
+ } as const
60
+
61
+ /** 结构约束:值放宽为 string,键集合来自中文表(新增语言缺 key 会编译报错)。 */
62
+ export type Translation = { [K in keyof typeof ZH_T]: string }
63
+
64
+ const EN_T: Translation = {
65
+ "panel.title": "SubAgent",
66
+ "status.none": "No sub-agents yet",
67
+ "agent.label": "agent",
68
+ "status.label": "status",
69
+ "time.label": "time",
70
+ "tokens.label": "tokens",
71
+ "error.label": "error",
72
+ "model.label": "model",
73
+ "todo.label": "todo",
74
+ "session.label": "session ID",
75
+ "session.toast.copy": "Copy the ID above manually",
76
+ "session.toast.copied": "Session ID copied",
77
+ "session.toast.copy_failed": "Cannot access the system clipboard; copy the ID above manually",
78
+ "open.label": "Open session",
79
+ "cost.label": "cost",
80
+ "scroll.more": "more",
81
+ "scroll.top": "Top",
82
+ "scroll.bottom": "Bottom",
83
+ "dismiss.label": "dismiss",
84
+ "cancel.label": "Cancel",
85
+ "status.running": "running",
86
+ "status.done": "done",
87
+ "status.cancelled": "cancelled",
88
+ "status.error": "error",
89
+ "order.desc": "Desc (newest first)",
90
+ "order.asc": "Asc (oldest first)",
91
+ "scroll.wheel": "Wheel Scroll",
92
+ "scroll.click": "Click Scroll",
93
+ "ttl.label": "TTL (Time to Live)",
94
+ "ttl.3d": "3 days",
95
+ "ttl.7d": "7 days",
96
+ "ttl.14d": "14 days",
97
+ "ttl.30d": "30 days",
98
+ "ttl.unlimited": "Never",
99
+ "ttl.toast": "TTL set to {n} days",
100
+ "ttl.toast_unlimited": "TTL set to Never",
101
+ "clear.title": "Confirm",
102
+ "clear.prompt": "Clear all sub-agent records for this session? This cannot be undone.",
103
+ "clear.prompt_running": "{n} sub-agent(s) are still running. Clearing will discard them permanently. Continue?",
104
+ "clear.done": "Cleared {n} sub-agent record(s)",
105
+ "clear.empty": "No sub-agent records in this session",
106
+ "cancel.no_session": "Child session ID is unavailable",
107
+ "cancel.not_child": "Target is not a child session",
108
+ "cancel.read_error": "Cannot read session info",
109
+ "cancel.outside_tree": "Target is outside the monitored session tree",
110
+ "cancel.already_ended": "Session already ended, no need to cancel",
111
+ "cancel.status_error": "Cannot query session status",
112
+ "cancel.sent": "Cancel instruction sent",
113
+ "cancel.failed": "Cancellation failed",
114
+ }
115
+
116
+ const JA_T: Translation = {
117
+ "panel.title": "サブエージェント",
118
+ "status.none": "サブエージェントなし",
119
+ "agent.label": "エージェント",
120
+ "status.label": "状態",
121
+ "time.label": "時間",
122
+ "tokens.label": "コンテキスト",
123
+ "error.label": "エラー",
124
+ "model.label": "モデル",
125
+ "todo.label": "進捗",
126
+ "session.label": "セッション ID",
127
+ "session.toast.copy": "上の ID を手動でコピーしてください",
128
+ "session.toast.copied": "セッション ID をコピーしました",
129
+ "session.toast.copy_failed": "システムのクリップボードにアクセスできないため、上の ID を手動でコピーしてください",
130
+ "open.label": "セッションを開く",
131
+ "cost.label": "費用",
132
+ "scroll.more": "もっと",
133
+ "scroll.top": "先頭へ",
134
+ "scroll.bottom": "末尾へ",
135
+ "dismiss.label": "完了にする",
136
+ "cancel.label": "キャンセル",
137
+ "status.running": "実行中",
138
+ "status.done": "完了",
139
+ "status.cancelled": "キャンセル済み",
140
+ "status.error": "エラー",
141
+ "order.desc": "降順(新しい順)",
142
+ "order.asc": "昇順(古い順)",
143
+ "scroll.wheel": "ホイールスクロール",
144
+ "scroll.click": "クリックスクロール",
145
+ "ttl.label": "保持期間",
146
+ "ttl.3d": "3 日",
147
+ "ttl.7d": "7 日",
148
+ "ttl.14d": "14 日",
149
+ "ttl.30d": "30 日",
150
+ "ttl.unlimited": "無期限",
151
+ "ttl.toast": "保持期間を {n} 日に設定しました",
152
+ "ttl.toast_unlimited": "保持期間を無期限に設定しました",
153
+ "clear.title": "削除の確認",
154
+ "clear.prompt": "このセッションの全サブエージェント記録を削除しますか?この操作は元に戻せません。",
155
+ "clear.prompt_running": "実行中のサブエージェントが {n} 個あります。削除すると復元できません。続行しますか?",
156
+ "clear.done": "{n} 件のサブエージェント記録を削除しました",
157
+ "clear.empty": "このセッションにサブエージェント記録はありません",
158
+ "cancel.no_session": "子セッション ID が利用できません",
159
+ "cancel.not_child": "対象は子セッションではありません",
160
+ "cancel.read_error": "セッション情報を読み取れません",
161
+ "cancel.outside_tree": "対象は監視対象のセッションツリー外です",
162
+ "cancel.already_ended": "セッションは既に終了しています",
163
+ "cancel.status_error": "セッション状態を照会できません",
164
+ "cancel.sent": "キャンセル指示を送信しました",
165
+ "cancel.failed": "キャンセルに失敗しました",
166
+ }
167
+
168
+ const KO_T: Translation = {
169
+ "panel.title": "서브 에이전트",
170
+ "status.none": "서브 에이전트 없음",
171
+ "agent.label": "에이전트",
172
+ "status.label": "상태",
173
+ "time.label": "시간",
174
+ "tokens.label": "컨텍스트",
175
+ "error.label": "오류",
176
+ "model.label": "모델",
177
+ "todo.label": "진행도",
178
+ "session.label": "세션 ID",
179
+ "session.toast.copy": "위 ID를 수동으로 복사하세요",
180
+ "session.toast.copied": "세션 ID가 복사되었습니다",
181
+ "session.toast.copy_failed": "시스템 클립보드에 접근할 수 없어 위 ID를 수동으로 복사하세요",
182
+ "open.label": "세션 열기",
183
+ "cost.label": "비용",
184
+ "scroll.more": "더 보기",
185
+ "scroll.top": "맨 위로",
186
+ "scroll.bottom": "맨 아래로",
187
+ "dismiss.label": "완료 표시",
188
+ "cancel.label": "취소",
189
+ "status.running": "실행 중",
190
+ "status.done": "완료",
191
+ "status.cancelled": "취소됨",
192
+ "status.error": "오류",
193
+ "order.desc": "내림차순(최신순)",
194
+ "order.asc": "오름차순(오래된순)",
195
+ "scroll.wheel": "휠 스크롤",
196
+ "scroll.click": "클릭 스크롤",
197
+ "ttl.label": "보존 기간",
198
+ "ttl.3d": "3일",
199
+ "ttl.7d": "7일",
200
+ "ttl.14d": "14일",
201
+ "ttl.30d": "30일",
202
+ "ttl.unlimited": "무기한",
203
+ "ttl.toast": "보존 기간을 {n}일로 설정했습니다",
204
+ "ttl.toast_unlimited": "보존 기간을 무기한으로 설정했습니다",
205
+ "clear.title": "삭제 확인",
206
+ "clear.prompt": "이 세션의 모든 서브 에이전트 기록을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
207
+ "clear.prompt_running": "실행 중인 서브 에이전트가 {n}개 있습니다. 삭제하면 복구할 수 없습니다. 계속하시겠습니까?",
208
+ "clear.done": "서브 에이전트 기록 {n}개를 삭제했습니다",
209
+ "clear.empty": "이 세션에 서브 에이전트 기록이 없습니다",
210
+ "cancel.no_session": "하위 세션 ID를 사용할 수 없습니다",
211
+ "cancel.not_child": "대상이 하위 세션이 아닙니다",
212
+ "cancel.read_error": "세션 정보를 읽을 수 없습니다",
213
+ "cancel.outside_tree": "대상이 모니터링 세션 트리 밖에 있습니다",
214
+ "cancel.already_ended": "세션이 이미 종료되었습니다",
215
+ "cancel.status_error": "세션 상태를 조회할 수 없습니다",
216
+ "cancel.sent": "취소 지시를 보냈습니다",
217
+ "cancel.failed": "취소에 실패했습니다",
218
+ }
219
+
220
+ export const LANGS: Record<LangCode, Translation> = { zh: ZH_T, en: EN_T, ja: JA_T, ko: KO_T }
221
+
222
+ /** 语言元数据:/subagent-lang 选项与自动检测共用。 */
223
+ export const LANG_META: { code: LangCode; label: string }[] = [
224
+ { code: "zh", label: "中文" },
225
+ { code: "en", label: "English" },
226
+ { code: "ja", label: "日本語" },
227
+ { code: "ko", label: "한국어" },
228
+ ]
229
+
230
+ /**
231
+ * 模板参数替换:`{key}` 占位符统一在此处理。
232
+ * 未提供的参数保留原占位符,避免静默丢失。
233
+ */
234
+ export function applyParams(tpl: string, params?: Record<string, string | number>): string {
235
+ if (!params) return tpl
236
+ return tpl.replace(/\{(\w+)\}/g, (m, k: string) =>
237
+ k in params ? String(params[k]) : m,
238
+ )
239
+ }
240
+
241
+ /**
242
+ * 宽松翻译工厂:未知 key 回退为 key 原样(兼容非翻译用途的键名透传)。
243
+ * `getCode` 读取语言信号——在 SolidJS 渲染/memo 上下文中调用时自动建立响应式依赖。
244
+ */
245
+ export function createT(getCode: () => LangCode) {
246
+ return (key: string, params?: Record<string, string | number>): string =>
247
+ applyParams(LANGS[getCode()][key as keyof Translation] ?? key, params)
248
+ }
249
+
250
+ /** 按系统 locale 自动检测语言(zh → 中文,ja → 日语,ko → 韩语,其余 → 英文)。 */
251
+ export function detectLang(): LangCode {
252
+ try {
253
+ const loc = Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase()
254
+ if (loc.startsWith("zh")) return "zh"
255
+ if (loc.startsWith("ja")) return "ja"
256
+ if (loc.startsWith("ko")) return "ko"
257
+ return "en"
258
+ } catch {
259
+ return "en"
260
+ }
261
+ }
package/src/index.tsx CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  } from "solid-js"
22
22
  import { PLUGIN_VERSION } from "./_version"
23
23
  import { copyText } from "./clipboard"
24
+ import { LangCode, LANG_META, createT, detectLang } from "./i18n"
24
25
 
25
26
  // ===================================================================
26
27
  // Types
@@ -48,130 +49,13 @@ interface SubEntry {
48
49
  cancelReason?: "manual"
49
50
  }
50
51
 
51
- type Lang = "zh" | "en"
52
+ type Lang = LangCode
52
53
  type SortOrder = "desc" | "asc"
53
54
  type ScrollMode = "wheel" | "click"
54
55
 
55
56
  /** OpenCode built-in tool names that spawn sub-agents or delegate tasks. */
56
57
  const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
57
58
 
58
- // ===================================================================
59
- // i18n
60
- // ===================================================================
61
-
62
- const I18N: Record<Lang, Record<string, string>> = {
63
- zh: {
64
- "panel.title": "子代理",
65
- "status.none": "暂无子代理",
66
- "agent.label": "代理",
67
- "status.label": "状态",
68
- "time.label": "耗时",
69
- "tokens.label": "上下文",
70
- "error.label": "错误",
71
- "model.label": "模型",
72
- "todo.label": "进度",
73
- "session.label": "会话 ID",
74
- "session.toast.copy": "可手动复制上方 ID",
75
- "session.toast.copied": "会话 ID 已复制",
76
- "session.toast.copy_failed": "无法访问系统剪贴板,请手动复制上方 ID",
77
- "open.label": "进入会话",
78
- "cost.label": "费用",
79
- "scroll.more": "更多",
80
- "scroll.top": "回顶",
81
- "scroll.bottom": "回底",
82
- "dismiss.label": "标记完成",
83
- "cancel.label": "取消",
84
- "status.running": "运行中",
85
- "status.done": "已完成",
86
- "status.cancelled": "已取消",
87
- "status.error": "错误",
88
- "order.desc": "降序(最新在前)",
89
- "order.asc": "升序(最早在前)",
90
- "scroll.wheel": "滚轮翻页",
91
- "scroll.click": "点击翻页",
92
- "ttl.label": "清理周期",
93
- "ttl.3d": "3 天",
94
- "ttl.7d": "7 天",
95
- "ttl.14d": "14 天",
96
- "ttl.30d": "30 天",
97
- "ttl.unlimited": "无期限",
98
- "ttl.toast": "清理周期已设为 {n} 天",
99
- "ttl.toast_unlimited": "清理周期已设为无期限",
100
- "clear.title": "确认清除",
101
- "clear.prompt": "确定清除当前会话所有子代理记录?此操作不可撤销。",
102
- "clear.prompt_running": "当前有 {n} 个运行中的子代理,清除后将不可恢复。确定继续?",
103
- "clear.done": "已清除 {n} 条子代理记录",
104
- "clear.empty": "当前会话无子代理记录",
105
- "cancel.no_session": "子会话 ID 不可用",
106
- "cancel.not_child": "目标不是子会话",
107
- "cancel.read_error": "无法读取会话信息",
108
- "cancel.outside_tree": "目标不在当前监控会话树中",
109
- "cancel.already_ended": "会话已结束,无需取消",
110
- "cancel.status_error": "无法查询会话状态",
111
- "cancel.sent": "已发送取消指令",
112
- "cancel.failed": "取消失败",
113
- },
114
- en: {
115
- "panel.title": "SubAgent",
116
- "status.none": "No sub-agents yet",
117
- "agent.label": "agent",
118
- "status.label": "status",
119
- "time.label": "time",
120
- "tokens.label": "tokens",
121
- "error.label": "error",
122
- "model.label": "model",
123
- "todo.label": "todo",
124
- "session.label": "session ID",
125
- "session.toast.copy": "Copy the ID above manually",
126
- "session.toast.copied": "Session ID copied",
127
- "session.toast.copy_failed": "Cannot access the system clipboard; copy the ID above manually",
128
- "open.label": "Open session",
129
- "cost.label": "cost",
130
- "scroll.more": "more",
131
- "scroll.top": "Top",
132
- "scroll.bottom": "Bottom",
133
- "dismiss.label": "dismiss",
134
- "cancel.label": "Cancel",
135
- "status.running": "running",
136
- "status.done": "done",
137
- "status.cancelled": "cancelled",
138
- "status.error": "error",
139
- "order.desc": "Desc (newest first)",
140
- "order.asc": "Asc (oldest first)",
141
- "scroll.wheel": "Wheel Scroll",
142
- "scroll.click": "Click Scroll",
143
- "ttl.label": "TTL (Time to Live)",
144
- "ttl.3d": "3 days",
145
- "ttl.7d": "7 days",
146
- "ttl.14d": "14 days",
147
- "ttl.30d": "30 days",
148
- "ttl.unlimited": "Never",
149
- "ttl.toast": "TTL set to {n} days",
150
- "ttl.toast_unlimited": "TTL set to Never",
151
- "clear.title": "Confirm",
152
- "clear.prompt": "Clear all sub-agent records for this session? This cannot be undone.",
153
- "clear.prompt_running": "{n} sub-agent(s) are still running. Clearing will discard them permanently. Continue?",
154
- "clear.done": "Cleared {n} sub-agent record(s)",
155
- "clear.empty": "No sub-agent records in this session",
156
- "cancel.no_session": "Child session ID is unavailable",
157
- "cancel.not_child": "Target is not a child session",
158
- "cancel.read_error": "Cannot read session info",
159
- "cancel.outside_tree": "Target is outside the monitored session tree",
160
- "cancel.already_ended": "Session already ended, no need to cancel",
161
- "cancel.status_error": "Cannot query session status",
162
- "cancel.sent": "Cancel instruction sent",
163
- "cancel.failed": "Cancellation failed",
164
- },
165
- }
166
-
167
- declare const process: { env: Record<string, string | undefined> } | undefined
168
-
169
- function detectLang(): Lang {
170
- const env = process?.env?.OPENCODE_LANG ?? process?.env?.LANG ?? ""
171
- if (env.startsWith("zh")) return "zh"
172
- return "en"
173
- }
174
-
175
59
  // ===================================================================
176
60
  // Helpers — visual width
177
61
  // ===================================================================
@@ -332,7 +216,7 @@ function SubAgentPanel(props: {
332
216
  scrollMode: () => ScrollMode
333
217
  sessionId: string
334
218
  }): JSX.Element {
335
- const t = (key: string) => I18N[props.lang()][key] ?? key
219
+ const t = createT(() => props.lang())
336
220
 
337
221
  // ── session data (single-key, true deletion on cleanup) ──
338
222
  const SESSION_DATA_KEY = `${KV_PREFIX}.session_data`
@@ -544,8 +428,9 @@ function SubAgentPanel(props: {
544
428
  let boxEl: any
545
429
  let disposed = false
546
430
 
547
- /** Total context tokens for a sub-agent session.
548
- * Matches opencode-visual-cache's "总计": last assistant message's input + cache.read. */
431
+ /** Total usage tokens for a sub-agent session.
432
+ * Matches opencode's bottom status bar usage: last assistant message with
433
+ * output > 0, summing input + output + reasoning + cache read/write. */
549
434
  const readSessionTokens = (sid: string): number | undefined => {
550
435
  if (!sid) return undefined
551
436
  try {
@@ -555,9 +440,13 @@ function SubAgentPanel(props: {
555
440
  const m = (msgs as any[])[i]
556
441
  if (m.role !== "assistant") continue
557
442
  const t = m.tokens
558
- if (!t) continue
559
- const cache = t.cache as { read?: number; write?: number } | undefined
560
- const ctx = (Number(t.input) || 0) + (cache?.read ?? 0)
443
+ if (!t || !(Number(t.output) > 0)) continue
444
+ const ctx =
445
+ (Number(t.input) || 0) +
446
+ (Number(t.output) || 0) +
447
+ (Number(t.reasoning) || 0) +
448
+ (Number(t.cache?.read) || 0) +
449
+ (Number(t.cache?.write) || 0)
561
450
  if (ctx > 0) return ctx
562
451
  }
563
452
  }
@@ -667,7 +556,7 @@ function SubAgentPanel(props: {
667
556
  if (!childId) {
668
557
  props.api.ui.toast({
669
558
  title: entry.title || entry.agent,
670
- message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.no_session"] ?? "Child session ID is unavailable"),
559
+ message: t("cancel.label") + ": " + t("cancel.no_session"),
671
560
  })
672
561
  return
673
562
  }
@@ -677,14 +566,14 @@ function SubAgentPanel(props: {
677
566
  if (!child?.parentID) {
678
567
  props.api.ui.toast({
679
568
  title: entry.title || entry.agent,
680
- message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.not_child"] ?? "Target is not a child session"),
569
+ message: t("cancel.label") + ": " + t("cancel.not_child"),
681
570
  })
682
571
  return
683
572
  }
684
573
  } catch {
685
574
  props.api.ui.toast({
686
575
  title: entry.title || entry.agent,
687
- message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.read_error"] ?? "Cannot read session info"),
576
+ message: t("cancel.label") + ": " + t("cancel.read_error"),
688
577
  })
689
578
  return
690
579
  }
@@ -692,7 +581,7 @@ function SubAgentPanel(props: {
692
581
  if (!isDescendantOf(childId, props.sessionId)) {
693
582
  props.api.ui.toast({
694
583
  title: entry.title || entry.agent,
695
- message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.outside_tree"] ?? "Target is outside the monitored session tree"),
584
+ message: t("cancel.label") + ": " + t("cancel.outside_tree"),
696
585
  })
697
586
  return
698
587
  }
@@ -712,7 +601,7 @@ function SubAgentPanel(props: {
712
601
  } catch {
713
602
  props.api.ui.toast({
714
603
  title: entry.title || entry.agent,
715
- message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.status_error"] ?? "Cannot query session status"),
604
+ message: t("cancel.label") + ": " + t("cancel.status_error"),
716
605
  })
717
606
  return
718
607
  }
@@ -730,7 +619,7 @@ function SubAgentPanel(props: {
730
619
  status: "cancel_requested", sessionId: entry.sessionId,
731
620
  abortAccepted: true,
732
621
  } as any)
733
- props.api.ui.toast({ message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.sent"] ?? "Cancel instruction sent") })
622
+ props.api.ui.toast({ message: t("cancel.label") + ": " + t("cancel.sent") })
734
623
  } catch (err) {
735
624
  upsertEntry({
736
625
  id: entry.id, title: entry.title, agent: entry.agent, prompt: entry.prompt,
@@ -739,7 +628,7 @@ function SubAgentPanel(props: {
739
628
  })
740
629
  props.api.ui.toast({
741
630
  title: entry.title || entry.agent,
742
- message: t("cancel.label") + ": " + (I18N[props.lang()]["cancel.failed"] ?? "Cancellation failed"),
631
+ message: t("cancel.label") + ": " + t("cancel.failed"),
743
632
  })
744
633
  }
745
634
  }
@@ -1895,7 +1784,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1895
1784
  // ── language ──
1896
1785
  const stored = String(api.kv.get(`${KV_PREFIX}.lang`, ""))
1897
1786
  const initialLang: Lang =
1898
- stored === "zh" || stored === "en" ? stored : detectLang()
1787
+ LANG_META.some((m) => m.code === stored) ? (stored as Lang) : detectLang()
1899
1788
  const [lang, setLang] = createSignal<Lang>(initialLang)
1900
1789
  const [maxEntries, setMaxEntries] = createSignal(
1901
1790
  parseInt(String(api.kv.get(`${KV_PREFIX}.max_entries`, "10")), 10) || 10
@@ -1922,16 +1811,13 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1922
1811
  dialog?.replace(() => (
1923
1812
  <api.ui.DialogSelect
1924
1813
  title="Language / 语言"
1925
- options={[
1926
- { title: "中文", value: "zh" },
1927
- { title: "English", value: "en" },
1928
- ]}
1814
+ options={LANG_META.map((m) => ({ title: m.label, value: m.code }))}
1929
1815
  onSelect={(opt) => {
1930
1816
  const l = opt.value as Lang
1931
1817
  setLang(l)
1932
1818
  api.kv.set(`${KV_PREFIX}.lang`, l)
1933
1819
  api.ui.toast({
1934
- message: l === "zh" ? "语言: 中文" : "Language: English",
1820
+ message: "Language: " + (LANG_META.find((m) => m.code === l)?.label ?? l),
1935
1821
  })
1936
1822
  dialog?.clear()
1937
1823
  }}
@@ -1945,7 +1831,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1945
1831
  description: "Set sub-agent entry sort order (desc / asc)",
1946
1832
  slash: { name: "subagent-order" },
1947
1833
  onSelect: (dialog) => {
1948
- const t = (k: string) => I18N[lang()][k] ?? k
1834
+ const t = createT(() => lang())
1949
1835
  dialog?.replace(() => (
1950
1836
  <api.ui.DialogSelect
1951
1837
  title="Sort Order / 排序方式"
@@ -1972,7 +1858,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
1972
1858
  description: "Set scroll mode (wheel / click)",
1973
1859
  slash: { name: "subagent-scroll" },
1974
1860
  onSelect: (dialog) => {
1975
- const t = (k: string) => I18N[lang()][k] ?? k
1861
+ const t = createT(() => lang())
1976
1862
  dialog?.replace(() => (
1977
1863
  <api.ui.DialogSelect
1978
1864
  title="Scroll Mode / 滚动模式"
@@ -2100,7 +1986,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
2100
1986
  description: "Set session data retention period (days before auto-cleanup)",
2101
1987
  slash: { name: "subagent-ttl" },
2102
1988
  onSelect: (dialog) => {
2103
- const t = (k: string) => I18N[signals.lang()][k] ?? k
1989
+ const t = createT(() => signals.lang())
2104
1990
  const curRaw = parseInt(String(api.kv.get(`${KV_PREFIX}.ttl_days`, "3")), 10)
2105
1991
  const curDays = Number.isNaN(curRaw) ? 3 : curRaw
2106
1992
  const curLabel = curDays === 0 ? t("ttl.unlimited") : `${curDays}d`
@@ -2117,7 +2003,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
2117
2003
  onSelect={(opt) => {
2118
2004
  const days = parseInt(opt.value, 10)
2119
2005
  api.kv.set(`${KV_PREFIX}.ttl_days`, String(days))
2120
- const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast").replace("{n}", String(days))
2006
+ const msg = days === 0 ? t("ttl.toast_unlimited") : t("ttl.toast", { n: days })
2121
2007
  api.ui.toast({ message: msg })
2122
2008
  dialog?.clear()
2123
2009
  }}
@@ -2131,7 +2017,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
2131
2017
  description: "Delete all sub-agent records for the current session (cannot be undone)",
2132
2018
  slash: { name: "subagent-clear-entries" },
2133
2019
  onSelect: (dialog) => {
2134
- const t = (k: string) => I18N[signals.lang()][k] ?? k
2020
+ const t = createT(() => signals.lang())
2135
2021
  const sid = signals.sessionId
2136
2022
  const sessionObj = api.state.session.get(sid)
2137
2023
  const parentID = (sessionObj as any)?.parentID as string | undefined
@@ -2142,7 +2028,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
2142
2028
  for (const [, e] of cached) { if (e.status === "running") runningCount++ }
2143
2029
  }
2144
2030
  const msg = runningCount > 0
2145
- ? t("clear.prompt_running").replace("{n}", String(runningCount))
2031
+ ? t("clear.prompt_running", { n: runningCount })
2146
2032
  : t("clear.prompt")
2147
2033
  dialog?.replace(() => (
2148
2034
  <api.ui.DialogConfirm
@@ -2175,7 +2061,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
2175
2061
  api.kv.set(`${KV_PREFIX}.session_data`, JSON.stringify(data))
2176
2062
  globalEntryCache.delete(sid)
2177
2063
  setClearTick((v) => v + 1)
2178
- const msg = t("clear.done").replace("{n}", String(count))
2064
+ const msg = t("clear.done", { n: count })
2179
2065
  api.ui.toast({ message: msg })
2180
2066
  } catch {}
2181
2067
  dialog?.clear()