@parhelia/core 0.1.12763 → 0.1.12767
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/agents-view/AgentsView.js +1 -1
- package/dist/agents-view/AgentsView.js.map +1 -1
- package/dist/agents-view/AgentsWorkspaceView.js +143 -26
- package/dist/agents-view/AgentsWorkspaceView.js.map +1 -1
- package/dist/editor/ai/AgentPanesGrid.d.ts +28 -0
- package/dist/editor/ai/AgentPanesGrid.js +62 -0
- package/dist/editor/ai/AgentPanesGrid.js.map +1 -0
- package/dist/editor/ai/AgentTerminal.d.ts +6 -1
- package/dist/editor/ai/AgentTerminal.js +142 -121
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/InlineAiDialog.js +20 -19
- package/dist/editor/ai/InlineAiDialog.js.map +1 -1
- package/dist/editor/ai/ToolCallDisplay.js +3 -0
- package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
- package/dist/editor/ai/agentDialogRegistry.d.ts +26 -0
- package/dist/editor/ai/agentDialogRegistry.js +221 -0
- package/dist/editor/ai/agentDialogRegistry.js.map +1 -0
- package/dist/editor/ai/agentPanesTypes.d.ts +27 -0
- package/dist/editor/ai/agentPanesTypes.js +16 -0
- package/dist/editor/ai/agentPanesTypes.js.map +1 -0
- package/dist/editor/ai/useAgentPanes.d.ts +34 -0
- package/dist/editor/ai/useAgentPanes.js +357 -0
- package/dist/editor/ai/useAgentPanes.js.map +1 -0
- package/dist/editor/ai/useAgentPanes.test.d.ts +1 -0
- package/dist/editor/ai/useAgentPanes.test.js +198 -0
- package/dist/editor/ai/useAgentPanes.test.js.map +1 -0
- package/dist/editor/client/EditorShell.js +9 -0
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +9 -0
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/operations.d.ts +2 -1
- package/dist/editor/client/operations.js +2 -0
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/client/pageModelBuilder.d.ts +2 -1
- package/dist/editor/client/pageModelBuilder.js +7 -5
- package/dist/editor/client/pageModelBuilder.js.map +1 -1
- package/dist/editor/menubar/GenericToolbar.js +5 -3
- package/dist/editor/menubar/GenericToolbar.js.map +1 -1
- package/dist/editor/menubar/ToolbarFactory.js +6 -1
- package/dist/editor/menubar/ToolbarFactory.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/LayoutModeControls.d.ts +10 -0
- package/dist/editor/menubar/toolbar-sections/LayoutModeControls.js +29 -0
- package/dist/editor/menubar/toolbar-sections/LayoutModeControls.js.map +1 -0
- package/dist/editor/menubar/toolbar-sections/index.d.ts +1 -0
- package/dist/editor/menubar/toolbar-sections/index.js +1 -0
- package/dist/editor/menubar/toolbar-sections/index.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +18 -1
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/page-viewer/pageViewContext.d.ts +3 -1
- package/dist/editor/page-viewer/pageViewContext.js +2 -2
- package/dist/editor/page-viewer/pageViewContext.js.map +1 -1
- package/dist/editor/personalization/PersonalizationPanel.js +8 -0
- package/dist/editor/personalization/PersonalizationPanel.js.map +1 -1
- package/dist/editor/services/agentService.js +4 -16
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/agentSubscriptionRegistry.d.ts +7 -0
- package/dist/editor/services/agentSubscriptionRegistry.js +77 -0
- package/dist/editor/services/agentSubscriptionRegistry.js.map +1 -0
- package/dist/editor/services/agentSubscriptionRegistry.test.d.ts +1 -0
- package/dist/editor/services/agentSubscriptionRegistry.test.js +87 -0
- package/dist/editor/services/agentSubscriptionRegistry.test.js.map +1 -0
- package/dist/editor/services/contentService.d.ts +8 -0
- package/dist/editor/services/contentService.js.map +1 -1
- package/dist/editor/services/editService.d.ts +1 -1
- package/dist/editor/services/editService.js +2 -2
- package/dist/editor/services/editService.js.map +1 -1
- package/dist/editor/settings/panels/ClusterInstancesPanel.js +40 -3
- package/dist/editor/settings/panels/ClusterInstancesPanel.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/task-board/TaskBoardWorkspace.js +9 -11
- package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
- package/dist/task-board/components/ProjectDashboard.d.ts +3 -0
- package/dist/task-board/components/ProjectDashboard.js +26 -3
- package/dist/task-board/components/ProjectDashboard.js.map +1 -1
- package/dist/task-board/types.d.ts +4 -0
- package/dist/test/setup.d.ts +0 -0
- package/dist/test/setup.js +37 -0
- package/dist/test/setup.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/package.json +8 -3
|
@@ -21,7 +21,7 @@ import { QueuedPromptsPanel } from "./QueuedPromptsPanel";
|
|
|
21
21
|
import { AgentCapacityBanner, AgentCostLimitBanner, AgentErrorBanner, } from "./AgentBanners";
|
|
22
22
|
import { InitialThinkingSplash } from "./InitialThinkingSplash";
|
|
23
23
|
import { AgentInlineDialogContent } from "./AgentInlineDialogContent";
|
|
24
|
-
import { AGENT_HISTORY_LIMIT, MACHINE_CAPACITY_REASON, buildPlaceholderAgentDetails, formatAllowanceLabel, formatAllowanceSource, getAgentRunMessageAgentId, getAgentRunMessageDetail, getAgentRunMessageSeq,
|
|
24
|
+
import { AGENT_HISTORY_LIMIT, MACHINE_CAPACITY_REASON, buildPlaceholderAgentDetails, formatAllowanceLabel, formatAllowanceSource, getAgentRunMessageAgentId, getAgentRunMessageDetail, getAgentRunMessageSeq, isAgentErrorStatusValue, isFinishedServerExecutionStatus, isHeartbeatRunEventMessage, mergeAgentOperationHistory, normalizeDialogAgentId, normalizeProfileAllowanceOperations, normalizeServerExecutionStatus, } from "./agentMessageHelpers";
|
|
25
25
|
import { AgentDocumentList, } from "./AgentDocumentList";
|
|
26
26
|
import { AgentEditOperationsPanel } from "./EditOperationsPanel";
|
|
27
27
|
import { SpawnedAgentsPanel } from "./SpawnedAgentsPanel";
|
|
@@ -43,11 +43,19 @@ import { AgentTerminalStatusBar } from "./AgentTerminalStatusBar";
|
|
|
43
43
|
import { SimpleTabs } from "../ui/SimpleTabs";
|
|
44
44
|
import { Splitter } from "../ui/Splitter";
|
|
45
45
|
import { ScrollingContentTree } from "../ScrollingContentTree";
|
|
46
|
+
import { subscribeAgent } from "../services/agentSubscriptionRegistry";
|
|
47
|
+
import { registerMountedInstance, unregisterMountedInstance, setVisibleDialogEntry, clearVisibleDialogEntriesForInstance, updateInstanceFocus, isElectedDialogReceiver, } from "./agentDialogRegistry";
|
|
46
48
|
const RECENT_RUN_EVENTS_LIMIT = 50;
|
|
47
49
|
// interface AgentTerminalProps {
|
|
48
50
|
// agentStub: Agent;
|
|
49
51
|
// }
|
|
50
|
-
export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadProfiles, isActive
|
|
52
|
+
export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadProfiles, isActive, isVisible, isFocused, compact = false, displayMode = "full", showSummaryInput = false, hideContext = false, hideBottomControls = false, hideGreeting = false, defaultCollapseJson = false, simpleMode = false, className, initialPrompt, onAgentUpdate, onMessage, onInteractionSubmitted, onQuestionnaireOpenChange, questionnaireFooterActions, hideSummaryMessages = false, summaryPlaceholderActions, summaryPlaceholderMessage, hideSummaryWaitingPlaceholder = false, }) {
|
|
53
|
+
// Derived from props. `isVisible` controls "is this terminal mounted and
|
|
54
|
+
// streaming" (subscriptions, polling). `isFocused` controls "does this
|
|
55
|
+
// terminal own keyboard focus and dialogs". Both fall back to legacy
|
|
56
|
+
// `isActive` for the 8 callers that haven't been updated to the split props.
|
|
57
|
+
const effectiveIsVisible = isVisible ?? isActive ?? true;
|
|
58
|
+
const effectiveIsFocused = isFocused ?? isActive ?? true;
|
|
51
59
|
const editContext = useEditContext();
|
|
52
60
|
const fieldsContext = useFieldsEditContext();
|
|
53
61
|
const [agent, setAgent] = useState(() => buildPlaceholderAgentDetails(agentStub));
|
|
@@ -127,25 +135,20 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
127
135
|
const orphanTimeoutRef = useRef(null);
|
|
128
136
|
useEffect(() => {
|
|
129
137
|
activeInlineDialogRef.current = activeInlineDialog;
|
|
130
|
-
const visibleRegistry = { ...getVisibleDialogRegistry() };
|
|
131
138
|
const callbackId = activeInlineDialog?.request.callbackId || null;
|
|
132
139
|
const terminalInstanceId = dialogTerminalInstanceIdRef.current;
|
|
133
140
|
const agentKeys = [
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
]
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
delete visibleRegistry[key];
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
globalThis.__agentDialogVisibleCallbacks = visibleRegistry;
|
|
141
|
+
agentStubIdRefForDialogs.current,
|
|
142
|
+
agentIdRefForDialogs.current,
|
|
143
|
+
];
|
|
144
|
+
if (callbackId) {
|
|
145
|
+
agentKeys.forEach((key) => {
|
|
146
|
+
setVisibleDialogEntry(key, terminalInstanceId, callbackId, isActiveRefForDialogs.current);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
clearVisibleDialogEntriesForInstance(agentKeys, terminalInstanceId);
|
|
151
|
+
}
|
|
149
152
|
}, [activeInlineDialog]);
|
|
150
153
|
useEffect(() => {
|
|
151
154
|
onQuestionnaireOpenChange?.(isQuestionnaireDialogOpen);
|
|
@@ -1108,7 +1111,13 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
1108
1111
|
setPrompt(localStorageService.getItem(`editor.agent.draftPrompt.${agentStub.id}`) || "");
|
|
1109
1112
|
}, [agentStub.id]);
|
|
1110
1113
|
// Persist the in-progress prompt per agent so it survives workspace switches/unmounts.
|
|
1114
|
+
// Gated on `effectiveIsFocused`: when the same agent is shown in two panes
|
|
1115
|
+
// only the focused pane is the "draft owner". Non-focused panes keep their
|
|
1116
|
+
// local prompt state (so the textarea displays it) but never persist —
|
|
1117
|
+
// otherwise the two would race-overwrite each other in localStorage.
|
|
1111
1118
|
useEffect(() => {
|
|
1119
|
+
if (!effectiveIsFocused)
|
|
1120
|
+
return;
|
|
1112
1121
|
const key = `editor.agent.draftPrompt.${agentStub.id}`;
|
|
1113
1122
|
if (prompt) {
|
|
1114
1123
|
localStorageService.setItem(key, prompt);
|
|
@@ -1116,7 +1125,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
1116
1125
|
else {
|
|
1117
1126
|
localStorageService.removeItem(key);
|
|
1118
1127
|
}
|
|
1119
|
-
}, [prompt, agentStub.id]);
|
|
1128
|
+
}, [prompt, agentStub.id, effectiveIsFocused]);
|
|
1120
1129
|
useEffect(() => {
|
|
1121
1130
|
// Keep messagesRef synchronized with messages state
|
|
1122
1131
|
messagesRef.current = messages;
|
|
@@ -2138,11 +2147,26 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
2138
2147
|
// WebSocket message (fired from AgentDocumentRepository), bridged to the in-process
|
|
2139
2148
|
// emitAgentDocumentsChanged event in EditorShell. No per-tool allow-list here.
|
|
2140
2149
|
}, [agent?.id, agentStub.id, appendToolUiEvent]);
|
|
2141
|
-
// Listen for local approval resolution to update UI
|
|
2150
|
+
// Listen for local approval resolution to update UI.
|
|
2151
|
+
// Filters on detail.agentId so that with multi-pane (same agent in two panes,
|
|
2152
|
+
// or any visible terminal that isn't the dispatch source) only the matching
|
|
2153
|
+
// mounts react. Compares via ref so an empty-deps listener tolerates
|
|
2154
|
+
// agentStub swaps without going stale.
|
|
2142
2155
|
useEffect(() => {
|
|
2143
2156
|
const onApprovalResolved = (ev) => {
|
|
2144
2157
|
try {
|
|
2145
2158
|
const detail = ev?.detail || {};
|
|
2159
|
+
const eventAgentId = detail.agentId;
|
|
2160
|
+
// Skip events that target a different agent. If the dispatch site
|
|
2161
|
+
// didn't include an agentId (legacy), fall through and accept — no
|
|
2162
|
+
// worse than today's behavior.
|
|
2163
|
+
if (eventAgentId) {
|
|
2164
|
+
const myStubId = agentStubIdRefForDialogs.current;
|
|
2165
|
+
const myAgentId = agentIdRefForDialogs.current;
|
|
2166
|
+
if (eventAgentId !== myStubId && eventAgentId !== myAgentId) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2146
2170
|
const messageId = detail.messageId;
|
|
2147
2171
|
const toolCallId = detail.toolCallId;
|
|
2148
2172
|
const approved = !!detail.approved;
|
|
@@ -2561,16 +2585,16 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
2561
2585
|
useEffect(() => {
|
|
2562
2586
|
loadAgent();
|
|
2563
2587
|
}, [loadAgent]);
|
|
2564
|
-
// Reload agent when tab becomes
|
|
2565
|
-
const
|
|
2588
|
+
// Reload agent when tab becomes visible to get latest messages
|
|
2589
|
+
const previousIsVisibleRef = useRef(effectiveIsVisible);
|
|
2566
2590
|
useEffect(() => {
|
|
2567
|
-
const
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2570
|
-
if (
|
|
2591
|
+
const wasHidden = !previousIsVisibleRef.current;
|
|
2592
|
+
const isNowVisible = effectiveIsVisible;
|
|
2593
|
+
previousIsVisibleRef.current = effectiveIsVisible;
|
|
2594
|
+
if (wasHidden && isNowVisible && agent) {
|
|
2571
2595
|
loadAgent();
|
|
2572
2596
|
}
|
|
2573
|
-
}, [
|
|
2597
|
+
}, [effectiveIsVisible, agent?.id, loadAgent]);
|
|
2574
2598
|
// Fetch agent operations on load and reload
|
|
2575
2599
|
useEffect(() => {
|
|
2576
2600
|
const fetchOperations = async () => {
|
|
@@ -3349,37 +3373,19 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3349
3373
|
useEffect(() => {
|
|
3350
3374
|
handleAgentWebSocketMessageRef.current = handleAgentWebSocketMessage;
|
|
3351
3375
|
}, [handleAgentWebSocketMessage]);
|
|
3352
|
-
// Subscribe to agent WebSocket messages when
|
|
3376
|
+
// Subscribe to agent WebSocket messages when visible.
|
|
3377
|
+
// The actual `agent:subscribe`/`agent:unsubscribe` send is delegated to
|
|
3378
|
+
// agentSubscriptionRegistry so multiple panes / dialogs / async helpers
|
|
3379
|
+
// share a single server subscription per agentId. The registry also owns
|
|
3380
|
+
// re-subscribe on socket reconnect, so socketConnectionVersion is no longer
|
|
3381
|
+
// a dep here.
|
|
3353
3382
|
useEffect(() => {
|
|
3354
3383
|
const addListener = editContext?.addSocketMessageListener;
|
|
3355
|
-
if (!
|
|
3356
|
-
|
|
3357
|
-
if (subscribedAgentIdRef.current) {
|
|
3358
|
-
const socket = globalThis.editorSocket;
|
|
3359
|
-
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
3360
|
-
const payload = {
|
|
3361
|
-
type: "agent:unsubscribe",
|
|
3362
|
-
agentId: subscribedAgentIdRef.current,
|
|
3363
|
-
};
|
|
3364
|
-
console.debug("[AgentWebSocket] sent", payload);
|
|
3365
|
-
socket.send(JSON.stringify(payload));
|
|
3366
|
-
}
|
|
3367
|
-
subscribedAgentIdRef.current = null;
|
|
3368
|
-
}
|
|
3384
|
+
if (!effectiveIsVisible || !addListener) {
|
|
3385
|
+
subscribedAgentIdRef.current = null;
|
|
3369
3386
|
return;
|
|
3370
3387
|
}
|
|
3371
|
-
|
|
3372
|
-
const socket = globalThis.editorSocket;
|
|
3373
|
-
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
3374
|
-
const payload = {
|
|
3375
|
-
type: "agent:subscribe",
|
|
3376
|
-
agentId: agentStub.id,
|
|
3377
|
-
};
|
|
3378
|
-
console.debug("[AgentWebSocket] sent", payload);
|
|
3379
|
-
socket.send(JSON.stringify(payload));
|
|
3380
|
-
}
|
|
3381
|
-
// Use the addSocketMessageListener helper from editContext
|
|
3382
|
-
// Wrap the handler in a stable function that uses the ref
|
|
3388
|
+
const releaseSubscription = subscribeAgent(agentStub.id);
|
|
3383
3389
|
const stableHandler = (message) => {
|
|
3384
3390
|
handleAgentWebSocketMessageRef.current(message);
|
|
3385
3391
|
};
|
|
@@ -3399,51 +3405,53 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3399
3405
|
setIsWaitingForResponse(true);
|
|
3400
3406
|
isWaitingRef.current = true;
|
|
3401
3407
|
shouldCreateNewMessage.current = false;
|
|
3402
|
-
// Agent is currently running, show thinking dots
|
|
3403
3408
|
setIsAgentThinking(true);
|
|
3404
3409
|
}
|
|
3405
3410
|
else if (isWaitingForApproval || isWaitingForInput) {
|
|
3406
3411
|
setIsWaitingForResponse(false);
|
|
3407
3412
|
isWaitingRef.current = false;
|
|
3408
|
-
// Agent is waiting for user input/approval, hide thinking dots
|
|
3409
3413
|
setIsAgentThinking(false);
|
|
3410
3414
|
}
|
|
3411
3415
|
else {
|
|
3412
3416
|
setIsWaitingForResponse(false);
|
|
3413
3417
|
isWaitingRef.current = false;
|
|
3414
|
-
// Agent is idle, hide thinking dots
|
|
3415
3418
|
setIsAgentThinking(false);
|
|
3416
3419
|
}
|
|
3417
3420
|
}
|
|
3418
3421
|
return () => {
|
|
3419
|
-
|
|
3420
|
-
const socket = globalThis.editorSocket;
|
|
3421
|
-
if (socket &&
|
|
3422
|
-
socket.readyState === WebSocket.OPEN &&
|
|
3423
|
-
subscribedAgentIdRef.current) {
|
|
3424
|
-
const payload = {
|
|
3425
|
-
type: "agent:unsubscribe",
|
|
3426
|
-
agentId: subscribedAgentIdRef.current,
|
|
3427
|
-
};
|
|
3428
|
-
console.debug("[AgentWebSocket] sent", payload);
|
|
3429
|
-
socket.send(JSON.stringify(payload));
|
|
3430
|
-
}
|
|
3422
|
+
releaseSubscription();
|
|
3431
3423
|
unsubscribe();
|
|
3432
3424
|
subscribedAgentIdRef.current = null;
|
|
3433
3425
|
};
|
|
3434
3426
|
}, [
|
|
3435
|
-
|
|
3427
|
+
effectiveIsVisible,
|
|
3436
3428
|
agentStub.id,
|
|
3437
3429
|
editContext?.addSocketMessageListener,
|
|
3438
|
-
editContext?.socketConnectionVersion,
|
|
3439
3430
|
]);
|
|
3440
|
-
// Focus prompt when requested globally (from AI command)
|
|
3431
|
+
// Focus prompt when requested globally (from AI command).
|
|
3432
|
+
// Listener semantics:
|
|
3433
|
+
// - If detail.agentId is provided: only the AgentTerminal whose
|
|
3434
|
+
// agentStub.id (or live agent id) matches reacts.
|
|
3435
|
+
// - If absent (legacy callers): only the focused pane reacts.
|
|
3436
|
+
// Both branches read via refs so empty-deps listener stays current after
|
|
3437
|
+
// agentStub swaps or focus changes.
|
|
3441
3438
|
useEffect(() => {
|
|
3442
|
-
const focusHandler = () => {
|
|
3439
|
+
const focusHandler = (ev) => {
|
|
3443
3440
|
try {
|
|
3441
|
+
const detail = ev.detail;
|
|
3442
|
+
if (detail?.agentId) {
|
|
3443
|
+
const myStubId = agentStubIdRefForDialogs.current;
|
|
3444
|
+
const myAgentId = agentIdRefForDialogs.current;
|
|
3445
|
+
if (detail.agentId !== myStubId && detail.agentId !== myAgentId) {
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
else {
|
|
3450
|
+
if (!isActiveRefForDialogs.current)
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3444
3453
|
if (textareaRef.current) {
|
|
3445
3454
|
textareaRef.current.focus();
|
|
3446
|
-
// Move caret to end
|
|
3447
3455
|
const value = textareaRef.current.value || "";
|
|
3448
3456
|
textareaRef.current.selectionStart = value.length;
|
|
3449
3457
|
textareaRef.current.selectionEnd = value.length;
|
|
@@ -3457,7 +3465,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3457
3465
|
// Keep stable refs so we don't miss window events during effect re-runs.
|
|
3458
3466
|
const agentIdRefForDialogs = useRef(null);
|
|
3459
3467
|
const agentStubIdRefForDialogs = useRef(agentStub.id);
|
|
3460
|
-
const isActiveRefForDialogs = useRef(
|
|
3468
|
+
const isActiveRefForDialogs = useRef(effectiveIsFocused);
|
|
3461
3469
|
const scrollToBottomRefForDialogs = useRef(scrollToBottom);
|
|
3462
3470
|
useEffect(() => {
|
|
3463
3471
|
agentIdRefForDialogs.current = agent?.id ?? null;
|
|
@@ -3465,13 +3473,13 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3465
3473
|
useEffect(() => {
|
|
3466
3474
|
const prevId = agentStubIdRefForDialogs.current;
|
|
3467
3475
|
agentStubIdRefForDialogs.current = agentStub.id;
|
|
3468
|
-
const visibleRegistry = { ...getVisibleDialogRegistry() };
|
|
3469
|
-
const normalizedPrevId = normalizeDialogAgentId(prevId);
|
|
3470
|
-
if (normalizedPrevId) {
|
|
3471
|
-
delete visibleRegistry[normalizedPrevId];
|
|
3472
|
-
globalThis.__agentDialogVisibleCallbacks = visibleRegistry;
|
|
3473
|
-
}
|
|
3474
3476
|
if (prevId && prevId !== agentStub.id) {
|
|
3477
|
+
const terminalInstanceId = dialogTerminalInstanceIdRef.current;
|
|
3478
|
+
// Move our registry entries from prevId to the new stub id so this
|
|
3479
|
+
// instance keeps participating in election under the right key.
|
|
3480
|
+
clearVisibleDialogEntriesForInstance([prevId], terminalInstanceId);
|
|
3481
|
+
unregisterMountedInstance(prevId, terminalInstanceId);
|
|
3482
|
+
registerMountedInstance(agentStub.id, terminalInstanceId);
|
|
3475
3483
|
const orphanedDialog = activeInlineDialogRef.current;
|
|
3476
3484
|
if (orphanedDialog) {
|
|
3477
3485
|
setActiveInlineDialog(null);
|
|
@@ -3482,8 +3490,11 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3482
3490
|
}
|
|
3483
3491
|
}, [agentStub.id]);
|
|
3484
3492
|
useEffect(() => {
|
|
3485
|
-
isActiveRefForDialogs.current =
|
|
3486
|
-
|
|
3493
|
+
isActiveRefForDialogs.current = effectiveIsFocused;
|
|
3494
|
+
// Keep the dialog visibility registry's focus flag in sync so election
|
|
3495
|
+
// sees the current state when an incoming `agent:dialog:show` arrives.
|
|
3496
|
+
updateInstanceFocus([agentStubIdRefForDialogs.current, agentIdRefForDialogs.current], dialogTerminalInstanceIdRef.current, effectiveIsFocused);
|
|
3497
|
+
}, [effectiveIsFocused]);
|
|
3487
3498
|
useEffect(() => {
|
|
3488
3499
|
scrollToBottomRefForDialogs.current = scrollToBottom;
|
|
3489
3500
|
}, [scrollToBottom]);
|
|
@@ -3493,40 +3504,38 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3493
3504
|
clearTimeout(orphanTimeoutRef.current);
|
|
3494
3505
|
orphanTimeoutRef.current = null;
|
|
3495
3506
|
}
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
globalThis.__agentDialogMountedAgents = globalListeners;
|
|
3507
|
+
const terminalInstanceId = dialogTerminalInstanceIdRef.current;
|
|
3508
|
+
// Register this mount in the per-instance registry. The agent stub id is
|
|
3509
|
+
// the stable handle while the live agent id (after creation) is also
|
|
3510
|
+
// tracked through agentIdRefForDialogs, but for mount tracking we use
|
|
3511
|
+
// the stub id since dialogs target the stub id.
|
|
3512
|
+
registerMountedInstance(agentStubIdRefForDialogs.current, terminalInstanceId);
|
|
3503
3513
|
const handleDialogShow = (event) => {
|
|
3504
3514
|
const detail = event?.detail;
|
|
3505
3515
|
const request = detail?.request;
|
|
3506
3516
|
const onComplete = detail?.onComplete;
|
|
3507
3517
|
const onCancel = detail?.onCancel;
|
|
3508
|
-
const terminalAgentId = normalizeDialogAgentId(agentIdRefForDialogs.current);
|
|
3509
|
-
const terminalAgentStubId = normalizeDialogAgentId(agentStubIdRefForDialogs.current);
|
|
3510
|
-
const isActiveNow = isActiveRefForDialogs.current;
|
|
3511
3518
|
if (!request)
|
|
3512
3519
|
return;
|
|
3520
|
+
const terminalAgentId = normalizeDialogAgentId(agentIdRefForDialogs.current);
|
|
3521
|
+
const terminalAgentStubId = normalizeDialogAgentId(agentStubIdRefForDialogs.current);
|
|
3513
3522
|
const requestAgentId = normalizeDialogAgentId(request.agentId);
|
|
3514
|
-
|
|
3515
|
-
!terminalAgentStubId ||
|
|
3516
|
-
requestAgentId === terminalAgentStubId ||
|
|
3517
|
-
requestAgentId === terminalAgentId;
|
|
3518
|
-
if (!isActiveNow) {
|
|
3519
|
-
return;
|
|
3520
|
-
}
|
|
3521
|
-
if (!request)
|
|
3522
|
-
return;
|
|
3523
|
-
// Only handle dialog requests for this terminal's agent stub
|
|
3523
|
+
// Agent identity match — required.
|
|
3524
3524
|
if (requestAgentId &&
|
|
3525
3525
|
terminalAgentStubId &&
|
|
3526
3526
|
requestAgentId !== terminalAgentStubId &&
|
|
3527
3527
|
requestAgentId !== terminalAgentId) {
|
|
3528
3528
|
return;
|
|
3529
3529
|
}
|
|
3530
|
+
// Election: among all mounted instances for this agent, exactly one
|
|
3531
|
+
// accepts. Focused instance wins; otherwise first-mounted by insertion
|
|
3532
|
+
// order. This replaces the old "only the focused/active one accepts"
|
|
3533
|
+
// gate so dialogs still surface when no pane is focused on the agent.
|
|
3534
|
+
const electionAgentId = requestAgentId || terminalAgentStubId;
|
|
3535
|
+
if (electionAgentId &&
|
|
3536
|
+
!isElectedDialogReceiver(electionAgentId, terminalInstanceId)) {
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3530
3539
|
console.log("[AgentTerminal] Received inline dialog request:", request);
|
|
3531
3540
|
setActiveInlineDialog({
|
|
3532
3541
|
request,
|
|
@@ -3555,15 +3564,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3555
3564
|
};
|
|
3556
3565
|
window.addEventListener("agent:dialog:show", handleDialogShow);
|
|
3557
3566
|
return () => {
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
const visibleRegistry = { ...getVisibleDialogRegistry() };
|
|
3561
|
-
const idsToClear = [
|
|
3562
|
-
normalizeDialogAgentId(agentStubIdRefForDialogs.current),
|
|
3563
|
-
normalizeDialogAgentId(agentIdRefForDialogs.current),
|
|
3564
|
-
].filter(Boolean);
|
|
3565
|
-
idsToClear.forEach((id) => delete visibleRegistry[id]);
|
|
3566
|
-
globalThis.__agentDialogVisibleCallbacks = visibleRegistry;
|
|
3567
|
+
unregisterMountedInstance(agentStubIdRefForDialogs.current, terminalInstanceId);
|
|
3568
|
+
clearVisibleDialogEntriesForInstance([agentStubIdRefForDialogs.current, agentIdRefForDialogs.current], terminalInstanceId);
|
|
3567
3569
|
// If unmounting with an active dialog, defer the orphan event so that
|
|
3568
3570
|
// React strict mode remounts can cancel it. Only real unmounts fire.
|
|
3569
3571
|
const orphanedDialog = activeInlineDialogRef.current;
|
|
@@ -3695,14 +3697,16 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3695
3697
|
scrollToBottom();
|
|
3696
3698
|
}
|
|
3697
3699
|
}, [messages, scrollToBottom, shouldAutoScroll]);
|
|
3698
|
-
// Maintain focus on textarea when messages update (prevents focus loss during agent responses)
|
|
3700
|
+
// Maintain focus on textarea when messages update (prevents focus loss during agent responses).
|
|
3701
|
+
// pane-aware: focused only — non-focused panes must not steal focus.
|
|
3699
3702
|
useEffect(() => {
|
|
3700
3703
|
if (shouldMaintainFocusRef.current &&
|
|
3701
3704
|
textareaRef.current &&
|
|
3702
|
-
!activePlaceholderInput
|
|
3705
|
+
!activePlaceholderInput &&
|
|
3706
|
+
effectiveIsFocused) {
|
|
3703
3707
|
textareaRef.current.focus();
|
|
3704
3708
|
}
|
|
3705
|
-
}, [messages, activePlaceholderInput]);
|
|
3709
|
+
}, [messages, activePlaceholderInput, effectiveIsFocused]);
|
|
3706
3710
|
// Persist any pending settings (mode/model/profile) once an agent exists server-side
|
|
3707
3711
|
const persistPendingSettingsIfNeeded = useCallback(async () => {
|
|
3708
3712
|
try {
|
|
@@ -3846,6 +3850,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3846
3850
|
});
|
|
3847
3851
|
window.dispatchEvent(new CustomEvent("agent:toolApprovalResolved", {
|
|
3848
3852
|
detail: {
|
|
3853
|
+
agentId: agentIdForReject,
|
|
3849
3854
|
messageId: pending.dbMessageId || pending.messageId,
|
|
3850
3855
|
toolCallId: pending.toolCallId,
|
|
3851
3856
|
approved: false,
|
|
@@ -4931,7 +4936,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
4931
4936
|
agent?.statusMessage,
|
|
4932
4937
|
]);
|
|
4933
4938
|
useEffect(() => {
|
|
4934
|
-
if (!
|
|
4939
|
+
if (!effectiveIsVisible || !isExecuting || !currentAgentId) {
|
|
4935
4940
|
return;
|
|
4936
4941
|
}
|
|
4937
4942
|
let disposed = false;
|
|
@@ -5031,7 +5036,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5031
5036
|
currentAgentId,
|
|
5032
5037
|
editContext?.sessionId,
|
|
5033
5038
|
editContext?.socketConnectionVersion,
|
|
5034
|
-
|
|
5039
|
+
effectiveIsVisible,
|
|
5035
5040
|
isExecuting,
|
|
5036
5041
|
appendToolUiEvent,
|
|
5037
5042
|
clearHeartbeatMessages,
|
|
@@ -5392,7 +5397,15 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5392
5397
|
setPrompt("");
|
|
5393
5398
|
setAllPlaceholdersFilled(false);
|
|
5394
5399
|
setInputPlaceholder("Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)");
|
|
5395
|
-
} })) : (_jsx("div", { className: "flex items-stretch gap-2", children: _jsx(Textarea, { ref: textareaRef,
|
|
5400
|
+
} })) : (_jsx("div", { className: "flex items-stretch gap-2", children: _jsx(Textarea, { ref: textareaRef,
|
|
5401
|
+
// Globally-named view transitions break with multiple
|
|
5402
|
+
// mounts of the same agent (split panes). Only the
|
|
5403
|
+
// focused pane sets the name so the splash → focused
|
|
5404
|
+
// animation still works without the others fighting
|
|
5405
|
+
// for the same key.
|
|
5406
|
+
style: effectiveIsFocused
|
|
5407
|
+
? { viewTransitionName: "assistant-chat-input" }
|
|
5408
|
+
: undefined, value: prompt, onChange: (e) => {
|
|
5396
5409
|
setPrompt(e.target.value);
|
|
5397
5410
|
if (!/\{\{[^{}]+\}\}|<<[^<>]+>>/.test(e.target.value)) {
|
|
5398
5411
|
setAllPlaceholdersFilled(false);
|
|
@@ -5406,7 +5419,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5406
5419
|
shouldMaintainFocusRef.current = false;
|
|
5407
5420
|
}, placeholder: readOnly
|
|
5408
5421
|
? "Read-only access — ask the owner for Full access to chat with this agent."
|
|
5409
|
-
: inputPlaceholder, className: "max-h-[250px] min-h-[80px] flex-1 resize-y overflow-y-auto text-[12px] lg:max-h-[450px]", "data-testid": "agent-terminal-prompt", disabled: isSubmitting || readOnly
|
|
5422
|
+
: inputPlaceholder, className: "max-h-[250px] min-h-[80px] flex-1 resize-y overflow-y-auto text-[12px] lg:max-h-[450px]", "data-testid": "agent-terminal-prompt", disabled: isSubmitting || readOnly, readOnly: !effectiveIsFocused, title: !effectiveIsFocused
|
|
5423
|
+
? "Editing in another pane — click here to take over"
|
|
5424
|
+
: undefined }) })), (() => {
|
|
5410
5425
|
const isInPlaceholderMode = activePlaceholderInput ||
|
|
5411
5426
|
(prompt && /\{\{[^{}]+\}\}|<<[^<>]+>>/.test(prompt));
|
|
5412
5427
|
const placeholderShowsOwnButtons = hideBottomControls && isInPlaceholderMode;
|
|
@@ -5642,7 +5657,11 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5642
5657
|
setPrompt("");
|
|
5643
5658
|
setAllPlaceholdersFilled(false);
|
|
5644
5659
|
setInputPlaceholder("Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)");
|
|
5645
|
-
} })) : (_jsx("div", { className: "flex items-stretch gap-2", children: _jsx(Textarea, { ref: textareaRef,
|
|
5660
|
+
} })) : (_jsx("div", { className: "flex items-stretch gap-2", children: _jsx(Textarea, { ref: textareaRef,
|
|
5661
|
+
// See note above: pane-aware viewTransitionName.
|
|
5662
|
+
style: effectiveIsFocused
|
|
5663
|
+
? { viewTransitionName: "assistant-chat-input" }
|
|
5664
|
+
: undefined, value: prompt, onChange: (e) => {
|
|
5646
5665
|
setPrompt(e.target.value);
|
|
5647
5666
|
// Reset placeholder filled state when prompt changes
|
|
5648
5667
|
if (!/\{\{[^{}]+\}\}|<<[^<>]+>>/.test(e.target.value)) {
|
|
@@ -5658,7 +5677,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5658
5677
|
shouldMaintainFocusRef.current = false;
|
|
5659
5678
|
}, placeholder: readOnly
|
|
5660
5679
|
? "Read-only access — ask the owner for Full access to chat with this agent."
|
|
5661
|
-
: inputPlaceholder, className: "max-h-[250px] min-h-[80px] flex-1 resize-y overflow-y-auto text-[12px] lg:max-h-[450px]", "data-testid": "agent-terminal-prompt", disabled: isSubmitting || readOnly
|
|
5680
|
+
: inputPlaceholder, className: "max-h-[250px] min-h-[80px] flex-1 resize-y overflow-y-auto text-[12px] lg:max-h-[450px]", "data-testid": "agent-terminal-prompt", disabled: isSubmitting || readOnly, readOnly: !effectiveIsFocused, title: !effectiveIsFocused
|
|
5681
|
+
? "Editing in another pane — click here to take over"
|
|
5682
|
+
: undefined }) })), (() => {
|
|
5662
5683
|
const isInPlaceholderMode = activePlaceholderInput ||
|
|
5663
5684
|
(prompt && /\{\{[^{}]+\}\}|<<[^<>]+>>/.test(prompt));
|
|
5664
5685
|
const placeholderShowsOwnButtons = hideBottomControls && isInPlaceholderMode;
|