tide-commander 1.150.6 → 1.151.1

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 (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/assets/{BossLogsModal-D-tjw9Ac.js → BossLogsModal-BRXVs2zX.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-DjXpREdF.js → BossSpawnModal-BqM8EeOS.js} +1 -1
  4. package/dist/assets/{ControlsModal-B0RCXoei.js → ControlsModal-Do6sfsyz.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-mumJuNPo.js → DockerLogsModal-Clb8M2rL.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-DfoY9sRz.js → EmbeddedEditor-DMFNGABf.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-BpyUQtYp.js → GmailOAuthSetup-D4-bi1Rt.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-C9QQjGf2.js → GoogleOAuthSetup-D2Wyhnw0.js} +1 -1
  9. package/dist/assets/{IframeModal-BWLfI74y.js → IframeModal-Zun7245W.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-0uKXKkLf.js → IntegrationsPanel-U8SIOpPt.js} +2 -2
  11. package/dist/assets/{LogViewerModal-B7Gi86T6.js → LogViewerModal-BnFRX2hq.js} +1 -1
  12. package/dist/assets/{MonitoringModal-xji_Xv-B.js → MonitoringModal-CObUxMPl.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-H8nPhO-m.js → PM2LogsModal-DLf14YCU.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-Ozqt_h97.js → RestoreArchivedAreaModal-Chg4LCCi.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-s79Q3zJP.js → Scene2DCanvas-CI_QSx9i.js} +1 -1
  16. package/dist/assets/{SceneManager-BZdHLI98.js → SceneManager-SS5ahbEe.js} +1 -1
  17. package/dist/assets/{SkillsPanel-BXJS-7XJ.js → SkillsPanel-DgPHgt3c.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-DaQBxH5G.js → SlackMultiInstanceSetup-iIjyM2kW.js} +1 -1
  19. package/dist/assets/{SpawnModal-CFTdUpq9.js → SpawnModal-BDgxHGYh.js} +1 -1
  20. package/dist/assets/{StatisticsModal-BoSeR-R6.js → StatisticsModal-BuYkJsul.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-C5t06fd5.js → SubordinateAssignmentModal-D71lnDXZ.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-CBOgu5O-.js → TriggerManagerPanel-DKiXSpSu.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-BaGkass4.js → WorkflowEditorPanel-Dz6fJy8X.js} +1 -1
  24. package/dist/assets/{area-logos-B6OVpGpC.js → area-logos-UIiMtczR.js} +1 -1
  25. package/dist/assets/{index-COpZfWU7.js → index-4BenQ81a.js} +5 -5
  26. package/dist/assets/{index-BSc9Q00B.js → index-BAvhLzXF.js} +3 -3
  27. package/dist/assets/{index-DAFiQxXk.js → index-BXnQVuar.js} +1 -1
  28. package/dist/assets/{index-eTW--ftk.js → index-BZKCuVK4.js} +2 -2
  29. package/dist/assets/index-CLA5nZlA.css +1 -0
  30. package/dist/assets/{index-DC-CNVYP.js → index-Ci67fScw.js} +1 -1
  31. package/dist/assets/index-DgxY9KIr.js +2 -0
  32. package/dist/assets/{index-D-TSUXRk.js → index-DrUrcPns.js} +1 -1
  33. package/dist/assets/index-DvSK7N4C.js +1 -0
  34. package/dist/assets/{index-CKXhw5I3.js → index-OQ-kWRyS.js} +1 -1
  35. package/dist/assets/main-CEq_Fs_S.js +289 -0
  36. package/dist/assets/main-CX7XkoZ4.css +1 -0
  37. package/dist/assets/{web-CT5Q3JAF.js → web-BSnmr2eo.js} +1 -1
  38. package/dist/assets/{web-Hha4yEWN.js → web-DSHhrW0P.js} +1 -1
  39. package/dist/assets/{web-DxtBxLo3.js → web-jsCXVjsN.js} +1 -1
  40. package/dist/index.html +2 -2
  41. package/dist/locales/en/config.json +14 -0
  42. package/dist/locales/en/terminal.json +3 -1
  43. package/dist/src/packages/server/data/builtin-skills/release-pipeline.js +18 -16
  44. package/dist/src/packages/server/grok/backend.js +3 -2
  45. package/dist/src/packages/server/routes/system.js +66 -0
  46. package/dist/src/packages/server/services/runtime-command-execution.js +53 -34
  47. package/dist/src/packages/server/services/runtime-service.js +4 -4
  48. package/dist/src/packages/server/websocket/handlers/command-handler.js +9 -8
  49. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +2 -2
  50. package/dist/src/packages/shared/agent-types.js +22 -0
  51. package/package.json +1 -1
  52. package/dist/assets/index-BhOEfqZJ.js +0 -1
  53. package/dist/assets/index-BtQSoC86.js +0 -2
  54. package/dist/assets/index-CHvsg8e5.css +0 -1
  55. package/dist/assets/main-BsgKYLZr.css +0 -1
  56. package/dist/assets/main-KW9NTHdK.js +0 -289
@@ -1 +1 @@
1
- import{dd as a}from"./main-KW9NTHdK.js";import{ImpactStyle as i,NotificationType as r}from"./index-eTW--ftk.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-CEq_Fs_S.js";import{ImpactStyle as i,NotificationType as r}from"./index-BZKCuVK4.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 t}from"./main-KW9NTHdK.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-CEq_Fs_S.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 s}from"./main-KW9NTHdK.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-CEq_Fs_S.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-KW9NTHdK.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-CEq_Fs_S.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-BsgKYLZr.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-CX7XkoZ4.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -553,6 +553,7 @@
553
553
  "kenneyCredit": "for the agent character models",
554
554
  "claudeCodeCredit": "by Anthropic, the AI backbone",
555
555
  "autoUpdateTitle": "Auto-update",
556
+ "autoUpdateServerTitle": "Server auto-update (npm)",
556
557
  "autoUpdateButton": "Update now",
557
558
  "autoUpdateRunning": "Installing update...",
558
559
  "autoUpdateConfirmTitle": "Update Tide Commander?",
@@ -569,6 +570,7 @@
569
570
  "autoUpdateManualCommand": "Run this in your terminal to update manually:",
570
571
  "autoUpdatePackageManagerNotice": "Auto-update only supports npm-installed globals. Detected: {{pm}}.",
571
572
  "unattendedTitle": "Automatic updates",
573
+ "unattendedServerTitle": "Server automatic updates",
572
574
  "unattendedDesc": "Install updates automatically when no agent is working (checks every 30 min)",
573
575
  "unattendedPendingRestart": "v{{version}} installed — will restart as soon as all agents are idle",
574
576
  "unattendedUnsupported": "Only available for npm global installs (runs on the server, not in dev mode)"
@@ -604,5 +606,17 @@
604
606
  "clickToChange": "Click to change shortcut",
605
607
  "clearShortcut": "Clear shortcut",
606
608
  "conflictsWith": "Conflicts with: {{names}}"
609
+ },
610
+ "appUpdateBanner": {
611
+ "newVersion": "App update {{version}} available",
612
+ "update": "Update",
613
+ "download": "Download",
614
+ "dismiss": "Ignore",
615
+ "downloading": "Downloading update…",
616
+ "downloadingPct": "Downloading update… {{percent}}%",
617
+ "installHint": "Download complete — confirm the install prompt to finish updating",
618
+ "close": "Close",
619
+ "retry": "Retry",
620
+ "failed": "App update failed"
607
621
  }
608
622
  }
@@ -53,8 +53,10 @@
53
53
  "queued": "queued",
54
54
  "show": "Show",
55
55
  "enforce": "Send now",
56
+ "enforceInterrupt": "Send now (interrupt current work)",
56
57
  "delete": "Delete",
57
- "tooltip": "{{count}} message(s) queued — will send when agent is idle",
58
+ "tooltip": "{{count}} message(s) queued — will send when agent is free",
59
+ "tooltipWorking": "{{count}} message(s) queued — will send when agent is free. Use Send now to interrupt.",
58
60
  "modal": {
59
61
  "title": "Queued message"
60
62
  }
@@ -1,7 +1,7 @@
1
1
  export const releasePipeline = {
2
2
  slug: 'release-pipeline',
3
3
  name: 'TC Release Pipeline',
4
- description: 'Full release workflow: lint, type-check, test, build, APK artifacts, version bump, changelog, git tag, GitHub release, npm public publish. Use when asked to release, publish, ship, or do a full build pipeline.',
4
+ description: 'Full release workflow: lint, type-check, test, version bump, changelog, build + APK artifact, git tag, GitHub release, npm public publish. Use when asked to release, publish, ship, or do a full build pipeline.',
5
5
  allowedTools: [
6
6
  'Bash(git:*)',
7
7
  'Bash(npm:*)',
@@ -16,7 +16,9 @@ export const releasePipeline = {
16
16
  ],
17
17
  content: `# Release Pipeline
18
18
 
19
- Full Tide Commander release: quality checks, web app + APK builds, version bump, changelog, tag, push, GitHub release with APKs attached, public npm publish.
19
+ Full Tide Commander release: quality checks, version bump, changelog, web app + APK build, tag, push, GitHub release with the APK attached, public npm publish.
20
+
21
+ PHASE ORDER IS LOAD-BEARING: the version bump (Phase 3) MUST run BEFORE the build (Phase 5). The web bundle bakes \`__APP_VERSION__\` and the Android build reads versionName/versionCode from package.json at build time — bumping after building shipped APKs that reported the PREVIOUS version (the v1.150.6 release APK embedded 1.150.5), so freshly-updated phones immediately saw themselves as outdated again and the in-app updater looped forever.
20
22
 
21
23
  ## Execution Model: Sub-Agent Delegation
22
24
 
@@ -81,17 +83,7 @@ Spawn all 3 Agent calls in a single response, each using the curl template above
81
83
 
82
84
  On FAIL, the sub-agent reports the lint output / type errors / failing test details. If ANY of the 3 reports FAIL: STOP and report all failures. Proceed only if all 3 PASS.
83
85
 
84
- ### Phase 3: Build (sub-agents; APK builds depend on web build output)
85
-
86
- First spawn one sub-agent: \`npm run build\` — PASS if exitCode 0, else FAIL with the build error output.
87
-
88
- If web build PASSED, spawn 2 APK sub-agents in parallel (one response):
89
- - Debug APK: \`make apk\` (runs npx cap sync android + gradlew assembleDebug). Output APK: android/app/build/outputs/apk/debug/app-debug.apk
90
- - Non-dev debug APK (signing-safe artifact): \`make apk-release-nondev\`. Output APK: android/app/build/outputs/apk/debug/app-debug.apk
91
-
92
- Each reports PASS if exitCode 0, else FAIL with the error. If any build sub-agent FAILs: STOP and report.
93
-
94
- ### Phase 4: Version Bump (run yourself — requires judgment, sequential)
86
+ ### Phase 3: Version Bump (run yourself requires judgment, sequential; MUST precede the build)
95
87
 
96
88
  \`\`\`bash
97
89
  npm pkg get version
@@ -106,7 +98,9 @@ Analyze commits since the last tag and decide the bump yourself from conventiona
106
98
  npm version <patch|minor|major> --no-git-tag-version
107
99
  \`\`\`
108
100
 
109
- ### Phase 5: Update Changelog (run yourself)
101
+ The Android versionCode/versionName derive automatically from package.json (android/app/build.gradle reads it at build time) — do NOT edit build.gradle and do NOT worry about versionCode; bumping package.json is enough.
102
+
103
+ ### Phase 4: Update Changelog (run yourself)
110
104
 
111
105
  \`\`\`bash
112
106
  git log --oneline $(git describe --tags --abbrev=0 2>/dev/null || echo "HEAD~20")..HEAD
@@ -132,6 +126,14 @@ Add the new version entry at the top of \`CHANGELOG.md\` (directly below the \`#
132
126
 
133
127
  CRITICAL — keep the per-version header EXACTLY \`## [X.Y.Z] - YYYY-MM-DD\` (Keep a Changelog): one header per release, newest at the top. The in-app changelog modal extracts a single release by matching the line \`## [X.Y.Z]\`; any other header format breaks the per-version view (the full-changelog view still works). Do NOT drop the \`[\` \`]\` brackets, the version number, or change the \` - YYYY-MM-DD\` suffix.
134
128
 
129
+ ### Phase 5: Build (ONE sub-agent, AFTER the version bump)
130
+
131
+ Spawn a single sub-agent: \`make apk-release-nondev\` — it clears CAP_SERVER_URL, builds the web app (\`npm run build\`), syncs Capacitor, and builds the APK. Output: android/app/build/outputs/apk/debug/app-debug.apk. PASS if exitCode 0 and the APK file exists, else FAIL with the error output.
132
+
133
+ Do NOT also run \`make apk\`, and NEVER run two APK builds in parallel: both write the same dist/ and the same output APK path (a race that can publish a corrupted or wrong artifact), and \`make apk\` inherits any ambient CAP_SERVER_URL — which bakes a private dev-server URL into a public release. \`make apk-release-nondev\` is the ONLY release artifact build.
134
+
135
+ (The npm tarball does not depend on this build — package.json \`prepack\` rebuilds dist at publish time.)
136
+
135
137
  ### Phase 6: Commit, Tag, Push (run yourself — sequential git ops)
136
138
 
137
139
  \`\`\`bash
@@ -195,8 +197,8 @@ If the sub-agent reports FAIL: STOP and report the error to the user.
195
197
  ## Partial Workflows
196
198
 
197
199
  - "check quality" / "run checks" / "lint and test" / "pre-release check": Phase 2 only (parallel sub-agents); report results without proceeding.
198
- - "build" / "build everything" / "build apk": Phase 3 only; skip version bump and release.
199
- - "tag" / "create release" / "push release" (version already bumped): skip Phases 2-4; run Phases 5-6 directly, Phase 7 via sub-agent.
200
+ - "build" / "build everything" / "build apk": Phase 5 only (builds the current package.json version); skip version bump and release.
201
+ - "tag" / "create release" / "push release" (version already bumped): skip Phases 2-3; run Phases 4-6 directly (changelog, build, commit/tag/push), Phase 7 via sub-agent.
200
202
 
201
203
  ## Failure Handling
202
204
 
@@ -217,8 +217,9 @@ export class GrokBackend {
217
217
  return false;
218
218
  }
219
219
  /**
220
- * Mark as stdin-closed so follow-up messages queue and respawn with --resume
221
- * (same delivery path as Codex/OpenCode).
220
+ * Mark as stdin-closed so mid-run follow-ups queue until the process exits,
221
+ * then respawn with --resume (same delivery path as Codex/OpenCode).
222
+ * Callers may force-interrupt instead via sendCommand({ forceInterrupt: true }).
222
223
  */
223
224
  shouldCloseStdinAfterPrompt() {
224
225
  return true;
@@ -74,6 +74,72 @@ router.get('/changelog', (_req, res) => {
74
74
  res.status(500).json({ error: message });
75
75
  }
76
76
  });
77
+ /**
78
+ * GET /api/system/app-update
79
+ *
80
+ * Update metadata for the Android APK, checked server-side. The device asks
81
+ * its own server instead of api.github.com because phones sit behind carrier
82
+ * CGNAT where GitHub's unauthenticated 60 req/hour/IP quota routinely 403s —
83
+ * the same failure that forced the changelog to be served locally. Responses
84
+ * are cached; on a GitHub outage the last good payload is served stale.
85
+ */
86
+ const GITHUB_REPO = 'deivid11/tide-commander';
87
+ const APP_UPDATE_CACHE_TTL_MS = 10 * 60 * 1000;
88
+ let appUpdateCache = null;
89
+ router.get('/app-update', async (_req, res) => {
90
+ if (appUpdateCache && Date.now() - appUpdateCache.fetchedAt < APP_UPDATE_CACHE_TTL_MS) {
91
+ res.json(appUpdateCache.payload);
92
+ return;
93
+ }
94
+ try {
95
+ const headers = {
96
+ Accept: 'application/vnd.github.v3+json',
97
+ // GitHub rejects requests without a User-Agent
98
+ 'User-Agent': 'tide-commander-server',
99
+ };
100
+ if (process.env.GITHUB_TOKEN) {
101
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
102
+ }
103
+ const [latestRes, listRes] = await Promise.all([
104
+ fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, { headers }),
105
+ fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=3`, { headers }),
106
+ ]);
107
+ if (!latestRes.ok) {
108
+ throw new Error(`GitHub API error: ${latestRes.status}`);
109
+ }
110
+ const release = (await latestRes.json());
111
+ const list = listRes.ok ? (await listRes.json()) : [];
112
+ const apkAsset = release.assets.find((asset) => asset.name.endsWith('.apk') &&
113
+ asset.content_type === 'application/vnd.android.package-archive');
114
+ const payload = {
115
+ latestVersion: release.tag_name,
116
+ name: release.name,
117
+ changelog: release.body,
118
+ releaseUrl: release.html_url,
119
+ apkUrl: apkAsset?.browser_download_url ?? null,
120
+ apkSize: apkAsset?.size ?? null,
121
+ publishedAt: release.published_at ?? null,
122
+ recentReleases: list.map((r) => ({
123
+ version: r.tag_name,
124
+ name: r.name,
125
+ publishedAt: r.published_at,
126
+ releaseUrl: r.html_url,
127
+ })),
128
+ };
129
+ appUpdateCache = { fetchedAt: Date.now(), payload };
130
+ res.json(payload);
131
+ }
132
+ catch (err) {
133
+ const message = err.message;
134
+ if (appUpdateCache) {
135
+ log.warn(`App-update check failed (${message}) — serving stale cache`);
136
+ res.json(appUpdateCache.payload);
137
+ return;
138
+ }
139
+ log.error(`App-update check failed: ${message}`);
140
+ res.status(502).json({ error: message });
141
+ }
142
+ });
77
143
  /**
78
144
  * POST /api/system/self-update
79
145
  *
@@ -1,3 +1,4 @@
1
+ import { providerDisplayName } from '../../shared/types.js';
1
2
  import * as agentService from './agent-service.js';
2
3
  import { withAgentContext } from '../utils/log-context.js';
3
4
  import { clearPendingSilentContextRefresh, clearStdinWatchdog, hasPendingSilentContextRefresh, markPendingSilentContextRefresh, startStdinWatchdog, } from './runtime-watchdog.js';
@@ -69,10 +70,10 @@ export function createRuntimeCommandExecution(deps) {
69
70
  forceNewSession,
70
71
  });
71
72
  }
72
- async function sendCommand(agentId, command, systemPrompt, forceNewSession, customAgent) {
73
- return withAgentContext(agentId, () => sendCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent));
73
+ async function sendCommand(agentId, command, systemPrompt, forceNewSession, customAgent, opts) {
74
+ return withAgentContext(agentId, () => sendCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent, opts));
74
75
  }
75
- async function sendCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent) {
76
+ async function sendCommandImpl(agentId, command, systemPrompt, forceNewSession, customAgent, opts) {
76
77
  const agent = agentService.getAgent(agentId);
77
78
  if (!agent) {
78
79
  throw new Error(`Agent not found: ${agentId}`);
@@ -82,44 +83,62 @@ export function createRuntimeCommandExecution(deps) {
82
83
  throw new Error(`Runtime provider not initialized: ${agent.provider}`);
83
84
  }
84
85
  const processRunning = runner.isRunning(agentId);
85
- // For backends that close stdin after the initial prompt (codex, opencode),
86
- // a new user prompt arriving while the process is alive should INTERRUPT
87
- // the current work and RESTART the session with the new prompt — not wait
88
- // for the current turn to finish and then deliver the queued message via
89
- // session-resume (which is the default queue-based behavior).
86
+ // Backends that close stdin after the initial prompt (Grok, Codex, OpenCode)
87
+ // cannot receive mid-run follow-ups via a pipe. Two delivery modes:
90
88
  //
91
- // Why no turnState gate: stdin is closed, so there's literally no way to
92
- // deliver a follow-up prompt without respawning. 'waiting_for_input' is
93
- // also not a reliable "turn is over" signal across these backends —
94
- // opencode's NDJSON emits `step_finish` per LLM step (see
95
- // src/packages/server/opencode/json-event-parser.ts:197-207), so during a
96
- // single conversational turn the runner's turnState oscillates
97
- // processing → waiting_for_input → processing → … between steps. Gating
98
- // on 'waiting_for_input' would strand the new prompt on those mid-turn
99
- // windows: sendMessage would queue it, but the process isn't exiting
100
- // (it's starting the next step), so the queue-respawn path never fires.
101
- // Codex happens to emit `step_complete` once per turn (via
102
- // src/packages/server/codex/json-event-parser.ts:300-301), so the old
103
- // gate looked fine there — but the correct invariant for ALL stdin-closed
104
- // backends is: process-alive + new-prompt ⇒ interrupt + respawn.
89
+ // 1. Default QUEUE until the process exits, then respawn with session
90
+ // resume and deliver via ClaudeRunner.messageQueue
91
+ // (respawnWithQueuedMessage). Soft mid-run guidance ("continua",
92
+ // "also do X") should not burn the in-flight turn.
105
93
  //
106
- // SIGINT on a process already at true turn-end (about to exit cleanly on
107
- // its own) is harmless process-lifecycle's stop() just short-circuits
108
- // the natural exit, and the fresh spawn still resumes the same sessionId.
94
+ // 2. forceInterrupt stop the process (clearQueue=true so prior queued
95
+ // mid-run messages are dropped as stale) and immediately respawn with
96
+ // this prompt. Used when the user explicitly wants "do this now
97
+ // instead" (UI "Send now" / forceInterrupt flag).
109
98
  //
110
- // Safe for tmux mode: runner.stop() routes through
111
- // src/packages/server/claude/runner/process-lifecycle.ts:281-284 which
112
- // calls killTmuxSession this destroys only the agent's detached tmux
113
- // session (created by spawnInTmux), NOT the user's own pane/window. The
114
- // subsequent executeCommand spawns a fresh tmux session for the new turn.
99
+ // Why no turnState gate on the queue path: 'waiting_for_input' is not a
100
+ // reliable "turn is over" signal across these backends — OpenCode's NDJSON
101
+ // emits `step_finish` per LLM step, so turnState oscillates within a single
102
+ // conversational turn. The queue is drained on process exit (the only
103
+ // trustworthy boundary for stdin-closed CLIs).
115
104
  //
116
- // clearQueue=true is intentional: user is replacing whatever was pending
117
- // with this new command, so any queued messages from prior mid-turn sends
118
- // are stale and must be dropped.
105
+ // Safe for tmux mode: runner.stop() only kills the agent's detached tmux
106
+ // session (spawnInTmux), not the user's own pane.
119
107
  const backendClosesStdin = runner.closesStdinAfterPrompt?.() === true;
120
108
  if (processRunning && backendClosesStdin && !forceNewSession) {
121
109
  const turnState = runner.getTurnState?.(agentId);
122
- log.log(`[sendCommand] Agent ${agentId} (${agent.provider}): in-flight prompt (turnState=${turnState ?? 'unknown'}) — interrupting current work and restarting session with new prompt`);
110
+ const providerLabel = providerDisplayName(agent.provider);
111
+ if (opts?.forceInterrupt) {
112
+ log.log(`[sendCommand] Agent ${agentId} (${agent.provider}): forceInterrupt mid-run (turnState=${turnState ?? 'unknown'}) — interrupting and restarting with new prompt`);
113
+ emitOutput(agentId, '🛑 [System] Interrupting current work to process new prompt…', false, undefined, 'system-interrupt-restart');
114
+ // clearQueue=true: user is replacing pending mid-run work with this command
115
+ await runner.stop(agentId, true);
116
+ agentService.updateAgent(agentId, { taskCount: (agent.taskCount || 0) + 1 });
117
+ await executeCommand(agentId, command, systemPrompt, forceNewSession, customAgent);
118
+ return;
119
+ }
120
+ // Default: queue for delivery after the current turn completes.
121
+ const queued = runner.sendMessage(agentId, command);
122
+ if (queued) {
123
+ log.log(`[sendCommand] Agent ${agentId} (${agent.provider}): mid-run prompt queued (turnState=${turnState ?? 'unknown'}, ${command.length} chars)`);
124
+ notifyCommandStarted(agentId, command, { queued: true });
125
+ emitOutput(agentId, `⏳ [System] Mid-run message for ${providerLabel} will be sent when the agent is free`, false, undefined, `system-midrun-queued-${Date.now()}`);
126
+ const isSystemMessage = command.startsWith('[System:');
127
+ if (!isSystemMessage) {
128
+ agentService.updateAgent(agentId, {
129
+ taskCount: (agent.taskCount || 0) + 1,
130
+ lastAssignedTask: command,
131
+ lastAssignedTaskTime: Date.now(),
132
+ });
133
+ }
134
+ else {
135
+ agentService.updateAgent(agentId, { taskCount: (agent.taskCount || 0) + 1 });
136
+ }
137
+ return;
138
+ }
139
+ // sendMessage failed (no active process race) — fall through to interrupt
140
+ // so the prompt is not dropped.
141
+ log.warn(`[sendCommand] Agent ${agentId}: mid-run queue failed, falling through to interrupt+respawn`);
123
142
  emitOutput(agentId, '🛑 [System] Interrupting current work to process new prompt…', false, undefined, 'system-interrupt-restart');
124
143
  await runner.stop(agentId, true);
125
144
  agentService.updateAgent(agentId, { taskCount: (agent.taskCount || 0) + 1 });
@@ -39,9 +39,9 @@ let commandStartedCallback = null;
39
39
  export function setCommandStartedCallback(callback) {
40
40
  commandStartedCallback = callback;
41
41
  }
42
- function notifyCommandStarted(agentId, command) {
42
+ function notifyCommandStarted(agentId, command, opts) {
43
43
  if (commandStartedCallback) {
44
- commandStartedCallback(agentId, command);
44
+ commandStartedCallback(agentId, command, opts);
45
45
  }
46
46
  }
47
47
  // Callback for broadcasting session updates to clients
@@ -201,8 +201,8 @@ export function getActiveSubagentByToolUseId(toolUseId) {
201
201
  export function getActiveSubagentsForAgent(parentAgentId) {
202
202
  return getTrackedSubagentsForAgent(parentAgentId);
203
203
  }
204
- export async function sendCommand(agentId, command, systemPrompt, forceNewSession, customAgent) {
205
- await commandExecution.sendCommand(agentId, command, systemPrompt, forceNewSession, customAgent);
204
+ export async function sendCommand(agentId, command, systemPrompt, forceNewSession, customAgent, opts) {
205
+ await commandExecution.sendCommand(agentId, command, systemPrompt, forceNewSession, customAgent, opts);
206
206
  }
207
207
  export async function sendSilentCommand(agentId, command) {
208
208
  await commandExecution.sendSilentCommand(agentId, command);
@@ -276,7 +276,7 @@ export function buildCustomAgentConfig(agentId, agentClass) {
276
276
  * Routes commands differently for boss agents vs regular agents
277
277
  */
278
278
  export async function handleSendCommand(ctx, payload, buildBossMessage) {
279
- const { agentId, command } = payload;
279
+ const { agentId, command, forceInterrupt } = payload;
280
280
  const agent = agentService.getAgent(agentId);
281
281
  if (!agent) {
282
282
  log.error(` Agent not found: ${agentId}`);
@@ -312,19 +312,20 @@ export async function handleSendCommand(ctx, payload, buildBossMessage) {
312
312
  }
313
313
  // Expand [@file:path] and [@folder:path] mentions by injecting file content
314
314
  const finalCommand = await expandFileMentions(command, agent.cwd);
315
+ const sendOpts = forceInterrupt ? { forceInterrupt: true } : undefined;
315
316
  // If this is a boss agent, handle differently
316
317
  if (agent.isBoss || agent.class === 'boss') {
317
- await handleBossCommand(ctx, agentId, finalCommand, agent.name, buildBossMessage);
318
+ await handleBossCommand(ctx, agentId, finalCommand, agent.name, buildBossMessage, sendOpts);
318
319
  }
319
320
  else {
320
- await handleRegularAgentCommand(ctx, agentId, finalCommand, agent);
321
+ await handleRegularAgentCommand(ctx, agentId, finalCommand, agent, sendOpts);
321
322
  }
322
323
  }
323
324
  /**
324
325
  * Handle command for boss agents
325
326
  * Boss agents get context injected in the user message
326
327
  */
327
- async function handleBossCommand(ctx, agentId, command, agentName, buildBossMessage) {
328
+ async function handleBossCommand(ctx, agentId, command, agentName, buildBossMessage, sendOpts) {
328
329
  log.log(` Boss ${agentName} received command: "${command.slice(0, 50)}..."`);
329
330
  // Track the last command sent to this boss (for delegation parsing)
330
331
  lastBossCommands.set(agentId, command);
@@ -336,12 +337,12 @@ async function handleBossCommand(ctx, agentId, command, agentName, buildBossMess
336
337
  // Also build customAgentConfig so boss gets its assigned skills (e.g. boss-instructions)
337
338
  const agent = agentService.getAgent(agentId);
338
339
  const customAgentConfig = agent ? buildCustomAgentConfig(agentId, agent.class) : undefined;
339
- runtimeService.sendCommand(agentId, bossMessage, systemPrompt, undefined, customAgentConfig);
340
+ runtimeService.sendCommand(agentId, bossMessage, systemPrompt, undefined, customAgentConfig, sendOpts);
340
341
  }
341
342
  catch (err) {
342
343
  log.error(` Boss ${agentName}: failed to build boss message:`, err);
343
344
  // Fallback to sending raw command
344
- runtimeService.sendCommand(agentId, command);
345
+ runtimeService.sendCommand(agentId, command, undefined, undefined, undefined, sendOpts);
345
346
  }
346
347
  if (isTeamQuestion) {
347
348
  log.log(` Boss ${agentName}: detected team question`);
@@ -354,7 +355,7 @@ async function handleBossCommand(ctx, agentId, command, agentName, buildBossMess
354
355
  * Handle command for regular agents
355
356
  * Regular agents get custom class instructions and skills combined into a prompt
356
357
  */
357
- async function handleRegularAgentCommand(ctx, agentId, command, agent) {
358
+ async function handleRegularAgentCommand(ctx, agentId, command, agent, sendOpts) {
358
359
  // Note: /context, /cost, /compact are intercepted at the handleSendCommand level
359
360
  // so they never reach here. This function only handles actual commands to send to the agent.
360
361
  const customAgentConfig = buildCustomAgentConfig(agentId, agent.class);
@@ -395,7 +396,7 @@ async function handleRegularAgentCommand(ctx, agentId, command, agent) {
395
396
  skillService.clearPendingSkillUpdates(agentId);
396
397
  }
397
398
  try {
398
- await runtimeService.sendCommand(agentId, finalCommand, undefined, undefined, customAgentConfig);
399
+ await runtimeService.sendCommand(agentId, finalCommand, undefined, undefined, customAgentConfig, sendOpts);
399
400
  }
400
401
  catch (err) {
401
402
  log.error(' Failed to send command:', err);
@@ -459,10 +459,10 @@ export function setupRuntimeListeners(ctx) {
459
459
  pendingBashCommands.delete(agentId);
460
460
  ctx.sendActivity(agentId, `Error: ${error}`);
461
461
  });
462
- runtimeService.setCommandStartedCallback((agentId, command) => {
462
+ runtimeService.setCommandStartedCallback((agentId, command, opts) => {
463
463
  ctx.broadcast({
464
464
  type: 'command_started',
465
- payload: { agentId, command },
465
+ payload: { agentId, command, queued: opts?.queued },
466
466
  });
467
467
  });
468
468
  runtimeService.setSessionUpdateCallback((agentId) => {
@@ -16,6 +16,28 @@ export const PERMISSION_MODES = {
16
16
  bypass: { label: 'Permissionless', description: 'Skip all permission prompts (less safe, faster)' },
17
17
  interactive: { label: 'Interactive', description: 'Ask for approval before sensitive operations' },
18
18
  };
19
+ /**
20
+ * Providers whose CLI takes the prompt via argv/file and closes stdin after
21
+ * launch. Mid-run follow-ups cannot be written to stdin — they must be queued
22
+ * until the process exits (or force-interrupted and respawned with --resume).
23
+ */
24
+ export function providerClosesStdinAfterPrompt(provider) {
25
+ return provider === 'codex' || provider === 'opencode' || provider === 'grok';
26
+ }
27
+ export function providerDisplayName(provider) {
28
+ switch (provider) {
29
+ case 'codex':
30
+ return 'Codex';
31
+ case 'opencode':
32
+ return 'OpenCode';
33
+ case 'grok':
34
+ return 'Grok';
35
+ case 'claude':
36
+ return 'Claude';
37
+ default:
38
+ return provider ? String(provider) : 'agent';
39
+ }
40
+ }
19
41
  export const GROK_MODELS = {
20
42
  'grok-4.5': {
21
43
  label: 'Grok 4.5',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.150.6",
3
+ "version": "1.151.1",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",