@steerable/agent-shell-web 0.6.15
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/LICENSE +91 -0
- package/README.md +20 -0
- package/package.json +69 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/src/AppShell.tsx +24 -0
- package/src/assets/logo-generic.png +0 -0
- package/src/brand.test.ts +22 -0
- package/src/brand.ts +48 -0
- package/src/components/AgentSidebar.test.tsx +127 -0
- package/src/components/AgentSidebar.tsx +1010 -0
- package/src/components/ChatHeader.test.tsx +230 -0
- package/src/components/ChatHeader.tsx +436 -0
- package/src/components/DockPanelHeader.tsx +69 -0
- package/src/components/LocalLlmSettingsModal.test.tsx +372 -0
- package/src/components/LocalLlmSettingsModal.tsx +72 -0
- package/src/components/TerminalPanel.tsx +68 -0
- package/src/components/TerminalView.tsx +228 -0
- package/src/components/chat/ApprovalModal.test.tsx +83 -0
- package/src/components/chat/ApprovalModal.tsx +229 -0
- package/src/components/chat/AskUserModalHost.test.tsx +176 -0
- package/src/components/chat/AskUserModalHost.tsx +61 -0
- package/src/components/chat/AskUserQuestionMenu.tsx +621 -0
- package/src/components/chat/AssistantMessage.test.tsx +68 -0
- package/src/components/chat/AssistantMessage.tsx +469 -0
- package/src/components/chat/ChatInput.test.tsx +609 -0
- package/src/components/chat/ChatInput.tsx +2068 -0
- package/src/components/chat/ChatProjectBadge.tsx +362 -0
- package/src/components/chat/EmptyChat.tsx +18 -0
- package/src/components/chat/ExecPolicyPicker.test.tsx +26 -0
- package/src/components/chat/ExecPolicyPicker.tsx +120 -0
- package/src/components/chat/ExecutedActionsCard.tsx +216 -0
- package/src/components/chat/InterruptedTurnCard.tsx +53 -0
- package/src/components/chat/LocalChatPanel.tsx +408 -0
- package/src/components/chat/Markdown.test.tsx +170 -0
- package/src/components/chat/Markdown.tsx +432 -0
- package/src/components/chat/MessageList.tsx +332 -0
- package/src/components/chat/ModelPicker.test.tsx +149 -0
- package/src/components/chat/ModelPicker.tsx +289 -0
- package/src/components/chat/OrchestrationChildrenCard.tsx +40 -0
- package/src/components/chat/PlanTodoList.tsx +121 -0
- package/src/components/chat/ProjectTrustBanner.tsx +91 -0
- package/src/components/chat/SessionTreeModal.test.tsx +161 -0
- package/src/components/chat/SessionTreeModal.tsx +221 -0
- package/src/components/chat/StreamingStatus.tsx +95 -0
- package/src/components/chat/TaskOutcomeCards.test.tsx +97 -0
- package/src/components/chat/TaskOutcomeCards.tsx +128 -0
- package/src/components/chat/TaskPanelModal.test.tsx +198 -0
- package/src/components/chat/TaskPanelModal.tsx +310 -0
- package/src/components/chat/TaskProcessPanel.test.tsx +111 -0
- package/src/components/chat/TaskProcessPanel.tsx +208 -0
- package/src/components/chat/TurnProcessGroup.test.tsx +161 -0
- package/src/components/chat/TurnProcessGroup.tsx +193 -0
- package/src/components/chat/UnifiedDiff.tsx +31 -0
- package/src/components/chat/UserMessage.tsx +155 -0
- package/src/components/chat/elapsed.test.ts +42 -0
- package/src/components/chat/elapsed.ts +45 -0
- package/src/components/chat/executed-actions-model.test.ts +134 -0
- package/src/components/chat/executed-actions-model.ts +143 -0
- package/src/components/chat/orchestration-children-model.test.ts +115 -0
- package/src/components/chat/orchestration-children-model.ts +104 -0
- package/src/components/chat/timestamp.ts +24 -0
- package/src/components/chat/turn-timeline.test.ts +97 -0
- package/src/components/chat/turn-timeline.ts +132 -0
- package/src/components/chat/useChatTasks.test.ts +136 -0
- package/src/components/chat/useChatTasks.ts +134 -0
- package/src/components/chat/useCopy.ts +62 -0
- package/src/components/settings/AgentCapabilityFields.tsx +343 -0
- package/src/components/settings/AgentsSettingsPanel.test.tsx +229 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +412 -0
- package/src/components/settings/DiagnoseSettingsPanel.tsx +119 -0
- package/src/components/settings/InsightsSettingsPanel.test.tsx +132 -0
- package/src/components/settings/InsightsSettingsPanel.tsx +309 -0
- package/src/components/settings/LlmSettingsPanel.tsx +875 -0
- package/src/components/settings/McpSettingsPanel.tsx +466 -0
- package/src/components/settings/ModelCapabilityChips.tsx +46 -0
- package/src/components/settings/ModelIdCombobox.test.tsx +88 -0
- package/src/components/settings/ModelIdCombobox.tsx +185 -0
- package/src/components/settings/SecuritySettingsPanel.test.tsx +122 -0
- package/src/components/settings/SecuritySettingsPanel.tsx +169 -0
- package/src/components/settings/SettingsSaveButton.tsx +35 -0
- package/src/components/settings/SkillsSettingsPanel.test.tsx +62 -0
- package/src/components/settings/SkillsSettingsPanel.tsx +334 -0
- package/src/components/settings/TelemetrySettingsPanel.tsx +173 -0
- package/src/components/settings/UsagePanel.tsx +196 -0
- package/src/components/settings/WebSearchSettingsPanel.test.tsx +90 -0
- package/src/components/settings/WebSearchSettingsPanel.tsx +183 -0
- package/src/components/settings/compat-flags-model.test.ts +71 -0
- package/src/components/settings/compat-flags-model.ts +57 -0
- package/src/components/settings/llm-vendors.test.ts +171 -0
- package/src/components/settings/llm-vendors.ts +275 -0
- package/src/components/settings/model-capabilities.test.ts +88 -0
- package/src/components/settings/model-capabilities.ts +123 -0
- package/src/components/settings/preset-choice-model.test.ts +96 -0
- package/src/components/settings/preset-choice-model.ts +75 -0
- package/src/fixtures/browser-dev-data.json +315 -0
- package/src/hooks/useChatsAndAgents.ts +182 -0
- package/src/layouts/AgentLayout.tsx +574 -0
- package/src/lib/attachments.ts +78 -0
- package/src/lib/browser-dev-electron-mock.ts +640 -0
- package/src/lib/chat-title.ts +33 -0
- package/src/lib/chat-transport.ts +346 -0
- package/src/lib/date-groups.ts +34 -0
- package/src/lib/demo-flag.ts +15 -0
- package/src/lib/electron-bridge.ts +270 -0
- package/src/lib/exec-policy.ts +25 -0
- package/src/lib/http-bridge.ts +293 -0
- package/src/lib/insights.ts +17 -0
- package/src/lib/local-api.ts +714 -0
- package/src/lib/pending-first-message.test.ts +17 -0
- package/src/lib/pending-first-message.ts +36 -0
- package/src/lib/slash-sources.ts +191 -0
- package/src/main.tsx +103 -0
- package/src/packs/registry.ts +64 -0
- package/src/pages/AgentPage.tsx +1226 -0
- package/src/pages/ChatPanelPreviewPage.tsx +264 -0
- package/src/pages/SettingsPage.test.tsx +102 -0
- package/src/pages/SettingsPage.tsx +197 -0
- package/src/styles/index.css +156 -0
- package/src/vite-env.d.ts +1 -0
- package/vite.base.ts +94 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { LuChevronDown, LuRefreshCw } from 'react-icons/lu';
|
|
3
|
+
import { ModelCapabilityChips } from '@/components/settings/ModelCapabilityChips';
|
|
4
|
+
import {
|
|
5
|
+
getLlmModels,
|
|
6
|
+
getLlmSettings,
|
|
7
|
+
type GatewayModelCatalog,
|
|
8
|
+
type GatewayModelEntry,
|
|
9
|
+
} from '@/lib/local-api';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ModelPicker — 聊天输入框的模型/档位选择器。
|
|
13
|
+
*
|
|
14
|
+
* 数据源是网关活目录(`GET /api/v2/llm/models` → sidecar `models.list`):
|
|
15
|
+
* 网关真正接受的 id,经 models.dev 能力表 join 出 reasoning 档位。目录
|
|
16
|
+
* 只做发现、不做路由白名单——设置里的模型即使不在目录里也照常显示、
|
|
17
|
+
* 照常可发(capabilities: 'unknown' 只是没有档位可选)。
|
|
18
|
+
*
|
|
19
|
+
* 目录状态按载荷如实披露:live 不标;stale 标「缓存」;offline 标
|
|
20
|
+
* 「目录不可用」(此时选择器仍可用,只是没有目录行)。
|
|
21
|
+
*
|
|
22
|
+
* 选择语义:model 为 null = 跟随全局设置(LocalLlmSettingsModal);
|
|
23
|
+
* effort 为 null = 自动(厂商预制/服务端默认)。显式选择的档位随每轮
|
|
24
|
+
* 请求下发,sidecar 严格校验——模型不支持的档位在流启动时就报错,
|
|
25
|
+
* 不会静默丢弃。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface ModelPickerProps {
|
|
29
|
+
/** 当前每轮覆盖的模型;null = 跟随全局设置。 */
|
|
30
|
+
model: string | null;
|
|
31
|
+
/** 当前每轮 reasoning 档位;null = 自动。 */
|
|
32
|
+
reasoningEffort: string | null;
|
|
33
|
+
onSelectModel: (model: string | null) => void;
|
|
34
|
+
onSelectEffort: (effort: string | null) => void;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ModelPicker({
|
|
39
|
+
model,
|
|
40
|
+
reasoningEffort,
|
|
41
|
+
onSelectModel,
|
|
42
|
+
onSelectEffort,
|
|
43
|
+
disabled = false,
|
|
44
|
+
}: ModelPickerProps) {
|
|
45
|
+
const [catalog, setCatalog] = useState<GatewayModelCatalog | null>(null);
|
|
46
|
+
const [settingsModel, setSettingsModel] = useState<string>('');
|
|
47
|
+
const [loading, setLoading] = useState(false);
|
|
48
|
+
const [modelMenuOpen, setModelMenuOpen] = useState(false);
|
|
49
|
+
const [effortMenuOpen, setEffortMenuOpen] = useState(false);
|
|
50
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
|
|
52
|
+
const refresh = useCallback(async () => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
const [modelsResult, settingsResult] = await Promise.all([
|
|
56
|
+
getLlmModels(),
|
|
57
|
+
getLlmSettings(),
|
|
58
|
+
]);
|
|
59
|
+
setCatalog(modelsResult);
|
|
60
|
+
setSettingsModel(settingsResult.model);
|
|
61
|
+
} catch {
|
|
62
|
+
// 桥不可达(浏览器预览等):视同目录不可用,选择器退化为纯展示。
|
|
63
|
+
setCatalog({ models: [], catalogStatus: 'offline' });
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
void refresh();
|
|
71
|
+
}, [refresh]);
|
|
72
|
+
|
|
73
|
+
// 点击组件外部时收起两个弹层。
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!modelMenuOpen && !effortMenuOpen) return;
|
|
76
|
+
const onPointerDown = (event: MouseEvent) => {
|
|
77
|
+
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
|
|
78
|
+
setModelMenuOpen(false);
|
|
79
|
+
setEffortMenuOpen(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
document.addEventListener('mousedown', onPointerDown);
|
|
83
|
+
return () => document.removeEventListener('mousedown', onPointerDown);
|
|
84
|
+
}, [modelMenuOpen, effortMenuOpen]);
|
|
85
|
+
|
|
86
|
+
const effectiveModel = model ?? settingsModel;
|
|
87
|
+
const selectedEntry: GatewayModelEntry | undefined = useMemo(
|
|
88
|
+
() => catalog?.models.find((entry) => entry.id === effectiveModel),
|
|
89
|
+
[catalog, effectiveModel],
|
|
90
|
+
);
|
|
91
|
+
const reasoningLevels = selectedEntry?.reasoningLevels ?? [];
|
|
92
|
+
|
|
93
|
+
const status = catalog?.catalogStatus ?? 'offline';
|
|
94
|
+
const statusBadge =
|
|
95
|
+
status === 'offline' ? (
|
|
96
|
+
<span className="inline-flex items-center gap-1 rounded-full border border-amber-400/50 bg-amber-400/10 px-1.5 py-0.5 text-[10px] text-amber-700 dark:text-amber-300">
|
|
97
|
+
目录不可用
|
|
98
|
+
</span>
|
|
99
|
+
) : status === 'stale' ? (
|
|
100
|
+
<span className="inline-flex items-center gap-1 rounded-full border border-agent-border bg-agent-muted px-1.5 py-0.5 text-[10px] text-agent-muted-foreground">
|
|
101
|
+
缓存
|
|
102
|
+
</span>
|
|
103
|
+
) : null;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div ref={rootRef} className="flex min-w-0 items-center gap-1">
|
|
107
|
+
<div className="relative">
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={() => {
|
|
111
|
+
const next = !modelMenuOpen;
|
|
112
|
+
setModelMenuOpen(next);
|
|
113
|
+
setEffortMenuOpen(false);
|
|
114
|
+
// 打开时若目录不可用/还没拉到,重试一次(网关可能刚恢复)。
|
|
115
|
+
if (next && status !== 'live' && !loading) void refresh();
|
|
116
|
+
}}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
className="inline-flex h-7 max-w-[160px] items-center gap-1.5 rounded-full border border-agent-border bg-agent-canvas px-2 text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-70 @sm:max-w-[220px]"
|
|
119
|
+
title={
|
|
120
|
+
status === 'offline'
|
|
121
|
+
? `模型目录不可用${catalog?.error ? `:${catalog.error}` : ''}(仍可手动指定模型)`
|
|
122
|
+
: `当前模型:${effectiveModel || '未配置'}`
|
|
123
|
+
}
|
|
124
|
+
aria-haspopup="menu"
|
|
125
|
+
aria-expanded={modelMenuOpen}
|
|
126
|
+
>
|
|
127
|
+
<span className="truncate">{effectiveModel || '选择模型'}</span>
|
|
128
|
+
{model != null && (
|
|
129
|
+
<span className="inline-flex shrink-0 items-center rounded bg-agent-foreground/10 px-1 py-0.5 text-[9px] font-medium text-agent-foreground">
|
|
130
|
+
本会话
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
<LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
{modelMenuOpen && (
|
|
137
|
+
<div
|
|
138
|
+
role="menu"
|
|
139
|
+
className="absolute bottom-full left-0 z-50 mb-1 max-h-72 w-80 overflow-y-auto rounded-agent-md border border-agent-border bg-agent-canvas p-1 shadow-lg"
|
|
140
|
+
>
|
|
141
|
+
<div className="flex items-center justify-between gap-2 px-2 py-1">
|
|
142
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-agent-muted-foreground">
|
|
143
|
+
模型(网关目录)
|
|
144
|
+
</span>
|
|
145
|
+
<span className="flex items-center gap-1">
|
|
146
|
+
{statusBadge}
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={() => void refresh()}
|
|
150
|
+
className="flex h-4 w-4 items-center justify-center rounded text-agent-muted-foreground transition-colors hover:text-agent-foreground"
|
|
151
|
+
title="刷新目录"
|
|
152
|
+
aria-label="刷新目录"
|
|
153
|
+
>
|
|
154
|
+
<LuRefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} />
|
|
155
|
+
</button>
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
role="menuitem"
|
|
161
|
+
onClick={() => {
|
|
162
|
+
onSelectModel(null);
|
|
163
|
+
setModelMenuOpen(false);
|
|
164
|
+
}}
|
|
165
|
+
className={[
|
|
166
|
+
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition-colors',
|
|
167
|
+
model == null
|
|
168
|
+
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
169
|
+
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
170
|
+
].join(' ')}
|
|
171
|
+
>
|
|
172
|
+
<span className="min-w-0 flex-1 truncate">
|
|
173
|
+
跟随设置{settingsModel ? `(${settingsModel})` : ''}
|
|
174
|
+
</span>
|
|
175
|
+
</button>
|
|
176
|
+
{catalog?.models.map((entry) => {
|
|
177
|
+
const isActive = entry.id === effectiveModel && model != null;
|
|
178
|
+
const displayName =
|
|
179
|
+
entry.name && entry.name !== entry.id ? entry.name : null;
|
|
180
|
+
return (
|
|
181
|
+
<button
|
|
182
|
+
key={entry.id}
|
|
183
|
+
type="button"
|
|
184
|
+
role="menuitem"
|
|
185
|
+
onClick={() => {
|
|
186
|
+
onSelectModel(entry.id);
|
|
187
|
+
// 换模型后旧档位未必支持:清掉让 sidecar 按新模型的
|
|
188
|
+
// 目录校验,而不是把旧档位硬发给新模型。
|
|
189
|
+
onSelectEffort(null);
|
|
190
|
+
setModelMenuOpen(false);
|
|
191
|
+
}}
|
|
192
|
+
className={[
|
|
193
|
+
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition-colors',
|
|
194
|
+
isActive
|
|
195
|
+
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
196
|
+
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
197
|
+
].join(' ')}
|
|
198
|
+
>
|
|
199
|
+
<span className="min-w-0 flex-1">
|
|
200
|
+
<span className="block truncate font-medium">{entry.id}</span>
|
|
201
|
+
{(displayName || entry.capabilities) && (
|
|
202
|
+
<span className="mt-0.5 flex min-w-0 flex-wrap items-center gap-1">
|
|
203
|
+
{displayName ? (
|
|
204
|
+
<span className="truncate text-[10px] text-agent-muted-foreground">
|
|
205
|
+
{displayName}
|
|
206
|
+
</span>
|
|
207
|
+
) : null}
|
|
208
|
+
<ModelCapabilityChips entry={entry} />
|
|
209
|
+
</span>
|
|
210
|
+
)}
|
|
211
|
+
</span>
|
|
212
|
+
</button>
|
|
213
|
+
);
|
|
214
|
+
})}
|
|
215
|
+
{catalog != null && catalog.models.length === 0 && (
|
|
216
|
+
<div className="px-2 py-2 text-[11px] text-agent-muted-foreground">
|
|
217
|
+
目录为空——{status === 'offline' ? '网关不可达,仍按设置里的模型发送' : '网关未报告任何模型'}
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{reasoningLevels.length > 0 && (
|
|
225
|
+
<div className="relative">
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={() => {
|
|
229
|
+
setEffortMenuOpen(!effortMenuOpen);
|
|
230
|
+
setModelMenuOpen(false);
|
|
231
|
+
}}
|
|
232
|
+
disabled={disabled}
|
|
233
|
+
className="inline-flex h-7 items-center gap-1 rounded-full border border-agent-border bg-agent-canvas px-2 text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-70"
|
|
234
|
+
title="推理档位(随每轮请求下发;模型不支持会直接报错)"
|
|
235
|
+
aria-haspopup="menu"
|
|
236
|
+
aria-expanded={effortMenuOpen}
|
|
237
|
+
>
|
|
238
|
+
<span className="truncate">{reasoningEffort ?? '自动'}</span>
|
|
239
|
+
<LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
|
|
240
|
+
</button>
|
|
241
|
+
{effortMenuOpen && (
|
|
242
|
+
<div
|
|
243
|
+
role="menu"
|
|
244
|
+
className="absolute bottom-full left-0 z-50 mb-1 w-36 overflow-y-auto rounded-agent-md border border-agent-border bg-agent-canvas p-1 shadow-lg"
|
|
245
|
+
>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
role="menuitem"
|
|
249
|
+
onClick={() => {
|
|
250
|
+
onSelectEffort(null);
|
|
251
|
+
setEffortMenuOpen(false);
|
|
252
|
+
}}
|
|
253
|
+
className={[
|
|
254
|
+
'flex w-full rounded px-2 py-1.5 text-left text-xs transition-colors',
|
|
255
|
+
reasoningEffort == null
|
|
256
|
+
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
257
|
+
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
258
|
+
].join(' ')}
|
|
259
|
+
>
|
|
260
|
+
自动
|
|
261
|
+
</button>
|
|
262
|
+
{reasoningLevels.map((level) => (
|
|
263
|
+
<button
|
|
264
|
+
key={level}
|
|
265
|
+
type="button"
|
|
266
|
+
role="menuitem"
|
|
267
|
+
onClick={() => {
|
|
268
|
+
onSelectEffort(level);
|
|
269
|
+
setEffortMenuOpen(false);
|
|
270
|
+
}}
|
|
271
|
+
className={[
|
|
272
|
+
'flex w-full rounded px-2 py-1.5 text-left text-xs transition-colors',
|
|
273
|
+
reasoningEffort === level
|
|
274
|
+
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
275
|
+
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
276
|
+
].join(' ')}
|
|
277
|
+
>
|
|
278
|
+
{level}
|
|
279
|
+
</button>
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export default ModelPicker;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { OrchestrationPlanCard } from '@steerable/agent-ui/cards';
|
|
3
|
+
import {
|
|
4
|
+
childrenToCardModel,
|
|
5
|
+
type ChildInfo,
|
|
6
|
+
} from './orchestration-children-model';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OrchestrationChildrenCard — the desktop surface of the framework's P3.1
|
|
10
|
+
* multi-agent orchestration. Adapts the live `ChildInfo` list (accumulated
|
|
11
|
+
* from `orchestration_child` SSE events) onto the framework's
|
|
12
|
+
* `OrchestrationPlanCard`: one row per child, status dot per row. The card
|
|
13
|
+
* payload is synthesized from spawn events, so there is no separate "plan"
|
|
14
|
+
* step: the plan IS the set of spawned children.
|
|
15
|
+
*
|
|
16
|
+
* The mapping logic lives in `./orchestration-children-model` (pure,
|
|
17
|
+
* runtime-dependency-free) so node-side tests do not pull the agent-ui
|
|
18
|
+
* dist build.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type { ChildInfo } from './orchestration-children-model';
|
|
22
|
+
|
|
23
|
+
export function OrchestrationChildrenCard({ children }: { children: ChildInfo[] }) {
|
|
24
|
+
const { payload, taskStatuses } = useMemo(
|
|
25
|
+
() => childrenToCardModel(children),
|
|
26
|
+
[children],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (children.length === 0) return null;
|
|
30
|
+
return (
|
|
31
|
+
<OrchestrationPlanCard
|
|
32
|
+
payload={payload}
|
|
33
|
+
taskStatuses={taskStatuses}
|
|
34
|
+
agentNameFor={(id) => id}
|
|
35
|
+
defaultExpanded
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default OrchestrationChildrenCard;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { LuCircle, LuCircleCheck, LuListChecks } from 'react-icons/lu';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PlanTodoList — renders the ```plan fenced block that the plan-mode skill
|
|
5
|
+
* instructs the model to emit, as a visual todolist card instead of a raw
|
|
6
|
+
* code block.
|
|
7
|
+
*
|
|
8
|
+
* Expected fence content (see src/local-backend/skills/70-plan-mode):
|
|
9
|
+
*
|
|
10
|
+
* # 一句话计划标题
|
|
11
|
+
* - [ ] 第一步……
|
|
12
|
+
* - [x] 已完成的步骤(执行回顾时可能出现)
|
|
13
|
+
*
|
|
14
|
+
* The parser is intentionally forgiving: numbered lists (`1. …`), plain
|
|
15
|
+
* dashes (`- …`) and bare `[ ] …` lines all count as steps, so a slightly
|
|
16
|
+
* off-spec local model still gets a proper todolist instead of a wall of
|
|
17
|
+
* monospace text. Unclosed fences during streaming parse the same way, so
|
|
18
|
+
* steps appear progressively as tokens arrive.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface PlanItem {
|
|
22
|
+
text: string;
|
|
23
|
+
done: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ParsedPlan {
|
|
27
|
+
title: string | null;
|
|
28
|
+
items: PlanItem[];
|
|
29
|
+
notes: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CHECKBOX_RE = /^(?:[-*]\s*)?\[([ xX])\]\s*(.*)$/;
|
|
33
|
+
const BULLET_RE = /^[-*]\s+(.*)$/;
|
|
34
|
+
const NUMBERED_RE = /^\d+[.、)]\s*(.*)$/;
|
|
35
|
+
|
|
36
|
+
export function parsePlanContent(raw: string): ParsedPlan {
|
|
37
|
+
const plan: ParsedPlan = { title: null, items: [], notes: [] };
|
|
38
|
+
for (const line of raw.split('\n')) {
|
|
39
|
+
const trimmed = line.trim();
|
|
40
|
+
if (!trimmed) continue;
|
|
41
|
+
|
|
42
|
+
if (trimmed.startsWith('#')) {
|
|
43
|
+
const title = trimmed.replace(/^#+\s*/, '');
|
|
44
|
+
if (title && plan.title === null) plan.title = title;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const checkbox = trimmed.match(CHECKBOX_RE);
|
|
49
|
+
if (checkbox) {
|
|
50
|
+
const text = checkbox[2].trim();
|
|
51
|
+
if (text) plan.items.push({ text, done: checkbox[1] !== ' ' });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const bullet = trimmed.match(BULLET_RE) ?? trimmed.match(NUMBERED_RE);
|
|
56
|
+
if (bullet) {
|
|
57
|
+
const text = bullet[1].trim();
|
|
58
|
+
if (text) plan.items.push({ text, done: false });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
plan.notes.push(trimmed);
|
|
63
|
+
}
|
|
64
|
+
return plan;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function PlanTodoList({ content }: { content: string }) {
|
|
68
|
+
const { title, items, notes } = parsePlanContent(content);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className="my-2 overflow-hidden rounded-agent-lg border border-amber-400/50 bg-amber-400/5 not-italic dark:border-amber-500/30"
|
|
73
|
+
data-testid="plan-todo-list"
|
|
74
|
+
>
|
|
75
|
+
<div className="flex items-center gap-2 border-b border-amber-400/30 bg-amber-400/10 px-3 py-2 dark:border-amber-500/20">
|
|
76
|
+
<LuListChecks className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
77
|
+
<span className="flex-1 truncate text-[13px] font-semibold text-amber-800 dark:text-amber-200">
|
|
78
|
+
{title || '执行计划'}
|
|
79
|
+
</span>
|
|
80
|
+
{items.length > 0 && (
|
|
81
|
+
<span className="shrink-0 rounded-full bg-amber-400/20 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:text-amber-300">
|
|
82
|
+
{items.filter((i) => i.done).length}/{items.length}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
{items.length > 0 && (
|
|
87
|
+
<ol className="m-0 list-none space-y-1 px-3 py-2">
|
|
88
|
+
{items.map((item, idx) => (
|
|
89
|
+
<li key={idx} className="flex items-start gap-2 text-[13px] leading-relaxed">
|
|
90
|
+
{item.done ? (
|
|
91
|
+
<LuCircleCheck className="mt-[3px] h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
92
|
+
) : (
|
|
93
|
+
<LuCircle className="mt-[3px] h-3.5 w-3.5 shrink-0 text-amber-500/60" />
|
|
94
|
+
)}
|
|
95
|
+
<span
|
|
96
|
+
className={
|
|
97
|
+
item.done
|
|
98
|
+
? 'text-agent-muted-foreground line-through'
|
|
99
|
+
: 'text-agent-foreground'
|
|
100
|
+
}
|
|
101
|
+
>
|
|
102
|
+
{item.text}
|
|
103
|
+
</span>
|
|
104
|
+
</li>
|
|
105
|
+
))}
|
|
106
|
+
</ol>
|
|
107
|
+
)}
|
|
108
|
+
{notes.length > 0 && (
|
|
109
|
+
<div className="border-t border-amber-400/20 px-3 py-1.5 text-[12px] text-agent-muted-foreground">
|
|
110
|
+
{notes.map((note, idx) => (
|
|
111
|
+
<p key={idx} className="my-0.5">
|
|
112
|
+
{note}
|
|
113
|
+
</p>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default PlanTodoList;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W6-5 项目信任门控的 UI 入口。
|
|
3
|
+
*
|
|
4
|
+
* 项目目录里的 `AGENTS.md` / `CLAUDE.md` 等规则文件是「项目作者写给 agent
|
|
5
|
+
* 的指令」,属于不可信输入——打开恶意仓库时一段构造的规则就能劫持 agent。
|
|
6
|
+
* 因此规则文件只在用户显式信任该项目后才注入模型上下文。本横幅在「会话绑定
|
|
7
|
+
* 了项目、项目里有规则文件、但尚未信任」时提示用户授权;已信任时提供一个
|
|
8
|
+
* 低调的「撤销」入口。信任状态持久化在后端(agent-projects.json)。
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
11
|
+
import { LuShieldAlert, LuShieldCheck } from 'react-icons/lu';
|
|
12
|
+
import type { LocalProject } from '@/lib/local-api';
|
|
13
|
+
import { getChatProjectContext, setProjectTrusted } from '@/lib/local-api';
|
|
14
|
+
|
|
15
|
+
export interface ProjectTrustBannerProps {
|
|
16
|
+
chatId: string;
|
|
17
|
+
project: LocalProject | null;
|
|
18
|
+
/** 信任状态变化后回调(重新拉项目列表,让徽章/设置同步)。 */
|
|
19
|
+
onTrustChanged?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProjectTrustBanner({ chatId, project, onTrustChanged }: ProjectTrustBannerProps) {
|
|
23
|
+
const [ruleFileCount, setRuleFileCount] = useState(0);
|
|
24
|
+
const [busy, setBusy] = useState(false);
|
|
25
|
+
|
|
26
|
+
const refresh = useCallback(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const res = await getChatProjectContext(chatId);
|
|
29
|
+
setRuleFileCount(res.ruleFileCount ?? 0);
|
|
30
|
+
} catch {
|
|
31
|
+
setRuleFileCount(0);
|
|
32
|
+
}
|
|
33
|
+
}, [chatId]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setRuleFileCount(0);
|
|
37
|
+
if (project) void refresh();
|
|
38
|
+
}, [project, refresh]);
|
|
39
|
+
|
|
40
|
+
if (!project || ruleFileCount === 0) return null;
|
|
41
|
+
const trusted = project.trusted === true;
|
|
42
|
+
|
|
43
|
+
const toggle = async () => {
|
|
44
|
+
setBusy(true);
|
|
45
|
+
try {
|
|
46
|
+
await setProjectTrusted(project.id, !trusted);
|
|
47
|
+
onTrustChanged?.();
|
|
48
|
+
} finally {
|
|
49
|
+
setBusy(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (!trusted) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="mx-3 mb-1 flex items-center justify-between gap-3 rounded-agent-md border border-sky-400/40 bg-sky-400/10 px-3 py-2 text-xs">
|
|
56
|
+
<div className="flex items-center gap-2 text-sky-700 dark:text-sky-300">
|
|
57
|
+
<LuShieldAlert className="h-4 w-4 shrink-0" />
|
|
58
|
+
<span>
|
|
59
|
+
项目「{project.name}」包含 {ruleFileCount} 个规则文件(AGENTS.md / CLAUDE.md)。
|
|
60
|
+
信任前这些项目方指令<strong>不会</strong>注入模型。
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
disabled={busy}
|
|
66
|
+
onClick={() => void toggle()}
|
|
67
|
+
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-agent-foreground px-3 py-1 font-medium text-agent-canvas transition hover:opacity-90 disabled:opacity-50"
|
|
68
|
+
>
|
|
69
|
+
信任并加载
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="mx-3 mb-1 flex items-center justify-between gap-3 rounded-agent-md border border-agent-border bg-agent-muted/30 px-3 py-1.5 text-[11px] text-agent-muted-foreground">
|
|
77
|
+
<div className="flex items-center gap-1.5">
|
|
78
|
+
<LuShieldCheck className="h-3.5 w-3.5 shrink-0 text-emerald-500" />
|
|
79
|
+
<span>已加载项目「{project.name}」的 {ruleFileCount} 个规则文件</span>
|
|
80
|
+
</div>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
disabled={busy}
|
|
84
|
+
onClick={() => void toggle()}
|
|
85
|
+
className="shrink-0 rounded px-1.5 py-0.5 transition-colors hover:text-agent-destructive disabled:opacity-50"
|
|
86
|
+
>
|
|
87
|
+
撤销信任
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|