@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.
@@ -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}