newmark-agent 0.3.10 → 0.3.12

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/config.example.json +6 -0
  2. package/dist/cli-commands.d.ts +7 -0
  3. package/dist/cli-commands.js +206 -15
  4. package/dist/cli-discovery.d.ts +15 -0
  5. package/dist/cli-discovery.js +182 -0
  6. package/dist/cli-help.d.ts +4 -0
  7. package/dist/cli-help.js +46 -0
  8. package/dist/conversation-utility-host.bundle.cjs +548 -137
  9. package/dist/core/agent.d.ts +18 -3
  10. package/dist/core/agent.js +236 -34
  11. package/dist/core/agentKernelRunner.js +72 -10
  12. package/dist/core/browserControl.d.ts +8 -0
  13. package/dist/core/browserUsePageAdapter.d.ts +3 -0
  14. package/dist/core/browserUsePageAdapter.js +19 -2
  15. package/dist/core/computerUseSession.d.ts +44 -0
  16. package/dist/core/computerUseSession.js +105 -0
  17. package/dist/core/config.d.ts +7 -2
  18. package/dist/core/config.js +24 -6
  19. package/dist/core/conversationKernel.d.ts +1 -0
  20. package/dist/core/conversationKernel.js +39 -1
  21. package/dist/core/electronBrowserUseHost.js +7 -0
  22. package/dist/core/electronUtilityAgentClient.js +84 -2
  23. package/dist/core/electronUtilityRuntimePool.d.ts +11 -0
  24. package/dist/core/electronUtilityRuntimePool.js +62 -0
  25. package/dist/core/flow-runner.js +1 -1
  26. package/dist/core/modelValidationStore.d.ts +4 -1
  27. package/dist/core/modelValidationStore.js +7 -1
  28. package/dist/core/utilityHostToolRouter.d.ts +7 -0
  29. package/dist/core/utilityHostToolRouter.js +25 -33
  30. package/dist/core/workspace.d.ts +6 -0
  31. package/dist/core/workspace.js +14 -0
  32. package/dist/core/wslAgentRuntimePool.d.ts +4 -0
  33. package/dist/core/wslAgentRuntimePool.js +56 -0
  34. package/dist/launcher.js +51 -10
  35. package/dist/llm/provider.d.ts +8 -5
  36. package/dist/llm/provider.js +85 -33
  37. package/dist/main.js +297 -61
  38. package/dist/preload.js +10 -2
  39. package/dist/providers/chat-completions.adapter.js +1 -5
  40. package/dist/providers/provider-events.d.ts +7 -0
  41. package/dist/providers/provider-events.js +44 -0
  42. package/dist/providers/responses.adapter.js +1 -3
  43. package/dist/tools/index.js +36 -49
  44. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  45. package/dist/tui/src/app.js +47 -13
  46. package/dist/tui/src/render.js +23 -7
  47. package/dist/tui/src/state.js +61 -9
  48. package/dist/ui/index.html +545 -122
  49. package/dist/wsl-agent-host.bundle.cjs +548 -137
  50. package/package.json +14 -5
@@ -5461,6 +5461,7 @@ html.newmark-memory-overview-viewer body {
5461
5461
  <button class="shell-btn browser-action-btn" onclick="window.browserReload()" title="Reload"><svg class="nm-icon small" aria-hidden="true" focusable="false"><use href="#refresh-cw"></use></svg><span class="icon-only-label">Reload</span></button>
5462
5462
  <input class="browser-url-input" id="browser-url" placeholder="Enter URL..." onkeydown="if(event.key==='Enter')window.navigateBrowser(this.value)">
5463
5463
  <button class="shell-btn" onclick="window.navigateBrowser(document.getElementById('browser-url').value)" title="Go"><svg class="nm-icon small" aria-hidden="true" focusable="false"><use href="#send"></use></svg><span class="icon-only-label">Go</span></button>
5464
+ <button class="shell-btn" id="computer-use-toggle" onclick="window.toggleComputerUse()" title="ComputerUse: enabled for this conversation" aria-pressed="true">CU</button>
5464
5465
  </div>
5465
5466
  <div class="browser-frame" id="browser-frame">
5466
5467
  <div class="browser-guest-placeholder" id="browser-guest-placeholder">Browser loads on first use</div>
@@ -5681,6 +5682,7 @@ var state = {
5681
5682
  terminalInterruptTimeoutMs: 0,
5682
5683
  nativeTools: [],
5683
5684
  nativeToolEnabled: {},
5685
+ computerUseByTarget: {},
5684
5686
  sshConnections: [],
5685
5687
  _layoutSaveTimer: null,
5686
5688
  _sendInFlight: false
@@ -6061,6 +6063,7 @@ var NEWMARK_I18N = {
6061
6063
  'flow.nextDisabled': 'Flow only accepts Guide input; Next is disabled.',
6062
6064
  'flow.saved': 'Flow saved',
6063
6065
  'flow.emptyRun': 'Flow is empty and cannot run.',
6066
+ 'flow.noAvailable': 'No workflows are available to run.',
6064
6067
  'flow.started': 'Flow started',
6065
6068
  'plugins.title': 'Plugins',
6066
6069
  'plugins.mcp': 'MCP Management',
@@ -6626,6 +6629,7 @@ var NEWMARK_I18N = {
6626
6629
  'flow.nextDisabled': 'Flow 仅允许 Guide 输入,已禁用 Next。',
6627
6630
  'flow.saved': 'Flow 已保存',
6628
6631
  'flow.emptyRun': 'Flow 为空,无法运行。',
6632
+ 'flow.noAvailable': '当前没有可运行的工作流。',
6629
6633
  'flow.started': 'Flow 已启动',
6630
6634
  'plugins.title': '插件',
6631
6635
  'plugins.mcp': 'MCP 管理',
@@ -8327,15 +8331,26 @@ function addWorkReview(diffs) {
8327
8331
  return review;
8328
8332
  }
8329
8333
 
8330
- function updateMsg(div, text, role, mode, model) {
8334
+ var STREAMING_PLAIN_TEXT_THRESHOLD = 12000;
8335
+ var STREAMING_LARGE_TEXT_INTERVAL_MS = 100;
8336
+
8337
+ function updateMsg(div, text, role, mode, model, options) {
8331
8338
  if (!div) return;
8332
8339
  if (role) div.className = 'chat-msg ' + role;
8333
8340
  text = redactSensitiveText(text);
8334
8341
  div._newmarkMessageText = String(text || '');
8335
8342
  var body = div.querySelector('.msg-body');
8336
- if (body) body.innerHTML = renderMessageContent(text);
8343
+ var streaming = !!(options && options.streaming);
8344
+ if (body) {
8345
+ // Re-running the full Markdown parser and rebuilding the entire message
8346
+ // DOM for every streamed token makes long responses monopolize the
8347
+ // renderer event loop. Keep large in-flight responses as one safe text
8348
+ // node; the terminal `done`/`final_response` path renders Markdown once.
8349
+ if (streaming && String(text || '').length >= STREAMING_PLAIN_TEXT_THRESHOLD) body.textContent = text;
8350
+ else body.innerHTML = renderMessageContent(text);
8351
+ }
8337
8352
  var meta = div.querySelector('.meta');
8338
- if (meta && (mode || model)) {
8353
+ if (meta && (mode || model) && !streaming) {
8339
8354
  var roleLabel = div.classList.contains('user')
8340
8355
  ? (String(mode || '').toLowerCase() === 'flow-user-input' ? t('flow.userInput') : t('message.user'))
8341
8356
  : (div.classList.contains('workflow') ? t('message.workflow') : (div.classList.contains('system') ? t('message.system') : t('message.agent')));
@@ -8744,6 +8759,60 @@ function setConversationRuntimeState(target, status, runId, extra) {
8744
8759
  return next;
8745
8760
  }
8746
8761
 
8762
+ // A renderer send marks its target as provisionally running before the IPC
8763
+ // request reaches the target runtime. Escape/Stop can therefore arrive in
8764
+ // that handoff window while the backend still reports `not_running`. Remember
8765
+ // the exact provisional run so the pending send can be cancelled before it
8766
+ // allocates a provider request; once the backend has a real run, the normal
8767
+ // target-scoped stop path takes over.
8768
+ function markProvisionalStop(target, runId) {
8769
+ if (!state.pendingProvisionalStops) state.pendingProvisionalStops = {};
8770
+ state.pendingProvisionalStops[runtimeKeyFor(target.workspaceId, target.conversationId)] = String(runId || '');
8771
+ }
8772
+
8773
+ function pendingProvisionalStopRunId(target) {
8774
+ var key = runtimeKeyFor(target.workspaceId, target.conversationId);
8775
+ if (!state.pendingProvisionalStops || !Object.prototype.hasOwnProperty.call(state.pendingProvisionalStops, key)) return undefined;
8776
+ return state.pendingProvisionalStops[key];
8777
+ }
8778
+
8779
+ function clearProvisionalStop(target) {
8780
+ if (state.pendingProvisionalStops) delete state.pendingProvisionalStops[runtimeKeyFor(target.workspaceId, target.conversationId)];
8781
+ }
8782
+
8783
+ function consumeProvisionalStop(target, runId) {
8784
+ var pending = pendingProvisionalStopRunId(target);
8785
+ if (pending === undefined || String(pending) !== String(runId || '')) return false;
8786
+ clearProvisionalStop(target);
8787
+ return true;
8788
+ }
8789
+
8790
+ function requestBackendStopForProvisionalStart(target, runId) {
8791
+ var stopPromise = api.stopConversation
8792
+ ? api.stopConversation({ target: target, runId: String(runId || ''), force: false })
8793
+ : (api.abortConversation ? api.abortConversation(target) : Promise.resolve({ action: 'not_running' }));
8794
+ Promise.resolve(stopPromise).then(function(result) {
8795
+ var action = String(result && result.action || '');
8796
+ if (action === 'not_running' || action === 'stale') {
8797
+ return refreshConversationRuntimeAfterStopRace(target, runId, result);
8798
+ }
8799
+ if (action === 'force' || String(result && result.status || '') === 'force_interrupted') {
8800
+ setConversationRuntimeState(target, 'force_interrupted', runId, { provisional: false });
8801
+ return;
8802
+ }
8803
+ if (action === 'graceful') {
8804
+ setConversationRuntimeState(target, 'stopping', runId, { provisional: false });
8805
+ return;
8806
+ }
8807
+ if (result === true || ['idle', 'interrupted'].indexOf(String(result && result.status || '')) >= 0) {
8808
+ setConversationRuntimeState(target, 'interrupted', runId, { provisional: false });
8809
+ }
8810
+ }).catch(function(error) {
8811
+ setConversationRuntimeState(target, 'running', runId, { provisional: false });
8812
+ showUiNotice(error && error.message ? error.message : String(error), 'error', 'stop-provisional-' + runtimeKeyFor(target.workspaceId, target.conversationId) + '-' + String(runId || ''));
8813
+ });
8814
+ }
8815
+
8747
8816
  function formatWorkDuration(milliseconds) {
8748
8817
  var seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000));
8749
8818
  var hours = Math.floor(seconds / 3600);
@@ -9474,7 +9543,14 @@ function renderWorkRunEvents(run, includeGuides) {
9474
9543
  var responseLabel = type === 'response' && terminalInterrupted
9475
9544
  ? (currentLang() === 'zh' ? '未完成回复片段\n' : 'Incomplete response fragment\n') + workEventLabel(event)
9476
9545
  : workEventLabel(event);
9477
- return '<div class="conversation-work-event narrative"><div class="conversation-work-event-content">' + renderMessageContent(responseLabel) + '</div></div>';
9546
+ // While a run is still receiving text, this view can be rebuilt once per
9547
+ // event. Avoid invoking the full Markdown parser for a large live
9548
+ // narrative; the final response path still gets the normal rich render.
9549
+ var liveNarrative = ['running', 'stopping', 'force_restarting'].indexOf(String(run && run.status || '').toLowerCase()) >= 0;
9550
+ var narrativeHtml = liveNarrative && String(responseLabel || '').length >= STREAMING_PLAIN_TEXT_THRESHOLD
9551
+ ? esc(responseLabel)
9552
+ : renderMessageContent(responseLabel);
9553
+ return '<div class="conversation-work-event narrative"><div class="conversation-work-event-content">' + narrativeHtml + '</div></div>';
9478
9554
  }
9479
9555
  if (type.indexOf('guide') === 0 || event.guide) return renderWorkRunGuideMessage(event);
9480
9556
  if (type === 'tool_group') return renderWorkToolGroup(event, eventIndex);
@@ -9594,17 +9670,22 @@ function renderConversationWorkRuns(runs, target) {
9594
9670
  return normalized;
9595
9671
  }
9596
9672
 
9597
- var pendingLargeWorkRunRenders = {};
9598
- var scheduledLargeWorkRunRenders = {};
9599
- function scheduleLargeConversationWorkRunRender(run) {
9673
+ // Work events can arrive much faster than the browser can paint them (text
9674
+ // deltas are especially noisy). Keep the authoritative run in memory for
9675
+ // every event, but coalesce its DOM replacement to one animation frame. This
9676
+ // is important for long real-model runs: replacing the whole work block for
9677
+ // every delta can starve Chromium's renderer/CDP event loop.
9678
+ var pendingConversationWorkRunRenders = {};
9679
+ var scheduledConversationWorkRunRenders = {};
9680
+ function scheduleConversationWorkRunRender(run) {
9600
9681
  var key = runtimeKeyFor(run.target && run.target.workspaceId, run.target && run.target.conversationId) + '::' + String(run.runId || '');
9601
- pendingLargeWorkRunRenders[key] = run;
9602
- if (scheduledLargeWorkRunRenders[key]) return;
9603
- scheduledLargeWorkRunRenders[key] = true;
9682
+ pendingConversationWorkRunRenders[key] = run;
9683
+ if (scheduledConversationWorkRunRenders[key]) return;
9684
+ scheduledConversationWorkRunRenders[key] = true;
9604
9685
  requestAnimationFrame(function() {
9605
- var latest = pendingLargeWorkRunRenders[key];
9606
- delete pendingLargeWorkRunRenders[key];
9607
- delete scheduledLargeWorkRunRenders[key];
9686
+ var latest = pendingConversationWorkRunRenders[key];
9687
+ delete pendingConversationWorkRunRenders[key];
9688
+ delete scheduledConversationWorkRunRenders[key];
9608
9689
  if (latest && isActiveConversationTarget(latest.target)) renderConversationWorkRun(latest);
9609
9690
  });
9610
9691
  }
@@ -9640,7 +9721,9 @@ function applyAgentWorkEventToRun(event) {
9640
9721
  var priorGuideEvent = run.events[guideEventIndex];
9641
9722
  if (guideWorkEventKey(priorGuideEvent) === incomingGuideKey) {
9642
9723
  run.events[guideEventIndex] = mergeGuideWorkEvent(priorGuideEvent, event);
9643
- updateConversationWorkRunElement(run);
9724
+ if (isActiveConversationTarget(target) && (!branchIds.viewed || !eventBranchId || branchIds.viewed === eventBranchId)) {
9725
+ scheduleConversationWorkRunRender(run);
9726
+ }
9644
9727
  return run;
9645
9728
  }
9646
9729
  }
@@ -9667,8 +9750,8 @@ function applyAgentWorkEventToRun(event) {
9667
9750
  if (isActiveConversationTarget(target) && (!branchIds.viewed || !eventBranchId || branchIds.viewed === eventBranchId)) {
9668
9751
  state.workRunsByTarget[runtimeKeyFor(target.workspaceId, target.conversationId)] = runs;
9669
9752
  var existingElement = findWorkRunElement(run);
9670
- if (existingElement && run.events.length >= 12 && (type === 'tool_call' || type === 'tool_result')) scheduleLargeConversationWorkRunRender(run);
9671
- else renderConversationWorkRun(run);
9753
+ if (!existingElement || type === 'start') renderConversationWorkRun(run);
9754
+ else scheduleConversationWorkRunRender(run);
9672
9755
  }
9673
9756
  return run;
9674
9757
  }
@@ -10214,7 +10297,13 @@ function applyStreamingWorkflowText(ui, eventConversationId, mode, model) {
10214
10297
  }
10215
10298
  return;
10216
10299
  }
10217
- updateMsg(ensureActiveAssistantMsg(mode, model, eventConversationId), ui.activeWorkflowText, 'assistant', mode, model);
10300
+ var largeText = ui.activeWorkflowText.length >= STREAMING_PLAIN_TEXT_THRESHOLD;
10301
+ if (largeText) {
10302
+ var now = Date.now();
10303
+ if (ui._lastLargeStreamRenderAt && now - ui._lastLargeStreamRenderAt < STREAMING_LARGE_TEXT_INTERVAL_MS) return;
10304
+ ui._lastLargeStreamRenderAt = now;
10305
+ }
10306
+ updateMsg(ensureActiveAssistantMsg(mode, model, eventConversationId), ui.activeWorkflowText, 'assistant', mode, model, { streaming: true });
10218
10307
  }
10219
10308
 
10220
10309
  function scheduleStreamingWorkflowTextFlush(ui, eventConversationId, eventWorkspaceId) {
@@ -10245,6 +10334,7 @@ function renderAgentWorkEvent(event) {
10245
10334
  ui.activeWorkflowText = '';
10246
10335
  ui.lastCompletedWorkflow = null;
10247
10336
  ui._streamFlushPending = false;
10337
+ ui._lastLargeStreamRenderAt = 0;
10248
10338
  } else if (type === 'text') {
10249
10339
  ui.activeWorkflowText = (ui.activeWorkflowText || '') + content;
10250
10340
  if (workRun) return;
@@ -10255,6 +10345,7 @@ function renderAgentWorkEvent(event) {
10255
10345
  ui.activeWorkflowText = '';
10256
10346
  ui.activeWorkflowMsg = null;
10257
10347
  ui._streamFlushPending = false;
10348
+ ui._lastLargeStreamRenderAt = 0;
10258
10349
  } else if (type === 'final_response' && workRun) {
10259
10350
  ui.activeWorkflowText = '';
10260
10351
  ui.activeWorkflowMsg = null;
@@ -10288,6 +10379,7 @@ function renderAgentWorkEvent(event) {
10288
10379
  }
10289
10380
  ui.activeWorkflowMsg = null;
10290
10381
  ui.activeWorkflowText = '';
10382
+ ui._lastLargeStreamRenderAt = 0;
10291
10383
  finishToolBatch(eventConversationId);
10292
10384
  } else if (type === 'error') {
10293
10385
  flushStreamingWorkflowTextNow(ui, eventConversationId, event.mode || state.mode, event.model || state.model);
@@ -10357,7 +10449,14 @@ function appendAgentWorkEvent(event) {
10357
10449
  var visibleRuntimeBranch = active && isViewingRuntimeConversationBranch(target);
10358
10450
  if (active) markConversationTracked(id, state.conversationTrackMs || 300000, workspaceId);
10359
10451
  if (active || isConversationTracked(id, workspaceId)) cacheAgentWorkEvent(event);
10360
- if (event.type === 'start') setConversationRuntimeState(target, event.status || 'running', event.runId || '', { provisional: false, generation: event.generation || 0, runtimeKey: event.runtimeKey || '' });
10452
+ var pendingProvisionalStop = (event.type === 'start' || event.status === 'running')
10453
+ ? pendingProvisionalStopRunId(target)
10454
+ : undefined;
10455
+ if (pendingProvisionalStop !== undefined && event.runId) {
10456
+ clearProvisionalStop(target);
10457
+ setConversationRuntimeState(target, 'stopping', event.runId, { provisional: false, generation: event.generation || 0, runtimeKey: event.runtimeKey || '' });
10458
+ requestBackendStopForProvisionalStart(target, event.runId);
10459
+ } else if (event.type === 'start') setConversationRuntimeState(target, event.status || 'running', event.runId || '', { provisional: false, generation: event.generation || 0, runtimeKey: event.runtimeKey || '' });
10361
10460
  else if (event.status && ['running', 'stopping', 'force_restarting'].indexOf(String(event.status)) >= 0) setConversationRuntimeState(target, event.status, event.runId || '');
10362
10461
  else if (event.status && ['completed', 'interrupted', 'force_interrupted', 'error'].indexOf(String(event.status)) >= 0) {
10363
10462
  setConversationRuntimeState(target, event.status, event.runId || '');
@@ -10459,7 +10558,14 @@ function hydrateConversationBranchState(snapshot) {
10459
10558
  }
10460
10559
 
10461
10560
  function applyConversationSnapshot(s, requestedConversationId) {
10462
- if (!s) return;
10561
+ if (!s) return;
10562
+ if (s.computerUse) {
10563
+ var computerTarget = currentConversationTarget(s.conversationId || requestedConversationId || activeConversationId());
10564
+ var computerKey = computerUseTargetKey(computerTarget);
10565
+ if (!state.computerUseByTarget) state.computerUseByTarget = {};
10566
+ state.computerUseByTarget[computerKey] = s.computerUse;
10567
+ if (isActiveConversationTarget(computerTarget)) renderComputerUseToggle(s.computerUse, computerTarget);
10568
+ }
10463
10569
  state.goalText = s.goal && s.goal.objective ? String(s.goal.objective) : '';
10464
10570
  state.goalVisible = !!state.goalText;
10465
10571
  state.goalPaused = !!(s.goal && s.goal.paused);
@@ -10813,6 +10919,44 @@ function isCurrentConversationRunning() {
10813
10919
  return !!(id && runningConversationRecord(id));
10814
10920
  }
10815
10921
 
10922
+ // A terminal final_response event can reach the renderer before the awaited
10923
+ // send promise performs its final local cleanup. If a user submits in that
10924
+ // small window, trusting the stale local running/active-send flags silently
10925
+ // drops the new message. Reconcile only when a send is already considered
10926
+ // active; a backend-confirmed running snapshot still keeps the guard intact.
10927
+ async function reconcileStaleLocalRuntimeBeforeSend(target) {
10928
+ if (!target || !api.getState) return false;
10929
+ var snapshot;
10930
+ try {
10931
+ snapshot = await api.getState(target);
10932
+ } catch (_) {
10933
+ return false;
10934
+ }
10935
+ var runtimeSnapshot = snapshot && snapshot.runtime || null;
10936
+ var snapshotStatus = String(snapshot && snapshot.status || '').toLowerCase();
10937
+ var backendStillRunning = !!(runtimeSnapshot && (runtimeSnapshot.running || runtimeSnapshot.stopRequested))
10938
+ || ['running', 'stopping', 'force_restarting'].indexOf(snapshotStatus) >= 0;
10939
+ if (backendStillRunning) return false;
10940
+ if (['idle', 'completed', 'interrupted', 'force_interrupted', 'error'].indexOf(snapshotStatus) < 0) return false;
10941
+ var runId = String((runtimeSnapshot && runtimeSnapshot.runId) || snapshot.runId || '');
10942
+ var generation = Number((runtimeSnapshot && runtimeSnapshot.generation) || snapshot.generation || 0);
10943
+ setConversationRuntimeState(target, snapshotStatus, runId, { provisional: false, generation: generation });
10944
+ var targetKey = runtimeKeyFor(target.workspaceId, target.conversationId);
10945
+ if (state.activeSendCallsByTarget) {
10946
+ delete state.activeSendCallsByTarget[targetKey];
10947
+ delete state.activeSendCallsByTarget[runtimeKeyFor('', target.conversationId)];
10948
+ }
10949
+ if (state.nextQueueSchedulesByTarget) {
10950
+ delete state.nextQueueSchedulesByTarget[targetKey];
10951
+ delete state.nextQueueSchedulesByTarget[runtimeKeyFor('', target.conversationId)];
10952
+ }
10953
+ state._sendInFlight = Object.keys(state.runningConversations || {}).length > 0;
10954
+ setWorking(state._sendInFlight);
10955
+ updateSubmitButtonState();
10956
+ if (window.renderInputStack) window.renderInputStack();
10957
+ return true;
10958
+ }
10959
+
10816
10960
  function setSubmitButtonVisual(icon, label, running, stopMode) {
10817
10961
  var btn = els['submit-btn'] || document.getElementById('submit-btn');
10818
10962
  if (!btn) return;
@@ -10923,18 +11067,33 @@ window.stopCurrentConversation = async function() {
10923
11067
  if (!conversationId || !runtime) return false;
10924
11068
  var target = runtime.target || currentConversationTarget(conversationId);
10925
11069
  var runId = String(runtime.runId || '');
11070
+ var provisional = runtime.provisional === true;
10926
11071
  if (String(runtime.status || '') === 'force_restarting') return false;
11072
+ if (provisional) markProvisionalStop(target, runId);
10927
11073
  var force = String(runtime.status || '') === 'stopping';
10928
11074
  if (typeof pauseQueueForTarget === 'function') pauseQueueForTarget(target);
10929
11075
  setConversationRuntimeState(target, force ? 'force_restarting' : 'stopping', runId);
10930
11076
  updateSubmitButtonState();
10931
11077
  renderConversations();
11078
+ if (provisional) {
11079
+ // The backend may not have allocated a utility runtime yet. Keep the
11080
+ // exact local cancellation marker and let sendMessage consume it, or let
11081
+ // the first backend start event issue a stop with the real run id. An
11082
+ // empty-run stop request here races runtime allocation and can return
11083
+ // not_running before the prompt reaches the worker.
11084
+ if (window.renderInputStack) window.renderInputStack();
11085
+ return true;
11086
+ }
10932
11087
  try {
10933
11088
  var result;
10934
11089
  // The target runtime supervisor owns the checkpoint + cooperative-stop
10935
11090
  // transaction. A separate awaited checkpoint can itself be starved by a
10936
11091
  // blocked worker and would prevent the supervisor from recording the first
10937
11092
  // stop, making a second-click hard restart impossible.
11093
+ // A provisional renderer run has no backend run id yet. Omitting the
11094
+ // expected id lets a concurrently-created backend run be stopped, while
11095
+ // the pending marker above cancels a request that has not reached the
11096
+ // runtime at all.
10938
11097
  if (api.stopConversation) result = await api.stopConversation({ target: target, runId: runId, force: force });
10939
11098
  else if (api.abortConversation) result = await api.abortConversation(currentConversationTarget(conversationId));
10940
11099
  var resultAction = String(result && result.action || '');
@@ -10945,6 +11104,9 @@ window.stopCurrentConversation = async function() {
10945
11104
  await refreshConversationRuntimeAfterStopRace(target, runId, result);
10946
11105
  return true;
10947
11106
  }
11107
+ if (provisional && state.pendingProvisionalStops) {
11108
+ delete state.pendingProvisionalStops[runtimeKeyFor(target.workspaceId, target.conversationId)];
11109
+ }
10948
11110
  var latestAfterStop = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(target.workspaceId, target.conversationId)];
10949
11111
  if (!force && latestAfterStop && String(latestAfterStop.runId || '') === runId && String(latestAfterStop.status || '') !== 'stopping') {
10950
11112
  // A second click or a terminal worker event won the race while the first
@@ -11837,6 +11999,13 @@ window.endQueueDrag = function(event) {
11837
11999
  if (event.currentTarget) event.currentTarget.classList.remove('dragging');
11838
12000
  };
11839
12001
 
12002
+ function recoverableGuideRejection(receipt) {
12003
+ if (!receipt || String(receipt.status || '').toLowerCase() !== 'rejected') return false;
12004
+ var reason = String(receipt.reason || receipt.error || '').trim();
12005
+ return reason === 'Target conversation is not running'
12006
+ || reason === 'Guide runId does not match the active run';
12007
+ }
12008
+
11840
12009
  window.sendMessage = async function(modeOverride, queuedText, opts) {
11841
12010
  if (!window.requireWorkspace('send')) return;
11842
12011
  opts = opts || {};
@@ -11960,6 +12129,28 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
11960
12129
  var conversationRunning = !!activeRuntime;
11961
12130
  markConversationTracked(lockedConversationId, state.conversationTrackMs || 300000, lockedTarget.workspaceId);
11962
12131
 
12132
+ var activeSendCall = !!(state.activeSendCallsByTarget && state.activeSendCallsByTarget[lockedRuntimeKey]);
12133
+ if ((conversationRunning || activeSendCall) && api.getState) {
12134
+ var reconciledLocalRuntime = await reconcileStaleLocalRuntimeBeforeSend(lockedTarget);
12135
+ if (reconciledLocalRuntime) {
12136
+ activeRuntime = runningConversationRecord(lockedConversationId, lockedTarget.workspaceId);
12137
+ conversationRunning = !!activeRuntime;
12138
+ if (!conversationRunning && effectiveInputMode === 'guide') effectiveInputMode = 'next';
12139
+ }
12140
+ }
12141
+
12142
+ if (opts.retryAfterGuideRejection && conversationRunning && api.getState) {
12143
+ try {
12144
+ var freshRuntimeState = await api.getState(lockedTarget);
12145
+ var freshStatus = String(freshRuntimeState && freshRuntimeState.status || '').toLowerCase();
12146
+ if (['idle', 'completed', 'interrupted', 'force_interrupted', 'error'].indexOf(freshStatus) >= 0) {
12147
+ setConversationRuntimeState(lockedTarget, 'idle', '');
12148
+ activeRuntime = null;
12149
+ conversationRunning = false;
12150
+ }
12151
+ } catch (_) {}
12152
+ }
12153
+
11963
12154
  if (effectiveInputMode === 'next' && !opts.fromQueue && conversationRunning) {
11964
12155
  if (!state.nextQueue) state.nextQueue = [];
11965
12156
  if (!state.nextQueueRequests) state.nextQueueRequests = [];
@@ -12041,6 +12232,57 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12041
12232
  throw new Error(currentLang() === 'zh' ? '未收到有效的 Guide 回执' : 'A valid Guide acknowledgement was not received');
12042
12233
  }
12043
12234
  var status = normalizeGuideUiStatus(receipt.status);
12235
+ if (recoverableGuideRejection(receipt)) {
12236
+ // Renderer runtime state can lag the backend's finalization event by
12237
+ // one IPC turn. The Guide was explicitly rejected, so replaying the
12238
+ // exact payload as a fresh Build is safe and prevents a normal user
12239
+ // send from disappearing behind a stale Guide route.
12240
+ var fallbackRequest = {
12241
+ text: typeof requestMessage === 'string' ? requestMessage : String(requestMessage.text || ''),
12242
+ images: typeof requestMessage === 'object' && Array.isArray(requestMessage.images)
12243
+ ? requestMessage.images.map(function(image) { return Object.assign({}, image); })
12244
+ : []
12245
+ };
12246
+ recordGuideUiMessage({
12247
+ clientMessageId: clientMessageId,
12248
+ target: receipt.target || lockedTarget,
12249
+ runId: receipt.runId || envelope.runId,
12250
+ status: 'deferred',
12251
+ content: displayText,
12252
+ attachments: optimisticAttachments,
12253
+ createdAt: receipt.createdAt || envelope.createdAt,
12254
+ updatedAt: receipt.updatedAt,
12255
+ reason: currentLang() === 'zh'
12256
+ ? '运行已结束,输入已作为新的 Build 继续。'
12257
+ : 'The run ended before Guide delivery; the input continued as a new Build.'
12258
+ }, lockedTarget);
12259
+ applyAgentWorkEventToRun({
12260
+ id: 'guide-' + clientMessageId + '-deferred',
12261
+ type: 'guide_deferred',
12262
+ content: displayText,
12263
+ workspaceId: lockedTarget.workspaceId,
12264
+ conversationId: lockedTarget.conversationId,
12265
+ runId: receipt.runId || envelope.runId,
12266
+ sequence: receipt.sequence || 0,
12267
+ guideStatus: 'deferred',
12268
+ clientMessageId: clientMessageId,
12269
+ guide: guideMessagesForTarget(lockedTarget)[clientMessageId]
12270
+ });
12271
+ return Promise.resolve(window.sendMessage('next', undefined, {
12272
+ requestedMode: requestedMode,
12273
+ goalObjective: opts.goalObjective,
12274
+ forceBuild: true,
12275
+ retryAfterGuideRejection: true,
12276
+ queuedRequest: fallbackRequest
12277
+ })).then(function(fallbackResult) {
12278
+ return {
12279
+ ok: !(fallbackResult && fallbackResult.ok === false),
12280
+ guideReceipt: receipt,
12281
+ recoveredAsBuild: true,
12282
+ fallback: fallbackResult
12283
+ };
12284
+ });
12285
+ }
12044
12286
  var guideRecord = recordGuideUiMessage({
12045
12287
  clientMessageId: clientMessageId,
12046
12288
  target: receipt.target || lockedTarget,
@@ -12099,6 +12341,15 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12099
12341
  var executionMode = opts.goalDeclaration || requestedMode === 'goal' ? 'build'
12100
12342
  : ((opts.forceBuild || idleBuildNextImmediate) && requestedMode === 'build' ? 'build' : requestedMode);
12101
12343
  await syncConversationExecutionState(executionMode, effectiveInputMode);
12344
+ if (consumeProvisionalStop(lockedTarget, localRunId)) {
12345
+ // Stop/Escape won before the backend accepted the prompt. Do not send the
12346
+ // provider request after the user has already cancelled it.
12347
+ delete state.activeSendCallsByTarget[lockedRuntimeKey];
12348
+ setConversationRuntimeState(lockedTarget, 'interrupted', localRunId, { provisional: false });
12349
+ setWorking(!!runningConversationRecord(activeConversationId()));
12350
+ window.renderInputStack();
12351
+ return { status: 'interrupted', runId: localRunId, target: lockedTarget, cancelledBeforeStart: true };
12352
+ }
12102
12353
  var renderOnViewedBranch = !queuedRuntimeBranch || queueBranchPathForTarget(lockedTarget, 'viewed') === queueBranchPathForTarget(lockedTarget, 'runtime');
12103
12354
  if (isActiveConversationTarget(lockedTarget) && renderOnViewedBranch) {
12104
12355
  if (opts.resumeGuideRunId && requestMessage.clientMessageId) {
@@ -12128,10 +12379,12 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12128
12379
  renderAutoRouteRatingControls();
12129
12380
  }
12130
12381
  var responseMsg = null;
12382
+ var sendFailure = '';
12131
12383
  try {
12132
12384
  var sendPromise = api.sendMessage(requestMessage, lockedTarget);
12133
12385
  if (requestedMode === 'goal') activateSubmittedGoal(opts.goalObjective || rawText);
12134
12386
  var r = await sendPromise;
12387
+ if (r && r.error) sendFailure = String(r.error);
12135
12388
  if (isActiveConversationTarget(lockedTarget)) applyReturnedGoalState(r);
12136
12389
  if (r && r.runId) {
12137
12390
  var anchorStore = workRunAnchorIndexStore(lockedTarget);
@@ -12212,9 +12465,10 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12212
12465
  }
12213
12466
  }
12214
12467
  } catch(e) {
12468
+ sendFailure = e && e.message ? String(e.message) : String(e || 'Agent run failed.');
12215
12469
  if (isActiveConversationTarget(lockedTarget)) {
12216
12470
  responseMsg = conversationWorkUiState(lockedConversationId, lockedTarget.workspaceId).activeWorkflowMsg || addMsg('assistant', '', state.mode, state.model);
12217
- updateMsg(responseMsg, formatChatError(e && e.message, 'Agent run failed.'), 'error', state.mode, state.model);
12471
+ updateMsg(responseMsg, formatChatError(sendFailure, 'Agent run failed.'), 'error', state.mode, state.model);
12218
12472
  }
12219
12473
  }
12220
12474
  if (api.getState) {
@@ -12265,7 +12519,15 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12265
12519
  if (resumedModeResult != null) state._syncedMode = state.mode;
12266
12520
  }
12267
12521
  var finalRuntime = state.conversationRuntimeStates && state.conversationRuntimeStates[lockedRuntimeKey];
12268
- if (!finalRuntime || !finalRuntime.runId || finalRuntime.runId === localRunId || (r && finalRuntime.runId === r.runId)) {
12522
+ var finalRunMatches = !finalRuntime || !finalRuntime.runId || finalRuntime.runId === localRunId || (r && finalRuntime.runId === r.runId);
12523
+ if (sendFailure) {
12524
+ // The IPC contract returns terminal failures as { error } while the
12525
+ // target-scoped error work event may arrive on the next task. Never let
12526
+ // the renderer's provisional run fall through to completed in that gap.
12527
+ if (finalRunMatches) {
12528
+ setConversationRuntimeState(lockedTarget, 'error', (r && r.runId) || (finalRuntime && finalRuntime.runId) || localRunId, { provisional: false });
12529
+ }
12530
+ } else if (finalRunMatches) {
12269
12531
  setConversationRuntimeState(lockedTarget, 'completed', (r && r.runId) || localRunId);
12270
12532
  }
12271
12533
  setWorking(!!runningConversationRecord(activeConversationId()));
@@ -12683,6 +12945,7 @@ window.switchRightTab = function(tab) {
12683
12945
  if (tab === 'subagent') window.renderSubagentList();
12684
12946
  if (tab === 'browser') {
12685
12947
  cancelBrowserGuestIdleDestroy();
12948
+ window.refreshComputerUseState(currentConversationTarget());
12686
12949
  window.ensureBrowserPanel({ activate: true }).catch(function(error) {
12687
12950
  console.error('[Browser] guest initialization failed:', error && error.message ? error.message : String(error));
12688
12951
  });
@@ -13167,7 +13430,7 @@ window.terminalSend = function() {
13167
13430
  var startedPane = document.querySelector('.terminal-pane[data-session="' + resp.sessionId + '"]');
13168
13431
  var startedInput = startedPane && startedPane.querySelector('.terminal-input');
13169
13432
  if (startedInput && startedInput.value === cmd) startedInput.value = '';
13170
- return api.terminalWrite(resp.sessionId, cmd + '\r\n');
13433
+ return api.terminalWrite(resp.sessionId, cmd + '\r');
13171
13434
  }).catch(function(err) {
13172
13435
  input.disabled = false;
13173
13436
  var currentPane = window.getActiveTerminalPane();
@@ -13176,7 +13439,7 @@ window.terminalSend = function() {
13176
13439
  });
13177
13440
  }
13178
13441
  input.value = '';
13179
- api.terminalWrite(sessionId, cmd + '\r\n').catch(function(err) {
13442
+ api.terminalWrite(sessionId, cmd + '\r').catch(function(err) {
13180
13443
  if (output) output.innerHTML += '\r\n<span style="color:#ff6666;">[' + esc(t('common.error')) + '] ' + esc(err.message) + '</span>';
13181
13444
  });
13182
13445
  };
@@ -15019,36 +15282,12 @@ renderArchiveSettings = function() {
15019
15282
  };
15020
15283
 
15021
15284
  window.archiveCurrent = function() {
15022
- if (!api.archive) return;
15023
15285
  var currentId = activeConversationId();
15024
- if (currentId && runningConversationRecord(currentId)) {
15025
- showUiNotice(currentLang() === 'zh' ? '请先停止当前对话再归档。' : 'Stop the active conversation before archiving it.', 'error', 'archive-running-' + currentRuntimeKey(currentId));
15026
- return;
15027
- }
15028
- api.archive(currentConversationTarget(currentId)).then(function(receipt) {
15029
- if (!receipt || !receipt.ok) throw new Error((receipt && receipt.error) || 'Archive failed');
15030
- showUiNotice('[Archive] ' + t('archive.saved') + ': ' + receipt.fileName, 'success', 'archive-saved-' + receipt.conversationId);
15031
- return Promise.all([api.listArchives ? api.listArchives('workspace') : [], api.getState ? api.getState() : null]);
15032
- }).then(function(result) {
15033
- var items = result[0] || [];
15034
- var refreshed = result[1];
15035
- state.workspaceArchives = (items || []).map(function(a) {
15036
- return { id: a.id || a.name || String(a), name: a.name || String(a), firstLine: a.firstLine || '', date: a.date || '', scope: a.scope || 'workspace', workspace: a.workspace || state.currentWorkspace || '', restorable: !!a.restorable, conversationId: a.conversationId || '' };
15037
- });
15038
- state.allArchives = [];
15039
- if (refreshed && refreshed.conversations) {
15040
- state.activeBackendConversationId = refreshed.conversationId || state.activeBackendConversationId;
15041
- applyBackendConversations(refreshed.conversations, state.activeBackendConversationId, refreshed.workspaceId || runtimeWorkspaceId(''));
15042
- if (refreshed.chatMessages) renderChatMessages(refreshed.chatMessages);
15043
- renderConversations();
15044
- }
15045
- window.renderRightArchives();
15046
- var stab = document.getElementById('stab-archive');
15047
- if (stab) stab.innerHTML = renderArchiveSettings();
15048
- if (state.settingsActiveTab === 'archive') window.settingsTab('archive');
15049
- }).catch(function(err) {
15050
- addMsg('assistant', '[Archive] ' + t('workspace.saveFailed') + ': ' + (err.message || String(err)), 'error', '');
15051
- });
15286
+ if (!currentId || !window.archiveConv) return;
15287
+ // Use the same optimistic path as the conversation-row action. This keeps
15288
+ // the active conversation immediately gone even while a running/queued
15289
+ // runtime is being hard-stopped by the main process.
15290
+ return window.archiveConv(currentId);
15052
15291
  };
15053
15292
 
15054
15293
  window.loadArchive = function(idx, scope) {
@@ -16088,11 +16327,17 @@ window.loadFileTree = async function(options) {
16088
16327
  };
16089
16328
 
16090
16329
  window.submitSelectedFlow = function() {
16330
+ if (!state._flowsLoaded && window.ensureFlowsLoaded) {
16331
+ return window.ensureFlowsLoaded().then(function() { return window.submitSelectedFlow(); });
16332
+ }
16091
16333
  var selected = String(state.defaultFlow || (document.getElementById('flow-select') && document.getElementById('flow-select').value) || '');
16092
16334
  var workIdx = state.flowWorks.findIndex(function(work) { return String(work && work.name || '') === selected; });
16093
16335
  if (workIdx < 0) {
16094
- showUiNotice(currentLang() === 'zh' ? '请先选择一个 Flow。' : 'Select a Flow first.', 'error', 'flow-select-required');
16095
- return;
16336
+ var message = state.flowWorks.length
16337
+ ? (currentLang() === 'zh' ? '请先选择一个 Flow。' : 'Select a Flow first.')
16338
+ : t('flow.noAvailable');
16339
+ showUiNotice(message, 'error', 'flow-select-required');
16340
+ return { ok: false, error: message };
16096
16341
  }
16097
16342
  return window.runFlowWork(workIdx);
16098
16343
  };
@@ -16849,11 +17094,63 @@ var NEWMARK_BROWSER_PARTITION = 'persist:newmark-browser';
16849
17094
  var NEWMARK_BROWSER_MIN_CREATE_DELAY_MS = 5000;
16850
17095
  var NEWMARK_BROWSER_IDLE_DESTROY_MS = 60000;
16851
17096
  var browserGuestCreatePromise = null;
17097
+ var browserGuestCreatePromises = {};
16852
17098
  var browserGuestIdleTimer = null;
16853
17099
  var browserRetainedUrl = 'about:blank';
17100
+ var browserRetainedUrls = {};
16854
17101
  var browserGuestCreateCount = 0;
16855
17102
  var browserGuestFirstCreatedAtMs = null;
16856
17103
 
17104
+ function computerUseTargetKey(target) {
17105
+ target = target || currentConversationTarget();
17106
+ return runtimeKeyFor(target.workspaceId, target.conversationId);
17107
+ }
17108
+
17109
+ function renderComputerUseToggle(value, target) {
17110
+ var button = document.getElementById('computer-use-toggle');
17111
+ if (!button) return;
17112
+ var enabled = value && value.enabled !== false;
17113
+ var occupied = !!(value && value.occupied);
17114
+ var owner = value && value.ownerLabel ? ' · ' + String(value.ownerLabel) : '';
17115
+ button.classList.toggle('active', enabled);
17116
+ button.setAttribute('aria-pressed', enabled ? 'true' : 'false');
17117
+ button.title = (enabled ? 'ComputerUse enabled for this conversation' : 'ComputerUse disabled for this conversation') + owner;
17118
+ button.textContent = enabled ? (occupied ? 'CU●' : 'CU') : 'CU×';
17119
+ button.dataset.computerUseRuntimeKey = computerUseTargetKey(target);
17120
+ }
17121
+
17122
+ window.refreshComputerUseState = function(target) {
17123
+ target = target || currentConversationTarget();
17124
+ var key = computerUseTargetKey(target);
17125
+ var cached = state.computerUseByTarget && state.computerUseByTarget[key];
17126
+ if (cached) renderComputerUseToggle(cached, target);
17127
+ if (!api.computerUseState) return Promise.resolve(cached || { enabled: true, occupied: false, runtimeKey: key });
17128
+ return api.computerUseState(target).then(function(value) {
17129
+ if (!state.computerUseByTarget) state.computerUseByTarget = {};
17130
+ state.computerUseByTarget[key] = value || { enabled: true, occupied: false, runtimeKey: key };
17131
+ if (isActiveConversationTarget(target)) renderComputerUseToggle(state.computerUseByTarget[key], target);
17132
+ return state.computerUseByTarget[key];
17133
+ }).catch(function() { return cached || { enabled: true, occupied: false, runtimeKey: key }; });
17134
+ };
17135
+
17136
+ window.toggleComputerUse = function() {
17137
+ var target = currentConversationTarget();
17138
+ var key = computerUseTargetKey(target);
17139
+ var current = state.computerUseByTarget && state.computerUseByTarget[key];
17140
+ var enabled = !(current && current.enabled !== false);
17141
+ if (!api.setComputerUseEnabled) return Promise.resolve(null);
17142
+ return api.setComputerUseEnabled(target, enabled).then(function(result) {
17143
+ if (result && result.ok === false) {
17144
+ showUiNotice(String(result.error || 'ComputerUse occupied'), 'error', 'computer-use-toggle-' + key);
17145
+ return result;
17146
+ }
17147
+ if (!state.computerUseByTarget) state.computerUseByTarget = {};
17148
+ state.computerUseByTarget[key] = result && result.state ? result.state : { enabled: enabled, occupied: false, runtimeKey: key };
17149
+ renderComputerUseToggle(state.computerUseByTarget[key], target);
17150
+ return result;
17151
+ });
17152
+ };
17153
+
16857
17154
  window.browserGuestLifecycleSnapshot = function() {
16858
17155
  return {
16859
17156
  createCount: browserGuestCreateCount,
@@ -16870,15 +17167,60 @@ function browserUrlsEquivalent(left, right) {
16870
17167
  return !!a && a === b;
16871
17168
  }
16872
17169
 
16873
- function currentBrowserGuest() {
16874
- var view = document.getElementById('browser-webview');
16875
- return view && view.isConnected ? view : null;
17170
+ function browserTargetKey(target) {
17171
+ target = target || currentConversationTarget();
17172
+ return runtimeKeyFor(target.workspaceId, target.conversationId);
17173
+ }
17174
+
17175
+ function browserPartitionForTarget(target) {
17176
+ var key = browserTargetKey(target).replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 150) || 'default';
17177
+ return NEWMARK_BROWSER_PARTITION + '-' + key;
17178
+ }
17179
+
17180
+ function browserGuestForTarget(target) {
17181
+ var key = browserTargetKey(target);
17182
+ var frame = els['browser-frame'] || document.getElementById('browser-frame');
17183
+ if (!frame) return null;
17184
+ var views = frame.querySelectorAll('webview[data-newmark-browser-runtime-key]');
17185
+ for (var i = 0; i < views.length; i++) {
17186
+ if (String(views[i].getAttribute('data-newmark-browser-runtime-key') || '') === key && views[i].isConnected) return views[i];
17187
+ }
17188
+ return null;
16876
17189
  }
16877
17190
 
16878
- function rememberBrowserUrl(url, force) {
17191
+ function setActiveBrowserGuest(view, target) {
17192
+ var key = browserTargetKey(target);
17193
+ var frame = els['browser-frame'] || document.getElementById('browser-frame');
17194
+ if (frame) {
17195
+ var views = frame.querySelectorAll('webview[data-newmark-browser-runtime-key]');
17196
+ for (var i = 0; i < views.length; i++) {
17197
+ var same = views[i] === view || String(views[i].getAttribute('data-newmark-browser-runtime-key') || '') === key;
17198
+ views[i].style.display = same ? '' : 'none';
17199
+ if (same) views[i].id = 'browser-webview';
17200
+ else if (views[i].id === 'browser-webview') views[i].id = 'browser-webview-' + String(views[i].getAttribute('data-newmark-browser-runtime-key') || 'guest').replace(/[^A-Za-z0-9_.-]/g, '_').slice(-80);
17201
+ }
17202
+ }
17203
+ els['browser-webview'] = view || null;
17204
+ if (view) view._newmarkBrowserTargetKey = key;
17205
+ }
17206
+
17207
+ function currentBrowserGuest(target) {
17208
+ var view = browserGuestForTarget(target || currentConversationTarget());
17209
+ if (view) return view;
17210
+ var active = document.getElementById('browser-webview');
17211
+ return active && active.isConnected ? active : null;
17212
+ }
17213
+
17214
+ function rememberBrowserUrl(url, force, target) {
17215
+ target = target || currentConversationTarget();
17216
+ var targetKey = browserTargetKey(target);
16879
17217
  url = String(url || '').trim();
16880
- if (!force && url === 'about:blank' && browserRetainedUrl !== 'about:blank') return;
16881
- if (url) browserRetainedUrl = url;
17218
+ var prior = browserRetainedUrls[targetKey] || 'about:blank';
17219
+ if (!force && url === 'about:blank' && prior !== 'about:blank') return;
17220
+ if (url) {
17221
+ browserRetainedUrls[targetKey] = url;
17222
+ if (isActiveConversationTarget(target)) browserRetainedUrl = url;
17223
+ }
16882
17224
  }
16883
17225
 
16884
17226
  function waitForBrowserCreationFloor() {
@@ -16887,8 +17229,9 @@ function waitForBrowserCreationFloor() {
16887
17229
  return new Promise(function(resolve) { setTimeout(resolve, remaining); });
16888
17230
  }
16889
17231
 
16890
- function createBrowserGuestElement() {
16891
- var existing = currentBrowserGuest();
17232
+ function createBrowserGuestElement(target) {
17233
+ target = target || currentConversationTarget();
17234
+ var existing = browserGuestForTarget(target);
16892
17235
  if (existing) return existing;
16893
17236
  var frame = els['browser-frame'] || document.getElementById('browser-frame');
16894
17237
  if (!frame) throw new Error('Built-in Browser frame is unavailable');
@@ -16898,8 +17241,14 @@ function createBrowserGuestElement() {
16898
17241
  browserGuestCreateCount += 1;
16899
17242
  if (browserGuestFirstCreatedAtMs === null) browserGuestFirstCreatedAtMs = Date.now() - newmarkRendererStartedAt;
16900
17243
  view.id = 'browser-webview';
16901
- view.setAttribute('partition', NEWMARK_BROWSER_PARTITION);
17244
+ view.setAttribute('data-newmark-browser-runtime-key', browserTargetKey(target));
17245
+ view.setAttribute('partition', browserPartitionForTarget(target));
16902
17246
  frame.appendChild(view);
17247
+ if (isActiveConversationTarget(target)) setActiveBrowserGuest(view, target);
17248
+ else {
17249
+ view.style.display = 'none';
17250
+ view.id = 'browser-webview-' + browserTargetKey(target).replace(/[^A-Za-z0-9_.-]/g, '_').slice(-80);
17251
+ }
16903
17252
  els['browser-webview'] = view;
16904
17253
  return view;
16905
17254
  }
@@ -16922,11 +17271,14 @@ function cancelBrowserGuestIdleDestroy() {
16922
17271
  function destroyIdleBrowserGuest() {
16923
17272
  browserGuestIdleTimer = null;
16924
17273
  if (state.rightTab === 'browser') return;
16925
- var view = currentBrowserGuest();
17274
+ var view = currentBrowserGuest(currentConversationTarget());
16926
17275
  if (!view) return;
16927
- try { rememberBrowserUrl(view.getURL ? view.getURL() : ''); } catch {}
17276
+ var target = { workspaceId: state.currentWorkspaceId || state.currentWorkspacePath || state.currentWorkspace || 'none', conversationId: activeConversationId() || 'default' };
17277
+ try { rememberBrowserUrl(view.getURL ? view.getURL() : '', false, target); } catch {}
16928
17278
  if (view.parentNode) view.parentNode.removeChild(view);
16929
17279
  if (els['browser-webview'] === view) els['browser-webview'] = null;
17280
+ var key = browserTargetKey(target);
17281
+ delete browserGuestCreatePromises[key];
16930
17282
  browserGuestCreatePromise = null;
16931
17283
  restoreBrowserPlaceholder();
16932
17284
  }
@@ -16958,16 +17310,17 @@ function navigateBrowserView(view, url) {
16958
17310
  }
16959
17311
 
16960
17312
  window.navigateBrowser = function(url) {
17313
+ var target = currentConversationTarget();
16961
17314
  url = String(url || '').trim();
16962
17315
  if (!/^(?:https?:\/\/|about:blank$|newmark-preview:\/\/)/i.test(url)) {
16963
17316
  url = 'https://' + url;
16964
17317
  }
16965
17318
  if (state.browserPreviewToken && url !== state.browserPreviewUrl) window.revokeBrowserPreviewCapability();
16966
- rememberBrowserUrl(url, true);
17319
+ rememberBrowserUrl(url, true, target);
16967
17320
  var input = els['browser-url'];
16968
17321
  if (input) input.value = url;
16969
17322
  cancelBrowserGuestIdleDestroy();
16970
- return window.ensureBrowserPanel({ activate: false }).then(function(view) {
17323
+ return window.ensureBrowserPanel({ activate: false, target: target }).then(function(view) {
16971
17324
  navigateBrowserView(view, url);
16972
17325
  if (state.rightTab !== 'browser') scheduleBrowserGuestIdleDestroy();
16973
17326
  return view;
@@ -16996,6 +17349,8 @@ window.browserReload = function() {
16996
17349
  };
16997
17350
 
16998
17351
  function initializeBrowserGuest(view) {
17352
+ var target = arguments.length > 1 && arguments[1] ? arguments[1] : currentConversationTarget();
17353
+ var targetKey = browserTargetKey(target);
16999
17354
  if (!view._newmarkReadyPromise) {
17000
17355
  view._newmarkDomReady = false;
17001
17356
  view._newmarkReadySettled = false;
@@ -17011,12 +17366,12 @@ function initializeBrowserGuest(view) {
17011
17366
  currentUrl = view.getURL ? view.getURL() : '';
17012
17367
  if (input && currentUrl && currentUrl !== 'about:blank') input.value = currentUrl;
17013
17368
  } catch {}
17014
- rememberBrowserUrl(currentUrl);
17369
+ rememberBrowserUrl(currentUrl, false, target);
17015
17370
  if (view._newmarkReadySettled) return;
17016
17371
  var registration = Promise.resolve({ accepted: true });
17017
17372
  try {
17018
17373
  if (!api.browserRegisterGuest || !view.getWebContentsId) throw new Error('Built-in Browser registration API is unavailable');
17019
- registration = api.browserRegisterGuest(view.getWebContentsId());
17374
+ registration = api.browserRegisterGuest(view.getWebContentsId(), target);
17020
17375
  } catch (registrationError) {
17021
17376
  registration = Promise.reject(registrationError);
17022
17377
  }
@@ -17056,13 +17411,13 @@ function initializeBrowserGuest(view) {
17056
17411
  view.addEventListener('did-navigate', function(e) {
17057
17412
  var input = els['browser-url'];
17058
17413
  if (input && e.url && e.url !== 'about:blank') input.value = e.url;
17059
- rememberBrowserUrl(e.url);
17414
+ rememberBrowserUrl(e.url, false, target);
17060
17415
  if (browserUrlsEquivalent(e.url, view._newmarkPendingUrl)) view._newmarkPendingUrl = '';
17061
17416
  });
17062
17417
  view.addEventListener('did-navigate-in-page', function(e) {
17063
17418
  var input = els['browser-url'];
17064
17419
  if (input && e.url && e.url !== 'about:blank') input.value = e.url;
17065
- rememberBrowserUrl(e.url);
17420
+ rememberBrowserUrl(e.url, false, target);
17066
17421
  if (browserUrlsEquivalent(e.url, view._newmarkPendingUrl)) view._newmarkPendingUrl = '';
17067
17422
  });
17068
17423
  view.addEventListener('did-fail-load', function(e) {
@@ -17076,27 +17431,38 @@ function initializeBrowserGuest(view) {
17076
17431
  });
17077
17432
  var current = '';
17078
17433
  try { current = view.getURL ? view.getURL() : ''; } catch { current = ''; }
17079
- if (!current && !view.getAttribute('src')) view.setAttribute('src', 'about:blank');
17434
+ if (!current && !view.getAttribute('src')) view.setAttribute('src', browserRetainedUrls[targetKey] || 'about:blank');
17080
17435
  }
17081
17436
  return view._newmarkReadyPromise;
17082
17437
  }
17083
17438
 
17084
17439
  window.ensureBrowserPanel = function(options) {
17085
17440
  options = options || {};
17441
+ var target = options.target || currentConversationTarget();
17442
+ var targetKey = browserTargetKey(target);
17086
17443
  cancelBrowserGuestIdleDestroy();
17087
- if (!browserGuestCreatePromise) {
17088
- browserGuestCreatePromise = waitForBrowserCreationFloor().then(function() {
17089
- return initializeBrowserGuest(createBrowserGuestElement());
17444
+ if (!browserGuestCreatePromises[targetKey]) {
17445
+ // Keep background Browser-Use demand off the startup hot path, but never
17446
+ // make a deliberate visible-tab click wait behind that memory guard. A
17447
+ // user explicitly opening Browser is an interaction deadline, not a
17448
+ // background prewarm request.
17449
+ var creationWait = options.activate === true
17450
+ ? Promise.resolve()
17451
+ : waitForBrowserCreationFloor();
17452
+ browserGuestCreatePromises[targetKey] = creationWait.then(function() {
17453
+ return initializeBrowserGuest(createBrowserGuestElement(target), target);
17090
17454
  }).catch(function(error) {
17091
- browserGuestCreatePromise = null;
17455
+ delete browserGuestCreatePromises[targetKey];
17092
17456
  throw error;
17093
17457
  });
17094
17458
  }
17095
- return browserGuestCreatePromise.then(function(view) {
17459
+ browserGuestCreatePromise = browserGuestCreatePromises[targetKey];
17460
+ return browserGuestCreatePromises[targetKey].then(function(view) {
17461
+ if (isActiveConversationTarget(target)) setActiveBrowserGuest(view, target);
17096
17462
  if (options.activate === true) {
17097
17463
  var current = '';
17098
17464
  try { current = view.getURL ? view.getURL() : ''; } catch { current = ''; }
17099
- var targetUrl = browserRetainedUrl || 'about:blank';
17465
+ var targetUrl = browserRetainedUrls[targetKey] || 'about:blank';
17100
17466
  if (targetUrl !== 'about:blank' && (!current || current === 'about:blank') && !view._newmarkPendingUrl) {
17101
17467
  navigateBrowserView(view, targetUrl);
17102
17468
  }
@@ -17323,7 +17689,7 @@ function renderConversations() {
17323
17689
  ? '<span class="conv-runtime-badge ' + escAttr(String(runtimeState.status)) + '">' + esc(String(runtimeState.status)) + '</span>' : '';
17324
17690
  div.innerHTML = '<span class="conv-summary" title="' + escAttr(String(conv.id || '')) + '">' + esc(displaySummary) + (conv.messageCount ? ' (' + esc(String(conv.messageCount)) + ')' : '') + '</span>' + runtimeBadge +
17325
17691
  '<button class="conv-rename-btn" onclick="event.stopPropagation();window.editConversationName(' + i + ')" title="' + escAttr(t('conversation.rename')) + '">' + iconOnly('pencil', t('conversation.rename')) + '</button>' +
17326
- '<button class="conv-archive-btn" ' + (runtimeState && ['running', 'stopping', 'force_restarting'].indexOf(String(runtimeState.status || '')) >= 0 ? 'disabled ' : '') + 'onclick="event.stopPropagation();window.archiveConv(this.closest(&quot;.conv-item&quot;).getAttribute(&quot;data-conversation-id&quot;))" title="' + escAttr(t('conversation.archive')) + '">' + iconOnly('archive', t('conversation.archive')) + '</button>' +
17692
+ '<button class="conv-archive-btn" onclick="event.stopPropagation();window.archiveConv(this.closest(&quot;.conv-item&quot;).getAttribute(&quot;data-conversation-id&quot;))" title="' + escAttr(t('conversation.archive')) + '">' + iconOnly('archive', t('conversation.archive')) + '</button>' +
17327
17693
  '<button class="conv-pin-btn' + (conv.pinned ? ' active' : '') + '" onclick="event.stopPropagation();window.toggleConversationPinned(' + i + ')" title="' + escAttr(conv.pinned ? t('conversation.unpin') : t('conversation.pin')) + '">' + iconOnly('pin', conv.pinned ? t('conversation.unpin') : t('conversation.pin')) + '</button>';
17328
17694
  div.onclick = function(idx) { return function() { window.switchConversation(idx); }; }(i);
17329
17695
  div.addEventListener('dragstart', function(event) {
@@ -17479,6 +17845,13 @@ window.switchConversation = function(idx) {
17479
17845
  if (previousId) markConversationTracked(previousId, state.conversationTrackMs || 300000);
17480
17846
  setActiveWorkspaceConversation(idx);
17481
17847
  var activeId = activeConversationId();
17848
+ var activeBrowserTarget = currentConversationTarget(activeId);
17849
+ var activeBrowserView = browserGuestForTarget(activeBrowserTarget);
17850
+ if (activeBrowserView) setActiveBrowserGuest(activeBrowserView, activeBrowserTarget);
17851
+ else if (state.rightTab === 'browser') window.ensureBrowserPanel({ target: activeBrowserTarget, activate: true }).catch(function(error) {
17852
+ console.error('[Browser] conversation-bound guest initialization failed:', error && error.message ? error.message : String(error));
17853
+ });
17854
+ window.refreshComputerUseState(activeBrowserTarget);
17482
17855
  state.backendQueue = backendQueueForTarget(currentConversationTarget(activeId));
17483
17856
  holdForegroundConversation(activeId, 4500);
17484
17857
  markConversationTracked(activeId, state.conversationTrackMs || 300000);
@@ -17518,8 +17891,13 @@ function refreshConversationArchivesAfterBatch() {
17518
17891
  state.workspaceArchives = (items || []).map(function(a) {
17519
17892
  return { id: a.id || a.name || String(a), name: a.name || String(a), firstLine: a.firstLine || '', date: a.date || '', scope: a.scope || 'workspace', workspace: a.workspace || state.currentWorkspace || '', restorable: !!a.restorable, conversationId: a.conversationId || '' };
17520
17893
  });
17521
- state.allArchives = [];
17894
+ var archiveSettingsOpen = typeof document !== 'undefined'
17895
+ && state.settingsActiveTab === 'archive'
17896
+ && !!document.getElementById('stab-archive');
17897
+ if (!archiveSettingsOpen) state.allArchives = [];
17522
17898
  window.renderRightArchives();
17899
+ var stab = typeof document !== 'undefined' ? document.getElementById('stab-archive') : null;
17900
+ if (stab && archiveSettingsOpen) stab.innerHTML = renderArchiveSettings();
17523
17901
  return items || [];
17524
17902
  });
17525
17903
  }
@@ -17554,10 +17932,6 @@ window.archiveConv = function(conversationId) {
17554
17932
  var targetRuntime = currentConversationTarget(targetId);
17555
17933
  var workspaceKey = currentWorkspaceKey();
17556
17934
  var pendingKey = workspaceKey + '::' + targetId;
17557
- if (runningConversationRecord(targetId)) {
17558
- showUiNotice(currentLang() === 'zh' ? '运行中的对话不能归档。' : 'A running conversation cannot be archived.', 'error', 'archive-running-' + currentRuntimeKey(targetId));
17559
- return;
17560
- }
17561
17935
  if (state.conversationArchivePending[pendingKey]) return;
17562
17936
  var priorActiveId = String(((convs.find(function(item) { return item && item.active; }) || {}).id) || '');
17563
17937
  var rollbackOrder = convs.map(function(item) { return String(item && item.id || 'default'); });
@@ -17597,39 +17971,37 @@ window.archiveConv = function(conversationId) {
17597
17971
  archivePromise.then(function(receipt) {
17598
17972
  if (!receipt || receipt.ok !== true) throw new Error((receipt && receipt.error) || 'Archive failed');
17599
17973
  delete state.conversationArchivePending[pendingKey];
17974
+ var optimisticArchive = {
17975
+ id: receipt.fileName,
17976
+ name: receipt.fileName,
17977
+ firstLine: target.summary || '',
17978
+ date: new Date().toISOString(),
17979
+ scope: 'workspace',
17980
+ workspace: state.currentWorkspace || '',
17981
+ restorable: true,
17982
+ conversationId: receipt.conversationId || targetId,
17983
+ };
17984
+ state.workspaceArchives = (state.workspaceArchives || []).filter(function(item) {
17985
+ return String(item && item.id || item && item.name || '') !== String(optimisticArchive.id);
17986
+ });
17987
+ state.workspaceArchives.unshift(optimisticArchive);
17988
+ if (Array.isArray(state.allArchives) && state.allArchives.length) {
17989
+ state.allArchives = state.allArchives.filter(function(item) {
17990
+ return String(item && item.id || item && item.name || '') !== String(optimisticArchive.id);
17991
+ });
17992
+ state.allArchives.unshift(optimisticArchive);
17993
+ }
17994
+ window.renderRightArchives();
17995
+ var archiveSettings = typeof document !== 'undefined' ? document.getElementById('stab-archive') : null;
17996
+ if (archiveSettings && state.settingsActiveTab === 'archive') archiveSettings.innerHTML = renderArchiveSettings();
17600
17997
  showUiNotice('[Archive] ' + t('archive.saved') + ': ' + receipt.fileName, 'success', 'archive-saved-' + targetId);
17601
17998
  scheduleConversationArchiveRefresh(workspaceKey);
17602
17999
  }).catch(function(err) {
17603
- var pending = state.conversationArchivePending[pendingKey];
17604
18000
  delete state.conversationArchivePending[pendingKey];
17605
- var workspaceConversations = state.workspaceConversations[workspaceKey] || [];
17606
- if (pending && !workspaceConversations.some(function(item) { return String(item && item.id || '') === targetId; })) {
17607
- var restoreAt = -1;
17608
- var orderIds = Array.isArray(pending.orderIds) ? pending.orderIds : [];
17609
- var targetOrderIndex = orderIds.indexOf(targetId);
17610
- for (var beforeIndex = targetOrderIndex - 1; beforeIndex >= 0 && restoreAt < 0; beforeIndex--) {
17611
- var precedingIndex = workspaceConversations.findIndex(function(item) { return String(item && item.id || '') === orderIds[beforeIndex]; });
17612
- if (precedingIndex >= 0) restoreAt = precedingIndex + 1;
17613
- }
17614
- for (var afterIndex = targetOrderIndex + 1; afterIndex < orderIds.length && restoreAt < 0; afterIndex++) {
17615
- var followingIndex = workspaceConversations.findIndex(function(item) { return String(item && item.id || '') === orderIds[afterIndex]; });
17616
- if (followingIndex >= 0) restoreAt = followingIndex;
17617
- }
17618
- if (restoreAt < 0) restoreAt = Math.min(Number(pending.index || 0), workspaceConversations.length);
17619
- workspaceConversations.splice(restoreAt, 0, pending.target);
17620
- if (pending.wasActive) {
17621
- for (var j = 0; j < workspaceConversations.length; j++) workspaceConversations[j].active = String(workspaceConversations[j].id || '') === targetId;
17622
- }
17623
- }
17624
- if (workspaceKey === currentWorkspaceKey()) {
17625
- state.conversations = workspaceConversations;
17626
- state.activeConversation = Math.max(0, workspaceConversations.findIndex(function(item) { return item && item.active; }));
17627
- if (pending && pending.wasActive) {
17628
- state.activeBackendConversationId = targetId;
17629
- if (els['chat-area']) els['chat-area'].innerHTML = pending.chatHtml || '';
17630
- }
17631
- renderConversations();
17632
- }
18001
+ // Keep the optimistic removal authoritative for this renderer session.
18002
+ // A failed IPC receipt is surfaced, but never resurrects the row under
18003
+ // the user's pointer; the next explicit workspace refresh is the only
18004
+ // path allowed to reconcile a failed destructive operation.
17633
18005
  showUiNotice('[Archive] ' + t('workspace.saveFailed') + ': ' + (err.message || String(err)), 'error', 'archive-failed-' + targetId);
17634
18006
  });
17635
18007
  }
@@ -19404,18 +19776,26 @@ window.saveWsSetting = function(key, value) {
19404
19776
  window.renderFlowSelector = function() {
19405
19777
  var flowSel = document.getElementById('flow-select');
19406
19778
  if (!flowSel) return;
19779
+ var flows = Array.isArray(state.flowWorks) ? state.flowWorks : [];
19407
19780
  var defaultFlow = state.defaultFlow || '';
19781
+ if (flows.length === 0) {
19782
+ state.defaultFlow = '';
19783
+ flowSel.disabled = true;
19784
+ flowSel.innerHTML = '<option value="">' + esc(t('flow.noAvailable')) + '</option>';
19785
+ if (window.syncNewmarkSelect) window.syncNewmarkSelect(flowSel);
19786
+ return;
19787
+ }
19788
+ flowSel.disabled = false;
19408
19789
  var html = '';
19409
- for (var fi = 0; fi < state.flowWorks.length; fi++) {
19410
- var selected = state.flowWorks[fi].name === defaultFlow ? ' selected' : '';
19411
- html += '<option value="' + esc(state.flowWorks[fi].name) + '"' + selected + '>' + esc(state.flowWorks[fi].name) + '</option>';
19790
+ for (var fi = 0; fi < flows.length; fi++) {
19791
+ var selected = flows[fi].name === defaultFlow ? ' selected' : '';
19792
+ html += '<option value="' + esc(flows[fi].name) + '"' + selected + '>' + esc(flows[fi].name) + '</option>';
19412
19793
  }
19413
19794
  flowSel.innerHTML = html;
19414
- if (state.flowWorks.length > 0) {
19415
- if (defaultFlow) flowSel.value = defaultFlow;
19416
- state.defaultFlow = flowSel.value;
19417
- }
19795
+ if (defaultFlow) flowSel.value = defaultFlow;
19796
+ state.defaultFlow = flowSel.value;
19418
19797
  flowSel.onchange = function() { state.defaultFlow = this.value; api.saveConfig({defaultFlow: state.defaultFlow}); };
19798
+ if (window.syncNewmarkSelect) window.syncNewmarkSelect(flowSel);
19419
19799
  };
19420
19800
 
19421
19801
  window.loadFlows = function(options) {
@@ -19680,9 +20060,10 @@ function schedulePostStartupUiRendering() {
19680
20060
  cacheEls();
19681
20061
  setupAgentWorkEvents();
19682
20062
  if (api.onBrowserEnsureGuest) {
19683
- api.onBrowserEnsureGuest(function() {
20063
+ api.onBrowserEnsureGuest(function(target) {
19684
20064
  cancelBrowserGuestIdleDestroy();
19685
- window.ensureBrowserPanel({ activate: false }).catch(function(error) {
20065
+ var requestedTarget = target || currentConversationTarget();
20066
+ window.ensureBrowserPanel({ target: requestedTarget, activate: isActiveConversationTarget(requestedTarget) }).catch(function(error) {
19686
20067
  console.error('[Browser] guest initialization failed:', error && error.message ? error.message : String(error));
19687
20068
  });
19688
20069
  });
@@ -19814,6 +20195,48 @@ function schedulePostStartupUiRendering() {
19814
20195
  updateWorkspaceGate();
19815
20196
 
19816
20197
  // === Event Listeners ===
20198
+ function escapeBelongsToFocusedControl(event) {
20199
+ var target = event && event.target;
20200
+ if (!target || target === document || target === document.body || target === document.documentElement) return false;
20201
+ if (els.prompt && (target === els.prompt || (els.prompt.contains && els.prompt.contains(target)))) return false;
20202
+ if (target.closest && target.closest('.modal.active, .sub-win.open, .newmark-select-shell.open')) return true;
20203
+ return !!(target.matches && target.matches('input, textarea, select, [contenteditable="true"]'));
20204
+ }
20205
+ if (api.onWorkspaceChanged) {
20206
+ var workspaceRefreshTimer = null;
20207
+ api.onWorkspaceChanged(function() {
20208
+ if (workspaceRefreshTimer) clearTimeout(workspaceRefreshTimer);
20209
+ workspaceRefreshTimer = setTimeout(function() {
20210
+ workspaceRefreshTimer = null;
20211
+ if (window.refreshWorkspaceState) window.refreshWorkspaceState().catch(function(){});
20212
+ if (document.getElementById('right-archive-list')) window.refreshRightArchives();
20213
+ }, 120);
20214
+ });
20215
+ }
20216
+
20217
+ function stopRunningFromEscape(event) {
20218
+ if (!event || event.key !== 'Escape' || event.defaultPrevented || escapeBelongsToFocusedControl(event)) return false;
20219
+ if (currentFlowRunning() && flowTakeoverMatchesCurrent() && !promptHasText()) {
20220
+ event.preventDefault();
20221
+ window.stopFlowRun();
20222
+ return true;
20223
+ }
20224
+ if (isCurrentConversationRunning() && !promptHasText()) {
20225
+ event.preventDefault();
20226
+ window.stopCurrentConversation();
20227
+ return true;
20228
+ }
20229
+ return false;
20230
+ }
20231
+
20232
+ // The prompt handler below covers the normal focused-input path. Keep a
20233
+ // document-level fallback so a physical Escape still stops a running target
20234
+ // after focus moved to the chat, title bar, button, or another non-editor
20235
+ // surface. Modal/editor/select Escape behavior remains owned by that UI.
20236
+ document.addEventListener('keydown', function(event) {
20237
+ if (stopRunningFromEscape(event)) event.stopPropagation();
20238
+ });
20239
+
19817
20240
  if (els.prompt) {
19818
20241
  els.prompt.addEventListener('keydown', function(e) {
19819
20242
  if (e.key === 'Escape' && currentFlowRunning() && flowTakeoverMatchesCurrent() && !promptHasText()) {