newmark-agent 0.3.12 → 0.4.2

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 (60) hide show
  1. package/dist/cli-commands.d.ts +1 -0
  2. package/dist/cli-commands.js +11 -2
  3. package/dist/context/domain/types.d.ts +37 -0
  4. package/dist/context/services/context-orchestrator.js +2 -0
  5. package/dist/conversation-utility-host.bundle.cjs +1259 -132
  6. package/dist/conversation-utility-host.js +3 -0
  7. package/dist/core/agent.d.ts +139 -5
  8. package/dist/core/agent.js +964 -82
  9. package/dist/core/agentKernel/agent-loop.js +29 -3
  10. package/dist/core/agentKernel/types.d.ts +7 -0
  11. package/dist/core/agentKernelRunner.d.ts +2 -0
  12. package/dist/core/agentKernelRunner.js +121 -19
  13. package/dist/core/conversationKernel.d.ts +5 -0
  14. package/dist/core/conversationKernel.js +29 -0
  15. package/dist/core/dshCompatibility.d.ts +198 -0
  16. package/dist/core/dshCompatibility.js +600 -0
  17. package/dist/core/electronUtilityAgentClient.d.ts +4 -0
  18. package/dist/core/electronUtilityAgentClient.js +4 -0
  19. package/dist/core/electronUtilityRuntimePool.d.ts +8 -0
  20. package/dist/core/electronUtilityRuntimePool.js +14 -0
  21. package/dist/core/mcpManager.d.ts +1 -0
  22. package/dist/core/mcpManager.js +100 -10
  23. package/dist/core/subagent.d.ts +6 -0
  24. package/dist/core/subagent.js +22 -1
  25. package/dist/core/toolPolicy.d.ts +15 -0
  26. package/dist/core/toolPolicy.js +174 -1
  27. package/dist/core/types.d.ts +1 -1
  28. package/dist/core/utilityAgentProtocol.d.ts +8 -1
  29. package/dist/core/workspace.d.ts +9 -0
  30. package/dist/core/workspace.js +48 -1
  31. package/dist/core/wslAgentClient.d.ts +4 -0
  32. package/dist/core/wslAgentClient.js +4 -0
  33. package/dist/core/wslAgentProtocol.d.ts +8 -1
  34. package/dist/core/wslAgentRuntimePool.d.ts +8 -0
  35. package/dist/core/wslAgentRuntimePool.js +15 -0
  36. package/dist/launcher.js +8 -0
  37. package/dist/llm/provider.d.ts +1 -1
  38. package/dist/llm/provider.js +4 -3
  39. package/dist/main.js +163 -11
  40. package/dist/preload.js +11 -0
  41. package/dist/providers/chat-completions.adapter.js +41 -15
  42. package/dist/providers/provider-adapter.d.ts +3 -0
  43. package/dist/toolchain/registry/tool-registry.d.ts +13 -1
  44. package/dist/toolchain/registry/tool-registry.js +8 -0
  45. package/dist/toolchain/registry-seeder.js +52 -6
  46. package/dist/tools/index.d.ts +1 -0
  47. package/dist/tools/index.js +39 -2
  48. package/dist/tools/nativeTools.js +6 -1
  49. package/dist/tui/src/app.js +24 -0
  50. package/dist/tui/src/i18n.js +151 -0
  51. package/dist/tui/src/render.js +152 -61
  52. package/dist/tui/src/state.js +83 -0
  53. package/dist/ui/index.html +2669 -234
  54. package/dist/ui/lucide-sprite.svg +31 -0
  55. package/dist/wsl-agent-host.bundle.cjs +1259 -132
  56. package/dist/wsl-agent-host.js +3 -0
  57. package/package.json +6 -10
  58. package/Flow/Electron-Debug-Release.Flow.json +0 -43
  59. package/Flow/Flow.md +0 -9
  60. package/Flow/UI-Feature-Integration.Flow.json +0 -96
@@ -11,6 +11,10 @@ export interface WslTargetRuntimeClient {
11
11
  requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslAgentStopResult>;
12
12
  enqueueGuide(target: ConversationRuntimeTarget, envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
13
13
  checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
14
+ contextCompress?(target: ConversationRuntimeTarget, options?: {
15
+ keepRecent?: number;
16
+ force?: boolean;
17
+ }): Promise<Record<string, unknown>>;
14
18
  rateAutoRoute?(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
15
19
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
16
20
  setMode?(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
@@ -69,6 +73,10 @@ export declare class WslAgentRuntimePool {
69
73
  requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslPoolStopResult>;
70
74
  enqueueGuide(envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
71
75
  checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
76
+ contextCompress(target: ConversationRuntimeTarget, options?: {
77
+ keepRecent?: number;
78
+ force?: boolean;
79
+ }): Promise<Record<string, unknown>>;
72
80
  rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
73
81
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
74
82
  setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next' | null>;
@@ -214,6 +214,21 @@ class WslAgentRuntimePool {
214
214
  this.release(entry, true);
215
215
  }
216
216
  }
217
+ async contextCompress(target, options = {}) {
218
+ const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
219
+ const entry = await this.acquire(normalized);
220
+ try {
221
+ if (entry.stopIntent)
222
+ return { ok: false, error: 'Context compression is unavailable while this conversation is stopping.' };
223
+ if (!entry.client.contextCompress)
224
+ return { ok: false, error: 'Context compression is unavailable in this runtime.' };
225
+ entry.lastSnapshot = null;
226
+ return await entry.client.contextCompress(normalized, options);
227
+ }
228
+ finally {
229
+ this.release(entry, true);
230
+ }
231
+ }
217
232
  async rateAutoRoute(target, score, routeId = '') {
218
233
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
219
234
  const entry = await this.acquireExisting(normalized);
package/dist/launcher.js CHANGED
@@ -49,6 +49,7 @@ const cli_help_1 = require("./cli-help");
49
49
  const rawArgs = process.argv.slice(2);
50
50
  const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
51
51
  const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
52
+ const cliCommand = args.find(a => cli_commands_1.CLI_COMMANDS.includes(a));
52
53
  const isEdit = args[0] === 'edit';
53
54
  const editFile = isEdit ? args[1] : '';
54
55
  const isFlow = args[0] === 'flow';
@@ -64,6 +65,13 @@ if (invalidArgument) {
64
65
  console.error(`Invalid Newmark argument: ${invalidArgument}`);
65
66
  process.exit(2);
66
67
  }
68
+ // Command help is a terminating, read-only discovery operation. Resolve it
69
+ // before first-run initialization or GUI forwarding so `Newmark.exe send
70
+ // --help` cannot accidentally enter Electron or instantiate an Agent.
71
+ if (hasCliCommand && cliCommand && (0, cli_commands_1.cliHelpRequested)(args)) {
72
+ console.log((0, cli_commands_1.cliCommandHelp)(cliCommand));
73
+ process.exit(0);
74
+ }
67
75
  function pathArgValue(values, key) {
68
76
  const prefix = `${key}=`;
69
77
  const inlineIdx = values.findIndex(a => a.startsWith(prefix));
@@ -97,7 +97,7 @@ export declare class LLMProvider {
97
97
  private toTransportResponse;
98
98
  private toNormalizedMessages;
99
99
  private toNormalizedTools;
100
- chatStreamWithTools(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, tools: unknown[], signal?: AbortSignal, reasoningTier?: string): AsyncGenerator<StreamToken>;
100
+ chatStreamWithTools(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, tools: unknown[], signal?: AbortSignal, reasoningTier?: string, sessionId?: string): AsyncGenerator<StreamToken>;
101
101
  /**
102
102
  * GitHub Models streaming path. Preserved as a dedicated implementation
103
103
  * because the provider adapters (V2) do not serialize the GitHub Models
@@ -843,7 +843,7 @@ class LLMProvider {
843
843
  * The emitted request body and StreamToken stream are byte-equivalent to
844
844
  * the legacy inlined path.
845
845
  */
846
- async *chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier) {
846
+ async *chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId) {
847
847
  const mode = this.openAITransportMode();
848
848
  if (mode === 'responses') {
849
849
  yield* this.adapterResponsesBridge(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier);
@@ -861,6 +861,7 @@ class LLMProvider {
861
861
  maxOutputTokens: maxTokens,
862
862
  apiKey: this.apiKey,
863
863
  baseUrl: this.cleanBaseUrl(),
864
+ ...(sessionId ? { sessionId } : {}),
864
865
  };
865
866
  const serialized = await adapter.serializeRequest(request);
866
867
  serialized.body.stream = mode === 'chat' ? false : true;
@@ -1080,7 +1081,7 @@ class LLMProvider {
1080
1081
  };
1081
1082
  });
1082
1083
  }
1083
- async *chatStreamWithTools(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier) {
1084
+ async *chatStreamWithTools(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId) {
1084
1085
  if (signal?.aborted)
1085
1086
  throw abortFailure(signal);
1086
1087
  if (this.protocol() === 'anthropic') {
@@ -1092,7 +1093,7 @@ class LLMProvider {
1092
1093
  return;
1093
1094
  }
1094
1095
  if (this.useProviderAdaptersV2) {
1095
- yield* this.chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier);
1096
+ yield* this.chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId);
1096
1097
  return;
1097
1098
  }
1098
1099
  throw new Error('LLMProvider legacy OpenAI streaming was removed in dev-0.3.0: enable provider_adapters_v2 (useProviderAdaptersV2).');
package/dist/main.js CHANGED
@@ -73,6 +73,7 @@ const startupPrewarm_1 = require("./core/startupPrewarm");
73
73
  const runtimeShutdown_1 = require("./core/runtimeShutdown");
74
74
  const runtimeLifecycle_1 = require("./core/runtimeLifecycle");
75
75
  const compat_1 = require("./core/compat");
76
+ const dshCompatibility_1 = require("./core/dshCompatibility");
76
77
  const mcpManager_1 = require("./core/mcpManager");
77
78
  const cli_help_1 = require("./cli-help");
78
79
  const APP_NAME = 'Newmark Agent';
@@ -123,6 +124,7 @@ let browserUseEngine = null;
123
124
  // the authoritative Browser-Use/right-sidebar binding.
124
125
  const browserGuestContentsByHost = new Map();
125
126
  const browserGuestBindingsByRuntime = new Map();
127
+ const browserGuestKeyboardBridgeIds = new Set();
126
128
  function browserGuestRuntimeKey(target) {
127
129
  return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
128
130
  }
@@ -686,9 +688,13 @@ function resolveTuiWorkspacePath(args, root) {
686
688
  // while making the safe one-argument form fully self-contained.
687
689
  return pathArgValue(args, '--root') ? root : process.cwd();
688
690
  }
691
+ // Set immediately after argument resolution so startup failures from an
692
+ // explicit temporary root are recorded beside that root instead of leaking a
693
+ // diagnostic file into the user's canonical runtime.
694
+ let startupRuntimeRoot = '';
689
695
  function startupLogPath() {
690
696
  try {
691
- const userData = userRuntimeRoot();
697
+ const userData = startupRuntimeRoot || userRuntimeRoot();
692
698
  fs.mkdirSync(userData, { recursive: true });
693
699
  return path.join(userData, 'startup.log');
694
700
  }
@@ -841,7 +847,29 @@ function registerBrowserGuest(host, guest, requestedTarget) {
841
847
  conversationId: target.conversationId,
842
848
  });
843
849
  browserGuestContentsByHost.set(host.id, guest.id);
850
+ if (!browserGuestKeyboardBridgeIds.has(guest.id)) {
851
+ browserGuestKeyboardBridgeIds.add(guest.id);
852
+ // WebView keyboard events do not bubble to the host renderer. Reserve only
853
+ // the two app-wide discovery surfaces and leave navigation/editing keys to
854
+ // the page itself.
855
+ guest.on('before-input-event', (event, input) => {
856
+ if (input.type !== 'keyDown' || input.isAutoRepeat || host.isDestroyed())
857
+ return;
858
+ const key = String(input.key || '').toLowerCase();
859
+ const noSecondaryModifiers = !input.alt && !input.control && !input.meta && !input.shift;
860
+ let commandId = '';
861
+ if (key === 'f1' && noSecondaryModifiers)
862
+ commandId = 'help.keyboardShortcuts';
863
+ else if (key === 'p' && (input.control || input.meta) && input.shift && !input.alt)
864
+ commandId = 'app.commandPalette';
865
+ if (!commandId)
866
+ return;
867
+ event.preventDefault();
868
+ host.send('keyboard:command', { id: commandId, source: 'browserGuest' });
869
+ });
870
+ }
844
871
  guest.once('destroyed', () => {
872
+ browserGuestKeyboardBridgeIds.delete(guest.id);
845
873
  if (browserGuestContentsByHost.get(host.id) === guest.id)
846
874
  browserGuestContentsByHost.delete(host.id);
847
875
  const binding = browserGuestBindingsByRuntime.get(target.runtimeKey);
@@ -1035,6 +1063,7 @@ const args = userArgs();
1035
1063
  const command = args.find(a => a === 'flow' || a === 'edit');
1036
1064
  const isTuiArg = args.some(arg => arg.toLowerCase() === '--tui');
1037
1065
  const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
1066
+ const cliCommand = args.find(a => cli_commands_1.CLI_COMMANDS.includes(a));
1038
1067
  const isFlowArg = command === 'flow';
1039
1068
  const isEditArg = command === 'edit';
1040
1069
  const isHelpArg = !hasCliCommand && (args.some(arg => ['--help', '-h'].includes(arg.toLowerCase())) || args[0]?.toLowerCase() === 'help');
@@ -1048,18 +1077,34 @@ if (invalidArgument) {
1048
1077
  console.error(`Invalid Newmark argument: ${invalidArgument}`);
1049
1078
  process.exit(2);
1050
1079
  }
1080
+ // Keep command-specific help on the same early-exit path as top-level help.
1081
+ // This matters for the packaged Electron entry: a command such as
1082
+ // `send --help` must never create a GUI window, start prewarm work, or touch a
1083
+ // user-data profile merely to print its contract.
1084
+ if (hasCliCommand && cliCommand && (0, cli_commands_1.cliHelpRequested)(args)) {
1085
+ console.log((0, cli_commands_1.cliCommandHelp)(cliCommand));
1086
+ process.exit(0);
1087
+ }
1051
1088
  // Electron's Chromium profile is a separate state boundary from Newmark's
1052
1089
  // business root. Bind both before any ready event so --root cannot leave
1053
1090
  // Preferences, DIPS, DevTools ports, cookies, or session storage in the real
1054
1091
  // default AppData directory. The dedicated subdirectories keep Chromium's
1055
1092
  // files separate from the durable Newmark config/workspace files.
1056
1093
  const runtimeRoot = resolveRoot(args);
1057
- const electronUserDataRoot = path.join(runtimeRoot, 'Electron');
1094
+ startupRuntimeRoot = runtimeRoot;
1095
+ const explicitElectronUserDataRoot = pathArgValue(args, '--user-data-dir');
1096
+ const electronUserDataRoot = path.resolve(explicitElectronUserDataRoot || path.join(runtimeRoot, 'Electron'));
1058
1097
  const electronSessionDataRoot = path.join(electronUserDataRoot, 'session-data');
1059
1098
  try {
1060
1099
  fs.mkdirSync(electronSessionDataRoot, { recursive: true });
1061
1100
  electron_1.app.setPath('userData', electronUserDataRoot);
1062
1101
  electron_1.app.setPath('sessionData', electronSessionDataRoot);
1102
+ // app.setPath is the Electron API contract; the switch makes the same
1103
+ // boundary visible to Chromium children, including the console-wrapper
1104
+ // `--` forwarding path where an early child may otherwise retain the default
1105
+ // profile before the first BrowserWindow is created.
1106
+ electron_1.app.commandLine.removeSwitch('user-data-dir');
1107
+ electron_1.app.commandLine.appendSwitch('user-data-dir', electronUserDataRoot);
1063
1108
  }
1064
1109
  catch (error) {
1065
1110
  console.error(`Unable to isolate Electron user-data directory: ${error instanceof Error ? error.message : String(error)}`);
@@ -1659,6 +1704,12 @@ else {
1659
1704
  win.focus();
1660
1705
  }, 150);
1661
1706
  });
1707
+ win.webContents.on('unresponsive', () => {
1708
+ logStartupFailure('renderer-unresponsive', new Error(`Window renderer became unresponsive (webContents ${win.webContents.id})`));
1709
+ });
1710
+ win.webContents.on('responsive', () => {
1711
+ recordStartup(`renderer-responsive-${win.webContents.id}`);
1712
+ });
1662
1713
  if (!automationWakeMode)
1663
1714
  win.maximize();
1664
1715
  if (!automationWakeMode && showWindow) {
@@ -1674,15 +1725,18 @@ else {
1674
1725
  return;
1675
1726
  if (agent) {
1676
1727
  const closeBehavior = agent.config.getStr('general', 'close_behavior');
1728
+ recordStartup(`window-close-requested-${closeBehavior || 'exit'}`);
1677
1729
  if (closeBehavior === 'minimize') {
1678
1730
  e.preventDefault();
1679
1731
  win.hide();
1680
1732
  createTray();
1733
+ recordStartup('window-hidden-to-tray');
1681
1734
  return;
1682
1735
  }
1683
1736
  if (agent.config.getBool('general', 'auto_archive_on_close')) {
1684
1737
  agent.archiveSession();
1685
1738
  }
1739
+ recordStartup('window-close-exit');
1686
1740
  }
1687
1741
  });
1688
1742
  win.on('closed', () => {
@@ -2299,6 +2353,7 @@ else {
2299
2353
  (0, runtimeLifecycle_1.markRuntimeLifecycleClean)(root, 'main');
2300
2354
  };
2301
2355
  electron_1.app.on('will-quit', event => {
2356
+ recordStartup('will-quit');
2302
2357
  // Window-close and tray-exit both enter this path. The runtime pools
2303
2358
  // must get a bounded graceful-shutdown window regardless of which
2304
2359
  // surface initiated the close; otherwise a stuck child can keep the
@@ -2462,6 +2517,32 @@ else {
2462
2517
  activeFlowsByRuntimeKey.delete(key);
2463
2518
  agent.clearStoredFlowSuspension(state.target.conversationId);
2464
2519
  };
2520
+ // Archive is a destructive lifecycle boundary. It must cancel the Flow
2521
+ // owner itself before touching the runtime pool; Flow runs bypass the
2522
+ // ConversationKernel and therefore cannot be stopped by the pool alone.
2523
+ // Marking the state before aborting is important: the provider may reject
2524
+ // on the same turn, and its late catch/finally must not recreate a paused
2525
+ // conversation after archive has removed it.
2526
+ const interruptActiveFlowForArchive = (target) => {
2527
+ const key = activeFlowStateKey(target);
2528
+ const state = activeFlowsByRuntimeKey.get(key) || null;
2529
+ if (!state)
2530
+ return;
2531
+ state.archiveRequested = true;
2532
+ try {
2533
+ state.abortController?.abort(new Error(`Flow discarded because conversation was archived: ${state.name || state.workflow.name}`));
2534
+ }
2535
+ catch { }
2536
+ state.flowAgent?.abortActiveKernelRun();
2537
+ state.flowAgent?.interruptRunningConversationWorkRuns(state.target, 'force_interrupted');
2538
+ state.flowAgent?.clearStoredFlowSuspension(state.target.conversationId);
2539
+ if (agent?.workspace.current
2540
+ && target.workspace
2541
+ && path.resolve(agent.workspace.current.path) === path.resolve(target.workspace.path)) {
2542
+ agent.clearStoredFlowSuspension(target.conversationId);
2543
+ }
2544
+ activeFlowsByRuntimeKey.delete(key);
2545
+ };
2465
2546
  const utilityHostToolHandler = (0, utilityHostToolRouter_1.createUtilityHostToolHandler)({
2466
2547
  persistenceRoot: root,
2467
2548
  isToolEnabled: toolName => !!agent && (0, nativeTools_1.isNativeToolEnabled)(toolName, agent.config.nativeToolEnabled()),
@@ -2851,6 +2932,14 @@ else {
2851
2932
  };
2852
2933
  }
2853
2934
  catch (e) {
2935
+ if (flowState.archiveRequested) {
2936
+ // Archive already force-finalized the current Build ledger. Do not
2937
+ // persist an interrupted Flow suspension or return Flow ownership
2938
+ // to the renderer after the target has been removed.
2939
+ flowAgent.pendingOptions = [];
2940
+ suspended = false;
2941
+ return { ok: false, archived: true, error: 'Flow discarded because the conversation was archived.' };
2942
+ }
2854
2943
  if (e instanceof flow_runner_1.FlowQuestionPendingError) {
2855
2944
  suspended = true;
2856
2945
  flowAgent.flowPc = e.componentId;
@@ -2944,6 +3033,11 @@ else {
2944
3033
  const flowKey = activeFlowStateKey(flowTarget);
2945
3034
  const flowAgent = isolatedConversationAgent(flowTarget);
2946
3035
  suspension.flowAgent = flowAgent;
3036
+ // A previous Flow may have been interrupted just before its isolated
3037
+ // Agent flushed the work ledger. Reconcile only this explicitly paused
3038
+ // target before starting the next component; the normal runner guard
3039
+ // remains intact for genuine concurrent Builds.
3040
+ flowAgent.interruptRunningConversationWorkRuns(flowTarget, 'interrupted');
2947
3041
  const flowAbortController = new AbortController();
2948
3042
  suspension.abortController = flowAbortController;
2949
3043
  activeFlowsByRuntimeKey.set(flowKey, suspension);
@@ -2978,6 +3072,11 @@ else {
2978
3072
  };
2979
3073
  }
2980
3074
  catch (error) {
3075
+ if (suspension.archiveRequested) {
3076
+ flowAgent.pendingOptions = [];
3077
+ suspendedAgain = false;
3078
+ return { ok: false, archived: true, error: 'Flow discarded because the conversation was archived.' };
3079
+ }
2981
3080
  if (error instanceof flow_runner_1.FlowQuestionPendingError) {
2982
3081
  suspendedAgain = true;
2983
3082
  flowAgent.flowPc = error.componentId;
@@ -2999,9 +3098,9 @@ else {
2999
3098
  workRuns: flowAgent.getConversationSnapshot(flowAgent.activeConversationId).workRuns,
3000
3099
  };
3001
3100
  }
3002
- if (isUserFlowAbort(error)) {
3003
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
3004
- }
3101
+ // A Stop/Esc during a resumed Flow is another pause, not a terminal
3102
+ // IPC failure. Fall through to the same interrupted-suspension path as
3103
+ // the initial Flow run so repeated pause/resume clicks remain valid.
3005
3104
  suspendedAgain = true;
3006
3105
  const resumeFailure = error;
3007
3106
  const interruptedComponentId = typeof resumeFailure.componentId === 'number'
@@ -3154,6 +3253,12 @@ else {
3154
3253
  runtimeDeferred: false,
3155
3254
  };
3156
3255
  });
3256
+ electron_1.ipcMain.handle('agent:setConversationBranchCommunication', async (_event, targetInput, enabled) => {
3257
+ if (!agent)
3258
+ return false;
3259
+ const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3260
+ return mutateTargetConversation(target, () => isolatedConversationAgent(target).setBranchCommunication(enabled !== false));
3261
+ });
3157
3262
  electron_1.ipcMain.handle('agent:computerUseState', async (_event, targetInput) => {
3158
3263
  if (!agent)
3159
3264
  return { enabled: false, occupied: false, runtimeKey: '' };
@@ -3691,6 +3796,18 @@ else {
3691
3796
  ? await ensureWslConversationPool().checkpoint(target)
3692
3797
  : await ensureElectronUtilityPool().checkpoint(target);
3693
3798
  });
3799
+ electron_1.ipcMain.handle('agent:compressContext', async (_event, request) => {
3800
+ if (!agent)
3801
+ throw new Error('Agent not initialized');
3802
+ const target = conversationRuntimeTarget(request);
3803
+ const options = {
3804
+ keepRecent: Number.isFinite(Number(request?.keepRecent)) ? Math.floor(Number(request.keepRecent)) : undefined,
3805
+ force: request?.force !== false,
3806
+ };
3807
+ return wslBackendEnabled()
3808
+ ? await ensureWslConversationPool().contextCompress(target, options)
3809
+ : await ensureElectronUtilityPool().contextCompress(target, options);
3810
+ });
3694
3811
  electron_1.ipcMain.handle('agent:rateAutoRoute', async (_event, request) => {
3695
3812
  if (!agent)
3696
3813
  return { ok: false, reason: 'no_active_auto_route' };
@@ -3847,9 +3964,14 @@ else {
3847
3964
  mutatingRuntimeKeys.add(normalized.runtimeKey);
3848
3965
  try {
3849
3966
  // Archive is a destructive lifecycle command. It intentionally
3850
- // bypasses the normal mutation/active-prompt guard and hard-stops
3851
- // any resident runtime before touching conversation persistence.
3852
- await forceStopTargetRuntime(normalized);
3967
+ // bypasses the normal mutation/active-prompt guard. Cancel the
3968
+ // conversation-local Flow synchronously, then start the resident
3969
+ // runtime hard-stop in the background so a stuck child cannot hold
3970
+ // the archive click hostage.
3971
+ interruptActiveFlowForArchive(normalized);
3972
+ void forceStopTargetRuntime(normalized).catch(error => {
3973
+ console.error('[Newmark] archive runtime force-stop failed:', error instanceof Error ? error.message : String(error));
3974
+ });
3853
3975
  const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
3854
3976
  const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
3855
3977
  const ownsTargetWorkspace = !!normalized.workspace
@@ -3861,6 +3983,7 @@ else {
3861
3983
  // latest locked state snapshot, so rapid clicks do not serialize on
3862
3984
  // large Markdown bodies or lose a sibling deletion.
3863
3985
  const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
3986
+ archiveOwner.clearStoredFlowSuspension(normalized.conversationId);
3864
3987
  const archived = await archiveOwner.archiveConversationAsync(normalized.conversationId);
3865
3988
  if (!archived)
3866
3989
  return { ok: false, error: 'Conversation archive could not be written.' };
@@ -4205,6 +4328,20 @@ else {
4205
4328
  }
4206
4329
  return { error: 'Agent not initialized' };
4207
4330
  });
4331
+ electron_1.ipcMain.handle('app:openWebUrl', async (_event, rawUrl) => {
4332
+ try {
4333
+ const target = new URL(String(rawUrl || ''));
4334
+ const allowedHosts = new Set(['github.com', 'www.github.com', 'npmjs.com', 'www.npmjs.com']);
4335
+ if (target.protocol !== 'https:' || !allowedHosts.has(target.hostname.toLowerCase()) || !!target.username || !!target.password || (!!target.port && target.port !== '443')) {
4336
+ return { ok: false, error: 'Only approved HTTPS documentation hosts can be opened.' };
4337
+ }
4338
+ await electron_1.shell.openExternal(target.toString());
4339
+ return { ok: true };
4340
+ }
4341
+ catch (error) {
4342
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
4343
+ }
4344
+ });
4208
4345
  electron_1.ipcMain.handle('agent:selectWorkspace', async (_event, id) => {
4209
4346
  if (agent) {
4210
4347
  const requested = String(id || '').trim();
@@ -4502,13 +4639,25 @@ else {
4502
4639
  const controller = new AbortController();
4503
4640
  editorCompletionControllers.set(ownerId, controller);
4504
4641
  try {
4505
- return await (agent?.editorModelRequest({ ...request, completion: true, preferCopilot: true }, controller.signal) || { ok: false, text: '', error: 'Agent not initialized' });
4642
+ const requestId = String(request.requestId || '');
4643
+ const onTextDelta = requestId
4644
+ ? (text) => {
4645
+ if (!controller.signal.aborted && !event.sender.isDestroyed())
4646
+ event.sender.send('agent:editorCompletionDelta', { requestId, text });
4647
+ }
4648
+ : undefined;
4649
+ return await (agent?.editorModelRequest({ ...request, completion: true, preferCopilot: true, onTextDelta }, controller.signal) || { ok: false, text: '', error: 'Agent not initialized' });
4506
4650
  }
4507
4651
  finally {
4508
4652
  if (editorCompletionControllers.get(ownerId) === controller)
4509
4653
  editorCompletionControllers.delete(ownerId);
4510
4654
  }
4511
4655
  });
4656
+ electron_1.ipcMain.handle('agent:editorCompleteCancel', async (event) => {
4657
+ const ownerId = event.sender.id;
4658
+ editorCompletionControllers.get(ownerId)?.abort(new Error('Editor completion cancelled'));
4659
+ return { ok: true };
4660
+ });
4512
4661
  electron_1.ipcMain.handle('agent:editorAssist', async (_event, request) => {
4513
4662
  return agent?.editorModelRequest({ ...request, completion: false }) || { ok: false, text: '', error: 'Agent not initialized' };
4514
4663
  });
@@ -4605,6 +4754,7 @@ else {
4605
4754
  })));
4606
4755
  return { servers: mcpManager.list(), discovered };
4607
4756
  });
4757
+ electron_1.ipcMain.handle('dsh:discover', async () => (0, dshCompatibility_1.discoverDshCompatibility)(root));
4608
4758
  electron_1.ipcMain.handle('mcp:upsert', async (_event, input) => {
4609
4759
  if (!mcpManager)
4610
4760
  return { ok: false, error: 'MCP manager is unavailable.' };
@@ -4619,12 +4769,14 @@ else {
4619
4769
  electron_1.ipcMain.handle('mcp:setEnabled', async (_event, id, enabled) => {
4620
4770
  if (!mcpManager)
4621
4771
  return { ok: false, error: 'MCP manager is unavailable.' };
4622
- return { ok: mcpManager.setEnabled(id, enabled), servers: mcpManager.list() };
4772
+ const ok = mcpManager.setEnabled(id, enabled);
4773
+ return { ok, error: ok ? undefined : 'MCP server was not found.', servers: mcpManager.list() };
4623
4774
  });
4624
4775
  electron_1.ipcMain.handle('mcp:remove', async (_event, id) => {
4625
4776
  if (!mcpManager)
4626
4777
  return { ok: false, error: 'MCP manager is unavailable.' };
4627
- return { ok: mcpManager.remove(id), servers: mcpManager.list() };
4778
+ const ok = mcpManager.remove(id);
4779
+ return { ok, error: ok ? undefined : 'MCP server was not found.', servers: mcpManager.list() };
4628
4780
  });
4629
4781
  electron_1.ipcMain.handle('memoryLab:read', async (_event, selector) => {
4630
4782
  if (!agent)
package/dist/preload.js CHANGED
@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('api', {
12
12
  sendMessage: (message, target) => ipcRenderer.invoke('agent:send', message, target),
13
13
  enqueueGuide: (envelope) => ipcRenderer.invoke('agent:enqueueGuide', envelope),
14
14
  checkpointConversation: (request) => ipcRenderer.invoke('agent:checkpointConversation', request),
15
+ compressContext: (request) => ipcRenderer.invoke('agent:compressContext', request),
15
16
  rateAutoRoute: (request) => ipcRenderer.invoke('agent:rateAutoRoute', request),
16
17
  stopConversation: (request) => ipcRenderer.invoke('agent:stopConversation', request),
17
18
  setWorkRunExpanded: (request) => ipcRenderer.invoke('agent:setWorkRunExpanded', request),
@@ -32,12 +33,16 @@ contextBridge.exposeInMainWorld('api', {
32
33
  getConversationPlan: (conversationId) => ipcRenderer.invoke('agent:getConversationPlan', conversationId),
33
34
  updateConversationPlan: (plan, conversationId) => ipcRenderer.invoke('agent:updateConversationPlan', plan, conversationId),
34
35
  setConversationPinned: (id, pinned) => ipcRenderer.invoke('agent:setConversationPinned', id, pinned),
36
+ setConversationBranchCommunication: (target, enabled) => ipcRenderer.invoke('agent:setConversationBranchCommunication', target, enabled),
35
37
  renameConversation: (id, title) => ipcRenderer.invoke('agent:renameConversation', id, title),
36
38
  reorderConversations: (ids) => ipcRenderer.invoke('agent:reorderConversations', ids),
37
39
  browserRegisterGuest: (guestContentsId, target) => ipcRenderer.invoke('browser:registerGuest', guestContentsId, target),
38
40
  onBrowserEnsureGuest: (callback) => {
39
41
  ipcRenderer.on('browser:ensureGuest', (_event, target) => callback(target));
40
42
  },
43
+ onKeyboardCommand: (callback) => {
44
+ ipcRenderer.on('keyboard:command', (_event, payload) => callback(payload));
45
+ },
41
46
  browserControl: (request) => ipcRenderer.invoke('browser:control', request),
42
47
  computerUseState: (target) => ipcRenderer.invoke('agent:computerUseState', target),
43
48
  setComputerUseEnabled: (target, enabled) => ipcRenderer.invoke('agent:setComputerUseEnabled', target, enabled),
@@ -72,6 +77,10 @@ contextBridge.exposeInMainWorld('api', {
72
77
  readWorkspacePrompt: () => ipcRenderer.invoke('workspace:readPrompt'),
73
78
  saveWorkspacePrompt: (content) => ipcRenderer.invoke('workspace:savePrompt', content),
74
79
  editorComplete: (request) => ipcRenderer.invoke('agent:editorComplete', request),
80
+ editorCompleteCancel: () => ipcRenderer.invoke('agent:editorCompleteCancel'),
81
+ onEditorCompletionDelta: (callback) => {
82
+ ipcRenderer.on('agent:editorCompletionDelta', (_event, payload) => callback(payload));
83
+ },
75
84
  editorAssist: (request) => ipcRenderer.invoke('agent:editorAssist', request),
76
85
  filePathForFile: (file) => {
77
86
  try {
@@ -85,6 +94,7 @@ contextBridge.exposeInMainWorld('api', {
85
94
  selectFolder: () => ipcRenderer.invoke('dialog:selectFolder'),
86
95
  executeBash: (cmd, shell, cwd) => ipcRenderer.invoke('agent:executeBash', cmd, shell, cwd),
87
96
  openExternal: (path) => ipcRenderer.invoke('agent:openExternal', path),
97
+ openWebUrl: (url) => ipcRenderer.invoke('app:openWebUrl', url),
88
98
  selectWorkspace: (id) => ipcRenderer.invoke('agent:selectWorkspace', id),
89
99
  createWorkspace: (name) => ipcRenderer.invoke('agent:createWorkspace', name),
90
100
  createExternalWorkspace: (name, dirPath) => ipcRenderer.invoke('agent:createExternalWorkspace', name, dirPath),
@@ -113,6 +123,7 @@ contextBridge.exposeInMainWorld('api', {
113
123
  removeSkill: (name) => ipcRenderer.invoke('skills:remove', name),
114
124
  refreshSkills: () => ipcRenderer.invoke('skills:refresh'),
115
125
  listMcpServers: () => ipcRenderer.invoke('mcp:list'),
126
+ discoverDshCompatibility: () => ipcRenderer.invoke('dsh:discover'),
116
127
  upsertMcpServer: (input) => ipcRenderer.invoke('mcp:upsert', input),
117
128
  setMcpServerEnabled: (id, enabled) => ipcRenderer.invoke('mcp:setEnabled', id, enabled),
118
129
  removeMcpServer: (id) => ipcRenderer.invoke('mcp:remove', id),
@@ -48,6 +48,10 @@ class ChatCompletionsAdapter {
48
48
  };
49
49
  if (request.reasoningEffort)
50
50
  body.reasoning_effort = request.reasoningEffort;
51
+ // 会话标识透传:仅当上层(支持 session_id 语义的 provider)显式填充时写进
52
+ // body,否则省略,避免严格 API 拒绝未知字段。
53
+ if (request.sessionId)
54
+ body.session_id = request.sessionId;
51
55
  const base = request.baseUrl.replace(/\/+$/, '');
52
56
  return {
53
57
  url: `${base}/chat/completions`,
@@ -90,7 +94,10 @@ class ChatCompletionsAdapter {
90
94
  }
91
95
  const decoder = new TextDecoder();
92
96
  let buffer = '';
93
- let currentToolCall = null;
97
+ const toolCalls = new Map();
98
+ const toolCallOrder = [];
99
+ let syntheticToolIndex = 0;
100
+ let lastToolIndex = 0;
94
101
  let contentPolicyBlocked = false;
95
102
  let emittedContent = false;
96
103
  let emittedTool = false;
@@ -137,32 +144,51 @@ class ChatCompletionsAdapter {
137
144
  emittedContent = true;
138
145
  yield { type: 'text.delta', delta: textDelta };
139
146
  }
140
- const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
141
- for (const raw of toolCalls) {
147
+ const deltaToolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
148
+ for (const raw of deltaToolCalls) {
142
149
  const tc = raw;
143
150
  const fn = tc.function && typeof tc.function === 'object' ? tc.function : {};
144
- if (tc.id) {
145
- if (currentToolCall) {
146
- emittedTool = true;
147
- yield { type: 'tool_call.completed', id: currentToolCall.id, name: currentToolCall.name, arguments: currentToolCall.arguments };
148
- }
151
+ const rawIndex = Number(tc.index);
152
+ const index = Number.isInteger(rawIndex) && rawIndex >= 0
153
+ ? rawIndex
154
+ : (tc.id ? syntheticToolIndex++ : lastToolIndex);
155
+ lastToolIndex = index;
156
+ let currentToolCall = toolCalls.get(index);
157
+ if (!currentToolCall && tc.id) {
149
158
  currentToolCall = {
150
159
  id: String(tc.id || ''),
151
160
  name: (0, chat_messages_1.openAIToolName)(String(fn.name || '')),
152
- arguments: String(fn.arguments || ''),
161
+ argumentParts: [],
153
162
  };
163
+ toolCalls.set(index, currentToolCall);
164
+ toolCallOrder.push(index);
154
165
  yield { type: 'tool_call.started', id: currentToolCall.id, name: currentToolCall.name };
155
166
  }
156
- else if (fn.arguments && currentToolCall) {
157
- currentToolCall.arguments += String(fn.arguments);
158
- yield { type: 'tool_call.arguments.delta', id: currentToolCall.id, delta: String(fn.arguments) };
167
+ if (currentToolCall && fn.name && !currentToolCall.name)
168
+ currentToolCall.name = (0, chat_messages_1.openAIToolName)(String(fn.name));
169
+ if (currentToolCall && fn.arguments !== undefined && fn.arguments !== null) {
170
+ const argumentDelta = String(fn.arguments);
171
+ if (argumentDelta) {
172
+ currentToolCall.argumentParts.push(argumentDelta);
173
+ yield { type: 'tool_call.arguments.delta', id: currentToolCall.id, delta: argumentDelta };
174
+ }
159
175
  }
160
176
  }
161
177
  }
162
178
  }
163
- if (currentToolCall && currentToolCall.arguments) {
164
- emittedTool = true;
165
- yield { type: 'tool_call.completed', id: currentToolCall.id, name: currentToolCall.name, arguments: currentToolCall.arguments };
179
+ if (toolCallOrder.length) {
180
+ for (const index of toolCallOrder) {
181
+ const currentToolCall = toolCalls.get(index);
182
+ if (!currentToolCall)
183
+ continue;
184
+ emittedTool = true;
185
+ yield {
186
+ type: 'tool_call.completed',
187
+ id: currentToolCall.id,
188
+ name: currentToolCall.name,
189
+ arguments: currentToolCall.argumentParts.join(''),
190
+ };
191
+ }
166
192
  }
167
193
  else if (!emittedContent && !emittedTool && contentPolicyBlocked) {
168
194
  yield { type: 'response.failed', error: '[Error] Content policy refusal (content_filter).' };
@@ -55,6 +55,9 @@ export interface NormalizedAgentRequest {
55
55
  reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
56
56
  apiKey: string;
57
57
  baseUrl: string;
58
+ /** 可选的会话标识。仅当目标 provider 显式支持 session_id 语义的上下文缓存
59
+ * 时才由上层填充;adapter 在存在时透传,否则省略该字段(避免严格 API 拒绝未知字段)。 */
60
+ sessionId?: string;
58
61
  }
59
62
  export interface TokenEstimate {
60
63
  inputTokens: number;