@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.
- package/README.md +3 -4
- package/package.json +4 -4
- package/src/AppShell.tsx +3 -3
- package/src/brand.ts +1 -1
- package/src/components/AgentSidebar.tsx +64 -56
- package/src/components/ChatHeader.tsx +41 -25
- package/src/components/ConfirmDialog.tsx +4 -3
- package/src/components/CreateProjectModal.tsx +31 -21
- package/src/components/LocalLlmSettingsModal.tsx +4 -3
- package/src/components/SidebarRelease.tsx +6 -5
- package/src/components/TerminalPanel.tsx +6 -5
- package/src/components/TerminalView.tsx +15 -10
- package/src/components/chat/ApprovalModal.tsx +40 -26
- package/src/components/chat/ApprovalPromptProvider.tsx +5 -5
- package/src/components/chat/AskUserPromptProvider.tsx +4 -4
- package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
- package/src/components/chat/AssistantMessage.tsx +18 -13
- package/src/components/chat/ChatInput.tsx +56 -40
- package/src/components/chat/ChatProjectBadge.tsx +27 -22
- 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 +33 -23
- package/src/components/chat/TaskProcessPanel.tsx +16 -15
- 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/chat/useChatTasks.ts +3 -3
- package/src/components/settings/AgentCapabilityFields.tsx +26 -23
- package/src/components/settings/AgentsSettingsPanel.tsx +33 -28
- 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 +58 -43
- package/src/components/settings/LlmSettingsPanel.tsx +107 -86
- package/src/components/settings/McpSettingsPanel.tsx +72 -46
- 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 +26 -23
- package/src/components/settings/SecuritySettingsPanel.tsx +58 -34
- package/src/components/settings/SettingsNavMenu.tsx +3 -2
- package/src/components/settings/SettingsSaveButton.tsx +2 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +43 -38
- package/src/components/settings/TelemetrySettingsPanel.tsx +20 -17
- package/src/components/settings/UsagePanel.tsx +22 -23
- package/src/components/settings/WebSearchSettingsPanel.tsx +28 -19
- 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/useChatsAndAgents.ts +9 -9
- package/src/hooks/useProjects.ts +3 -3
- 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 +13 -12
- package/src/lib/app-release.ts +16 -11
- package/src/lib/attachments.ts +18 -9
- package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +42 -42
- package/src/lib/chat-title.ts +3 -2
- package/src/lib/chat-transport.ts +16 -12
- package/src/lib/date-groups.ts +23 -15
- package/src/lib/host-bridge.ts +371 -4
- package/src/lib/host-paste.ts +1 -1
- package/src/lib/http-bridge.ts +12 -9
- package/src/lib/insights.ts +2 -2
- package/src/lib/local-api.ts +5 -5
- package/src/lib/portable.ts +11 -10
- package/src/lib/show-thinking-content.ts +15 -3
- package/src/lib/slash-sources.ts +3 -3
- package/src/lib/tauri-bridge.ts +1 -1
- package/src/main.tsx +27 -4
- package/src/pages/AgentPage.tsx +39 -34
- package/src/pages/ChatPanelPreviewPage.tsx +46 -44
- package/src/pages/SettingsPage.tsx +51 -45
- package/src/vite-env.d.ts +3 -0
- package/vite.base.js +6 -2
- package/src/lib/electron-bridge.ts +0 -390
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { LuLoaderCircle, LuShieldCheck } from 'react-icons/lu';
|
|
3
|
-
import {
|
|
3
|
+
import { t } from '@/i18n';
|
|
4
|
+
import { hasHostBridge } from '@/lib/host-bridge';
|
|
4
5
|
import {
|
|
5
6
|
getSidecarSandboxPosture,
|
|
6
7
|
type EgressPosture,
|
|
@@ -23,44 +24,52 @@ type PostureReason = SidecarSandboxPosture['reason'];
|
|
|
23
24
|
|
|
24
25
|
/** 收容失败、已拒绝启动(告警行首句)。 */
|
|
25
26
|
const REFUSED_REASON_TEXT: Partial<Record<PostureReason, string>> = {
|
|
26
|
-
platform_unsupported: '
|
|
27
|
-
seatbelt_missing: '
|
|
28
|
-
profile_failed: '
|
|
29
|
-
wrap_failed: 'Linux
|
|
30
|
-
helper_missing: '
|
|
27
|
+
platform_unsupported: 'No process sandbox backend is available on this platform',
|
|
28
|
+
seatbelt_missing: '/usr/bin/sandbox-exec not found',
|
|
29
|
+
profile_failed: 'Failed to generate the sandbox configuration (Seatbelt profile)',
|
|
30
|
+
wrap_failed: 'The Linux process sandbox (bwrap / Landlock) could not wrap the sidecar',
|
|
31
|
+
helper_missing: 'win-spawn-helper.exe not found',
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
/** 手动关闭原因的中文说明(中性行,不告警)。 */
|
|
34
35
|
const OPT_OUT_REASON_TEXT: Partial<Record<PostureReason, string>> = {
|
|
35
|
-
disabled_by_option: '
|
|
36
|
-
disabled_by_env: '
|
|
36
|
+
disabled_by_option: 'Turned off with sandbox: false',
|
|
37
|
+
disabled_by_env: 'Turned off with STEERABLE_SIDECAR_SANDBOX=0',
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
function activeHeadline(backend: SidecarSandboxPosture['backend']): string {
|
|
40
41
|
switch (backend) {
|
|
41
42
|
case 'seatbelt':
|
|
42
|
-
return 'Seatbelt ·
|
|
43
|
+
return t('Seatbelt · partial enforcement');
|
|
43
44
|
case 'bwrap':
|
|
44
|
-
return 'bwrap ·
|
|
45
|
+
return t('bwrap · partial enforcement');
|
|
45
46
|
case 'landlock':
|
|
46
|
-
return 'Landlock ·
|
|
47
|
+
return t('Landlock · partial enforcement');
|
|
47
48
|
case 'windows-restricted-token':
|
|
48
|
-
return 'Windows
|
|
49
|
+
return t('Windows restricted token · partial enforcement');
|
|
49
50
|
case 'none':
|
|
50
|
-
return '
|
|
51
|
+
return t('Not sandboxed');
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function activeBody(backend: SidecarSandboxPosture['backend']): string {
|
|
55
56
|
switch (backend) {
|
|
56
57
|
case 'seatbelt':
|
|
57
|
-
return
|
|
58
|
+
return t(
|
|
59
|
+
'The sidecar process runs under macOS Seatbelt: writes are limited to ~/.steerable and the temp directory. Outbound network to remote hosts can only be limited by port (Seatbelt does not recognize host names), so this is recorded as "partial" rather than "full".',
|
|
60
|
+
);
|
|
58
61
|
case 'bwrap':
|
|
59
|
-
return
|
|
62
|
+
return t(
|
|
63
|
+
'The sidecar process runs under bubblewrap: writes are limited to ~/.steerable and the temp directory. Outbound network stays open (the sidecar needs to reach the LLM), so this is recorded as "partial".',
|
|
64
|
+
);
|
|
60
65
|
case 'landlock':
|
|
61
|
-
return
|
|
66
|
+
return t(
|
|
67
|
+
'The sidecar process runs under Linux Landlock: writes are limited to ~/.steerable and the temp directory. Outbound network stays open (the sidecar needs to reach the LLM), so this is recorded as "partial".',
|
|
68
|
+
);
|
|
62
69
|
case 'windows-restricted-token':
|
|
63
|
-
return
|
|
70
|
+
return t(
|
|
71
|
+
'The sidecar process runs under a Windows restricted token + Job Object: writes are limited to ~/.steerable. The helper does not enforce network policy (no WFP), so this is recorded as "partial".',
|
|
72
|
+
);
|
|
64
73
|
case 'none':
|
|
65
74
|
return '';
|
|
66
75
|
}
|
|
@@ -71,20 +80,24 @@ function EgressPostureRow({ egress }: { egress: EgressPosture }) {
|
|
|
71
80
|
if (egress.mode === 'per-host-proxy') {
|
|
72
81
|
return (
|
|
73
82
|
<p className="text-[11px] text-agent-muted-foreground">
|
|
74
|
-
|
|
83
|
+
{t(
|
|
84
|
+
'Egress control: the per-host allowlist proxy is active. Requests outside the allowlist ask you to allow them one by one (valid for this session only).',
|
|
85
|
+
)}
|
|
75
86
|
</p>
|
|
76
87
|
);
|
|
77
88
|
}
|
|
78
89
|
if (egress.mode === 'disabled') {
|
|
79
90
|
return (
|
|
80
91
|
<p className="text-[11px] text-agent-muted-foreground">
|
|
81
|
-
|
|
92
|
+
{t('Egress control: off{reason}.', { reason: egress.reason ? ` — ${egress.reason}` : '' })}
|
|
82
93
|
</p>
|
|
83
94
|
);
|
|
84
95
|
}
|
|
85
96
|
return (
|
|
86
97
|
<p className="text-[11px] text-amber-600 dark:text-amber-400">
|
|
87
|
-
|
|
98
|
+
{t('Egress control: fell back to port level (coarser control){reason}.', {
|
|
99
|
+
reason: egress.reason ? ` — ${egress.reason}` : '',
|
|
100
|
+
})}
|
|
88
101
|
</p>
|
|
89
102
|
);
|
|
90
103
|
}
|
|
@@ -96,19 +109,19 @@ export function SecuritySettingsPanel() {
|
|
|
96
109
|
const [unavailable, setUnavailable] = useState<string | null>(null);
|
|
97
110
|
|
|
98
111
|
const fetchPosture = useCallback(async () => {
|
|
99
|
-
if (!
|
|
112
|
+
if (!hasHostBridge()) return;
|
|
100
113
|
setLoading(true);
|
|
101
114
|
setUnavailable(null);
|
|
102
115
|
try {
|
|
103
116
|
const res = await getSidecarSandboxPosture();
|
|
104
117
|
setPosture(res.posture ?? null);
|
|
105
118
|
setEgress(res.egress ?? null);
|
|
106
|
-
if (!res.posture) setUnavailable('
|
|
119
|
+
if (!res.posture) setUnavailable(t('Could not read sandbox status'));
|
|
107
120
|
} catch {
|
|
108
121
|
// 503 = sidecar 未就绪且没有收容失败记录。
|
|
109
122
|
setPosture(null);
|
|
110
123
|
setEgress(null);
|
|
111
|
-
setUnavailable('
|
|
124
|
+
setUnavailable(t('Sidecar not ready — could not read sandbox status'));
|
|
112
125
|
} finally {
|
|
113
126
|
setLoading(false);
|
|
114
127
|
}
|
|
@@ -125,35 +138,46 @@ export function SecuritySettingsPanel() {
|
|
|
125
138
|
<div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
|
|
126
139
|
<h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
|
|
127
140
|
<LuShieldCheck className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
128
|
-
Sidecar
|
|
141
|
+
{t('Sidecar process sandbox')}
|
|
129
142
|
</h4>
|
|
130
143
|
|
|
131
|
-
{!
|
|
132
|
-
<p className="text-[11px] text-agent-muted-foreground"
|
|
144
|
+
{!hasHostBridge() ? (
|
|
145
|
+
<p className="text-[11px] text-agent-muted-foreground">{t('Open this in the desktop app')}</p>
|
|
133
146
|
) : loading ? (
|
|
134
147
|
<div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
|
|
135
148
|
<LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
136
|
-
|
|
149
|
+
{t('Loading sandbox status...')}
|
|
137
150
|
</div>
|
|
138
151
|
) : unavailable ? (
|
|
139
152
|
<p className="text-[11px] text-agent-muted-foreground">{unavailable}</p>
|
|
140
153
|
) : posture && refusedReason ? (
|
|
141
154
|
<div className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive space-y-1.5">
|
|
142
|
-
<p className="font-medium"
|
|
155
|
+
<p className="font-medium">
|
|
156
|
+
{t('Cannot contain the process, so startup was refused — {reason}.', {
|
|
157
|
+
reason: t(refusedReason),
|
|
158
|
+
})}
|
|
159
|
+
</p>
|
|
143
160
|
<p>
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
{t(
|
|
162
|
+
'The sidecar process that holds the provider API keys did not start, so it is not running without isolation either.',
|
|
163
|
+
)}{' '}
|
|
164
|
+
{t('The only way to run it unsandboxed is to set STEERABLE_SIDECAR_SANDBOX=0 explicitly.')}
|
|
146
165
|
</p>
|
|
147
166
|
<p>
|
|
148
|
-
|
|
149
|
-
|
|
167
|
+
{t(
|
|
168
|
+
'For isolation, install the containment backend for your platform (macOS Seatbelt, Linux bwrap/Landlock, Windows win-spawn-helper), or run this app in a container.',
|
|
169
|
+
)}
|
|
150
170
|
</p>
|
|
151
171
|
</div>
|
|
152
172
|
) : posture && optOutReason ? (
|
|
153
173
|
<div className="rounded-agent-md border border-agent-border/60 bg-agent-canvas p-2.5 text-xs text-agent-muted-foreground space-y-1.5">
|
|
154
|
-
<p className="font-medium text-agent-foreground"
|
|
174
|
+
<p className="font-medium text-agent-foreground">
|
|
175
|
+
{t('Turned off manually — {reason}.', { reason: t(optOutReason) })}
|
|
176
|
+
</p>
|
|
155
177
|
<p>
|
|
156
|
-
|
|
178
|
+
{t(
|
|
179
|
+
'The sidecar process (which holds the provider API keys) runs without OS isolation. To restore it, remove the setting and restart the app.',
|
|
180
|
+
)}
|
|
157
181
|
</p>
|
|
158
182
|
</div>
|
|
159
183
|
) : posture ? (
|
|
@@ -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,7 +5,8 @@ import {
|
|
|
5
5
|
LuLoaderCircle,
|
|
6
6
|
LuTrash2,
|
|
7
7
|
} from 'react-icons/lu';
|
|
8
|
-
import {
|
|
8
|
+
import { t } from '@/i18n';
|
|
9
|
+
import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
|
|
9
10
|
import { getPackHiddenSlashSkills } from '@/packs/registry';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -58,17 +59,17 @@ export function SkillsSettingsPanel() {
|
|
|
58
59
|
const [error, setError] = useState<string | null>(null);
|
|
59
60
|
|
|
60
61
|
const fetchSkills = useCallback(async () => {
|
|
61
|
-
if (!
|
|
62
|
+
if (!hasHostBridge()) return;
|
|
62
63
|
setSkillsLoading(true);
|
|
63
64
|
setError(null);
|
|
64
65
|
try {
|
|
65
|
-
const res = await
|
|
66
|
+
const res = await getHostBridge()!.localBackend.request<{ skills: any[] }>({
|
|
66
67
|
method: 'GET',
|
|
67
68
|
path: '/api/v2/chat-agents/skills',
|
|
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);
|
|
@@ -80,24 +81,24 @@ export function SkillsSettingsPanel() {
|
|
|
80
81
|
}, [fetchSkills]);
|
|
81
82
|
|
|
82
83
|
const handleBrowseFolder = async () => {
|
|
83
|
-
if (!
|
|
84
|
+
if (!hasHostBridge()) return;
|
|
84
85
|
try {
|
|
85
|
-
const bridge =
|
|
86
|
+
const bridge = getHostBridge();
|
|
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,25 +108,25 @@ 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
|
-
const res = await
|
|
113
|
+
const res = await getHostBridge()!.localBackend.request<{ success: boolean; name: string }>({
|
|
113
114
|
method: 'POST',
|
|
114
115
|
path: '/api/v2/chat-agents/skills/import',
|
|
115
116
|
body: { path: trimmed },
|
|
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
|
}
|
|
@@ -136,14 +137,14 @@ export function SkillsSettingsPanel() {
|
|
|
136
137
|
setDeletingName(name);
|
|
137
138
|
setError(null);
|
|
138
139
|
try {
|
|
139
|
-
const res = await
|
|
140
|
+
const res = await getHostBridge()!.localBackend.request<{ success: boolean }>({
|
|
140
141
|
method: 'DELETE',
|
|
141
142
|
path: `/api/v2/chat-agents/skills/delete/${encodeURIComponent(name)}`,
|
|
142
143
|
});
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { LuLoaderCircle, LuActivity } from 'react-icons/lu';
|
|
3
|
-
import {
|
|
3
|
+
import { t } from '@/i18n';
|
|
4
|
+
import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* TelemetrySettingsPanel — W6-6 遥测合规化的桌面 collector 配置面板。
|
|
@@ -35,11 +36,11 @@ export function TelemetrySettingsPanel() {
|
|
|
35
36
|
const [error, setError] = useState<string | null>(null);
|
|
36
37
|
|
|
37
38
|
const fetchSettings = useCallback(async () => {
|
|
38
|
-
if (!
|
|
39
|
+
if (!hasHostBridge()) return;
|
|
39
40
|
setLoading(true);
|
|
40
41
|
setError(null);
|
|
41
42
|
try {
|
|
42
|
-
const res = await
|
|
43
|
+
const res = await getHostBridge()!.localBackend.request<TelemetrySettingsWire | null>({
|
|
43
44
|
method: 'GET',
|
|
44
45
|
path: '/api/v2/local-settings/telemetry',
|
|
45
46
|
});
|
|
@@ -57,12 +58,12 @@ export function TelemetrySettingsPanel() {
|
|
|
57
58
|
}, [fetchSettings]);
|
|
58
59
|
|
|
59
60
|
const handleSave = async () => {
|
|
60
|
-
if (!
|
|
61
|
+
if (!hasHostBridge()) return;
|
|
61
62
|
setSaving(true);
|
|
62
63
|
setError(null);
|
|
63
64
|
setStatus(null);
|
|
64
65
|
try {
|
|
65
|
-
const saved = await
|
|
66
|
+
const saved = await getHostBridge()!.localBackend.request<TelemetrySettingsWire>({
|
|
66
67
|
method: 'POST',
|
|
67
68
|
path: '/api/v2/local-settings/telemetry',
|
|
68
69
|
body: { endpoint: endpoint.trim(), privacyMode },
|
|
@@ -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
|
|