tide-commander 1.148.0 → 1.150.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 (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +6 -3
  3. package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
  4. package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
  5. package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
  6. package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
  7. package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
  8. package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
  9. package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
  10. package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
  11. package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
  12. package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
  13. package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
  14. package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
  15. package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
  16. package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
  17. package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
  18. package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
  19. package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
  20. package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
  21. package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
  22. package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
  23. package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
  24. package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
  25. package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
  26. package/dist/assets/grok.ico +0 -0
  27. package/dist/assets/grok.png +0 -0
  28. package/dist/assets/index-Ay7dKyeR.js +1 -0
  29. package/dist/assets/index-BMw7AAAK.js +5 -0
  30. package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
  31. package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
  32. package/dist/assets/index-CIon0DLf.js +2 -0
  33. package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
  34. package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
  35. package/dist/assets/index-DR6_WeF_.js +1 -0
  36. package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
  37. package/dist/assets/main-D5sqeuwB.js +286 -0
  38. package/dist/assets/main-JBkwGXfE.css +1 -0
  39. package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
  40. package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
  41. package/dist/assets/{web-bcYaAwP6.js → web-vG1Smf_N.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/dist/locales/de/config.json +3 -1
  44. package/dist/locales/en/config.json +2 -0
  45. package/dist/locales/en/terminal.json +6 -0
  46. package/dist/locales/en/tools.json +4 -2
  47. package/dist/locales/es/config.json +2 -0
  48. package/dist/locales/fr/config.json +3 -1
  49. package/dist/locales/hi/config.json +3 -1
  50. package/dist/locales/it/config.json +3 -1
  51. package/dist/locales/ja/config.json +3 -1
  52. package/dist/locales/pt/config.json +3 -1
  53. package/dist/locales/ru/config.json +3 -1
  54. package/dist/locales/zh-CN/config.json +3 -1
  55. package/dist/src/packages/server/claude/backend.js +114 -10
  56. package/dist/src/packages/server/claude/runner/process-lifecycle.js +62 -0
  57. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
  58. package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
  59. package/dist/src/packages/server/claude/session-loader.js +295 -0
  60. package/dist/src/packages/server/codex/json-event-parser.js +149 -4
  61. package/dist/src/packages/server/data/index.js +1 -0
  62. package/dist/src/packages/server/grok/backend.js +235 -0
  63. package/dist/src/packages/server/grok/index.js +4 -0
  64. package/dist/src/packages/server/grok/json-event-parser.js +153 -0
  65. package/dist/src/packages/server/grok/session-watcher.js +493 -0
  66. package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
  67. package/dist/src/packages/server/routes/agents.js +19 -11
  68. package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
  69. package/dist/src/packages/server/runtime/index.js +1 -0
  70. package/dist/src/packages/server/services/agent-service.js +26 -5
  71. package/dist/src/packages/server/services/grok-usage-service.js +374 -0
  72. package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
  73. package/dist/src/packages/server/services/runtime-events.js +46 -7
  74. package/dist/src/packages/server/services/runtime-service.js +19 -3
  75. package/dist/src/packages/server/utils/tool-formatting.js +40 -12
  76. package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
  77. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +73 -14
  78. package/dist/src/packages/shared/agent-types.js +10 -0
  79. package/package.json +1 -1
  80. package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
  81. package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
  82. package/dist/assets/SceneManager-QqKygyaw.js +0 -104
  83. package/dist/assets/SpawnModal-BOeStihN.js +0 -1
  84. package/dist/assets/index-DOzFBkSH.js +0 -1
  85. package/dist/assets/index-DP7kd91h.js +0 -1
  86. package/dist/assets/index-DRyMARFk.js +0 -5
  87. package/dist/assets/index-DuWmOZdY.js +0 -2
  88. package/dist/assets/main-C2zgPci-.js +0 -256
  89. package/dist/assets/main-C3JO2TRI.css +0 -1
@@ -1 +1 @@
1
- import{d8 as s}from"./main-C2zgPci-.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-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 +1 @@
1
- import{d8 as t}from"./main-C2zgPci-.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-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 +1 @@
1
- import{d8 as a}from"./main-C2zgPci-.js";import{ImpactStyle as i,NotificationType as r}from"./index-h3Cuh2Zn.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-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};
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-C2zgPci-.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-D5sqeuwB.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-C3JO2TRI.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-JBkwGXfE.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -35,7 +35,9 @@
35
35
  "compactView": "Kompakte Ansicht",
36
36
  "autoScroll": "Automatisches Scrollen",
37
37
  "notifications": "Benachrichtigungen",
38
- "soundEffects": "Soundeffekte"
38
+ "soundEffects": "Soundeffekte",
39
+ "streamTextLive": "Wort-für-Wort-Streaming",
40
+ "streamTextLiveTitle": "Claude- und Grok-Text/Denken während des Streamings anzeigen. Deaktivieren, um nur vollständige Antworten zu sehen."
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "{{count}} benutzerdefinierte Namen konfiguriert",
@@ -25,6 +25,8 @@
25
25
  "general": {
26
26
  "history": "History",
27
27
  "hideCosts": "Hide Costs",
28
+ "streamTextLive": "Word-by-word streaming",
29
+ "streamTextLiveTitle": "Show Claude and Grok text/thinking as it streams. Turn off to only show complete replies.",
28
30
  "grid": "Grid",
29
31
  "showFPS": "Show FPS",
30
32
  "fpsLimit": "FPS Limit",
@@ -1267,6 +1267,7 @@
1267
1267
  },
1268
1268
  "usage": {
1269
1269
  "title": "Claude Usage",
1270
+ "titleGrok": "Grok Usage",
1270
1271
  "session": "Session",
1271
1272
  "tokensUsed": "Tokens used",
1272
1273
  "taskCount": "Tasks sent",
@@ -1283,6 +1284,11 @@
1283
1284
  "currentWeekAll": "Current week (all models)",
1284
1285
  "currentWeekOpus": "Current week (Opus only)",
1285
1286
  "currentWeekSonnet": "Current week (Sonnet only)",
1287
+ "weeklyLimit": "Weekly limit",
1288
+ "monthlyLimit": "Monthly limit",
1289
+ "periodLimit": "Plan period limit",
1290
+ "onDemandLimit": "Pay-as-you-go limit",
1291
+ "creditsUsed": "{{used}} / {{limit}} credits",
1286
1292
  "percentUsed": "{{percent}}% used",
1287
1293
  "resets": "Resets {{time}}",
1288
1294
  "limitsError": "Live limits unavailable: {{message}}"
@@ -43,11 +43,13 @@
43
43
  "askFollowupQuestion": "AskFollowupQuestion",
44
44
  "askUserQuestion": "AskUserQuestion",
45
45
  "attemptCompletion": "AttemptCompletion",
46
- "listFiles": "ListFiles",
47
- "searchFiles": "SearchFiles",
46
+ "listFiles": "List Files",
47
+ "searchFiles": "Search Files",
48
48
  "executeCommand": "ExecuteCommand",
49
49
  "spawnAgent": "Spawn Agent",
50
50
  "sendInput": "Send Input",
51
+ "sendMessage": "Message Agent",
52
+ "taskOutput": "Task Output",
51
53
  "wait": "Wait"
52
54
  },
53
55
  "showOutputsInline": "Show all bash outputs inline",
@@ -21,6 +21,8 @@
21
21
  "general": {
22
22
  "history": "Historial",
23
23
  "hideCosts": "Ocultar costos",
24
+ "streamTextLive": "Streaming palabra por palabra",
25
+ "streamTextLiveTitle": "Mostrar el texto/pensamiento de Claude y Grok mientras se genera. Desactívalo para ver solo respuestas completas.",
24
26
  "grid": "Cuadricula",
25
27
  "showFPS": "Mostrar FPS",
26
28
  "fpsLimit": "Limite de FPS",
@@ -35,7 +35,9 @@
35
35
  "compactView": "Vue compacte",
36
36
  "autoScroll": "Défilement automatique",
37
37
  "notifications": "Notifications",
38
- "soundEffects": "Effets sonores"
38
+ "soundEffects": "Effets sonores",
39
+ "streamTextLive": "Streaming mot à mot",
40
+ "streamTextLiveTitle": "Afficher le texte/la réflexion de Claude et Grok au fur et à mesure. Désactivez pour n'afficher que les réponses complètes."
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "{{count}} noms personnalisés configurés",
@@ -35,7 +35,9 @@
35
35
  "compactView": "कॉम्पैक्ट दृश्य",
36
36
  "autoScroll": "ऑटो स्क्रॉल",
37
37
  "notifications": "सूचनाएं",
38
- "soundEffects": "ध्वनि प्रभाव"
38
+ "soundEffects": "ध्वनि प्रभाव",
39
+ "streamTextLive": "शब्द-दर-शब्द स्ट्रीमिंग",
40
+ "streamTextLiveTitle": "Claude और Grok का टेक्स्ट/सोच स्ट्रीम होते हुए दिखाएँ। केवल पूरी प्रतिक्रियाएँ देखने के लिए बंद करें।"
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "{{count}} कस्टम नाम कॉन्फ़िगर किए गए",
@@ -35,7 +35,9 @@
35
35
  "compactView": "Vista compatta",
36
36
  "autoScroll": "Scorrimento automatico",
37
37
  "notifications": "Notifiche",
38
- "soundEffects": "Effetti sonori"
38
+ "soundEffects": "Effetti sonori",
39
+ "streamTextLive": "Streaming parola per parola",
40
+ "streamTextLiveTitle": "Mostra testo/pensiero di Claude e Grok mentre vengono generati. Disattiva per vedere solo le risposte complete."
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "{{count}} nomi personalizzati configurati",
@@ -35,7 +35,9 @@
35
35
  "compactView": "コンパクト表示",
36
36
  "autoScroll": "自動スクロール",
37
37
  "notifications": "通知",
38
- "soundEffects": "効果音"
38
+ "soundEffects": "効果音",
39
+ "streamTextLive": "単語ごとのストリーミング",
40
+ "streamTextLiveTitle": "Claude と Grok のテキスト/思考をストリーミング表示します。オフにすると完成した返答のみ表示します。"
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "{{count}}個のカスタム名が設定済み",
@@ -35,7 +35,9 @@
35
35
  "compactView": "Visualizacao Compacta",
36
36
  "autoScroll": "Rolagem Automatica",
37
37
  "notifications": "Notificacoes",
38
- "soundEffects": "Efeitos Sonoros"
38
+ "soundEffects": "Efeitos Sonoros",
39
+ "streamTextLive": "Streaming palavra a palavra",
40
+ "streamTextLiveTitle": "Mostrar texto/pensamento do Claude e Grok à medida que são gerados. Desative para ver apenas respostas completas."
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "{{count}} nomes personalizados configurados",
@@ -35,7 +35,9 @@
35
35
  "compactView": "Компактный вид",
36
36
  "autoScroll": "Автопрокрутка",
37
37
  "notifications": "Уведомления",
38
- "soundEffects": "Звуковые эффекты"
38
+ "soundEffects": "Звуковые эффекты",
39
+ "streamTextLive": "Потоковая передача по словам",
40
+ "streamTextLiveTitle": "Показывать текст/мысли Claude и Grok по мере генерации. Отключите, чтобы видеть только готовые ответы."
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "{{count}} пользовательских имён настроено",
@@ -35,7 +35,9 @@
35
35
  "compactView": "紧凑视图",
36
36
  "autoScroll": "自动滚动",
37
37
  "notifications": "通知",
38
- "soundEffects": "音效"
38
+ "soundEffects": "音效",
39
+ "streamTextLive": "逐字流式输出",
40
+ "streamTextLiveTitle": "实时显示 Claude 和 Grok 的文本/思考。关闭后仅显示完整回复。"
39
41
  },
40
42
  "agentNames": {
41
43
  "customConfigured": "已配置 {{count}} 个自定义名称",
@@ -124,6 +124,20 @@ export function buildAppendedProjectInstructions(config) {
124
124
  }
125
125
  export class ClaudeBackend {
126
126
  name = 'claude';
127
+ // ---------------------------------------------------------------------------
128
+ // Token-stream state (--include-partial-messages)
129
+ //
130
+ // Claude's stream_event lines each carry a *different* outer `uuid`, so the
131
+ // client cannot merge word-by-word deltas by that field (unlike Grok, which
132
+ // emits tiny tokens with no uuid and we mint one). We pin a stable stream id
133
+ // per (message_id, content_block index, kind) so addOutput merges into one
134
+ // terminal row. The final assistant message reuses the same id to finalize.
135
+ // ---------------------------------------------------------------------------
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;
127
141
  /**
128
142
  * Build CLI arguments for Claude Code
129
143
  */
@@ -135,6 +149,10 @@ export class ClaudeBackend {
135
149
  args.push('--verbose');
136
150
  args.push('--output-format', 'stream-json');
137
151
  args.push('--input-format', 'stream-json');
152
+ // Without this flag, stream-json only emits complete assistant messages —
153
+ // no content_block_delta / text_delta tokens — so the UI cannot typewriter
154
+ // the reply the way Grok's streaming-json does by default.
155
+ args.push('--include-partial-messages');
138
156
  // Resume existing session if available
139
157
  if (config.sessionId) {
140
158
  args.push('--resume', config.sessionId);
@@ -427,19 +445,54 @@ export class ClaudeBackend {
427
445
  // Use event UUID if available (unique identifier from Claude)
428
446
  const uuid = event.uuid;
429
447
  const usage = event.message.usage;
430
- // Extract text blocks - emit as non-streaming final text
431
- // This ensures text is captured even if streaming deltas were missed
432
- const textBlocks = event.message.content.filter((b) => b.type === 'text');
433
- for (const block of textBlocks) {
434
- if (block.text && block.text.trim()) {
448
+ // Capture message id from the assistant payload when stream_event's
449
+ // message_start was missed or arrived without an id.
450
+ const messageId = typeof event.message?.id === 'string'
451
+ ? event.message.id
452
+ : null;
453
+ if (messageId && !this.currentStreamMessageId) {
454
+ this.currentStreamMessageId = messageId;
455
+ }
456
+ // Extract text blocks - emit as non-streaming final text.
457
+ //
458
+ // IMPORTANT: with --include-partial-messages Claude may emit *multiple*
459
+ // assistant events per turn (e.g. thinking-only first, then full text).
460
+ // Stream content_block indexes (thinking=0, text=1) do NOT match the final
461
+ // assistant content array (text-only → index 0). Match stream uuids by the
462
+ // order of text blocks among open text streams, not raw content index.
463
+ //
464
+ // Never wipe stream state on thinking-only assistant events — that used to
465
+ // force the later full-text assistant onto a new uuid → duplicate bubbles.
466
+ const content = event.message.content;
467
+ const textStreamUuids = [...this.streamUuids.entries()]
468
+ .filter(([key]) => key.startsWith('text:'))
469
+ .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
470
+ .map(([, id]) => id);
471
+ let textStreamOrdinal = 0;
472
+ let emittedFinalText = false;
473
+ for (let i = 0; i < content.length; i++) {
474
+ const block = content[i];
475
+ if (block?.type === 'text' && block.text && block.text.trim()) {
476
+ // Prefer ordinal text stream uuid, then index key, then outer uuid.
477
+ const streamUuid = textStreamUuids[textStreamOrdinal] ||
478
+ this.streamUuids.get(`text:${i}`) ||
479
+ null;
480
+ textStreamOrdinal += 1;
435
481
  events.push({
436
482
  type: 'text',
437
483
  text: block.text,
438
484
  isStreaming: false, // Mark as final, non-streaming text
439
- uuid, // Add message UUID for deduplication
485
+ uuid: streamUuid || uuid,
440
486
  });
487
+ emittedFinalText = true;
441
488
  }
442
489
  }
490
+ // Only clear stream state after we actually finalized a text row. Intermediate
491
+ // thinking-only assistant events must leave stream uuids intact for later deltas.
492
+ if (emittedFinalText) {
493
+ this.suppressTextDeltas = true;
494
+ this.resetStreamState();
495
+ }
443
496
  // Extract tool_use blocks
444
497
  const toolUseBlocks = event.message.content.filter((b) => b.type === 'tool_use');
445
498
  for (const block of toolUseBlocks) {
@@ -487,7 +540,9 @@ export class ClaudeBackend {
487
540
  });
488
541
  }
489
542
  if (events.length > 0) {
490
- log.log(`parseAssistantEvent: extracted ${textBlocks.length} text block(s), ${toolUseBlocks.length} tool_use block(s), usage=${usage ? 'yes' : 'no'}, uuid=${uuid}`);
543
+ const textCount = events.filter((e) => e.type === 'text').length;
544
+ const toolCount = events.filter((e) => e.type === 'tool_start').length;
545
+ log.log(`parseAssistantEvent: extracted ${textCount} text block(s), ${toolCount} tool_use block(s), usage=${usage ? 'yes' : 'no'}, uuid=${uuid}`);
491
546
  return events.length === 1 ? events[0] : events;
492
547
  }
493
548
  }
@@ -566,45 +621,94 @@ export class ClaudeBackend {
566
621
  permissionDenials,
567
622
  };
568
623
  }
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
+ /** Stable uuid so token-sized Claude deltas merge into one terminal row. */
633
+ ensureStreamUuid(kind, index) {
634
+ const key = `${kind}:${index}`;
635
+ let id = this.streamUuids.get(key);
636
+ if (!id) {
637
+ const msg = this.currentStreamMessageId || 'anon';
638
+ id = `claude-stream-${msg}-${kind}-${index}`;
639
+ this.streamUuids.set(key, id);
640
+ }
641
+ return id;
642
+ }
569
643
  parseStreamEvent(event) {
570
644
  const streamEvent = event.event;
571
645
  if (!streamEvent)
572
646
  return null;
647
+ // Capture Anthropic message id so every delta in this turn shares a stream key.
648
+ if (streamEvent.type === 'message_start') {
649
+ const mid = streamEvent.message?.id;
650
+ // New message — drop any stale stream ids and reopen the stream gate.
651
+ this.suppressTextDeltas = false;
652
+ this.resetStreamState();
653
+ if (typeof mid === 'string' && mid) {
654
+ this.currentStreamMessageId = mid;
655
+ }
656
+ return null;
657
+ }
658
+ const blockIndex = typeof streamEvent.index === 'number' && Number.isFinite(streamEvent.index)
659
+ ? streamEvent.index
660
+ : 0;
573
661
  if (streamEvent.type === 'content_block_delta') {
574
662
  if (streamEvent.delta?.type === 'text_delta' && streamEvent.delta.text) {
663
+ // Drop late deltas that race after the full assistant text already finalized.
664
+ if (this.suppressTextDeltas) {
665
+ return null;
666
+ }
667
+ this.streamTextEmitted = true;
575
668
  return {
576
669
  type: 'text',
577
670
  text: streamEvent.delta.text,
578
671
  isStreaming: true,
579
- uuid: event.uuid, // Propagate UUID for streaming chunks
672
+ // Stable per-block id (NOT event.uuid those differ every delta).
673
+ uuid: this.ensureStreamUuid('text', blockIndex),
580
674
  };
581
675
  }
582
676
  else if (streamEvent.delta?.type === 'thinking_delta' &&
583
677
  streamEvent.delta.text) {
678
+ if (this.suppressTextDeltas) {
679
+ return null;
680
+ }
584
681
  return {
585
682
  type: 'thinking',
586
683
  text: streamEvent.delta.text,
587
684
  isStreaming: true,
588
- uuid: event.uuid, // Propagate UUID for streaming chunks
685
+ uuid: this.ensureStreamUuid('thinking', blockIndex),
589
686
  };
590
687
  }
591
688
  }
592
689
  else if (streamEvent.type === 'content_block_start') {
593
690
  const blockType = streamEvent.content_block?.type;
594
691
  if (blockType === 'text' || blockType === 'thinking') {
692
+ const streamUuid = this.ensureStreamUuid(blockType, blockIndex);
595
693
  return {
596
694
  type: 'block_start',
597
695
  blockType: blockType,
598
- uuid: event.uuid, // Include UUID for block markers too
696
+ uuid: streamUuid,
599
697
  };
600
698
  }
601
699
  }
602
700
  else if (streamEvent.type === 'content_block_stop') {
701
+ // Keep stream uuids alive until the assistant final message lands so
702
+ // parseAssistantEvent can reuse them for isStreaming:false finalize.
603
703
  return {
604
704
  type: 'block_end',
605
705
  uuid: event.uuid,
606
706
  };
607
707
  }
708
+ else if (streamEvent.type === 'message_stop') {
709
+ // If no assistant event follows (rare), leave state for next message_start.
710
+ return null;
711
+ }
608
712
  return null;
609
713
  }
610
714
  /**
@@ -3,6 +3,7 @@ import * as path from 'path';
3
3
  import { StringDecoder } from 'string_decoder';
4
4
  import { createLogger } from '../../utils/logger.js';
5
5
  import { isTmuxEnabled, checkTmuxAvailability, spawnInTmux, killTmuxSession, interruptTmuxSession, } from './tmux-helper.js';
6
+ import { startGrokSessionWatcher } from '../../grok/session-watcher.js';
6
7
  const log = createLogger('Runner');
7
8
  export class RunnerProcessLifecycle {
8
9
  backend;
@@ -25,6 +26,44 @@ export class RunnerProcessLifecycle {
25
26
  this.recoveryStore = deps.recoveryStore;
26
27
  this.onDisableAutoRestart = deps.onDisableAutoRestart;
27
28
  }
29
+ /**
30
+ * Grok headless does not stream tool events on stdout. Tail session files
31
+ * and inject tool_start/tool_result into the same pipeline as stdout events.
32
+ */
33
+ attachGrokSideChannel(activeProcess, agentId, workingDir, sessionId) {
34
+ if (this.backend.name !== 'grok')
35
+ return;
36
+ const watcher = startGrokSessionWatcher({
37
+ agentId,
38
+ workingDir,
39
+ sessionId,
40
+ startedAt: activeProcess.startTime,
41
+ onEvent: (event) => {
42
+ // Grok does not emit stdout `end` between tool rounds in one agentic
43
+ // turn. Finalize open text/thinking streams before the tool card so
44
+ // intermediate status lines stay separate from the final answer.
45
+ if (event.type === 'tool_start' && typeof this.backend.breakOpenStreams === 'function') {
46
+ const breaks = this.backend.breakOpenStreams();
47
+ for (const ev of breaks) {
48
+ this.stdoutPipeline.emitStandardEvent(agentId, ev);
49
+ }
50
+ }
51
+ this.stdoutPipeline.emitStandardEvent(agentId, event);
52
+ },
53
+ onSessionId: (sid) => {
54
+ const proc = this.activeProcesses.get(agentId);
55
+ if (proc && !proc.sessionId) {
56
+ proc.sessionId = sid;
57
+ if (proc.lastRequest && !proc.lastRequest.sessionId) {
58
+ proc.lastRequest.sessionId = sid;
59
+ }
60
+ }
61
+ this.bus.emit({ type: 'runner.session_id', agentId, sessionId: sid });
62
+ this.callbacks.onSessionId(agentId, sid);
63
+ },
64
+ });
65
+ activeProcess.sideChannelStop = () => watcher.stop();
66
+ }
28
67
  async run(request) {
29
68
  const { agentId, prompt, workingDir, sessionId, model, effort, useChrome, permissionMode = 'bypass', systemPrompt, forceNewSession, forkSession, customAgent, } = request;
30
69
  await this.stop(agentId);
@@ -90,6 +129,7 @@ export class RunnerProcessLifecycle {
90
129
  tmuxExpectedCommand: path.basename(executable),
91
130
  };
92
131
  this.activeProcesses.set(agentId, activeProcess);
132
+ this.attachGrokSideChannel(activeProcess, agentId, workingDir, forceNewSession ? undefined : sessionId);
93
133
  // Use file-tailing stdout pipeline for tmux mode
94
134
  const tailer = this.stdoutPipeline.handleTmuxLog(agentId, tmuxResult.logFile);
95
135
  activeProcess.tmuxTailer = tailer;
@@ -136,10 +176,25 @@ export class RunnerProcessLifecycle {
136
176
  turnState: 'processing',
137
177
  };
138
178
  this.activeProcesses.set(agentId, activeProcess);
179
+ this.attachGrokSideChannel(activeProcess, agentId, workingDir, forceNewSession ? undefined : sessionId);
139
180
  const stdoutDone = this.stdoutPipeline.handleStdout(agentId, childProcess);
140
181
  this.handleStderr(agentId, childProcess);
141
182
  childProcess.on('close', async (code, signal) => {
142
183
  await stdoutDone;
184
+ // Keep tailing session files briefly so the last tool_result lines flush to disk
185
+ // after the CLI exits (common race on Grok headless).
186
+ const sideStop = activeProcess.sideChannelStop;
187
+ activeProcess.sideChannelStop = undefined;
188
+ if (sideStop) {
189
+ setTimeout(() => {
190
+ try {
191
+ sideStop();
192
+ }
193
+ catch {
194
+ // ignore
195
+ }
196
+ }, 2000);
197
+ }
143
198
  this.bus.emit({
144
199
  type: 'runner.process_closed',
145
200
  agentId,
@@ -221,6 +276,13 @@ export class RunnerProcessLifecycle {
221
276
  if (activeProcess.tmuxTailer) {
222
277
  activeProcess.tmuxTailer.stop();
223
278
  }
279
+ try {
280
+ activeProcess.sideChannelStop?.();
281
+ }
282
+ catch {
283
+ // ignore
284
+ }
285
+ activeProcess.sideChannelStop = undefined;
224
286
  this.activeProcesses.delete(agentId);
225
287
  this.activityCallbacks.delete(agentId);
226
288
  // tmux mode: kill the tmux session and clean up