@parhelia/core 0.1.12763 → 0.1.12766

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 (46) hide show
  1. package/dist/agents-view/AgentsWorkspaceView.js +126 -15
  2. package/dist/agents-view/AgentsWorkspaceView.js.map +1 -1
  3. package/dist/editor/ai/AgentPanesGrid.d.ts +28 -0
  4. package/dist/editor/ai/AgentPanesGrid.js +62 -0
  5. package/dist/editor/ai/AgentPanesGrid.js.map +1 -0
  6. package/dist/editor/ai/AgentTerminal.d.ts +6 -1
  7. package/dist/editor/ai/AgentTerminal.js +142 -121
  8. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  9. package/dist/editor/ai/InlineAiDialog.js +20 -19
  10. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  11. package/dist/editor/ai/ToolCallDisplay.js +3 -0
  12. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  13. package/dist/editor/ai/agentDialogRegistry.d.ts +26 -0
  14. package/dist/editor/ai/agentDialogRegistry.js +221 -0
  15. package/dist/editor/ai/agentDialogRegistry.js.map +1 -0
  16. package/dist/editor/ai/agentPanesTypes.d.ts +27 -0
  17. package/dist/editor/ai/agentPanesTypes.js +16 -0
  18. package/dist/editor/ai/agentPanesTypes.js.map +1 -0
  19. package/dist/editor/ai/useAgentPanes.d.ts +33 -0
  20. package/dist/editor/ai/useAgentPanes.js +357 -0
  21. package/dist/editor/ai/useAgentPanes.js.map +1 -0
  22. package/dist/editor/ai/useAgentPanes.test.d.ts +1 -0
  23. package/dist/editor/ai/useAgentPanes.test.js +198 -0
  24. package/dist/editor/ai/useAgentPanes.test.js.map +1 -0
  25. package/dist/editor/client/EditorShell.js +3 -0
  26. package/dist/editor/client/EditorShell.js.map +1 -1
  27. package/dist/editor/services/agentService.js +4 -16
  28. package/dist/editor/services/agentService.js.map +1 -1
  29. package/dist/editor/services/agentSubscriptionRegistry.d.ts +7 -0
  30. package/dist/editor/services/agentSubscriptionRegistry.js +77 -0
  31. package/dist/editor/services/agentSubscriptionRegistry.js.map +1 -0
  32. package/dist/editor/services/agentSubscriptionRegistry.test.d.ts +1 -0
  33. package/dist/editor/services/agentSubscriptionRegistry.test.js +87 -0
  34. package/dist/editor/services/agentSubscriptionRegistry.test.js.map +1 -0
  35. package/dist/revision.d.ts +2 -2
  36. package/dist/revision.js +2 -2
  37. package/dist/task-board/TaskBoardWorkspace.js +3 -3
  38. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  39. package/dist/task-board/components/ProjectDashboard.d.ts +3 -0
  40. package/dist/task-board/components/ProjectDashboard.js +26 -3
  41. package/dist/task-board/components/ProjectDashboard.js.map +1 -1
  42. package/dist/task-board/types.d.ts +4 -0
  43. package/dist/test/setup.d.ts +0 -0
  44. package/dist/test/setup.js +37 -0
  45. package/dist/test/setup.js.map +1 -0
  46. 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, getVisibleDialogRegistry, isAgentErrorStatusValue, isFinishedServerExecutionStatus, isHeartbeatRunEventMessage, mergeAgentOperationHistory, normalizeDialogAgentId, normalizeProfileAllowanceOperations, normalizeServerExecutionStatus, } from "./agentMessageHelpers";
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 = true, 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, }) {
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
- normalizeDialogAgentId(agentStubIdRefForDialogs.current),
135
- normalizeDialogAgentId(agentIdRefForDialogs.current),
136
- ].filter(Boolean);
137
- agentKeys.forEach((key) => {
138
- if (callbackId) {
139
- visibleRegistry[key] = {
140
- callbackId,
141
- terminalInstanceId,
142
- };
143
- }
144
- else {
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 active to get latest messages
2565
- const previousIsActiveRef = useRef(isActive);
2588
+ // Reload agent when tab becomes visible to get latest messages
2589
+ const previousIsVisibleRef = useRef(effectiveIsVisible);
2566
2590
  useEffect(() => {
2567
- const wasInactive = !previousIsActiveRef.current;
2568
- const isNowActive = isActive;
2569
- previousIsActiveRef.current = isActive;
2570
- if (wasInactive && isNowActive && agent) {
2591
+ const wasHidden = !previousIsVisibleRef.current;
2592
+ const isNowVisible = effectiveIsVisible;
2593
+ previousIsVisibleRef.current = effectiveIsVisible;
2594
+ if (wasHidden && isNowVisible && agent) {
2571
2595
  loadAgent();
2572
2596
  }
2573
- }, [isActive, agent?.id, loadAgent]);
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 active
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 (!isActive || !addListener) {
3356
- // Unsubscribe if we were previously subscribed
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
- // Send subscription message to server
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
- // Send unsubscribe message to server
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
- isActive,
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(isActive);
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 = isActive;
3486
- }, [isActive]);
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 globalListeners = (globalThis.__agentDialogMountedAgents ?? []).filter((x) => typeof x === "string");
3497
- const normalizedAgentStubId = normalizeDialogAgentId(agentStubIdRefForDialogs.current);
3498
- if (normalizedAgentStubId &&
3499
- !globalListeners.includes(normalizedAgentStubId)) {
3500
- globalListeners.push(normalizedAgentStubId);
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
- const agentMatch = !requestAgentId ||
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
- const mounted = (globalThis.__agentDialogMountedAgents ?? []).filter((x) => typeof x === "string");
3559
- globalThis.__agentDialogMountedAgents = mounted.filter((id) => id !== normalizeDialogAgentId(agentStubIdRefForDialogs.current));
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 (!isActive || !isExecuting || !currentAgentId) {
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
- isActive,
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, style: { viewTransitionName: "assistant-chat-input" }, value: prompt, onChange: (e) => {
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, style: { viewTransitionName: "assistant-chat-input" }, value: prompt, onChange: (e) => {
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;