newmark-agent 0.3.11 → 0.4.0

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 (69) hide show
  1. package/config.example.json +6 -0
  2. package/dist/cli-commands.d.ts +8 -0
  3. package/dist/cli-commands.js +216 -16
  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/context/domain/types.d.ts +37 -0
  9. package/dist/context/services/context-orchestrator.js +2 -0
  10. package/dist/conversation-utility-host.bundle.cjs +1503 -214
  11. package/dist/conversation-utility-host.js +3 -0
  12. package/dist/core/agent.d.ts +157 -8
  13. package/dist/core/agent.js +1176 -112
  14. package/dist/core/agentKernel/agent-loop.js +29 -3
  15. package/dist/core/agentKernel/types.d.ts +7 -0
  16. package/dist/core/agentKernelRunner.d.ts +2 -0
  17. package/dist/core/agentKernelRunner.js +174 -27
  18. package/dist/core/config.d.ts +7 -2
  19. package/dist/core/config.js +24 -6
  20. package/dist/core/conversationKernel.d.ts +5 -0
  21. package/dist/core/conversationKernel.js +30 -1
  22. package/dist/core/dshCompatibility.d.ts +198 -0
  23. package/dist/core/dshCompatibility.js +600 -0
  24. package/dist/core/electronUtilityAgentClient.d.ts +4 -0
  25. package/dist/core/electronUtilityAgentClient.js +4 -0
  26. package/dist/core/electronUtilityRuntimePool.d.ts +19 -0
  27. package/dist/core/electronUtilityRuntimePool.js +76 -0
  28. package/dist/core/flow-runner.js +1 -1
  29. package/dist/core/mcpManager.d.ts +1 -0
  30. package/dist/core/mcpManager.js +100 -10
  31. package/dist/core/modelValidationStore.d.ts +4 -1
  32. package/dist/core/modelValidationStore.js +7 -1
  33. package/dist/core/subagent.d.ts +6 -0
  34. package/dist/core/subagent.js +22 -1
  35. package/dist/core/toolPolicy.d.ts +6 -0
  36. package/dist/core/toolPolicy.js +49 -1
  37. package/dist/core/types.d.ts +1 -1
  38. package/dist/core/utilityAgentProtocol.d.ts +8 -1
  39. package/dist/core/workspace.d.ts +15 -0
  40. package/dist/core/workspace.js +62 -1
  41. package/dist/core/wslAgentClient.d.ts +4 -0
  42. package/dist/core/wslAgentClient.js +4 -0
  43. package/dist/core/wslAgentProtocol.d.ts +8 -1
  44. package/dist/core/wslAgentRuntimePool.d.ts +12 -0
  45. package/dist/core/wslAgentRuntimePool.js +71 -0
  46. package/dist/launcher.js +48 -11
  47. package/dist/llm/provider.d.ts +9 -6
  48. package/dist/llm/provider.js +89 -36
  49. package/dist/main.js +326 -52
  50. package/dist/preload.js +17 -0
  51. package/dist/providers/chat-completions.adapter.js +42 -20
  52. package/dist/providers/provider-adapter.d.ts +3 -0
  53. package/dist/providers/provider-events.d.ts +7 -0
  54. package/dist/providers/provider-events.js +44 -0
  55. package/dist/providers/responses.adapter.js +1 -3
  56. package/dist/toolchain/registry/tool-registry.d.ts +13 -1
  57. package/dist/toolchain/registry/tool-registry.js +8 -0
  58. package/dist/toolchain/registry-seeder.js +51 -5
  59. package/dist/tools/index.js +11 -2
  60. package/dist/tools/nativeTools.js +5 -1
  61. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  62. package/dist/tui/src/app.js +47 -13
  63. package/dist/tui/src/render.js +23 -7
  64. package/dist/tui/src/state.js +61 -9
  65. package/dist/ui/index.html +2775 -284
  66. package/dist/ui/lucide-sprite.svg +26 -0
  67. package/dist/wsl-agent-host.bundle.cjs +1503 -214
  68. package/dist/wsl-agent-host.js +3 -0
  69. package/package.json +16 -5
package/dist/main.js CHANGED
@@ -51,6 +51,7 @@ const electronBrowserUseHost_1 = require("./core/electronBrowserUseHost");
51
51
  const flow_1 = require("./core/flow");
52
52
  const flow_runner_1 = require("./core/flow-runner");
53
53
  const cli_commands_1 = require("./cli-commands");
54
+ const cli_discovery_1 = require("./cli-discovery");
54
55
  const config_1 = require("./core/config");
55
56
  const memoryLab_1 = require("./core/memoryLab");
56
57
  const installUpdate_1 = require("./core/installUpdate");
@@ -72,6 +73,7 @@ const startupPrewarm_1 = require("./core/startupPrewarm");
72
73
  const runtimeShutdown_1 = require("./core/runtimeShutdown");
73
74
  const runtimeLifecycle_1 = require("./core/runtimeLifecycle");
74
75
  const compat_1 = require("./core/compat");
76
+ const dshCompatibility_1 = require("./core/dshCompatibility");
75
77
  const mcpManager_1 = require("./core/mcpManager");
76
78
  const cli_help_1 = require("./cli-help");
77
79
  const APP_NAME = 'Newmark Agent';
@@ -122,6 +124,7 @@ let browserUseEngine = null;
122
124
  // the authoritative Browser-Use/right-sidebar binding.
123
125
  const browserGuestContentsByHost = new Map();
124
126
  const browserGuestBindingsByRuntime = new Map();
127
+ const browserGuestKeyboardBridgeIds = new Set();
125
128
  function browserGuestRuntimeKey(target) {
126
129
  return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
127
130
  }
@@ -426,7 +429,11 @@ function createAppIconImage(size) {
426
429
  return size ? icon.resize({ width: size, height: size }) : icon;
427
430
  }
428
431
  function userArgs() {
429
- return process.argv.slice(1);
432
+ const args = process.argv.slice(1);
433
+ // The native Windows console wrapper inserts Electron's `--` boundary before
434
+ // user arguments. Electron leaves that boundary in process.argv; normalize it
435
+ // before command discovery so the literal `Newmark.exe help` path terminates.
436
+ return args[0] === '--' ? args.slice(1) : args;
430
437
  }
431
438
  function argValue(args, key) {
432
439
  const idx = args.indexOf(key);
@@ -486,14 +493,14 @@ function positionalAfter(args, commandName) {
486
493
  return values;
487
494
  }
488
495
  // First-run initialization
489
- function firstRunInit(root) {
496
+ function firstRunInit(root, options = {}) {
490
497
  fs.mkdirSync(root, { recursive: true });
491
498
  for (const d of ['skills', 'Work', 'Flow', 'archive', 'Memory Lab']) {
492
499
  fs.mkdirSync(path.join(root, d), { recursive: true });
493
500
  }
494
501
  new memoryLab_1.MemoryLabManager(root);
495
502
  const configModule = require('./core/config');
496
- configModule.ensureRootConfig(root);
503
+ configModule.ensureRootConfig(root, options);
497
504
  if (!fs.existsSync(path.join(root, 'agent.md'))) {
498
505
  fs.writeFileSync(path.join(root, 'agent.md'), '# Newmark Agent\n\nYou are a powerful coding assistant.\n', 'utf-8');
499
506
  }
@@ -583,18 +590,13 @@ function ensureElectronUtilityRuntimeHost() {
583
590
  return host;
584
591
  }
585
592
  function legacyUserDataRoot() {
586
- try {
587
- return electron_1.app.getPath('userData');
588
- }
589
- catch {
590
- if (process.platform === 'win32') {
591
- const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
592
- return path.join(appData, 'Newmark Agent');
593
- }
594
- if (process.platform === 'darwin')
595
- return path.join(os.homedir(), 'Library', 'Application Support', 'Newmark Agent');
596
- return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Newmark Agent');
593
+ if (process.platform === 'win32') {
594
+ const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
595
+ return path.join(appData, 'Newmark Agent');
597
596
  }
597
+ if (process.platform === 'darwin')
598
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Newmark Agent');
599
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Newmark Agent');
598
600
  }
599
601
  function migrateLegacyRuntimeRoot(root) {
600
602
  const targetRoot = path.resolve(root);
@@ -677,9 +679,22 @@ function resolveRoot(args) {
677
679
  return writableRuntimeRoot(explicitRoot);
678
680
  return getRoot();
679
681
  }
682
+ function resolveTuiWorkspacePath(args, root) {
683
+ const explicitWorkspace = pathArgValue(args, '--workspace');
684
+ if (explicitWorkspace)
685
+ return explicitWorkspace;
686
+ // An explicitly isolated runtime must not silently register the caller's
687
+ // cwd as an external workspace. Keep the opt-in --workspace escape hatch,
688
+ // while making the safe one-argument form fully self-contained.
689
+ return pathArgValue(args, '--root') ? root : process.cwd();
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 = '';
680
695
  function startupLogPath() {
681
696
  try {
682
- const userData = userRuntimeRoot();
697
+ const userData = startupRuntimeRoot || userRuntimeRoot();
683
698
  fs.mkdirSync(userData, { recursive: true });
684
699
  return path.join(userData, 'startup.log');
685
700
  }
@@ -832,7 +847,29 @@ function registerBrowserGuest(host, guest, requestedTarget) {
832
847
  conversationId: target.conversationId,
833
848
  });
834
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
+ }
835
871
  guest.once('destroyed', () => {
872
+ browserGuestKeyboardBridgeIds.delete(guest.id);
836
873
  if (browserGuestContentsByHost.get(host.id) === guest.id)
837
874
  browserGuestContentsByHost.delete(host.id);
838
875
  const binding = browserGuestBindingsByRuntime.get(target.runtimeKey);
@@ -1026,24 +1063,71 @@ const args = userArgs();
1026
1063
  const command = args.find(a => a === 'flow' || a === 'edit');
1027
1064
  const isTuiArg = args.some(arg => arg.toLowerCase() === '--tui');
1028
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));
1067
+ const isFlowArg = command === 'flow';
1068
+ const isEditArg = command === 'edit';
1029
1069
  const isHelpArg = !hasCliCommand && (args.some(arg => ['--help', '-h'].includes(arg.toLowerCase())) || args[0]?.toLowerCase() === 'help');
1030
- const isVersionArg = !hasCliCommand && args.some(arg => ['--version', '-v'].includes(arg.toLowerCase()));
1070
+ const isVersionArg = !hasCliCommand && (0, cli_discovery_1.isVersionArgument)(args);
1071
+ const isReadOnlyValidation = hasCliCommand && args.includes('validate-models') && !args.includes('--persist');
1031
1072
  const isViewerArg = args.some(arg => arg.toLowerCase() === '--newmark-viewer');
1032
1073
  const isCliArg = args.includes('--cli');
1033
1074
  const isServerArg = args.includes('--server');
1034
- const isFlowArg = command === 'flow';
1035
- const isEditArg = command === 'edit';
1075
+ const invalidArgument = (0, cli_discovery_1.invalidTopLevelArgument)(args);
1076
+ if (invalidArgument) {
1077
+ console.error(`Invalid Newmark argument: ${invalidArgument}`);
1078
+ process.exit(2);
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
+ }
1088
+ // Electron's Chromium profile is a separate state boundary from Newmark's
1089
+ // business root. Bind both before any ready event so --root cannot leave
1090
+ // Preferences, DIPS, DevTools ports, cookies, or session storage in the real
1091
+ // default AppData directory. The dedicated subdirectories keep Chromium's
1092
+ // files separate from the durable Newmark config/workspace files.
1093
+ const runtimeRoot = resolveRoot(args);
1094
+ startupRuntimeRoot = runtimeRoot;
1095
+ const explicitElectronUserDataRoot = pathArgValue(args, '--user-data-dir');
1096
+ const electronUserDataRoot = path.resolve(explicitElectronUserDataRoot || path.join(runtimeRoot, 'Electron'));
1097
+ const electronSessionDataRoot = path.join(electronUserDataRoot, 'session-data');
1098
+ try {
1099
+ fs.mkdirSync(electronSessionDataRoot, { recursive: true });
1100
+ electron_1.app.setPath('userData', electronUserDataRoot);
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);
1108
+ }
1109
+ catch (error) {
1110
+ console.error(`Unable to isolate Electron user-data directory: ${error instanceof Error ? error.message : String(error)}`);
1111
+ process.exit(1);
1112
+ }
1036
1113
  // Help/version are terminating discovery commands. They must be handled
1037
1114
  // before Electron's GUI/TUI/server branches can initialize runtime state or
1038
1115
  // spawn a window, so a product-new tester can use them safely in any surface.
1039
1116
  if (isHelpArg) {
1040
- console.log((0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
1117
+ console.log(isFlowArg ? (0, cli_help_1.newmarkFlowHelpText)() : isEditArg ? (0, cli_help_1.newmarkEditHelpText)() : (0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
1041
1118
  process.exit(0);
1042
1119
  }
1043
1120
  if (isVersionArg) {
1044
1121
  console.log((0, installUpdate_1.currentAppVersion)());
1045
1122
  process.exit(0);
1046
1123
  }
1124
+ const unknownCommand = electron_1.app.isPackaged && !hasCliCommand && !isTuiArg && !isCliArg && !isServerArg && !isViewerArg
1125
+ ? (0, cli_discovery_1.unknownTopLevelCommand)(args)
1126
+ : undefined;
1127
+ if (unknownCommand) {
1128
+ console.error(`Unknown Newmark command or argument: ${unknownCommand}. Run --help to see the supported entrypoints.`);
1129
+ process.exit(2);
1130
+ }
1047
1131
  function viewerEscape(value) {
1048
1132
  return String(value || '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] || character);
1049
1133
  }
@@ -1118,7 +1202,9 @@ if (isViewerArg) {
1118
1202
  }).catch(error => { console.error(`Unable to open Newmark viewer: ${error instanceof Error ? error.message : String(error)}`); electron_1.app.quit(); });
1119
1203
  }
1120
1204
  else if (isTuiArg) {
1121
- const isConsoleLauncher = electron_1.app.isPackaged && path.basename(process.execPath).toLowerCase() === 'newmark.exe';
1205
+ const isConsoleLauncher = electron_1.app.isPackaged && (path.basename(process.execPath).toLowerCase() === 'newmark.exe'
1206
+ || path.basename(process.execPath).toLowerCase() === 'newmark console runtime.exe'
1207
+ || process.env.NEWMARK_CONSOLE_WRAPPER === '1');
1122
1208
  if (isConsoleLauncher && process.env.NEWMARK_TUI_SIDECAR !== '1') {
1123
1209
  const tuiProcess = (0, child_process_1.spawnSync)(process.execPath, [path.join(__dirname, 'launcher.js'), ...args], {
1124
1210
  cwd: process.cwd(),
@@ -1126,6 +1212,11 @@ else if (isTuiArg) {
1126
1212
  ...process.env,
1127
1213
  ELECTRON_RUN_AS_NODE: '1',
1128
1214
  NEWMARK_TUI_SIDECAR: '1',
1215
+ // The GUI Electron host does not expose the inherited ConPTY handles
1216
+ // as Node TTY streams. The console wrapper has already established
1217
+ // that this is the terminal entrypoint, so preserve the terminal
1218
+ // contract for the sidecar without weakening ordinary GUI launches.
1219
+ NEWMARK_FORCE_TTY: isConsoleLauncher ? '1' : process.env.NEWMARK_FORCE_TTY,
1129
1220
  },
1130
1221
  stdio: 'inherit',
1131
1222
  windowsHide: false,
@@ -1146,12 +1237,12 @@ else if (isTuiArg) {
1146
1237
  const root = resolveRoot(args);
1147
1238
  firstRunInit(root);
1148
1239
  const { start } = require('./tui/src/app');
1149
- start({ root, workspacePath: process.cwd(), desktopDist: __dirname });
1240
+ start({ root, workspacePath: resolveTuiWorkspacePath(args, root), desktopDist: __dirname });
1150
1241
  }
1151
1242
  else if (hasCliCommand) {
1152
1243
  (async () => {
1153
1244
  const root = resolveRoot(args);
1154
- firstRunInit(root);
1245
+ firstRunInit(root, { readOnly: isReadOnlyValidation });
1155
1246
  const handled = await (0, cli_commands_1.runCliCommand)(root, args);
1156
1247
  const code = typeof process.exitCode === 'number' ? process.exitCode : 0;
1157
1248
  exitCli(handled ? code : 1);
@@ -1312,6 +1403,51 @@ else {
1312
1403
  });
1313
1404
  electron_1.app.whenReady().then(async () => {
1314
1405
  let root = resolveRoot(args);
1406
+ let workspaceRegistryWatcher = null;
1407
+ let workspaceRegistryWatchTimer = null;
1408
+ const workspaceRegistryFiles = new Set(['Local.json', 'External.json', 'State.json']);
1409
+ const broadcastWorkspaceChanged = (files) => {
1410
+ for (const win of electron_1.BrowserWindow.getAllWindows()) {
1411
+ if (win.isDestroyed())
1412
+ continue;
1413
+ win.webContents.send('workspace:changed', { files, root });
1414
+ }
1415
+ };
1416
+ const refreshWorkspaceRegistryFromDisk = (files) => {
1417
+ if (!agent)
1418
+ return;
1419
+ agent.refreshWorkspaceRegistryFromStorage();
1420
+ const current = agent.workspace.current;
1421
+ if (current)
1422
+ workspaceSelectionCoordinator?.setCurrent(current.id || current.path || current.name);
1423
+ broadcastWorkspaceChanged(files);
1424
+ };
1425
+ const ensureWorkspaceRegistryWatcher = () => {
1426
+ if (workspaceRegistryWatcher)
1427
+ return;
1428
+ const workDir = path.join(root, 'Work');
1429
+ try {
1430
+ workspaceRegistryWatcher = fs.watch(workDir, { persistent: false }, (_eventType, filename) => {
1431
+ const changed = String(filename || '');
1432
+ if (changed && !workspaceRegistryFiles.has(path.basename(changed)))
1433
+ return;
1434
+ if (workspaceRegistryWatchTimer)
1435
+ clearTimeout(workspaceRegistryWatchTimer);
1436
+ workspaceRegistryWatchTimer = setTimeout(() => {
1437
+ workspaceRegistryWatchTimer = null;
1438
+ try {
1439
+ refreshWorkspaceRegistryFromDisk(changed ? [path.basename(changed)] : ['Work']);
1440
+ }
1441
+ catch (error) {
1442
+ logStartupFailure('workspace-registry-refresh', error);
1443
+ }
1444
+ }, 90);
1445
+ });
1446
+ }
1447
+ catch (error) {
1448
+ logStartupFailure('workspace-registry-watch', error);
1449
+ }
1450
+ };
1315
1451
  const fileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
1316
1452
  const pdfPreviewServer = new pdfPreviewServer_1.PdfPreviewServer((token, ownerId) => fileRouter.resolvePdfCapability(token, ownerId));
1317
1453
  await pdfPreviewServer.start();
@@ -1568,6 +1704,12 @@ else {
1568
1704
  win.focus();
1569
1705
  }, 150);
1570
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
+ });
1571
1713
  if (!automationWakeMode)
1572
1714
  win.maximize();
1573
1715
  if (!automationWakeMode && showWindow) {
@@ -1583,15 +1725,18 @@ else {
1583
1725
  return;
1584
1726
  if (agent) {
1585
1727
  const closeBehavior = agent.config.getStr('general', 'close_behavior');
1728
+ recordStartup(`window-close-requested-${closeBehavior || 'exit'}`);
1586
1729
  if (closeBehavior === 'minimize') {
1587
1730
  e.preventDefault();
1588
1731
  win.hide();
1589
1732
  createTray();
1733
+ recordStartup('window-hidden-to-tray');
1590
1734
  return;
1591
1735
  }
1592
1736
  if (agent.config.getBool('general', 'auto_archive_on_close')) {
1593
1737
  agent.archiveSession();
1594
1738
  }
1739
+ recordStartup('window-close-exit');
1595
1740
  }
1596
1741
  });
1597
1742
  win.on('closed', () => {
@@ -1718,6 +1863,7 @@ else {
1718
1863
  agent = new agent_1.Agent(root);
1719
1864
  mcpManager = new mcpManager_1.McpManager(root);
1720
1865
  activeAgentBackendMode = process.platform === 'win32' && agent.config.getBool('agent', 'run_in_wsl') ? 'wsl' : 'windows';
1866
+ ensureWorkspaceRegistryWatcher();
1721
1867
  restoreStoredFlowSuspension();
1722
1868
  recordStartup('agent-ready');
1723
1869
  }
@@ -2207,8 +2353,18 @@ else {
2207
2353
  (0, runtimeLifecycle_1.markRuntimeLifecycleClean)(root, 'main');
2208
2354
  };
2209
2355
  electron_1.app.on('will-quit', event => {
2210
- if (_forceQuit)
2356
+ recordStartup('will-quit');
2357
+ // Window-close and tray-exit both enter this path. The runtime pools
2358
+ // must get a bounded graceful-shutdown window regardless of which
2359
+ // surface initiated the close; otherwise a stuck child can keep the
2360
+ // Electron parent and its renderer tree alive indefinitely.
2361
+ if (_forceQuit || !appExitCleanupComplete)
2211
2362
  armForcedExitDeadline('will-quit');
2363
+ if (workspaceRegistryWatchTimer)
2364
+ clearTimeout(workspaceRegistryWatchTimer);
2365
+ workspaceRegistryWatchTimer = null;
2366
+ workspaceRegistryWatcher?.close();
2367
+ workspaceRegistryWatcher = null;
2212
2368
  startupDeferredTasks?.cancel();
2213
2369
  agent?.flushWorkspaceConversationState();
2214
2370
  conversationKernel?.flushPersistence();
@@ -2361,6 +2517,32 @@ else {
2361
2517
  activeFlowsByRuntimeKey.delete(key);
2362
2518
  agent.clearStoredFlowSuspension(state.target.conversationId);
2363
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
+ };
2364
2546
  const utilityHostToolHandler = (0, utilityHostToolRouter_1.createUtilityHostToolHandler)({
2365
2547
  persistenceRoot: root,
2366
2548
  isToolEnabled: toolName => !!agent && (0, nativeTools_1.isNativeToolEnabled)(toolName, agent.config.nativeToolEnabled()),
@@ -2539,6 +2721,13 @@ else {
2539
2721
  else
2540
2722
  await electronUtilityRuntimePool?.stopTarget(target);
2541
2723
  };
2724
+ const forceStopTargetRuntime = async (target) => {
2725
+ if (wslBackendEnabled())
2726
+ await wslAgentRuntimePool?.forceStopTarget(target);
2727
+ else
2728
+ await electronUtilityRuntimePool?.forceStopTarget(target);
2729
+ };
2730
+ const archiveInFlight = new Map();
2542
2731
  const isolatedConversationAgent = (target) => {
2543
2732
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
2544
2733
  const isolated = new agent_1.Agent(root, { agentOnly: true });
@@ -2743,6 +2932,14 @@ else {
2743
2932
  };
2744
2933
  }
2745
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
+ }
2746
2943
  if (e instanceof flow_runner_1.FlowQuestionPendingError) {
2747
2944
  suspended = true;
2748
2945
  flowAgent.flowPc = e.componentId;
@@ -2836,6 +3033,11 @@ else {
2836
3033
  const flowKey = activeFlowStateKey(flowTarget);
2837
3034
  const flowAgent = isolatedConversationAgent(flowTarget);
2838
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');
2839
3041
  const flowAbortController = new AbortController();
2840
3042
  suspension.abortController = flowAbortController;
2841
3043
  activeFlowsByRuntimeKey.set(flowKey, suspension);
@@ -2870,6 +3072,11 @@ else {
2870
3072
  };
2871
3073
  }
2872
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
+ }
2873
3080
  if (error instanceof flow_runner_1.FlowQuestionPendingError) {
2874
3081
  suspendedAgain = true;
2875
3082
  flowAgent.flowPc = error.componentId;
@@ -2891,9 +3098,9 @@ else {
2891
3098
  workRuns: flowAgent.getConversationSnapshot(flowAgent.activeConversationId).workRuns,
2892
3099
  };
2893
3100
  }
2894
- if (isUserFlowAbort(error)) {
2895
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
2896
- }
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.
2897
3104
  suspendedAgain = true;
2898
3105
  const resumeFailure = error;
2899
3106
  const interruptedComponentId = typeof resumeFailure.componentId === 'number'
@@ -3046,6 +3253,12 @@ else {
3046
3253
  runtimeDeferred: false,
3047
3254
  };
3048
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
+ });
3049
3262
  electron_1.ipcMain.handle('agent:computerUseState', async (_event, targetInput) => {
3050
3263
  if (!agent)
3051
3264
  return { enabled: false, occupied: false, runtimeKey: '' };
@@ -3583,6 +3796,18 @@ else {
3583
3796
  ? await ensureWslConversationPool().checkpoint(target)
3584
3797
  : await ensureElectronUtilityPool().checkpoint(target);
3585
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
+ });
3586
3811
  electron_1.ipcMain.handle('agent:rateAutoRoute', async (_event, request) => {
3587
3812
  if (!agent)
3588
3813
  return { ok: false, reason: 'no_active_auto_route' };
@@ -3732,32 +3957,52 @@ else {
3732
3957
  return { ok: false, error: 'Agent not initialized' };
3733
3958
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3734
3959
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
3735
- assertTargetNotMutating(normalized);
3736
- mutatingRuntimeKeys.add(normalized.runtimeKey);
3960
+ const existing = archiveInFlight.get(normalized.runtimeKey);
3961
+ if (existing)
3962
+ return existing;
3963
+ const operation = (async () => {
3964
+ mutatingRuntimeKeys.add(normalized.runtimeKey);
3965
+ try {
3966
+ // Archive is a destructive lifecycle command. It intentionally
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
+ });
3975
+ const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
3976
+ const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
3977
+ const ownsTargetWorkspace = !!normalized.workspace
3978
+ && !!agent.workspace.current
3979
+ && currentWorkspacePath === targetWorkspacePath;
3980
+ // The host Agent owns the current workspace persistence cache. An
3981
+ // isolated owner is used for another workspace. The archive writer
3982
+ // starts payload I/O in parallel and finalizes deletion against the
3983
+ // latest locked state snapshot, so rapid clicks do not serialize on
3984
+ // large Markdown bodies or lose a sibling deletion.
3985
+ const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
3986
+ archiveOwner.clearStoredFlowSuspension(normalized.conversationId);
3987
+ const archived = await archiveOwner.archiveConversationAsync(normalized.conversationId);
3988
+ if (!archived)
3989
+ return { ok: false, error: 'Conversation archive could not be written.' };
3990
+ return { ok: true, fileName: archived, conversationId: normalized.conversationId, workspaceId: normalized.workspaceId };
3991
+ }
3992
+ catch (error) {
3993
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
3994
+ }
3995
+ finally {
3996
+ mutatingRuntimeKeys.delete(normalized.runtimeKey);
3997
+ }
3998
+ })();
3999
+ archiveInFlight.set(normalized.runtimeKey, operation);
3737
4000
  try {
3738
- const peek = peekTargetRuntime(normalized);
3739
- if (peek.running || peek.stopping)
3740
- return { ok: false, error: 'Cannot archive a conversation while its runtime is running or stopping.' };
3741
- if (peek.resident)
3742
- await stopTargetRuntime(normalized);
3743
- const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
3744
- const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
3745
- const ownsTargetWorkspace = !!normalized.workspace
3746
- && !!agent.workspace.current
3747
- && currentWorkspacePath === targetWorkspacePath;
3748
- // The host Agent owns the current workspace persistence cache. Archiving
3749
- // through it prevents a delayed host flush from resurrecting the target.
3750
- const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
3751
- const archived = archiveOwner.archiveConversation(normalized.conversationId);
3752
- if (!archived)
3753
- return { ok: false, error: 'Conversation archive could not be written.' };
3754
- return { ok: true, fileName: archived, conversationId: normalized.conversationId, workspaceId: normalized.workspaceId };
3755
- }
3756
- catch (error) {
3757
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
4001
+ return await operation;
3758
4002
  }
3759
4003
  finally {
3760
- mutatingRuntimeKeys.delete(normalized.runtimeKey);
4004
+ if (archiveInFlight.get(normalized.runtimeKey) === operation)
4005
+ archiveInFlight.delete(normalized.runtimeKey);
3761
4006
  }
3762
4007
  });
3763
4008
  electron_1.ipcMain.handle('agent:listArchives', async (_event, scope) => {
@@ -4083,6 +4328,20 @@ else {
4083
4328
  }
4084
4329
  return { error: 'Agent not initialized' };
4085
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
+ });
4086
4345
  electron_1.ipcMain.handle('agent:selectWorkspace', async (_event, id) => {
4087
4346
  if (agent) {
4088
4347
  const requested = String(id || '').trim();
@@ -4380,13 +4639,25 @@ else {
4380
4639
  const controller = new AbortController();
4381
4640
  editorCompletionControllers.set(ownerId, controller);
4382
4641
  try {
4383
- 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' });
4384
4650
  }
4385
4651
  finally {
4386
4652
  if (editorCompletionControllers.get(ownerId) === controller)
4387
4653
  editorCompletionControllers.delete(ownerId);
4388
4654
  }
4389
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
+ });
4390
4661
  electron_1.ipcMain.handle('agent:editorAssist', async (_event, request) => {
4391
4662
  return agent?.editorModelRequest({ ...request, completion: false }) || { ok: false, text: '', error: 'Agent not initialized' };
4392
4663
  });
@@ -4483,6 +4754,7 @@ else {
4483
4754
  })));
4484
4755
  return { servers: mcpManager.list(), discovered };
4485
4756
  });
4757
+ electron_1.ipcMain.handle('dsh:discover', async () => (0, dshCompatibility_1.discoverDshCompatibility)(root));
4486
4758
  electron_1.ipcMain.handle('mcp:upsert', async (_event, input) => {
4487
4759
  if (!mcpManager)
4488
4760
  return { ok: false, error: 'MCP manager is unavailable.' };
@@ -4497,12 +4769,14 @@ else {
4497
4769
  electron_1.ipcMain.handle('mcp:setEnabled', async (_event, id, enabled) => {
4498
4770
  if (!mcpManager)
4499
4771
  return { ok: false, error: 'MCP manager is unavailable.' };
4500
- 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() };
4501
4774
  });
4502
4775
  electron_1.ipcMain.handle('mcp:remove', async (_event, id) => {
4503
4776
  if (!mcpManager)
4504
4777
  return { ok: false, error: 'MCP manager is unavailable.' };
4505
- 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() };
4506
4780
  });
4507
4781
  electron_1.ipcMain.handle('memoryLab:read', async (_event, selector) => {
4508
4782
  if (!agent)