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.
Files changed (60) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/assets/{BossLogsModal-DrYF13xp.js → BossLogsModal-lsLcNR35.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-2ZgbW8Iy.js → BossSpawnModal-WxYBtXP5.js} +1 -1
  4. package/dist/assets/{ControlsModal-CRdNNTEo.js → ControlsModal-5M6SyKnw.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-BGPZP9_V.js → DockerLogsModal-D5IPlAoh.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-B99bT-bl.js → EmbeddedEditor-B9vgQ8Ig.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-DmMCxdUW.js → GmailOAuthSetup-CVTbR9FZ.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-BZqit-mW.js → GoogleOAuthSetup-zAdgAFKW.js} +1 -1
  9. package/dist/assets/{IframeModal-vMY7XxHP.js → IframeModal-eYlclM2R.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-DVmqPRTs.js → IntegrationsPanel-Bv-ZSSJU.js} +2 -2
  11. package/dist/assets/{LogViewerModal-rYC6IVPV.js → LogViewerModal-CVpFonLI.js} +1 -1
  12. package/dist/assets/{MonitoringModal-C-QXpZkE.js → MonitoringModal-39qKyt0y.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-C3A_ohFv.js → PM2LogsModal-BXkii-1F.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-Dx94NzSz.js → RestoreArchivedAreaModal-CEmlAVBn.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-BxOqJ0IB.js → Scene2DCanvas-BFNkVJ3k.js} +1 -1
  16. package/dist/assets/{SceneManager-CxcXfq_u.js → SceneManager-nmTk3bz4.js} +1 -1
  17. package/dist/assets/{SkillsPanel-Fu5sfG7Q.js → SkillsPanel-D0KXp4DD.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-jnGMesoq.js → SlackMultiInstanceSetup-DAILVwTM.js} +1 -1
  19. package/dist/assets/{SpawnModal-CGYg4uTC.js → SpawnModal-CQ34dkWV.js} +1 -1
  20. package/dist/assets/{StatisticsModal-BOV6Juts.js → StatisticsModal-DT8IPgDF.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-Bhgj9FvT.js → SubordinateAssignmentModal-vp8fGpQ6.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-Qkw4ajmL.js → TriggerManagerPanel-BdeaMglJ.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-DTIBRGz4.js → WorkflowEditorPanel-Dpoq7d7Q.js} +1 -1
  24. package/dist/assets/{area-logos-uM2FM0I2.js → area-logos-MPuWTVE6.js} +1 -1
  25. package/dist/assets/{index-B03BKiDN.js → index-B5Zce39Z.js} +1 -1
  26. package/dist/assets/{index-BvOgm4yp.js → index-BDmPOuWq.js} +4 -4
  27. package/dist/assets/index-BsO3a12k.js +2 -0
  28. package/dist/assets/{index-5tz2e3op.js → index-CRuw0L8S.js} +1 -1
  29. package/dist/assets/{index-CpQeaxZ7.js → index-D4o6bUBS.js} +2 -2
  30. package/dist/assets/{index-BvD1v4JH.js → index-DHH9e1Of.js} +1 -1
  31. package/dist/assets/{index-BFJWNGpT.js → index-DbKWgVZ-.js} +6 -6
  32. package/dist/assets/index-DiTv1htN.js +1 -0
  33. package/dist/assets/{index-DzQxMm67.js → index-cjcZzxw3.js} +3 -3
  34. package/dist/assets/main-B1w4ybci.js +298 -0
  35. package/dist/assets/main-D5J-fbZv.css +1 -0
  36. package/dist/assets/{web-NCIGsXXA.js → web-DJKyfwYK.js} +1 -1
  37. package/dist/assets/{web-ml-7D2g7.js → web-DRZHW7yx.js} +1 -1
  38. package/dist/assets/{web-ClQyiX7L.js → web-yMkJObs8.js} +1 -1
  39. package/dist/build-info.json +1 -1
  40. package/dist/index.html +2 -2
  41. package/dist/locales/en/config.json +5 -0
  42. package/dist/src/packages/server/claude/runner.js +13 -0
  43. package/dist/src/packages/server/codex/app-server/app-server-event-adapter.js +8 -2
  44. package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +50 -0
  45. package/dist/src/packages/server/codex/json-event-parser.js +8 -0
  46. package/dist/src/packages/server/opencode/server/opencode-server-runner.js +53 -0
  47. package/dist/src/packages/server/routes/agents.js +35 -0
  48. package/dist/src/packages/server/routes/system.js +55 -1
  49. package/dist/src/packages/server/runtime/claude-runner-router.js +6 -0
  50. package/dist/src/packages/server/runtime/codex-runner-router.js +12 -0
  51. package/dist/src/packages/server/runtime/opencode-runner-router.js +12 -0
  52. package/dist/src/packages/server/services/claude-usage-service.js +58 -6
  53. package/dist/src/packages/server/services/runtime-command-execution.js +61 -3
  54. package/dist/src/packages/server/services/runtime-service.js +10 -0
  55. package/dist/src/packages/server/services/self-update-service.js +44 -0
  56. package/package.json +1 -1
  57. package/dist/assets/index-3y5Ajiyr.js +0 -1
  58. package/dist/assets/index-cA0kPNUq.js +0 -2
  59. package/dist/assets/main-B-jhtDyZ.css +0 -1
  60. package/dist/assets/main-rX2ghMIV.js +0 -297
@@ -1 +1 @@
1
- import{dh as t}from"./main-rX2ghMIV.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
+ 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{dh as s}from"./main-rX2ghMIV.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
1
+ import{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{dh as a}from"./main-rX2ghMIV.js";import{ImpactStyle as i,NotificationType as r}from"./index-CpQeaxZ7.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
1
+ import{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};
@@ -1 +1 @@
1
- {"version":"1.162.0"}
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-rX2ghMIV.js"></script>
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-B-jhtDyZ.css">
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
- return [{ type: 'tool_start', toolName: 'Bash', toolInput: { command, status: item.status } }];
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
- return [{ type: 'tool_result', toolName: 'Bash', toolOutput: output }];
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
- // Claude Code 2.1.207 labels this allowance "Fable", although the live
332
- // endpoint still exposes it under the legacy seven_day_sonnet key.
333
- const sevenDayFable = parseRateLimitWindow(body.seven_day_fable ?? body.seven_day_sonnet);
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=true: user is replacing pending mid-run work with this command
115
- await runner.stop(agentId, true);
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
- notifyCommandStarted(agentId, command);
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