tide-commander 1.177.2 → 1.178.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 (120) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/assets/{BossLogsModal-BJ85hhZB.js → BossLogsModal-CS4LIvSu.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-iF7jBjoW.js → BossSpawnModal-DFqNOCaI.js} +1 -1
  4. package/dist/assets/{ControlsModal-C-Dlj2C2.js → ControlsModal-BTX5H6jw.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-CPK49emd.js → DockerLogsModal-C8pdXuqO.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-CtM591W2.js → EmbeddedEditor--FuYpTIa.js} +1 -1
  7. package/dist/assets/FcStdViewer-DwgwMwrx.js +1 -0
  8. package/dist/assets/GcodeViewer-CynUwDmS.js +1 -0
  9. package/dist/assets/{GmailOAuthSetup-DBbZpNj4.js → GmailOAuthSetup-BwdW6138.js} +1 -1
  10. package/dist/assets/{GoogleOAuthSetup-BF233CPj.js → GoogleOAuthSetup-D1r3EeDF.js} +1 -1
  11. package/dist/assets/{IframeModal-DoLqp8i3.js → IframeModal-B9yOGPoP.js} +1 -1
  12. package/dist/assets/{IntegrationsPanel-Dpx2slxW.js → IntegrationsPanel-lF8DIGck.js} +2 -2
  13. package/dist/assets/{LogViewerModal-CI6NVsnn.js → LogViewerModal-QtEg0GoV.js} +1 -1
  14. package/dist/assets/{MonitoringModal-CGQjCKh8.js → MonitoringModal-zHaxFAy8.js} +1 -1
  15. package/dist/assets/OrbitControls-B1__nj_c.js +1 -0
  16. package/dist/assets/{PM2LogsModal-cprJemBk.js → PM2LogsModal-DPRGsqvu.js} +1 -1
  17. package/dist/assets/{RestoreArchivedAreaModal-CD-nkwHT.js → RestoreArchivedAreaModal-4Yp2K-Vg.js} +1 -1
  18. package/dist/assets/{Scene2DCanvas-C0d2NOZF.js → Scene2DCanvas-Cn672tbC.js} +1 -1
  19. package/dist/assets/SceneManager-CKaBryd4.js +104 -0
  20. package/dist/assets/{SkillsPanel-Bea0jqNk.js → SkillsPanel-BNSoS60S.js} +1 -1
  21. package/dist/assets/{SlackMultiInstanceSetup-BvYk2UWs.js → SlackMultiInstanceSetup-BsckrPs-.js} +1 -1
  22. package/dist/assets/{SpawnModal-EOdsRMsU.js → SpawnModal-BxdMXDMp.js} +1 -1
  23. package/dist/assets/{StatisticsModal-DMTuwOyD.js → StatisticsModal-FNbvlYGU.js} +1 -1
  24. package/dist/assets/StlViewer-CRt0aUAf.js +1 -0
  25. package/dist/assets/{SubordinateAssignmentModal-kaXTALIo.js → SubordinateAssignmentModal-DxUqNMOR.js} +1 -1
  26. package/dist/assets/ThreeAreaSelector-DkbNFeb4.js +2 -0
  27. package/dist/assets/ThreeViewShortcuts-C6XYQvVm.js +1 -0
  28. package/dist/assets/{TriggerManagerPanel-BwmFtZCh.js → TriggerManagerPanel-DcX4J5m7.js} +1 -1
  29. package/dist/assets/{WorkflowEditorPanel-2SLzQ_F7.js → WorkflowEditorPanel-5VPqFGzD.js} +1 -1
  30. package/dist/assets/{abnfDiagram-VRR7QNED-Bn16ZJvn.js → abnfDiagram-VRR7QNED-6F-3T6-Y.js} +1 -1
  31. package/dist/assets/{arc-BS1yGfg8.js → arc-NHzv0EA5.js} +1 -1
  32. package/dist/assets/{architectureDiagram-ZJ3FMSHR-BowAVE2g.js → architectureDiagram-ZJ3FMSHR-CsJhNPjv.js} +1 -1
  33. package/dist/assets/{area-logos-iPYyTqRS.js → area-logos-CYEXA4lp.js} +1 -1
  34. package/dist/assets/{blockDiagram-677ZJIJ3-ZcRTeACC.js → blockDiagram-677ZJIJ3-UT3b2Q12.js} +1 -1
  35. package/dist/assets/{c4Diagram-LMCZKHZV-DVyCmWLe.js → c4Diagram-LMCZKHZV-uMYRTZix.js} +1 -1
  36. package/dist/assets/channel-D4Oo3ZHk.js +1 -0
  37. package/dist/assets/{chunk-2Q5K7J3B-Do4loXOf.js → chunk-2Q5K7J3B-BwXKcNaP.js} +1 -1
  38. package/dist/assets/{chunk-32BRIVSS-BteUBJrm.js → chunk-32BRIVSS-C8dNk4Hn.js} +1 -1
  39. package/dist/assets/{chunk-5VM5RSS4-CCzvXqtX.js → chunk-5VM5RSS4-Z2pZcBbh.js} +1 -1
  40. package/dist/assets/{chunk-EX3LRPZG-CWfoK8TS.js → chunk-EX3LRPZG-dA9bU_uS.js} +1 -1
  41. package/dist/assets/{chunk-JWPE2WC7-T2EVEO7f.js → chunk-JWPE2WC7-CMkfDWfd.js} +1 -1
  42. package/dist/assets/{chunk-MOJQB5TN-Bms_j_vp.js → chunk-MOJQB5TN-CRQhypCf.js} +1 -1
  43. package/dist/assets/{chunk-RYQCIY6F-BHaTGeSM.js → chunk-RYQCIY6F-CPvbNKNZ.js} +1 -1
  44. package/dist/assets/{chunk-V7JOEXUC-CTk13LTh.js → chunk-V7JOEXUC-6JsV0G3U.js} +1 -1
  45. package/dist/assets/{chunk-VR4S4FIN-DbAzJVcf.js → chunk-VR4S4FIN-n0aWG4-j.js} +1 -1
  46. package/dist/assets/{chunk-XXDRQBXY--nHyvZQ6.js → chunk-XXDRQBXY-BLQ81Hhu.js} +1 -1
  47. package/dist/assets/classDiagram-OUVF2IWQ-ChKBeWCX.js +1 -0
  48. package/dist/assets/classDiagram-v2-EOCWNBFH-ChKBeWCX.js +1 -0
  49. package/dist/assets/{cose-bilkent-JH36ORCC-D4STnGjf.js → cose-bilkent-JH36ORCC-BSHuRRPa.js} +1 -1
  50. package/dist/assets/{cynefin-VYW2F7L2-C3bGlbuH.js → cynefin-VYW2F7L2-vKDum-r-.js} +1 -1
  51. package/dist/assets/{cynefinDiagram-TSTJHNR4-NtQ7fqyK.js → cynefinDiagram-TSTJHNR4-B7hlff7z.js} +1 -1
  52. package/dist/assets/{dagre-VKFMJZFB-4_7veLhL.js → dagre-VKFMJZFB-K2lW3A2u.js} +1 -1
  53. package/dist/assets/{diagram-FQU43EPY-BXSSs1Tt.js → diagram-FQU43EPY-gtpv6DY0.js} +1 -1
  54. package/dist/assets/{diagram-G47NLZAW-0IkpFOVq.js → diagram-G47NLZAW-D9yHOqQq.js} +1 -1
  55. package/dist/assets/{diagram-NH7WQ7WH-DdjwsYrQ.js → diagram-NH7WQ7WH-BzeJ_2nM.js} +1 -1
  56. package/dist/assets/{diagram-OA4YK3LP-DVlmCA6A.js → diagram-OA4YK3LP-CM0HWugE.js} +1 -1
  57. package/dist/assets/{diagram-WEI45ONY-COn2Rq3e.js → diagram-WEI45ONY-BMyBFCkS.js} +1 -1
  58. package/dist/assets/{ebnfDiagram-CCIWWBDH-BBrme_H-.js → ebnfDiagram-CCIWWBDH-8Kj0FCEb.js} +1 -1
  59. package/dist/assets/{erDiagram-Q63AITRT-Dued2fRQ.js → erDiagram-Q63AITRT-BWDV58w3.js} +1 -1
  60. package/dist/assets/fcstd.worker-BSFnALBZ.js +24 -0
  61. package/dist/assets/{flowDiagram-23GEKE2U-CI3IU-MQ.js → flowDiagram-23GEKE2U-Bvu7iKuy.js} +1 -1
  62. package/dist/assets/{ganttDiagram-NO4QXBWP-DjLcWub4.js → ganttDiagram-NO4QXBWP-oOnRqEGi.js} +1 -1
  63. package/dist/assets/gcode.worker-DayjyJb4.js +1 -0
  64. package/dist/assets/{gitGraphDiagram-IHSO6WYX-CDGs8fL2.js → gitGraphDiagram-IHSO6WYX-BLNHz3A1.js} +1 -1
  65. package/dist/assets/{index-Ds6C-5SZ.js → index-BALtikL3.js} +1 -1
  66. package/dist/assets/{index-e1--CClk.js → index-BYRIJxh3.js} +3 -3
  67. package/dist/assets/{index-BeFfmS97.js → index-BZge8Vqz.js} +1 -1
  68. package/dist/assets/{index-BcJI4Wf2.js → index-Bzpy8ZrM.js} +1 -1
  69. package/dist/assets/index-CGjogkgR.js +2 -0
  70. package/dist/assets/{index-CPT2yDF7.js → index-CQrwfFmj.js} +3 -3
  71. package/dist/assets/{index-BO9rcCxW.js → index-DaieR5TQ.js} +1 -1
  72. package/dist/assets/{index-DY0EdedI.js → index-DcPCPN6v.js} +6 -6
  73. package/dist/assets/index-DhnKpoWm.js +2 -0
  74. package/dist/assets/{infoDiagram-FWYZ7A6U-BR8lw-07.js → infoDiagram-FWYZ7A6U-CBoekTgL.js} +1 -1
  75. package/dist/assets/{ishikawaDiagram-FXEZZL3T-BvoUdvxM.js → ishikawaDiagram-FXEZZL3T-i92jYFyn.js} +1 -1
  76. package/dist/assets/{journeyDiagram-5HDEW3XC-5ZTKSL91.js → journeyDiagram-5HDEW3XC-Dlb8tdvI.js} +1 -1
  77. package/dist/assets/{kanban-definition-HUTT4EX6-Crw59w7d.js → kanban-definition-HUTT4EX6-C357GaQI.js} +1 -1
  78. package/dist/assets/{linear-BClkkbnW.js → linear-CIJgBkf3.js} +1 -1
  79. package/dist/assets/{main-anrHq8ki.js → main-DN8Eh8GZ.js} +144 -144
  80. package/dist/assets/main-ZtOMcfnf.css +1 -0
  81. package/dist/assets/{mermaid.core-BDhiHGyU.js → mermaid.core-DpoI2o0n.js} +4 -4
  82. package/dist/assets/{mindmap-definition-LN4V7U3C-kGtzuV8p.js → mindmap-definition-LN4V7U3C-C5jHYqmi.js} +1 -1
  83. package/dist/assets/occt-import-js-BhHfLpto.wasm +0 -0
  84. package/dist/assets/{pegDiagram-2B236MQR-DacPFZVr.js → pegDiagram-2B236MQR-CxZiEIc6.js} +1 -1
  85. package/dist/assets/{pieDiagram-ENE6RG2P-a7t3bV4m.js → pieDiagram-ENE6RG2P-DP--XMmv.js} +1 -1
  86. package/dist/assets/{quadrantDiagram-ABIIQ3AL-D1LJoytH.js → quadrantDiagram-ABIIQ3AL-SVVI0nj0.js} +1 -1
  87. package/dist/assets/{railroadDiagram-RFXS5EU6-DtgETCCu.js → railroadDiagram-RFXS5EU6-B96CUfFG.js} +1 -1
  88. package/dist/assets/{requirementDiagram-TGXJPOKE-B8ofroLn.js → requirementDiagram-TGXJPOKE-CY0Y9Spd.js} +1 -1
  89. package/dist/assets/{sankeyDiagram-HTMAVEWB-BLJxfQiX.js → sankeyDiagram-HTMAVEWB-DUwF25eW.js} +1 -1
  90. package/dist/assets/{sequenceDiagram-DBY2YBRQ-DkXTwgge.js → sequenceDiagram-DBY2YBRQ-B1BXiSWm.js} +1 -1
  91. package/dist/assets/{sizeCapture-X5ZJPWSS-Tu0vJw5U.js → sizeCapture-X5ZJPWSS-DOInCz7x.js} +1 -1
  92. package/dist/assets/{stateDiagram-2N3HPSRC-Y8r1KoB6.js → stateDiagram-2N3HPSRC-DbJfBNBw.js} +1 -1
  93. package/dist/assets/stateDiagram-v2-6OUMAXLB-Cw6k7KjM.js +1 -0
  94. package/dist/assets/{swimlanes-5IMT3BWC-BFl--h7F.js → swimlanes-5IMT3BWC-BiSKR-2-.js} +2 -2
  95. package/dist/assets/swimlanesDiagram-G3AALYLV-De7ovgag.js +8 -0
  96. package/dist/assets/{timeline-definition-FHXFAJF6-CVxVk8L_.js → timeline-definition-FHXFAJF6-CjofGPLC.js} +1 -1
  97. package/dist/assets/{vendor-three-3OKhI9bz.js → vendor-three-CpheMTQa.js} +169 -169
  98. package/dist/assets/{vennDiagram-L72KCM5P-BfIiqSop.js → vennDiagram-L72KCM5P-B_6UlK4t.js} +1 -1
  99. package/dist/assets/{wardleyDiagram-EHGQE667-K-vB1d_8.js → wardleyDiagram-EHGQE667-CdJsoBXi.js} +1 -1
  100. package/dist/assets/{web-BWzU4tbP.js → web-DEGVeF3E.js} +1 -1
  101. package/dist/assets/{web-sV6-seS-.js → web-DINOAJn_.js} +1 -1
  102. package/dist/assets/{web-e-nG7wcx.js → web-Dt2qR7v-.js} +1 -1
  103. package/dist/assets/{xychartDiagram-FW5EYKEG-D6q7J6Kr.js → xychartDiagram-FW5EYKEG-Cp2dbS-J.js} +1 -1
  104. package/dist/build-info.json +1 -1
  105. package/dist/index.html +3 -3
  106. package/dist/locales/en/terminal.json +90 -1
  107. package/dist/locales/es/terminal.json +92 -2
  108. package/dist/src/packages/server/codex/app-server/app-server-process.js +153 -12
  109. package/dist/src/packages/server/routes/files.js +80 -6
  110. package/dist/src/packages/server/routes/system.js +9 -2
  111. package/package.json +2 -1
  112. package/dist/assets/SceneManager-CtgRFnMf.js +0 -104
  113. package/dist/assets/channel-CohCoDiY.js +0 -1
  114. package/dist/assets/classDiagram-OUVF2IWQ-BxHRM6o6.js +0 -1
  115. package/dist/assets/classDiagram-v2-EOCWNBFH-BxHRM6o6.js +0 -1
  116. package/dist/assets/index-CD2Gv3oe.js +0 -2
  117. package/dist/assets/index-evqtZdbD.js +0 -2
  118. package/dist/assets/main-BNsRZCIt.css +0 -1
  119. package/dist/assets/stateDiagram-v2-6OUMAXLB-2qL1wJnY.js +0 -1
  120. package/dist/assets/swimlanesDiagram-G3AALYLV-CrYKOH_2.js +0 -8
@@ -20,6 +20,12 @@
20
20
  *
21
21
  * One process multiplexes every Codex agent: each agent owns a `threadId`, and
22
22
  * notifications carry that id so the runner can fan them back out.
23
+ *
24
+ * The daemon reads ~/.codex/auth.json ONCE at startup and never re-reads it, so
25
+ * a daemon that outlives an account switch keeps running every agent on the
26
+ * PREVIOUS account. Both entry points guard against that: boot() refuses to
27
+ * rejoin a daemon whose account no longer matches the auth file, and
28
+ * `killDetachedAppServerDaemon()` is called by the account switcher.
23
29
  */
24
30
  import { spawn } from 'child_process';
25
31
  import * as http from 'http';
@@ -27,11 +33,14 @@ import * as net from 'net';
27
33
  import * as fs from 'fs';
28
34
  import { WebSocket } from 'ws';
29
35
  import { createLogger } from '../../utils/logger.js';
36
+ import { listProviderCredentialProfiles } from '../../services/provider-credentials-service.js';
30
37
  import { loadDaemonInfo, saveDaemonInfo, clearDaemonInfo, APP_SERVER_LOG_FILE, } from './app-server-recovery-store.js';
31
38
  const log = createLogger('CodexAppServer');
32
39
  const INITIALIZE_TIMEOUT_MS = 15000;
33
40
  const READYZ_TIMEOUT_MS = 20000;
34
41
  const READYZ_POLL_MS = 250;
42
+ const ACCOUNT_PROBE_TIMEOUT_MS = 5000;
43
+ const DAEMON_KILL_GRACE_MS = 2000;
35
44
  export class CodexAppServerProcess {
36
45
  opts;
37
46
  ws = null;
@@ -60,14 +69,21 @@ export class CodexAppServerProcess {
60
69
  }
61
70
  async boot() {
62
71
  this.stopped = false;
63
- // 1. Rejoin an already-running daemon if one is healthy.
72
+ // 1. Rejoin an already-running daemon if one is healthy AND still logged in
73
+ // as the account ~/.codex/auth.json currently holds. A daemon that
74
+ // survived an account switch (ours or a plain `codex login`) would keep
75
+ // every agent on the old, usually rate-limited, account.
64
76
  const existing = loadDaemonInfo();
65
77
  if (existing && (await this.isDaemonHealthy(existing))) {
66
- log.log(`🔗 Rejoining existing app-server daemon pid=${existing.pid} port=${existing.port}`);
67
- this.port = existing.port;
68
- this.reconnectedToExisting = true;
69
- await this.connectAndInitialize(existing.port);
70
- return;
78
+ if (await daemonAccountIsCurrent(existing.port)) {
79
+ log.log(`🔗 Rejoining existing app-server daemon pid=${existing.pid} port=${existing.port}`);
80
+ this.port = existing.port;
81
+ this.reconnectedToExisting = true;
82
+ await this.connectAndInitialize(existing.port);
83
+ return;
84
+ }
85
+ log.warn(`Existing app-server daemon pid=${existing.pid} is signed in as a different Codex account than ~/.codex/auth.json — killing it so the active account takes effect`);
86
+ killDetachedAppServerDaemon();
71
87
  }
72
88
  // 2. Spawn a fresh detached daemon.
73
89
  this.reconnectedToExisting = false;
@@ -253,15 +269,140 @@ export class CodexAppServerProcess {
253
269
  /** Disconnect AND kill the daemon (used on explicit full shutdown). */
254
270
  killDaemon() {
255
271
  this.disconnect();
256
- const info = loadDaemonInfo();
257
- if (info && isPidAlive(info.pid)) {
272
+ killDetachedAppServerDaemon();
273
+ }
274
+ }
275
+ /**
276
+ * Kill the detached daemon recorded on disk, if any, and forget it.
277
+ *
278
+ * Called when the live Codex account changes: the daemon caches
279
+ * ~/.codex/auth.json in memory at startup, so switching accounts is a silent
280
+ * no-op for every agent until the daemon is replaced. The next launch spawns a
281
+ * fresh one with the new credentials; in-flight turns die with the old daemon
282
+ * (they were running on the account the user just abandoned), and the runner's
283
+ * socket-close handler finalizes those agents so none stays stuck 'working'.
284
+ */
285
+ export function killDetachedAppServerDaemon() {
286
+ const info = loadDaemonInfo();
287
+ if (!info)
288
+ return { stopped: false, pid: null };
289
+ const alive = isPidAlive(info.pid);
290
+ if (alive) {
291
+ // The recorded pid is the `codex` node wrapper, which spawns the real Rust
292
+ // binary as a child — signal the whole process GROUP (spawn used
293
+ // detached:true, so the wrapper leads one) or the binary outlives the
294
+ // wrapper and keeps holding the old account. SIGKILL the stragglers after a
295
+ // grace period; the timer is unref'd so it can't hold the process open.
296
+ signalDaemon(info.pid, 'SIGTERM');
297
+ log.log(`🔪 Killed app-server daemon pid=${info.pid} (Codex credentials changed)`);
298
+ setTimeout(() => {
299
+ if (isPidAlive(info.pid))
300
+ signalDaemon(info.pid, 'SIGKILL');
301
+ }, DAEMON_KILL_GRACE_MS).unref();
302
+ }
303
+ clearDaemonInfo();
304
+ return { stopped: alive, pid: alive ? info.pid : null };
305
+ }
306
+ function signalDaemon(pid, signal) {
307
+ try {
308
+ process.kill(-pid, signal);
309
+ return;
310
+ }
311
+ catch { /* not a group leader — fall back to the pid itself */ }
312
+ try {
313
+ process.kill(pid, signal);
314
+ }
315
+ catch (err) {
316
+ log.warn(`Failed to ${signal} app-server daemon pid=${pid}: ${err.message}`);
317
+ }
318
+ }
319
+ /**
320
+ * Compare the account a daemon is signed in as against the one in the auth
321
+ * file. Anything unknown (API-key auth, unreachable daemon, probe error) counts
322
+ * as current — a probe glitch must never cost the user a running daemon.
323
+ */
324
+ export function isDaemonAccountCurrent(daemonEmail, activeEmail) {
325
+ if (!daemonEmail || !activeEmail)
326
+ return true;
327
+ return daemonEmail.trim().toLowerCase() === activeEmail.trim().toLowerCase();
328
+ }
329
+ async function daemonAccountIsCurrent(port) {
330
+ let activeEmail = null;
331
+ try {
332
+ activeEmail = listProviderCredentialProfiles('codex').active?.email ?? null;
333
+ }
334
+ catch {
335
+ return true;
336
+ }
337
+ if (!activeEmail)
338
+ return true;
339
+ return isDaemonAccountCurrent(await probeDaemonAccountEmail(port), activeEmail);
340
+ }
341
+ /**
342
+ * Ask a running daemon which ChatGPT account it holds, over a throwaway
343
+ * WebSocket so the runner's own connection/lifecycle is untouched. Returns null
344
+ * when the answer isn't a ChatGPT login (API key) or the probe fails.
345
+ */
346
+ function probeDaemonAccountEmail(port) {
347
+ return new Promise((resolve) => {
348
+ let ws;
349
+ try {
350
+ ws = new WebSocket(`ws://127.0.0.1:${port}`);
351
+ }
352
+ catch {
353
+ resolve(null);
354
+ return;
355
+ }
356
+ let settled = false;
357
+ const finish = (email) => {
358
+ if (settled)
359
+ return;
360
+ settled = true;
361
+ clearTimeout(timer);
258
362
  try {
259
- process.kill(info.pid, 'SIGTERM');
363
+ ws.close();
260
364
  }
261
365
  catch { /* ignore */ }
262
- }
263
- clearDaemonInfo();
264
- }
366
+ resolve(email);
367
+ };
368
+ const timer = setTimeout(() => finish(null), ACCOUNT_PROBE_TIMEOUT_MS);
369
+ ws.on('open', () => {
370
+ ws.send(JSON.stringify({
371
+ id: 1,
372
+ method: 'initialize',
373
+ params: {
374
+ clientInfo: { name: 'tide-commander', title: 'Tide Commander', version: '1.0.0' },
375
+ capabilities: { experimentalApi: true },
376
+ },
377
+ }));
378
+ });
379
+ ws.on('message', (data) => {
380
+ let msg;
381
+ try {
382
+ msg = JSON.parse(data.toString());
383
+ }
384
+ catch {
385
+ return;
386
+ }
387
+ if (msg.id === undefined || msg.method !== undefined)
388
+ return;
389
+ if (Number(msg.id) === 1) {
390
+ if (msg.error) {
391
+ finish(null);
392
+ return;
393
+ }
394
+ ws.send(JSON.stringify({ method: 'initialized', params: {} }));
395
+ ws.send(JSON.stringify({ id: 2, method: 'account/read', params: {} }));
396
+ return;
397
+ }
398
+ if (Number(msg.id) === 2) {
399
+ const account = msg.result?.account;
400
+ finish(typeof account?.email === 'string' ? account.email : null);
401
+ }
402
+ });
403
+ ws.on('error', () => finish(null));
404
+ ws.on('close', () => finish(null));
405
+ });
265
406
  }
266
407
  function isPidAlive(pid) {
267
408
  if (!pid || pid < 1)
@@ -5,7 +5,7 @@
5
5
  import { Router } from 'express';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
- import { execFile, execSync, spawn } from 'child_process';
8
+ import { execFile, execFileSync, execSync, spawn } from 'child_process';
9
9
  import * as os from 'os';
10
10
  import { pathToFileURL } from 'url';
11
11
  import { logger } from '../utils/logger.js';
@@ -21,6 +21,23 @@ if (!fs.existsSync(TEMP_DIR)) {
21
21
  }
22
22
  log.log(` Temp upload directory: ${TEMP_DIR}`);
23
23
  const router = Router();
24
+ function looksLikeBinaryBuffer(buffer) {
25
+ if (buffer.length === 0)
26
+ return false;
27
+ const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
28
+ // NUL catches the common case; replacement characters catch compressed or
29
+ // media formats that contain no NUL in their first block but are not UTF-8.
30
+ return sample.includes(0) || buffer.toString('utf-8').includes('\uFFFD');
31
+ }
32
+ function binaryContentTypeForExtension(extension) {
33
+ const mimeTypes = {
34
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
35
+ '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
36
+ '.ico': 'image/x-icon', '.pdf': 'application/pdf', '.stl': 'model/stl',
37
+ '.fcstd': 'application/vnd.freecad', '.gcode': 'text/x-gcode', '.gco': 'text/x-gcode',
38
+ };
39
+ return mimeTypes[extension] || 'application/octet-stream';
40
+ }
24
41
  // Windows absolute paths — drive-letter (`C:\…` or `C:/…`) and UNC (`\\server\share`)
25
42
  // — are NOT recognized by path.isAbsolute() when the server runs on POSIX. Without
26
43
  // this, a configured Windows path is misclassified as relative and resolved into
@@ -546,7 +563,7 @@ router.get('/read', async (req, res) => {
546
563
  res.status(400).json({ error: 'File too large (max 1MB)' });
547
564
  return;
548
565
  }
549
- const content = fs.readFileSync(filePath, 'utf-8');
566
+ const fileBuffer = fs.readFileSync(filePath);
550
567
  const extension = path.extname(filePath).toLowerCase();
551
568
  const filename = path.basename(filePath);
552
569
  const base = {
@@ -559,6 +576,11 @@ router.get('/read', async (req, res) => {
559
576
  areaId: resolution.areaId,
560
577
  areaName: resolution.areaName,
561
578
  };
579
+ if (looksLikeBinaryBuffer(fileBuffer)) {
580
+ res.status(415).json({ ...base, binary: true, error: 'Binary file cannot be read as text' });
581
+ return;
582
+ }
583
+ const content = fileBuffer.toString('utf-8');
562
584
  // Preview mode (Ctrl+hover tooltip in the terminal): return only the
563
585
  // requested line window. Reading the file is cheap; shipping a 1MB body
564
586
  // over the wire on every hover is not.
@@ -762,6 +784,10 @@ router.get('/binary', async (req, res) => {
762
784
  '.ico': 'image/x-icon',
763
785
  '.svg': 'image/svg+xml',
764
786
  '.pdf': 'application/pdf',
787
+ '.stl': 'model/stl',
788
+ '.fcstd': 'application/vnd.freecad',
789
+ '.gcode': 'text/x-gcode',
790
+ '.gco': 'text/x-gcode',
765
791
  '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
766
792
  '.xls': 'application/vnd.ms-excel',
767
793
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@@ -1817,11 +1843,10 @@ router.get('/git-original', async (req, res) => {
1817
1843
  // Get relative path from git root
1818
1844
  const relativePath = path.relative(gitRoot, filePath);
1819
1845
  // Get original content from HEAD
1820
- let originalContent;
1846
+ let originalBuffer;
1821
1847
  try {
1822
- originalContent = execSync(`git show HEAD:"${relativePath}"`, {
1848
+ originalBuffer = execFileSync('git', ['show', `HEAD:${relativePath}`], {
1823
1849
  cwd: gitRoot,
1824
- encoding: 'utf-8',
1825
1850
  maxBuffer: 10 * 1024 * 1024, // 10MB
1826
1851
  });
1827
1852
  }
@@ -1839,11 +1864,22 @@ router.get('/git-original', async (req, res) => {
1839
1864
  }
1840
1865
  throw err;
1841
1866
  }
1867
+ if (looksLikeBinaryBuffer(originalBuffer)) {
1868
+ res.status(415).json({
1869
+ path: filePath,
1870
+ filename: path.basename(filePath),
1871
+ extension: path.extname(filePath).toLowerCase(),
1872
+ binary: true,
1873
+ isNew: false,
1874
+ error: 'Binary file cannot be read as text',
1875
+ });
1876
+ return;
1877
+ }
1842
1878
  res.json({
1843
1879
  path: filePath,
1844
1880
  filename: path.basename(filePath),
1845
1881
  extension: path.extname(filePath).toLowerCase(),
1846
- content: originalContent,
1882
+ content: originalBuffer.toString('utf-8'),
1847
1883
  isNew: false,
1848
1884
  });
1849
1885
  }
@@ -1852,6 +1888,40 @@ router.get('/git-original', async (req, res) => {
1852
1888
  res.status(500).json({ error: err.message });
1853
1889
  }
1854
1890
  });
1891
+ // GET /api/files/git-original-binary - Stream the HEAD version of a binary
1892
+ // file. This keeps deleted image/PDF/STL/FCStd previews useful in the Git modal.
1893
+ router.get('/git-original-binary', async (req, res) => {
1894
+ try {
1895
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
1896
+ if (!resolution.ok) {
1897
+ res.status(resolution.status).json({ error: resolution.error });
1898
+ return;
1899
+ }
1900
+ const filePath = resolution.path;
1901
+ const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
1902
+ cwd: path.dirname(filePath),
1903
+ encoding: 'utf-8',
1904
+ }).trim();
1905
+ const relativePath = path.relative(gitRoot, filePath);
1906
+ if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
1907
+ res.status(400).json({ error: 'Path is outside the git repository' });
1908
+ return;
1909
+ }
1910
+ const buffer = execFileSync('git', ['show', `HEAD:${relativePath}`], {
1911
+ cwd: gitRoot,
1912
+ maxBuffer: 50 * 1024 * 1024,
1913
+ });
1914
+ const extension = path.extname(filePath).toLowerCase();
1915
+ res.setHeader('Content-Type', binaryContentTypeForExtension(extension));
1916
+ res.setHeader('Content-Length', buffer.length);
1917
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
1918
+ res.send(buffer);
1919
+ }
1920
+ catch (err) {
1921
+ log.error(' Failed to get original binary from git:', err);
1922
+ res.status(404).json({ error: 'Original binary file not found' });
1923
+ }
1924
+ });
1855
1925
  // GET /api/files/git-diff - Get unified diff for a file
1856
1926
  router.get('/git-diff', async (req, res) => {
1857
1927
  try {
@@ -3093,6 +3163,10 @@ router.post('/by-path', async (req, res) => {
3093
3163
  '.mp3': 'audio/mpeg',
3094
3164
  '.mp4': 'video/mp4',
3095
3165
  '.wav': 'audio/wav',
3166
+ '.stl': 'model/stl',
3167
+ '.fcstd': 'application/vnd.freecad',
3168
+ '.gcode': 'text/x-gcode',
3169
+ '.gco': 'text/x-gcode',
3096
3170
  };
3097
3171
  const contentType = mimeTypes[extension] || 'application/octet-stream';
3098
3172
  res.setHeader('Content-Type', contentType);
@@ -15,6 +15,7 @@ import { isDevInstall, maybeTriggerWebBundleRebuild } from '../services/web-bund
15
15
  import { deleteClaudeCredentialProfile, getClaudeCredentialProfilesUsage, listClaudeCredentialProfiles, renameClaudeCredentialProfile, saveActiveClaudeCredentialProfile, switchClaudeCredentialProfile, } from '../services/claude-credentials-service.js';
16
16
  import { deleteProviderCredentialProfile, listProviderCredentialProfiles, renameProviderCredentialProfile, saveActiveProviderCredentialProfile, switchProviderCredentialProfile, } from '../services/provider-credentials-service.js';
17
17
  import { getCodexCredentialProfilesUsage } from '../services/codex-usage-service.js';
18
+ import { killDetachedAppServerDaemon } from '../codex/app-server/app-server-process.js';
18
19
  const log = createLogger('SystemRoutes');
19
20
  const router = Router();
20
21
  const PACKAGE_NAME = 'tide-commander';
@@ -609,9 +610,15 @@ function mountProviderCredentialRoutes(provider, basePath) {
609
610
  res.status(400).json({ error: 'stashActiveAs must be a string when provided' });
610
611
  return;
611
612
  }
612
- res.json(switchProviderCredentialProfile(provider, name.trim(), {
613
+ const result = switchProviderCredentialProfile(provider, name.trim(), {
613
614
  stashActiveAs: typeof stashActiveAs === 'string' ? stashActiveAs.trim() : undefined,
614
- }));
615
+ });
616
+ // The detached `codex app-server` daemon caches ~/.codex/auth.json in
617
+ // memory at startup, so without this every Codex agent would keep running
618
+ // on the account the user just switched away from. Killing it makes the
619
+ // next launch spawn a fresh daemon with the new credentials.
620
+ const appServerStopped = provider === 'codex' ? killDetachedAppServerDaemon().stopped : false;
621
+ res.json({ ...result, codexAppServerStopped: appServerStopped });
615
622
  }
616
623
  catch (err) {
617
624
  const message = err.message;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.177.2",
3
+ "version": "1.178.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",
@@ -91,6 +91,7 @@
91
91
  "mssql": "^12.4.0",
92
92
  "multer": "^2.1.1",
93
93
  "mysql2": "^3.16.2",
94
+ "occt-import-js": "^0.0.23",
94
95
  "oracledb": "^6.10.0",
95
96
  "pdfjs-dist": "^5.6.205",
96
97
  "pg": "^8.17.2",