@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
@@ -9,6 +9,7 @@ import {
9
9
  regenerateChatMessage,
10
10
  } from "@/lib/chat-transport";
11
11
  import { ChatHeader } from "@/components/ChatHeader";
12
+ import { EmptyChatHero } from "@/components/chat/EmptyChatHero";
12
13
  import { LocalChatPanel } from "@/components/chat/LocalChatPanel";
13
14
  import { ModelPicker } from "@/components/chat/ModelPicker";
14
15
  import {
@@ -51,17 +52,17 @@ import {
51
52
  import { ProjectTrustBanner } from "@/components/chat/ProjectTrustBanner";
52
53
  import {
53
54
  getChatLiveStream,
54
- listProjects,
55
55
  type ChatLiveStream,
56
56
  type LocalChatMessage,
57
- type LocalProject,
58
57
  } from "@/lib/local-api";
58
+ import { useProjects } from "@/hooks/useProjects";
59
59
  import type { AgentOutletContext } from "@/layouts/AgentLayout";
60
60
  import {
61
61
  setPendingFirstMessage,
62
62
  takePendingFirstMessage,
63
63
  } from "@/lib/pending-first-message";
64
- import { BRAND_NAME, pickDefaultAgentId } from "@/brand";
64
+ import { isOrchestrationSettingEnabled } from "@/lib/orchestration-settings";
65
+ import { pickDefaultAgentId } from "@/brand";
65
66
  import {
66
67
  appendAttachmentRefs,
67
68
  collectImageAttachments,
@@ -379,20 +380,8 @@ function AgentChatView({
379
380
 
380
381
  // 当前会话绑定的项目 — Codex 式 cwd 指示:输入框上方显示项目名徽章,
381
382
  // 点击可关联到其他项目 / 修改项目目录 / 移出项目。无项目会话不显示。
382
- const [projects, setProjects] = useState<LocalProject[]>([]);
383
383
  const showProjectsChrome = hostToolChrome("projects");
384
- const fetchProjects = useCallback(async () => {
385
- if (!isElectron() || !hostToolChrome("projects")) return;
386
- try {
387
- const res = await listProjects();
388
- setProjects(res.projects ?? []);
389
- } catch {
390
- /* 列表失败保持旧数据 */
391
- }
392
- }, []);
393
- useEffect(() => {
394
- void fetchProjects();
395
- }, [fetchProjects]);
384
+ const { projects, refresh: fetchProjects } = useProjects();
396
385
  const chatProject = chat?.projectId
397
386
  ? (projects.find((p) => p.id === chat.projectId) ?? null)
398
387
  : null;
@@ -694,6 +683,7 @@ function AgentChatView({
694
683
  pendingFollowUps,
695
684
  removeFollowUp,
696
685
  cancel,
686
+ setMessages,
697
687
  appendMessage,
698
688
  } = useChatStream({
699
689
  transport,
@@ -707,6 +697,15 @@ function AgentChatView({
707
697
  // 新 mount 没发起流,所以 isStreaming=false,但后端快照 liveStream.active
708
698
  // 为 true —— 此时用快照叠出「正在运行」的助手气泡 + 部分工具卡片/时间线。
709
699
  const remoteStreaming = liveStream.active === true && !isStreaming;
700
+ // 切回运行中的远端回合时,本 mount 没有 useChatStream 的本地队列。显式
701
+ // 保留 Enter 排队的消息,等远端回合结束后再由本 mount 依次发出。
702
+ const [remoteFollowUps, setRemoteFollowUps] = useState<string[]>([]);
703
+ const remoteFollowUpsRef = useRef<string[]>([]);
704
+ const updateRemoteFollowUps = useCallback((update: (current: string[]) => string[]) => {
705
+ const next = update(remoteFollowUpsRef.current);
706
+ remoteFollowUpsRef.current = next;
707
+ setRemoteFollowUps(next);
708
+ }, []);
710
709
 
711
710
  // 远端轮中转向:transport.steer 按 chatId 找活跃流,天然支持「切回后注入」
712
711
  // ——不像 useChatStream.steerOrFollowUpUserMessage 那样被本地 isStreamingRef
@@ -724,8 +723,8 @@ function AgentChatView({
724
723
  });
725
724
  return "steered";
726
725
  }
727
- // 回合可能恰好已结束——回落 queued 让 ChatInput 给出提示,紧跟着的
728
- // re-hydrate 会把最终消息拉出来。
726
+ // 回合可能恰好已结束——回落 queued,由 handleSteer 放进远端待发
727
+ // 队列;轮询确认回合结束后先对账历史,再启动下一轮。
729
728
  return "queued";
730
729
  },
731
730
  [transport, appendMessage],
@@ -751,16 +750,9 @@ function AgentChatView({
751
750
  };
752
751
  }, [chatId, remoteStreaming]);
753
752
 
754
- // 快照从 active=true → false 的边沿:远端回合结束了,re-hydrate 出最终
755
- // 落库消息(含真实 message id / executedActions / timeline)。
753
+ // 快照从 active=true → false 的边沿在 handleSubmit 声明后处理:没有排队
754
+ // 消息时重新水合,有排队消息时直接启动下一轮。
756
755
  const wasLiveActiveRef = useRef(liveStream.active === true);
757
- useEffect(() => {
758
- const nowActive = liveStream.active === true;
759
- if (wasLiveActiveRef.current && !nowActive) {
760
- onBranchTick();
761
- }
762
- wasLiveActiveRef.current = nowActive;
763
- }, [liveStream.active, onBranchTick]);
764
756
 
765
757
  // W7-1: 中断提示卡的可见性。初值来自 messages 响应的 interrupted 标记;
766
758
  // 点击「继续」/「忽略」或用户手动发起新一轮时本地清除(继续成功后后端的
@@ -820,7 +812,8 @@ function AgentChatView({
820
812
  const handleCancel = useCallback(() => {
821
813
  transport.cancelActive();
822
814
  cancel();
823
- }, [transport, cancel]);
815
+ updateRemoteFollowUps(() => []);
816
+ }, [transport, cancel, updateRemoteFollowUps]);
824
817
 
825
818
  // Reset the in-flight queue on every new user submit. We do this in a
826
819
  // wrapper rather than directly in `handleUnknownEvent`, because there's no
@@ -869,6 +862,7 @@ function AgentChatView({
869
862
  ...(modelOverride ? { model: modelOverride } : {}),
870
863
  ...(effortOverride ? { reasoningEffort: effortOverride } : {}),
871
864
  ...(execPolicy === "full" ? { execPolicy: "full" } : {}),
865
+ orchestration: isOrchestrationSettingEnabled(),
872
866
  };
873
867
  return sendUserMessage({
874
868
  ...input,
@@ -885,6 +879,48 @@ function AgentChatView({
885
879
  [handleSubmit],
886
880
  );
887
881
 
882
+ // 远端回合结束时,优先发送用户在等待期间排队的内容。第一条启动新回合,
883
+ // 其余条目进入 useChatStream 的本地队列;没有排队内容才重新水合历史。
884
+ useEffect(() => {
885
+ const nowActive = liveStream.active === true;
886
+ if (wasLiveActiveRef.current && !nowActive) {
887
+ const queued = remoteFollowUpsRef.current;
888
+ if (queued.length > 0) {
889
+ updateRemoteFollowUps(() => []);
890
+ void (async () => {
891
+ try {
892
+ const response = await getElectronBridge()?.localBackend.request<{
893
+ messages?: ChatMessageWithMetadata[];
894
+ }>({
895
+ method: "GET",
896
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/messages?limit=200`,
897
+ });
898
+ if (response?.messages) {
899
+ setMessages(chronological(response.messages));
900
+ }
901
+ } catch {
902
+ // 排队内容仍须发出;后续刷新会补齐刚结束的远端回复。
903
+ }
904
+ void handleSubmit({ content: queued[0] });
905
+ for (const content of queued.slice(1)) {
906
+ followUpUserMessage({ content });
907
+ }
908
+ })();
909
+ } else {
910
+ onBranchTick();
911
+ }
912
+ }
913
+ wasLiveActiveRef.current = nowActive;
914
+ }, [
915
+ followUpUserMessage,
916
+ chatId,
917
+ handleSubmit,
918
+ liveStream.active,
919
+ onBranchTick,
920
+ setMessages,
921
+ updateRemoteFollowUps,
922
+ ]);
923
+
888
924
  useEffect(() => {
889
925
  const bridge = getElectronBridge();
890
926
  if (!bridge?.onSuggestedReplies) return;
@@ -944,28 +980,38 @@ function AgentChatView({
944
980
  setInterrupted(false);
945
981
  }, []);
946
982
 
947
- // 轮中转向 + 失败兜底(W6-2):streaming 期间 Enter 优先注入运行中的
983
+ // 轮中插队 + 失败兜底(W6-2):streaming 期间 ⌘/Ctrl+Enter 优先注入运行中的
948
984
  // CoreLoop 回合;注入不了时 hook 按回合实况降级——仍在跑则排入待发队列
949
985
  // ('queued',ChatInput 提示"已改为排队"),恰好已结束则作为新回合直发
950
986
  // ('sent')。消息不会丢,ChatInput 对任何结果都清草稿。
951
987
  const handleSteer = useCallback(
952
- (text: string) =>
953
- remoteStreaming ? steerRemote(text) : steerOrFollowUpUserMessage(text),
954
- [remoteStreaming, steerRemote, steerOrFollowUpUserMessage],
988
+ async (text: string) => {
989
+ if (!remoteStreaming) return steerOrFollowUpUserMessage(text);
990
+ const outcome = await steerRemote(text);
991
+ if (outcome === "queued") {
992
+ updateRemoteFollowUps((current) => [...current, text]);
993
+ }
994
+ return outcome;
995
+ },
996
+ [
997
+ remoteStreaming,
998
+ steerRemote,
999
+ steerOrFollowUpUserMessage,
1000
+ updateRemoteFollowUps,
1001
+ ],
955
1002
  );
956
1003
 
957
- // follow-up 排队(W6-2):streaming 期间 ⌘/Ctrl+Enter 把文本排入待发
958
- // 队列,本轮结束后自动作为下一轮发出。与转向(注入当前回合)相对。
959
- // 切回后的远端回合没有本地 follow-up 队列可挂,降级为注入当前回合。
1004
+ // follow-up 排队(W6-2):streaming 期间 Enter 把文本排入待发队列,
1005
+ // 本轮结束后自动作为下一轮发出。与 ⌘/Ctrl+Enter 插队相对。
960
1006
  const handleFollowUp = useCallback(
961
1007
  (text: string) => {
962
1008
  if (remoteStreaming) {
963
- void steerRemote(text);
1009
+ updateRemoteFollowUps((current) => [...current, text]);
964
1010
  return;
965
1011
  }
966
1012
  followUpUserMessage({ content: text });
967
1013
  },
968
- [remoteStreaming, steerRemote, followUpUserMessage],
1014
+ [remoteStreaming, followUpUserMessage, updateRemoteFollowUps],
969
1015
  );
970
1016
 
971
1017
  // 首页输入框接力:EmptyChatGate 建好 chat 并跳转后,首条消息暂存在
@@ -1051,6 +1097,10 @@ function AgentChatView({
1051
1097
  }),
1052
1098
  );
1053
1099
  return () => {
1100
+ // 切走不取消回合。正在流式时卸载不要把侧栏指示清掉,侧栏会轮询
1101
+ // live-stream,等回合结束后再摘。本页确认已结束时,下一次 effect
1102
+ // 会带 isStreaming:false。
1103
+ if (effectiveIsStreaming) return;
1054
1104
  window.dispatchEvent(
1055
1105
  new CustomEvent("chat:streaming-change", {
1056
1106
  detail: { chatId, isStreaming: false },
@@ -1085,6 +1135,20 @@ function AgentChatView({
1085
1135
  suggestedReplies.messageId === tailAssistantDbId)
1086
1136
  ? suggestedReplies.suggestions
1087
1137
  : undefined;
1138
+ const visiblePendingFollowUps = [
1139
+ ...pendingFollowUps.map((message) => message.content),
1140
+ ...remoteFollowUps,
1141
+ ];
1142
+ const handleRemovePendingFollowUp = (index: number) => {
1143
+ if (index < pendingFollowUps.length) {
1144
+ removeFollowUp(index);
1145
+ return;
1146
+ }
1147
+ const remoteIndex = index - pendingFollowUps.length;
1148
+ updateRemoteFollowUps((current) =>
1149
+ current.filter((_, itemIndex) => itemIndex !== remoteIndex),
1150
+ );
1151
+ };
1088
1152
 
1089
1153
  return (
1090
1154
  <div className="flex h-full w-full flex-col">
@@ -1101,13 +1165,10 @@ function AgentChatView({
1101
1165
  onCancel={handleCancel}
1102
1166
  onSteer={handleSteer}
1103
1167
  onFollowUp={handleFollowUp}
1104
- pendingFollowUps={pendingFollowUps.map((m) => m.content)}
1105
- onRemoveFollowUp={removeFollowUp}
1168
+ pendingFollowUps={visiblePendingFollowUps}
1169
+ onRemoveFollowUp={handleRemovePendingFollowUp}
1106
1170
  className="flex-1"
1107
- emptyHero={{
1108
- title: BRAND_NAME,
1109
- subtitle: '输入消息,直接开始一段新对话。',
1110
- }}
1171
+ emptyHero
1111
1172
  header={
1112
1173
  <ChatHeader
1113
1174
  chat={chat}
@@ -1169,6 +1230,11 @@ function AgentChatView({
1169
1230
  reasoningEffort={effortOverride}
1170
1231
  onSelectModel={setModelOverride}
1171
1232
  onSelectEffort={setEffortOverride}
1233
+ onOpenSettings={
1234
+ isElectron() && settingsChrome("llm")
1235
+ ? () => setLlmSettingsOpen(true)
1236
+ : undefined
1237
+ }
1172
1238
  disabled={isStreaming}
1173
1239
  />
1174
1240
  }
@@ -1273,7 +1339,6 @@ function EmptyChatGate() {
1273
1339
 
1274
1340
  // 落地页项目选择:还没有 chat 可绑,选中的 projectId 在首次提交
1275
1341
  // createChat 时一并传入。侧栏项目组「+」会带 ?projectId= 预选。
1276
- const [projects, setProjects] = useState<LocalProject[]>([]);
1277
1342
  const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
1278
1343
  projectIdFromUrl,
1279
1344
  );
@@ -1281,18 +1346,7 @@ function EmptyChatGate() {
1281
1346
  setSelectedProjectId(projectIdFromUrl);
1282
1347
  }, [projectIdFromUrl]);
1283
1348
  const showProjectsChrome = hostToolChrome("projects");
1284
- const fetchProjects = useCallback(async () => {
1285
- if (!isElectron() || !hostToolChrome("projects")) return;
1286
- try {
1287
- const res = await listProjects();
1288
- setProjects(res.projects ?? []);
1289
- } catch {
1290
- /* 列表失败保持旧数据 */
1291
- }
1292
- }, []);
1293
- useEffect(() => {
1294
- void fetchProjects();
1295
- }, [fetchProjects]);
1349
+ const { projects, refresh: fetchProjects } = useProjects();
1296
1350
 
1297
1351
  useEffect(() => {
1298
1352
  inputRef.current?.focusAtEnd();
@@ -1402,14 +1456,7 @@ function EmptyChatGate() {
1402
1456
  data-testid="empty-chat-home"
1403
1457
  >
1404
1458
  <div className="flex w-full max-w-3xl flex-col items-center gap-4">
1405
- <div className="text-center">
1406
- <h1 className="text-xl font-semibold tracking-tight text-agent-foreground">
1407
- {BRAND_NAME}
1408
- </h1>
1409
- <p className="mt-1.5 text-xs text-agent-muted-foreground">
1410
- 输入消息,直接开始一段新对话。
1411
- </p>
1412
- </div>
1459
+ <EmptyChatHero />
1413
1460
  <div className="w-full">
1414
1461
  <ChatInput
1415
1462
  ref={inputRef}
@@ -1438,6 +1485,11 @@ function EmptyChatGate() {
1438
1485
  reasoningEffort={effortOverride}
1439
1486
  onSelectModel={setModelOverride}
1440
1487
  onSelectEffort={setEffortOverride}
1488
+ onOpenSettings={
1489
+ isElectron() && settingsChrome("llm")
1490
+ ? () => setLlmSettingsOpen(true)
1491
+ : undefined
1492
+ }
1441
1493
  disabled={!isElectron() || isCreating}
1442
1494
  />
1443
1495
  }