@steerable/agent-shell-web 0.6.53 → 0.6.56

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 (100) hide show
  1. package/README.md +3 -4
  2. package/package.json +4 -4
  3. package/src/AppShell.tsx +3 -3
  4. package/src/brand.ts +1 -1
  5. package/src/components/AgentSidebar.tsx +64 -56
  6. package/src/components/ChatHeader.tsx +41 -25
  7. package/src/components/ConfirmDialog.tsx +4 -3
  8. package/src/components/CreateProjectModal.tsx +31 -21
  9. package/src/components/LocalLlmSettingsModal.tsx +4 -3
  10. package/src/components/SidebarRelease.tsx +6 -5
  11. package/src/components/TerminalPanel.tsx +6 -5
  12. package/src/components/TerminalView.tsx +15 -10
  13. package/src/components/chat/ApprovalModal.tsx +40 -26
  14. package/src/components/chat/ApprovalPromptProvider.tsx +5 -5
  15. package/src/components/chat/AskUserPromptProvider.tsx +4 -4
  16. package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
  17. package/src/components/chat/AssistantMessage.tsx +18 -13
  18. package/src/components/chat/ChatInput.tsx +56 -40
  19. package/src/components/chat/ChatProjectBadge.tsx +27 -22
  20. package/src/components/chat/EmptyChatHero.tsx +2 -1
  21. package/src/components/chat/ExecPolicyPicker.tsx +12 -11
  22. package/src/components/chat/ExecutedActionsCard.tsx +45 -33
  23. package/src/components/chat/FilePathCode.tsx +8 -3
  24. package/src/components/chat/InterruptedTurnCard.tsx +4 -3
  25. package/src/components/chat/LocalChatPanel.tsx +2 -1
  26. package/src/components/chat/Markdown.tsx +2 -1
  27. package/src/components/chat/MessageList.tsx +3 -2
  28. package/src/components/chat/ModelPicker.tsx +25 -16
  29. package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
  30. package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
  31. package/src/components/chat/PlanTodoList.tsx +2 -1
  32. package/src/components/chat/ProjectTrustBanner.tsx +19 -5
  33. package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
  34. package/src/components/chat/SessionTodoList.tsx +8 -7
  35. package/src/components/chat/SessionTreeModal.tsx +10 -9
  36. package/src/components/chat/StreamingStatus.tsx +4 -3
  37. package/src/components/chat/SuggestedReplies.tsx +3 -2
  38. package/src/components/chat/TaskOutcomeCards.tsx +8 -5
  39. package/src/components/chat/TaskPanelModal.tsx +33 -23
  40. package/src/components/chat/TaskProcessPanel.tsx +16 -15
  41. package/src/components/chat/TurnFilesCard.tsx +14 -13
  42. package/src/components/chat/UserMessage.tsx +12 -11
  43. package/src/components/chat/executed-actions-model.ts +47 -35
  44. package/src/components/chat/orchestration-flow-model.ts +16 -5
  45. package/src/components/chat/process-status.ts +8 -7
  46. package/src/components/chat/todo-list-model.ts +5 -4
  47. package/src/components/chat/turn-files.ts +8 -8
  48. package/src/components/chat/useChatTasks.ts +3 -3
  49. package/src/components/settings/AgentCapabilityFields.tsx +26 -23
  50. package/src/components/settings/AgentsSettingsPanel.tsx +33 -28
  51. package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
  52. package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
  53. package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
  54. package/src/components/settings/InsightsSettingsPanel.tsx +58 -43
  55. package/src/components/settings/LlmSettingsPanel.tsx +107 -86
  56. package/src/components/settings/McpSettingsPanel.tsx +72 -46
  57. package/src/components/settings/ModelIdCombobox.tsx +2 -1
  58. package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
  59. package/src/components/settings/PortableSettingsPanel.tsx +83 -55
  60. package/src/components/settings/PythonRunnerSettingsPanel.tsx +26 -23
  61. package/src/components/settings/SecuritySettingsPanel.tsx +58 -34
  62. package/src/components/settings/SettingsNavMenu.tsx +3 -2
  63. package/src/components/settings/SettingsSaveButton.tsx +2 -1
  64. package/src/components/settings/SkillsSettingsPanel.tsx +43 -38
  65. package/src/components/settings/TelemetrySettingsPanel.tsx +20 -17
  66. package/src/components/settings/UsagePanel.tsx +22 -23
  67. package/src/components/settings/WebSearchSettingsPanel.tsx +28 -19
  68. package/src/components/settings/llm-vendors.ts +12 -7
  69. package/src/components/settings/model-capabilities.ts +16 -15
  70. package/src/components/settings/preset-choice-model.ts +6 -3
  71. package/src/fixtures/browser-dev-data.json +24 -24
  72. package/src/hooks/useChatsAndAgents.ts +9 -9
  73. package/src/hooks/useProjects.ts +3 -3
  74. package/src/i18n/agent-label.ts +15 -0
  75. package/src/i18n/index.tsx +68 -0
  76. package/src/i18n/runtime.ts +117 -0
  77. package/src/i18n/translate.ts +65 -0
  78. package/src/layouts/AgentLayout.tsx +13 -12
  79. package/src/lib/app-release.ts +16 -11
  80. package/src/lib/attachments.ts +18 -9
  81. package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +42 -42
  82. package/src/lib/chat-title.ts +3 -2
  83. package/src/lib/chat-transport.ts +16 -12
  84. package/src/lib/date-groups.ts +23 -15
  85. package/src/lib/host-bridge.ts +371 -4
  86. package/src/lib/host-paste.ts +1 -1
  87. package/src/lib/http-bridge.ts +12 -9
  88. package/src/lib/insights.ts +2 -2
  89. package/src/lib/local-api.ts +5 -5
  90. package/src/lib/portable.ts +11 -10
  91. package/src/lib/show-thinking-content.ts +15 -3
  92. package/src/lib/slash-sources.ts +3 -3
  93. package/src/lib/tauri-bridge.ts +1 -1
  94. package/src/main.tsx +27 -4
  95. package/src/pages/AgentPage.tsx +39 -34
  96. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  97. package/src/pages/SettingsPage.tsx +51 -45
  98. package/src/vite-env.d.ts +3 -0
  99. package/vite.base.js +6 -2
  100. package/src/lib/electron-bridge.ts +0 -390
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuChartBar, LuRefreshCw } from 'react-icons/lu';
3
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
3
+ import { t } from '@/i18n';
4
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
4
5
 
5
6
  /**
6
7
  * UsagePanel — W6-9 用量与成本归因的桌面面板。
@@ -40,11 +41,7 @@ interface UsageSummaryWire {
40
41
  };
41
42
  }
42
43
 
43
- const WINDOW_OPTIONS = [
44
- { days: 7, label: '近 7 天' },
45
- { days: 30, label: '近 30 天' },
46
- { days: 90, label: '近 90 天' },
47
- ] as const;
44
+ const WINDOW_OPTIONS = [{ days: 7 }, { days: 30 }, { days: 90 }] as const;
48
45
 
49
46
  function formatTokens(n: number): string {
50
47
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
@@ -69,11 +66,11 @@ export function UsagePanel() {
69
66
  const [error, setError] = useState<string | null>(null);
70
67
 
71
68
  const fetchSummary = useCallback(async (windowDays: number) => {
72
- if (!isElectron()) return;
69
+ if (!hasHostBridge()) return;
73
70
  setLoading(true);
74
71
  setError(null);
75
72
  try {
76
- const res = await getElectronBridge()!.localBackend.request<UsageSummaryWire>({
73
+ const res = await getHostBridge()!.localBackend.request<UsageSummaryWire>({
77
74
  method: 'GET',
78
75
  path: `/api/v2/usage/summary?days=${windowDays}`,
79
76
  });
@@ -95,7 +92,7 @@ export function UsagePanel() {
95
92
  <div className="flex items-center justify-between">
96
93
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
97
94
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
98
- 用量与成本
95
+ {t('Usage and cost')}
99
96
  </h4>
100
97
  <div className="flex items-center gap-1.5">
101
98
  {WINDOW_OPTIONS.map((opt) => (
@@ -109,7 +106,7 @@ export function UsagePanel() {
109
106
  : 'bg-agent-canvas text-agent-muted-foreground border border-agent-border hover:bg-agent-muted/40'
110
107
  }`}
111
108
  >
112
- {opt.label}
109
+ {t('Last {days} days', { days: opt.days })}
113
110
  </button>
114
111
  ))}
115
112
  <button
@@ -117,7 +114,7 @@ export function UsagePanel() {
117
114
  onClick={() => void fetchSummary(days)}
118
115
  disabled={loading}
119
116
  className="h-6 w-6 rounded-full border border-agent-border bg-agent-canvas text-agent-muted-foreground hover:bg-agent-muted/40 flex items-center justify-center"
120
- title="刷新"
117
+ title={t('Refresh')}
121
118
  >
122
119
  <LuRefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} />
123
120
  </button>
@@ -127,30 +124,30 @@ export function UsagePanel() {
127
124
  {loading && !summary ? (
128
125
  <div className="flex items-center gap-2 py-4 text-xs text-agent-muted-foreground">
129
126
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
130
- 读取用量...
127
+ {t('Loading usage...')}
131
128
  </div>
132
129
  ) : !summary || summary.byModel.length === 0 ? (
133
130
  <p className="text-[11px] text-agent-muted-foreground py-2">
134
- 该时间窗内暂无用量记录。完成一轮对话后,这里会按模型聚合 token 与成本。
131
+ {t('No usage in this time window yet. After a conversation turn finishes, tokens and cost are grouped by model here.')}
135
132
  </p>
136
133
  ) : (
137
134
  <>
138
135
  {/* 总计卡片 */}
139
136
  <div className="grid grid-cols-4 gap-2">
140
137
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
141
- <div className="text-[10px] text-agent-muted-foreground">总轮次</div>
138
+ <div className="text-[10px] text-agent-muted-foreground">{t('Total turns')}</div>
142
139
  <div className="text-xs font-semibold text-agent-foreground">{summary.totals.turns}</div>
143
140
  </div>
144
141
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
145
- <div className="text-[10px] text-agent-muted-foreground">总 Token</div>
142
+ <div className="text-[10px] text-agent-muted-foreground">{t('Total tokens')}</div>
146
143
  <div className="text-xs font-semibold text-agent-foreground">{formatTokens(summary.totals.totalTokens)}</div>
147
144
  </div>
148
145
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
149
- <div className="text-[10px] text-agent-muted-foreground">缓存命中</div>
146
+ <div className="text-[10px] text-agent-muted-foreground">{t('Cache hits')}</div>
150
147
  <div className="text-xs font-semibold text-agent-foreground">{formatTokens(summary.totals.cachedPromptTokens)}</div>
151
148
  </div>
152
149
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
153
- <div className="text-[10px] text-agent-muted-foreground">估算成本</div>
150
+ <div className="text-[10px] text-agent-muted-foreground">{t('Estimated cost')}</div>
154
151
  <div className="text-xs font-semibold text-agent-foreground">{formatCost(summary.totals.costUsd)}</div>
155
152
  </div>
156
153
  </div>
@@ -160,11 +157,11 @@ export function UsagePanel() {
160
157
  <table className="w-full text-[11px]">
161
158
  <thead>
162
159
  <tr className="bg-agent-muted/40 text-agent-muted-foreground">
163
- <th className="text-left font-medium px-2.5 py-1.5">模型</th>
164
- <th className="text-right font-medium px-2.5 py-1.5">轮次</th>
165
- <th className="text-right font-medium px-2.5 py-1.5">输入</th>
166
- <th className="text-right font-medium px-2.5 py-1.5">输出</th>
167
- <th className="text-right font-medium px-2.5 py-1.5">成本</th>
160
+ <th className="text-left font-medium px-2.5 py-1.5">{t('Model')}</th>
161
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Turns')}</th>
162
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Input')}</th>
163
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Output')}</th>
164
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Cost')}</th>
168
165
  </tr>
169
166
  </thead>
170
167
  <tbody>
@@ -184,7 +181,9 @@ export function UsagePanel() {
184
181
  </div>
185
182
 
186
183
  <p className="text-[10px] text-agent-muted-foreground">
187
- 成本按 framework 单价表估算后按约 1 美元 = 7.2 元换算为人民币,只统计有单价的模型;"—" 表示该模型无单价(本地/未知)。缓存命中为命中 prompt 缓存的 token 数。
184
+ {t(
185
+ 'Cost is estimated from the framework price table and converted to RMB at about 1 USD = 7.2 CNY. Only models with a price are counted; "—" means the model has no price (local or unknown). Cache hits are the tokens served from the prompt cache.',
186
+ )}
188
187
  </p>
189
188
  </>
190
189
  )}
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
3
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
3
+ import { t } from '@/i18n';
4
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
4
5
 
5
6
  /**
6
7
  * Search backend settings. `ddg` registers `web_search` with no key.
@@ -32,11 +33,11 @@ export function WebSearchSettingsPanel() {
32
33
  const [error, setError] = useState<string | null>(null);
33
34
 
34
35
  const fetchSettings = useCallback(async () => {
35
- if (!isElectron()) return;
36
+ if (!hasHostBridge()) return;
36
37
  setLoading(true);
37
38
  setError(null);
38
39
  try {
39
- const res = await getElectronBridge()!.localBackend.request<WebSearchSettingsWire | null>({
40
+ const res = await getHostBridge()!.localBackend.request<WebSearchSettingsWire | null>({
40
41
  method: 'GET',
41
42
  path: '/api/v2/local-settings/web-search',
42
43
  });
@@ -54,13 +55,13 @@ export function WebSearchSettingsPanel() {
54
55
  }, [fetchSettings]);
55
56
 
56
57
  const handleSave = async () => {
57
- if (!isElectron()) return;
58
+ if (!hasHostBridge()) return;
58
59
  setSaving(true);
59
60
  setError(null);
60
61
  setStatus(null);
61
62
  try {
62
63
  const posted = provider;
63
- const saved = await getElectronBridge()!.localBackend.request<WebSearchSettingsWire>({
64
+ const saved = await getHostBridge()!.localBackend.request<WebSearchSettingsWire>({
64
65
  method: 'POST',
65
66
  path: '/api/v2/local-settings/web-search',
66
67
  body: { provider: posted, apiKey: apiKey.trim() },
@@ -71,11 +72,15 @@ export function WebSearchSettingsPanel() {
71
72
  setProvider(nextProvider);
72
73
  setApiKey(saved?.apiKey ?? '');
73
74
  if (nextProvider === 'ddg') {
74
- setStatus('已保存 — 重启应用后 sidecar 会注册免费搜索(DuckDuckGo)');
75
+ setStatus(t('Saved. After you restart the app, the sidecar registers free search (DuckDuckGo).'));
75
76
  } else if (saved?.apiKey) {
76
- setStatus('已保存 — 重启应用后 sidecar 会注册 web_search');
77
+ setStatus(t('Saved. After you restart the app, the sidecar registers web_search.'));
77
78
  } else {
78
- setStatus('已保存 — 未配置则模型没有搜索工具(OpenAI 可用聊天凭证走托管搜索)');
79
+ setStatus(
80
+ t(
81
+ 'Saved. Without a key the model has no search tool (OpenAI can use the chat credentials for hosted search).',
82
+ ),
83
+ );
79
84
  }
80
85
  } catch (err) {
81
86
  setError(err instanceof Error ? err.message : String(err));
@@ -89,25 +94,25 @@ export function WebSearchSettingsPanel() {
89
94
  <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
90
95
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
91
96
  <LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
92
- 网络搜索
97
+ {t('Web search')}
93
98
  </h4>
94
99
 
95
100
  {loading ? (
96
101
  <div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
97
102
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
98
- 读取搜索设置...
103
+ {t('Loading search settings...')}
99
104
  </div>
100
105
  ) : (
101
106
  <>
102
107
  <div className="space-y-1.5">
103
108
  <label className="text-[11px] font-medium text-agent-muted-foreground">
104
- 搜索后端
109
+ {t('Search backend')}
105
110
  </label>
106
111
  <div className="flex gap-2">
107
112
  {(
108
113
  [
109
- { value: 'ddg', label: '免费', hint: 'DuckDuckGo,无需 Key' },
110
- { value: 'tavily', label: 'Tavily', hint: '需 API Key,结果更稳' },
114
+ { value: 'ddg', label: 'Free', hint: 'DuckDuckGo, no key needed' },
115
+ { value: 'tavily', label: 'Tavily', hint: 'Needs an API key, more reliable results' },
111
116
  ] as const
112
117
  ).map((opt) => (
113
118
  <button
@@ -121,8 +126,8 @@ export function WebSearchSettingsPanel() {
121
126
  : 'border-agent-border bg-agent-canvas hover:bg-agent-muted/40'
122
127
  }`}
123
128
  >
124
- <div className="text-xs font-medium text-agent-foreground">{opt.label}</div>
125
- <div className="text-[10px] text-agent-muted-foreground">{opt.hint}</div>
129
+ <div className="text-xs font-medium text-agent-foreground">{t(opt.label)}</div>
130
+ <div className="text-[10px] text-agent-muted-foreground">{t(opt.hint)}</div>
126
131
  </button>
127
132
  ))}
128
133
  </div>
@@ -137,7 +142,7 @@ export function WebSearchSettingsPanel() {
137
142
  type="password"
138
143
  value={apiKey}
139
144
  onChange={(e) => setApiKey(e.target.value)}
140
- placeholder="tvly-...(留空 = 不注册 Tavily)"
145
+ placeholder={t('tvly-... (leave blank = do not register Tavily)')}
141
146
  className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 font-mono text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
142
147
  autoComplete="off"
143
148
  />
@@ -147,8 +152,12 @@ export function WebSearchSettingsPanel() {
147
152
  <div className="flex items-center justify-between gap-2">
148
153
  <p className="text-[10px] text-agent-muted-foreground">
149
154
  {provider === 'ddg'
150
- ? '免费后端走 DuckDuckGo 公开搜索页,无需 Key。质量与稳定性不如 Tavily;国内网络可能需要系统代理。Harbor 评测仍关闭网络工具。'
151
- : 'GLM / OpenRouter / DeepSeek 需要这把钥;OpenAI(api.openai.com)可用聊天凭证走托管搜索,不必另配。Harbor 评测仍关闭网络工具。'}
155
+ ? t(
156
+ 'The free backend uses the public DuckDuckGo search page and needs no key. Quality and reliability are lower than Tavily, and networks in mainland China may need a system proxy. Harbor evaluations still turn off network tools.',
157
+ )
158
+ : t(
159
+ 'GLM / OpenRouter / DeepSeek need this key. OpenAI (api.openai.com) can use the chat credentials for hosted search with no extra setup. Harbor evaluations still turn off network tools.',
160
+ )}
152
161
  </p>
153
162
  <button
154
163
  type="button"
@@ -160,7 +169,7 @@ export function WebSearchSettingsPanel() {
160
169
  : 'bg-agent-foreground text-agent-canvas hover:opacity-90'
161
170
  }`}
162
171
  >
163
- {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : '保存'}
172
+ {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : t('Save')}
164
173
  </button>
165
174
  </div>
166
175
 
@@ -3,6 +3,7 @@
3
3
  * 常用置顶、中文标签、以及 sidecar 未就绪时的本地回退。
4
4
  */
5
5
 
6
+ import { t } from '@/i18n';
6
7
  import type {
7
8
  CatalogProviderDescriptor,
8
9
  GatewayModelEntry,
@@ -42,14 +43,14 @@ export const VENDOR_LABELS: Record<string, string> = {
42
43
  anthropic: 'Anthropic',
43
44
  google: 'Google Gemini',
44
45
  openrouter: 'OpenRouter',
45
- 'alibaba-cn': '阿里云百炼',
46
- 'moonshotai-cn': '月之暗面 Kimi',
47
- zhipuai: '智谱 GLM',
46
+ 'alibaba-cn': 'Alibaba Cloud Model Studio',
47
+ 'moonshotai-cn': 'Moonshot AI Kimi',
48
+ zhipuai: 'Zhipu GLM',
48
49
  groq: 'Groq',
49
50
  xai: 'xAI',
50
51
  lmstudio: 'LM Studio',
51
- ollama: 'Ollama (本地)',
52
- custom: '自定义 OpenAI 兼容',
52
+ ollama: 'Ollama (local)',
53
+ custom: 'Custom OpenAI-compatible',
53
54
  };
54
55
 
55
56
  /** sidecar 未就绪时仍能选常用服务商。 */
@@ -155,7 +156,8 @@ const LOCAL_VENDORS: VendorOption[] = [
155
156
  ];
156
157
 
157
158
  export function vendorLabel(id: string): string {
158
- return VENDOR_LABELS[id] ?? id;
159
+ const label = VENDOR_LABELS[id];
160
+ return label ? t(label) : id;
159
161
  }
160
162
 
161
163
  export function llmProviderFromWireKind(wireKind: string): LlmProvider {
@@ -190,7 +192,10 @@ export function mergeVendorOptions(catalog: CatalogProviderDescriptor[]): Vendor
190
192
  featured: featured.has(row.id),
191
193
  }));
192
194
  const seen = new Set(fromCatalog.map((v) => v.id));
193
- const extras = LOCAL_VENDORS.filter((v) => !seen.has(v.id));
195
+ const extras = LOCAL_VENDORS.filter((v) => !seen.has(v.id)).map((v) => ({
196
+ ...v,
197
+ label: vendorLabel(v.id),
198
+ }));
194
199
  const featuredOrder = [...FEATURED_VENDOR_IDS, 'ollama', 'custom'];
195
200
  const rank = (id: string) => {
196
201
  const idx = featuredOrder.indexOf(id);
@@ -1,3 +1,4 @@
1
+ import { t } from '@/i18n';
1
2
  import type { GatewayModelEntry } from '@/lib/local-api';
2
3
 
3
4
  /**
@@ -18,17 +19,17 @@ export interface ModelCapabilityChip {
18
19
  const MODALITY_ORDER = ['image', 'pdf', 'audio', 'video'] as const;
19
20
 
20
21
  const MODALITY_LABELS: Record<(typeof MODALITY_ORDER)[number], string> = {
21
- image: '图像',
22
+ image: 'Image',
22
23
  pdf: 'PDF',
23
- audio: '音频',
24
- video: '视频',
24
+ audio: 'Audio',
25
+ video: 'Video',
25
26
  };
26
27
 
27
28
  const MODALITY_TITLES: Record<(typeof MODALITY_ORDER)[number], string> = {
28
- image: '支持图像输入',
29
- pdf: '支持 PDF 输入',
30
- audio: '支持音频输入',
31
- video: '支持视频输入',
29
+ image: 'Supports image input',
30
+ pdf: 'Supports PDF input',
31
+ audio: 'Supports audio input',
32
+ video: 'Supports video input',
32
33
  };
33
34
 
34
35
  /** 上下文窗口:1_000_000 → 1M,131_072 → 131K。 */
@@ -45,8 +46,8 @@ export function formatContextWindow(window: number | null | undefined): string |
45
46
  function unknownChip(): ModelCapabilityChip {
46
47
  return {
47
48
  key: 'unknown',
48
- label: '未识别',
49
- title: '网关目录未匹配到该 id,思考档位与多模态未核实。',
49
+ label: t('Unrecognized'),
50
+ title: t('The gateway catalog has no match for this id. Thinking levels and multimodal input are unverified.'),
50
51
  kind: 'unknown',
51
52
  };
52
53
  }
@@ -68,8 +69,8 @@ export function modelCapabilityChips(
68
69
  const joined = levels.join(' / ');
69
70
  chips.push({
70
71
  key: 'reasoning',
71
- label: options?.detail ? `思考(${joined})` : '思考',
72
- title: `支持思考档位:${joined}`,
72
+ label: options?.detail ? t('Thinking ({levels})', { levels: joined }) : t('Thinking'),
73
+ title: t('Supported thinking levels: {levels}', { levels: joined }),
73
74
  kind: 'reasoning',
74
75
  });
75
76
  }
@@ -80,8 +81,8 @@ export function modelCapabilityChips(
80
81
  seen.add(modality);
81
82
  chips.push({
82
83
  key: modality,
83
- label: MODALITY_LABELS[modality],
84
- title: MODALITY_TITLES[modality],
84
+ label: t(MODALITY_LABELS[modality]),
85
+ title: t(MODALITY_TITLES[modality]),
85
86
  kind: 'modality',
86
87
  });
87
88
  }
@@ -92,7 +93,7 @@ export function modelCapabilityChips(
92
93
  chips.push({
93
94
  key: key,
94
95
  label: modality,
95
- title: `输入模态:${modality}`,
96
+ title: t('Input modality: {modality}', { modality }),
96
97
  kind: 'modality',
97
98
  });
98
99
  }
@@ -102,7 +103,7 @@ export function modelCapabilityChips(
102
103
  chips.push({
103
104
  key: 'window',
104
105
  label: windowLabel,
105
- title: `上下文窗口 ${entry.window} tokens`,
106
+ title: t('Context window {window} tokens', { window: entry.window ?? '' }),
106
107
  kind: 'window',
107
108
  });
108
109
  }
@@ -5,6 +5,7 @@
5
5
  * 载荷之间的转换,不硬编码任何厂商参数。
6
6
  */
7
7
 
8
+ import { t } from '@/i18n';
8
9
  import type {
9
10
  ProviderPresetDescriptor,
10
11
  ProviderPresetOverride,
@@ -28,12 +29,12 @@ export function overrideFromDescriptor(d: ProviderPresetDescriptor): ProviderPre
28
29
  /** 选择器选项标签:`host + 前缀*`,缺省键名退化为「未命名」。 */
29
30
  export function descriptorLabel(d: ProviderPresetDescriptor): string {
30
31
  const parts = [d.host, d.modelPrefix ? `${d.modelPrefix}*` : null].filter(Boolean);
31
- return parts.join(' + ') || '(未命名预制)';
32
+ return parts.join(' + ') || t('(Unnamed preset)');
32
33
  }
33
34
 
34
35
  /** 预制参数的一行摘要,用于生效预览。 */
35
36
  export function summarizePreset(p: ProviderPresetOverride | null): string {
36
- if (!p) return '不下发额外采样参数';
37
+ if (!p) return t('No extra sampling parameters are sent');
37
38
  const parts: string[] = [];
38
39
  if (p.temperature != null) parts.push(`temperature ${p.temperature}`);
39
40
  if (p.topP != null) parts.push(`top_p ${p.topP}`);
@@ -42,7 +43,9 @@ export function summarizePreset(p: ProviderPresetOverride | null): string {
42
43
  if (p.extraBody) {
43
44
  for (const [k, v] of Object.entries(p.extraBody)) parts.push(`${k} ${JSON.stringify(v)}`);
44
45
  }
45
- return parts.length > 0 ? parts.join(' · ') : '不下发额外采样参数(该厂商最优请求即不带采样字段)';
46
+ return parts.length > 0
47
+ ? parts.join(' · ')
48
+ : t('No extra sampling parameters are sent (the best request for this vendor omits sampling fields)');
46
49
  }
47
50
 
48
51
  export function overridesEqual(a: ProviderPresetOverride, b: ProviderPresetOverride): boolean {
@@ -6,11 +6,11 @@
6
6
  {
7
7
  "id": "local-assistant",
8
8
  "slug": "local-assistant",
9
- "name": "电脑操作员",
9
+ "name": "Computer operator",
10
10
  "icon": "Cpu",
11
11
  "color": "#4f46e5",
12
- "description": "默认本地助手,可调用 shell、文件与 MCP 工具。",
13
- "rolePrompt": "你是 **电脑操作员**,本地离线助手,回答时清晰、可执行。",
12
+ "description": "Default local assistant. It can use the shell, files, and MCP tools.",
13
+ "rolePrompt": "You are **Computer operator**, a local offline assistant. Answer clearly and in steps that can be carried out.",
14
14
  "forbiddenPrompt": null,
15
15
  "skillIds": [],
16
16
  "toolPolicy": {
@@ -27,11 +27,11 @@
27
27
  {
28
28
  "id": "data-analyst",
29
29
  "slug": "data-analyst",
30
- "name": "数据分析助手",
30
+ "name": "Data analyst",
31
31
  "icon": "Activity",
32
32
  "color": "#0ea5e9",
33
- "description": "处理本地 CSV 与日志文件:清洗、统计、出图与汇总报告。",
34
- "rolePrompt": "你是 **数据分析助手**,专注本地数据文件的清洗、统计与可视化。\n工作语言:中文。回答简洁、结论先行。\n\n工作纪律:\n1) 先探查文件结构再动手;\n2) 每一步都说明做了什么;\n3) 结果用表格或要点呈现。",
33
+ "description": "Works on local CSV and log files: cleaning, stats, charts, and a summary.",
34
+ "rolePrompt": "You are **Data analyst**. You clean, summarize, and chart local data files.\nBe concise and lead with the conclusion.\n\nWorking rules:\n1) Inspect the file structure before changing it;\n2) Say what each step did;\n3) Present results as a table or bullets.",
35
35
  "forbiddenPrompt": null,
36
36
  "skillIds": [],
37
37
  "toolPolicy": {
@@ -48,11 +48,11 @@
48
48
  {
49
49
  "id": "all-round-assistant",
50
50
  "slug": "all-round-assistant",
51
- "name": "智能助手",
51
+ "name": "Assistant",
52
52
  "icon": "🔮",
53
53
  "color": "#a855f7",
54
- "description": "全能智能体,自动加载全部已安装本地技能和工具,无需限定条件。",
55
- "rolePrompt": "你是 **智能助手**,一个具备高权限且集成了全部本地技能(Skills)的行动体。\n你具备完整的本地命令执行(local_exec_shell)、文件读写与数据分析能力。\n你的系统提示词中已经无条件加载了所有已安装的本地技能与工具。\n请根据用户的需求,直接、高效地调用最合适的本地工具或执行脚本来解决任务。",
54
+ "description": "General agent. It loads every installed local skill and tool, with no extra trigger.",
55
+ "rolePrompt": "You are **Assistant**, with full access to every installed local skill.\nYou can run local commands (local_exec_shell), read and write files, and analyze data.\nEvery installed local skill and tool is already in your system prompt.\nCall the most direct local tool or script for the task.",
56
56
  "forbiddenPrompt": null,
57
57
  "skillIds": [],
58
58
  "toolPolicy": {
@@ -72,7 +72,7 @@
72
72
  "id": "8286e3c8-2d83-409e-930a-3e99b2004d41",
73
73
  "projectId": null,
74
74
  "userId": "local",
75
- "title": "你好",
75
+ "title": "Hello",
76
76
  "agentId": "local-assistant",
77
77
  "createdAt": "2026-07-01T10:05:58.604Z",
78
78
  "updatedAt": "2026-07-01T10:06:06.736Z",
@@ -84,7 +84,7 @@
84
84
  "id": "8b13a92e-4b3e-4bac-885f-879ddd9e4835",
85
85
  "projectId": null,
86
86
  "userId": "local",
87
- "title": "启用 csv-data-tools 技能",
87
+ "title": "Enable the csv-data-tools skill",
88
88
  "agentId": "all-round-assistant",
89
89
  "createdAt": "2026-06-22T04:30:54.662Z",
90
90
  "updatedAt": "2026-06-22T04:32:12.657Z",
@@ -96,7 +96,7 @@
96
96
  "id": "d536659c-6492-424d-97e8-2f701a9ce5d8",
97
97
  "projectId": null,
98
98
  "userId": "local",
99
- "title": "新对话",
99
+ "title": "New chat",
100
100
  "agentId": "all-round-assistant",
101
101
  "createdAt": "2026-06-22T04:19:52.607Z",
102
102
  "updatedAt": "2026-06-22T04:20:00.151Z",
@@ -108,7 +108,7 @@
108
108
  "id": "4845b527-c9c9-467c-9d72-5fc5f8763aa5",
109
109
  "projectId": null,
110
110
  "userId": "local",
111
- "title": "新对话",
111
+ "title": "New chat",
112
112
  "agentId": "all-round-assistant",
113
113
  "createdAt": "2026-06-22T04:12:17.151Z",
114
114
  "updatedAt": "2026-06-22T04:12:22.038Z",
@@ -120,7 +120,7 @@
120
120
  "id": "405f5964-f01c-4ece-9c8f-02a73d539469",
121
121
  "projectId": null,
122
122
  "userId": "local",
123
- "title": "启用 csv-data-tools 技能",
123
+ "title": "Enable the csv-data-tools skill",
124
124
  "agentId": "all-round-assistant",
125
125
  "createdAt": "2026-06-22T04:06:21.745Z",
126
126
  "updatedAt": "2026-06-22T04:11:42.782Z",
@@ -132,7 +132,7 @@
132
132
  "id": "19a1d8e7-7e9c-425b-8509-0fe9dd292971",
133
133
  "projectId": null,
134
134
  "userId": "local",
135
- "title": "启用 csv-data-tools 技能",
135
+ "title": "Enable the csv-data-tools skill",
136
136
  "agentId": "all-round-assistant",
137
137
  "createdAt": "2026-06-22T04:01:21.158Z",
138
138
  "updatedAt": "2026-06-22T04:05:08.801Z",
@@ -144,7 +144,7 @@
144
144
  "id": "492c1a80-85b2-486a-b365-3d668c9ad7e6",
145
145
  "projectId": null,
146
146
  "userId": "local",
147
- "title": "新对话",
147
+ "title": "New chat",
148
148
  "agentId": "all-round-assistant",
149
149
  "createdAt": "2026-06-22T03:44:47.381Z",
150
150
  "updatedAt": "2026-06-22T03:44:54.479Z",
@@ -171,7 +171,7 @@
171
171
  "id": "ea36a5de-e2e0-428b-b882-f5e67ca326b0",
172
172
  "chatId": "8286e3c8-2d83-409e-930a-3e99b2004d41",
173
173
  "role": "assistant",
174
- "content": "你好!我是**本地智能助手**,运行在你的本地机器上。\n\n我可以帮你做这些事情:\n\n**💻 本地终端操作**\n- 执行 shell 命令、读写文件\n- 搜索代码库、批量替换\n\n**📊 数据处理**\n- 清洗 CSV / 日志文件\n- 生成统计汇总\n\n直接输入需求即可开始。",
174
+ "content": "Hello. I am the **local assistant**, running on this computer.\n\nI can help with:\n\n**Local terminal**\n- Run shell commands and read or write files\n- Search a codebase and replace in bulk\n\n**Data**\n- Clean CSV and log files\n- Build a summary\n\nType what you want done.",
175
175
  "createdAt": "2026-07-01T10:06:06.733Z",
176
176
  "messageMetadata": "{\"traceId\":\"tr_269aa1b0f5244962b3279304d6f26323\",\"completionStatus\":\"completed\",\"trace\":{\"traceId\":\"tr_269aa1b0f5244962b3279304d6f26323\",\"sessionId\":\"8286e3c8-2d83-409e-930a-3e99b2004d41\",\"chatId\":\"8286e3c8-2d83-409e-930a-3e99b2004d41\",\"startedAtMs\":1782900364396,\"eventCount\":3,\"spanCount\":1,\"events\":[{\"name\":\"stream_start\",\"timestampMs\":1782900364396,\"data\":{\"path\":\"/api/v2/chats/8286e3c8-2d83-409e-930a-3e99b2004d41/run\",\"method\":\"POST\"}},{\"name\":\"completion_decision\",\"timestampMs\":1782900366733,\"data\":{\"status\":\"completed\",\"reason\":\"assistant produced final response with no pending tools\",\"confidence\":0.85,\"evidence\":[{\"textLength\":218,\"toolCalls\":[],\"toolErrorCount\":0}]}},{\"name\":\"stream_complete\",\"timestampMs\":1782900366733,\"data\":{\"usedTool\":false,\"completionStatus\":\"completed\",\"assistantLength\":218}}],\"spans\":[{\"name\":\"llm_call\",\"stepId\":\"step_1\",\"startMs\":1782900364396,\"endMs\":1782900366733,\"status\":\"ok\",\"data\":{\"turn\":0,\"model\":\"default\",\"tokensUsed\":{\"prompt\":8596,\"completion\":130,\"total\":8726},\"toolCalls\":0},\"durationMs\":2337}]},\"executedActions\":[]}"
177
177
  },
@@ -179,7 +179,7 @@
179
179
  "id": "ab1387a6-5c43-4d4b-986d-073d98b3f5e2",
180
180
  "chatId": "8286e3c8-2d83-409e-930a-3e99b2004d41",
181
181
  "role": "user",
182
- "content": "你好",
182
+ "content": "Hello",
183
183
  "createdAt": "2026-07-01T10:06:04.392Z",
184
184
  "messageMetadata": null
185
185
  }
@@ -189,7 +189,7 @@
189
189
  "id": "6599eb84-515c-4f58-bec8-0f37fcfbb234",
190
190
  "chatId": "8b13a92e-4b3e-4bac-885f-879ddd9e4835",
191
191
  "role": "assistant",
192
- "content": "好的,我先查看 `csv-data-tools` 技能的可用模块。\n\n**`csv-data-tools`** 技能已就绪。以下是它的能力概览:\n\n---\n\n## 📦 csv-data-tools\n\n- `csv_stats` — 列级统计(行数、缺失、分布)\n- `csv_filter` — 条件过滤与导出\n- `csv_plot` — 快速出图\n\n告诉我文件路径即可开始。",
192
+ "content": "I'll look at the modules in `csv-data-tools`.\n\n**`csv-data-tools`** is ready:\n\n---\n\n## csv-data-tools\n\n- `csv_stats` — column stats (rows, missing values, distribution)\n- `csv_filter` — filter and export\n- `csv_plot` — a quick chart\n\nSend a file path to start.",
193
193
  "createdAt": "2026-06-22T04:32:11.576Z",
194
194
  "messageMetadata": null
195
195
  },
@@ -227,7 +227,7 @@
227
227
  "id": "c8dc9e96-642b-4232-8c3c-c8abc32a615c",
228
228
  "chatId": "405f5964-f01c-4ece-9c8f-02a73d539469",
229
229
  "role": "assistant",
230
- "content": "我来查看当前可用的 MCP 工具,找到 `csv-data-tools` 的相关信息。\n\n看起来 `csv-data-tools` 没有作为独立的 MCP 服务器注册。换个方式,先扫描本地脚本和可执行工具。\n\n结论:该技能当前未安装,可通过技能设置页导入。",
230
+ "content": "I'll check the MCP tools for `csv-data-tools`.\n\nIt is not registered as its own MCP server. I'll scan local scripts and executables instead.\n\nIt is not installed. Import it from the skills settings.",
231
231
  "createdAt": "2026-06-22T04:11:42.077Z",
232
232
  "messageMetadata": null
233
233
  },
@@ -245,7 +245,7 @@
245
245
  "id": "cfb0f03c-7faa-4594-b29f-f4c11fb09105",
246
246
  "chatId": "19a1d8e7-7e9c-425b-8509-0fe9dd292971",
247
247
  "role": "assistant",
248
- "content": "本地模型调用失败:无法连接 OpenAI 兼容网关 (https://api.deepseek.com),请检查 baseUrl / 网络。原始错误: fetch failed",
248
+ "content": "The local model call failed: could not reach the OpenAI-compatible gateway (https://api.deepseek.com). Check baseUrl and the network. Original error: fetch failed",
249
249
  "createdAt": "2026-06-22T04:05:06.751Z",
250
250
  "messageMetadata": null
251
251
  },
@@ -273,7 +273,7 @@
273
273
  "id": "c5d6136b-eaa4-4cc7-878b-450099b7d226",
274
274
  "chatId": "4e997d62-20ef-4937-a866-27778dee3a17",
275
275
  "role": "assistant",
276
- "content": "我来查看当前可用的 MCP 工具,找到 `csv-data-tools` 的相关信息。\n\n看起来 `csv-data-tools` 没有作为独立的 MCP 服务器注册。换个方式,先扫描本地脚本和可执行工具。\n\n结论:该技能当前未安装,可通过技能设置页导入。",
276
+ "content": "I'll check the MCP tools for `csv-data-tools`.\n\nIt is not registered as its own MCP server. I'll scan local scripts and executables instead.\n\nIt is not installed. Import it from the skills settings.",
277
277
  "createdAt": "2026-06-22T03:37:37.601Z",
278
278
  "messageMetadata": null
279
279
  },
@@ -289,7 +289,7 @@
289
289
  "id": "fa89030e-5db1-4d04-9320-e796baeb89e2",
290
290
  "chatId": "4e997d62-20ef-4937-a866-27778dee3a17",
291
291
  "role": "assistant",
292
- "content": "你好!我是**本地智能助手**,运行在你的本地机器上。\n\n我可以帮你做这些事情:\n\n**💻 本地终端操作**\n- 执行 shell 命令、读写文件\n- 搜索代码库、批量替换\n\n**📊 数据处理**\n- 清洗 CSV / 日志文件\n- 生成统计汇总\n\n直接输入需求即可开始。",
292
+ "content": "Hello. I am the **local assistant**, running on this computer.\n\nI can help with:\n\n**Local terminal**\n- Run shell commands and read or write files\n- Search a codebase and replace in bulk\n\n**Data**\n- Clean CSV and log files\n- Build a summary\n\nType what you want done.",
293
293
  "createdAt": "2026-06-22T03:34:48.778Z",
294
294
  "messageMetadata": "{\"traceId\":\"tr_f53c2142bbd347b5888f9d50c5444478\",\"completionStatus\":\"completed\",\"trace\":{\"traceId\":\"tr_f53c2142bbd347b5888f9d50c5444478\",\"sessionId\":\"4e997d62-20ef-4937-a866-27778dee3a17\",\"chatId\":\"4e997d62-20ef-4937-a866-27778dee3a17\",\"startedAtMs\":1782099286405,\"eventCount\":3,\"spanCount\":1,\"events\":[{\"name\":\"stream_start\",\"timestampMs\":1782099286405,\"data\":{\"path\":\"/api/v2/chats/4e997d62-20ef-4937-a866-27778dee3a17/run\",\"method\":\"POST\"}},{\"name\":\"completion_decision\",\"timestampMs\":1782099288778,\"data\":{\"status\":\"completed\",\"reason\":\"assistant produced final response with no pending tools\",\"confidence\":0.85,\"evidence\":[{\"textLength\":224,\"toolCalls\":[],\"toolErrorCount\":0}]}},{\"name\":\"stream_complete\",\"timestampMs\":1782099288778,\"data\":{\"usedTool\":false,\"completionStatus\":\"completed\",\"assistantLength\":224}}],\"spans\":[{\"name\":\"llm_call\",\"stepId\":\"step_1\",\"startMs\":1782099286405,\"endMs\":1782099288777,\"status\":\"ok\",\"data\":{\"turn\":0,\"model\":\"default\",\"tokensUsed\":{\"prompt\":2234,\"completion\":130,\"total\":2364},\"toolCalls\":0},\"durationMs\":2372}]},\"executedActions\":[]}"
295
295
  },
@@ -309,7 +309,7 @@
309
309
  "baseUrl": "https://api.deepseek.com",
310
310
  "apiKey": "",
311
311
  "temperature": 0.3,
312
- "systemPrompt": "你是一款本地行动助手,优先给出可执行建议并在需要时调用本地工具。"
312
+ "systemPrompt": "You are a local assistant. Prefer advice that can be carried out, and call local tools when that helps."
313
313
  },
314
314
  "skills": []
315
315
  }