newmark-agent 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/config.example.json +6 -0
  2. package/dist/cli-commands.d.ts +7 -0
  3. package/dist/cli-commands.js +206 -15
  4. package/dist/cli-discovery.d.ts +15 -0
  5. package/dist/cli-discovery.js +182 -0
  6. package/dist/cli-help.d.ts +4 -0
  7. package/dist/cli-help.js +46 -0
  8. package/dist/conversation-utility-host.bundle.cjs +548 -137
  9. package/dist/core/agent.d.ts +18 -3
  10. package/dist/core/agent.js +236 -34
  11. package/dist/core/agentKernelRunner.js +72 -10
  12. package/dist/core/browserControl.d.ts +8 -0
  13. package/dist/core/browserUsePageAdapter.d.ts +3 -0
  14. package/dist/core/browserUsePageAdapter.js +19 -2
  15. package/dist/core/computerUseSession.d.ts +44 -0
  16. package/dist/core/computerUseSession.js +105 -0
  17. package/dist/core/config.d.ts +7 -2
  18. package/dist/core/config.js +24 -6
  19. package/dist/core/conversationKernel.d.ts +1 -0
  20. package/dist/core/conversationKernel.js +39 -1
  21. package/dist/core/electronBrowserUseHost.js +7 -0
  22. package/dist/core/electronUtilityAgentClient.js +84 -2
  23. package/dist/core/electronUtilityRuntimePool.d.ts +11 -0
  24. package/dist/core/electronUtilityRuntimePool.js +62 -0
  25. package/dist/core/flow-runner.js +1 -1
  26. package/dist/core/modelValidationStore.d.ts +4 -1
  27. package/dist/core/modelValidationStore.js +7 -1
  28. package/dist/core/utilityHostToolRouter.d.ts +7 -0
  29. package/dist/core/utilityHostToolRouter.js +25 -33
  30. package/dist/core/workspace.d.ts +6 -0
  31. package/dist/core/workspace.js +14 -0
  32. package/dist/core/wslAgentRuntimePool.d.ts +4 -0
  33. package/dist/core/wslAgentRuntimePool.js +56 -0
  34. package/dist/launcher.js +51 -10
  35. package/dist/llm/provider.d.ts +8 -5
  36. package/dist/llm/provider.js +85 -33
  37. package/dist/main.js +297 -61
  38. package/dist/preload.js +10 -2
  39. package/dist/providers/chat-completions.adapter.js +1 -5
  40. package/dist/providers/provider-events.d.ts +7 -0
  41. package/dist/providers/provider-events.js +44 -0
  42. package/dist/providers/responses.adapter.js +1 -3
  43. package/dist/tools/index.js +36 -49
  44. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  45. package/dist/tui/src/app.js +47 -13
  46. package/dist/tui/src/render.js +23 -7
  47. package/dist/tui/src/state.js +61 -9
  48. package/dist/ui/index.html +545 -122
  49. package/dist/wsl-agent-host.bundle.cjs +548 -137
  50. package/package.json +14 -5
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");
@@ -73,6 +74,7 @@ const runtimeShutdown_1 = require("./core/runtimeShutdown");
73
74
  const runtimeLifecycle_1 = require("./core/runtimeLifecycle");
74
75
  const compat_1 = require("./core/compat");
75
76
  const mcpManager_1 = require("./core/mcpManager");
77
+ const cli_help_1 = require("./cli-help");
76
78
  const APP_NAME = 'Newmark Agent';
77
79
  const APP_ID = 'ai.newmark.agent';
78
80
  const CONFIG_RELOAD_RUNTIME_TIMEOUT_MS = 6_000;
@@ -116,7 +118,27 @@ let _forceQuit = false;
116
118
  let forcedExitTimer = null;
117
119
  let electronBrowserUseHost = null;
118
120
  let browserUseEngine = null;
121
+ // A single renderer can host several hidden conversation-bound guests. The
122
+ // legacy host map is retained for focused-window discovery, while this map is
123
+ // the authoritative Browser-Use/right-sidebar binding.
119
124
  const browserGuestContentsByHost = new Map();
125
+ const browserGuestBindingsByRuntime = new Map();
126
+ function browserGuestRuntimeKey(target) {
127
+ return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
128
+ }
129
+ function browserGuestBindingFor(runtimeKey) {
130
+ const binding = browserGuestBindingsByRuntime.get(String(runtimeKey || ''));
131
+ if (!binding)
132
+ return null;
133
+ const guest = electron_1.webContents.fromId(binding.guestId);
134
+ if (!guest || guest.isDestroyed() || guest.getType() !== 'webview' || guest.hostWebContents?.id !== binding.hostId) {
135
+ browserGuestBindingsByRuntime.delete(String(runtimeKey || ''));
136
+ if (browserGuestContentsByHost.get(binding.hostId) === binding.guestId)
137
+ browserGuestContentsByHost.delete(binding.hostId);
138
+ return null;
139
+ }
140
+ return binding;
141
+ }
120
142
  let workspaceSwitchGeneration = 0;
121
143
  let workspaceSelectionCoordinator = null;
122
144
  function defaultTerminalShell() {
@@ -405,7 +427,11 @@ function createAppIconImage(size) {
405
427
  return size ? icon.resize({ width: size, height: size }) : icon;
406
428
  }
407
429
  function userArgs() {
408
- return process.argv.slice(1);
430
+ const args = process.argv.slice(1);
431
+ // The native Windows console wrapper inserts Electron's `--` boundary before
432
+ // user arguments. Electron leaves that boundary in process.argv; normalize it
433
+ // before command discovery so the literal `Newmark.exe help` path terminates.
434
+ return args[0] === '--' ? args.slice(1) : args;
409
435
  }
410
436
  function argValue(args, key) {
411
437
  const idx = args.indexOf(key);
@@ -465,14 +491,14 @@ function positionalAfter(args, commandName) {
465
491
  return values;
466
492
  }
467
493
  // First-run initialization
468
- function firstRunInit(root) {
494
+ function firstRunInit(root, options = {}) {
469
495
  fs.mkdirSync(root, { recursive: true });
470
496
  for (const d of ['skills', 'Work', 'Flow', 'archive', 'Memory Lab']) {
471
497
  fs.mkdirSync(path.join(root, d), { recursive: true });
472
498
  }
473
499
  new memoryLab_1.MemoryLabManager(root);
474
500
  const configModule = require('./core/config');
475
- configModule.ensureRootConfig(root);
501
+ configModule.ensureRootConfig(root, options);
476
502
  if (!fs.existsSync(path.join(root, 'agent.md'))) {
477
503
  fs.writeFileSync(path.join(root, 'agent.md'), '# Newmark Agent\n\nYou are a powerful coding assistant.\n', 'utf-8');
478
504
  }
@@ -562,18 +588,13 @@ function ensureElectronUtilityRuntimeHost() {
562
588
  return host;
563
589
  }
564
590
  function legacyUserDataRoot() {
565
- try {
566
- return electron_1.app.getPath('userData');
567
- }
568
- catch {
569
- if (process.platform === 'win32') {
570
- const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
571
- return path.join(appData, 'Newmark Agent');
572
- }
573
- if (process.platform === 'darwin')
574
- return path.join(os.homedir(), 'Library', 'Application Support', 'Newmark Agent');
575
- return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Newmark Agent');
591
+ if (process.platform === 'win32') {
592
+ const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
593
+ return path.join(appData, 'Newmark Agent');
576
594
  }
595
+ if (process.platform === 'darwin')
596
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Newmark Agent');
597
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Newmark Agent');
577
598
  }
578
599
  function migrateLegacyRuntimeRoot(root) {
579
600
  const targetRoot = path.resolve(root);
@@ -656,6 +677,15 @@ function resolveRoot(args) {
656
677
  return writableRuntimeRoot(explicitRoot);
657
678
  return getRoot();
658
679
  }
680
+ function resolveTuiWorkspacePath(args, root) {
681
+ const explicitWorkspace = pathArgValue(args, '--workspace');
682
+ if (explicitWorkspace)
683
+ return explicitWorkspace;
684
+ // An explicitly isolated runtime must not silently register the caller's
685
+ // cwd as an external workspace. Keep the opt-in --workspace escape hatch,
686
+ // while making the safe one-argument form fully self-contained.
687
+ return pathArgValue(args, '--root') ? root : process.cwd();
688
+ }
659
689
  function startupLogPath() {
660
690
  try {
661
691
  const userData = userRuntimeRoot();
@@ -737,7 +767,15 @@ async function waitForWebContentsLoad(contents, timeoutMs = 15000) {
737
767
  contents.once('did-fail-load', finish);
738
768
  });
739
769
  }
740
- function registeredBrowserGuest(hostContentsId) {
770
+ function registeredBrowserGuest(hostContentsId, runtimeKey) {
771
+ if (runtimeKey) {
772
+ const binding = browserGuestBindingFor(runtimeKey);
773
+ if (binding && (!hostContentsId || binding.hostId === hostContentsId)) {
774
+ const guest = electron_1.webContents.fromId(binding.guestId);
775
+ if (guest && !guest.isDestroyed())
776
+ return guest;
777
+ }
778
+ }
741
779
  const hostIds = hostContentsId
742
780
  ? [hostContentsId]
743
781
  : [
@@ -752,8 +790,14 @@ function registeredBrowserGuest(hostContentsId) {
752
790
  if (!guestId)
753
791
  continue;
754
792
  const guest = electron_1.webContents.fromId(guestId);
755
- if (guest && !guest.isDestroyed() && guest.getType() === 'webview' && guest.hostWebContents?.id === hostId)
793
+ if (guest && !guest.isDestroyed() && guest.getType() === 'webview' && guest.hostWebContents?.id === hostId) {
794
+ if (runtimeKey) {
795
+ const bound = browserGuestBindingsByRuntime.get(runtimeKey);
796
+ if (!bound || bound.hostId !== hostId || bound.guestId !== guest.id)
797
+ continue;
798
+ }
756
799
  return guest;
800
+ }
757
801
  browserGuestContentsByHost.delete(hostId);
758
802
  }
759
803
  return null;
@@ -774,37 +818,64 @@ async function boundedOperation(operation, timeoutMs, label) {
774
818
  clearTimeout(timer);
775
819
  }
776
820
  }
777
- function registerBrowserGuest(host, guest) {
821
+ function registerBrowserGuest(host, guest, requestedTarget) {
778
822
  if (host.isDestroyed() || guest.isDestroyed() || guest.getType() !== 'webview' || guest.hostWebContents?.id !== host.id)
779
823
  return false;
824
+ let target;
825
+ try {
826
+ target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(requestedTarget));
827
+ }
828
+ catch {
829
+ return false;
830
+ }
831
+ const prior = browserGuestBindingsByRuntime.get(target.runtimeKey);
832
+ if (prior && prior.guestId !== guest.id) {
833
+ const priorGuest = electron_1.webContents.fromId(prior.guestId);
834
+ if (priorGuest && !priorGuest.isDestroyed())
835
+ priorGuest.hostWebContents?.send('browser:guestVisibility', { runtimeKey: target.runtimeKey, visible: false });
836
+ }
837
+ browserGuestBindingsByRuntime.set(target.runtimeKey, {
838
+ hostId: host.id,
839
+ guestId: guest.id,
840
+ workspaceId: target.workspaceId,
841
+ conversationId: target.conversationId,
842
+ });
780
843
  browserGuestContentsByHost.set(host.id, guest.id);
781
844
  guest.once('destroyed', () => {
782
845
  if (browserGuestContentsByHost.get(host.id) === guest.id)
783
846
  browserGuestContentsByHost.delete(host.id);
847
+ const binding = browserGuestBindingsByRuntime.get(target.runtimeKey);
848
+ if (binding?.guestId === guest.id)
849
+ browserGuestBindingsByRuntime.delete(target.runtimeKey);
784
850
  });
785
851
  ensureElectronBrowserUseHost().attach(guest);
786
852
  return true;
787
853
  }
788
- async function waitForRegisteredBrowserGuest(host, timeoutMs = 12_000) {
854
+ async function waitForRegisteredBrowserGuest(host, runtimeKey, timeoutMs = 12_000) {
789
855
  const deadline = Date.now() + Math.max(250, timeoutMs);
790
856
  while (!host.isDestroyed() && Date.now() < deadline) {
791
- const guest = registeredBrowserGuest(host.id);
857
+ const guest = registeredBrowserGuest(host.id, runtimeKey);
792
858
  if (guest)
793
859
  return guest;
794
860
  await new Promise(resolve => setTimeout(resolve, 25));
795
861
  }
796
862
  throw new Error('Built-in Browser guest did not become ready before the Browser-Use timeout');
797
863
  }
798
- async function ensureBrowserWebContents(boundContentsId) {
864
+ async function ensureBrowserWebContents(boundContentsId, runtimeKey) {
799
865
  if (boundContentsId) {
800
866
  const bound = electron_1.webContents.fromId(boundContentsId);
801
867
  if (bound && !bound.isDestroyed() && bound.getType() === 'webview'
802
868
  && browserGuestContentsByHost.get(bound.hostWebContents?.id || 0) === bound.id) {
869
+ if (runtimeKey) {
870
+ const binding = browserGuestBindingsByRuntime.get(runtimeKey);
871
+ if (!binding || binding.guestId !== bound.id)
872
+ return await ensureBrowserWebContents(undefined, runtimeKey);
873
+ }
803
874
  bound.hostWebContents?.send('browser:ensureGuest');
804
875
  return bound;
805
876
  }
806
877
  }
807
- const registered = registeredBrowserGuest();
878
+ const registered = registeredBrowserGuest(undefined, runtimeKey);
808
879
  if (registered) {
809
880
  registered.hostWebContents?.send('browser:ensureGuest');
810
881
  return registered;
@@ -814,13 +885,13 @@ async function ensureBrowserWebContents(boundContentsId) {
814
885
  const host = hostWindow?.webContents;
815
886
  if (!host || host.isDestroyed())
816
887
  throw new Error('Built-in Browser UI is unavailable');
817
- host.send('browser:ensureGuest');
818
- return await waitForRegisteredBrowserGuest(host);
888
+ host.send('browser:ensureGuest', runtimeKey ? { runtimeKey } : undefined);
889
+ return await waitForRegisteredBrowserGuest(host, runtimeKey, 12_000);
819
890
  }
820
891
  function ensureElectronBrowserUseHost() {
821
892
  if (!electronBrowserUseHost) {
822
893
  electronBrowserUseHost = new electronBrowserUseHost_1.ElectronBrowserUseHost({
823
- resolveContents: async (_scope, boundContentsId) => await ensureBrowserWebContents(boundContentsId),
894
+ resolveContents: async (scope, boundContentsId) => await ensureBrowserWebContents(boundContentsId, scope.runtimeKey),
824
895
  openExternal: async (url) => { await electron_1.shell.openExternal(url); },
825
896
  });
826
897
  }
@@ -833,10 +904,18 @@ function ensureBrowserUseEngine() {
833
904
  }
834
905
  return browserUseEngine;
835
906
  }
836
- function currentBrowserUseContext() {
837
- const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget());
907
+ function currentBrowserUseContext(requestedTarget) {
908
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(requestedTarget));
838
909
  return { runtimeKey: target.runtimeKey, actorId: agent?.runtimeActorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID };
839
910
  }
911
+ function browserControlTargetInput(input) {
912
+ if (!input)
913
+ return undefined;
914
+ return {
915
+ workspaceId: String(input.workspaceId || ''),
916
+ conversationId: String(input.conversationId || 'default'),
917
+ };
918
+ }
840
919
  async function runBoundBrowserUse(input, context, signal) {
841
920
  return await ensureBrowserUseEngine().run((0, browserUse_1.bindBrowserUseRequest)(input, context), signal);
842
921
  }
@@ -856,8 +935,10 @@ function browserSnapshotScript(maxChars) {
856
935
  }
857
936
  async function runBrowserControl(request) {
858
937
  const action = request.action;
938
+ const requestedTarget = browserControlTargetInput(request.target);
939
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(requestedTarget));
859
940
  if (action === 'use') {
860
- const receipt = await runBoundBrowserUse(request.browserUse, currentBrowserUseContext());
941
+ const receipt = await runBoundBrowserUse(request.browserUse, currentBrowserUseContext(requestedTarget));
861
942
  return {
862
943
  ok: receipt.ok,
863
944
  action,
@@ -868,7 +949,7 @@ async function runBrowserControl(request) {
868
949
  error: receipt.error,
869
950
  };
870
951
  }
871
- const contents = await ensureBrowserWebContents();
952
+ const contents = await ensureBrowserWebContents(undefined, target.runtimeKey);
872
953
  try {
873
954
  if (action === 'open') {
874
955
  await contents.loadURL(request.url || 'about:blank');
@@ -953,12 +1034,55 @@ function installBrowserControlBackend() {
953
1034
  const args = userArgs();
954
1035
  const command = args.find(a => a === 'flow' || a === 'edit');
955
1036
  const isTuiArg = args.some(arg => arg.toLowerCase() === '--tui');
1037
+ const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
1038
+ const isFlowArg = command === 'flow';
1039
+ const isEditArg = command === 'edit';
1040
+ const isHelpArg = !hasCliCommand && (args.some(arg => ['--help', '-h'].includes(arg.toLowerCase())) || args[0]?.toLowerCase() === 'help');
1041
+ const isVersionArg = !hasCliCommand && (0, cli_discovery_1.isVersionArgument)(args);
1042
+ const isReadOnlyValidation = hasCliCommand && args.includes('validate-models') && !args.includes('--persist');
956
1043
  const isViewerArg = args.some(arg => arg.toLowerCase() === '--newmark-viewer');
957
1044
  const isCliArg = args.includes('--cli');
958
1045
  const isServerArg = args.includes('--server');
959
- const isFlowArg = command === 'flow';
960
- const isEditArg = command === 'edit';
961
- const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
1046
+ const invalidArgument = (0, cli_discovery_1.invalidTopLevelArgument)(args);
1047
+ if (invalidArgument) {
1048
+ console.error(`Invalid Newmark argument: ${invalidArgument}`);
1049
+ process.exit(2);
1050
+ }
1051
+ // Electron's Chromium profile is a separate state boundary from Newmark's
1052
+ // business root. Bind both before any ready event so --root cannot leave
1053
+ // Preferences, DIPS, DevTools ports, cookies, or session storage in the real
1054
+ // default AppData directory. The dedicated subdirectories keep Chromium's
1055
+ // files separate from the durable Newmark config/workspace files.
1056
+ const runtimeRoot = resolveRoot(args);
1057
+ const electronUserDataRoot = path.join(runtimeRoot, 'Electron');
1058
+ const electronSessionDataRoot = path.join(electronUserDataRoot, 'session-data');
1059
+ try {
1060
+ fs.mkdirSync(electronSessionDataRoot, { recursive: true });
1061
+ electron_1.app.setPath('userData', electronUserDataRoot);
1062
+ electron_1.app.setPath('sessionData', electronSessionDataRoot);
1063
+ }
1064
+ catch (error) {
1065
+ console.error(`Unable to isolate Electron user-data directory: ${error instanceof Error ? error.message : String(error)}`);
1066
+ process.exit(1);
1067
+ }
1068
+ // Help/version are terminating discovery commands. They must be handled
1069
+ // before Electron's GUI/TUI/server branches can initialize runtime state or
1070
+ // spawn a window, so a product-new tester can use them safely in any surface.
1071
+ if (isHelpArg) {
1072
+ console.log(isFlowArg ? (0, cli_help_1.newmarkFlowHelpText)() : isEditArg ? (0, cli_help_1.newmarkEditHelpText)() : (0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
1073
+ process.exit(0);
1074
+ }
1075
+ if (isVersionArg) {
1076
+ console.log((0, installUpdate_1.currentAppVersion)());
1077
+ process.exit(0);
1078
+ }
1079
+ const unknownCommand = electron_1.app.isPackaged && !hasCliCommand && !isTuiArg && !isCliArg && !isServerArg && !isViewerArg
1080
+ ? (0, cli_discovery_1.unknownTopLevelCommand)(args)
1081
+ : undefined;
1082
+ if (unknownCommand) {
1083
+ console.error(`Unknown Newmark command or argument: ${unknownCommand}. Run --help to see the supported entrypoints.`);
1084
+ process.exit(2);
1085
+ }
962
1086
  function viewerEscape(value) {
963
1087
  return String(value || '').replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] || character);
964
1088
  }
@@ -1033,7 +1157,9 @@ if (isViewerArg) {
1033
1157
  }).catch(error => { console.error(`Unable to open Newmark viewer: ${error instanceof Error ? error.message : String(error)}`); electron_1.app.quit(); });
1034
1158
  }
1035
1159
  else if (isTuiArg) {
1036
- const isConsoleLauncher = electron_1.app.isPackaged && path.basename(process.execPath).toLowerCase() === 'newmark.exe';
1160
+ const isConsoleLauncher = electron_1.app.isPackaged && (path.basename(process.execPath).toLowerCase() === 'newmark.exe'
1161
+ || path.basename(process.execPath).toLowerCase() === 'newmark console runtime.exe'
1162
+ || process.env.NEWMARK_CONSOLE_WRAPPER === '1');
1037
1163
  if (isConsoleLauncher && process.env.NEWMARK_TUI_SIDECAR !== '1') {
1038
1164
  const tuiProcess = (0, child_process_1.spawnSync)(process.execPath, [path.join(__dirname, 'launcher.js'), ...args], {
1039
1165
  cwd: process.cwd(),
@@ -1041,6 +1167,11 @@ else if (isTuiArg) {
1041
1167
  ...process.env,
1042
1168
  ELECTRON_RUN_AS_NODE: '1',
1043
1169
  NEWMARK_TUI_SIDECAR: '1',
1170
+ // The GUI Electron host does not expose the inherited ConPTY handles
1171
+ // as Node TTY streams. The console wrapper has already established
1172
+ // that this is the terminal entrypoint, so preserve the terminal
1173
+ // contract for the sidecar without weakening ordinary GUI launches.
1174
+ NEWMARK_FORCE_TTY: isConsoleLauncher ? '1' : process.env.NEWMARK_FORCE_TTY,
1044
1175
  },
1045
1176
  stdio: 'inherit',
1046
1177
  windowsHide: false,
@@ -1061,12 +1192,12 @@ else if (isTuiArg) {
1061
1192
  const root = resolveRoot(args);
1062
1193
  firstRunInit(root);
1063
1194
  const { start } = require('./tui/src/app');
1064
- start({ root, workspacePath: process.cwd(), desktopDist: __dirname });
1195
+ start({ root, workspacePath: resolveTuiWorkspacePath(args, root), desktopDist: __dirname });
1065
1196
  }
1066
1197
  else if (hasCliCommand) {
1067
1198
  (async () => {
1068
1199
  const root = resolveRoot(args);
1069
- firstRunInit(root);
1200
+ firstRunInit(root, { readOnly: isReadOnlyValidation });
1070
1201
  const handled = await (0, cli_commands_1.runCliCommand)(root, args);
1071
1202
  const code = typeof process.exitCode === 'number' ? process.exitCode : 0;
1072
1203
  exitCli(handled ? code : 1);
@@ -1227,6 +1358,51 @@ else {
1227
1358
  });
1228
1359
  electron_1.app.whenReady().then(async () => {
1229
1360
  let root = resolveRoot(args);
1361
+ let workspaceRegistryWatcher = null;
1362
+ let workspaceRegistryWatchTimer = null;
1363
+ const workspaceRegistryFiles = new Set(['Local.json', 'External.json', 'State.json']);
1364
+ const broadcastWorkspaceChanged = (files) => {
1365
+ for (const win of electron_1.BrowserWindow.getAllWindows()) {
1366
+ if (win.isDestroyed())
1367
+ continue;
1368
+ win.webContents.send('workspace:changed', { files, root });
1369
+ }
1370
+ };
1371
+ const refreshWorkspaceRegistryFromDisk = (files) => {
1372
+ if (!agent)
1373
+ return;
1374
+ agent.refreshWorkspaceRegistryFromStorage();
1375
+ const current = agent.workspace.current;
1376
+ if (current)
1377
+ workspaceSelectionCoordinator?.setCurrent(current.id || current.path || current.name);
1378
+ broadcastWorkspaceChanged(files);
1379
+ };
1380
+ const ensureWorkspaceRegistryWatcher = () => {
1381
+ if (workspaceRegistryWatcher)
1382
+ return;
1383
+ const workDir = path.join(root, 'Work');
1384
+ try {
1385
+ workspaceRegistryWatcher = fs.watch(workDir, { persistent: false }, (_eventType, filename) => {
1386
+ const changed = String(filename || '');
1387
+ if (changed && !workspaceRegistryFiles.has(path.basename(changed)))
1388
+ return;
1389
+ if (workspaceRegistryWatchTimer)
1390
+ clearTimeout(workspaceRegistryWatchTimer);
1391
+ workspaceRegistryWatchTimer = setTimeout(() => {
1392
+ workspaceRegistryWatchTimer = null;
1393
+ try {
1394
+ refreshWorkspaceRegistryFromDisk(changed ? [path.basename(changed)] : ['Work']);
1395
+ }
1396
+ catch (error) {
1397
+ logStartupFailure('workspace-registry-refresh', error);
1398
+ }
1399
+ }, 90);
1400
+ });
1401
+ }
1402
+ catch (error) {
1403
+ logStartupFailure('workspace-registry-watch', error);
1404
+ }
1405
+ };
1230
1406
  const fileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
1231
1407
  const pdfPreviewServer = new pdfPreviewServer_1.PdfPreviewServer((token, ownerId) => fileRouter.resolvePdfCapability(token, ownerId));
1232
1408
  await pdfPreviewServer.start();
@@ -1513,7 +1689,12 @@ else {
1513
1689
  rejectUiReadinessById(Number(fileRouterOwnerId), new Error('Startup window closed before UI readiness'));
1514
1690
  fileRouter.revokeOwner(fileRouterOwnerId);
1515
1691
  pdfPreviewServer.revokeOwner(fileRouterOwnerId);
1516
- browserGuestContentsByHost.delete(Number(fileRouterOwnerId));
1692
+ const closedHostId = Number(fileRouterOwnerId);
1693
+ browserGuestContentsByHost.delete(closedHostId);
1694
+ for (const [runtimeKey, binding] of browserGuestBindingsByRuntime) {
1695
+ if (binding.hostId === closedHostId)
1696
+ browserGuestBindingsByRuntime.delete(runtimeKey);
1697
+ }
1517
1698
  const remaining = electron_1.BrowserWindow.getAllWindows().filter(candidate => !candidate.isDestroyed() && candidate !== win);
1518
1699
  if (mainWindow === win)
1519
1700
  mainWindow = remaining[0] || null;
@@ -1628,6 +1809,7 @@ else {
1628
1809
  agent = new agent_1.Agent(root);
1629
1810
  mcpManager = new mcpManager_1.McpManager(root);
1630
1811
  activeAgentBackendMode = process.platform === 'win32' && agent.config.getBool('agent', 'run_in_wsl') ? 'wsl' : 'windows';
1812
+ ensureWorkspaceRegistryWatcher();
1631
1813
  restoreStoredFlowSuspension();
1632
1814
  recordStartup('agent-ready');
1633
1815
  }
@@ -2117,8 +2299,17 @@ else {
2117
2299
  (0, runtimeLifecycle_1.markRuntimeLifecycleClean)(root, 'main');
2118
2300
  };
2119
2301
  electron_1.app.on('will-quit', event => {
2120
- if (_forceQuit)
2302
+ // Window-close and tray-exit both enter this path. The runtime pools
2303
+ // must get a bounded graceful-shutdown window regardless of which
2304
+ // surface initiated the close; otherwise a stuck child can keep the
2305
+ // Electron parent and its renderer tree alive indefinitely.
2306
+ if (_forceQuit || !appExitCleanupComplete)
2121
2307
  armForcedExitDeadline('will-quit');
2308
+ if (workspaceRegistryWatchTimer)
2309
+ clearTimeout(workspaceRegistryWatchTimer);
2310
+ workspaceRegistryWatchTimer = null;
2311
+ workspaceRegistryWatcher?.close();
2312
+ workspaceRegistryWatcher = null;
2122
2313
  startupDeferredTasks?.cancel();
2123
2314
  agent?.flushWorkspaceConversationState();
2124
2315
  conversationKernel?.flushPersistence();
@@ -2179,6 +2370,7 @@ else {
2179
2370
  electronBrowserUseHost = null;
2180
2371
  browserControl_1.BrowserControl.setBackend(null);
2181
2372
  browserGuestContentsByHost.clear();
2373
+ browserGuestBindingsByRuntime.clear();
2182
2374
  if (sidecarProcess) {
2183
2375
  sidecarProcess.kill();
2184
2376
  sidecarProcess = null;
@@ -2448,6 +2640,13 @@ else {
2448
2640
  else
2449
2641
  await electronUtilityRuntimePool?.stopTarget(target);
2450
2642
  };
2643
+ const forceStopTargetRuntime = async (target) => {
2644
+ if (wslBackendEnabled())
2645
+ await wslAgentRuntimePool?.forceStopTarget(target);
2646
+ else
2647
+ await electronUtilityRuntimePool?.forceStopTarget(target);
2648
+ };
2649
+ const archiveInFlight = new Map();
2451
2650
  const isolatedConversationAgent = (target) => {
2452
2651
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
2453
2652
  const isolated = new agent_1.Agent(root, { agentOnly: true });
@@ -2569,9 +2768,9 @@ else {
2569
2768
  }
2570
2769
  }
2571
2770
  });
2572
- electron_1.ipcMain.handle('browser:registerGuest', (event, guestContentsId) => {
2771
+ electron_1.ipcMain.handle('browser:registerGuest', (event, guestContentsId, target) => {
2573
2772
  const guest = electron_1.webContents.fromId(Number(guestContentsId || 0));
2574
- return { accepted: !!guest && registerBrowserGuest(event.sender, guest) };
2773
+ return { accepted: !!guest && registerBrowserGuest(event.sender, guest, target) };
2575
2774
  });
2576
2775
  electron_1.ipcMain.handle('browser:control', async (_event, request) => {
2577
2776
  return await browserControl_1.BrowserControl.run(request);
@@ -2955,6 +3154,28 @@ else {
2955
3154
  runtimeDeferred: false,
2956
3155
  };
2957
3156
  });
3157
+ electron_1.ipcMain.handle('agent:computerUseState', async (_event, targetInput) => {
3158
+ if (!agent)
3159
+ return { enabled: false, occupied: false, runtimeKey: '' };
3160
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default'));
3161
+ return utilityHostToolHandler.computerUseState(target.runtimeKey);
3162
+ });
3163
+ electron_1.ipcMain.handle('agent:setComputerUseEnabled', async (_event, targetInput, enabled) => {
3164
+ if (!agent)
3165
+ return { ok: false, error: 'Agent not initialized' };
3166
+ const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default'));
3167
+ const result = utilityHostToolHandler.setComputerUseEnabled(target.runtimeKey, enabled !== false, `conversation:${target.conversationId}`);
3168
+ if (!result.ok)
3169
+ return result;
3170
+ if (enabled === false) {
3171
+ try {
3172
+ const { runComputerUse } = require('./tools/computerUse');
3173
+ await runComputerUse({ action: 'takeover_stop', workspacePath: target.workspace?.path || root, ownerId: target.runtimeKey, invocation: 'agent' });
3174
+ }
3175
+ catch { }
3176
+ }
3177
+ return result;
3178
+ });
2958
3179
  electron_1.ipcMain.handle('agent:updateGoal', async (_event, goal, targetInput) => {
2959
3180
  if (!agent)
2960
3181
  return null;
@@ -3057,6 +3278,7 @@ else {
3057
3278
  pendingOptions: conversationSnapshot.pendingOptions || agent.pendingOptions,
3058
3279
  flowSuspension: flowSuspensionForTarget(target),
3059
3280
  flowRunning: flowRunningForTarget(target),
3281
+ computerUse: utilityHostToolHandler.computerUseState((0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey),
3060
3282
  draft: agent.getStoredConversationDraft(target.conversationId) || '',
3061
3283
  proxyEnabled: agent.config.getBool('proxy', 'enabled'),
3062
3284
  proxyUrl: agent.config.getStr('proxy', 'url'),
@@ -3618,32 +3840,46 @@ else {
3618
3840
  return { ok: false, error: 'Agent not initialized' };
3619
3841
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3620
3842
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
3621
- assertTargetNotMutating(normalized);
3622
- mutatingRuntimeKeys.add(normalized.runtimeKey);
3843
+ const existing = archiveInFlight.get(normalized.runtimeKey);
3844
+ if (existing)
3845
+ return existing;
3846
+ const operation = (async () => {
3847
+ mutatingRuntimeKeys.add(normalized.runtimeKey);
3848
+ try {
3849
+ // 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);
3853
+ const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
3854
+ const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
3855
+ const ownsTargetWorkspace = !!normalized.workspace
3856
+ && !!agent.workspace.current
3857
+ && currentWorkspacePath === targetWorkspacePath;
3858
+ // The host Agent owns the current workspace persistence cache. An
3859
+ // isolated owner is used for another workspace. The archive writer
3860
+ // starts payload I/O in parallel and finalizes deletion against the
3861
+ // latest locked state snapshot, so rapid clicks do not serialize on
3862
+ // large Markdown bodies or lose a sibling deletion.
3863
+ const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
3864
+ const archived = await archiveOwner.archiveConversationAsync(normalized.conversationId);
3865
+ if (!archived)
3866
+ return { ok: false, error: 'Conversation archive could not be written.' };
3867
+ return { ok: true, fileName: archived, conversationId: normalized.conversationId, workspaceId: normalized.workspaceId };
3868
+ }
3869
+ catch (error) {
3870
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
3871
+ }
3872
+ finally {
3873
+ mutatingRuntimeKeys.delete(normalized.runtimeKey);
3874
+ }
3875
+ })();
3876
+ archiveInFlight.set(normalized.runtimeKey, operation);
3623
3877
  try {
3624
- const peek = peekTargetRuntime(normalized);
3625
- if (peek.running || peek.stopping)
3626
- return { ok: false, error: 'Cannot archive a conversation while its runtime is running or stopping.' };
3627
- if (peek.resident)
3628
- await stopTargetRuntime(normalized);
3629
- const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
3630
- const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
3631
- const ownsTargetWorkspace = !!normalized.workspace
3632
- && !!agent.workspace.current
3633
- && currentWorkspacePath === targetWorkspacePath;
3634
- // The host Agent owns the current workspace persistence cache. Archiving
3635
- // through it prevents a delayed host flush from resurrecting the target.
3636
- const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
3637
- const archived = archiveOwner.archiveConversation(normalized.conversationId);
3638
- if (!archived)
3639
- return { ok: false, error: 'Conversation archive could not be written.' };
3640
- return { ok: true, fileName: archived, conversationId: normalized.conversationId, workspaceId: normalized.workspaceId };
3641
- }
3642
- catch (error) {
3643
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
3878
+ return await operation;
3644
3879
  }
3645
3880
  finally {
3646
- mutatingRuntimeKeys.delete(normalized.runtimeKey);
3881
+ if (archiveInFlight.get(normalized.runtimeKey) === operation)
3882
+ archiveInFlight.delete(normalized.runtimeKey);
3647
3883
  }
3648
3884
  });
3649
3885
  electron_1.ipcMain.handle('agent:listArchives', async (_event, scope) => {
package/dist/preload.js CHANGED
@@ -34,11 +34,13 @@ contextBridge.exposeInMainWorld('api', {
34
34
  setConversationPinned: (id, pinned) => ipcRenderer.invoke('agent:setConversationPinned', id, pinned),
35
35
  renameConversation: (id, title) => ipcRenderer.invoke('agent:renameConversation', id, title),
36
36
  reorderConversations: (ids) => ipcRenderer.invoke('agent:reorderConversations', ids),
37
- browserRegisterGuest: (guestContentsId) => ipcRenderer.invoke('browser:registerGuest', guestContentsId),
37
+ browserRegisterGuest: (guestContentsId, target) => ipcRenderer.invoke('browser:registerGuest', guestContentsId, target),
38
38
  onBrowserEnsureGuest: (callback) => {
39
- ipcRenderer.on('browser:ensureGuest', () => callback());
39
+ ipcRenderer.on('browser:ensureGuest', (_event, target) => callback(target));
40
40
  },
41
41
  browserControl: (request) => ipcRenderer.invoke('browser:control', request),
42
+ computerUseState: (target) => ipcRenderer.invoke('agent:computerUseState', target),
43
+ setComputerUseEnabled: (target, enabled) => ipcRenderer.invoke('agent:setComputerUseEnabled', target, enabled),
42
44
  runFlow: (name, input, start) => ipcRenderer.invoke('flow:run', name, input, start),
43
45
  resumeFlow: (response, target) => ipcRenderer.invoke('flow:resume', response, target),
44
46
  guideFlow: (message, target) => ipcRenderer.invoke('flow:guide', message, target),
@@ -171,6 +173,12 @@ contextBridge.exposeInMainWorld('api', {
171
173
  onAgentWorkEvent: (callback) => {
172
174
  ipcRenderer.on('agent:workEvent', callback);
173
175
  },
176
+ onWorkspaceChanged: (callback) => {
177
+ ipcRenderer.on('workspace:changed', (_event, payload) => callback(payload));
178
+ },
179
+ removeWorkspaceChangedListener: () => {
180
+ ipcRenderer.removeAllListeners('workspace:changed');
181
+ },
174
182
  removeAgentWorkEventListener: () => {
175
183
  ipcRenderer.removeAllListeners('agent:workEvent');
176
184
  },
@@ -96,11 +96,7 @@ class ChatCompletionsAdapter {
96
96
  let emittedTool = false;
97
97
  try {
98
98
  while (true) {
99
- if (signal.aborted)
100
- throw (0, provider_events_1.providerAbortError)(signal);
101
- const readPromise = reader.read();
102
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Stream read timeout')), 30000));
103
- const { done, value } = await Promise.race([readPromise, timeoutPromise]);
99
+ const { done, value } = await (0, provider_events_1.readProviderStreamChunk)(reader, signal);
104
100
  if (done)
105
101
  break;
106
102
  buffer += decoder.decode(value, { stream: true });
@@ -12,6 +12,13 @@ export declare function defaultProviderTransport(request: SerializedProviderRequ
12
12
  * (`name === 'AbortError'`, preserves the abort reason when present).
13
13
  */
14
14
  export declare function providerAbortError(signal?: AbortSignal): Error;
15
+ export declare function providerStreamTimeoutError(timeoutMs: number): Error;
16
+ /**
17
+ * Read one SSE chunk with both user cancellation and an inactivity deadline.
18
+ * Cancelling the reader is important: rejecting the race alone leaves the
19
+ * provider socket alive and lets later requests accumulate behind it.
20
+ */
21
+ export declare function readProviderStreamChunk(reader: ReadableStreamDefaultReader<Uint8Array>, signal: AbortSignal, timeoutMs?: number): Promise<ReadableStreamReadResult<Uint8Array>>;
15
22
  export declare function parseProviderSse(raw: string): Array<{
16
23
  event?: string;
17
24
  data: string;