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.
- package/CHANGELOG.md +33 -0
- package/README.md +6 -3
- package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
- package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
- package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
- package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
- package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
- package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
- package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
- package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
- package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
- package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
- package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
- package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
- package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
- package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
- package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
- package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
- package/dist/assets/grok.ico +0 -0
- package/dist/assets/grok.png +0 -0
- package/dist/assets/index-Ay7dKyeR.js +1 -0
- package/dist/assets/index-BMw7AAAK.js +5 -0
- package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
- package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
- package/dist/assets/index-CIon0DLf.js +2 -0
- package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
- package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
- package/dist/assets/index-DR6_WeF_.js +1 -0
- package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
- package/dist/assets/main-D5sqeuwB.js +286 -0
- package/dist/assets/main-JBkwGXfE.css +1 -0
- package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
- package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
- package/dist/assets/{web-bcYaAwP6.js → web-vG1Smf_N.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +3 -1
- package/dist/locales/en/config.json +2 -0
- package/dist/locales/en/terminal.json +6 -0
- package/dist/locales/en/tools.json +4 -2
- package/dist/locales/es/config.json +2 -0
- package/dist/locales/fr/config.json +3 -1
- package/dist/locales/hi/config.json +3 -1
- package/dist/locales/it/config.json +3 -1
- package/dist/locales/ja/config.json +3 -1
- package/dist/locales/pt/config.json +3 -1
- package/dist/locales/ru/config.json +3 -1
- package/dist/locales/zh-CN/config.json +3 -1
- package/dist/src/packages/server/claude/backend.js +114 -10
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +62 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
- package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
- package/dist/src/packages/server/claude/session-loader.js +295 -0
- package/dist/src/packages/server/codex/json-event-parser.js +149 -4
- package/dist/src/packages/server/data/index.js +1 -0
- package/dist/src/packages/server/grok/backend.js +235 -0
- package/dist/src/packages/server/grok/index.js +4 -0
- package/dist/src/packages/server/grok/json-event-parser.js +153 -0
- package/dist/src/packages/server/grok/session-watcher.js +493 -0
- package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
- package/dist/src/packages/server/routes/agents.js +19 -11
- package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +1 -0
- package/dist/src/packages/server/services/agent-service.js +26 -5
- package/dist/src/packages/server/services/grok-usage-service.js +374 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
- package/dist/src/packages/server/services/runtime-events.js +46 -7
- package/dist/src/packages/server/services/runtime-service.js +19 -3
- package/dist/src/packages/server/utils/tool-formatting.js +40 -12
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +73 -14
- package/dist/src/packages/shared/agent-types.js +10 -0
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
- package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
- package/dist/assets/SceneManager-QqKygyaw.js +0 -104
- package/dist/assets/SpawnModal-BOeStihN.js +0 -1
- package/dist/assets/index-DOzFBkSH.js +0 -1
- package/dist/assets/index-DP7kd91h.js +0 -1
- package/dist/assets/index-DRyMARFk.js +0 -5
- package/dist/assets/index-DuWmOZdY.js +0 -2
- package/dist/assets/main-C2zgPci-.js +0 -256
- package/dist/assets/main-C3JO2TRI.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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{
|
|
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{
|
|
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-
|
|
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-
|
|
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": "
|
|
47
|
-
"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
|
-
//
|
|
431
|
-
//
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|