newmark-agent 0.3.11 → 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 (39) 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 +2 -0
  7. package/dist/cli-help.js +25 -1
  8. package/dist/conversation-utility-host.bundle.cjs +357 -84
  9. package/dist/core/agent.d.ts +18 -3
  10. package/dist/core/agent.js +214 -30
  11. package/dist/core/agentKernelRunner.js +53 -8
  12. package/dist/core/config.d.ts +7 -2
  13. package/dist/core/config.js +24 -6
  14. package/dist/core/conversationKernel.js +1 -1
  15. package/dist/core/electronUtilityRuntimePool.d.ts +11 -0
  16. package/dist/core/electronUtilityRuntimePool.js +62 -0
  17. package/dist/core/flow-runner.js +1 -1
  18. package/dist/core/modelValidationStore.d.ts +4 -1
  19. package/dist/core/modelValidationStore.js +7 -1
  20. package/dist/core/workspace.d.ts +6 -0
  21. package/dist/core/workspace.js +14 -0
  22. package/dist/core/wslAgentRuntimePool.d.ts +4 -0
  23. package/dist/core/wslAgentRuntimePool.js +56 -0
  24. package/dist/launcher.js +40 -11
  25. package/dist/llm/provider.d.ts +8 -5
  26. package/dist/llm/provider.js +85 -33
  27. package/dist/main.js +167 -45
  28. package/dist/preload.js +6 -0
  29. package/dist/providers/chat-completions.adapter.js +1 -5
  30. package/dist/providers/provider-events.d.ts +7 -0
  31. package/dist/providers/provider-events.js +44 -0
  32. package/dist/providers/responses.adapter.js +1 -3
  33. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  34. package/dist/tui/src/app.js +47 -13
  35. package/dist/tui/src/render.js +23 -7
  36. package/dist/tui/src/state.js +61 -9
  37. package/dist/ui/index.html +250 -84
  38. package/dist/wsl-agent-host.bundle.cjs +357 -84
  39. package/package.json +14 -5
@@ -6063,6 +6063,7 @@ var NEWMARK_I18N = {
6063
6063
  'flow.nextDisabled': 'Flow only accepts Guide input; Next is disabled.',
6064
6064
  'flow.saved': 'Flow saved',
6065
6065
  'flow.emptyRun': 'Flow is empty and cannot run.',
6066
+ 'flow.noAvailable': 'No workflows are available to run.',
6066
6067
  'flow.started': 'Flow started',
6067
6068
  'plugins.title': 'Plugins',
6068
6069
  'plugins.mcp': 'MCP Management',
@@ -6628,6 +6629,7 @@ var NEWMARK_I18N = {
6628
6629
  'flow.nextDisabled': 'Flow 仅允许 Guide 输入,已禁用 Next。',
6629
6630
  'flow.saved': 'Flow 已保存',
6630
6631
  'flow.emptyRun': 'Flow 为空,无法运行。',
6632
+ 'flow.noAvailable': '当前没有可运行的工作流。',
6631
6633
  'flow.started': 'Flow 已启动',
6632
6634
  'plugins.title': '插件',
6633
6635
  'plugins.mcp': 'MCP 管理',
@@ -8329,15 +8331,26 @@ function addWorkReview(diffs) {
8329
8331
  return review;
8330
8332
  }
8331
8333
 
8332
- 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) {
8333
8338
  if (!div) return;
8334
8339
  if (role) div.className = 'chat-msg ' + role;
8335
8340
  text = redactSensitiveText(text);
8336
8341
  div._newmarkMessageText = String(text || '');
8337
8342
  var body = div.querySelector('.msg-body');
8338
- 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
+ }
8339
8352
  var meta = div.querySelector('.meta');
8340
- if (meta && (mode || model)) {
8353
+ if (meta && (mode || model) && !streaming) {
8341
8354
  var roleLabel = div.classList.contains('user')
8342
8355
  ? (String(mode || '').toLowerCase() === 'flow-user-input' ? t('flow.userInput') : t('message.user'))
8343
8356
  : (div.classList.contains('workflow') ? t('message.workflow') : (div.classList.contains('system') ? t('message.system') : t('message.agent')));
@@ -8746,6 +8759,60 @@ function setConversationRuntimeState(target, status, runId, extra) {
8746
8759
  return next;
8747
8760
  }
8748
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
+
8749
8816
  function formatWorkDuration(milliseconds) {
8750
8817
  var seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000));
8751
8818
  var hours = Math.floor(seconds / 3600);
@@ -9476,7 +9543,14 @@ function renderWorkRunEvents(run, includeGuides) {
9476
9543
  var responseLabel = type === 'response' && terminalInterrupted
9477
9544
  ? (currentLang() === 'zh' ? '未完成回复片段\n' : 'Incomplete response fragment\n') + workEventLabel(event)
9478
9545
  : workEventLabel(event);
9479
- 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>';
9480
9554
  }
9481
9555
  if (type.indexOf('guide') === 0 || event.guide) return renderWorkRunGuideMessage(event);
9482
9556
  if (type === 'tool_group') return renderWorkToolGroup(event, eventIndex);
@@ -10223,7 +10297,13 @@ function applyStreamingWorkflowText(ui, eventConversationId, mode, model) {
10223
10297
  }
10224
10298
  return;
10225
10299
  }
10226
- 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 });
10227
10307
  }
10228
10308
 
10229
10309
  function scheduleStreamingWorkflowTextFlush(ui, eventConversationId, eventWorkspaceId) {
@@ -10254,6 +10334,7 @@ function renderAgentWorkEvent(event) {
10254
10334
  ui.activeWorkflowText = '';
10255
10335
  ui.lastCompletedWorkflow = null;
10256
10336
  ui._streamFlushPending = false;
10337
+ ui._lastLargeStreamRenderAt = 0;
10257
10338
  } else if (type === 'text') {
10258
10339
  ui.activeWorkflowText = (ui.activeWorkflowText || '') + content;
10259
10340
  if (workRun) return;
@@ -10264,6 +10345,7 @@ function renderAgentWorkEvent(event) {
10264
10345
  ui.activeWorkflowText = '';
10265
10346
  ui.activeWorkflowMsg = null;
10266
10347
  ui._streamFlushPending = false;
10348
+ ui._lastLargeStreamRenderAt = 0;
10267
10349
  } else if (type === 'final_response' && workRun) {
10268
10350
  ui.activeWorkflowText = '';
10269
10351
  ui.activeWorkflowMsg = null;
@@ -10297,6 +10379,7 @@ function renderAgentWorkEvent(event) {
10297
10379
  }
10298
10380
  ui.activeWorkflowMsg = null;
10299
10381
  ui.activeWorkflowText = '';
10382
+ ui._lastLargeStreamRenderAt = 0;
10300
10383
  finishToolBatch(eventConversationId);
10301
10384
  } else if (type === 'error') {
10302
10385
  flushStreamingWorkflowTextNow(ui, eventConversationId, event.mode || state.mode, event.model || state.model);
@@ -10366,7 +10449,14 @@ function appendAgentWorkEvent(event) {
10366
10449
  var visibleRuntimeBranch = active && isViewingRuntimeConversationBranch(target);
10367
10450
  if (active) markConversationTracked(id, state.conversationTrackMs || 300000, workspaceId);
10368
10451
  if (active || isConversationTracked(id, workspaceId)) cacheAgentWorkEvent(event);
10369
- 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 || '' });
10370
10460
  else if (event.status && ['running', 'stopping', 'force_restarting'].indexOf(String(event.status)) >= 0) setConversationRuntimeState(target, event.status, event.runId || '');
10371
10461
  else if (event.status && ['completed', 'interrupted', 'force_interrupted', 'error'].indexOf(String(event.status)) >= 0) {
10372
10462
  setConversationRuntimeState(target, event.status, event.runId || '');
@@ -10977,18 +11067,33 @@ window.stopCurrentConversation = async function() {
10977
11067
  if (!conversationId || !runtime) return false;
10978
11068
  var target = runtime.target || currentConversationTarget(conversationId);
10979
11069
  var runId = String(runtime.runId || '');
11070
+ var provisional = runtime.provisional === true;
10980
11071
  if (String(runtime.status || '') === 'force_restarting') return false;
11072
+ if (provisional) markProvisionalStop(target, runId);
10981
11073
  var force = String(runtime.status || '') === 'stopping';
10982
11074
  if (typeof pauseQueueForTarget === 'function') pauseQueueForTarget(target);
10983
11075
  setConversationRuntimeState(target, force ? 'force_restarting' : 'stopping', runId);
10984
11076
  updateSubmitButtonState();
10985
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
+ }
10986
11087
  try {
10987
11088
  var result;
10988
11089
  // The target runtime supervisor owns the checkpoint + cooperative-stop
10989
11090
  // transaction. A separate awaited checkpoint can itself be starved by a
10990
11091
  // blocked worker and would prevent the supervisor from recording the first
10991
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.
10992
11097
  if (api.stopConversation) result = await api.stopConversation({ target: target, runId: runId, force: force });
10993
11098
  else if (api.abortConversation) result = await api.abortConversation(currentConversationTarget(conversationId));
10994
11099
  var resultAction = String(result && result.action || '');
@@ -10999,6 +11104,9 @@ window.stopCurrentConversation = async function() {
10999
11104
  await refreshConversationRuntimeAfterStopRace(target, runId, result);
11000
11105
  return true;
11001
11106
  }
11107
+ if (provisional && state.pendingProvisionalStops) {
11108
+ delete state.pendingProvisionalStops[runtimeKeyFor(target.workspaceId, target.conversationId)];
11109
+ }
11002
11110
  var latestAfterStop = state.conversationRuntimeStates && state.conversationRuntimeStates[runtimeKeyFor(target.workspaceId, target.conversationId)];
11003
11111
  if (!force && latestAfterStop && String(latestAfterStop.runId || '') === runId && String(latestAfterStop.status || '') !== 'stopping') {
11004
11112
  // A second click or a terminal worker event won the race while the first
@@ -12233,6 +12341,15 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12233
12341
  var executionMode = opts.goalDeclaration || requestedMode === 'goal' ? 'build'
12234
12342
  : ((opts.forceBuild || idleBuildNextImmediate) && requestedMode === 'build' ? 'build' : requestedMode);
12235
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
+ }
12236
12353
  var renderOnViewedBranch = !queuedRuntimeBranch || queueBranchPathForTarget(lockedTarget, 'viewed') === queueBranchPathForTarget(lockedTarget, 'runtime');
12237
12354
  if (isActiveConversationTarget(lockedTarget) && renderOnViewedBranch) {
12238
12355
  if (opts.resumeGuideRunId && requestMessage.clientMessageId) {
@@ -12262,10 +12379,12 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12262
12379
  renderAutoRouteRatingControls();
12263
12380
  }
12264
12381
  var responseMsg = null;
12382
+ var sendFailure = '';
12265
12383
  try {
12266
12384
  var sendPromise = api.sendMessage(requestMessage, lockedTarget);
12267
12385
  if (requestedMode === 'goal') activateSubmittedGoal(opts.goalObjective || rawText);
12268
12386
  var r = await sendPromise;
12387
+ if (r && r.error) sendFailure = String(r.error);
12269
12388
  if (isActiveConversationTarget(lockedTarget)) applyReturnedGoalState(r);
12270
12389
  if (r && r.runId) {
12271
12390
  var anchorStore = workRunAnchorIndexStore(lockedTarget);
@@ -12346,9 +12465,10 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12346
12465
  }
12347
12466
  }
12348
12467
  } catch(e) {
12468
+ sendFailure = e && e.message ? String(e.message) : String(e || 'Agent run failed.');
12349
12469
  if (isActiveConversationTarget(lockedTarget)) {
12350
12470
  responseMsg = conversationWorkUiState(lockedConversationId, lockedTarget.workspaceId).activeWorkflowMsg || addMsg('assistant', '', state.mode, state.model);
12351
- 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);
12352
12472
  }
12353
12473
  }
12354
12474
  if (api.getState) {
@@ -12399,7 +12519,15 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
12399
12519
  if (resumedModeResult != null) state._syncedMode = state.mode;
12400
12520
  }
12401
12521
  var finalRuntime = state.conversationRuntimeStates && state.conversationRuntimeStates[lockedRuntimeKey];
12402
- 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) {
12403
12531
  setConversationRuntimeState(lockedTarget, 'completed', (r && r.runId) || localRunId);
12404
12532
  }
12405
12533
  setWorking(!!runningConversationRecord(activeConversationId()));
@@ -13302,7 +13430,7 @@ window.terminalSend = function() {
13302
13430
  var startedPane = document.querySelector('.terminal-pane[data-session="' + resp.sessionId + '"]');
13303
13431
  var startedInput = startedPane && startedPane.querySelector('.terminal-input');
13304
13432
  if (startedInput && startedInput.value === cmd) startedInput.value = '';
13305
- return api.terminalWrite(resp.sessionId, cmd + '\r\n');
13433
+ return api.terminalWrite(resp.sessionId, cmd + '\r');
13306
13434
  }).catch(function(err) {
13307
13435
  input.disabled = false;
13308
13436
  var currentPane = window.getActiveTerminalPane();
@@ -13311,7 +13439,7 @@ window.terminalSend = function() {
13311
13439
  });
13312
13440
  }
13313
13441
  input.value = '';
13314
- api.terminalWrite(sessionId, cmd + '\r\n').catch(function(err) {
13442
+ api.terminalWrite(sessionId, cmd + '\r').catch(function(err) {
13315
13443
  if (output) output.innerHTML += '\r\n<span style="color:#ff6666;">[' + esc(t('common.error')) + '] ' + esc(err.message) + '</span>';
13316
13444
  });
13317
13445
  };
@@ -15154,36 +15282,12 @@ renderArchiveSettings = function() {
15154
15282
  };
15155
15283
 
15156
15284
  window.archiveCurrent = function() {
15157
- if (!api.archive) return;
15158
15285
  var currentId = activeConversationId();
15159
- if (currentId && runningConversationRecord(currentId)) {
15160
- showUiNotice(currentLang() === 'zh' ? '请先停止当前对话再归档。' : 'Stop the active conversation before archiving it.', 'error', 'archive-running-' + currentRuntimeKey(currentId));
15161
- return;
15162
- }
15163
- api.archive(currentConversationTarget(currentId)).then(function(receipt) {
15164
- if (!receipt || !receipt.ok) throw new Error((receipt && receipt.error) || 'Archive failed');
15165
- showUiNotice('[Archive] ' + t('archive.saved') + ': ' + receipt.fileName, 'success', 'archive-saved-' + receipt.conversationId);
15166
- return Promise.all([api.listArchives ? api.listArchives('workspace') : [], api.getState ? api.getState() : null]);
15167
- }).then(function(result) {
15168
- var items = result[0] || [];
15169
- var refreshed = result[1];
15170
- state.workspaceArchives = (items || []).map(function(a) {
15171
- 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 || '' };
15172
- });
15173
- state.allArchives = [];
15174
- if (refreshed && refreshed.conversations) {
15175
- state.activeBackendConversationId = refreshed.conversationId || state.activeBackendConversationId;
15176
- applyBackendConversations(refreshed.conversations, state.activeBackendConversationId, refreshed.workspaceId || runtimeWorkspaceId(''));
15177
- if (refreshed.chatMessages) renderChatMessages(refreshed.chatMessages);
15178
- renderConversations();
15179
- }
15180
- window.renderRightArchives();
15181
- var stab = document.getElementById('stab-archive');
15182
- if (stab) stab.innerHTML = renderArchiveSettings();
15183
- if (state.settingsActiveTab === 'archive') window.settingsTab('archive');
15184
- }).catch(function(err) {
15185
- addMsg('assistant', '[Archive] ' + t('workspace.saveFailed') + ': ' + (err.message || String(err)), 'error', '');
15186
- });
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);
15187
15291
  };
15188
15292
 
15189
15293
  window.loadArchive = function(idx, scope) {
@@ -16223,11 +16327,17 @@ window.loadFileTree = async function(options) {
16223
16327
  };
16224
16328
 
16225
16329
  window.submitSelectedFlow = function() {
16330
+ if (!state._flowsLoaded && window.ensureFlowsLoaded) {
16331
+ return window.ensureFlowsLoaded().then(function() { return window.submitSelectedFlow(); });
16332
+ }
16226
16333
  var selected = String(state.defaultFlow || (document.getElementById('flow-select') && document.getElementById('flow-select').value) || '');
16227
16334
  var workIdx = state.flowWorks.findIndex(function(work) { return String(work && work.name || '') === selected; });
16228
16335
  if (workIdx < 0) {
16229
- showUiNotice(currentLang() === 'zh' ? '请先选择一个 Flow。' : 'Select a Flow first.', 'error', 'flow-select-required');
16230
- 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 };
16231
16341
  }
16232
16342
  return window.runFlowWork(workIdx);
16233
16343
  };
@@ -17332,7 +17442,14 @@ window.ensureBrowserPanel = function(options) {
17332
17442
  var targetKey = browserTargetKey(target);
17333
17443
  cancelBrowserGuestIdleDestroy();
17334
17444
  if (!browserGuestCreatePromises[targetKey]) {
17335
- browserGuestCreatePromises[targetKey] = waitForBrowserCreationFloor().then(function() {
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() {
17336
17453
  return initializeBrowserGuest(createBrowserGuestElement(target), target);
17337
17454
  }).catch(function(error) {
17338
17455
  delete browserGuestCreatePromises[targetKey];
@@ -17572,7 +17689,7 @@ function renderConversations() {
17572
17689
  ? '<span class="conv-runtime-badge ' + escAttr(String(runtimeState.status)) + '">' + esc(String(runtimeState.status)) + '</span>' : '';
17573
17690
  div.innerHTML = '<span class="conv-summary" title="' + escAttr(String(conv.id || '')) + '">' + esc(displaySummary) + (conv.messageCount ? ' (' + esc(String(conv.messageCount)) + ')' : '') + '</span>' + runtimeBadge +
17574
17691
  '<button class="conv-rename-btn" onclick="event.stopPropagation();window.editConversationName(' + i + ')" title="' + escAttr(t('conversation.rename')) + '">' + iconOnly('pencil', t('conversation.rename')) + '</button>' +
17575
- '<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>' +
17576
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>';
17577
17694
  div.onclick = function(idx) { return function() { window.switchConversation(idx); }; }(i);
17578
17695
  div.addEventListener('dragstart', function(event) {
@@ -17774,8 +17891,13 @@ function refreshConversationArchivesAfterBatch() {
17774
17891
  state.workspaceArchives = (items || []).map(function(a) {
17775
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 || '' };
17776
17893
  });
17777
- state.allArchives = [];
17894
+ var archiveSettingsOpen = typeof document !== 'undefined'
17895
+ && state.settingsActiveTab === 'archive'
17896
+ && !!document.getElementById('stab-archive');
17897
+ if (!archiveSettingsOpen) state.allArchives = [];
17778
17898
  window.renderRightArchives();
17899
+ var stab = typeof document !== 'undefined' ? document.getElementById('stab-archive') : null;
17900
+ if (stab && archiveSettingsOpen) stab.innerHTML = renderArchiveSettings();
17779
17901
  return items || [];
17780
17902
  });
17781
17903
  }
@@ -17810,10 +17932,6 @@ window.archiveConv = function(conversationId) {
17810
17932
  var targetRuntime = currentConversationTarget(targetId);
17811
17933
  var workspaceKey = currentWorkspaceKey();
17812
17934
  var pendingKey = workspaceKey + '::' + targetId;
17813
- if (runningConversationRecord(targetId)) {
17814
- showUiNotice(currentLang() === 'zh' ? '运行中的对话不能归档。' : 'A running conversation cannot be archived.', 'error', 'archive-running-' + currentRuntimeKey(targetId));
17815
- return;
17816
- }
17817
17935
  if (state.conversationArchivePending[pendingKey]) return;
17818
17936
  var priorActiveId = String(((convs.find(function(item) { return item && item.active; }) || {}).id) || '');
17819
17937
  var rollbackOrder = convs.map(function(item) { return String(item && item.id || 'default'); });
@@ -17853,39 +17971,37 @@ window.archiveConv = function(conversationId) {
17853
17971
  archivePromise.then(function(receipt) {
17854
17972
  if (!receipt || receipt.ok !== true) throw new Error((receipt && receipt.error) || 'Archive failed');
17855
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();
17856
17997
  showUiNotice('[Archive] ' + t('archive.saved') + ': ' + receipt.fileName, 'success', 'archive-saved-' + targetId);
17857
17998
  scheduleConversationArchiveRefresh(workspaceKey);
17858
17999
  }).catch(function(err) {
17859
- var pending = state.conversationArchivePending[pendingKey];
17860
18000
  delete state.conversationArchivePending[pendingKey];
17861
- var workspaceConversations = state.workspaceConversations[workspaceKey] || [];
17862
- if (pending && !workspaceConversations.some(function(item) { return String(item && item.id || '') === targetId; })) {
17863
- var restoreAt = -1;
17864
- var orderIds = Array.isArray(pending.orderIds) ? pending.orderIds : [];
17865
- var targetOrderIndex = orderIds.indexOf(targetId);
17866
- for (var beforeIndex = targetOrderIndex - 1; beforeIndex >= 0 && restoreAt < 0; beforeIndex--) {
17867
- var precedingIndex = workspaceConversations.findIndex(function(item) { return String(item && item.id || '') === orderIds[beforeIndex]; });
17868
- if (precedingIndex >= 0) restoreAt = precedingIndex + 1;
17869
- }
17870
- for (var afterIndex = targetOrderIndex + 1; afterIndex < orderIds.length && restoreAt < 0; afterIndex++) {
17871
- var followingIndex = workspaceConversations.findIndex(function(item) { return String(item && item.id || '') === orderIds[afterIndex]; });
17872
- if (followingIndex >= 0) restoreAt = followingIndex;
17873
- }
17874
- if (restoreAt < 0) restoreAt = Math.min(Number(pending.index || 0), workspaceConversations.length);
17875
- workspaceConversations.splice(restoreAt, 0, pending.target);
17876
- if (pending.wasActive) {
17877
- for (var j = 0; j < workspaceConversations.length; j++) workspaceConversations[j].active = String(workspaceConversations[j].id || '') === targetId;
17878
- }
17879
- }
17880
- if (workspaceKey === currentWorkspaceKey()) {
17881
- state.conversations = workspaceConversations;
17882
- state.activeConversation = Math.max(0, workspaceConversations.findIndex(function(item) { return item && item.active; }));
17883
- if (pending && pending.wasActive) {
17884
- state.activeBackendConversationId = targetId;
17885
- if (els['chat-area']) els['chat-area'].innerHTML = pending.chatHtml || '';
17886
- }
17887
- renderConversations();
17888
- }
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.
17889
18005
  showUiNotice('[Archive] ' + t('workspace.saveFailed') + ': ' + (err.message || String(err)), 'error', 'archive-failed-' + targetId);
17890
18006
  });
17891
18007
  }
@@ -19660,18 +19776,26 @@ window.saveWsSetting = function(key, value) {
19660
19776
  window.renderFlowSelector = function() {
19661
19777
  var flowSel = document.getElementById('flow-select');
19662
19778
  if (!flowSel) return;
19779
+ var flows = Array.isArray(state.flowWorks) ? state.flowWorks : [];
19663
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;
19664
19789
  var html = '';
19665
- for (var fi = 0; fi < state.flowWorks.length; fi++) {
19666
- var selected = state.flowWorks[fi].name === defaultFlow ? ' selected' : '';
19667
- 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>';
19668
19793
  }
19669
19794
  flowSel.innerHTML = html;
19670
- if (state.flowWorks.length > 0) {
19671
- if (defaultFlow) flowSel.value = defaultFlow;
19672
- state.defaultFlow = flowSel.value;
19673
- }
19795
+ if (defaultFlow) flowSel.value = defaultFlow;
19796
+ state.defaultFlow = flowSel.value;
19674
19797
  flowSel.onchange = function() { state.defaultFlow = this.value; api.saveConfig({defaultFlow: state.defaultFlow}); };
19798
+ if (window.syncNewmarkSelect) window.syncNewmarkSelect(flowSel);
19675
19799
  };
19676
19800
 
19677
19801
  window.loadFlows = function(options) {
@@ -20071,6 +20195,48 @@ function schedulePostStartupUiRendering() {
20071
20195
  updateWorkspaceGate();
20072
20196
 
20073
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
+
20074
20240
  if (els.prompt) {
20075
20241
  els.prompt.addEventListener('keydown', function(e) {
20076
20242
  if (e.key === 'Escape' && currentFlowRunning() && flowTakeoverMatchesCurrent() && !promptHasText()) {