@steerable/agent-shell-web 0.6.52 → 0.6.55
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/package.json +4 -4
- package/src/brand.ts +1 -1
- package/src/components/AgentSidebar.tsx +60 -52
- package/src/components/ChatHeader.tsx +41 -25
- package/src/components/ConfirmDialog.tsx +4 -3
- package/src/components/CreateProjectModal.tsx +29 -19
- package/src/components/LocalLlmSettingsModal.tsx +4 -3
- package/src/components/SidebarRelease.tsx +2 -1
- package/src/components/TerminalPanel.tsx +3 -2
- package/src/components/TerminalView.tsx +12 -7
- package/src/components/chat/ApprovalModal.tsx +39 -25
- package/src/components/chat/ApprovalPromptProvider.tsx +1 -1
- package/src/components/chat/AskUserPromptProvider.tsx +1 -1
- package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
- package/src/components/chat/AssistantMessage.tsx +18 -13
- package/src/components/chat/ChatInput.tsx +138 -42
- package/src/components/chat/ChatProjectBadge.tsx +24 -19
- package/src/components/chat/EmptyChatHero.tsx +2 -1
- package/src/components/chat/ExecPolicyPicker.tsx +12 -11
- package/src/components/chat/ExecutedActionsCard.tsx +45 -33
- package/src/components/chat/FilePathCode.tsx +8 -3
- package/src/components/chat/InterruptedTurnCard.tsx +4 -3
- package/src/components/chat/LocalChatPanel.tsx +2 -1
- package/src/components/chat/Markdown.tsx +2 -1
- package/src/components/chat/MessageList.tsx +3 -2
- package/src/components/chat/ModelPicker.tsx +25 -16
- package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
- package/src/components/chat/PlanTodoList.tsx +2 -1
- package/src/components/chat/ProjectTrustBanner.tsx +19 -5
- package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
- package/src/components/chat/SessionTodoList.tsx +8 -7
- package/src/components/chat/SessionTreeModal.tsx +10 -9
- package/src/components/chat/StreamingStatus.tsx +4 -3
- package/src/components/chat/SuggestedReplies.tsx +3 -2
- package/src/components/chat/TaskOutcomeCards.tsx +8 -5
- package/src/components/chat/TaskPanelModal.tsx +31 -21
- package/src/components/chat/TaskProcessPanel.tsx +14 -13
- package/src/components/chat/TurnFilesCard.tsx +14 -13
- package/src/components/chat/UserMessage.tsx +12 -11
- package/src/components/chat/executed-actions-model.ts +47 -35
- package/src/components/chat/orchestration-flow-model.ts +16 -5
- package/src/components/chat/process-status.ts +8 -7
- package/src/components/chat/todo-list-model.ts +5 -4
- package/src/components/chat/turn-files.ts +8 -8
- package/src/components/settings/AgentCapabilityFields.tsx +24 -21
- package/src/components/settings/AgentsSettingsPanel.tsx +30 -25
- package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
- package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
- package/src/components/settings/InsightsSettingsPanel.tsx +43 -28
- package/src/components/settings/LlmSettingsPanel.tsx +104 -83
- package/src/components/settings/McpSettingsPanel.tsx +193 -71
- package/src/components/settings/ModelIdCombobox.tsx +2 -1
- package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
- package/src/components/settings/PortableSettingsPanel.tsx +83 -55
- package/src/components/settings/PythonRunnerSettingsPanel.tsx +22 -19
- package/src/components/settings/SecuritySettingsPanel.tsx +55 -31
- package/src/components/settings/SettingsNavMenu.tsx +3 -2
- package/src/components/settings/SettingsSaveButton.tsx +2 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +36 -31
- package/src/components/settings/TelemetrySettingsPanel.tsx +15 -12
- package/src/components/settings/UsagePanel.tsx +19 -20
- package/src/components/settings/WebSearchSettingsPanel.tsx +23 -14
- package/src/components/settings/llm-vendors.ts +12 -7
- package/src/components/settings/model-capabilities.ts +16 -15
- package/src/components/settings/preset-choice-model.ts +6 -3
- package/src/fixtures/browser-dev-data.json +24 -24
- package/src/hooks/useProjects.ts +1 -1
- package/src/i18n/agent-label.ts +15 -0
- package/src/i18n/index.tsx +68 -0
- package/src/i18n/runtime.ts +117 -0
- package/src/i18n/translate.ts +65 -0
- package/src/layouts/AgentLayout.tsx +7 -6
- package/src/lib/app-release.ts +15 -10
- package/src/lib/attachments.ts +24 -7
- package/src/lib/browser-dev-electron-mock.ts +34 -34
- package/src/lib/chat-title.ts +3 -2
- package/src/lib/chat-transport.ts +6 -2
- package/src/lib/date-groups.ts +23 -15
- package/src/lib/electron-bridge.ts +19 -6
- package/src/lib/host-file-drop.ts +68 -0
- package/src/lib/host-paste.ts +38 -9
- package/src/lib/http-bridge.ts +4 -1
- package/src/lib/portable.ts +8 -7
- package/src/lib/show-thinking-content.ts +15 -3
- package/src/lib/tauri-bridge.ts +5 -0
- package/src/main.tsx +25 -2
- package/src/pages/AgentPage.tsx +17 -12
- package/src/pages/ChatPanelPreviewPage.tsx +46 -44
- package/src/pages/SettingsPage.tsx +48 -42
- package/src/vite-env.d.ts +3 -0
- package/vite.base.js +6 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { t } from '@/i18n';
|
|
2
3
|
|
|
3
4
|
export interface SettingsNavItem {
|
|
4
5
|
id: string;
|
|
@@ -21,10 +22,10 @@ export function SettingsNavMenu({
|
|
|
21
22
|
<aside
|
|
22
23
|
className="sticky top-0 h-fit w-44 shrink-0 py-1 select-none"
|
|
23
24
|
data-testid="settings-side-nav"
|
|
24
|
-
aria-label=
|
|
25
|
+
aria-label={t('Settings section navigation')}
|
|
25
26
|
>
|
|
26
27
|
<div className="mb-2 px-2.5 text-[11px] font-semibold tracking-wider text-agent-muted-foreground uppercase">
|
|
27
|
-
|
|
28
|
+
{t('Settings navigation')}
|
|
28
29
|
</div>
|
|
29
30
|
<nav className="space-y-0.5">
|
|
30
31
|
{items.map((item) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LuCheck, LuLoaderCircle } from 'react-icons/lu';
|
|
2
|
+
import { t } from '@/i18n';
|
|
2
3
|
|
|
3
4
|
interface SettingsSaveButtonProps {
|
|
4
5
|
saving: boolean;
|
|
@@ -29,7 +30,7 @@ export function SettingsSaveButton({
|
|
|
29
30
|
) : savedOk ? (
|
|
30
31
|
<LuCheck className="h-3.5 w-3.5" />
|
|
31
32
|
) : null}
|
|
32
|
-
|
|
33
|
+
{t('Save')}
|
|
33
34
|
</button>
|
|
34
35
|
);
|
|
35
36
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
LuLoaderCircle,
|
|
6
6
|
LuTrash2,
|
|
7
7
|
} from 'react-icons/lu';
|
|
8
|
+
import { t } from '@/i18n';
|
|
8
9
|
import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
|
|
9
10
|
import { getPackHiddenSlashSkills } from '@/packs/registry';
|
|
10
11
|
|
|
@@ -68,7 +69,7 @@ export function SkillsSettingsPanel() {
|
|
|
68
69
|
});
|
|
69
70
|
setSkills(res.skills || []);
|
|
70
71
|
} catch (err) {
|
|
71
|
-
console.error('
|
|
72
|
+
console.error('Failed to load skill list:', err);
|
|
72
73
|
setError(err instanceof Error ? err.message : String(err));
|
|
73
74
|
} finally {
|
|
74
75
|
setSkillsLoading(false);
|
|
@@ -84,20 +85,20 @@ export function SkillsSettingsPanel() {
|
|
|
84
85
|
try {
|
|
85
86
|
const bridge = getElectronBridge();
|
|
86
87
|
if (bridge?.local?.selectDirectory) {
|
|
87
|
-
setImportStatus('
|
|
88
|
+
setImportStatus(t('Opening folder picker...'));
|
|
88
89
|
const result = await bridge.local.selectDirectory();
|
|
89
90
|
if (result && !result.canceled && result.filePaths.length > 0) {
|
|
90
91
|
const selectedPath = result.filePaths[0];
|
|
91
92
|
setImportPath(selectedPath);
|
|
92
|
-
setImportStatus(
|
|
93
|
+
setImportStatus(t('Selected path: {path}', { path: selectedPath }));
|
|
93
94
|
} else {
|
|
94
|
-
setImportStatus('
|
|
95
|
+
setImportStatus(t('Folder selection canceled'));
|
|
95
96
|
}
|
|
96
97
|
} else {
|
|
97
|
-
setError('
|
|
98
|
+
setError(t('This version of the app does not support the folder picker'));
|
|
98
99
|
}
|
|
99
100
|
} catch (err) {
|
|
100
|
-
console.error('
|
|
101
|
+
console.error('Failed to select folder:', err);
|
|
101
102
|
setError(err instanceof Error ? err.message : String(err));
|
|
102
103
|
}
|
|
103
104
|
};
|
|
@@ -107,7 +108,7 @@ export function SkillsSettingsPanel() {
|
|
|
107
108
|
if (!trimmed) return;
|
|
108
109
|
setImporting(true);
|
|
109
110
|
setError(null);
|
|
110
|
-
setImportStatus('
|
|
111
|
+
setImportStatus(t('Importing local skill...'));
|
|
111
112
|
try {
|
|
112
113
|
const res = await getElectronBridge()!.localBackend.request<{ success: boolean; name: string }>({
|
|
113
114
|
method: 'POST',
|
|
@@ -116,16 +117,16 @@ export function SkillsSettingsPanel() {
|
|
|
116
117
|
});
|
|
117
118
|
if (res.success) {
|
|
118
119
|
setImportPath('');
|
|
119
|
-
setImportStatus(
|
|
120
|
+
setImportStatus(t('Import succeeded. Added skill "{name}"', { name: res.name }));
|
|
120
121
|
await fetchSkills();
|
|
121
122
|
} else {
|
|
122
|
-
setError('
|
|
123
|
-
setImportStatus('
|
|
123
|
+
setError(t('Failed to import skill'));
|
|
124
|
+
setImportStatus(t('Import failed: could not copy the skill to the runtime directory'));
|
|
124
125
|
}
|
|
125
126
|
} catch (err) {
|
|
126
127
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
127
128
|
setError(errMsg);
|
|
128
|
-
setImportStatus(
|
|
129
|
+
setImportStatus(t('Import error: {error}', { error: errMsg }));
|
|
129
130
|
} finally {
|
|
130
131
|
setImporting(false);
|
|
131
132
|
}
|
|
@@ -143,7 +144,7 @@ export function SkillsSettingsPanel() {
|
|
|
143
144
|
if (res.success) {
|
|
144
145
|
await fetchSkills();
|
|
145
146
|
} else {
|
|
146
|
-
setError('
|
|
147
|
+
setError(t('Failed to delete skill'));
|
|
147
148
|
}
|
|
148
149
|
} catch (err) {
|
|
149
150
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -158,11 +159,15 @@ export function SkillsSettingsPanel() {
|
|
|
158
159
|
<div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
|
|
159
160
|
<h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
|
|
160
161
|
<LuBlocks className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
161
|
-
|
|
162
|
+
{t('Import local skill directory')}
|
|
162
163
|
</h4>
|
|
163
164
|
<p className="text-[11px] text-agent-muted-foreground">
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
{t('Skills saved in the current project (or working directory) under')}{' '}
|
|
166
|
+
<code>skills/{t('skill-name')}/</code>{' '}
|
|
167
|
+
{t('appear in the list below automatically. No import needed.')}{' '}
|
|
168
|
+
{t('You can also import other local directories here (any folder with')}{' '}
|
|
169
|
+
<code>SKILL.md</code> {t('inside, for example')} <code>my-team/sql-tools</code>
|
|
170
|
+
{t(').')}
|
|
166
171
|
</p>
|
|
167
172
|
<div className="flex gap-2">
|
|
168
173
|
<div className="relative flex-1 flex items-center">
|
|
@@ -170,7 +175,7 @@ export function SkillsSettingsPanel() {
|
|
|
170
175
|
type="text"
|
|
171
176
|
value={importPath}
|
|
172
177
|
onChange={(e) => setImportPath(e.target.value)}
|
|
173
|
-
placeholder=
|
|
178
|
+
placeholder={t('Enter a skill path, or click the button on the right to choose a folder')}
|
|
174
179
|
className="w-full pl-3 pr-8 h-8 text-xs bg-agent-canvas text-agent-foreground border border-agent-border rounded-agent-md focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
|
|
175
180
|
disabled={importing}
|
|
176
181
|
data-testid="skills-import-path"
|
|
@@ -180,7 +185,7 @@ export function SkillsSettingsPanel() {
|
|
|
180
185
|
onClick={handleBrowseFolder}
|
|
181
186
|
disabled={importing}
|
|
182
187
|
className="absolute right-2.5 text-agent-muted-foreground hover:text-agent-foreground transition-colors"
|
|
183
|
-
title=
|
|
188
|
+
title={t('Choose a local folder')}
|
|
184
189
|
data-testid="skills-import-browse"
|
|
185
190
|
>
|
|
186
191
|
<LuFolderOpen className="h-4 w-4" />
|
|
@@ -200,7 +205,7 @@ export function SkillsSettingsPanel() {
|
|
|
200
205
|
{importing ? (
|
|
201
206
|
<LuLoaderCircle className="h-3 w-3 animate-spin" />
|
|
202
207
|
) : (
|
|
203
|
-
'
|
|
208
|
+
t('Import')
|
|
204
209
|
)}
|
|
205
210
|
</button>
|
|
206
211
|
</div>
|
|
@@ -217,16 +222,16 @@ export function SkillsSettingsPanel() {
|
|
|
217
222
|
{/* Skills List */}
|
|
218
223
|
<div className="space-y-2">
|
|
219
224
|
<h4 className="text-xs font-semibold text-agent-muted-foreground uppercase tracking-wide">
|
|
220
|
-
|
|
225
|
+
{t('Loaded skills')}
|
|
221
226
|
</h4>
|
|
222
227
|
{skillsLoading ? (
|
|
223
228
|
<div className="flex items-center gap-2 py-4 text-xs text-agent-muted-foreground">
|
|
224
229
|
<LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
225
|
-
|
|
230
|
+
{t('Fetching loaded skills...')}
|
|
226
231
|
</div>
|
|
227
232
|
) : skills.length === 0 ? (
|
|
228
233
|
<div className="text-center py-6 text-xs text-agent-muted-foreground">
|
|
229
|
-
|
|
234
|
+
{t('No loaded skills yet')}
|
|
230
235
|
</div>
|
|
231
236
|
) : (
|
|
232
237
|
<div className="divide-y divide-agent-border/40 pr-1">
|
|
@@ -257,38 +262,38 @@ export function SkillsSettingsPanel() {
|
|
|
257
262
|
)}
|
|
258
263
|
{isBuiltin && (
|
|
259
264
|
<span className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left">
|
|
260
|
-
|
|
265
|
+
{t('Built-in')}
|
|
261
266
|
</span>
|
|
262
267
|
)}
|
|
263
268
|
{isWorkspace && (
|
|
264
269
|
<span
|
|
265
270
|
className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
|
|
266
|
-
title=
|
|
271
|
+
title={t('From the skills/ folder of the project or working directory. Anything saved there appears in this list.')}
|
|
267
272
|
>
|
|
268
|
-
|
|
273
|
+
{t('Workspace')}
|
|
269
274
|
</span>
|
|
270
275
|
)}
|
|
271
276
|
{skill.layer === 'eager' ? (
|
|
272
277
|
<span
|
|
273
278
|
className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
|
|
274
|
-
title=
|
|
279
|
+
title={t('Always-on layer: the body is always injected into the system prompt')}
|
|
275
280
|
>
|
|
276
|
-
|
|
281
|
+
{t('Always on')}
|
|
277
282
|
</span>
|
|
278
283
|
) : (
|
|
279
284
|
<span
|
|
280
285
|
className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
|
|
281
|
-
title=
|
|
286
|
+
title={t('On-demand layer: only listed in the catalog. The model loads it through the skill tool when needed.')}
|
|
282
287
|
>
|
|
283
|
-
|
|
288
|
+
{t('On demand')}
|
|
284
289
|
</span>
|
|
285
290
|
)}
|
|
286
291
|
{skill.modelInvocable === false && (
|
|
287
292
|
<span
|
|
288
293
|
className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
|
|
289
|
-
title=
|
|
294
|
+
title={t('disable-model-invocation: the model cannot call it. Trigger it manually with /name.')}
|
|
290
295
|
>
|
|
291
|
-
|
|
296
|
+
{t('Manual only')}
|
|
292
297
|
</span>
|
|
293
298
|
)}
|
|
294
299
|
</div>
|
|
@@ -304,7 +309,7 @@ export function SkillsSettingsPanel() {
|
|
|
304
309
|
onClick={() => handleDeleteSkill(skill.name)}
|
|
305
310
|
disabled={deletingName === skill.name}
|
|
306
311
|
className="text-agent-muted-foreground hover:text-agent-destructive p-1 rounded-full hover:bg-agent-muted transition-colors"
|
|
307
|
-
title=
|
|
312
|
+
title={t('Uninstall this skill')}
|
|
308
313
|
data-testid={`skill-uninstall-${skill.name}`}
|
|
309
314
|
>
|
|
310
315
|
{deletingName === skill.name ? (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { LuLoaderCircle, LuActivity } from 'react-icons/lu';
|
|
3
|
+
import { t } from '@/i18n';
|
|
3
4
|
import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -70,8 +71,10 @@ export function TelemetrySettingsPanel() {
|
|
|
70
71
|
setEndpoint(saved.endpoint ?? '');
|
|
71
72
|
setStatus(
|
|
72
73
|
saved.endpoint
|
|
73
|
-
?
|
|
74
|
-
|
|
74
|
+
? t('Saved. Telemetry is on ({mode})', {
|
|
75
|
+
mode: saved.privacyMode === 'full' ? t('Full (redacted)') : t('Metadata only'),
|
|
76
|
+
})
|
|
77
|
+
: t('Saved. No collector URL is set, so telemetry stays off'),
|
|
75
78
|
);
|
|
76
79
|
} catch (err) {
|
|
77
80
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -91,32 +94,32 @@ export function TelemetrySettingsPanel() {
|
|
|
91
94
|
{loading ? (
|
|
92
95
|
<div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
|
|
93
96
|
<LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
94
|
-
|
|
97
|
+
{t('Loading telemetry settings...')}
|
|
95
98
|
</div>
|
|
96
99
|
) : (
|
|
97
100
|
<>
|
|
98
101
|
<div className="space-y-1.5">
|
|
99
102
|
<label className="text-[11px] font-medium text-agent-muted-foreground">
|
|
100
|
-
Collector
|
|
103
|
+
{t('Collector URL (/v1/traces)')}
|
|
101
104
|
</label>
|
|
102
105
|
<input
|
|
103
106
|
type="text"
|
|
104
107
|
value={endpoint}
|
|
105
108
|
onChange={(e) => setEndpoint(e.target.value)}
|
|
106
|
-
placeholder=
|
|
109
|
+
placeholder={t('http://127.0.0.1:4318/v1/traces (leave blank = telemetry off)')}
|
|
107
110
|
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"
|
|
108
111
|
/>
|
|
109
112
|
</div>
|
|
110
113
|
|
|
111
114
|
<div className="space-y-1.5">
|
|
112
115
|
<label className="text-[11px] font-medium text-agent-muted-foreground">
|
|
113
|
-
|
|
116
|
+
{t('Privacy level')}
|
|
114
117
|
</label>
|
|
115
118
|
<div className="flex gap-2">
|
|
116
119
|
{(
|
|
117
120
|
[
|
|
118
|
-
{ value: 'metadata', label: '
|
|
119
|
-
{ value: 'full', label: '
|
|
121
|
+
{ value: 'metadata', label: 'Metadata only', hint: 'Structure, latency, and status. No content.' },
|
|
122
|
+
{ value: 'full', label: 'Full (redacted)', hint: 'Includes payloads. Secrets are masked.' },
|
|
120
123
|
] as const
|
|
121
124
|
).map((opt) => (
|
|
122
125
|
<button
|
|
@@ -129,8 +132,8 @@ export function TelemetrySettingsPanel() {
|
|
|
129
132
|
: 'border-agent-border bg-agent-canvas hover:bg-agent-muted/40'
|
|
130
133
|
}`}
|
|
131
134
|
>
|
|
132
|
-
<div className="text-xs font-medium text-agent-foreground">{opt.label}</div>
|
|
133
|
-
<div className="text-[10px] text-agent-muted-foreground">{opt.hint}</div>
|
|
135
|
+
<div className="text-xs font-medium text-agent-foreground">{t(opt.label)}</div>
|
|
136
|
+
<div className="text-[10px] text-agent-muted-foreground">{t(opt.hint)}</div>
|
|
134
137
|
</button>
|
|
135
138
|
))}
|
|
136
139
|
</div>
|
|
@@ -138,7 +141,7 @@ export function TelemetrySettingsPanel() {
|
|
|
138
141
|
|
|
139
142
|
<div className="flex items-center justify-between gap-2">
|
|
140
143
|
<p className="text-[10px] text-agent-muted-foreground">
|
|
141
|
-
|
|
144
|
+
{t('At either level, keys and tokens are redacted before export. Leave the URL blank to export nothing.')}
|
|
142
145
|
</p>
|
|
143
146
|
<button
|
|
144
147
|
type="button"
|
|
@@ -150,7 +153,7 @@ export function TelemetrySettingsPanel() {
|
|
|
150
153
|
: 'bg-agent-foreground text-agent-canvas hover:opacity-90'
|
|
151
154
|
}`}
|
|
152
155
|
>
|
|
153
|
-
{saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : '
|
|
156
|
+
{saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : t('Save')}
|
|
154
157
|
</button>
|
|
155
158
|
</div>
|
|
156
159
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { LuLoaderCircle, LuChartBar, LuRefreshCw } from 'react-icons/lu';
|
|
3
|
+
import { t } from '@/i18n';
|
|
3
4
|
import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -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`;
|
|
@@ -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.
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
164
|
-
<th className="text-right font-medium px-2.5 py-1.5"
|
|
165
|
-
<th className="text-right font-medium px-2.5 py-1.5"
|
|
166
|
-
<th className="text-right font-medium px-2.5 py-1.5"
|
|
167
|
-
<th className="text-right font-medium px-2.5 py-1.5"
|
|
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
|
-
|
|
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,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
|
|
3
|
+
import { t } from '@/i18n';
|
|
3
4
|
import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -71,11 +72,15 @@ export function WebSearchSettingsPanel() {
|
|
|
71
72
|
setProvider(nextProvider);
|
|
72
73
|
setApiKey(saved?.apiKey ?? '');
|
|
73
74
|
if (nextProvider === 'ddg') {
|
|
74
|
-
setStatus('
|
|
75
|
+
setStatus(t('Saved. After you restart the app, the sidecar registers free search (DuckDuckGo).'));
|
|
75
76
|
} else if (saved?.apiKey) {
|
|
76
|
-
setStatus('
|
|
77
|
+
setStatus(t('Saved. After you restart the app, the sidecar registers web_search.'));
|
|
77
78
|
} else {
|
|
78
|
-
setStatus(
|
|
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: '
|
|
110
|
-
{ value: 'tavily', label: 'Tavily', hint: '
|
|
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=
|
|
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
|
-
?
|
|
151
|
-
|
|
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': '
|
|
47
|
-
zhipuai: '
|
|
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: '
|
|
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
|
-
|
|
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);
|