tide-commander 1.150.5 → 1.151.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 +24 -0
- package/dist/assets/{BossLogsModal-Dxigta6E.js → BossLogsModal-D-S4rOrC.js} +1 -1
- package/dist/assets/{BossSpawnModal-SuML_v9m.js → BossSpawnModal-DdDd2bcg.js} +1 -1
- package/dist/assets/{ControlsModal-DqoA0S-G.js → ControlsModal-BFYaYJbi.js} +1 -1
- package/dist/assets/{DockerLogsModal-BqLcjFfy.js → DockerLogsModal-YYGlZk3h.js} +1 -1
- package/dist/assets/{EmbeddedEditor-CHQYCaqS.js → EmbeddedEditor-BigtXB5T.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-CLsT2bZB.js → GmailOAuthSetup-UiaeOEz1.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-mDShfn-l.js → GoogleOAuthSetup-HvV20jH2.js} +1 -1
- package/dist/assets/{IframeModal-BnwRV54k.js → IframeModal-wlSqLJZq.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BcAoN2Z5.js → IntegrationsPanel-D6uSg71w.js} +2 -2
- package/dist/assets/{LogViewerModal-N2Jh8lUf.js → LogViewerModal-BPgqylsz.js} +1 -1
- package/dist/assets/{MonitoringModal-D58d69_o.js → MonitoringModal-OkyYCiN2.js} +1 -1
- package/dist/assets/{PM2LogsModal-BZQxIzRo.js → PM2LogsModal-kvkSsKxO.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-Be4XKsp0.js → RestoreArchivedAreaModal-C03VBun0.js} +1 -1
- package/dist/assets/{Scene2DCanvas-CuomfRYj.js → Scene2DCanvas-DghU6Gn_.js} +1 -1
- package/dist/assets/{SceneManager-CMSoguzc.js → SceneManager-70TSeT5o.js} +1 -1
- package/dist/assets/{SkillsPanel-1dtlCTBK.js → SkillsPanel-CFPmptA3.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-DiVVx1_a.js → SlackMultiInstanceSetup-MqGVgYER.js} +1 -1
- package/dist/assets/{SpawnModal-Dv4sjH4X.js → SpawnModal-HmVHUU8h.js} +1 -1
- package/dist/assets/{StatisticsModal-DMwL6gTE.js → StatisticsModal-lUXImuXl.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-k97D9H4O.js → SubordinateAssignmentModal-CzKL1nOB.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Dz7h7AIE.js → TriggerManagerPanel-BYX4Hva3.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CIC5FnoC.js → WorkflowEditorPanel-C4LutCZx.js} +1 -1
- package/dist/assets/{area-logos-CFw9rGcl.js → area-logos-CaRg99D1.js} +1 -1
- package/dist/assets/index-CMW39Y0k.js +1 -0
- package/dist/assets/{index-tVFqrSl0.js → index-CQAYomCy.js} +5 -5
- package/dist/assets/{index-0r9c3uBq.js → index-CS0lWvoz.js} +3 -3
- package/dist/assets/{index-C5XSGzmA.js → index-CSlz7pDe.js} +1 -1
- package/dist/assets/{index-C_00vCRv.js → index-CZ100uIL.js} +1 -1
- package/dist/assets/{index-hjhsIs0G.js → index-Cyfs1wMT.js} +1 -1
- package/dist/assets/{index-DHPU3A0Q.js → index-DMzFmBbv.js} +3 -3
- package/dist/assets/{index-BIxRvjd9.js → index-DdA0Ifmz.js} +1 -1
- package/dist/assets/{index-CF_H9g13.js → index-DklzfP3L.js} +2 -2
- package/dist/assets/main-FzDnoDwP.css +1 -0
- package/dist/assets/main-n5EAKGub.js +289 -0
- package/dist/assets/{web-Cc01r1gB.js → web-7hMdOejv.js} +1 -1
- package/dist/assets/{web-KJr1b2Lc.js → web-CFfQhn_0.js} +1 -1
- package/dist/assets/{web-BE3dPa-9.js → web-DddUX-gl.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/config.json +12 -0
- package/dist/locales/en/terminal.json +3 -1
- package/dist/src/packages/server/data/builtin-skills/release-pipeline.js +18 -16
- package/dist/src/packages/server/grok/backend.js +3 -2
- package/dist/src/packages/server/routes/system.js +66 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +53 -34
- package/dist/src/packages/server/services/runtime-service.js +4 -4
- package/dist/src/packages/server/websocket/handlers/command-handler.js +9 -8
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +2 -2
- package/dist/src/packages/shared/agent-types.js +22 -0
- package/package.json +1 -1
- package/dist/assets/index-DCoTXcYl.js +0 -1
- package/dist/assets/main-BsgKYLZr.css +0 -1
- package/dist/assets/main-vSVgpZh8.js +0 -289
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{dc as t}from"./main-n5EAKGub.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{dc as s}from"./main-n5EAKGub.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{dc as a}from"./main-n5EAKGub.js";import{ImpactStyle as i,NotificationType as r}from"./index-DklzfP3L.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-n5EAKGub.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-FzDnoDwP.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -604,5 +604,17 @@
|
|
|
604
604
|
"clickToChange": "Click to change shortcut",
|
|
605
605
|
"clearShortcut": "Clear shortcut",
|
|
606
606
|
"conflictsWith": "Conflicts with: {{names}}"
|
|
607
|
+
},
|
|
608
|
+
"appUpdateBanner": {
|
|
609
|
+
"newVersion": "App update {{version}} available",
|
|
610
|
+
"update": "Update",
|
|
611
|
+
"download": "Download",
|
|
612
|
+
"dismiss": "Ignore",
|
|
613
|
+
"downloading": "Downloading update…",
|
|
614
|
+
"downloadingPct": "Downloading update… {{percent}}%",
|
|
615
|
+
"installHint": "Download complete — confirm the install prompt to finish updating",
|
|
616
|
+
"close": "Close",
|
|
617
|
+
"retry": "Retry",
|
|
618
|
+
"failed": "App update failed"
|
|
607
619
|
}
|
|
608
620
|
}
|
|
@@ -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
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
199
|
-
- "tag" / "create release" / "push release" (version already bumped): skip Phases 2-
|
|
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-
|
|
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
|
-
//
|
|
86
|
-
//
|
|
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
|
-
//
|
|
92
|
-
// deliver
|
|
93
|
-
//
|
|
94
|
-
//
|
|
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
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
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
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
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
|
-
//
|
|
117
|
-
//
|
|
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
|
-
|
|
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',
|