tide-commander 1.70.0 → 1.72.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 (57) hide show
  1. package/dist/assets/{BossLogsModal-BsOGAk9j.js → BossLogsModal-DlpAJubL.js} +1 -1
  2. package/dist/assets/BossSpawnModal-CNFxhhc5.js +1 -0
  3. package/dist/assets/ControlsModal-DaxduzM0.js +1 -0
  4. package/dist/assets/{DockerLogsModal-DOJuVWO5.js → DockerLogsModal-CL1yesIS.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-DSMa-_pv.js → EmbeddedEditor-CPnEchoJ.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-BcXXMAz0.js → GmailOAuthSetup-D-elay-s.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-Dz6GJobg.js → GoogleOAuthSetup-CZlI2XXd.js} +1 -1
  8. package/dist/assets/{IframeModal-JzopEsaR.js → IframeModal-CZIQrQEx.js} +1 -1
  9. package/dist/assets/IntegrationsPanel-Bz6bFSVE.js +2 -0
  10. package/dist/assets/{LogViewerModal-nLCP_OiC.js → LogViewerModal-BqZ7cNOD.js} +1 -1
  11. package/dist/assets/{MonitoringModal-DWAF5qrY.js → MonitoringModal-BLSJCF0o.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-CMjRymzO.js → PM2LogsModal-B2iZEmM3.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-CE6qum43.js → RestoreArchivedAreaModal-BT_7W4MX.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-C_jzVCus.js → Scene2DCanvas-SlszbWjN.js} +1 -1
  15. package/dist/assets/{SceneManager-DJi4cg4u.js → SceneManager-fQ9mY-na.js} +1 -1
  16. package/dist/assets/{SkillsPanel-CICFRA3e.js → SkillsPanel-oWxlFfS4.js} +2 -2
  17. package/dist/assets/{SpawnModal-DbMA6ZCI.js → SpawnModal-BDR39F-T.js} +1 -1
  18. package/dist/assets/SubordinateAssignmentModal-UEenzUT0.js +1 -0
  19. package/dist/assets/TriggerManagerPanel-omMLL6Mi.js +3 -0
  20. package/dist/assets/{WorkflowEditorPanel-CG_rbU3P.js → WorkflowEditorPanel-X403MCNp.js} +1 -1
  21. package/dist/assets/index-BoMGSybR.js +1 -0
  22. package/dist/assets/{index-CG9ZWXHv.js → index-BvX7U4GZ.js} +4 -4
  23. package/dist/assets/index-C-dhn0OI.js +2 -0
  24. package/dist/assets/index-C2Fyjj0L.css +1 -0
  25. package/dist/assets/{index-COtpp3Go.js → index-CTz_qHJG.js} +2 -2
  26. package/dist/assets/{index-BjRsxWRo.js → index-CUbWwZvL.js} +6 -6
  27. package/dist/assets/{index-BTg-aafr.js → index-DLtcDXEt.js} +1 -1
  28. package/dist/assets/index-Dkx8tXJi.js +1 -0
  29. package/dist/assets/{index-DL5oZ7JS.js → index-DmmNbFv1.js} +1 -1
  30. package/dist/assets/{index-AbUJ8e_m.js → index-Dy4hZ0Me.js} +3 -3
  31. package/dist/assets/main-DN-PQyw6.css +1 -0
  32. package/dist/assets/main-mSA6_Yi8.js +200 -0
  33. package/dist/assets/{web-C_fa0QQi.js → web-6zI01vYP.js} +1 -1
  34. package/dist/assets/{web-xu-du3ss.js → web-DEhWQfjr.js} +1 -1
  35. package/dist/index.html +2 -2
  36. package/dist/locales/en/config.json +6 -1
  37. package/dist/locales/en/terminal.json +4 -1
  38. package/dist/src/packages/server/claude/runner/process-lifecycle.js +2 -0
  39. package/dist/src/packages/server/claude/runner/tmux-helper.js +127 -21
  40. package/dist/src/packages/server/claude/runner/watchdog.js +18 -3
  41. package/dist/src/packages/server/claude/runner.js +17 -0
  42. package/dist/src/packages/server/codex/backend.js +12 -8
  43. package/dist/src/packages/server/routes/agents.js +8 -1
  44. package/dist/src/packages/server/services/cron-service.js +49 -0
  45. package/dist/src/packages/server/services/trigger-service.js +65 -0
  46. package/package.json +1 -1
  47. package/dist/assets/BossSpawnModal-BiQgeh5g.js +0 -1
  48. package/dist/assets/ControlsModal-rRm2EyLt.js +0 -1
  49. package/dist/assets/IntegrationsPanel-Csc5cKbJ.js +0 -2
  50. package/dist/assets/SubordinateAssignmentModal-S5pOdh3o.js +0 -1
  51. package/dist/assets/TriggerManagerPanel-_2o_eFr0.js +0 -3
  52. package/dist/assets/index-4VITMkmG.css +0 -1
  53. package/dist/assets/index-BM1t8tTI.js +0 -2
  54. package/dist/assets/index-LOZw5AOm.js +0 -1
  55. package/dist/assets/index-XfOLLSJr.js +0 -1
  56. package/dist/assets/main-BXDdkAuE.css +0 -1
  57. package/dist/assets/main-C-iXDssE.js +0 -200
@@ -1 +1 @@
1
- import{b$ as s}from"./main-C-iXDssE.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
1
+ import{c1 as s}from"./main-mSA6_Yi8.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
@@ -1 +1 @@
1
- import{b$ as a}from"./main-C-iXDssE.js";import{ImpactStyle as i,NotificationType as r}from"./index-COtpp3Go.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
1
+ import{c1 as a}from"./main-mSA6_Yi8.js";import{ImpactStyle as i,NotificationType as r}from"./index-CTz_qHJG.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
package/dist/index.html CHANGED
@@ -22,10 +22,10 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-C-iXDssE.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-mSA6_Yi8.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
28
- <link rel="stylesheet" crossorigin href="/assets/main-BXDdkAuE.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-DN-PQyw6.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -68,7 +68,12 @@
68
68
  "hideToken": "Hide token",
69
69
  "codexBinaryPath": "Codex Binary Path",
70
70
  "codexBinaryPathPlaceholder": "Leave empty for auto-detect",
71
- "codexBinaryPathHint": "Path to the native Codex binary. Bypasses the Node.js wrapper for better session persistence."
71
+ "codexBinaryPathHint": "Path to the native Codex binary. Bypasses the Node.js wrapper for better session persistence.",
72
+ "connectScreen": {
73
+ "authTokenLabel": "Auth Token",
74
+ "authTokenPlaceholder": "X-Auth-Token (optional)",
75
+ "authTokenHint": "Optional. Required if your server enforces auth."
76
+ }
72
77
  },
73
78
  "scene": {
74
79
  "characterSize": "Character Size",
@@ -220,7 +220,9 @@
220
220
  "approvalsUntrusted": "Approvals: untrusted",
221
221
  "approvalsOnFailure": "Approvals: on-failure",
222
222
  "approvalsOnRequest": "Approvals: on-request",
223
- "approvalsNever": "Approvals: never"
223
+ "approvalsNever": "Approvals: never",
224
+ "reasoningEffort": "Reasoning Effort",
225
+ "reasoningEffortDefault": "Reasoning Effort: default"
224
226
  }
225
227
  },
226
228
  "overview": {
@@ -247,6 +249,7 @@
247
249
  "clickToSwitch": "Click to switch to this agent",
248
250
  "sameAreaOnly": "Same Area",
249
251
  "subagentsCompleted": "{{count}} subagents (all completed)",
252
+ "hasDraft": "Has unsent draft",
250
253
  "statusLabels": {
251
254
  "working": "Working",
252
255
  "idle": "Idle",
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
+ import * as path from 'path';
2
3
  import { StringDecoder } from 'string_decoder';
3
4
  import { createLogger } from '../../utils/logger.js';
4
5
  import { isTmuxEnabled, checkTmuxAvailability, spawnInTmux, killTmuxSession, interruptTmuxSession, } from './tmux-helper.js';
@@ -82,6 +83,7 @@ export class RunnerProcessLifecycle {
82
83
  turnState: 'processing',
83
84
  tmuxSession: tmuxResult.sessionName,
84
85
  tmuxLogFile: tmuxResult.logFile,
86
+ tmuxExpectedCommand: path.basename(executable),
85
87
  };
86
88
  this.activeProcesses.set(agentId, activeProcess);
87
89
  // Use file-tailing stdout pipeline for tmux mode
@@ -12,6 +12,7 @@ import { execSync, spawn } from 'child_process';
12
12
  import * as fs from 'fs';
13
13
  import * as os from 'os';
14
14
  import * as path from 'path';
15
+ import { StringDecoder } from 'string_decoder';
15
16
  import { createLogger } from '../../utils/logger.js';
16
17
  import { isTmuxModeEnabled } from '../../services/system-prompt-service.js';
17
18
  const log = createLogger('Tmux');
@@ -73,6 +74,14 @@ export function spawnInTmux(executable, args, options) {
73
74
  // Stderr goes to its own file for debugging.
74
75
  // Stdin remains connected to the tmux pane for send-keys input.
75
76
  const escapedArgs = args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
77
+ // `stty raw -echo` puts the pane PTY into raw mode before any reader runs.
78
+ // Why: the second `cat` in `(cat <file>; cat) | claude …` reads from the
79
+ // pane PTY, which is in canonical mode by default. Linux's n_tty driver
80
+ // truncates any line longer than ~4096 bytes (N_TTY_BUF_SIZE) in canonical
81
+ // mode, silently corrupting long stream-json messages and killing the CLI
82
+ // with "Error parsing streaming input line: Unterminated string". Raw mode
83
+ // disables the line-discipline buffer so bytes flow through unchanged.
84
+ const sttyPrefix = 'stty raw -echo 2>/dev/null; ';
76
85
  let fullCmd;
77
86
  if (options.initialStdin) {
78
87
  const initialStdinFile = path.join(os.tmpdir(), `tc-initial-${options.agentId}.tmp`);
@@ -80,16 +89,16 @@ export function spawnInTmux(executable, args, options) {
80
89
  if (options.closeStdinAfterPrompt) {
81
90
  // Pipe the prompt then close stdin (EOF). For one-shot backends
82
91
  // like opencode that need EOF to start processing.
83
- fullCmd = `cat '${initialStdinFile}' | ${executable} ${escapedArgs} > '${logFile}' 2> '${stderrFile}'`;
92
+ fullCmd = `${sttyPrefix}cat '${initialStdinFile}' | ${executable} ${escapedArgs} > '${logFile}' 2> '${stderrFile}'`;
84
93
  }
85
94
  else {
86
95
  // Pipe the prompt then keep stdin open via the tmux pane pty.
87
96
  // The second `cat` reads from the pane so sendToTmux() still works.
88
- fullCmd = `(cat '${initialStdinFile}'; cat) | ${executable} ${escapedArgs} > '${logFile}' 2> '${stderrFile}'`;
97
+ fullCmd = `${sttyPrefix}(cat '${initialStdinFile}'; cat) | ${executable} ${escapedArgs} > '${logFile}' 2> '${stderrFile}'`;
89
98
  }
90
99
  }
91
100
  else {
92
- fullCmd = `${executable} ${escapedArgs} > '${logFile}' 2> '${stderrFile}'`;
101
+ fullCmd = `${sttyPrefix}${executable} ${escapedArgs} > '${logFile}' 2> '${stderrFile}'`;
93
102
  }
94
103
  // Spawn the tmux session
95
104
  const launcherProcess = spawn('tmux', [
@@ -120,10 +129,15 @@ export function sendToTmux(agentId, text) {
120
129
  const sessionName = tmuxSessionName(agentId);
121
130
  try {
122
131
  // Write the text to a temp file and use load-buffer + paste-buffer
123
- // to avoid shell escaping issues with send-keys
132
+ // to avoid shell escaping issues with send-keys.
133
+ // Per-agent buffer name avoids cross-agent races on concurrent sends.
134
+ // `-r` preserves LF (without it tmux translates LF→CR; the receiving
135
+ // pane is in raw mode now so a CR would not be re-translated to LF and
136
+ // claude's stream-json line terminator would be wrong).
124
137
  const tmpFile = path.join(os.tmpdir(), `tc-stdin-${agentId}.tmp`);
138
+ const bufferName = `tc-input-${agentId}`;
125
139
  fs.writeFileSync(tmpFile, text + '\n');
126
- execSync(`tmux load-buffer -b tc-input ${tmpFile} && tmux paste-buffer -b tc-input -t ${sessionName} -d`, { stdio: 'ignore', timeout: 5000 });
140
+ execSync(`tmux load-buffer -b ${bufferName} ${tmpFile} && tmux paste-buffer -b ${bufferName} -t ${sessionName} -r -d`, { stdio: 'ignore', timeout: 5000 });
127
141
  fs.unlinkSync(tmpFile);
128
142
  return true;
129
143
  }
@@ -182,32 +196,70 @@ export function interruptTmuxSession(agentId) {
182
196
  }
183
197
  /**
184
198
  * Create a file tailer that reads new lines appended to a log file.
185
- * Uses `fs.watchFile` (polling) for reliability with pipe-pane output.
199
+ * Uses polling (`setInterval` + `fs.statSync`) for reliability with pipe-pane
200
+ * output. Keeps a partial-line buffer across polls so long JSON events that
201
+ * span multiple ticks (common with codex — single lines can be >20KB) are
202
+ * delivered to `onLine` as complete lines, never mid-line fragments.
186
203
  */
187
204
  export function createFileTailer(logFile, onLine) {
188
205
  let offset = 0;
189
206
  let watching = false;
190
207
  let pollInterval = null;
208
+ // Buffer for bytes that arrive before the terminating '\n'. Flushed to
209
+ // `onLine` only when a newline is seen. `StringDecoder` handles multibyte
210
+ // UTF-8 sequences that straddle a chunk boundary.
211
+ const decoder = new StringDecoder('utf8');
212
+ let partialLine = '';
191
213
  function readNewData() {
214
+ let stat;
215
+ try {
216
+ stat = fs.statSync(logFile);
217
+ }
218
+ catch {
219
+ return; // file may not exist yet or be temporarily unavailable
220
+ }
221
+ // Truncation recovery: if the file shrank (rotated, truncated, or the
222
+ // agent was restarted into a fresh log), reset to the start so we don't
223
+ // stall forever waiting for size to grow past a stale offset.
224
+ if (stat.size < offset) {
225
+ offset = 0;
226
+ partialLine = '';
227
+ }
228
+ if (stat.size <= offset)
229
+ return;
230
+ let fd;
231
+ try {
232
+ fd = fs.openSync(logFile, 'r');
233
+ }
234
+ catch {
235
+ return;
236
+ }
237
+ const buf = Buffer.alloc(stat.size - offset);
192
238
  try {
193
- const stat = fs.statSync(logFile);
194
- if (stat.size <= offset)
195
- return;
196
- const fd = fs.openSync(logFile, 'r');
197
- const buf = Buffer.alloc(stat.size - offset);
198
239
  fs.readSync(fd, buf, 0, buf.length, offset);
199
- fs.closeSync(fd);
200
- offset = stat.size;
201
- const text = buf.toString('utf8');
202
- const lines = text.split('\n');
203
- for (const line of lines) {
204
- if (line.trim()) {
205
- onLine(line);
206
- }
207
- }
208
240
  }
209
241
  catch {
210
- // file may not exist yet or be temporarily unavailable
242
+ try {
243
+ fs.closeSync(fd);
244
+ }
245
+ catch { /* ignore */ }
246
+ return;
247
+ }
248
+ try {
249
+ fs.closeSync(fd);
250
+ }
251
+ catch { /* ignore */ }
252
+ offset = stat.size;
253
+ const text = decoder.write(buf);
254
+ if (!text)
255
+ return;
256
+ const combined = partialLine + text;
257
+ const lines = combined.split('\n');
258
+ partialLine = lines.pop() ?? '';
259
+ for (const line of lines) {
260
+ if (line.trim()) {
261
+ onLine(line);
262
+ }
211
263
  }
212
264
  }
213
265
  return {
@@ -226,12 +278,20 @@ export function createFileTailer(logFile, onLine) {
226
278
  clearInterval(pollInterval);
227
279
  pollInterval = null;
228
280
  }
281
+ // Flush any bytes left in the decoder; drop the trailing partial line
282
+ // (it will be re-read if the tailer is restarted from a saved offset).
283
+ decoder.end();
284
+ partialLine = '';
229
285
  },
230
286
  getOffset() {
231
287
  return offset;
232
288
  },
233
289
  setOffset(newOffset) {
234
290
  offset = newOffset;
291
+ // Offset was repositioned externally — drop any buffered bytes that
292
+ // belong to the old position so we don't concatenate them onto the new
293
+ // starting point.
294
+ partialLine = '';
235
295
  },
236
296
  };
237
297
  }
@@ -250,6 +310,52 @@ export function getTmuxPanePid(agentId) {
250
310
  return undefined;
251
311
  }
252
312
  }
313
+ /**
314
+ * Returns true when the expected CLI binary is alive somewhere in the pane's
315
+ * descendant process tree. Catches the "zombie tmux" case where the CLI died
316
+ * (e.g. claude crashed on a malformed stream-json line) but the wrapping
317
+ * `(cat;cat) | claude` shell pipeline keeps the session alive — in that case
318
+ * `hasTmuxSession` returns true but the agent is silently broken.
319
+ *
320
+ * Returns true on probe failure to avoid false-positive restarts.
321
+ */
322
+ export function isTmuxPaneCommandAlive(agentId, expectedCommand) {
323
+ const panePid = getTmuxPanePid(agentId);
324
+ if (!panePid)
325
+ return false;
326
+ let psOutput;
327
+ try {
328
+ psOutput = execSync('ps -e -o pid=,ppid=,comm=', { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
329
+ }
330
+ catch {
331
+ return true;
332
+ }
333
+ const procs = new Map();
334
+ for (const line of psOutput.split('\n')) {
335
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
336
+ if (!m)
337
+ continue;
338
+ procs.set(parseInt(m[1], 10), { ppid: parseInt(m[2], 10), comm: m[3].trim() });
339
+ }
340
+ const target = expectedCommand.trim();
341
+ // BFS through descendants of panePid
342
+ const queue = [panePid];
343
+ const visited = new Set();
344
+ while (queue.length > 0) {
345
+ const cur = queue.shift();
346
+ if (visited.has(cur))
347
+ continue;
348
+ visited.add(cur);
349
+ const info = procs.get(cur);
350
+ if (info && info.comm === target)
351
+ return true;
352
+ for (const [pid, p] of procs) {
353
+ if (p.ppid === cur && !visited.has(pid))
354
+ queue.push(pid);
355
+ }
356
+ }
357
+ return false;
358
+ }
253
359
  // ---------------------------------------------------------------------------
254
360
  // Internal
255
361
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from '../../utils/logger.js';
2
2
  import { isProcessRunning } from '../../data/index.js';
3
- import { hasTmuxSession } from './tmux-helper.js';
3
+ import { hasTmuxSession, isTmuxPaneCommandAlive } from './tmux-helper.js';
4
4
  const log = createLogger('Runner');
5
5
  const MAX_DEATH_HISTORY = 50;
6
6
  export class RunnerWatchdog {
@@ -21,8 +21,23 @@ export class RunnerWatchdog {
21
21
  for (const [agentId, activeProcess] of this.activeProcesses) {
22
22
  // tmux mode: the launcher PID exits immediately — check the tmux session instead
23
23
  if (activeProcess.tmuxSession) {
24
- if (!hasTmuxSession(agentId)) {
25
- log.error(`🐕 [WATCHDOG] Agent ${agentId}: tmux session ${activeProcess.tmuxSession} no longer exists!`);
24
+ const sessionAlive = hasTmuxSession(agentId);
25
+ // Zombie-session detection: the wrapping `(cat;cat) | claude` shell
26
+ // pipeline keeps the tmux session alive even when the inner CLI dies
27
+ // (e.g. claude exits on a stream-json parse error but cat is still
28
+ // hung on the pane stdin). hasTmuxSession alone misses this — check
29
+ // that the expected CLI binary is still in the pane's process tree.
30
+ const expected = activeProcess.tmuxExpectedCommand;
31
+ const cliAlive = !sessionAlive
32
+ ? false
33
+ : (expected ? isTmuxPaneCommandAlive(agentId, expected) : true);
34
+ if (!sessionAlive || !cliAlive) {
35
+ if (!sessionAlive) {
36
+ log.error(`🐕 [WATCHDOG] Agent ${agentId}: tmux session ${activeProcess.tmuxSession} no longer exists!`);
37
+ }
38
+ else {
39
+ log.error(`🐕 [WATCHDOG] Agent ${agentId}: tmux session ${activeProcess.tmuxSession} is alive but CLI '${expected}' is gone (zombie session)`);
40
+ }
26
41
  activeProcess.tmuxTailer?.stop();
27
42
  this.recordDeath({
28
43
  agentId,
@@ -137,6 +137,23 @@ export class ClaudeRunner {
137
137
  // messages because the tmux launcher PID doesn't emit 'close' events —
138
138
  // the watchdog is the only signal we get when the session dies.
139
139
  const queuedCount = this.messageQueue.get(agentId)?.length ?? 0;
140
+ // Surface mid-turn deaths in the UI. A waiting_for_input death is the
141
+ // expected end-of-turn exit for stdin-closed backends and shouldn't
142
+ // alarm the user. Anything else is a real failure worth showing as
143
+ // `status=error` — if recovery succeeds the next 'init' event flips it
144
+ // back to 'working' automatically.
145
+ const wasMidTurn = activeProcess.turnState !== 'waiting_for_input';
146
+ if (wasMidTurn) {
147
+ const errorReason = activeProcess.tmuxSession
148
+ ? `Process died unexpectedly inside tmux session ${activeProcess.tmuxSession}`
149
+ : 'Process died unexpectedly';
150
+ agentService.updateAgent(agentId, {
151
+ status: 'error',
152
+ currentTask: `[runtime] ${errorReason} — attempting recovery`,
153
+ currentTool: undefined,
154
+ });
155
+ this.callbacks.onError(agentId, errorReason);
156
+ }
140
157
  if (activeProcess.turnState === 'waiting_for_input'
141
158
  && queuedCount > 0
142
159
  && activeProcess.lastRequest) {
@@ -120,15 +120,18 @@ export class CodexBackend {
120
120
  return null;
121
121
  }
122
122
  getExecutablePath() {
123
- // Priority: 1) CODEX_BINARY env var 2) Settings UI 3) auto-detect
124
- const envBinary = process.env.CODEX_BINARY;
125
- if (envBinary && fs.existsSync(envBinary)) {
126
- return envBinary;
127
- }
123
+ // Priority: 1) Settings UI 2) CODEX_BINARY env var 3) auto-detect
124
+ // The Settings UI value is an explicit user override and must win over
125
+ // the env var default so the user can point Tide Commander at a specific
126
+ // codex install without clearing their shell environment.
128
127
  const settingsBinary = getCodexBinaryPath();
129
128
  if (settingsBinary && fs.existsSync(settingsBinary)) {
130
129
  return settingsBinary;
131
130
  }
131
+ const envBinary = process.env.CODEX_BINARY;
132
+ if (envBinary && fs.existsSync(envBinary)) {
133
+ return envBinary;
134
+ }
132
135
  return this.detectInstallation() || 'codex';
133
136
  }
134
137
  detectInstallation() {
@@ -154,10 +157,11 @@ export class CodexBackend {
154
157
  getExtraEnv() {
155
158
  // When using the native binary directly, we need to add
156
159
  // the vendor path directory to PATH so codex can find bundled tools like rg.
157
- const envBinary = process.env.CODEX_BINARY || getCodexBinaryPath();
158
- if (!envBinary)
160
+ // Match the precedence used by getExecutablePath(): Settings UI wins over env var.
161
+ const binary = getCodexBinaryPath() || process.env.CODEX_BINARY;
162
+ if (!binary)
159
163
  return {};
160
- const codexDir = path.dirname(envBinary); // .../codex/
164
+ const codexDir = path.dirname(binary); // .../codex/
161
165
  const archRoot = path.dirname(codexDir); // .../x86_64-unknown-linux-musl/
162
166
  const pathDir = path.join(archRoot, 'path');
163
167
  if (fs.existsSync(pathDir)) {
@@ -13,6 +13,7 @@ import { loadSession } from '../claude/session-loader.js';
13
13
  import { getAllCustomClasses } from '../services/custom-class-service.js';
14
14
  // Session listing is done inline for performance
15
15
  import { createLogger } from '../utils/logger.js';
16
+ import { truncateOrEmpty } from '../utils/string.js';
16
17
  import { buildCustomAgentConfig } from '../websocket/handlers/command-handler.js';
17
18
  import { clearDelegation, getBossForSubordinate } from '../websocket/handlers/boss-response-handler.js';
18
19
  import { OpencodeBackend } from '../opencode/backend.js';
@@ -883,7 +884,13 @@ router.post('/:id/report-task', async (req, res) => {
883
884
  // If the boss agent is no longer available, accept the report without failing.
884
885
  let forwarded = false;
885
886
  if (bossAgent) {
886
- const reportMessage = `[TASK REPORT from ${agent.name} (${subordinateId})]\n\nStatus: ${taskStatus === 'completed' ? 'COMPLETED' : 'FAILED'}\nOriginal task: ${taskDescription}\n${summary ? `\nSummary: ${summary}` : ''}\n\nYou may review the result, give follow-up instructions, or dismiss this agent's progress indicator.`;
887
+ // Truncate the original task description: the boss already issued the
888
+ // delegation and has it in its own conversation history; replaying it
889
+ // verbatim wastes tokens and can blow past line-length limits in the
890
+ // CLI's stream-json input. A short label is enough to disambiguate
891
+ // which delegation this report refers to.
892
+ const taskLabel = truncateOrEmpty(taskDescription, 160);
893
+ const reportMessage = `[TASK REPORT from ${agent.name} (${subordinateId})]\n\nStatus: ${taskStatus === 'completed' ? 'COMPLETED' : 'FAILED'}\nOriginal task: ${taskLabel}\n${summary ? `\nSummary: ${summary}` : ''}\n\nYou may review the result, give follow-up instructions, or dismiss this agent's progress indicator.`;
887
894
  try {
888
895
  if (bossAgent.isBoss || bossAgent.class === 'boss') {
889
896
  const { message: bossMessage, systemPrompt } = await bossMessageService.buildBossMessage(resolvedBossId, reportMessage);
@@ -132,6 +132,55 @@ function getDateInTimezone(timezone) {
132
132
  }
133
133
  // ─── Public API ───
134
134
  const CHECK_INTERVAL_MS = 30_000; // Check every 30 seconds
135
+ // Max safe setTimeout delay (2^31-1 ms ≈ 24.8 days). Longer waits are chained.
136
+ const MAX_SETTIMEOUT_MS = 2_147_483_647;
137
+ /**
138
+ * Schedule a callback to fire exactly once at the given absolute datetime.
139
+ * If the time is already in the past, the callback is fired immediately (next tick).
140
+ * Returns a CronJob-compatible handle that can be stopped via `stop(job)`.
141
+ */
142
+ export function scheduleOnce(runAtIso, callback) {
143
+ const id = `once_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
144
+ const runAtMs = new Date(runAtIso).getTime();
145
+ const job = {
146
+ id,
147
+ expression: `@once ${runAtIso}`,
148
+ timezone: 'UTC',
149
+ callback,
150
+ timer: null,
151
+ lastFired: null,
152
+ };
153
+ const fire = (reason) => {
154
+ job.lastFired = Date.now();
155
+ log.log(`One-shot cron ${id} fired (runAt=${runAtIso}, ${reason})`);
156
+ activeJobs.delete(id);
157
+ try {
158
+ callback();
159
+ }
160
+ catch (err) {
161
+ log.error(`One-shot cron ${id} callback error:`, err);
162
+ }
163
+ };
164
+ const armTimer = () => {
165
+ const remaining = runAtMs - Date.now();
166
+ // Always schedule via setTimeout (delay clamped to >= 0) so the callback
167
+ // never runs synchronously within scheduleOnce — callers often update state
168
+ // after this returns, and a sync callback would race that.
169
+ const delay = Math.max(0, Math.min(remaining, MAX_SETTIMEOUT_MS));
170
+ job.timer = setTimeout(() => {
171
+ if (remaining > MAX_SETTIMEOUT_MS) {
172
+ // Chained wait: re-arm for the remaining interval
173
+ armTimer();
174
+ return;
175
+ }
176
+ fire('scheduled');
177
+ }, delay);
178
+ };
179
+ armTimer();
180
+ activeJobs.set(id, job);
181
+ log.log(`Scheduled one-shot cron ${id} for ${runAtIso}`);
182
+ return job;
183
+ }
135
184
  export function schedule(expression, timezone, callback) {
136
185
  const id = `cron_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
137
186
  const job = {
@@ -615,9 +615,15 @@ function interpolateTemplate(template, variables) {
615
615
  });
616
616
  }
617
617
  // ─── Cron Management ───
618
+ // Grace window for firing a one-shot whose runAt slipped past while the server was down.
619
+ const ONE_SHOT_MISS_GRACE_MS = 5 * 60 * 1000;
618
620
  function startCronJob(trigger) {
619
621
  // Stop existing job if any
620
622
  stopCronJob(trigger.id);
623
+ if (trigger.config.runOnce) {
624
+ startOneShotJob(trigger);
625
+ return;
626
+ }
621
627
  const job = cronService.schedule(trigger.config.expression, trigger.config.timezone, () => {
622
628
  const variables = {
623
629
  'cron.expression': trigger.config.expression,
@@ -629,6 +635,65 @@ function startCronJob(trigger) {
629
635
  cronJobs.set(trigger.id, job);
630
636
  log.log(`Started cron job for trigger ${trigger.name}: ${trigger.config.expression}`);
631
637
  }
638
+ function startOneShotJob(trigger) {
639
+ const runAt = trigger.config.runAt;
640
+ if (!runAt) {
641
+ log.warn(`One-shot trigger ${trigger.name} has no runAt; skipping schedule`);
642
+ return;
643
+ }
644
+ // Already completed — do not re-arm.
645
+ if (trigger.config.completedAt) {
646
+ log.log(`One-shot trigger ${trigger.name} already completed at ${new Date(trigger.config.completedAt).toISOString()}`);
647
+ return;
648
+ }
649
+ const runAtMs = new Date(runAt).getTime();
650
+ if (Number.isNaN(runAtMs)) {
651
+ log.error(`One-shot trigger ${trigger.name} has invalid runAt: ${runAt}`);
652
+ return;
653
+ }
654
+ const now = Date.now();
655
+ const overdueMs = now - runAtMs;
656
+ if (overdueMs > ONE_SHOT_MISS_GRACE_MS) {
657
+ // Missed by more than the grace window — mark missed and disable.
658
+ log.warn(`One-shot trigger ${trigger.name} missed (overdue by ${Math.round(overdueMs / 1000)}s); marking as missed`);
659
+ const updatedConfig = { ...trigger.config, missedAt: now };
660
+ updateTrigger(trigger.id, {
661
+ enabled: false,
662
+ status: 'error',
663
+ lastError: 'missed: server was down at runAt',
664
+ config: updatedConfig,
665
+ });
666
+ return;
667
+ }
668
+ const job = cronService.scheduleOnce(runAt, () => {
669
+ const variables = {
670
+ 'cron.runAt': runAt,
671
+ 'cron.scheduledAt': new Date().toISOString(),
672
+ ...(trigger.config.payload || {}),
673
+ };
674
+ void handleOneShotFire(trigger.id, variables);
675
+ });
676
+ cronJobs.set(trigger.id, job);
677
+ log.log(`Armed one-shot trigger ${trigger.name} for ${runAt}${overdueMs > 0 ? ' (within grace window, firing immediately)' : ''}`);
678
+ }
679
+ async function handleOneShotFire(triggerId, variables) {
680
+ const trigger = triggers.get(triggerId);
681
+ if (!trigger || trigger.type !== 'cron')
682
+ return;
683
+ const cronTrigger = trigger;
684
+ if (!cronTrigger.config.runOnce)
685
+ return;
686
+ // Mark completed + disabled BEFORE firing. fireTrigger internally calls
687
+ // updateTrigger which would otherwise re-enter startCronJob and re-arm the
688
+ // one-shot. The completedAt guard in startOneShotJob short-circuits that.
689
+ const updatedConfig = { ...cronTrigger.config, completedAt: Date.now() };
690
+ updateTrigger(triggerId, {
691
+ enabled: false,
692
+ config: updatedConfig,
693
+ });
694
+ cronJobs.delete(triggerId);
695
+ await fireTrigger(triggerId, variables);
696
+ }
632
697
  function stopCronJob(triggerId) {
633
698
  const job = cronJobs.get(triggerId);
634
699
  if (job) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.70.0",
3
+ "version": "1.72.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- import{u as Ee,a4 as Ie,as as De,ar as _e,r as t,C as Be,S as ae,l as Re,av as z,A as ne,aE as O,s as T,aF as j,j as s,N as S,ax as ze,az as Oe,I as L,aA as F,ac as g,aa as E,E as Te}from"./main-C-iXDssE.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";function le(u){const h=j.filter(C=>!u.has(`Boss ${C}`));return h.length===0?`Boss ${j[Math.floor(Math.random()*j.length)]}-${Date.now()%1e3}`:`Boss ${h[Math.floor(Math.random()*h.length)]}`}function Ue({isOpen:u,onClose:h,onSpawnStart:C,onSpawnEnd:I,spawnPosition:te}){const{t:n}=Ee(["terminal","common"]),P=Ie(),o=De(),k=_e(),[r,p]=t.useState(""),[M,ie]=t.useState(()=>Be(ae.LAST_CWD)),[c,D]=t.useState("boss"),[q,U]=t.useState(!1),[ce,y]=t.useState(!1),[W,oe]=t.useState(!0),[G,re]=t.useState("bypass"),[d,_]=t.useState("claude"),[b,N]=t.useState({fullAuto:!0,sandbox:"workspace-write",approvalMode:"on-request",search:!1}),[K,de]=t.useState("opus[1m]"),[H,pe]=t.useState("gpt-5.3-codex"),[v,A]=t.useState(new Set),[J,B]=t.useState(new Set),[m,me]=t.useState(""),[f,ue]=t.useState(""),[V,he]=t.useState(""),$=t.useRef(null),R=t.useRef(!1),X=t.useRef(!1),w=t.useMemo(()=>k.filter(e=>e.enabled),[k]),Y=t.useMemo(()=>{if(!f.trim())return w;const e=f.toLowerCase();return w.filter(a=>a.name.toLowerCase().includes(e)||a.description.toLowerCase().includes(e)||a.slug.toLowerCase().includes(e))},[w,f]),xe=t.useMemo(()=>{var a;const e=o.find(l=>l.id===c);return(a=e==null?void 0:e.defaultSkillIds)!=null&&a.length?k.filter(l=>e.defaultSkillIds.includes(l.id)):[]},[o,c,k]),we=["full-notifications","streaming-exec","task-label","report-task-to-boss","send-message-to-agent"];t.useEffect(()=>{if(u&&!X.current&&w.length>0){const a=w.filter(l=>we.includes(l.slug)).map(l=>l.id);a.length>0&&B(new Set(a))}X.current=u},[u,w]);const be=t.useCallback(e=>{B(a=>{const l=new Set(a);return l.has(e)?l.delete(e):l.add(e),l})},[]),i=t.useMemo(()=>o.find(e=>e.id===c),[o,c]),fe=t.useMemo(()=>{if(i!=null&&i.model)return i.model},[i]),je=t.useMemo(()=>{if(i!=null&&i.customModelPath)return Re(`/api/custom-models/${i.id}`)},[i]),Ne=i==null?void 0:i.modelScale,ve=t.useMemo(()=>i?"scout":c==="boss"?"architect":c,[c,i]),Q=t.useMemo(()=>Array.from(P.values()).filter(e=>!e.isBoss&&e.class!=="boss"&&!e.bossId),[P]),Z=t.useMemo(()=>{if(!m.trim())return o;const e=m.toLowerCase();return o.filter(a=>a.name.toLowerCase().includes(e)||a.description.toLowerCase().includes(e)||a.id.toLowerCase().includes(e))},[o,m]),ee=t.useMemo(()=>{if(!m.trim())return z;const e=m.toLowerCase();return z.filter(a=>{const l=ne[a.id];return l?a.name.toLowerCase().includes(e)||a.id.toLowerCase().includes(e)||l.description.toLowerCase().includes(e):!1})},[m]),se=t.useMemo(()=>{if(!m.trim())return!0;const e=m.toLowerCase(),a=O.boss;return"boss".includes(e)||a.description.toLowerCase().includes(e)},[m]);t.useEffect(()=>{if(u&&!R.current){R.current=!0;const e=new Set(Array.from(T.getState().agents.values()).map(l=>l.name)),a=o.find(l=>l.id===c);if(a){const l=j.filter(x=>!e.has(`${a.name} ${x}`));if(l.length===0){const x=j[Math.floor(Math.random()*j.length)];p(`${a.name} ${x}-${Date.now()%1e3}`)}else p(`${a.name} ${l[Math.floor(Math.random()*l.length)]}`)}else p(le(e));A(new Set),$.current&&($.current.focus(),$.current.select())}else u||(R.current=!1)},[u]),t.useEffect(()=>{if(!u)return;const e=o.find(a=>a.id===c);if(e)if(r.startsWith("Boss ")){const a=r.substring(5);p(`${e.name} ${a}`)}else{const a=o.find(l=>r.startsWith(l.name+" "));if(a){const l=r.substring(a.name.length+1);p(`${e.name} ${l}`)}else p(`${e.name} ${r}`)}else{const a=o.find(l=>r.startsWith(l.name+" "));if(a){const l=r.substring(a.name.length+1);p(`Boss ${l}`)}else r.startsWith("Boss ")||p(`Boss ${r}`)}},[c]);const Se=()=>{if(y(!1),!M.trim()){y(!0);return}if(!r.trim()){const l=new Set(Array.from(T.getState().agents.values()).map(x=>x.name));p(le(l));return}Te(ae.LAST_CWD,M),C();const e=V.trim()||void 0,a=Array.from(J);T.spawnBossAgent(r.trim(),c,M.trim(),te||void 0,Array.from(v),d==="claude"?W:!1,G,d,d==="codex"?b:void 0,d==="codex"?H:void 0,d==="claude"?K:void 0,e,a),p(""),A(new Set),B(new Set),I(),h()},ge=()=>{U(!1),p(""),A(new Set),I(),h()},Ce=()=>{U(!1),y(!0),I()};t.useEffect(()=>(window.__bossSpawnModalSuccess=ge,window.__bossSpawnModalError=Ce,()=>{delete window.__bossSpawnModalSuccess,delete window.__bossSpawnModalError}),[r]);const ke=e=>{e.target===e.currentTarget&&h()},Me=e=>{e.key==="Escape"&&h()},ye=e=>{const a=new Set(v);a.has(e)?a.delete(e):a.add(e),A(a)};if(!u)return null;const Ae=O.boss;return s.jsx("div",{className:`modal-overlay ${u?"visible":""}`,onClick:ke,onKeyDown:Me,children:s.jsxs("div",{className:"modal boss-spawn-modal",children:[s.jsxs("div",{className:"modal-header",children:[s.jsx(S,{classId:"boss",size:22,className:"boss-header-icon"}),n("terminal:spawn.deployBossTitle")]}),s.jsxs("div",{className:"modal-body spawn-modal-body",children:[s.jsxs("div",{className:"spawn-top-section",children:[s.jsx("div",{className:"spawn-preview-compact",children:s.jsx(ze,{agentClass:ve,modelFile:fe,customModelUrl:je,modelScale:Ne,width:100,height:120})}),s.jsxs("div",{className:"spawn-class-section",children:[s.jsx("div",{className:"spawn-class-label",children:n("terminal:spawn.bossClass")}),o.length+z.length+1>6&&s.jsx("input",{type:"text",className:"spawn-input class-search-input",placeholder:n("terminal:spawn.filterClasses"),value:m,onChange:e=>me(e.target.value)}),s.jsxs("div",{className:"class-selector-inline",children:[Z.map(e=>s.jsxs("button",{className:`class-chip ${c===e.id?"selected":""}`,onClick:()=>D(e.id),title:e.description,children:[s.jsx(S,{classId:e.id,size:18,className:"class-chip-icon"}),s.jsx("span",{className:"class-chip-name",children:e.name})]},e.id)),se&&s.jsxs("button",{className:`class-chip ${c==="boss"?"selected":""}`,onClick:()=>D("boss"),title:Ae.description,children:[s.jsx(S,{classId:"boss",size:18,className:"class-chip-icon"}),s.jsx("span",{className:"class-chip-name",children:n("terminal:spawn.bossClassName")})]}),ee.map(e=>{const a=ne[e.id];return a?s.jsxs("button",{className:`class-chip ${c===e.id?"selected":""}`,onClick:()=>D(e.id),title:a.description,children:[s.jsx(S,{classId:e.id,size:18,className:"class-chip-icon"}),s.jsx("span",{className:"class-chip-name",children:e.name})]},e.id):null}),m&&Z.length===0&&!se&&ee.length===0&&s.jsx("div",{className:"class-search-empty",children:n("terminal:spawn.noClassesMatch",{query:m})})]})]})]}),s.jsxs("div",{className:"spawn-form-section",children:[s.jsxs("div",{className:"spawn-form-row",children:[s.jsxs("div",{className:"spawn-field",children:[s.jsx("label",{className:"spawn-label",children:n("common:labels.name")}),s.jsx("input",{ref:$,type:"text",className:"spawn-input",placeholder:n("terminal:spawn.bossNamePlaceholder"),value:r,onChange:e=>p(e.target.value)})]}),s.jsxs("div",{className:"spawn-field spawn-field-wide",children:[s.jsx("label",{className:"spawn-label",children:n("terminal:spawn.workingDir")}),s.jsx(Oe,{value:M,onChange:e=>{ie(e),y(!1)},placeholder:n("terminal:spawn.workingDirPlaceholder"),className:"spawn-input",hasError:ce,directoriesOnly:!0})]})]}),s.jsxs("div",{className:"spawn-form-row",children:[s.jsxs("div",{className:"spawn-field",children:[s.jsx("label",{className:"spawn-label",children:n("common:labels.runtime")}),s.jsxs("div",{className:"spawn-select-row",children:[s.jsxs("button",{className:`spawn-select-btn ${d==="claude"?"selected":""}`,onClick:()=>_("claude"),title:n("terminal:spawn.useClaudeCli"),children:[s.jsx("img",{src:"/assets/claude.ico",alt:"Claude",className:"spawn-provider-icon"}),s.jsx("span",{children:"Claude"})]}),s.jsxs("button",{className:`spawn-select-btn ${d==="codex"?"selected":""}`,onClick:()=>_("codex"),title:n("terminal:spawn.useCodexCli"),children:[s.jsx("img",{src:"/assets/codex.ico",alt:"Codex",className:"spawn-provider-icon"}),s.jsx("span",{children:"Codex"})]}),s.jsxs("button",{className:`spawn-select-btn spawn-select-btn--opencode ${d==="opencode"?"selected":""}`,onClick:()=>_("opencode"),title:"Use OpenCode CLI (multi-provider)",children:[s.jsx("span",{children:s.jsx(L,{name:"status-pending",size:14,weight:"fill",color:"#4ade80"})}),s.jsx("span",{children:"OpenCode"})]})]})]}),s.jsxs("div",{className:"spawn-field",children:[s.jsx("label",{className:"spawn-label",children:n("common:labels.permissions")}),s.jsx("div",{className:"spawn-select-row",children:Object.keys(F).map(e=>s.jsxs("button",{className:`spawn-select-btn ${G===e?"selected":""}`,onClick:()=>re(e),title:F[e].description,children:[s.jsx("span",{children:s.jsx(L,{name:e==="bypass"?"bolt":"lock",size:14})}),s.jsx("span",{children:F[e].label})]},e))})]})]}),s.jsxs("div",{className:"spawn-form-row",children:[s.jsxs("div",{className:"spawn-field",children:[s.jsx("label",{className:"spawn-label",children:n("common:labels.model")}),d==="claude"?s.jsx("div",{className:"spawn-select-row",children:Object.keys(g).filter(e=>!g[e].deprecated).map(e=>s.jsxs("button",{className:`spawn-select-btn ${K===e?"selected":""}`,onClick:()=>de(e),title:g[e].description,children:[s.jsx("span",{children:g[e].icon}),s.jsx("span",{children:g[e].label})]},e))}):d==="codex"?s.jsx("div",{className:"spawn-select-row spawn-select-row--codex-models",children:Object.keys(E).map(e=>s.jsxs("button",{className:`spawn-select-btn ${H===e?"selected":""}`,onClick:()=>pe(e),title:E[e].description,children:[s.jsx("span",{children:E[e].icon}),s.jsx("span",{children:E[e].label})]},e))}):d==="opencode"?s.jsx("input",{type:"text",className:"spawn-input",defaultValue:"minimax/MiniMax-M1-80k",placeholder:"provider/model (e.g., minimax/MiniMax-M1-80k)"}):s.jsx("div",{className:"spawn-inline-hint",children:n("terminal:spawn.codex.configuration")})]}),s.jsxs("div",{className:"spawn-field",children:[s.jsx("label",{className:"spawn-label",children:n("common:labels.browser")}),s.jsx("div",{className:"spawn-form-row spawn-options-row",children:s.jsxs("label",{className:"spawn-checkbox",children:[s.jsx("input",{type:"checkbox",checked:W,onChange:e=>oe(e.target.checked),disabled:d!=="claude"}),s.jsx("span",{children:n("terminal:spawn.chromeBrowser")})]})})]})]}),d==="codex"&&s.jsx("div",{className:"spawn-form-row",children:s.jsxs("div",{className:"spawn-field",children:[s.jsx("label",{className:"spawn-label",children:n("terminal:spawn.codex.config")}),s.jsxs("div",{className:"spawn-options-row",style:{display:"flex",flexDirection:"column",gap:8},children:[s.jsxs("label",{className:"spawn-checkbox",children:[s.jsx("input",{type:"checkbox",checked:b.fullAuto!==!1,onChange:e=>N(a=>({...a,fullAuto:e.target.checked}))}),s.jsx("span",{children:n("terminal:spawn.codex.useFullAuto")})]}),s.jsxs("label",{className:"spawn-checkbox",children:[s.jsx("input",{type:"checkbox",checked:!!b.search,onChange:e=>N(a=>({...a,search:e.target.checked}))}),s.jsx("span",{children:n("terminal:spawn.codex.enableSearch")})]}),b.fullAuto===!1&&s.jsxs(s.Fragment,{children:[s.jsxs("select",{className:"spawn-input",value:b.sandbox||"workspace-write",onChange:e=>N(a=>({...a,sandbox:e.target.value})),children:[s.jsx("option",{value:"read-only",children:n("terminal:spawn.codex.sandboxReadOnly")}),s.jsx("option",{value:"workspace-write",children:n("terminal:spawn.codex.sandboxWorkspaceWrite")}),s.jsx("option",{value:"danger-full-access",children:n("terminal:spawn.codex.sandboxDangerFullAccess")})]}),s.jsxs("select",{className:"spawn-input",value:b.approvalMode||"on-request",onChange:e=>N(a=>({...a,approvalMode:e.target.value})),children:[s.jsx("option",{value:"untrusted",children:n("terminal:spawn.codex.approvalsUntrusted")}),s.jsx("option",{value:"on-failure",children:n("terminal:spawn.codex.approvalsOnFailure")}),s.jsx("option",{value:"on-request",children:n("terminal:spawn.codex.approvalsOnRequest")}),s.jsx("option",{value:"never",children:n("terminal:spawn.codex.approvalsNever")})]})]}),s.jsx("input",{type:"text",className:"spawn-input",placeholder:n("terminal:spawn.codex.profileOptional"),value:b.profile||"",onChange:e=>N(a=>({...a,profile:e.target.value||void 0}))})]})]})}),w.length>0&&s.jsxs("div",{className:"spawn-skills-section",children:[s.jsxs("label",{className:"spawn-label",children:[n("terminal:spawn.skills")," ",s.jsxs("span",{className:"spawn-label-hint",children:["(",n("common:labels.optional"),")"]})]}),w.length>6&&s.jsx("input",{type:"text",className:"spawn-input skill-search-input",placeholder:n("terminal:spawn.filterSkills"),value:f,onChange:e=>ue(e.target.value)}),s.jsxs("div",{className:"spawn-skills-inline",children:[Y.map(e=>{const a=J.has(e.id);return xe.some(x=>x.id===e.id)?null:s.jsxs("button",{className:`spawn-skill-chip ${a?"selected":""}`,onClick:()=>be(e.id),title:e.description,children:[a&&s.jsx("span",{className:"spawn-skill-check",children:s.jsx(L,{name:"check",size:12})}),s.jsx("span",{children:e.name}),e.builtin&&s.jsx("span",{className:"spawn-skill-builtin",children:"TC"})]},e.id)}),f&&Y.length===0&&s.jsx("div",{className:"skill-search-empty",children:n("terminal:spawn.noSkillsMatch",{query:f})})]})]}),s.jsxs("div",{className:"spawn-custom-instructions-section",children:[s.jsxs("label",{className:"spawn-label",children:[n("terminal:spawn.customInstructions")," ",s.jsxs("span",{className:"spawn-label-hint",children:["(",n("common:labels.optional"),")"]})]}),s.jsx("textarea",{className:"spawn-input spawn-textarea",placeholder:n("terminal:spawn.customInstructionsBossPlaceholder"),value:V,onChange:e=>he(e.target.value),rows:3})]}),s.jsxs("div",{className:"spawn-subordinates-section",children:[s.jsxs("label",{className:"spawn-label",children:[n("terminal:spawn.initialSubordinates")," ",s.jsxs("span",{className:"spawn-label-hint",children:["(",n("common:labels.optional"),")"]})]}),s.jsx("div",{className:"subordinates-selector-compact",children:Q.length===0?s.jsx("div",{className:"subordinates-empty",children:n("terminal:spawn.noAvailableAgents")}):Q.map(e=>{const a=v.has(e.id),l=O[e.class],x=o.find(Le=>Le.id===e.class),$e=l||x||{color:"#888888"};return s.jsxs("button",{className:`subordinate-chip ${a?"selected":""}`,onClick:()=>ye(e.id),children:[a&&s.jsx("span",{className:"subordinate-check",children:s.jsx(L,{name:"check",size:12})}),s.jsx("span",{className:"subordinate-chip-icon",style:{color:$e.color},children:s.jsx(S,{classId:e.class,size:16})}),s.jsx("span",{className:"subordinate-chip-name",children:e.name})]},e.id)})}),v.size>0&&s.jsxs("div",{className:"subordinates-count",children:[v.size," ",n("common:labels.selected").toLowerCase()]})]})]})]}),s.jsxs("div",{className:"modal-footer",children:[s.jsx("button",{className:"btn btn-secondary",onClick:h,children:n("common:buttons.cancel")}),s.jsx("button",{className:"btn btn-boss",onClick:Se,disabled:q,children:n(q?"common:buttons.deploying":"common:buttons2.deployBoss")})]})]})})}export{Ue as BossSpawnModal};