@steerable/agent-shell-web 0.6.50 → 0.6.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +4 -4
  2. package/src/brand.ts +7 -0
  3. package/src/components/AgentSidebar.tsx +284 -188
  4. package/src/components/ConfirmDialog.tsx +90 -0
  5. package/src/components/CreateProjectModal.tsx +32 -16
  6. package/src/components/chat/ApprovalModal.tsx +2 -5
  7. package/src/components/chat/ApprovalPromptProvider.tsx +38 -22
  8. package/src/components/chat/AskUserPromptProvider.tsx +1 -32
  9. package/src/components/chat/ChatInput.tsx +47 -57
  10. package/src/components/chat/ChatProjectBadge.tsx +1 -1
  11. package/src/components/chat/EmptyChatHero.tsx +22 -0
  12. package/src/components/chat/ExecPolicyPicker.tsx +2 -2
  13. package/src/components/chat/ExecutedActionsCard.tsx +23 -13
  14. package/src/components/chat/LocalChatPanel.tsx +61 -18
  15. package/src/components/chat/MessageList.tsx +77 -5
  16. package/src/components/chat/ModelPicker.tsx +21 -1
  17. package/src/components/chat/OrchestrationFlowCard.tsx +484 -0
  18. package/src/components/chat/SessionOrchestrationFlow.tsx +573 -0
  19. package/src/components/chat/SessionTodoList.tsx +12 -3
  20. package/src/components/chat/UserMessage.tsx +116 -25
  21. package/src/components/chat/composer-status-stack.tsx +79 -0
  22. package/src/components/chat/orchestration-flow-model.ts +401 -0
  23. package/src/components/chat/prompt-chat-scope.ts +33 -0
  24. package/src/components/settings/AgentsSettingsPanel.tsx +33 -22
  25. package/src/components/settings/McpSettingsPanel.tsx +1 -1
  26. package/src/components/settings/OrchestrationSettingsPanel.tsx +67 -0
  27. package/src/components/settings/SettingsNavMenu.tsx +55 -0
  28. package/src/components/settings/SkillsSettingsPanel.tsx +1 -1
  29. package/src/hooks/useProjects.ts +82 -0
  30. package/src/lib/electron-bridge.ts +1 -0
  31. package/src/lib/exec-policy.ts +1 -1
  32. package/src/lib/local-api.ts +1 -1
  33. package/src/lib/orchestration-settings.ts +58 -0
  34. package/src/pages/AgentPage.tsx +116 -64
  35. package/src/pages/SettingsPage.tsx +338 -55
  36. package/src/vite-env.d.ts +2 -0
  37. package/vite.base.js +1 -0
@@ -5,6 +5,7 @@ import {
5
5
  LuPlus,
6
6
  LuTrash2,
7
7
  } from 'react-icons/lu';
8
+ import { ConfirmDialog } from '@/components/ConfirmDialog';
8
9
  import { isElectron } from '@/lib/electron-bridge';
9
10
  import {
10
11
  archiveChatAgent,
@@ -22,7 +23,7 @@ import {
22
23
  /**
23
24
  * AgentsSettingsPanel — 智能体目录(列表 / 新建 / 编辑 / 归档)。
24
25
  *
25
- * 渲染在 `/settings?section=agents` 独立页。自定义智能体出现在输入框
26
+ * 渲染在 `/settings?section=plugins` 的智能体分类。自定义智能体出现在输入框
26
27
  * 上方的专家选择器里。内置智能体可改文案与能力,不能归档。
27
28
  *
28
29
  * 除文案外,每个智能体还持有真实生效的能力面(勾选技能 / 技能范围 /
@@ -91,7 +92,10 @@ export function AgentsSettingsPanel({
91
92
  const [capability, setCapability] = useState<AgentCapabilityDraft>(
92
93
  DEFAULT_CAPABILITY_DRAFT,
93
94
  );
94
- const [confirmArchiveId, setConfirmArchiveId] = useState<string | null>(null);
95
+ const [pendingArchive, setPendingArchive] = useState<{
96
+ id: string;
97
+ name: string;
98
+ } | null>(null);
95
99
  const [archivingId, setArchivingId] = useState<string | null>(null);
96
100
  const [error, setError] = useState<string | null>(null);
97
101
 
@@ -179,17 +183,14 @@ export function AgentsSettingsPanel({
179
183
  }
180
184
  };
181
185
 
182
- const handleArchive = async (agent: LocalChatAgent) => {
183
- if (agent.isBuiltin) return;
184
- if (confirmArchiveId !== agent.id) {
185
- setConfirmArchiveId(agent.id);
186
- return;
187
- }
188
- setArchivingId(agent.id);
186
+ const handleArchive = async () => {
187
+ if (!pendingArchive || archivingId === pendingArchive.id) return;
188
+ const id = pendingArchive.id;
189
+ setArchivingId(id);
189
190
  setError(null);
190
191
  try {
191
- await archiveChatAgent(agent.id);
192
- setConfirmArchiveId(null);
192
+ await archiveChatAgent(id);
193
+ setPendingArchive(null);
193
194
  await fetchAgents();
194
195
  await onCatalogChange?.();
195
196
  } catch (err) {
@@ -371,18 +372,12 @@ export function AgentsSettingsPanel({
371
372
  {!agent.isBuiltin && (
372
373
  <button
373
374
  type="button"
374
- onClick={() => void handleArchive(agent)}
375
- disabled={archivingId === agent.id}
376
- className={`rounded-full p-1 transition-colors ${
377
- confirmArchiveId === agent.id
378
- ? 'bg-agent-destructive/10 text-agent-destructive hover:bg-agent-destructive/20'
379
- : 'text-agent-muted-foreground hover:bg-agent-muted hover:text-agent-destructive'
380
- }`}
381
- title={
382
- confirmArchiveId === agent.id
383
- ? '再次点击确认删除'
384
- : '删除'
375
+ onClick={() =>
376
+ setPendingArchive({ id: agent.id, name: agent.name })
385
377
  }
378
+ disabled={archivingId === agent.id}
379
+ className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-destructive"
380
+ title="删除"
386
381
  data-testid={`agent-archive-${agent.id}`}
387
382
  >
388
383
  {archivingId === agent.id ? (
@@ -407,6 +402,22 @@ export function AgentsSettingsPanel({
407
402
  {error}
408
403
  </div>
409
404
  )}
405
+ <ConfirmDialog
406
+ open={pendingArchive !== null}
407
+ title="删除智能体"
408
+ description={
409
+ pendingArchive
410
+ ? `确定删除「${pendingArchive.name}」?它会从专家选择器中移除,已有会话不受影响。`
411
+ : ''
412
+ }
413
+ pending={pendingArchive !== null && archivingId === pendingArchive.id}
414
+ onCancel={() => {
415
+ if (archivingId) return;
416
+ setPendingArchive(null);
417
+ }}
418
+ onConfirm={() => void handleArchive()}
419
+ testId="agent-archive-dialog"
420
+ />
410
421
  </div>
411
422
  );
412
423
  }
@@ -15,7 +15,7 @@ import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
15
15
  * McpSettingsPanel — MCP 服务管理面板(JSON 导入 / 手动增改 / 启停 / 测试 /
16
16
  * 删除)。
17
17
  *
18
- * 渲染在 `/settings?section=mcp` 独立页(AgentLayout 右侧内容区)。
18
+ * 渲染在 `/settings?section=plugins` 的 MCP 分类(AgentLayout 右侧内容区)。
19
19
  *
20
20
  * 数据与状态完全自管理:挂载时拉一次列表,操作后刷新。后端走
21
21
  * local-backend REST(`/api/v2/mcp/servers*`),注册表持久化在 main 进程的
@@ -0,0 +1,67 @@
1
+ import { LuGitFork } from 'react-icons/lu';
2
+ import { useOrchestrationSetting } from '@/lib/orchestration-settings';
3
+
4
+ /**
5
+ * OrchestrationSettingsPanel — 多智能体协同编排设置面板。
6
+ * 默认开启。用户可手动关闭或再次开启,变更实时持久化并影响后续对话请求。
7
+ */
8
+ export function OrchestrationSettingsPanel() {
9
+ const [enabled, setEnabled] = useOrchestrationSetting();
10
+
11
+ return (
12
+ <div
13
+ className="space-y-2.5 rounded-agent-md border border-agent-border bg-agent-card p-3"
14
+ data-testid="orchestration-settings-panel"
15
+ >
16
+ <div className="flex items-start justify-between gap-3">
17
+ <div className="space-y-1">
18
+ <div className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
19
+ <LuGitFork className="h-3.5 w-3.5 text-violet-600 dark:text-violet-400" />
20
+ <span>多智能体协同编排 (Fork-Join)</span>
21
+ </div>
22
+ <p className="text-xs leading-relaxed text-agent-muted-foreground">
23
+ 允许主智能体通过派生子任务(agent_spawn)并行推理计算,并在输入框上方呈现流程图与实时协同进度看板。
24
+ </p>
25
+ </div>
26
+
27
+ {/* 开关 Radio Group / Toggle */}
28
+ <div
29
+ className="flex shrink-0 rounded-full border border-agent-border bg-agent-canvas p-0.5"
30
+ role="radiogroup"
31
+ aria-label="多智能体协同编排功能开关"
32
+ >
33
+ <button
34
+ type="button"
35
+ role="radio"
36
+ aria-checked={enabled}
37
+ onClick={() => setEnabled(true)}
38
+ className={[
39
+ 'h-6 rounded-full px-2.5 text-[11px] font-medium transition-colors',
40
+ enabled
41
+ ? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 shadow-2xs'
42
+ : 'text-agent-muted-foreground hover:text-agent-foreground',
43
+ ].join(' ')}
44
+ >
45
+ 开启
46
+ </button>
47
+ <button
48
+ type="button"
49
+ role="radio"
50
+ aria-checked={!enabled}
51
+ onClick={() => setEnabled(false)}
52
+ className={[
53
+ 'h-6 rounded-full px-2.5 text-[11px] font-medium transition-colors',
54
+ !enabled
55
+ ? 'bg-agent-foreground/10 text-agent-foreground shadow-2xs'
56
+ : 'text-agent-muted-foreground hover:text-agent-foreground',
57
+ ].join(' ')}
58
+ >
59
+ 关闭
60
+ </button>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export default OrchestrationSettingsPanel;
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+
3
+ export interface SettingsNavItem {
4
+ id: string;
5
+ label: string;
6
+ Icon: React.ComponentType<{ className?: string }>;
7
+ }
8
+
9
+ export function SettingsNavMenu({
10
+ items,
11
+ activeId,
12
+ onSelect,
13
+ }: {
14
+ items: SettingsNavItem[];
15
+ activeId: string | null;
16
+ onSelect: (id: string) => void;
17
+ }) {
18
+ if (items.length === 0) return null;
19
+
20
+ return (
21
+ <aside
22
+ className="sticky top-0 h-fit w-44 shrink-0 py-1 select-none"
23
+ data-testid="settings-side-nav"
24
+ aria-label="设置目录导航"
25
+ >
26
+ <div className="mb-2 px-2.5 text-[11px] font-semibold tracking-wider text-agent-muted-foreground uppercase">
27
+ 设置导航
28
+ </div>
29
+ <nav className="space-y-0.5">
30
+ {items.map((item) => {
31
+ const isActive = activeId === item.id;
32
+ return (
33
+ <button
34
+ key={item.id}
35
+ type="button"
36
+ data-testid={`settings-nav-item-${item.id}`}
37
+ onClick={() => onSelect(item.id)}
38
+ className={[
39
+ 'flex w-full items-center gap-2 rounded-agent-sm px-2.5 py-1.5 text-left text-xs transition-colors',
40
+ isActive
41
+ ? 'bg-agent-foreground/10 font-semibold text-agent-foreground'
42
+ : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
43
+ ].join(' ')}
44
+ >
45
+ <item.Icon className={`h-3.5 w-3.5 shrink-0 ${isActive ? 'text-agent-foreground' : 'text-agent-muted-foreground'}`} />
46
+ <span className="truncate">{item.label}</span>
47
+ </button>
48
+ );
49
+ })}
50
+ </nav>
51
+ </aside>
52
+ );
53
+ }
54
+
55
+ export default SettingsNavMenu;
@@ -11,7 +11,7 @@ import { getPackHiddenSlashSkills } from '@/packs/registry';
11
11
  /**
12
12
  * SkillsSettingsPanel — 本地技能管理面板(导入目录 / 列表 / 卸载)。
13
13
  *
14
- * 渲染在 `/settings?section=skills` 独立页(AgentLayout 右侧内容区)。
14
+ * 渲染在 `/settings?section=plugins` 的 Skills 分类(AgentLayout 右侧内容区)。
15
15
  *
16
16
  * 数据与状态完全自管理:挂载时拉一次列表,操作后刷新。后端走
17
17
  * local-backend REST(`GET/POST /api/v2/chat-agents/skills*`),无专用 IPC。
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 侧栏项目分组和输入框「关联到项目」共用这一份列表。
3
+ * 任一侧新建、改名或改目录后调用 refresh,其它订阅者一起更新。
4
+ */
5
+ import { useCallback, useEffect, useSyncExternalStore } from 'react';
6
+ import { isElectron } from '@/lib/electron-bridge';
7
+ import { hostToolChrome } from '@/lib/host-tools';
8
+ import { listProjects, type LocalProject } from '@/lib/local-api';
9
+
10
+ type ProjectsSnapshot = {
11
+ projects: LocalProject[];
12
+ error: string | null;
13
+ };
14
+
15
+ const EMPTY_SNAPSHOT: ProjectsSnapshot = { projects: [], error: null };
16
+
17
+ let snapshot: ProjectsSnapshot = EMPTY_SNAPSHOT;
18
+ let requestSeq = 0;
19
+ const listeners = new Set<() => void>();
20
+
21
+ function emit() {
22
+ for (const listener of listeners) listener();
23
+ }
24
+
25
+ export function subscribeProjects(listener: () => void): () => void {
26
+ listeners.add(listener);
27
+ return () => {
28
+ listeners.delete(listener);
29
+ };
30
+ }
31
+
32
+ export function getProjectsSnapshot(): ProjectsSnapshot {
33
+ return snapshot;
34
+ }
35
+
36
+ /** 重新拉取项目列表并通知所有订阅者。非 Electron 或关掉项目入口时清空。 */
37
+ export async function refreshProjects(): Promise<void> {
38
+ const seq = ++requestSeq;
39
+ if (!isElectron() || !hostToolChrome('projects')) {
40
+ if (snapshot.projects.length > 0 || snapshot.error) {
41
+ snapshot = EMPTY_SNAPSHOT;
42
+ emit();
43
+ }
44
+ return;
45
+ }
46
+ try {
47
+ const res = await listProjects();
48
+ if (seq !== requestSeq) return;
49
+ snapshot = { projects: res.projects ?? [], error: null };
50
+ emit();
51
+ } catch (err) {
52
+ if (seq !== requestSeq) return;
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ console.error('获取项目列表失败:', err);
55
+ snapshot = { projects: snapshot.projects, error: message };
56
+ emit();
57
+ }
58
+ }
59
+
60
+ export function useProjects(): {
61
+ projects: LocalProject[];
62
+ error: string | null;
63
+ refresh: () => Promise<void>;
64
+ } {
65
+ const current = useSyncExternalStore(
66
+ subscribeProjects,
67
+ getProjectsSnapshot,
68
+ getProjectsSnapshot,
69
+ );
70
+ useEffect(() => {
71
+ void refreshProjects();
72
+ }, []);
73
+ const refresh = useCallback(() => refreshProjects(), []);
74
+ return { projects: current.projects, error: current.error, refresh };
75
+ }
76
+
77
+ /** 测试隔离:丢掉上一个用例留下的列表,并忽略还在飞的请求。 */
78
+ export function resetProjectsStoreForTests(): void {
79
+ requestSeq += 1;
80
+ snapshot = EMPTY_SNAPSHOT;
81
+ emit();
82
+ }
@@ -48,6 +48,7 @@ export interface ApprovalPromptRequest {
48
48
  mode: string;
49
49
  category: string;
50
50
  round: number;
51
+ /** 发起审批的对话。缺省时卡片不绑定会话(旧请求)。 */
51
52
  chatId?: string;
52
53
  }
53
54
 
@@ -2,7 +2,7 @@
2
2
  * 会话级命令执行沙箱(类 Codex 的 sandbox_mode)。
3
3
  *
4
4
  * 作用于下一轮 `chat.stream` 的 `execSandbox`,不是 sidecar 进程沙箱
5
- * (设置页那一层)。工作区 = 只能写入当前项目或本对话工作区;完整权限
5
+ * (设置页那一层)。工作区 = 只能写入项目家目录、源文件夹(含各自子目录)或本对话工作区;完整权限
6
6
  * = 关闭逐条命令沙箱。工具审批(允许/拒绝)仍按原策略生效。
7
7
  */
8
8
 
@@ -387,7 +387,7 @@ export interface LocalProject {
387
387
  id: string;
388
388
  name: string;
389
389
  folderPath: string;
390
- /** 附加源文件夹(只读)。缺省空。 */
390
+ /** 附加源文件夹(可读写,含其子目录)。缺省空。 */
391
391
  sourceFolders?: string[];
392
392
  /** W6-5: 信任后该项目目录里的规则文件才会注入模型上下文。缺省 false。 */
393
393
  trusted?: boolean;
@@ -0,0 +1,58 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const ORCHESTRATION_SETTING_STORAGE_KEY = 'steerable.orchestration.enabled';
4
+
5
+ const CHANGE_EVENT = 'steerable:orchestration-setting-changed';
6
+
7
+ /**
8
+ * 读取多智能体协同编排功能开关,默认开启 (true)。
9
+ */
10
+ export function isOrchestrationSettingEnabled(): boolean {
11
+ if (typeof localStorage === 'undefined') return true;
12
+ const stored = localStorage.getItem(ORCHESTRATION_SETTING_STORAGE_KEY);
13
+ if (stored === null) return true; // 默认开启
14
+ return stored !== 'false' && stored !== '0';
15
+ }
16
+
17
+ /**
18
+ * 持久化多智能体协同编排开关到 localStorage 并广播变更。
19
+ */
20
+ export function persistOrchestrationSetting(enabled: boolean): void {
21
+ if (typeof localStorage === 'undefined') return;
22
+ localStorage.setItem(ORCHESTRATION_SETTING_STORAGE_KEY, enabled ? 'true' : 'false');
23
+ if (typeof window !== 'undefined') {
24
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: enabled }));
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 响应式 hook:订阅并切换编排功能开关。
30
+ */
31
+ export function useOrchestrationSetting(): [boolean, (enabled: boolean) => void] {
32
+ const [enabled, setEnabled] = useState<boolean>(() => isOrchestrationSettingEnabled());
33
+
34
+ useEffect(() => {
35
+ if (typeof window === 'undefined') return;
36
+ const handler = (e: Event) => {
37
+ const custom = e as CustomEvent<boolean>;
38
+ if (typeof custom.detail === 'boolean') {
39
+ setEnabled(custom.detail);
40
+ } else {
41
+ setEnabled(isOrchestrationSettingEnabled());
42
+ }
43
+ };
44
+ window.addEventListener(CHANGE_EVENT, handler);
45
+ window.addEventListener('storage', handler);
46
+ return () => {
47
+ window.removeEventListener(CHANGE_EVENT, handler);
48
+ window.removeEventListener('storage', handler);
49
+ };
50
+ }, []);
51
+
52
+ const update = (next: boolean) => {
53
+ setEnabled(next);
54
+ persistOrchestrationSetting(next);
55
+ };
56
+
57
+ return [enabled, update];
58
+ }