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.
- package/dist/assets/{BossLogsModal-BsOGAk9j.js → BossLogsModal-DlpAJubL.js} +1 -1
- package/dist/assets/BossSpawnModal-CNFxhhc5.js +1 -0
- package/dist/assets/ControlsModal-DaxduzM0.js +1 -0
- package/dist/assets/{DockerLogsModal-DOJuVWO5.js → DockerLogsModal-CL1yesIS.js} +1 -1
- package/dist/assets/{EmbeddedEditor-DSMa-_pv.js → EmbeddedEditor-CPnEchoJ.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-BcXXMAz0.js → GmailOAuthSetup-D-elay-s.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-Dz6GJobg.js → GoogleOAuthSetup-CZlI2XXd.js} +1 -1
- package/dist/assets/{IframeModal-JzopEsaR.js → IframeModal-CZIQrQEx.js} +1 -1
- package/dist/assets/IntegrationsPanel-Bz6bFSVE.js +2 -0
- package/dist/assets/{LogViewerModal-nLCP_OiC.js → LogViewerModal-BqZ7cNOD.js} +1 -1
- package/dist/assets/{MonitoringModal-DWAF5qrY.js → MonitoringModal-BLSJCF0o.js} +1 -1
- package/dist/assets/{PM2LogsModal-CMjRymzO.js → PM2LogsModal-B2iZEmM3.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CE6qum43.js → RestoreArchivedAreaModal-BT_7W4MX.js} +1 -1
- package/dist/assets/{Scene2DCanvas-C_jzVCus.js → Scene2DCanvas-SlszbWjN.js} +1 -1
- package/dist/assets/{SceneManager-DJi4cg4u.js → SceneManager-fQ9mY-na.js} +1 -1
- package/dist/assets/{SkillsPanel-CICFRA3e.js → SkillsPanel-oWxlFfS4.js} +2 -2
- package/dist/assets/{SpawnModal-DbMA6ZCI.js → SpawnModal-BDR39F-T.js} +1 -1
- package/dist/assets/SubordinateAssignmentModal-UEenzUT0.js +1 -0
- package/dist/assets/TriggerManagerPanel-omMLL6Mi.js +3 -0
- package/dist/assets/{WorkflowEditorPanel-CG_rbU3P.js → WorkflowEditorPanel-X403MCNp.js} +1 -1
- package/dist/assets/index-BoMGSybR.js +1 -0
- package/dist/assets/{index-CG9ZWXHv.js → index-BvX7U4GZ.js} +4 -4
- package/dist/assets/index-C-dhn0OI.js +2 -0
- package/dist/assets/index-C2Fyjj0L.css +1 -0
- package/dist/assets/{index-COtpp3Go.js → index-CTz_qHJG.js} +2 -2
- package/dist/assets/{index-BjRsxWRo.js → index-CUbWwZvL.js} +6 -6
- package/dist/assets/{index-BTg-aafr.js → index-DLtcDXEt.js} +1 -1
- package/dist/assets/index-Dkx8tXJi.js +1 -0
- package/dist/assets/{index-DL5oZ7JS.js → index-DmmNbFv1.js} +1 -1
- package/dist/assets/{index-AbUJ8e_m.js → index-Dy4hZ0Me.js} +3 -3
- package/dist/assets/main-DN-PQyw6.css +1 -0
- package/dist/assets/main-mSA6_Yi8.js +200 -0
- package/dist/assets/{web-C_fa0QQi.js → web-6zI01vYP.js} +1 -1
- package/dist/assets/{web-xu-du3ss.js → web-DEhWQfjr.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/config.json +6 -1
- package/dist/locales/en/terminal.json +4 -1
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +2 -0
- package/dist/src/packages/server/claude/runner/tmux-helper.js +127 -21
- package/dist/src/packages/server/claude/runner/watchdog.js +18 -3
- package/dist/src/packages/server/claude/runner.js +17 -0
- package/dist/src/packages/server/codex/backend.js +12 -8
- package/dist/src/packages/server/routes/agents.js +8 -1
- package/dist/src/packages/server/services/cron-service.js +49 -0
- package/dist/src/packages/server/services/trigger-service.js +65 -0
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-BiQgeh5g.js +0 -1
- package/dist/assets/ControlsModal-rRm2EyLt.js +0 -1
- package/dist/assets/IntegrationsPanel-Csc5cKbJ.js +0 -2
- package/dist/assets/SubordinateAssignmentModal-S5pOdh3o.js +0 -1
- package/dist/assets/TriggerManagerPanel-_2o_eFr0.js +0 -3
- package/dist/assets/index-4VITMkmG.css +0 -1
- package/dist/assets/index-BM1t8tTI.js +0 -2
- package/dist/assets/index-LOZw5AOm.js +0 -1
- package/dist/assets/index-XfOLLSJr.js +0 -1
- package/dist/assets/main-BXDdkAuE.css +0 -1
- package/dist/assets/main-C-iXDssE.js +0 -200
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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{
|
|
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-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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(
|
|
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
|
-
|
|
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 +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};
|