@steerable/agent-shell-web 0.7.4 → 0.8.1
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/components/LocalLlmSettingsModal.tsx +40 -3
- package/src/components/chat/AssistantMessage.tsx +252 -15
- package/src/components/chat/LocalChatPanel.tsx +10 -0
- package/src/components/chat/MessageList.tsx +21 -0
- package/src/components/chat/RetryStatus.tsx +77 -0
- package/src/components/chat/StreamingStatus.tsx +9 -0
- package/src/components/chat/TurnFailureCard.tsx +272 -0
- package/src/components/chat/turn-failure.ts +185 -0
- package/src/components/settings/DiagnoseSettingsPanel.tsx +113 -2
- package/src/components/settings/LlmSettingsPanel.tsx +18 -0
- package/src/lib/local-api.ts +18 -0
- package/src/pages/AgentPage.tsx +100 -0
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "@/lib/chat-transport";
|
|
11
11
|
import { ChatHeader } from "@/components/ChatHeader";
|
|
12
12
|
import { EmptyChatHero } from "@/components/chat/EmptyChatHero";
|
|
13
|
+
import type { RetryState } from "@/components/chat/RetryStatus";
|
|
14
|
+
import type { FailureAction } from "@/components/chat/turn-failure";
|
|
13
15
|
import { LocalChatPanel } from "@/components/chat/LocalChatPanel";
|
|
14
16
|
import { ModelPicker } from "@/components/chat/ModelPicker";
|
|
15
17
|
import {
|
|
@@ -540,6 +542,11 @@ function AgentChatView({
|
|
|
540
542
|
// before this signal, the UI looked frozen whenever the model paused to
|
|
541
543
|
// wait for tool results.
|
|
542
544
|
const [currentRound, setCurrentRound] = useState(1);
|
|
545
|
+
// Live "reconnecting" state for the in-flight turn. Set by the backend's
|
|
546
|
+
// `turn_retry` notice, cleared when the round ends or the stream settles —
|
|
547
|
+
// a retry that succeeded leaves no trace in the UI or in history.
|
|
548
|
+
const [retryState, setRetryState] = useState<RetryState | null>(null);
|
|
549
|
+
const navigate = useNavigate();
|
|
543
550
|
// Holds the latest in-flight actions for the `message_id` reconciliation
|
|
544
551
|
// — `useState`'s setter sees the latest value via the functional update
|
|
545
552
|
// form, but we keep this ref so the reconciliation event handler can read
|
|
@@ -624,6 +631,21 @@ function AgentChatView({
|
|
|
624
631
|
// 这不是新一轮,跳过计数。
|
|
625
632
|
if (ev.payload?.status === "cancelled") return;
|
|
626
633
|
setCurrentRound((r) => r + 1);
|
|
634
|
+
// A new round means the previous request finished (or was replaced), so
|
|
635
|
+
// any "reconnecting" line belongs to the round that is now over.
|
|
636
|
+
setRetryState(null);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (ev.event === "turn_retry") {
|
|
640
|
+
// The backend announces each retry; the first one starts the clock the
|
|
641
|
+
// status line counts from. Later ones keep it so the count is the whole
|
|
642
|
+
// wait, not just the latest attempt.
|
|
643
|
+
const delayMs =
|
|
644
|
+
typeof ev.payload?.delayMs === "number" ? ev.payload.delayMs : undefined;
|
|
645
|
+
setRetryState((prev) => ({
|
|
646
|
+
startedAt: prev?.startedAt ?? Date.now(),
|
|
647
|
+
...(delayMs !== undefined ? { delayMs } : {}),
|
|
648
|
+
}));
|
|
627
649
|
return;
|
|
628
650
|
}
|
|
629
651
|
if (ev.event === "orchestration_child") {
|
|
@@ -942,6 +964,7 @@ function AgentChatView({
|
|
|
942
964
|
pendingTurnFilesRef.current = [];
|
|
943
965
|
setCurrentTurnFiles([]);
|
|
944
966
|
setCurrentRound(1);
|
|
967
|
+
setRetryState(null);
|
|
945
968
|
const started = Date.now();
|
|
946
969
|
turnStartedAtRef.current = started;
|
|
947
970
|
setCurrentTurnStartedAtMs(started);
|
|
@@ -1059,6 +1082,7 @@ function AgentChatView({
|
|
|
1059
1082
|
pendingTurnFilesRef.current = [];
|
|
1060
1083
|
setCurrentTurnFiles([]);
|
|
1061
1084
|
setCurrentRound(1);
|
|
1085
|
+
setRetryState(null);
|
|
1062
1086
|
const started = Date.now();
|
|
1063
1087
|
turnStartedAtRef.current = started;
|
|
1064
1088
|
setCurrentTurnStartedAtMs(started);
|
|
@@ -1081,6 +1105,63 @@ function AgentChatView({
|
|
|
1081
1105
|
setInterrupted(false);
|
|
1082
1106
|
}, []);
|
|
1083
1107
|
|
|
1108
|
+
// 「新建对话」失败时的就地提示;点一次清一次。
|
|
1109
|
+
const [newChatError, setNewChatError] = useState<string | null>(null);
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* 失败卡片的按钮。和「继续上次回复」走同一条 resume 通道:sidecar 回放
|
|
1113
|
+
* durable record,已完成的工具调用不会再执行一遍——这正是失败后点
|
|
1114
|
+
* 「继续」想要的语义。本轮正在跑时按钮禁用(同一会话不能同时两轮)。
|
|
1115
|
+
*/
|
|
1116
|
+
const failureAction = useCallback(
|
|
1117
|
+
(action: FailureAction) => {
|
|
1118
|
+
switch (action.kind) {
|
|
1119
|
+
case "retry":
|
|
1120
|
+
return {
|
|
1121
|
+
onClick: () => void handleContinueInterrupted(),
|
|
1122
|
+
disabled: isStreaming,
|
|
1123
|
+
hint: isStreaming ? t("This chat is running. Continue when it finishes.") : undefined,
|
|
1124
|
+
};
|
|
1125
|
+
case "network-diagnostics":
|
|
1126
|
+
// 诊断区挂在设置页「通用」下(settingsChrome('diagnose') 控制显隐)。
|
|
1127
|
+
return settingsChrome("diagnose")
|
|
1128
|
+
? { onClick: () => navigate("/settings") }
|
|
1129
|
+
: null;
|
|
1130
|
+
case "open-model-settings":
|
|
1131
|
+
return settingsChrome("llm") && hasHostBridge()
|
|
1132
|
+
? { onClick: () => setLlmSettingsOpen(true) }
|
|
1133
|
+
: null;
|
|
1134
|
+
case "new-chat":
|
|
1135
|
+
// 同项目的空会话:对话过长时的新出路,而不是把用户丢回落地页再
|
|
1136
|
+
// 重选项目、重填输入。失败时提示并留在原地,不吞掉这次点击。
|
|
1137
|
+
return {
|
|
1138
|
+
onClick: () => {
|
|
1139
|
+
void ctx
|
|
1140
|
+
.createChat({
|
|
1141
|
+
...(chat?.projectId ? { projectId: chat.projectId } : {}),
|
|
1142
|
+
...(chat?.agentId ? { agentId: chat.agentId } : {}),
|
|
1143
|
+
})
|
|
1144
|
+
.then((id) => {
|
|
1145
|
+
if (id) navigate(`/agent/${id}`);
|
|
1146
|
+
else setNewChatError(t("Failed to create the chat. Please try again."));
|
|
1147
|
+
})
|
|
1148
|
+
.catch((err: unknown) =>
|
|
1149
|
+
setNewChatError(err instanceof Error ? err.message : String(err)),
|
|
1150
|
+
);
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
[
|
|
1156
|
+
chat?.agentId,
|
|
1157
|
+
chat?.projectId,
|
|
1158
|
+
ctx,
|
|
1159
|
+
handleContinueInterrupted,
|
|
1160
|
+
isStreaming,
|
|
1161
|
+
navigate,
|
|
1162
|
+
],
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1084
1165
|
// 轮中插队 + 失败兜底(W6-2):streaming 期间 ⌘/Ctrl+Enter 优先注入运行中的
|
|
1085
1166
|
// CoreLoop 回合;注入不了时 hook 按回合实况降级——仍在跑则排入待发队列
|
|
1086
1167
|
// ('queued',ChatInput 提示"已改为排队"),恰好已结束则作为新回合直发
|
|
@@ -1322,6 +1403,8 @@ function AgentChatView({
|
|
|
1322
1403
|
currentTurnChildren={effectiveCurrentTurnChildren}
|
|
1323
1404
|
orchestrationChildrenByMessageId={orchestrationChildrenByMessageId}
|
|
1324
1405
|
currentRound={currentRound}
|
|
1406
|
+
retryState={retryState}
|
|
1407
|
+
failureAction={failureAction}
|
|
1325
1408
|
interrupted={interrupted}
|
|
1326
1409
|
onContinueInterrupted={handleContinueInterrupted}
|
|
1327
1410
|
onDismissInterrupted={handleDismissInterrupted}
|
|
@@ -1369,6 +1452,23 @@ function AgentChatView({
|
|
|
1369
1452
|
}
|
|
1370
1453
|
inputBanner={
|
|
1371
1454
|
<>
|
|
1455
|
+
{newChatError ? (
|
|
1456
|
+
<div
|
|
1457
|
+
role="status"
|
|
1458
|
+
data-testid="new-chat-error"
|
|
1459
|
+
className="mb-1 flex items-center justify-between gap-2 rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 px-2.5 py-1.5 text-xs text-agent-destructive"
|
|
1460
|
+
>
|
|
1461
|
+
<span>{newChatError}</span>
|
|
1462
|
+
<button
|
|
1463
|
+
type="button"
|
|
1464
|
+
onClick={() => setNewChatError(null)}
|
|
1465
|
+
className="rounded px-1 hover:bg-agent-destructive/10"
|
|
1466
|
+
aria-label={t("Dismiss")}
|
|
1467
|
+
>
|
|
1468
|
+
✕
|
|
1469
|
+
</button>
|
|
1470
|
+
</div>
|
|
1471
|
+
) : null}
|
|
1372
1472
|
<GoalLoopStatusBar
|
|
1373
1473
|
chatId={chatId}
|
|
1374
1474
|
goal={goal}
|