tide-commander 1.162.0 → 1.164.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/CHANGELOG.md +38 -0
- package/dist/assets/{BossLogsModal-DrYF13xp.js → BossLogsModal-lsLcNR35.js} +1 -1
- package/dist/assets/{BossSpawnModal-2ZgbW8Iy.js → BossSpawnModal-WxYBtXP5.js} +1 -1
- package/dist/assets/{ControlsModal-CRdNNTEo.js → ControlsModal-5M6SyKnw.js} +1 -1
- package/dist/assets/{DockerLogsModal-BGPZP9_V.js → DockerLogsModal-D5IPlAoh.js} +1 -1
- package/dist/assets/{EmbeddedEditor-B99bT-bl.js → EmbeddedEditor-B9vgQ8Ig.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-DmMCxdUW.js → GmailOAuthSetup-CVTbR9FZ.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BZqit-mW.js → GoogleOAuthSetup-zAdgAFKW.js} +1 -1
- package/dist/assets/{IframeModal-vMY7XxHP.js → IframeModal-eYlclM2R.js} +1 -1
- package/dist/assets/{IntegrationsPanel-DVmqPRTs.js → IntegrationsPanel-Bv-ZSSJU.js} +2 -2
- package/dist/assets/{LogViewerModal-rYC6IVPV.js → LogViewerModal-CVpFonLI.js} +1 -1
- package/dist/assets/{MonitoringModal-C-QXpZkE.js → MonitoringModal-39qKyt0y.js} +1 -1
- package/dist/assets/{PM2LogsModal-C3A_ohFv.js → PM2LogsModal-BXkii-1F.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-Dx94NzSz.js → RestoreArchivedAreaModal-CEmlAVBn.js} +1 -1
- package/dist/assets/{Scene2DCanvas-BxOqJ0IB.js → Scene2DCanvas-BFNkVJ3k.js} +1 -1
- package/dist/assets/{SceneManager-CxcXfq_u.js → SceneManager-nmTk3bz4.js} +1 -1
- package/dist/assets/{SkillsPanel-Fu5sfG7Q.js → SkillsPanel-D0KXp4DD.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-jnGMesoq.js → SlackMultiInstanceSetup-DAILVwTM.js} +1 -1
- package/dist/assets/{SpawnModal-CGYg4uTC.js → SpawnModal-CQ34dkWV.js} +1 -1
- package/dist/assets/{StatisticsModal-BOV6Juts.js → StatisticsModal-DT8IPgDF.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-Bhgj9FvT.js → SubordinateAssignmentModal-vp8fGpQ6.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Qkw4ajmL.js → TriggerManagerPanel-BdeaMglJ.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-DTIBRGz4.js → WorkflowEditorPanel-Dpoq7d7Q.js} +1 -1
- package/dist/assets/{area-logos-uM2FM0I2.js → area-logos-MPuWTVE6.js} +1 -1
- package/dist/assets/{index-B03BKiDN.js → index-B5Zce39Z.js} +1 -1
- package/dist/assets/{index-BvOgm4yp.js → index-BDmPOuWq.js} +4 -4
- package/dist/assets/index-BsO3a12k.js +2 -0
- package/dist/assets/{index-5tz2e3op.js → index-CRuw0L8S.js} +1 -1
- package/dist/assets/{index-CpQeaxZ7.js → index-D4o6bUBS.js} +2 -2
- package/dist/assets/{index-BvD1v4JH.js → index-DHH9e1Of.js} +1 -1
- package/dist/assets/{index-BFJWNGpT.js → index-DbKWgVZ-.js} +6 -6
- package/dist/assets/index-DiTv1htN.js +1 -0
- package/dist/assets/{index-DzQxMm67.js → index-cjcZzxw3.js} +3 -3
- package/dist/assets/main-B1w4ybci.js +298 -0
- package/dist/assets/main-D5J-fbZv.css +1 -0
- package/dist/assets/{web-NCIGsXXA.js → web-DJKyfwYK.js} +1 -1
- package/dist/assets/{web-ml-7D2g7.js → web-DRZHW7yx.js} +1 -1
- package/dist/assets/{web-ClQyiX7L.js → web-yMkJObs8.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/config.json +5 -0
- package/dist/src/packages/server/claude/runner.js +13 -0
- package/dist/src/packages/server/codex/app-server/app-server-event-adapter.js +8 -2
- package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +50 -0
- package/dist/src/packages/server/codex/json-event-parser.js +8 -0
- package/dist/src/packages/server/opencode/server/opencode-server-runner.js +53 -0
- package/dist/src/packages/server/routes/agents.js +35 -0
- package/dist/src/packages/server/routes/system.js +55 -1
- package/dist/src/packages/server/runtime/claude-runner-router.js +6 -0
- package/dist/src/packages/server/runtime/codex-runner-router.js +12 -0
- package/dist/src/packages/server/runtime/opencode-runner-router.js +12 -0
- package/dist/src/packages/server/services/claude-usage-service.js +58 -6
- package/dist/src/packages/server/services/runtime-command-execution.js +61 -3
- package/dist/src/packages/server/services/runtime-service.js +10 -0
- package/dist/src/packages/server/services/self-update-service.js +44 -0
- package/package.json +1 -1
- package/dist/assets/index-3y5Ajiyr.js +0 -1
- package/dist/assets/index-cA0kPNUq.js +0 -2
- package/dist/assets/main-B-jhtDyZ.css +0 -1
- package/dist/assets/main-rX2ghMIV.js +0 -297
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{dn as t}from"./main-B1w4ybci.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{dn as s}from"./main-B1w4ybci.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{dn as a}from"./main-B1w4ybci.js";import{ImpactStyle as i,NotificationType as r}from"./index-D4o6bUBS.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/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.164.0"}
|
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-B1w4ybci.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-D5J-fbZv.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -596,6 +596,11 @@
|
|
|
596
596
|
"stuckHint": "Taking longer than expected?",
|
|
597
597
|
"reload": "Reload",
|
|
598
598
|
"recheck": "Recheck",
|
|
599
|
+
"gitPullBtn": "Update (git pull)",
|
|
600
|
+
"gitPulling": "Running git pull…",
|
|
601
|
+
"gitPullSuccess": "Updated to v{{version}} via git pull",
|
|
602
|
+
"gitPullConflict": "git pull hit a conflict — resolve it manually in the checkout",
|
|
603
|
+
"gitPullFailed": "git pull failed",
|
|
599
604
|
"updated": "Updated to v{{version}}",
|
|
600
605
|
"viewChangelog": "View changelog",
|
|
601
606
|
"changelogTitle": "What's new in v{{version}}",
|
|
@@ -537,6 +537,19 @@ export class ClaudeRunner {
|
|
|
537
537
|
this.callbacks.onError(agentId, `Failed to deliver queued message: ${err}`);
|
|
538
538
|
});
|
|
539
539
|
}
|
|
540
|
+
getQueuedMessages(agentId) {
|
|
541
|
+
return [...(this.messageQueue.get(agentId) ?? [])];
|
|
542
|
+
}
|
|
543
|
+
removeQueuedMessage(agentId, index, expectedText) {
|
|
544
|
+
const queue = this.messageQueue.get(agentId);
|
|
545
|
+
if (!queue || index < 0 || index >= queue.length || queue[index] !== expectedText)
|
|
546
|
+
return false;
|
|
547
|
+
queue.splice(index, 1);
|
|
548
|
+
if (queue.length === 0)
|
|
549
|
+
this.messageQueue.delete(agentId);
|
|
550
|
+
log.log(`🗑️ [QUEUE] Agent ${agentId}: removed queued message at ${index} (${queue.length} remaining)`);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
540
553
|
/**
|
|
541
554
|
* Reconnect to a live tmux session after a server restart.
|
|
542
555
|
* Creates a minimal ActiveProcess and starts tailing the log file from the saved offset.
|
|
@@ -85,7 +85,10 @@ export class CodexAppServerEventAdapter {
|
|
|
85
85
|
switch (item.type) {
|
|
86
86
|
case 'commandExecution': {
|
|
87
87
|
const command = item.command || '';
|
|
88
|
-
|
|
88
|
+
// item.id lets the client attach the eventual tool_result output to
|
|
89
|
+
// this card live (attachToolResult) instead of waiting for /history.
|
|
90
|
+
const ids = item.id ? { uuid: item.id, toolUseId: item.id } : {};
|
|
91
|
+
return [{ type: 'tool_start', toolName: 'Bash', toolInput: { command, status: item.status }, ...ids }];
|
|
89
92
|
}
|
|
90
93
|
case 'webSearch':
|
|
91
94
|
return [{ type: 'tool_start', toolName: 'web_search', toolInput: this.webSearchInput(item) }];
|
|
@@ -113,7 +116,10 @@ export class CodexAppServerEventAdapter {
|
|
|
113
116
|
case 'commandExecution': {
|
|
114
117
|
const output = item.aggregatedOutput ??
|
|
115
118
|
(item.exitCode !== undefined ? `[exit ${item.exitCode}]` : item.status ? `Command status: ${item.status}` : '');
|
|
116
|
-
|
|
119
|
+
// Same id as the matching tool_start so the client enriches that card
|
|
120
|
+
// in place (attachToolResult) instead of waiting for a /history reload.
|
|
121
|
+
const ids = item.id ? { uuid: item.id, toolUseId: item.id } : {};
|
|
122
|
+
return [{ type: 'tool_result', toolName: 'Bash', toolOutput: output, ...ids }];
|
|
117
123
|
}
|
|
118
124
|
case 'fileChange':
|
|
119
125
|
return this.handleFileChange(item);
|
|
@@ -184,6 +184,18 @@ export class CodexAppServerRunner {
|
|
|
184
184
|
if (event.type === 'text' || event.type === 'tool_start' || event.type === 'thinking' || event.type === 'init') {
|
|
185
185
|
state.turnState = 'processing';
|
|
186
186
|
}
|
|
187
|
+
// A user-requested interrupt ("Send now") can surface as an error event
|
|
188
|
+
// from the aborted turn. The 🛑 system line already tells the user what
|
|
189
|
+
// happened — keep the expected fallout quiet.
|
|
190
|
+
if (state.interruptRequested) {
|
|
191
|
+
if (event.type === 'error' && /interrupt|abort/i.test(event.errorMessage || '')) {
|
|
192
|
+
log.log(`Suppressing expected interrupt error for ${agentId.slice(0, 8)} (user interrupt)`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (event.type === 'step_complete') {
|
|
196
|
+
state.interruptRequested = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
187
199
|
this.pipeline.emitStandardEvent(agentId, event);
|
|
188
200
|
if (event.type === 'step_complete') {
|
|
189
201
|
state.turnState = 'waiting_for_input';
|
|
@@ -371,6 +383,44 @@ export class CodexAppServerRunner {
|
|
|
371
383
|
return true;
|
|
372
384
|
});
|
|
373
385
|
}
|
|
386
|
+
getQueuedMessages(agentId) {
|
|
387
|
+
return [...(this.agents.get(agentId)?.queue ?? [])];
|
|
388
|
+
}
|
|
389
|
+
removeQueuedMessage(agentId, index, expectedText) {
|
|
390
|
+
const queue = this.agents.get(agentId)?.queue;
|
|
391
|
+
if (!queue || index < 0 || index >= queue.length || queue[index] !== expectedText)
|
|
392
|
+
return false;
|
|
393
|
+
queue.splice(index, 1);
|
|
394
|
+
log.log(`🗑️ Removed queued message at ${index} for ${agentId.slice(0, 8)} (${queue.length} remaining)`);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
async interruptTurn(agentId, clearQueue = true) {
|
|
398
|
+
return withAgentContext(agentId, async () => {
|
|
399
|
+
const state = this.agents.get(agentId);
|
|
400
|
+
if (!state)
|
|
401
|
+
return false;
|
|
402
|
+
const proc = this.process;
|
|
403
|
+
if (!proc || !proc.isAlive())
|
|
404
|
+
return false;
|
|
405
|
+
if (state.turnState === 'processing') {
|
|
406
|
+
state.interruptRequested = true;
|
|
407
|
+
try {
|
|
408
|
+
await proc.request('turn/interrupt', { threadId: state.threadId });
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
state.interruptRequested = false;
|
|
412
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
413
|
+
log.warn(`turn/interrupt failed for ${agentId.slice(0, 8)}: ${msg}`);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Clear only after a successful interrupt so a failed request never
|
|
418
|
+
// silently drops queued messages.
|
|
419
|
+
if (clearQueue)
|
|
420
|
+
state.queue.length = 0;
|
|
421
|
+
return true;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
374
424
|
async stop(agentId, _clearQueue = true) {
|
|
375
425
|
await withAgentContext(agentId, async () => {
|
|
376
426
|
const state = this.agents.get(agentId);
|
|
@@ -677,6 +677,9 @@ export class CodexJsonEventParser {
|
|
|
677
677
|
type: 'tool_start',
|
|
678
678
|
toolName,
|
|
679
679
|
toolInput: pureRead ?? this.buildCommandExecutionToolInput(item),
|
|
680
|
+
// item.id lets the client attach the eventual tool_result output to
|
|
681
|
+
// this card live (attachToolResult) instead of waiting for /history.
|
|
682
|
+
...(item.id ? { uuid: item.id, toolUseId: item.id } : {}),
|
|
680
683
|
},
|
|
681
684
|
];
|
|
682
685
|
}
|
|
@@ -754,6 +757,9 @@ export class CodexJsonEventParser {
|
|
|
754
757
|
if (item.id) {
|
|
755
758
|
this.activeToolByItemId.delete(item.id);
|
|
756
759
|
}
|
|
760
|
+
// Same id as the matching tool_start so the client enriches that card
|
|
761
|
+
// in place (attachToolResult) instead of waiting for a /history reload.
|
|
762
|
+
const resultIds = item.id ? { uuid: item.id, toolUseId: item.id } : {};
|
|
757
763
|
// Pure-read commands were mapped to a Read tool at item.started — emit a
|
|
758
764
|
// single Read result and skip the inferred-tool/Bash duplication.
|
|
759
765
|
if (toolName === 'Read') {
|
|
@@ -762,6 +768,7 @@ export class CodexJsonEventParser {
|
|
|
762
768
|
type: 'tool_result',
|
|
763
769
|
toolName: 'Read',
|
|
764
770
|
toolOutput: this.buildCommandExecutionToolOutput(item),
|
|
771
|
+
...resultIds,
|
|
765
772
|
},
|
|
766
773
|
];
|
|
767
774
|
}
|
|
@@ -772,6 +779,7 @@ export class CodexJsonEventParser {
|
|
|
772
779
|
type: 'tool_result',
|
|
773
780
|
toolName,
|
|
774
781
|
toolOutput: this.buildCommandExecutionToolOutput(item),
|
|
782
|
+
...resultIds,
|
|
775
783
|
},
|
|
776
784
|
];
|
|
777
785
|
}
|
|
@@ -154,6 +154,21 @@ export class OpencodeServerRunner {
|
|
|
154
154
|
if (event.type === 'text' || event.type === 'tool_start' || event.type === 'thinking' || event.type === 'init') {
|
|
155
155
|
state.turnState = 'processing';
|
|
156
156
|
}
|
|
157
|
+
// A user-requested interrupt ("Send now") makes the abort look like a
|
|
158
|
+
// failure: session.error "Aborted" + an "(Empty response: …)" dump. The
|
|
159
|
+
// 🛑 system line already tells the user what happened — keep the rest quiet.
|
|
160
|
+
if (state.interruptRequested) {
|
|
161
|
+
if (event.type === 'error' && /abort/i.test(event.errorMessage || '')) {
|
|
162
|
+
log.log(`Suppressing expected abort error for ${agentId.slice(0, 8)} (user interrupt)`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (event.type === 'step_complete') {
|
|
166
|
+
state.interruptRequested = false;
|
|
167
|
+
if (typeof event.resultText === 'string' && event.resultText.startsWith('(Empty response:')) {
|
|
168
|
+
delete event.resultText;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
157
172
|
this.pipeline.emitStandardEvent(agentId, event);
|
|
158
173
|
if (event.type === 'step_complete') {
|
|
159
174
|
state.turnState = 'waiting_for_input';
|
|
@@ -292,6 +307,44 @@ export class OpencodeServerRunner {
|
|
|
292
307
|
return true;
|
|
293
308
|
});
|
|
294
309
|
}
|
|
310
|
+
getQueuedMessages(agentId) {
|
|
311
|
+
return [...(this.agents.get(agentId)?.queue ?? [])];
|
|
312
|
+
}
|
|
313
|
+
removeQueuedMessage(agentId, index, expectedText) {
|
|
314
|
+
const queue = this.agents.get(agentId)?.queue;
|
|
315
|
+
if (!queue || index < 0 || index >= queue.length || queue[index] !== expectedText)
|
|
316
|
+
return false;
|
|
317
|
+
queue.splice(index, 1);
|
|
318
|
+
log.log(`🗑️ Removed queued message at ${index} for ${agentId.slice(0, 8)} (${queue.length} remaining)`);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
async interruptTurn(agentId, clearQueue = true) {
|
|
322
|
+
return withAgentContext(agentId, async () => {
|
|
323
|
+
const state = this.agents.get(agentId);
|
|
324
|
+
if (!state)
|
|
325
|
+
return false;
|
|
326
|
+
const proc = this.process;
|
|
327
|
+
if (!proc || !proc.isAlive())
|
|
328
|
+
return false;
|
|
329
|
+
if (state.turnState === 'processing') {
|
|
330
|
+
state.interruptRequested = true;
|
|
331
|
+
try {
|
|
332
|
+
await proc.abort(state.sessionId);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
state.interruptRequested = false;
|
|
336
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
337
|
+
log.warn(`abort failed for ${agentId.slice(0, 8)}: ${msg}`);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Clear only after a successful abort so a failed request never
|
|
342
|
+
// silently drops queued messages.
|
|
343
|
+
if (clearQueue)
|
|
344
|
+
state.queue.length = 0;
|
|
345
|
+
return true;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
295
348
|
async stop(agentId, _clearQueue = true) {
|
|
296
349
|
await withAgentContext(agentId, async () => {
|
|
297
350
|
const state = this.agents.get(agentId);
|
|
@@ -910,6 +910,41 @@ router.delete('/:id/memory', (req, res) => {
|
|
|
910
910
|
log.log(`Agent ${updated.name} (${updated.id}): memory cleared`);
|
|
911
911
|
res.json({ ok: true, id: updated.id });
|
|
912
912
|
});
|
|
913
|
+
// GET /api/agents/:id/queue - Snapshot of the server-side mid-run message
|
|
914
|
+
// queue (messages awaiting delivery once the agent's current turn ends).
|
|
915
|
+
// Positional: each entry's `index` is only valid against THIS snapshot.
|
|
916
|
+
router.get('/:id/queue', (req, res) => {
|
|
917
|
+
const agent = agentService.getAgent(req.params.id);
|
|
918
|
+
if (!agent) {
|
|
919
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const messages = runtimeService.getQueuedMessagesForAgent(req.params.id);
|
|
923
|
+
res.json({ messages: messages.map((text, index) => ({ index, text })) });
|
|
924
|
+
});
|
|
925
|
+
// DELETE /api/agents/:id/queue/:index - Remove one queued message. Body:
|
|
926
|
+
// { text } — must match the entry at `index` (guards against a queue that
|
|
927
|
+
// drained/mutated since the caller's snapshot; on mismatch → 409, refetch).
|
|
928
|
+
router.delete('/:id/queue/:index', (req, res) => {
|
|
929
|
+
const agent = agentService.getAgent(req.params.id);
|
|
930
|
+
if (!agent) {
|
|
931
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const index = Number.parseInt(req.params.index, 10);
|
|
935
|
+
const { text } = (req.body ?? {});
|
|
936
|
+
if (!Number.isInteger(index) || index < 0 || typeof text !== 'string') {
|
|
937
|
+
res.status(400).json({ error: 'index must be a non-negative integer and text a string' });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const removed = runtimeService.removeQueuedMessageForAgent(req.params.id, index, text);
|
|
941
|
+
if (!removed) {
|
|
942
|
+
res.status(409).json({ error: 'Queue changed since snapshot — refetch', removed: false });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
log.log(`Agent ${agent.name} (${agent.id}): queued message ${index} removed via API`);
|
|
946
|
+
res.json({ removed: true });
|
|
947
|
+
});
|
|
913
948
|
// POST /api/agents/:id/terminal - Start (or reuse) a ttyd attached to the
|
|
914
949
|
// agent's interactive-TUI tmux session (Classic TUI view). Returns the proxy URL.
|
|
915
950
|
router.post('/:id/terminal', async (req, res) => {
|
|
@@ -9,7 +9,7 @@ import * as path from 'path';
|
|
|
9
9
|
import archiver from 'archiver';
|
|
10
10
|
import { createLogger } from '../utils/logger.js';
|
|
11
11
|
import { fetchLatestNpmVersion, getVersionRelation } from '../../shared/version.js';
|
|
12
|
-
import { endUpdate, getInstallInfo, isAutoUpdateSupported, isUpdateInProgress, runNpmGlobalUpdate, schedulePostUpdateRestart, tryBeginUpdate, } from '../services/self-update-service.js';
|
|
12
|
+
import { endUpdate, getInstallInfo, isAutoUpdateSupported, isUpdateInProgress, runGitPull, runNpmGlobalUpdate, schedulePostUpdateRestart, tryBeginUpdate, } from '../services/self-update-service.js';
|
|
13
13
|
import { getAutoUpdateStatus, setAutoUpdateEnabled } from '../services/auto-update-service.js';
|
|
14
14
|
import { isDevInstall, maybeTriggerWebBundleRebuild } from '../services/web-bundle-builder.js';
|
|
15
15
|
import { deleteClaudeCredentialProfile, getClaudeCredentialProfilesUsage, listClaudeCredentialProfiles, renameClaudeCredentialProfile, saveActiveClaudeCredentialProfile, switchClaudeCredentialProfile, } from '../services/claude-credentials-service.js';
|
|
@@ -39,6 +39,7 @@ router.get('/install-info', async (_req, res) => {
|
|
|
39
39
|
latestVersion,
|
|
40
40
|
updateAvailable,
|
|
41
41
|
autoUpdateSupported: isAutoUpdateSupported(info),
|
|
42
|
+
gitPullSupported: isDevInstall(info.installRoot),
|
|
42
43
|
writable: info.writable,
|
|
43
44
|
suggestedManualCommand: info.suggestedManualCommand,
|
|
44
45
|
reason: info.reason,
|
|
@@ -403,6 +404,59 @@ router.post('/self-update', async (_req, res) => {
|
|
|
403
404
|
endUpdate();
|
|
404
405
|
}
|
|
405
406
|
});
|
|
407
|
+
/**
|
|
408
|
+
* POST /api/system/git-pull
|
|
409
|
+
*
|
|
410
|
+
* Dev checkouts only (install root has .git/): updates the running install by
|
|
411
|
+
* executing `git pull --ff-only` in the checkout. A conflict / diverged branch
|
|
412
|
+
* is a NORMAL outcome — git aborts without touching the tree and we return
|
|
413
|
+
* { success: false, conflict: true, error } for the UI to display; only
|
|
414
|
+
* unexpected server errors produce a 5xx. On success the dev tooling picks the
|
|
415
|
+
* new code up by itself (tsx watch restarts the server, the web bundle
|
|
416
|
+
* auto-rebuilds); the client just reloads.
|
|
417
|
+
*/
|
|
418
|
+
router.post('/git-pull', async (_req, res) => {
|
|
419
|
+
const info = getInstallInfo();
|
|
420
|
+
if (!info.installRoot || !isDevInstall(info.installRoot)) {
|
|
421
|
+
res.status(400).json({
|
|
422
|
+
error: 'git pull update is only available when running from a repo checkout (dev mode).',
|
|
423
|
+
reason: info.reason,
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!tryBeginUpdate()) {
|
|
428
|
+
res.status(409).json({ error: 'An update is already in progress.' });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const result = await runGitPull(info.installRoot);
|
|
433
|
+
const combined = `${result.stdout}\n${result.stderr}`.trim();
|
|
434
|
+
if (result.exitCode === 0) {
|
|
435
|
+
const upToDate = /already up.to.date/i.test(result.stdout);
|
|
436
|
+
// Re-read package.json — the pull may have bumped the version.
|
|
437
|
+
const newVersion = getInstallInfo().currentVersion;
|
|
438
|
+
log.log(`git pull succeeded${upToDate ? ' (already up to date)' : ''} — now at v${newVersion}`);
|
|
439
|
+
res.json({ success: true, upToDate, newVersion, output: combined });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// Conflict-shaped failures: diverged history, overlapping local changes,
|
|
443
|
+
// or an unfinished merge. --ff-only aborted cleanly — just report it.
|
|
444
|
+
const conflict = /not possible to fast-forward|divergent branches|would be overwritten by|conflict|needs merge|unmerged/i.test(combined);
|
|
445
|
+
const error = result.timedOut
|
|
446
|
+
? 'git pull timed out.'
|
|
447
|
+
: result.stderr.trim() || `git pull exited with code ${result.exitCode}.`;
|
|
448
|
+
log.warn(`git pull failed (conflict=${conflict}): ${error}`);
|
|
449
|
+
res.json({ success: false, conflict, error, output: combined });
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
const message = err.message;
|
|
453
|
+
log.error(`git pull update failed: ${message}`);
|
|
454
|
+
res.status(500).json({ error: message });
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
endUpdate();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
406
460
|
/**
|
|
407
461
|
* GET /api/system/auto-update — unattended-update scheduler status.
|
|
408
462
|
* POST /api/system/auto-update { enabled } — opt in/out (persisted).
|
|
@@ -97,6 +97,12 @@ export class ClaudeRunnerRouter {
|
|
|
97
97
|
getTurnState(agentId) {
|
|
98
98
|
return this.ownerRunner(agentId).getTurnState?.(agentId);
|
|
99
99
|
}
|
|
100
|
+
getQueuedMessages(agentId) {
|
|
101
|
+
return this.ownerRunner(agentId).getQueuedMessages?.(agentId) ?? [];
|
|
102
|
+
}
|
|
103
|
+
removeQueuedMessage(agentId, index, expectedText) {
|
|
104
|
+
return this.ownerRunner(agentId).removeQueuedMessage?.(agentId, index, expectedText) ?? false;
|
|
105
|
+
}
|
|
100
106
|
/** PID lookup used by runtime-service.getAgentRuntimeProcessInfo. */
|
|
101
107
|
getActiveProcessesState() {
|
|
102
108
|
const headlessState = this.headless.getActiveProcessesState().map((p) => ({ agentId: p.agentId, pid: p.pid }));
|
|
@@ -64,6 +64,18 @@ export class CodexRunnerRouter {
|
|
|
64
64
|
sendMessage(agentId, message) {
|
|
65
65
|
return this.ownerRunner(agentId).sendMessage(agentId, message);
|
|
66
66
|
}
|
|
67
|
+
async interruptTurn(agentId, clearQueue) {
|
|
68
|
+
const owner = this.ownerRunner(agentId);
|
|
69
|
+
// Only the app-server runner can interrupt a turn in place; exec falls
|
|
70
|
+
// back to the caller's stop+respawn path.
|
|
71
|
+
return owner.interruptTurn ? owner.interruptTurn(agentId, clearQueue) : false;
|
|
72
|
+
}
|
|
73
|
+
getQueuedMessages(agentId) {
|
|
74
|
+
return this.ownerRunner(agentId).getQueuedMessages?.(agentId) ?? [];
|
|
75
|
+
}
|
|
76
|
+
removeQueuedMessage(agentId, index, expectedText) {
|
|
77
|
+
return this.ownerRunner(agentId).removeQueuedMessage?.(agentId, index, expectedText) ?? false;
|
|
78
|
+
}
|
|
67
79
|
async stop(agentId, clearQueue) {
|
|
68
80
|
await this.exec.stop(agentId, clearQueue);
|
|
69
81
|
await this.appServer.stop(agentId, clearQueue);
|
|
@@ -55,6 +55,18 @@ export class OpencodeRunnerRouter {
|
|
|
55
55
|
sendMessage(agentId, message) {
|
|
56
56
|
return this.ownerRunner(agentId).sendMessage(agentId, message);
|
|
57
57
|
}
|
|
58
|
+
async interruptTurn(agentId, clearQueue) {
|
|
59
|
+
const owner = this.ownerRunner(agentId);
|
|
60
|
+
// Only the server runner can interrupt a turn in place; `opencode run`
|
|
61
|
+
// falls back to the caller's stop+respawn path.
|
|
62
|
+
return owner.interruptTurn ? owner.interruptTurn(agentId, clearQueue) : false;
|
|
63
|
+
}
|
|
64
|
+
getQueuedMessages(agentId) {
|
|
65
|
+
return this.ownerRunner(agentId).getQueuedMessages?.(agentId) ?? [];
|
|
66
|
+
}
|
|
67
|
+
removeQueuedMessage(agentId, index, expectedText) {
|
|
68
|
+
return this.ownerRunner(agentId).removeQueuedMessage?.(agentId, index, expectedText) ?? false;
|
|
69
|
+
}
|
|
58
70
|
async stop(agentId, clearQueue) {
|
|
59
71
|
await this.run_.stop(agentId, clearQueue);
|
|
60
72
|
await this.server.stop(agentId, clearQueue);
|
|
@@ -291,6 +291,57 @@ function parseRateLimitWindow(value) {
|
|
|
291
291
|
resetsAt: window.resets_at,
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
|
+
// One entry of the endpoint's `limits` array, which carries `percent` where the
|
|
295
|
+
// legacy top-level windows carry `utilization`.
|
|
296
|
+
function parseLimitEntry(entry) {
|
|
297
|
+
if (typeof entry.percent !== 'number' || !Number.isFinite(entry.percent))
|
|
298
|
+
return null;
|
|
299
|
+
if (typeof entry.resets_at !== 'string' || entry.resets_at === '')
|
|
300
|
+
return null;
|
|
301
|
+
return { utilization: entry.percent, resetsAt: entry.resets_at };
|
|
302
|
+
}
|
|
303
|
+
// `scope.model.display_name` for a `weekly_scoped` entry ("Fable", "Opus", ...).
|
|
304
|
+
// Its `id` is null, so the display name is the only model discriminator.
|
|
305
|
+
function parseScopedModelName(entry) {
|
|
306
|
+
const scope = entry.scope;
|
|
307
|
+
const model = scope?.model;
|
|
308
|
+
const name = model?.display_name;
|
|
309
|
+
return typeof name === 'string' && name !== '' ? name.toLowerCase() : null;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Read the gauges out of the endpoint's `limits` array — its current shape, and
|
|
313
|
+
* the only one that still carries the per-model weekly allowances: the legacy
|
|
314
|
+
* top-level `seven_day_opus` / `seven_day_sonnet` keys now come back null even
|
|
315
|
+
* while those gauges are live in the CLI's `/usage` panel.
|
|
316
|
+
*/
|
|
317
|
+
function parseLimitsArray(body) {
|
|
318
|
+
const parsed = {};
|
|
319
|
+
if (!Array.isArray(body.limits))
|
|
320
|
+
return parsed;
|
|
321
|
+
for (const raw of body.limits) {
|
|
322
|
+
if (!raw || typeof raw !== 'object')
|
|
323
|
+
continue;
|
|
324
|
+
const entry = raw;
|
|
325
|
+
const window = parseLimitEntry(entry);
|
|
326
|
+
if (!window)
|
|
327
|
+
continue;
|
|
328
|
+
if (entry.kind === 'session') {
|
|
329
|
+
parsed.fiveHour = window;
|
|
330
|
+
}
|
|
331
|
+
else if (entry.kind === 'weekly_all') {
|
|
332
|
+
parsed.sevenDay = window;
|
|
333
|
+
}
|
|
334
|
+
else if (entry.kind === 'weekly_scoped') {
|
|
335
|
+
// Substring match so a renamed "Claude Fable 5" still lands on its gauge.
|
|
336
|
+
const model = parseScopedModelName(entry);
|
|
337
|
+
if (model?.includes('fable'))
|
|
338
|
+
parsed.sevenDayFable = window;
|
|
339
|
+
else if (model?.includes('opus'))
|
|
340
|
+
parsed.sevenDayOpus = window;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return parsed;
|
|
344
|
+
}
|
|
294
345
|
// `Retry-After` is either delta-seconds or an HTTP date; return ms-from-now.
|
|
295
346
|
function parseRetryAfterMs(headerValue) {
|
|
296
347
|
if (!headerValue)
|
|
@@ -328,14 +379,15 @@ export async function fetchClaudeRateLimitsForToken(accessToken) {
|
|
|
328
379
|
};
|
|
329
380
|
}
|
|
330
381
|
const body = await response.json();
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
const sevenDayFable =
|
|
382
|
+
// `limits[]` wins; the top-level windows are the fallback for older payloads.
|
|
383
|
+
const limits = parseLimitsArray(body);
|
|
384
|
+
const sevenDayFable = limits.sevenDayFable
|
|
385
|
+
?? parseRateLimitWindow(body.seven_day_fable ?? body.seven_day_sonnet);
|
|
334
386
|
return {
|
|
335
387
|
rateLimits: {
|
|
336
|
-
fiveHour: parseRateLimitWindow(body.five_hour),
|
|
337
|
-
sevenDay: parseRateLimitWindow(body.seven_day),
|
|
338
|
-
sevenDayOpus: parseRateLimitWindow(body.seven_day_opus),
|
|
388
|
+
fiveHour: limits.fiveHour ?? parseRateLimitWindow(body.five_hour),
|
|
389
|
+
sevenDay: limits.sevenDay ?? parseRateLimitWindow(body.seven_day),
|
|
390
|
+
sevenDayOpus: limits.sevenDayOpus ?? parseRateLimitWindow(body.seven_day_opus),
|
|
339
391
|
sevenDayFable,
|
|
340
392
|
sevenDaySonnet: sevenDayFable,
|
|
341
393
|
},
|
|
@@ -111,8 +111,9 @@ export function createRuntimeCommandExecution(deps) {
|
|
|
111
111
|
if (opts?.forceInterrupt) {
|
|
112
112
|
log.log(`[sendCommand] Agent ${agentId} (${agent.provider}): forceInterrupt mid-run (turnState=${turnState ?? 'unknown'}) — interrupting and restarting with new prompt`);
|
|
113
113
|
emitOutput(agentId, '🛑 [System] Interrupting current work to process new prompt…', false, undefined, 'system-interrupt-restart');
|
|
114
|
-
// clearQueue=
|
|
115
|
-
|
|
114
|
+
// clearQueue=false: other queued mid-run messages survive the interrupt
|
|
115
|
+
// (visible as chips in the queue bar) and drain after the forced turn.
|
|
116
|
+
await runner.stop(agentId, false);
|
|
116
117
|
agentService.updateAgent(agentId, { taskCount: (agent.taskCount || 0) + 1 });
|
|
117
118
|
await executeCommand(agentId, command, systemPrompt, forceNewSession, customAgent);
|
|
118
119
|
return;
|
|
@@ -145,13 +146,70 @@ export function createRuntimeCommandExecution(deps) {
|
|
|
145
146
|
await executeCommand(agentId, command, systemPrompt, forceNewSession, customAgent);
|
|
146
147
|
return;
|
|
147
148
|
}
|
|
149
|
+
// Persistent-stream backends (Codex app-server, OpenCode serve) keep one
|
|
150
|
+
// daemon alive across turns, so "Send now" cannot stop+respawn like the
|
|
151
|
+
// stdin-closed CLIs above — runner.stop() would abandon the live thread/
|
|
152
|
+
// session. Instead interrupt the in-flight turn in place (clearQueue=true:
|
|
153
|
+
// the user is replacing pending mid-run work) and hand the prompt to the
|
|
154
|
+
// runner queue; it is delivered the moment the aborted turn finalizes.
|
|
155
|
+
if (processRunning &&
|
|
156
|
+
!forceNewSession &&
|
|
157
|
+
opts?.forceInterrupt &&
|
|
158
|
+
runner.interruptTurn &&
|
|
159
|
+
runner.getTurnState?.(agentId) === 'processing') {
|
|
160
|
+
log.log(`[sendCommand] Agent ${agentId} (${agent.provider}): forceInterrupt mid-turn on persistent runner — interrupting turn in place`);
|
|
161
|
+
emitOutput(agentId, '🛑 [System] Interrupting current work to process new prompt…', false, undefined, 'system-interrupt-restart');
|
|
162
|
+
// Snapshot other queued messages so the forced prompt can jump the line
|
|
163
|
+
// without dropping them: clear, send the forced prompt (queue head),
|
|
164
|
+
// then re-append the siblings behind it.
|
|
165
|
+
const siblings = runner.getQueuedMessages?.(agentId) ?? [];
|
|
166
|
+
const interrupted = await runner.interruptTurn(agentId, true);
|
|
167
|
+
if (interrupted) {
|
|
168
|
+
const sentNow = runner.sendMessage(agentId, command);
|
|
169
|
+
for (const sibling of siblings) {
|
|
170
|
+
if (!runner.sendMessage(agentId, sibling)) {
|
|
171
|
+
log.warn(`[sendCommand] Agent ${agentId}: could not restore a queued message after interrupt (${sibling.length} chars dropped)`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (sentNow) {
|
|
175
|
+
notifyCommandStarted(agentId, command);
|
|
176
|
+
const isSystemMessage = command.startsWith('[System:');
|
|
177
|
+
const updateData = {
|
|
178
|
+
status: 'working',
|
|
179
|
+
taskCount: (agent.taskCount || 0) + 1,
|
|
180
|
+
};
|
|
181
|
+
if (!isSystemMessage) {
|
|
182
|
+
updateData.lastAssignedTask = command;
|
|
183
|
+
updateData.lastAssignedTaskTime = Date.now();
|
|
184
|
+
updateData.taskLabel = undefined; // Clear for agent to regenerate via skill
|
|
185
|
+
}
|
|
186
|
+
agentService.updateAgent(agentId, updateData);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
log.warn(`[sendCommand] Agent ${agentId}: in-place turn interrupt failed — falling through to default delivery`);
|
|
191
|
+
}
|
|
148
192
|
if (processRunning && !forceNewSession) {
|
|
149
193
|
if (runner.supportsStdin()) {
|
|
150
194
|
const turnState = runner.getTurnState?.(agentId) || 'unknown';
|
|
151
195
|
log.log(`[sendCommand] Agent ${agentId}: Process alive, reusing via stdin (turnState=${turnState}, cmd=${command.substring(0, 60)})`);
|
|
196
|
+
// Persistent-stream runners (codex app-server, opencode serve) queue
|
|
197
|
+
// mid-turn messages internally instead of writing them through. Detect
|
|
198
|
+
// that via the queue length so the client sees the same queued
|
|
199
|
+
// feedback (⏳ + queued command_started) as the stdin-closed path.
|
|
200
|
+
const queueLenBefore = runner.getQueuedMessages?.(agentId).length ?? 0;
|
|
152
201
|
const sent = runner.sendMessage(agentId, command);
|
|
202
|
+
const wasQueued = (runner.getQueuedMessages?.(agentId).length ?? 0) > queueLenBefore;
|
|
153
203
|
if (sent) {
|
|
154
|
-
|
|
204
|
+
if (wasQueued) {
|
|
205
|
+
notifyCommandStarted(agentId, command, { queued: true });
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
notifyCommandStarted(agentId, command);
|
|
209
|
+
}
|
|
210
|
+
if (wasQueued) {
|
|
211
|
+
emitOutput(agentId, `⏳ [System] Mid-run message for ${providerDisplayName(agent.provider)} will be sent when the agent is free`, false, undefined, `system-midrun-queued-${Date.now()}`);
|
|
212
|
+
}
|
|
155
213
|
const isSystemMessage = command.startsWith('[System:');
|
|
156
214
|
const updateData = {
|
|
157
215
|
status: 'working',
|
|
@@ -207,6 +207,16 @@ export async function sendCommand(agentId, command, systemPrompt, forceNewSessio
|
|
|
207
207
|
export async function sendSilentCommand(agentId, command) {
|
|
208
208
|
await commandExecution.sendSilentCommand(agentId, command);
|
|
209
209
|
}
|
|
210
|
+
/** Snapshot of the agent's server-side mid-run message queue (runner-owned). */
|
|
211
|
+
export function getQueuedMessagesForAgent(agentId) {
|
|
212
|
+
const runner = getRunnerForAgent(agentId);
|
|
213
|
+
return runner?.getQueuedMessages?.(agentId) ?? [];
|
|
214
|
+
}
|
|
215
|
+
/** Remove one queued mid-run message by position (guarded by expectedText). */
|
|
216
|
+
export function removeQueuedMessageForAgent(agentId, index, expectedText) {
|
|
217
|
+
const runner = getRunnerForAgent(agentId);
|
|
218
|
+
return runner?.removeQueuedMessage?.(agentId, index, expectedText) ?? false;
|
|
219
|
+
}
|
|
210
220
|
/**
|
|
211
221
|
* Send Claude Code's `/compact` slash command to collapse an agent's context.
|
|
212
222
|
* Only fires when the agent exists AND is idle — the CLI rejects slash commands
|