@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.
- package/package.json +4 -4
- package/src/brand.ts +7 -0
- package/src/components/AgentSidebar.tsx +284 -188
- package/src/components/ConfirmDialog.tsx +90 -0
- package/src/components/CreateProjectModal.tsx +32 -16
- package/src/components/chat/ApprovalModal.tsx +2 -5
- package/src/components/chat/ApprovalPromptProvider.tsx +38 -22
- package/src/components/chat/AskUserPromptProvider.tsx +1 -32
- package/src/components/chat/ChatInput.tsx +47 -57
- package/src/components/chat/ChatProjectBadge.tsx +1 -1
- package/src/components/chat/EmptyChatHero.tsx +22 -0
- package/src/components/chat/ExecPolicyPicker.tsx +2 -2
- package/src/components/chat/ExecutedActionsCard.tsx +23 -13
- package/src/components/chat/LocalChatPanel.tsx +61 -18
- package/src/components/chat/MessageList.tsx +77 -5
- package/src/components/chat/ModelPicker.tsx +21 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +484 -0
- package/src/components/chat/SessionOrchestrationFlow.tsx +573 -0
- package/src/components/chat/SessionTodoList.tsx +12 -3
- package/src/components/chat/UserMessage.tsx +116 -25
- package/src/components/chat/composer-status-stack.tsx +79 -0
- package/src/components/chat/orchestration-flow-model.ts +401 -0
- package/src/components/chat/prompt-chat-scope.ts +33 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +33 -22
- package/src/components/settings/McpSettingsPanel.tsx +1 -1
- package/src/components/settings/OrchestrationSettingsPanel.tsx +67 -0
- package/src/components/settings/SettingsNavMenu.tsx +55 -0
- package/src/components/settings/SkillsSettingsPanel.tsx +1 -1
- package/src/hooks/useProjects.ts +82 -0
- package/src/lib/electron-bridge.ts +1 -0
- package/src/lib/exec-policy.ts +1 -1
- package/src/lib/local-api.ts +1 -1
- package/src/lib/orchestration-settings.ts +58 -0
- package/src/pages/AgentPage.tsx +116 -64
- package/src/pages/SettingsPage.tsx +338 -55
- package/src/vite-env.d.ts +2 -0
- package/vite.base.js +1 -0
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
728
|
-
//
|
|
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
|
|
755
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
954
|
-
|
|
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 期间
|
|
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
|
-
|
|
1009
|
+
updateRemoteFollowUps((current) => [...current, text]);
|
|
964
1010
|
return;
|
|
965
1011
|
}
|
|
966
1012
|
followUpUserMessage({ content: text });
|
|
967
1013
|
},
|
|
968
|
-
[remoteStreaming,
|
|
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={
|
|
1105
|
-
onRemoveFollowUp={
|
|
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 =
|
|
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
|
-
<
|
|
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
|
}
|