@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.
- package/dist/components/PageHeader.js +1 -1
- package/dist/components/PageHeader.js.map +1 -1
- package/dist/components/ui/breadcrumb.d.ts +12 -0
- package/dist/components/ui/breadcrumb.js +32 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/editor/ContentTree.js +2 -1
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/ai/AgentBanners.js +1 -1
- package/dist/editor/ai/AgentBanners.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.js +243 -50
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/agentDiagnostics.d.ts +56 -1
- package/dist/editor/ai/agentDiagnostics.js +207 -0
- package/dist/editor/ai/agentDiagnostics.js.map +1 -1
- package/dist/editor/ai/agentDiagnostics.test.d.ts +1 -0
- package/dist/editor/ai/agentDiagnostics.test.js +372 -0
- package/dist/editor/ai/agentDiagnostics.test.js.map +1 -0
- package/dist/editor/ai-image-editor/AiImageEditorDialog.js +5 -2
- package/dist/editor/ai-image-editor/AiImageEditorDialog.js.map +1 -1
- package/dist/editor/client/EditorShell.js +6 -3
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/hooks/useEditorWebSocket.d.ts +12 -0
- package/dist/editor/client/hooks/useEditorWebSocket.js +27 -0
- package/dist/editor/client/hooks/useEditorWebSocket.js.map +1 -1
- package/dist/editor/personalization/RuleParameterInput.js +12 -1
- package/dist/editor/personalization/RuleParameterInput.js.map +1 -1
- package/dist/editor/services/agentService.d.ts +9 -0
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/agentSubscriptionRegistry.d.ts +1 -0
- package/dist/editor/services/agentSubscriptionRegistry.js +6 -0
- package/dist/editor/services/agentSubscriptionRegistry.js.map +1 -1
- package/dist/editor/services/agentSubscriptionRegistry.test.js +18 -1
- package/dist/editor/services/agentSubscriptionRegistry.test.js.map +1 -1
- package/dist/editor/settings/SettingsBreadcrumb.d.ts +7 -0
- package/dist/editor/settings/SettingsBreadcrumb.js +9 -0
- package/dist/editor/settings/SettingsBreadcrumb.js.map +1 -0
- package/dist/editor/settings/SettingsHeaderActionsContext.d.ts +5 -0
- package/dist/editor/settings/SettingsHeaderActionsContext.js +13 -0
- package/dist/editor/settings/SettingsHeaderActionsContext.js.map +1 -0
- package/dist/editor/settings/SettingsView.js +16 -8
- package/dist/editor/settings/SettingsView.js.map +1 -1
- package/dist/editor/settings/panels/ProjectTemplateSelector.d.ts +18 -0
- package/dist/editor/settings/panels/ProjectTemplateSelector.js +57 -0
- package/dist/editor/settings/panels/ProjectTemplateSelector.js.map +1 -0
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js +81 -67
- package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/types.d.ts +6 -0
- 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
|
-
|
|
1185
|
-
|
|
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
|
|
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(
|
|
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
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
:
|
|
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
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
if (
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
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
|
-
|
|
5004
|
-
|
|
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
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
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
|
-
|
|
5013
|
-
|
|
5171
|
+
case "reconnect": {
|
|
5172
|
+
tryReconnect(state, "agent-recovery-escalation");
|
|
5173
|
+
break;
|
|
5014
5174
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
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
|
-
|
|
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 ||
|