@aiworker/web 1.23.1 → 1.23.3

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 (72) hide show
  1. package/dist/assets/{CodeMirrorCommentWidgets-teB3HmBT.js → CodeMirrorCommentWidgets-pFeKzADY.js} +1 -1
  2. package/dist/assets/{DiffView-DC9RX3MF.js → DiffView-BYwAHIlW.js} +3 -3
  3. package/dist/assets/{DirtyBranchSwitchDialog-DwPkzXsS.js → DirtyBranchSwitchDialog-BeyJczAi.js} +1 -1
  4. package/dist/assets/{FilesView-BFZkuu4O.js → FilesView-lrP5OlzK.js} +6 -6
  5. package/dist/assets/{GitView-B_x52MHY.js → GitView-CCUK73JL.js} +4 -4
  6. package/dist/assets/{JsonTreeView-cmAzUTiw.js → JsonTreeView-CqUi8Ytj.js} +1 -1
  7. package/dist/assets/{LinearIssuesView-Dehh5afm.js → LinearIssuesView-C0WwIDQN.js} +1 -1
  8. package/dist/assets/{MarkdownImageGallery-DgNOqqhE.js → MarkdownImageGallery-0NXPad4K.js} +1 -1
  9. package/dist/assets/{MarkdownRendererImpl-DusmMZ_p.js → MarkdownRendererImpl-q-rUxBkU.js} +4 -4
  10. package/dist/assets/{OnboardingScreen-B8fm1H4m.js → OnboardingScreen-Cy6xZ-IQ.js} +1 -1
  11. package/dist/assets/{PatchDiff-BVr7gJU0.js → PatchDiff-DuYWs-7m.js} +1 -1
  12. package/dist/assets/{PierreDiffViewer-DzLt-nel.js → PierreDiffViewer-C51SEtjJ.js} +1 -1
  13. package/dist/assets/{PlanView-BmQUYT_z.js → PlanView-k63mHfFF.js} +1 -1
  14. package/dist/assets/SettingsView-DNJXHPYC.js +28 -0
  15. package/dist/assets/{SettingsWindow-Dy3M6ZB9.js → SettingsWindow-DFMJqIkx.js} +1 -1
  16. package/dist/assets/{ToolOutputDialog-cTl_eLIH.js → ToolOutputDialog-B4kBDjLK.js} +4 -4
  17. package/dist/assets/{ToolPartDiffPreview-C_3iYNJQ.js → ToolPartDiffPreview-DhVBmwis.js} +2 -2
  18. package/dist/assets/{VirtualizedFileDiff-CAWthkoy.js → VirtualizedFileDiff-C-MFNoKo.js} +1 -1
  19. package/dist/assets/{WalkthroughView-C7AHOpCA.js → WalkthroughView-hmnM70S9.js} +1 -1
  20. package/dist/assets/{computeVirtualFileMetrics-BkiDSlTW.js → computeVirtualFileMetrics-Bv64vrxY.js} +1 -1
  21. package/dist/assets/{de-DJsQr-p7.js → de-i-51iOMQ.js} +2 -2
  22. package/dist/assets/{es-BuYMlA9g.js → es-CNkqJISC.js} +3 -3
  23. package/dist/assets/{fr-ePDay33C.js → fr-a9t4bHWo.js} +3 -3
  24. package/dist/assets/index-BvwlOZOg.js +40 -0
  25. package/dist/assets/{index-n6O3hklM.js → index-C-KjDH6t.js} +1 -1
  26. package/dist/assets/index-n5p3SnES.css +1 -0
  27. package/dist/assets/{isDiffPlainText-GMdPz-71.js → isDiffPlainText-B0pMS3gs.js} +1 -1
  28. package/dist/assets/{isFilePlainText-BVuH1zH9.js → isFilePlainText-IXDeiXmH.js} +1 -1
  29. package/dist/assets/{ja-BzR9xOY_.js → ja-C2HQKC-_.js} +3 -3
  30. package/dist/assets/{ko-BEjHAm14.js → ko-CvVt6P_Q.js} +3 -3
  31. package/dist/assets/main-6ZT1mvpO.js +930 -0
  32. package/dist/assets/{main-D3ZDQglc.js → main-BeWvA5d9.js} +2 -2
  33. package/dist/assets/markdownCore-DTW1POUM.js +5 -0
  34. package/dist/assets/{miniChat-d01k801U.js → miniChat-DzMGD1Hv.js} +2 -2
  35. package/dist/assets/{mobile-D7VV-Ufz.js → mobile-CrDSF4dQ.js} +2 -2
  36. package/dist/assets/{patchFileDiff-CdztVMz0.js → patchFileDiff-DHG-znZQ.js} +3 -3
  37. package/dist/assets/{pl-rQmpxNIF.js → pl-CQi2SXLP.js} +3 -3
  38. package/dist/assets/{pt-BR-CorbsSW-.js → pt-BR-FdnmJOe8.js} +3 -3
  39. package/dist/assets/{renderElectronMiniChatApp-BGG9UIlh.js → renderElectronMiniChatApp-BuaS0XYk.js} +2 -2
  40. package/dist/assets/renderMobileApp-hxXJf_Db.js +25 -0
  41. package/dist/assets/runtimeEndpointReset-DB4KI-ED.js +41 -0
  42. package/dist/assets/{scheduledTasksApi-BEuUF_cj.js → scheduledTasksApi-DZaxQ19y.js} +1 -1
  43. package/dist/assets/{shikiHighlight-BzXUjlby.js → shikiHighlight-B1goMnV6.js} +1 -1
  44. package/dist/assets/{tr-CoAMXPtI.js → tr-BwD9HLzh.js} +2 -2
  45. package/dist/assets/{uk-YlAX0rRU.js → uk-BWNCwGJO.js} +3 -3
  46. package/dist/assets/{useAppFontEffects-Cwyhjfl5.js → useAppFontEffects-cYK1WODe.js} +130 -130
  47. package/dist/assets/useGitComparison-DIetwQtj.js +1 -0
  48. package/dist/assets/{useInlineCommentController-DDkpBEU8.js → useInlineCommentController-DscFZWyI.js} +2 -2
  49. package/dist/assets/{useRootScrollLock-CwjoWuC4.js → useRootScrollLock-CE3HJBOu.js} +1 -1
  50. package/dist/assets/{vendor-codemirror-language-data-Cb1YnICj.js → vendor-codemirror-language-data-CsvoASYk.js} +2 -2
  51. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  52. package/dist/assets/{web-components-LV3vjTxy.js → web-components-HM6S8xyb.js} +1 -1
  53. package/dist/assets/{zh-CN-DDYg0Jl7.js → zh-CN-CH6L_7cX.js} +6 -6
  54. package/dist/assets/{zh-TW-CQKaCayr.js → zh-TW--py9VFnc.js} +3 -3
  55. package/dist/index.html +3 -3
  56. package/dist/mini-chat.html +3 -3
  57. package/dist/mobile.html +3 -3
  58. package/package.json +2 -2
  59. package/server/lib/opencode/DOCUMENTATION.md +14 -0
  60. package/server/lib/opencode/openchamber-routes.js +72 -14
  61. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  62. package/server/lib/opencode/settings-helpers.js +5 -0
  63. package/server/lib/opencode/settings-helpers.test.js +13 -1
  64. package/server/lib/opencode/settings-registry.json +3 -0
  65. package/dist/assets/SettingsView-ChWmAcZT.js +0 -28
  66. package/dist/assets/index-BD8ILbxF.js +0 -40
  67. package/dist/assets/index-zhgg-SDz.css +0 -1
  68. package/dist/assets/main-BydQ1tr_.js +0 -930
  69. package/dist/assets/markdownCore-Cb2MdZnV.js +0 -5
  70. package/dist/assets/renderMobileApp-BRP3J1iv.js +0 -25
  71. package/dist/assets/runtimeEndpointReset-BhT_-c52.js +0 -41
  72. package/dist/assets/useGitComparison-BCZVefdE.js +0 -1
package/dist/index.html CHANGED
@@ -509,16 +509,16 @@
509
509
  pointer-events: none;
510
510
  }
511
511
  </style>
512
- <script type="module" crossorigin src="/assets/main-D3ZDQglc.js"></script>
512
+ <script type="module" crossorigin src="/assets/main-BeWvA5d9.js"></script>
513
513
  <link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
514
514
  <link rel="modulepreload" crossorigin href="/assets/vendor-zod-B6551dwG.js">
515
515
  <link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
516
516
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
517
517
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
518
518
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
519
- <link rel="modulepreload" crossorigin href="/assets/index-BD8ILbxF.js">
519
+ <link rel="modulepreload" crossorigin href="/assets/index-BvwlOZOg.js">
520
520
  <link rel="modulepreload" crossorigin href="/assets/contextPanelEmbeddedChat-BdA6bdky.js">
521
- <link rel="stylesheet" crossorigin href="/assets/index-zhgg-SDz.css">
521
+ <link rel="stylesheet" crossorigin href="/assets/index-n5p3SnES.css">
522
522
  </head>
523
523
  <body class="h-full bg-background text-foreground">
524
524
  <div id="root" class="h-full">
@@ -74,15 +74,15 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-d01k801U.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-DzMGD1Hv.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/vendor-zod-B6551dwG.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
82
82
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
84
- <link rel="modulepreload" crossorigin href="/assets/index-BD8ILbxF.js">
85
- <link rel="stylesheet" crossorigin href="/assets/index-zhgg-SDz.css">
84
+ <link rel="modulepreload" crossorigin href="/assets/index-BvwlOZOg.js">
85
+ <link rel="stylesheet" crossorigin href="/assets/index-n5p3SnES.css">
86
86
  </head>
87
87
  <body class="h-full bg-background text-foreground">
88
88
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -63,15 +63,15 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-D7VV-Ufz.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-CrDSF4dQ.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/vendor-vite-runtime-DD1OZmLC.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/vendor-zod-B6551dwG.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/vendor-scheduler-7OC5HNn7.js">
70
70
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BUCujlKS.js">
71
71
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-BAGL70zf.js">
72
72
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-BJ73u-bH.js">
73
- <link rel="modulepreload" crossorigin href="/assets/index-BD8ILbxF.js">
74
- <link rel="stylesheet" crossorigin href="/assets/index-zhgg-SDz.css">
73
+ <link rel="modulepreload" crossorigin href="/assets/index-BvwlOZOg.js">
74
+ <link rel="stylesheet" crossorigin href="/assets/index-n5p3SnES.css">
75
75
  </head>
76
76
  <body class="h-full bg-background text-foreground">
77
77
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiworker/web",
3
- "version": "1.23.1",
3
+ "version": "1.23.03",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "@clack/prompts": "^1.1.0",
27
27
  "@octokit/rest": "^22.0.1",
28
- "@opencode-ai/sdk": "1.18.30",
28
+ "@opencode-ai/sdk": "1.18.31",
29
29
  "@simplewebauthn/server": "13.3.1",
30
30
  "bun-pty": "^0.4.5",
31
31
  "compression": "^1.8.1",
@@ -389,6 +389,20 @@ within a ten-minute overall deadline.
389
389
  - Foreground servers running under a systemd user unit queue installation in
390
390
  a separate transient unit and restart the configured service afterwards.
391
391
  `AIWORKER_SYSTEMD_UNIT` overrides the default `aiworker.service`.
392
+ - On Windows the install-and-restart script is written to
393
+ `<data dir>/update-install.cmd` before the response and run with
394
+ `cmd.exe /c <file>`. A newline ends a `cmd.exe /c` command line, so the
395
+ same script passed as an argument ran nothing and exited 0; the batch
396
+ file keeps every line. The package-manager line is `call`ed because
397
+ npm, pnpm and yarn are `.cmd` shims that would otherwise end the script,
398
+ the pre-install pause is a loopback `ping` because `timeout` rejects a
399
+ detached child's stdin, and the file deletes itself on its last line
400
+ because the restart command carries the server's flags. If the file
401
+ cannot be written the route answers 500 and the server keeps running.
402
+ The listener is closed before the batch is spawned: on Windows the
403
+ detached child inherits the listening socket and would hold the port
404
+ for the whole batch, so the restart inside it failed with "port already
405
+ in use" and the update ended with no server.
392
406
  - `GET /api/openchamber/models-metadata`
393
407
  - `GET /api/zen/models`
394
408
 
@@ -16,6 +16,43 @@ function quotePosixShell(value) {
16
16
  return `'${String(value).replace(/'/g, "'\\''")}'`;
17
17
  }
18
18
 
19
+ /**
20
+ * `echo` in batch treats `& | < > ( ) ^` as syntax and `%var%` as expansion,
21
+ * so a line of the log preamble is escaped before it becomes an `echo`.
22
+ */
23
+ const escapeBatchEchoLine = (line) => {
24
+ if (!line) return 'echo.';
25
+ return `echo ${line.replace(/%/g, '%%').replace(/[&|<>()^]/g, '^$&')}`;
26
+ };
27
+
28
+ /**
29
+ * The Windows update runs as a batch file, one command per line, so the
30
+ * `if ... else` block and the multi-line log preamble keep their structure.
31
+ */
32
+ const buildWindowsUpdateScript = ({ logPreamble, updateCmd, restartCmd }) => [
33
+ '@echo off',
34
+ ...logPreamble.split('\n').map(escapeBatchEchoLine),
35
+ // `timeout` refuses redirected stdin, which is what a detached child has;
36
+ // a ping to loopback waits about two seconds without a console.
37
+ 'ping -n 3 127.0.0.1 >nul',
38
+ // npm, pnpm and yarn are .cmd shims on Windows. Without `call`, a batch
39
+ // file hands control to them for good and the restart below never runs.
40
+ // Read from a file, cmd expands `%x%` and drops a lone `%`, so a `%` in a
41
+ // host or UI password would change the restart command. Doubling keeps it.
42
+ `call ${updateCmd.replace(/%/g, '%%')}`,
43
+ 'if %ERRORLEVEL% EQU 0 (',
44
+ ' echo Update successful, restarting AiWorker...',
45
+ ` ${restartCmd ? restartCmd.replace(/%/g, '%%') : 'echo Service manager will restart AiWorker.'}`,
46
+ ') else (',
47
+ ' echo Update failed',
48
+ ')',
49
+ // The restart command carries the server's own flags, `--ui-password`
50
+ // included, so the file does not outlive the run. Deleting the running
51
+ // batch file on its last line is safe: cmd has already read it.
52
+ 'del "%~f0"',
53
+ '',
54
+ ].join('\r\n');
55
+
19
56
  export const registerAiWorkerRoutes = (app, dependencies) => {
20
57
  const {
21
58
  fs,
@@ -300,6 +337,26 @@ export const registerAiWorkerRoutes = (app, dependencies) => {
300
337
  `logPath=${updateLogPath}`,
301
338
  ].join('\n');
302
339
 
340
+ const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh';
341
+ const shellFlag = isWindows ? '/c' : '-c';
342
+ // cmd.exe /c takes one command line: a newline inside it ends the
343
+ // command, so a multi-line script passed as the argument ran nothing
344
+ // and exited 0, and the server shut itself down believing the update
345
+ // was under way (#3084). Batch runs from a file instead, written before
346
+ // the client is told to expect a restart.
347
+ const windowsScriptPath = path.join(aiworkerDataDir, 'update-install.cmd');
348
+ if (isWindows) {
349
+ try {
350
+ fs.mkdirSync(path.dirname(windowsScriptPath), { recursive: true });
351
+ fs.writeFileSync(windowsScriptPath, buildWindowsUpdateScript({ logPreamble, updateCmd, restartCmd }), 'utf8');
352
+ } catch (scriptError) {
353
+ console.error('Failed to write the update script, update not started:', scriptError);
354
+ return res.status(500).json({
355
+ error: `Could not write the update script at ${windowsScriptPath}: ${scriptError instanceof Error ? scriptError.message : String(scriptError)}`,
356
+ });
357
+ }
358
+ }
359
+
303
360
  res.json({
304
361
  success: true,
305
362
  message: 'Update starting, server will restart shortly',
@@ -314,21 +371,8 @@ export const registerAiWorkerRoutes = (app, dependencies) => {
314
371
  console.log(`Running: ${updateCmd}`);
315
372
  console.log(logPreamble);
316
373
 
317
- const shell = isWindows ? (process.env.ComSpec || 'cmd.exe') : 'sh';
318
- const shellFlag = isWindows ? '/c' : '-c';
319
374
  const script = isWindows
320
- ? `
321
- echo ${quoteCmd(logPreamble)}
322
- timeout /t 2 /nobreak >nul
323
- ${updateCmd}
324
- if %ERRORLEVEL% EQU 0 (
325
- echo Update successful, restarting AiWorker...
326
- ${restartCmd || 'echo Service manager will restart AiWorker.'}
327
- ) else (
328
- echo Update failed
329
- exit /b 1
330
- )
331
- `
375
+ ? windowsScriptPath
332
376
  : `
333
377
  printf '%s\n' ${quotePosix(logPreamble)}
334
378
  sleep 2
@@ -350,10 +394,24 @@ export const registerAiWorkerRoutes = (app, dependencies) => {
350
394
  console.warn('Failed to open update log file, continuing without log capture:', logError);
351
395
  }
352
396
 
397
+ if (isWindows) {
398
+ // On Windows the detached child inherits this process's listening
399
+ // socket, and keeps the port for as long as the batch runs. The
400
+ // restart inside that batch then fails with "port already in use",
401
+ // and the update ends with no server. Closing the listener first
402
+ // leaves nothing to inherit; the process exits right after anyway.
403
+ try {
404
+ server.close();
405
+ } catch (closeError) {
406
+ console.warn('Failed to close the listener before the update script:', closeError);
407
+ }
408
+ }
409
+
353
410
  const child = spawnChild(shell, [shellFlag, script], {
354
411
  detached: true,
355
412
  stdio: logFd !== null ? ['ignore', logFd, logFd] : 'ignore',
356
413
  env: process.env,
414
+ windowsHide: true,
357
415
  });
358
416
  child.unref();
359
417
 
@@ -18,11 +18,15 @@ const childProcess = await import('child_process');
18
18
  const packageManager = await import('../package-manager.js');
19
19
  const { registerAiWorkerRoutes } = await import('./openchamber-routes.js');
20
20
 
21
- const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater } = {}) => {
21
+ const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater, platform = 'linux', execPath = '/usr/bin/node' } = {}) => {
22
22
  const app = express();
23
23
  const dependencies = {
24
24
  fs: {
25
25
  existsSync: vi.fn(() => false),
26
+ mkdirSync: vi.fn(),
27
+ writeFileSync: vi.fn(),
28
+ openSync: vi.fn(() => 7),
29
+ closeSync: vi.fn(),
26
30
  promises: {
27
31
  readFile: vi.fn(async () => JSON.stringify({
28
32
  launchMode: 'foreground',
@@ -34,11 +38,13 @@ const createApp = ({ environment = {}, storedOptions = {}, desktopUpdater } = {}
34
38
  path,
35
39
  process: {
36
40
  env: environment,
37
- platform: 'linux',
38
- execPath: '/usr/bin/node',
41
+ platform,
42
+ execPath,
43
+ exit: vi.fn(),
39
44
  },
40
45
  server: {
41
46
  address: () => ({ port: 7897 }),
47
+ close: vi.fn(),
42
48
  },
43
49
  __dirname: '/opt/openchamber/server',
44
50
  aiworkerDataDir: '/tmp/openchamber',
@@ -266,3 +272,65 @@ describe('AiWorker foreground update route', () => {
266
272
  });
267
273
  });
268
274
  });
275
+
276
+ describe('AiWorker web update route on Windows', () => {
277
+ it('runs the install-and-restart script from a batch file instead of a cmd.exe /c argument', async () => {
278
+ const { app, dependencies } = createApp({
279
+ platform: 'win32',
280
+ execPath: 'C:\\Program Files\\nodejs\\node.exe',
281
+ environment: { ComSpec: 'C:\\Windows\\system32\\cmd.exe' },
282
+ storedOptions: { launchMode: 'daemon', port: 7897, uiPassword: 'pa%ss' },
283
+ });
284
+ childProcess.spawn.mockReturnValue({ unref: vi.fn() });
285
+ vi.spyOn(console, 'log').mockImplementation(() => {});
286
+
287
+ await request(app).post('/api/openchamber/update-install').expect(200);
288
+ await new Promise((resolve) => setTimeout(resolve, 1300));
289
+
290
+ const scriptPath = path.join('/tmp/openchamber', 'update-install.cmd');
291
+ expect(dependencies.fs.writeFileSync).toHaveBeenCalledWith(scriptPath, expect.any(String), 'utf8');
292
+ const script = dependencies.fs.writeFileSync.mock.calls[0][1];
293
+ const lines = script.split('\r\n');
294
+ expect(lines[0]).toBe('@echo off');
295
+ // Every preamble line is an echo; none is left to run as a command.
296
+ expect(lines.filter((line) => line.startsWith('currentVersion=') || line.startsWith('restartCommand='))).toEqual([]);
297
+ expect(lines).toContain('echo packageManager=npm');
298
+ expect(lines).toContain('echo restartCommand=^("C:\\Program Files\\nodejs\\node.exe" "/opt/openchamber/bin/cli.js" serve --port 7897 --ui-password "pa%%ss"^) ^|^| ^(aiworker serve --port 7897 --ui-password "pa%%ss"^)');
299
+ // A .cmd shim (npm, pnpm, yarn) must be `call`ed or the script ends there.
300
+ expect(lines).toContain('call npm install -g @aiworker/web@latest');
301
+ expect(lines).toContain('ping -n 3 127.0.0.1 >nul');
302
+ expect(lines.some((line) => line.startsWith('timeout '))).toBe(false);
303
+ expect(lines).toContain('if %ERRORLEVEL% EQU 0 (');
304
+ expect(lines.at(-2)).toBe('del "%~f0"');
305
+ // A `%` in the password survives batch expansion only when doubled.
306
+ expect(lines).toContain(' ("C:\\Program Files\\nodejs\\node.exe" "/opt/openchamber/bin/cli.js" serve --port 7897 --ui-password "pa%%ss") || (aiworker serve --port 7897 --ui-password "pa%%ss")');
307
+
308
+ expect(childProcess.spawn).toHaveBeenCalledWith(
309
+ 'C:\\Windows\\system32\\cmd.exe',
310
+ ['/c', scriptPath],
311
+ expect.objectContaining({ detached: true, windowsHide: true }),
312
+ );
313
+ // The listener is closed before the batch is spawned, so the detached
314
+ // child cannot inherit the socket and hold the port against the restart.
315
+ expect(dependencies.server.close).toHaveBeenCalledOnce();
316
+ expect(dependencies.server.close.mock.invocationCallOrder[0]).toBeLessThan(childProcess.spawn.mock.invocationCallOrder[0]);
317
+ expect(dependencies.process.exit).toHaveBeenCalledWith(0);
318
+ });
319
+
320
+ it('answers 500 and keeps the server up when the batch file cannot be written', async () => {
321
+ const { app, dependencies } = createApp({ platform: 'win32', storedOptions: { launchMode: 'daemon', port: 7897 } });
322
+ dependencies.fs.writeFileSync.mockImplementation(() => { throw new Error('EACCES'); });
323
+ vi.spyOn(console, 'log').mockImplementation(() => {});
324
+ const logError = vi.spyOn(console, 'error').mockImplementation(() => {});
325
+
326
+ const response = await request(app).post('/api/openchamber/update-install').expect(500);
327
+ await new Promise((resolve) => setTimeout(resolve, 1300));
328
+
329
+ expect(response.body.error).toContain('update-install.cmd');
330
+ expect(response.body.error).toContain('EACCES');
331
+ expect(childProcess.spawn).not.toHaveBeenCalled();
332
+ expect(dependencies.process.exit).not.toHaveBeenCalled();
333
+ expect(logError).toHaveBeenCalledOnce();
334
+ });
335
+ });
336
+
@@ -233,6 +233,11 @@ export const createSettingsHelpers = (dependencies) => {
233
233
  if (typeof candidate.workStatusHiddenSectionsExplicit === 'boolean') {
234
234
  result.workStatusHiddenSectionsExplicit = candidate.workStatusHiddenSectionsExplicit;
235
235
  }
236
+ if (Array.isArray(candidate.workStatusSectionOrder)) {
237
+ result.workStatusSectionOrder = [
238
+ ...new Set(candidate.workStatusSectionOrder.filter((entry) => typeof entry === 'string' && entry.length > 0)),
239
+ ];
240
+ }
236
241
  if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
237
242
  result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
238
243
  }
@@ -69,6 +69,18 @@ const createTestHelpersWithRealSanitizers = () => {
69
69
  };
70
70
 
71
71
  describe('settings helpers', () => {
72
+ it('round-trips section order and preserves it across unrelated writes', () => {
73
+ const helpers = createTestHelpers();
74
+ const changes = helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: ['mcp', 'session', 'mcp', null, ''] });
75
+ expect(changes.workStatusSectionOrder).toEqual(['mcp', 'session']);
76
+ const saved = helpers.mergePersistedSettings({}, changes);
77
+ const reloaded = helpers.formatSettingsResponse(JSON.parse(JSON.stringify(saved)));
78
+ expect(reloaded.workStatusSectionOrder).toEqual(['mcp', 'session']);
79
+ const next = helpers.mergePersistedSettings(reloaded, helpers.sanitizeSettingsUpdate({ workStatusPanelEnabled: false }));
80
+ expect(helpers.formatSettingsResponse(next).workStatusSectionOrder).toEqual(['mcp', 'session']);
81
+ expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: 'bad' }).workStatusSectionOrder).toBeUndefined();
82
+ expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: [] }).workStatusSectionOrder).toEqual([]);
83
+ });
72
84
  it('round-trips telemetry opt-in with the hidden list and preserves it across unrelated writes', () => {
73
85
  const helpers = createTestHelpers();
74
86
  const legacy = helpers.sanitizeSettingsUpdate({ workStatusHiddenSections: [] });
@@ -762,7 +774,7 @@ describe('settings registry gate', () => {
762
774
  managedRemoteTunnelHostname: 'x.example', managedRemoteTunnelToken: 'token', managedRemoteTunnelPresets: [{ id: 'a', name: 'A', hostname: 'a.example' }],
763
775
  managedRemoteTunnelSelectedPresetId: 'a', managedRemoteTunnelPresetTokens: { a: 'token' },
764
776
  sidebarProjectDisplayMode: 'all', sidebarSessionGroupingMode: 'flat', sidebarProjectSortOrder: 'manual', sidebarShowRecentSection: true,
765
- workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true,
777
+ workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true, workStatusSectionOrder: ['mcp', 'session'],
766
778
  showReasoningTraces: true, streamingAutoFollowEnabled: true, collapsibleThinkingBlocks: true, showTextJustificationActivity: true,
767
779
  chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
768
780
  stickyUserHeader: true, promptNavigatorEnabled: true, wideChatLayoutEnabled: true, showSplitAssistantMessageActions: true, showToolFileIcons: true,
@@ -217,6 +217,9 @@
217
217
  "sidebarShowRecentSection": {
218
218
  "scope": "profile"
219
219
  },
220
+ "workStatusSectionOrder": {
221
+ "scope": "profile"
222
+ },
220
223
  "workStatusPanelEnabled": {
221
224
  "scope": "profile"
222
225
  },