@parhelia/core 0.1.12767 → 0.1.12772

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 (50) hide show
  1. package/dist/components/PageHeader.js +1 -1
  2. package/dist/components/PageHeader.js.map +1 -1
  3. package/dist/components/ui/breadcrumb.d.ts +12 -0
  4. package/dist/components/ui/breadcrumb.js +32 -0
  5. package/dist/components/ui/breadcrumb.js.map +1 -0
  6. package/dist/editor/ContentTree.js +2 -1
  7. package/dist/editor/ContentTree.js.map +1 -1
  8. package/dist/editor/ai/AgentBanners.js +1 -1
  9. package/dist/editor/ai/AgentBanners.js.map +1 -1
  10. package/dist/editor/ai/AgentTerminal.js +243 -50
  11. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  12. package/dist/editor/ai/agentDiagnostics.d.ts +56 -1
  13. package/dist/editor/ai/agentDiagnostics.js +207 -0
  14. package/dist/editor/ai/agentDiagnostics.js.map +1 -1
  15. package/dist/editor/ai/agentDiagnostics.test.d.ts +1 -0
  16. package/dist/editor/ai/agentDiagnostics.test.js +372 -0
  17. package/dist/editor/ai/agentDiagnostics.test.js.map +1 -0
  18. package/dist/editor/ai-image-editor/AiImageEditorDialog.js +5 -2
  19. package/dist/editor/ai-image-editor/AiImageEditorDialog.js.map +1 -1
  20. package/dist/editor/client/EditorShell.js +6 -3
  21. package/dist/editor/client/EditorShell.js.map +1 -1
  22. package/dist/editor/client/hooks/useEditorWebSocket.d.ts +12 -0
  23. package/dist/editor/client/hooks/useEditorWebSocket.js +27 -0
  24. package/dist/editor/client/hooks/useEditorWebSocket.js.map +1 -1
  25. package/dist/editor/personalization/RuleParameterInput.js +12 -1
  26. package/dist/editor/personalization/RuleParameterInput.js.map +1 -1
  27. package/dist/editor/services/agentService.d.ts +9 -0
  28. package/dist/editor/services/agentService.js.map +1 -1
  29. package/dist/editor/services/agentSubscriptionRegistry.d.ts +1 -0
  30. package/dist/editor/services/agentSubscriptionRegistry.js +6 -0
  31. package/dist/editor/services/agentSubscriptionRegistry.js.map +1 -1
  32. package/dist/editor/services/agentSubscriptionRegistry.test.js +18 -1
  33. package/dist/editor/services/agentSubscriptionRegistry.test.js.map +1 -1
  34. package/dist/editor/settings/SettingsBreadcrumb.d.ts +7 -0
  35. package/dist/editor/settings/SettingsBreadcrumb.js +9 -0
  36. package/dist/editor/settings/SettingsBreadcrumb.js.map +1 -0
  37. package/dist/editor/settings/SettingsHeaderActionsContext.d.ts +5 -0
  38. package/dist/editor/settings/SettingsHeaderActionsContext.js +13 -0
  39. package/dist/editor/settings/SettingsHeaderActionsContext.js.map +1 -0
  40. package/dist/editor/settings/SettingsView.js +16 -8
  41. package/dist/editor/settings/SettingsView.js.map +1 -1
  42. package/dist/editor/settings/panels/ProjectTemplateSelector.d.ts +18 -0
  43. package/dist/editor/settings/panels/ProjectTemplateSelector.js +57 -0
  44. package/dist/editor/settings/panels/ProjectTemplateSelector.js.map +1 -0
  45. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +81 -67
  46. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
  47. package/dist/revision.d.ts +2 -2
  48. package/dist/revision.js +2 -2
  49. package/dist/types.d.ts +6 -0
  50. package/package.json +1 -1
@@ -40,10 +40,12 @@ import { cn } from "../../lib/utils";
40
40
  import { sanitizeSvg } from "../../lib/sanitize";
41
41
  import { Select } from "../../components/ui/select";
42
42
  import { AgentTerminalStatusBar } from "./AgentTerminalStatusBar";
43
+ import { checkReplayVerified, createRecoveryStateSlice, decideRecoveryAction, interpretAgentRunDiagnostics, reconcilePendingDialogTracker, } from "./agentDiagnostics";
44
+ import { forceEditorSocketReconnect } from "../client/hooks/useEditorWebSocket";
43
45
  import { SimpleTabs } from "../ui/SimpleTabs";
44
46
  import { Splitter } from "../ui/Splitter";
45
47
  import { ScrollingContentTree } from "../ScrollingContentTree";
46
- import { subscribeAgent } from "../services/agentSubscriptionRegistry";
48
+ import { requestAgentSubscriptionReplay, subscribeAgent, } from "../services/agentSubscriptionRegistry";
47
49
  import { registerMountedInstance, unregisterMountedInstance, setVisibleDialogEntry, clearVisibleDialogEntriesForInstance, updateInstanceFocus, isElectedDialogReceiver, } from "./agentDialogRegistry";
48
50
  const RECENT_RUN_EVENTS_LIMIT = 50;
49
51
  // interface AgentTerminalProps {
@@ -133,6 +135,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
133
135
  const activeInlineDialogRef = useRef(activeInlineDialog);
134
136
  const isQuestionnaireDialogOpen = activeInlineDialog?.request.dialogType === "questionnaire";
135
137
  const orphanTimeoutRef = useRef(null);
138
+ const pendingDialogReplayCallbackIdsRef = useRef(new Set());
139
+ const pendingDialogReplayTimeoutsRef = useRef(new Set());
136
140
  useEffect(() => {
137
141
  activeInlineDialogRef.current = activeInlineDialog;
138
142
  const callbackId = activeInlineDialog?.request.callbackId || null;
@@ -316,6 +320,34 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
316
320
  return next.slice(-40);
317
321
  });
318
322
  }, []);
323
+ const schedulePendingDialogReplay = useCallback((agentId, callbackId, dialogType) => {
324
+ const normalizedCallbackId = callbackId.trim();
325
+ if (!agentId || !normalizedCallbackId) {
326
+ return;
327
+ }
328
+ if (pendingDialogReplayCallbackIdsRef.current.has(normalizedCallbackId)) {
329
+ return;
330
+ }
331
+ pendingDialogReplayCallbackIdsRef.current.add(normalizedCallbackId);
332
+ const timeout = setTimeout(() => {
333
+ pendingDialogReplayTimeoutsRef.current.delete(timeout);
334
+ const activeCallbackId = activeInlineDialogRef.current?.request.callbackId?.trim();
335
+ if (activeCallbackId === normalizedCallbackId) {
336
+ return;
337
+ }
338
+ const replayRequested = requestAgentSubscriptionReplay(agentId);
339
+ appendToolUiEvent(replayRequested
340
+ ? "ui:pending-dialog-replay-requested"
341
+ : "ui:pending-dialog-replay-skipped", `${dialogType || "dialog"} callbackId=${normalizedCallbackId} agentId=${agentId}`);
342
+ }, 750);
343
+ pendingDialogReplayTimeoutsRef.current.add(timeout);
344
+ }, [appendToolUiEvent]);
345
+ useEffect(() => {
346
+ return () => {
347
+ pendingDialogReplayTimeoutsRef.current.forEach((timeout) => clearTimeout(timeout));
348
+ pendingDialogReplayTimeoutsRef.current.clear();
349
+ };
350
+ }, []);
319
351
  // Collect all pending tool calls for batch approval functionality
320
352
  const allPendingApprovals = useMemo(() => {
321
353
  const pending = [];
@@ -1181,8 +1213,16 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
1181
1213
  const lastSeqRef = useRef(0);
1182
1214
  const subscribedAgentIdRef = useRef(null);
1183
1215
  const reconcileServerStateInFlightRef = useRef(false);
1184
- const streamRecoveryInFlightRef = useRef(false);
1185
- const lastStreamRecoveryAtRef = useRef(0);
1216
+ // Recovery ladder state. The graduated replay → reconnect → stale path lives in the
1217
+ // diagnostics polling effect below. State is keyed by agentId so switching between
1218
+ // agents in the same terminal doesn't cross-contaminate cooldowns.
1219
+ const recoveryStateByAgentRef = useRef(new Map());
1220
+ const replayVerifyTimeoutRef = useRef(null);
1221
+ // Mirror refs so the one-shot verify timer (which fires outside the polling effect's
1222
+ // closure) can read the latest snapshot/diagnostics without re-running the effect.
1223
+ const runDiagnosticsSnapshotRef = useRef(null);
1224
+ const lastDiagnosticsResponseRef = useRef(null);
1225
+ const [showStaleAgentBanner, setShowStaleAgentBanner] = useState(false);
1186
1226
  const toolCallFirstSeenAtRef = useRef({});
1187
1227
  const pendingToolCompletionTimersRef = useRef({});
1188
1228
  // Cache mode/model/profile changes made while the agent is still "new" (not yet persisted)
@@ -3191,14 +3231,17 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
3191
3231
  const dialogType = typeof statusData?.dialogType === "string"
3192
3232
  ? statusData.dialogType
3193
3233
  : null;
3194
- const isBrowserCaptureWait = dialogType === DIALOG_TYPES.CAPTURE_PAGE_DOM ||
3195
- dialogType === DIALOG_TYPES.CAPTURE_PAGE_SCREENSHOT;
3196
- setPendingBrowserCaptureDialogType(isBrowserCaptureWait ? dialogType : null);
3197
- const captureCallbackId = isBrowserCaptureWait &&
3198
- typeof statusData?.callbackId === "string" &&
3234
+ const callbackId = typeof statusData?.callbackId === "string" &&
3199
3235
  statusData.callbackId.trim()
3200
3236
  ? statusData.callbackId.trim()
3201
3237
  : null;
3238
+ const isBrowserCaptureWait = dialogType === DIALOG_TYPES.CAPTURE_PAGE_DOM ||
3239
+ dialogType === DIALOG_TYPES.CAPTURE_PAGE_SCREENSHOT;
3240
+ if (callbackId && !isBrowserCaptureWait) {
3241
+ schedulePendingDialogReplay(agentId, callbackId, dialogType);
3242
+ }
3243
+ setPendingBrowserCaptureDialogType(isBrowserCaptureWait ? dialogType : null);
3244
+ const captureCallbackId = isBrowserCaptureWait && callbackId ? callbackId : null;
3202
3245
  setPendingBrowserCaptureCallbackId(captureCallbackId);
3203
3246
  setAgent((prev) => prev
3204
3247
  ? {
@@ -3362,6 +3405,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
3362
3405
  handleToolCall,
3363
3406
  handleToolResult,
3364
3407
  onAgentUpdate,
3408
+ schedulePendingDialogReplay,
3365
3409
  settleCompletedRun,
3366
3410
  ]);
3367
3411
  // Keep refs for latest agent to avoid adding them to effect deps
@@ -4888,6 +4932,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
4888
4932
  const lastEvent = recentAgentRunEvents[recentAgentRunEvents.length - 1];
4889
4933
  const { receivedSeqs, missingSeqs, totalCount } = currentRunDiagnostics;
4890
4934
  const observedMaxSeq = receivedSeqs[receivedSeqs.length - 1] ?? 0;
4935
+ const inlineCallback = activeInlineDialogRef.current?.request.callbackId?.trim() || null;
4891
4936
  return {
4892
4937
  agentId: currentAgentId,
4893
4938
  isSubmitting,
@@ -4914,6 +4959,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
4914
4959
  totalToolCallCount,
4915
4960
  incompleteToolCallCount,
4916
4961
  recentToolUiEvents,
4962
+ activeInlineDialogCallbackId: inlineCallback,
4963
+ pendingDialogReplayCallbackIds: Array.from(pendingDialogReplayCallbackIdsRef.current),
4917
4964
  };
4918
4965
  }, [
4919
4966
  assistantGroupCount,
@@ -4934,20 +4981,78 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
4934
4981
  recentToolUiEvents,
4935
4982
  totalToolCallCount,
4936
4983
  agent?.statusMessage,
4984
+ activeInlineDialog,
4937
4985
  ]);
4986
+ // Mirror the latest snapshot/diagnostics so the one-shot replay verify timer can read
4987
+ // them after the polling effect's closure has gone stale.
4988
+ useEffect(() => {
4989
+ runDiagnosticsSnapshotRef.current = runDiagnosticsSnapshot;
4990
+ }, [runDiagnosticsSnapshot]);
4991
+ // Reset all per-agent recovery state when the active agent changes.
4992
+ useEffect(() => {
4993
+ recoveryStateByAgentRef.current.clear();
4994
+ if (replayVerifyTimeoutRef.current) {
4995
+ clearTimeout(replayVerifyTimeoutRef.current);
4996
+ replayVerifyTimeoutRef.current = null;
4997
+ }
4998
+ setShowStaleAgentBanner(false);
4999
+ }, [currentAgentId]);
5000
+ // When the editor socket reconnects (socketConnectionVersion bump) clear the stale
5001
+ // banner — the registry has just re-issued subscriptions, so any "stuck" state should
5002
+ // resolve on the next poll. If it doesn't, the banner will reappear.
5003
+ useEffect(() => {
5004
+ setShowStaleAgentBanner(false);
5005
+ }, [editContext?.socketConnectionVersion]);
4938
5006
  useEffect(() => {
4939
5007
  if (!effectiveIsVisible || !isExecuting || !currentAgentId) {
4940
5008
  return;
4941
5009
  }
4942
5010
  let disposed = false;
5011
+ const agentIdAtMount = currentAgentId;
5012
+ const getRecoveryState = () => {
5013
+ let state = recoveryStateByAgentRef.current.get(agentIdAtMount);
5014
+ if (!state) {
5015
+ state = createRecoveryStateSlice();
5016
+ recoveryStateByAgentRef.current.set(agentIdAtMount, state);
5017
+ }
5018
+ return state;
5019
+ };
5020
+ const logRecoveryEvent = (kind, payload) => {
5021
+ try {
5022
+ appendToolUiEvent(`recovery:${kind}`, JSON.stringify({ agentId: agentIdAtMount, ...payload }));
5023
+ }
5024
+ catch {
5025
+ // Telemetry must never throw out of the polling path.
5026
+ }
5027
+ };
5028
+ const clearVerifyTimer = () => {
5029
+ if (replayVerifyTimeoutRef.current) {
5030
+ clearTimeout(replayVerifyTimeoutRef.current);
5031
+ replayVerifyTimeoutRef.current = null;
5032
+ }
5033
+ };
5034
+ const tryReconnect = (state, reason) => {
5035
+ const now = Date.now();
5036
+ if (state.lastReconnectAt > 0 &&
5037
+ now - state.lastReconnectAt < 60_000) {
5038
+ logRecoveryEvent("reconnect-cooldown", { reason });
5039
+ return false;
5040
+ }
5041
+ state.lastReconnectAt = now;
5042
+ const closed = forceEditorSocketReconnect(reason);
5043
+ logRecoveryEvent("reconnect-started", { reason, closed });
5044
+ return true;
5045
+ };
4943
5046
  const checkServerCompletion = async () => {
4944
5047
  try {
4945
- const diagnostics = await getAgentDiagnostics(currentAgentId, editContext?.sessionId);
5048
+ const diagnostics = await getAgentDiagnostics(agentIdAtMount, editContext?.sessionId);
4946
5049
  if (disposed) {
4947
5050
  return;
4948
5051
  }
5052
+ lastDiagnosticsResponseRef.current = diagnostics;
4949
5053
  const serverStatus = diagnostics.execution?.status;
4950
5054
  if (isFinishedServerExecutionStatus(serverStatus)) {
5055
+ setShowStaleAgentBanner(false);
4951
5056
  settleCompletedRun(normalizeServerExecutionStatus(serverStatus) === "cancelled"
4952
5057
  ? "cancelled"
4953
5058
  : "completed");
@@ -4970,58 +5075,117 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
4970
5075
  isWaitingRef.current = false;
4971
5076
  setIsConnecting(false);
4972
5077
  setIsAgentThinking(false);
5078
+ setShowStaleAgentBanner(false);
4973
5079
  return;
4974
5080
  }
4975
- const serverLastSeq = diagnostics.currentSession?.lastDelivery?.lastSeq ??
4976
- diagnostics.transport?.lastSeq ??
4977
- null;
4978
- const serverLastSuccessAt = diagnostics.currentSession?.lastDelivery?.lastSuccessAt ??
4979
- diagnostics.transport?.lastBroadcastAt ??
4980
- diagnostics.transport?.lastProducedAt ??
4981
- null;
4982
- const serverLastSuccessTime = serverLastSuccessAt
4983
- ? Date.parse(serverLastSuccessAt)
4984
- : Number.NaN;
5081
+ const snapshot = runDiagnosticsSnapshot;
5082
+ if (!editContext?.socketDiagnostics) {
5083
+ // No edit context to read socket diagnostics from — recovery decisions need it,
5084
+ // and without it we'd misclassify the stream. Wait for the next tick.
5085
+ return;
5086
+ }
5087
+ const summary = interpretAgentRunDiagnostics({
5088
+ socketDiagnostics: editContext.socketDiagnostics,
5089
+ localSnapshot: snapshot,
5090
+ serverDiagnostics: diagnostics,
5091
+ });
5092
+ const state = getRecoveryState();
4985
5093
  const now = Date.now();
4986
- const serverDeliveryOldEnough = Number.isFinite(serverLastSuccessTime) &&
4987
- now - serverLastSuccessTime > 8_000;
4988
- const recoveryCooldownElapsed = now - lastStreamRecoveryAtRef.current > 30_000;
4989
- const localLastSeq = lastSeqRef.current;
4990
- if (diagnostics.currentSession?.isConnected &&
4991
- diagnostics.currentSession?.isSubscribed &&
4992
- typeof serverLastSeq === "number" &&
4993
- serverLastSeq > localLastSeq &&
4994
- serverDeliveryOldEnough &&
4995
- recoveryCooldownElapsed &&
4996
- !streamRecoveryInFlightRef.current) {
4997
- streamRecoveryInFlightRef.current = true;
4998
- lastStreamRecoveryAtRef.current = now;
4999
- appendToolUiEvent("ui:stream-recovery", `server seq ${serverLastSeq} is ahead of local seq ${localLastSeq}; reconnecting socket`, serverLastSeq);
5000
- try {
5001
- await loadAgent();
5094
+ reconcilePendingDialogTracker(state, diagnostics.pendingDialogs, now);
5095
+ // --- Verify-window resolution. Structured to avoid the "expired branch
5096
+ // unreachable" pitfall: first check whether a verify is in flight, then branch on
5097
+ // whether it's still inside the window or just expired.
5098
+ if (state.lastReplayVerifyDeadlineAt > 0) {
5099
+ if (now < state.lastReplayVerifyDeadlineAt) {
5100
+ // Still inside the 4s window — leave verification to the one-shot timer.
5101
+ return;
5002
5102
  }
5003
- catch (loadError) {
5004
- console.warn("[AgentTerminal] Failed to reload agent during stream recovery", loadError);
5103
+ const verified = checkReplayVerified(state, snapshot, diagnostics);
5104
+ state.lastReplayVerifyDeadlineAt = 0;
5105
+ state.lastReplayVerifySnapshot = null;
5106
+ logRecoveryEvent(verified ? "replay-succeeded" : "replay-failed", { kind: "poll", summaryCode: summary.code });
5107
+ if (verified) {
5108
+ setShowStaleAgentBanner(false);
5109
+ return;
5005
5110
  }
5006
- try {
5007
- const socket = globalThis.editorSocket;
5008
- if (socket && socket.readyState === WebSocket.OPEN) {
5009
- socket.close(4000, "agent-stream-lag");
5111
+ // Fall through — decideRecoveryAction will likely return "reconnect" now.
5112
+ }
5113
+ const decision = decideRecoveryAction({
5114
+ summary,
5115
+ localSnapshot: snapshot,
5116
+ serverDiagnostics: diagnostics,
5117
+ recoveryState: state,
5118
+ now,
5119
+ });
5120
+ state.lastDecidedAction = decision.action;
5121
+ logRecoveryEvent("decided", {
5122
+ action: decision.action,
5123
+ summaryCode: summary.code,
5124
+ reasonCode: decision.reasonCode,
5125
+ replayKey: decision.replayKey,
5126
+ });
5127
+ switch (decision.action) {
5128
+ case "replay": {
5129
+ const sent = requestAgentSubscriptionReplay(agentIdAtMount);
5130
+ if (!sent) {
5131
+ tryReconnect(state, "agent-recovery-replay-no-socket");
5132
+ break;
5010
5133
  }
5134
+ const serverLastSeq = diagnostics.currentSession?.lastDelivery?.lastSeq ??
5135
+ diagnostics.transport?.lastSeq ??
5136
+ null;
5137
+ const replayCallbackId = decision.replayKey?.split("|").pop() ?? null;
5138
+ state.lastReplayKey = decision.replayKey;
5139
+ state.lastReplayAt = now;
5140
+ state.lastReplayVerifyDeadlineAt = now + 4_000;
5141
+ state.lastReplayVerifySnapshot = {
5142
+ localSeq: snapshot.lastSeq,
5143
+ serverLastSeq,
5144
+ pendingDialogCallbackId: replayCallbackId === "none" ? null : replayCallbackId,
5145
+ };
5146
+ clearVerifyTimer();
5147
+ replayVerifyTimeoutRef.current = setTimeout(() => {
5148
+ replayVerifyTimeoutRef.current = null;
5149
+ if (disposed)
5150
+ return;
5151
+ const latestSnapshot = runDiagnosticsSnapshotRef.current ?? snapshot;
5152
+ const latestDiagnostics = lastDiagnosticsResponseRef.current;
5153
+ const verified = checkReplayVerified(state, latestSnapshot, latestDiagnostics);
5154
+ state.lastReplayVerifyDeadlineAt = 0;
5155
+ state.lastReplayVerifySnapshot = null;
5156
+ logRecoveryEvent(verified ? "replay-succeeded" : "replay-failed", { kind: "one-shot" });
5157
+ if (verified) {
5158
+ setShowStaleAgentBanner(false);
5159
+ }
5160
+ // Do not auto-escalate from here; the next poll will see the failed verify
5161
+ // and ask decideRecoveryAction for the next step (typically reconnect).
5162
+ }, 4_000);
5163
+ logRecoveryEvent("replay-started", {
5164
+ replayKey: decision.replayKey,
5165
+ summaryCode: summary.code,
5166
+ serverLastSeq,
5167
+ localSeq: snapshot.lastSeq,
5168
+ });
5169
+ break;
5011
5170
  }
5012
- catch (socketError) {
5013
- console.warn("[AgentTerminal] Failed to reconnect stale agent stream socket", socketError);
5171
+ case "reconnect": {
5172
+ tryReconnect(state, "agent-recovery-escalation");
5173
+ break;
5014
5174
  }
5015
- finally {
5016
- window.setTimeout(() => {
5017
- streamRecoveryInFlightRef.current = false;
5018
- }, 5_000);
5175
+ case "stale": {
5176
+ setShowStaleAgentBanner(true);
5177
+ logRecoveryEvent("stale", {
5178
+ summaryCode: summary.code,
5179
+ reasonCode: decision.reasonCode,
5180
+ });
5181
+ break;
5019
5182
  }
5183
+ case "none":
5184
+ break;
5020
5185
  }
5021
5186
  }
5022
5187
  catch (error) {
5023
5188
  console.warn("[AgentTerminal] Failed to reconcile agent run status", error);
5024
- streamRecoveryInFlightRef.current = false;
5025
5189
  }
5026
5190
  };
5027
5191
  // Avoid racing a freshly submitted run before the backend has registered it.
@@ -5031,16 +5195,18 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
5031
5195
  disposed = true;
5032
5196
  window.clearTimeout(timeoutId);
5033
5197
  window.clearInterval(intervalId);
5198
+ clearVerifyTimer();
5034
5199
  };
5035
5200
  }, [
5036
5201
  currentAgentId,
5037
5202
  editContext?.sessionId,
5038
5203
  editContext?.socketConnectionVersion,
5204
+ editContext?.socketDiagnostics,
5039
5205
  effectiveIsVisible,
5040
5206
  isExecuting,
5041
5207
  appendToolUiEvent,
5042
5208
  clearHeartbeatMessages,
5043
- loadAgent,
5209
+ runDiagnosticsSnapshot,
5044
5210
  settleCompletedRun,
5045
5211
  ]);
5046
5212
  const showInitialThinkingSplash = messages.length === 0 &&
@@ -5200,6 +5366,33 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
5200
5366
  "Waiting for capacity. The agent will start automatically when a slot becomes available.";
5201
5367
  return _jsx(AgentCapacityBanner, { message: message });
5202
5368
  };
5369
+ const renderStaleAgentBanner = () => {
5370
+ if (!showStaleAgentBanner || !currentAgentId)
5371
+ return null;
5372
+ const handleRefreshStream = () => {
5373
+ const state = recoveryStateByAgentRef.current.get(currentAgentId);
5374
+ if (state) {
5375
+ state.lastReplayKey = null;
5376
+ state.lastReplayAt = 0;
5377
+ }
5378
+ requestAgentSubscriptionReplay(currentAgentId);
5379
+ void loadAgent();
5380
+ setShowStaleAgentBanner(false);
5381
+ };
5382
+ const handleReconnect = () => {
5383
+ const state = recoveryStateByAgentRef.current.get(currentAgentId);
5384
+ if (state) {
5385
+ state.lastReconnectAt = 0;
5386
+ }
5387
+ forceEditorSocketReconnect("user-stale-agent-action");
5388
+ setShowStaleAgentBanner(false);
5389
+ };
5390
+ const handleCancel = () => {
5391
+ void handleStop();
5392
+ setShowStaleAgentBanner(false);
5393
+ };
5394
+ return (_jsxs("div", { role: "status", className: "m-3 rounded border border-amber-300 bg-amber-50 p-3 text-[12px] text-amber-900", children: [_jsx("p", { className: "font-medium", children: "Agent run looks stuck" }), _jsx("p", { className: "mt-1 text-[11px] text-amber-800", children: "The backend reports this run is still active, but recent recovery attempts did not resolve the stream. Pick an action below \u2014 automatic retries are paused until then." }), _jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [_jsx(Button, { size: "sm", variant: "outline", onClick: handleRefreshStream, children: "Refresh stream" }), _jsx(Button, { size: "sm", variant: "outline", onClick: handleReconnect, children: "Reconnect socket" }), _jsx(Button, { size: "sm", variant: "outline", onClick: handleCancel, children: "Cancel agent" })] })] }));
5395
+ };
5203
5396
  const renderBrowserClaimBanner = (variant = "inline") => {
5204
5397
  if (!agent?.id || !editContext?.sessionId)
5205
5398
  return null;
@@ -5323,7 +5516,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
5323
5516
  ? getOperationsForMessageGroup(summaryMessages, agentOperations)
5324
5517
  : [];
5325
5518
  return (_jsxs("div", { className: `flex h-full min-h-0 flex-col ${className || ""}`, children: [fixedBrowserClaimBanner, error &&
5326
- !isAgentErrorStatusValue((agent || agentStub)?.status) && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3 select-text", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-[11px] font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-[11px] text-red-700", children: error })] })] }) })), renderCapacityBanner(), renderErrorBanner(), _jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [showInitialThinkingSplash && (_jsx(InitialThinkingSplash, { svgIcon: activeProfile?.svgIcon })), inlineBrowserClaimBanner, inlineDialog ? (inlineDialog) : latestSummaryAssistantGroup ? (_jsx("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: _jsx(AiResponseMessage, { messages: summaryMessages, finished: !latestSummaryAssistantGroup.isLastGroup || !isExecuting, editOperations: summaryOperations, defaultCollapseJson: defaultCollapseJson, profileSvgIcon: activeProfile?.svgIcon, agentId: agent?.id || agentStub.id, agentName: activeProfile?.agentName ||
5519
+ !isAgentErrorStatusValue((agent || agentStub)?.status) && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3 select-text", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-[11px] font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-[11px] text-red-700", children: error })] })] }) })), renderCapacityBanner(), renderErrorBanner(), renderStaleAgentBanner(), _jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [showInitialThinkingSplash && (_jsx(InitialThinkingSplash, { svgIcon: activeProfile?.svgIcon })), inlineBrowserClaimBanner, inlineDialog ? (inlineDialog) : latestSummaryAssistantGroup ? (_jsx("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: _jsx(AiResponseMessage, { messages: summaryMessages, finished: !latestSummaryAssistantGroup.isLastGroup || !isExecuting, editOperations: summaryOperations, defaultCollapseJson: defaultCollapseJson, profileSvgIcon: activeProfile?.svgIcon, agentId: agent?.id || agentStub.id, agentName: activeProfile?.agentName ||
5327
5520
  activeProfile?.displayTitle ||
5328
5521
  activeProfile?.name, allPendingApprovals: allPendingApprovals, onSwitchToAutonomous: handleSwitchToAutonomous, browserCaptureInlinePrompt: browserCaptureInlinePrompt, readOnly: readOnly, onQuickAction: (action) => {
5329
5522
  const text = (action.prompt ||