@ctrl-spc/cs 0.7.13 → 0.7.15
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/README.md +50 -3
- package/dist/autostart.js +103 -122
- package/dist/companion-ui.js +34 -6
- package/dist/companion.js +59 -150
- package/dist/config.js +52 -1
- package/dist/daemon-lifecycle.js +548 -0
- package/dist/daemon-lock.js +149 -42
- package/dist/daemon-processes.js +756 -0
- package/dist/daemon.js +14 -46
- package/dist/darwin-coalition.js +340 -0
- package/dist/index.js +70 -74
- package/dist/login.js +5 -3
- package/dist/native/darwin-coalition +0 -0
- package/dist/native/darwin-coalition.build.json +1 -0
- package/dist/native/darwin-coalition.c +145 -0
- package/dist/orchestrator.js +620 -428
- package/dist/panel3/checkout.js +100 -8
- package/dist/panel3/prompt.js +16 -8
- package/dist/panel3/run.js +891 -541
- package/dist/panel3/spawn.js +59 -11
- package/dist/panel3/tools.js +28 -5
- package/dist/presence.js +183 -24
- package/dist/supabase.js +43 -9
- package/dist/win-shell.js +464 -1
- package/dist/windows-job.js +312 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -7,10 +7,11 @@ signing in and attaching your codebases to your projects.
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm i -g @ctrl-spc/cs
|
|
10
|
+
npm i -g @ctrl-spc/cs
|
|
11
|
+
cs
|
|
11
12
|
```
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Running `cs` opens your browser straight to sign-in — you barely touch
|
|
14
15
|
the terminal:
|
|
15
16
|
|
|
16
17
|
```
|
|
@@ -43,7 +44,11 @@ cs Open the Companion app (the front door)
|
|
|
43
44
|
cs open Open the Companion app in your browser
|
|
44
45
|
cs login Sign in from the terminal and link this computer
|
|
45
46
|
cs start Come online now, no window (used by auto-start)
|
|
46
|
-
cs status
|
|
47
|
+
cs status Inspect this service, its running version and cloud connection
|
|
48
|
+
cs stop Stop this service when no owned work is active
|
|
49
|
+
cs restart Load the installed CLI and keep it running in the background
|
|
50
|
+
cs stop --force Interrupt owned work, preserve recovery, then stop
|
|
51
|
+
cs restart --force Interrupt owned work, then load the installed CLI
|
|
47
52
|
cs autostart on Come online automatically at login
|
|
48
53
|
cs autostart off Stop coming online at login
|
|
49
54
|
cs logout Sign this computer out
|
|
@@ -53,5 +58,47 @@ cs help Show this help
|
|
|
53
58
|
## Requirements
|
|
54
59
|
|
|
55
60
|
- Node.js >= 22.
|
|
61
|
+
- Service stop and restart support macOS and Windows.
|
|
62
|
+
|
|
63
|
+
## Updating and restarting
|
|
64
|
+
|
|
65
|
+
Installing a package does not replace a service already running in memory. On
|
|
66
|
+
the computer you want to update, run:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm install -g @ctrl-spc/cs
|
|
70
|
+
cs restart
|
|
71
|
+
cs status
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`cs status` separates the version of the installed command from the verified
|
|
75
|
+
version running in the service. A successful restart stays running after the
|
|
76
|
+
terminal closes. `cs stop` leaves the computer's next-login startup preference
|
|
77
|
+
unchanged; `cs autostart off` changes that future preference.
|
|
78
|
+
|
|
79
|
+
A normal stop or restart refuses while an assignment is active. Let the work
|
|
80
|
+
finish, or explicitly use `--force` to interrupt the owned assignments. Saved
|
|
81
|
+
files and conversations remain; open each interrupted card and send a new
|
|
82
|
+
message to continue. Restarting the service does not automatically replay work.
|
|
83
|
+
|
|
84
|
+
Local service controls remain available without a cloud connection or usable
|
|
85
|
+
sign-in. Cloud readiness is reported separately. An offline interruption appears
|
|
86
|
+
in the app after the service can reconnect and reconcile it.
|
|
87
|
+
|
|
88
|
+
### One-time upgrade from an older service
|
|
89
|
+
|
|
90
|
+
A service installed before these lifecycle controls cannot prove which work is
|
|
91
|
+
still running. The new command prepares the future-login launcher and explains
|
|
92
|
+
when a one-time **computer restart** is required. Save your work, restart that
|
|
93
|
+
computer, then run `cs status` after signing in. If automatic startup is off, run
|
|
94
|
+
`cs start` first. Interrupted work needs an explicit new message to continue.
|
|
95
|
+
|
|
96
|
+
Later service stop and restart do not need a computer restart. If ownership or
|
|
97
|
+
execution cannot be verified, the command reports that problem and does not
|
|
98
|
+
start a second service.
|
|
99
|
+
|
|
100
|
+
The app's **Restart worker** control reconnects work processing inside the
|
|
101
|
+
current service. Activating an installed CLI update uses `cs restart` in a
|
|
102
|
+
terminal on the named computer.
|
|
56
103
|
|
|
57
104
|
Not sure what a piece does? Open the Companion (`cs`) and click **How it works**.
|
package/dist/autostart.js
CHANGED
|
@@ -1,141 +1,122 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { homedir } from 'node:os';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
6
8
|
import { configDir } from './config.js';
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
9
|
+
const BASE_LABEL = 'com.ctrl-spc.presence';
|
|
10
|
+
const execute = promisify(execFile);
|
|
11
|
+
/** Capture a loaded job only while its PID is the verified service owner. */
|
|
12
|
+
export async function loadedStartupJob(pid, deadline) {
|
|
13
|
+
if (process.platform !== 'darwin')
|
|
14
|
+
return null;
|
|
15
|
+
const target = 'gui/' + process.getuid() + '/' + startupLabel();
|
|
16
|
+
try {
|
|
17
|
+
const { stdout } = await execute('/bin/launchctl', ['print', target], { timeout: Math.max(1, Math.min(2000, deadline - Date.now())) });
|
|
18
|
+
const match = stdout.match(/^\s*pid = (\d+)\s*$/m);
|
|
19
|
+
return match && Number(match[1]) === pid ? target : null;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === 113)
|
|
23
|
+
return null;
|
|
24
|
+
throw new Error('The login service could not be inspected. No replacement was started.', { cause: error });
|
|
25
|
+
}
|
|
19
26
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* no-op (so the launchd/VBS-spawned `cs start` doesn't re-install itself), and
|
|
24
|
-
* an unsupported platform or transient error never breaks coming online.
|
|
25
|
-
*/
|
|
26
|
-
export function ensureAutostart() {
|
|
27
|
-
if (existsSync(offMarkerPath()) || autostartEnabled())
|
|
27
|
+
/** Work has already drained. Unload the current job, retaining its next-login file. */
|
|
28
|
+
export async function unloadStoppedStartupJob(target, deadline) {
|
|
29
|
+
if (!target)
|
|
28
30
|
return;
|
|
29
31
|
try {
|
|
30
|
-
|
|
32
|
+
await execute('/bin/launchctl', ['bootout', target], { timeout: Math.max(1, deadline - Date.now()) });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== 113)
|
|
36
|
+
throw new Error('The previous login service could not be unloaded. No replacement was started.', { cause: error });
|
|
31
37
|
}
|
|
32
|
-
catch { /* unsupported platform or transient failure — ignore */ }
|
|
33
|
-
}
|
|
34
|
-
function plistPath() {
|
|
35
|
-
return join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
function
|
|
39
|
-
|
|
39
|
+
function offMarkerPath() { return join(configDir(), 'autostart-off'); }
|
|
40
|
+
export function startupLabel() {
|
|
41
|
+
if (resolve(configDir()) === resolve(join(homedir(), '.config', 'ctrl-spc-v2')) && !process.env.CTRL_SPC_V2_MACHINE_ID)
|
|
42
|
+
return BASE_LABEL;
|
|
43
|
+
return BASE_LABEL + '.' + createHash('sha256').update(resolve(configDir()) + '\0' + (process.env.CTRL_SPC_V2_MACHINE_ID ?? '')).digest('hex').slice(0, 12);
|
|
40
44
|
}
|
|
41
|
-
|
|
42
|
-
export function
|
|
43
|
-
if (existsSync(offMarkerPath()))
|
|
44
|
-
rmSync(offMarkerPath());
|
|
45
|
+
export function entryPath() { return join(dirname(fileURLToPath(import.meta.url)), 'index.js'); }
|
|
46
|
+
export function startupPath() {
|
|
45
47
|
if (process.platform === 'darwin')
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
return join(homedir(), 'Library', 'LaunchAgents', startupLabel() + '.plist');
|
|
49
|
+
const appData = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
|
|
50
|
+
const stem = startupLabel() === BASE_LABEL ? 'ctrl-spc-presence' : startupLabel();
|
|
51
|
+
return join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', stem + '.vbs');
|
|
50
52
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
mkdirSync(configDir(), { recursive: true });
|
|
54
|
-
writeFileSync(offMarkerPath(), '');
|
|
55
|
-
if (process.platform === 'darwin')
|
|
56
|
-
return macOff();
|
|
57
|
-
if (process.platform === 'win32')
|
|
58
|
-
return winOff();
|
|
59
|
-
throw new Error('Auto-start supports macOS and Windows only.');
|
|
53
|
+
export function autostartEnabled() {
|
|
54
|
+
return !existsSync(offMarkerPath()) && ['darwin', 'win32'].includes(process.platform) && existsSync(startupPath());
|
|
60
55
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// login shim. Upgrade to a Scheduled Task with restart settings if the daemon
|
|
65
|
-
// crashing between logins ever matters.
|
|
66
|
-
function startupVbsPath() {
|
|
67
|
-
const appData = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
|
|
68
|
-
return join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'ctrl-spc-presence.vbs');
|
|
56
|
+
export function autostartDisabled() { return existsSync(offMarkerPath()); }
|
|
57
|
+
function escapeXml(value) {
|
|
58
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
69
59
|
}
|
|
70
|
-
function
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.log('Auto-start enabled. CTRL+SPC comes online at login. Disable: cs autostart off');
|
|
60
|
+
function instanceEnvironment() {
|
|
61
|
+
const env = { PATH: process.env.PATH ?? '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin', HOME: homedir(), USER: process.env.USER ?? process.env.USERNAME ?? '' };
|
|
62
|
+
for (const key of ['CTRL_SPC_V2_CONFIG_DIR', 'CTRL_SPC_V2_MACHINE_ID', 'CTRL_SPC_V2_COMPANION_PORT', 'CTRL_SPC_V2_TOOLS_PORT', 'CTRL_SPC_SUPABASE_URL', 'CTRL_SPC_SUPABASE_KEY', 'CTRL_SPC_V3_AGENT', 'CTRL_SPC_V2_MAX_CONCURRENT', 'CTRL_SPC_WEB_URL', 'CODEX_HOME', 'CLAUDE_CONFIG_DIR']) {
|
|
63
|
+
if (process.env[key])
|
|
64
|
+
env[key] = process.env[key];
|
|
65
|
+
}
|
|
66
|
+
return env;
|
|
78
67
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
function launcherContents() {
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
const quoted = (value) => value.replace(/"/g, '""');
|
|
71
|
+
const command = '"' + process.execPath + '" "' + entryPath() + '" start';
|
|
72
|
+
const env = Object.entries(instanceEnvironment()).map(([key, value]) => 's.Environment("PROCESS")("' + quoted(key) + '") = "' + quoted(value) + '"').join('\r\n');
|
|
73
|
+
return 'Set s = CreateObject("WScript.Shell")\r\n' + env + '\r\ns.Run "' + quoted(command) + '", 0, False\r\n';
|
|
74
|
+
}
|
|
75
|
+
const env = Object.entries(instanceEnvironment()).map(([key, value]) => '<key>' + escapeXml(key) + '</key><string>' + escapeXml(value) + '</string>').join('');
|
|
76
|
+
return '<?xml version="1.0" encoding="UTF-8"?>\n<plist version="1.0"><dict>' +
|
|
77
|
+
'<key>Label</key><string>' + startupLabel() + '</string>' +
|
|
78
|
+
'<key>ProgramArguments</key><array><string>' + escapeXml(process.execPath) + '</string><string>' + escapeXml(entryPath()) + '</string><string>start</string></array>' +
|
|
79
|
+
'<key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>' +
|
|
80
|
+
'<key>StandardOutPath</key><string>' + escapeXml(join(configDir(), 'daemon.log')) + '</string>' +
|
|
81
|
+
'<key>StandardErrorPath</key><string>' + escapeXml(join(configDir(), 'daemon.log')) + '</string>' +
|
|
82
|
+
'<key>EnvironmentVariables</key><dict>' + env + '</dict></dict></plist>\n';
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
sends you to re-authenticate something that was never broken. Reproduced
|
|
102
|
-
by hand: `env -i HOME=… PATH=… claude -p` fails, and adding USER alone
|
|
103
|
-
fixes it. HOME is set for the same reason, though launchd does supply it. */
|
|
104
|
-
const userName = process.env.USER ?? process.env.LOGNAME ?? '';
|
|
105
|
-
const logDir = join(homedir(), '.config', 'ctrl-spc-v2');
|
|
106
|
-
mkdirSync(logDir, { recursive: true });
|
|
107
|
-
mkdirSync(dirname(plistPath()), { recursive: true });
|
|
108
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
109
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
110
|
-
<plist version="1.0"><dict>
|
|
111
|
-
<key>Label</key><string>${LABEL}</string>
|
|
112
|
-
<key>ProgramArguments</key><array>
|
|
113
|
-
<string>${node}</string><string>${entry}</string><string>start</string>
|
|
114
|
-
</array>
|
|
115
|
-
<key>RunAtLoad</key><true/>
|
|
116
|
-
<key>KeepAlive</key><true/>
|
|
117
|
-
<key>StandardOutPath</key><string>${join(logDir, 'daemon.log')}</string>
|
|
118
|
-
<key>StandardErrorPath</key><string>${join(logDir, 'daemon.log')}</string>
|
|
119
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string><key>USER</key><string>${userName}</string><key>HOME</key><string>${homedir()}</string></dict>
|
|
120
|
-
</dict></plist>
|
|
121
|
-
`;
|
|
122
|
-
writeFileSync(plistPath(), plist);
|
|
84
|
+
/** Writes future-login configuration only. The lifecycle controller owns launch. */
|
|
85
|
+
export function prepareAutostart({ defaultOn = false } = {}) {
|
|
86
|
+
if (autostartDisabled())
|
|
87
|
+
return;
|
|
88
|
+
if (!['darwin', 'win32'].includes(process.platform)) {
|
|
89
|
+
if (defaultOn)
|
|
90
|
+
return; // Existing CLI permits manually launched Linux instances.
|
|
91
|
+
throw new Error('Automatic login startup supports macOS and Windows.');
|
|
92
|
+
}
|
|
93
|
+
if (!defaultOn && !autostartEnabled())
|
|
94
|
+
return;
|
|
95
|
+
mkdirSync(configDir(), { recursive: true });
|
|
96
|
+
const path = startupPath();
|
|
97
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
98
|
+
const text = launcherContents();
|
|
99
|
+
const temp = path + '.' + randomUUID() + '.tmp';
|
|
123
100
|
try {
|
|
124
|
-
|
|
101
|
+
writeFileSync(temp, text, { mode: 0o600 });
|
|
102
|
+
renameSync(temp, path);
|
|
125
103
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
console.log('Auto-start enabled. CTRL+SPC comes online at login. Disable: cs autostart off');
|
|
129
|
-
}
|
|
130
|
-
/** Removes the LaunchAgent. */
|
|
131
|
-
function macOff() {
|
|
132
|
-
const uid = process.getuid?.() ?? 0;
|
|
133
|
-
if (existsSync(plistPath())) {
|
|
134
|
-
try {
|
|
135
|
-
execFileSync('launchctl', ['bootout', `gui/${uid}`, plistPath()], { stdio: 'ignore' });
|
|
136
|
-
}
|
|
137
|
-
catch { /* already out */ }
|
|
138
|
-
rmSync(plistPath());
|
|
104
|
+
finally {
|
|
105
|
+
rmSync(temp, { force: true });
|
|
139
106
|
}
|
|
140
|
-
|
|
107
|
+
if (readFileSync(path, 'utf8') !== text)
|
|
108
|
+
throw new Error('The future-login launcher could not be verified. Retry before restarting this computer.');
|
|
109
|
+
}
|
|
110
|
+
export function ensureAutostart() { prepareAutostart({ defaultOn: true }); }
|
|
111
|
+
export function autostartOn() {
|
|
112
|
+
rmSync(offMarkerPath(), { force: true });
|
|
113
|
+
prepareAutostart({ defaultOn: true });
|
|
114
|
+
console.log('Auto-start enabled for the next login. Start now: cs start');
|
|
115
|
+
}
|
|
116
|
+
export function autostartOff() {
|
|
117
|
+
mkdirSync(configDir(), { recursive: true });
|
|
118
|
+
writeFileSync(offMarkerPath(), '', { mode: 0o600 });
|
|
119
|
+
if (['darwin', 'win32'].includes(process.platform))
|
|
120
|
+
rmSync(startupPath(), { force: true });
|
|
121
|
+
console.log('Auto-start disabled for future logins. Stop this service: cs stop');
|
|
141
122
|
}
|
package/dist/companion-ui.js
CHANGED
|
@@ -286,9 +286,27 @@ async function api(path, opts) {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
async function boot() {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
try {
|
|
290
|
+
const r = await api('/api/session');
|
|
291
|
+
if (!r.ok || r.data.signedIn === null) {
|
|
292
|
+
renderSessionUnavailable(r.data.sessionError || r.data.error);
|
|
293
|
+
} else if (r.data.signedIn) renderHome(r.data);
|
|
294
|
+
else renderSignIn(r.data || {});
|
|
295
|
+
} catch (_) { renderSessionUnavailable(); }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderSessionUnavailable(message) {
|
|
299
|
+
stopBadgePoll();
|
|
300
|
+
app.replaceChildren();
|
|
301
|
+
const wrap = el('div', 'auth');
|
|
302
|
+
const card = el('div', 'auth-card');
|
|
303
|
+
card.append(logo(), el('h1', 'auth-title', 'Connection unavailable'));
|
|
304
|
+
card.append(el('p', 'auth-sub', message || 'Sign-in could not be checked. Check your connection and try again.'));
|
|
305
|
+
const retry = el('button', 'btn btn-primary', 'Try again');
|
|
306
|
+
retry.addEventListener('click', boot);
|
|
307
|
+
card.append(retry);
|
|
308
|
+
wrap.append(card);
|
|
309
|
+
app.append(wrap);
|
|
292
310
|
}
|
|
293
311
|
|
|
294
312
|
/** The passive "Agent tools" badge (feature 05). Read-only: no button, no
|
|
@@ -308,6 +326,9 @@ function agentToolsBadge(state) {
|
|
|
308
326
|
if (reason === 'connected') {
|
|
309
327
|
title = 'Agent tools: connected \\u2713';
|
|
310
328
|
sub = (state.agent || 'Claude') + ' \\u00b7 ' + (state.server || 'ctrl-spc');
|
|
329
|
+
} else if (reason === 'unavailable') {
|
|
330
|
+
title = 'Connection unavailable';
|
|
331
|
+
sub = state.message || 'Sign-in could not be checked. Retrying.';
|
|
311
332
|
} else if (reason === 'connecting') {
|
|
312
333
|
title = 'Agent tools: connecting\\u2026';
|
|
313
334
|
sub = state.agent || '';
|
|
@@ -348,13 +369,20 @@ async function pollBadgeOnce() {
|
|
|
348
369
|
const r = await api('/api/session');
|
|
349
370
|
// Home may have been torn down while the fetch was in flight.
|
|
350
371
|
if (!badge.isConnected) return;
|
|
351
|
-
if (
|
|
352
|
-
const fresh = agentToolsBadge(r.data.
|
|
372
|
+
if (r.ok && r.data.signedIn === false) { renderSignIn(r.data); return; }
|
|
373
|
+
const fresh = agentToolsBadge(!r.ok || r.data.signedIn === null
|
|
374
|
+
? { reason: 'unavailable', message: r.data.sessionError }
|
|
375
|
+
: r.data.agentTools);
|
|
353
376
|
fresh.id = 'agent-tools';
|
|
354
377
|
fresh.style.marginBottom = 'var(--sp-5)';
|
|
355
378
|
badge.replaceWith(fresh);
|
|
356
379
|
} catch (e) {
|
|
357
|
-
|
|
380
|
+
if (badge.isConnected) {
|
|
381
|
+
const fresh = agentToolsBadge({ reason: 'unavailable' });
|
|
382
|
+
fresh.id = 'agent-tools';
|
|
383
|
+
fresh.style.marginBottom = 'var(--sp-5)';
|
|
384
|
+
badge.replaceWith(fresh);
|
|
385
|
+
}
|
|
358
386
|
} finally {
|
|
359
387
|
badgePolling = false;
|
|
360
388
|
}
|