tide-commander 1.150.0 → 1.150.2

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 (48) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/{BossLogsModal-DvczRUvP.js → BossLogsModal-DClgunJD.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-B2autFKk.js → BossSpawnModal-C_FZJD09.js} +1 -1
  4. package/dist/assets/{ControlsModal-BaREMQxc.js → ControlsModal-rfIMbmzt.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-DZ0udMCo.js → DockerLogsModal-CWMX-ZSk.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-CEjr0ZSZ.js → EmbeddedEditor-BlOp1ctO.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-76BP8J8C.js → GmailOAuthSetup-OblNwH22.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-D45GImPI.js → GoogleOAuthSetup-yObPsrIv.js} +1 -1
  9. package/dist/assets/{IframeModal-De63RfJZ.js → IframeModal-CnqsbsJs.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-DYrw1Sqk.js → IntegrationsPanel-D-rcP2Rf.js} +2 -2
  11. package/dist/assets/{LogViewerModal-DW-2lpln.js → LogViewerModal-BJyowigx.js} +1 -1
  12. package/dist/assets/{MonitoringModal-B0-XXyd3.js → MonitoringModal-raphhiXZ.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-DOfOhkCY.js → PM2LogsModal-D8Ovkw2b.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-DNGmIJmM.js → RestoreArchivedAreaModal-rNEBPF0G.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-BW1E76CQ.js → Scene2DCanvas-DEU63SHO.js} +1 -1
  16. package/dist/assets/{SceneManager-DAvc9-Wp.js → SceneManager-B8M7G0IB.js} +1 -1
  17. package/dist/assets/{SkillsPanel-Br-gLCLA.js → SkillsPanel-DD4Cq68m.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-DG_bIfDu.js → SlackMultiInstanceSetup-C22rxkX1.js} +1 -1
  19. package/dist/assets/{SpawnModal-lWM8OuZW.js → SpawnModal-DrgRWQql.js} +1 -1
  20. package/dist/assets/{StatisticsModal-LvA0Fcvn.js → StatisticsModal-CbE2FHRC.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-8YxNXcR3.js → SubordinateAssignmentModal-NXOWA4-P.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-DftIuKUG.js → TriggerManagerPanel-DLJGXA9B.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-oLwyO_C6.js → WorkflowEditorPanel-B8drq_L1.js} +1 -1
  24. package/dist/assets/{area-logos-mcpQqUMZ.js → area-logos-W-V1BJXF.js} +1 -1
  25. package/dist/assets/{index-C6-68CXL.js → index--C9FOQUp.js} +3 -3
  26. package/dist/assets/{index-CIon0DLf.js → index-35zpG6tD.js} +1 -1
  27. package/dist/assets/{index-Cwt-E2td.js → index-BBKm8pU7.js} +1 -1
  28. package/dist/assets/{index-O0t07OB_.js → index-BPE5Mi1H.js} +2 -2
  29. package/dist/assets/{index-Ay7dKyeR.js → index-CvyJsbe_.js} +1 -1
  30. package/dist/assets/{index-BMw7AAAK.js → index-D1gkgjaB.js} +1 -1
  31. package/dist/assets/{index-BQpd34Mc.js → index-DENq1s--.js} +1 -1
  32. package/dist/assets/{index-DFVroqA5.js → index-DeEjSnXC.js} +1 -1
  33. package/dist/assets/{index-DR6_WeF_.js → index-uazTq6eV.js} +1 -1
  34. package/dist/assets/main-BsgKYLZr.css +1 -0
  35. package/dist/assets/{main-D5sqeuwB.js → main-Ce8hSw4G.js} +136 -133
  36. package/dist/assets/{web-DPETNqOf.js → web-BKeVejC8.js} +1 -1
  37. package/dist/assets/{web-vG1Smf_N.js → web-CIOB2Dsi.js} +1 -1
  38. package/dist/assets/{web-BkrshujV.js → web-CVh7vOIA.js} +1 -1
  39. package/dist/index.html +2 -2
  40. package/dist/src/packages/server/claude/backend.js +51 -37
  41. package/dist/src/packages/server/claude/runner/process-lifecycle.js +1 -1
  42. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +1 -1
  43. package/dist/src/packages/server/codex/backend.js +16 -4
  44. package/dist/src/packages/server/grok/backend.js +17 -5
  45. package/dist/src/packages/server/grok/session-watcher.js +15 -0
  46. package/dist/src/packages/server/opencode/backend.js +14 -3
  47. package/package.json +1 -1
  48. package/dist/assets/main-JBkwGXfE.css +0 -1
@@ -1 +1 @@
1
- import{dd as t}from"./main-D5sqeuwB.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{dd as t}from"./main-Ce8hSw4G.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{dd as a}from"./main-D5sqeuwB.js";import{ImpactStyle as i,NotificationType as r}from"./index-O0t07OB_.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{dd as a}from"./main-Ce8hSw4G.js";import{ImpactStyle as i,NotificationType as r}from"./index-BPE5Mi1H.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
- import{dd as s}from"./main-D5sqeuwB.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{dd as s}from"./main-Ce8hSw4G.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};
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-D5sqeuwB.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-Ce8hSw4G.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-JBkwGXfE.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-BsgKYLZr.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -122,6 +122,13 @@ export function buildAppendedProjectInstructions(config) {
122
122
  }
123
123
  return sections.join('\n\n');
124
124
  }
125
+ function resetClaudeStreamState(state) {
126
+ state.currentStreamMessageId = null;
127
+ state.streamUuids.clear();
128
+ // suppressTextDeltas is intentionally NOT cleared here — only message_start
129
+ // opens a new turn. That way late content_block_delta lines after the final
130
+ // assistant event cannot open a second bubble with a new stream uuid.
131
+ }
125
132
  export class ClaudeBackend {
126
133
  name = 'claude';
127
134
  // ---------------------------------------------------------------------------
@@ -132,12 +139,26 @@ export class ClaudeBackend {
132
139
  // emits tiny tokens with no uuid and we mint one). We pin a stable stream id
133
140
  // per (message_id, content_block index, kind) so addOutput merges into one
134
141
  // terminal row. The final assistant message reuses the same id to finalize.
142
+ //
143
+ // ONE ClaudeBackend instance serves every Claude agent (single runner per
144
+ // provider), so this state is keyed by agentId — otherwise two concurrent
145
+ // agents reset each other's stream uuids mid-turn (dropped deltas, stuck
146
+ // streaming rows, duplicate final bubbles).
135
147
  // ---------------------------------------------------------------------------
136
- currentStreamMessageId = null;
137
- streamUuids = new Map(); // key = `${kind}:${index}`
138
- streamTextEmitted = false;
139
- /** After the full assistant text finalizes, ignore late stream deltas for this turn. */
140
- suppressTextDeltas = false;
148
+ streamStates = new Map();
149
+ streamState(agentId) {
150
+ const key = agentId || '__default';
151
+ let state = this.streamStates.get(key);
152
+ if (!state) {
153
+ state = {
154
+ currentStreamMessageId: null,
155
+ streamUuids: new Map(),
156
+ suppressTextDeltas: false,
157
+ };
158
+ this.streamStates.set(key, state);
159
+ }
160
+ return state;
161
+ }
141
162
  /**
142
163
  * Build CLI arguments for Claude Code
143
164
  */
@@ -232,7 +253,7 @@ export class ClaudeBackend {
232
253
  /**
233
254
  * Parse Claude CLI raw event into normalized StandardEvent
234
255
  */
235
- parseEvent(rawEvent) {
256
+ parseEvent(rawEvent, agentId) {
236
257
  const event = rawEvent;
237
258
  // Log ALL events to understand what we're receiving
238
259
  log.log(`parseEvent: type=${event.type}, subtype=${event.subtype || 'none'}, tool_name=${event.tool_name || 'n/a'}`);
@@ -251,7 +272,7 @@ export class ClaudeBackend {
251
272
  result = this.parseSystemEvent(event);
252
273
  break;
253
274
  case 'assistant':
254
- result = this.parseAssistantEvent(event);
275
+ result = this.parseAssistantEvent(event, agentId);
255
276
  break;
256
277
  case 'tool_use':
257
278
  result = this.parseToolUseEvent(event);
@@ -260,7 +281,7 @@ export class ClaudeBackend {
260
281
  result = this.parseResultEvent(event);
261
282
  break;
262
283
  case 'stream_event':
263
- result = this.parseStreamEvent(event);
284
+ result = this.parseStreamEvent(event, agentId);
264
285
  break;
265
286
  case 'user':
266
287
  result = this.parseUserEvent(event);
@@ -437,10 +458,11 @@ export class ClaudeBackend {
437
458
  }
438
459
  return null;
439
460
  }
440
- parseAssistantEvent(event) {
461
+ parseAssistantEvent(event, agentId) {
441
462
  // Check for content blocks in assistant message
442
463
  // Claude CLI sends both text and tool_use as content blocks within assistant events
443
464
  if (event.message?.content && Array.isArray(event.message.content)) {
465
+ const state = this.streamState(agentId);
444
466
  const events = [];
445
467
  // Use event UUID if available (unique identifier from Claude)
446
468
  const uuid = event.uuid;
@@ -450,8 +472,8 @@ export class ClaudeBackend {
450
472
  const messageId = typeof event.message?.id === 'string'
451
473
  ? event.message.id
452
474
  : null;
453
- if (messageId && !this.currentStreamMessageId) {
454
- this.currentStreamMessageId = messageId;
475
+ if (messageId && !state.currentStreamMessageId) {
476
+ state.currentStreamMessageId = messageId;
455
477
  }
456
478
  // Extract text blocks - emit as non-streaming final text.
457
479
  //
@@ -464,7 +486,7 @@ export class ClaudeBackend {
464
486
  // Never wipe stream state on thinking-only assistant events — that used to
465
487
  // force the later full-text assistant onto a new uuid → duplicate bubbles.
466
488
  const content = event.message.content;
467
- const textStreamUuids = [...this.streamUuids.entries()]
489
+ const textStreamUuids = [...state.streamUuids.entries()]
468
490
  .filter(([key]) => key.startsWith('text:'))
469
491
  .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
470
492
  .map(([, id]) => id);
@@ -475,7 +497,7 @@ export class ClaudeBackend {
475
497
  if (block?.type === 'text' && block.text && block.text.trim()) {
476
498
  // Prefer ordinal text stream uuid, then index key, then outer uuid.
477
499
  const streamUuid = textStreamUuids[textStreamOrdinal] ||
478
- this.streamUuids.get(`text:${i}`) ||
500
+ state.streamUuids.get(`text:${i}`) ||
479
501
  null;
480
502
  textStreamOrdinal += 1;
481
503
  events.push({
@@ -490,8 +512,8 @@ export class ClaudeBackend {
490
512
  // Only clear stream state after we actually finalized a text row. Intermediate
491
513
  // thinking-only assistant events must leave stream uuids intact for later deltas.
492
514
  if (emittedFinalText) {
493
- this.suppressTextDeltas = true;
494
- this.resetStreamState();
515
+ state.suppressTextDeltas = true;
516
+ resetClaudeStreamState(state);
495
517
  }
496
518
  // Extract tool_use blocks
497
519
  const toolUseBlocks = event.message.content.filter((b) => b.type === 'tool_use');
@@ -621,37 +643,30 @@ export class ClaudeBackend {
621
643
  permissionDenials,
622
644
  };
623
645
  }
624
- resetStreamState() {
625
- this.currentStreamMessageId = null;
626
- this.streamUuids.clear();
627
- this.streamTextEmitted = false;
628
- // suppressTextDeltas is intentionally NOT cleared here — only message_start
629
- // opens a new turn. That way late content_block_delta lines after the final
630
- // assistant event cannot open a second bubble with a new stream uuid.
631
- }
632
646
  /** Stable uuid so token-sized Claude deltas merge into one terminal row. */
633
- ensureStreamUuid(kind, index) {
647
+ ensureStreamUuid(state, kind, index) {
634
648
  const key = `${kind}:${index}`;
635
- let id = this.streamUuids.get(key);
649
+ let id = state.streamUuids.get(key);
636
650
  if (!id) {
637
- const msg = this.currentStreamMessageId || 'anon';
651
+ const msg = state.currentStreamMessageId || 'anon';
638
652
  id = `claude-stream-${msg}-${kind}-${index}`;
639
- this.streamUuids.set(key, id);
653
+ state.streamUuids.set(key, id);
640
654
  }
641
655
  return id;
642
656
  }
643
- parseStreamEvent(event) {
657
+ parseStreamEvent(event, agentId) {
644
658
  const streamEvent = event.event;
645
659
  if (!streamEvent)
646
660
  return null;
661
+ const state = this.streamState(agentId);
647
662
  // Capture Anthropic message id so every delta in this turn shares a stream key.
648
663
  if (streamEvent.type === 'message_start') {
649
664
  const mid = streamEvent.message?.id;
650
665
  // New message — drop any stale stream ids and reopen the stream gate.
651
- this.suppressTextDeltas = false;
652
- this.resetStreamState();
666
+ state.suppressTextDeltas = false;
667
+ resetClaudeStreamState(state);
653
668
  if (typeof mid === 'string' && mid) {
654
- this.currentStreamMessageId = mid;
669
+ state.currentStreamMessageId = mid;
655
670
  }
656
671
  return null;
657
672
  }
@@ -661,35 +676,34 @@ export class ClaudeBackend {
661
676
  if (streamEvent.type === 'content_block_delta') {
662
677
  if (streamEvent.delta?.type === 'text_delta' && streamEvent.delta.text) {
663
678
  // Drop late deltas that race after the full assistant text already finalized.
664
- if (this.suppressTextDeltas) {
679
+ if (state.suppressTextDeltas) {
665
680
  return null;
666
681
  }
667
- this.streamTextEmitted = true;
668
682
  return {
669
683
  type: 'text',
670
684
  text: streamEvent.delta.text,
671
685
  isStreaming: true,
672
686
  // Stable per-block id (NOT event.uuid — those differ every delta).
673
- uuid: this.ensureStreamUuid('text', blockIndex),
687
+ uuid: this.ensureStreamUuid(state, 'text', blockIndex),
674
688
  };
675
689
  }
676
690
  else if (streamEvent.delta?.type === 'thinking_delta' &&
677
691
  streamEvent.delta.text) {
678
- if (this.suppressTextDeltas) {
692
+ if (state.suppressTextDeltas) {
679
693
  return null;
680
694
  }
681
695
  return {
682
696
  type: 'thinking',
683
697
  text: streamEvent.delta.text,
684
698
  isStreaming: true,
685
- uuid: this.ensureStreamUuid('thinking', blockIndex),
699
+ uuid: this.ensureStreamUuid(state, 'thinking', blockIndex),
686
700
  };
687
701
  }
688
702
  }
689
703
  else if (streamEvent.type === 'content_block_start') {
690
704
  const blockType = streamEvent.content_block?.type;
691
705
  if (blockType === 'text' || blockType === 'thinking') {
692
- const streamUuid = this.ensureStreamUuid(blockType, blockIndex);
706
+ const streamUuid = this.ensureStreamUuid(state, blockType, blockIndex);
693
707
  return {
694
708
  type: 'block_start',
695
709
  blockType: blockType,
@@ -43,7 +43,7 @@ export class RunnerProcessLifecycle {
43
43
  // turn. Finalize open text/thinking streams before the tool card so
44
44
  // intermediate status lines stay separate from the final answer.
45
45
  if (event.type === 'tool_start' && typeof this.backend.breakOpenStreams === 'function') {
46
- const breaks = this.backend.breakOpenStreams();
46
+ const breaks = this.backend.breakOpenStreams(agentId);
47
47
  for (const ev of breaks) {
48
48
  this.stdoutPipeline.emitStandardEvent(agentId, ev);
49
49
  }
@@ -102,7 +102,7 @@ export class RunnerStdoutPipeline {
102
102
  });
103
103
  this.callbacks.onSessionId(agentId, sessionId);
104
104
  }
105
- const eventOrEvents = this.backend.parseEvent(rawEvent);
105
+ const eventOrEvents = this.backend.parseEvent(rawEvent, agentId);
106
106
  if (!eventOrEvents) {
107
107
  // Even unmapped events (task_progress, thinking_tokens, ...) prove the CLI is
108
108
  // alive and mid-work — keep the watchdog's activity clock fresh so a session
@@ -74,13 +74,25 @@ export function buildCodexPrompt(config) {
74
74
  }
75
75
  export class CodexBackend {
76
76
  name = 'codex';
77
- parser = new CodexJsonEventParser({ enableFileDiffEnrichment: true });
77
+ // ONE CodexBackend serves every Codex agent — parser state (streamed text
78
+ // accumulators, working dir for diff enrichment) must be per agent or
79
+ // concurrent agents corrupt each other's turn state.
80
+ parsers = new Map();
78
81
  // Prompts are passed via stdin (not argv) so large prompts (skills + system
79
82
  // prompt + class instructions) don't blow past tmux's ~16KB argv limit,
80
83
  // which silently rejects the spawn with "command too long".
81
84
  pendingStdinPrompt;
85
+ parserFor(agentId) {
86
+ const key = agentId || '__default';
87
+ let parser = this.parsers.get(key);
88
+ if (!parser) {
89
+ parser = new CodexJsonEventParser({ enableFileDiffEnrichment: true });
90
+ this.parsers.set(key, parser);
91
+ }
92
+ return parser;
93
+ }
82
94
  buildArgs(config) {
83
- this.parser.setWorkingDirectory(config.workingDir);
95
+ this.parserFor(config.agentId).setWorkingDirectory(config.workingDir);
84
96
  this.pendingStdinPrompt = buildCodexPrompt(config);
85
97
  const args = ['exec', '--experimental-json'];
86
98
  // Codex renamed [features].collab → [features].multi_agent. Enable the new
@@ -131,8 +143,8 @@ export class CodexBackend {
131
143
  args.push('-');
132
144
  return args;
133
145
  }
134
- parseEvent(rawEvent) {
135
- const events = this.parser.parseEvent(rawEvent);
146
+ parseEvent(rawEvent, agentId) {
147
+ const events = this.parserFor(agentId).parseEvent(rawEvent);
136
148
  if (events.length === 0)
137
149
  return null;
138
150
  return events.length === 1 ? events[0] : events;
@@ -77,7 +77,19 @@ function shouldPassGrokModel(model) {
77
77
  }
78
78
  export class GrokBackend {
79
79
  name = 'grok';
80
- parser = new GrokJsonEventParser();
80
+ // ONE GrokBackend serves every Grok agent — parser state (stream uuids,
81
+ // accumulated text for breakOpenStreams) must be per agent or concurrent
82
+ // agents leak each other's text into their finalize events.
83
+ parsers = new Map();
84
+ parserFor(agentId) {
85
+ const key = agentId || '__default';
86
+ let parser = this.parsers.get(key);
87
+ if (!parser) {
88
+ parser = new GrokJsonEventParser();
89
+ this.parsers.set(key, parser);
90
+ }
91
+ return parser;
92
+ }
81
93
  /** Temp prompt files created for this process; cleaned after formatStdin (no-op) or next buildArgs. */
82
94
  lastPromptFile;
83
95
  buildArgs(config) {
@@ -135,15 +147,15 @@ export class GrokBackend {
135
147
  }
136
148
  return args;
137
149
  }
138
- parseEvent(rawEvent) {
139
- const events = this.parser.parseEvent(rawEvent);
150
+ parseEvent(rawEvent, agentId) {
151
+ const events = this.parserFor(agentId).parseEvent(rawEvent);
140
152
  if (events.length === 0)
141
153
  return null;
142
154
  return events.length === 1 ? events[0] : events;
143
155
  }
144
156
  /** See CLIBackend.breakOpenStreams — tool-boundary stream split for Grok. */
145
- breakOpenStreams() {
146
- return this.parser.breakOpenStreams();
157
+ breakOpenStreams(agentId) {
158
+ return this.parserFor(agentId).breakOpenStreams();
147
159
  }
148
160
  extractSessionId(rawEvent) {
149
161
  const event = rawEvent;
@@ -184,6 +184,8 @@ export function startGrokSessionWatcher(opts) {
184
184
  let tailTimer = null;
185
185
  /** Dedupe key for last-emitted signals usage snapshot. */
186
186
  let lastSignalsUsageKey = '';
187
+ /** mtime+size of signals.json at last read — skip the 200ms-tick read+parse when unchanged. */
188
+ let lastSignalsStatKey = '';
187
189
  const emittedToolStarts = new Set();
188
190
  const emittedToolResults = new Set();
189
191
  /**
@@ -231,6 +233,18 @@ export function startGrokSessionWatcher(opts) {
231
233
  const pollSignalsUsage = () => {
232
234
  if (!sessionDirPath)
233
235
  return;
236
+ // signals.json is rewritten in place; stat is far cheaper than read+parse
237
+ // on every 200ms tick, and the file only changes a few times per turn.
238
+ try {
239
+ const st = fs.statSync(path.join(sessionDirPath, 'signals.json'));
240
+ const statKey = `${st.mtimeMs}:${st.size}`;
241
+ if (statKey === lastSignalsStatKey)
242
+ return;
243
+ lastSignalsStatKey = statKey;
244
+ }
245
+ catch {
246
+ return; // missing file — nothing to read
247
+ }
234
248
  const usage = readGrokSignalsUsage(sessionDirPath);
235
249
  if (!usage)
236
250
  return;
@@ -412,6 +426,7 @@ export function startGrokSessionWatcher(opts) {
412
426
  eventsTailer.initAtEof();
413
427
  // Seed context bar from existing signals (resume / mid-session attach).
414
428
  lastSignalsUsageKey = '';
429
+ lastSignalsStatKey = '';
415
430
  pollSignalsUsage();
416
431
  if (discoverTimer) {
417
432
  clearInterval(discoverTimer);
@@ -66,8 +66,19 @@ export function buildOpencodePrompt(config) {
66
66
  }
67
67
  export class OpencodeBackend {
68
68
  name = 'opencode';
69
- parser = new OpencodeJsonEventParser();
69
+ // ONE OpencodeBackend serves every OpenCode agent — parser turn state
70
+ // (streamed text accumulators, open stream uuids) must be per agent.
71
+ parsers = new Map();
70
72
  pendingStdinPrompt;
73
+ parserFor(agentId) {
74
+ const key = agentId || '__default';
75
+ let parser = this.parsers.get(key);
76
+ if (!parser) {
77
+ parser = new OpencodeJsonEventParser();
78
+ this.parsers.set(key, parser);
79
+ }
80
+ return parser;
81
+ }
71
82
  buildArgs(config) {
72
83
  const args = ['run', '--format', 'json'];
73
84
  // Always skip permissions for autonomous agents
@@ -91,8 +102,8 @@ export class OpencodeBackend {
91
102
  log.log(`buildArgs: ${args.length} args, sessionId=${config.sessionId ? 'yes' : 'no'}, model=${config.model || 'default'}`);
92
103
  return args;
93
104
  }
94
- parseEvent(rawEvent) {
95
- const events = this.parser.parseEvent(rawEvent);
105
+ parseEvent(rawEvent, agentId) {
106
+ const events = this.parserFor(agentId).parseEvent(rawEvent);
96
107
  if (events.length === 0)
97
108
  return null;
98
109
  return events.length === 1 ? events[0] : events;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.150.0",
3
+ "version": "1.150.2",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",