ai-or-die 0.1.74 → 0.1.76

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/bin/ai-or-die.js CHANGED
@@ -43,7 +43,9 @@ program
43
43
  .option('--no-sticky-notes', 'disable per-tab AI session summaries + auto tab titles (on by default)')
44
44
  .option('--sticky-notes-model-dir <path>', 'custom directory for the sticky-note model file')
45
45
  .option('--sticky-notes-model <url>', 'override the sticky-note model GGUF download URL')
46
- .option('--sticky-notes-threads <number>', 'CPU threads for sticky-note inference (default: auto, max 4)');
46
+ .option('--sticky-notes-threads <number>', 'CPU threads for sticky-note inference (default: auto, max 4)')
47
+ .option('--no-keepalive', 'disable keeping the machine awake while the server runs (Windows only; on by default)')
48
+ .option('--keepalive-display', 'also keep the display on (default keeps the system awake but lets the monitor sleep)');
47
49
 
48
50
  // Auto-open is OFF by default and opt-in via --open. Legacy callers may still pass
49
51
  // --no-open (the old opt-out flag); filter it out so it parses harmlessly as a no-op.
@@ -123,6 +125,12 @@ async function main() {
123
125
  stickyNotesModelDir: options.stickyNotesModelDir || process.env.STICKY_NOTES_MODEL_DIR,
124
126
  stickyNotesModel: options.stickyNotesModel || process.env.STICKY_NOTES_MODEL,
125
127
  stickyNotesThreads: options.stickyNotesThreads || process.env.STICKY_NOTES_THREADS,
128
+ // Keep the host awake while the server runs (Windows only; on by default;
129
+ // --no-keepalive / AIORDIE_DISABLE_KEEPALIVE=1 disables). System-awake by
130
+ // default; --keepalive-display / AIORDIE_KEEPALIVE_DISPLAY=1 also holds
131
+ // the display on.
132
+ keepalive: options.keepalive !== false && process.env.AIORDIE_DISABLE_KEEPALIVE !== '1',
133
+ keepaliveDisplay: options.keepaliveDisplay === true || process.env.AIORDIE_KEEPALIVE_DISPLAY === '1',
126
134
  };
127
135
 
128
136
  console.log('Starting ai-or-die...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+
3
+ const childProcess = require('child_process');
4
+ const path = require('path');
5
+
6
+ // Keep the host machine awake while the ai-or-die server runs (Windows 11).
7
+ //
8
+ // Mechanism: spawn ONE long-lived Windows PowerShell (5.1, in-box) helper that
9
+ // P/Invokes SetThreadExecutionState from kernel32.dll to hold a power
10
+ // assertion, then blocks on stdin. When the parent closes stdin (graceful
11
+ // release) OR dies (the OS tears down the pipe -> ReadLine() returns null), the
12
+ // helper clears the assertion and exits. Windows also drops a thread's
13
+ // execution-state flags when the holding process dies, so even taskkill /F
14
+ // cannot leak the assertion past reboot. No native deps, no powercfg.
15
+ //
16
+ // Windows-only by design; an instant no-op on macOS/Linux. See
17
+ // docs/specs/keepalive.md and docs/adrs/0028-windows-keepalive.md.
18
+
19
+ // SetThreadExecutionState flags as DECIMAL uint32 literals. PowerShell 5.1
20
+ // parses 0x80000001 as a negative Int32 and the [uint32] cast then throws, so
21
+ // the decimal forms are load-bearing (verified on a real Windows host):
22
+ // ES_CONTINUOUS 0x80000000 = 2147483648 (clear value, continuous alone)
23
+ // ES_SYSTEM_REQUIRED 0x00000001 -> system = 2147483649
24
+ // ES_DISPLAY_REQUIRED 0x00000002 -> +display = 2147483651
25
+ const ES_CONTINUOUS = 2147483648;
26
+ const ES_SYSTEM = 2147483649;
27
+ const ES_SYSTEM_DISPLAY = 2147483651;
28
+
29
+ const READY_TIMEOUT_MS = 5000;
30
+
31
+ class KeepaliveManager {
32
+ constructor(options = {}) {
33
+ this._enabled = !!options.enabled;
34
+ this._keepDisplayOn = !!options.keepDisplayOn;
35
+ this._platform = options.platform || process.platform;
36
+ this._spawn = options.spawn || childProcess.spawn;
37
+ this._logger = options.logger || console;
38
+ this._readyTimeoutMs = options.readyTimeoutMs || READY_TIMEOUT_MS;
39
+
40
+ this._started = false;
41
+ this._child = null;
42
+ this._readyTimer = null;
43
+ // Stable reference so we can both add and remove the process 'exit' hook
44
+ // (a fresh closure each time would leak listeners across start/release).
45
+ this._exitHandler = null;
46
+ // Resolves true once the assertion is confirmed held, false on any failure
47
+ // (spawn error, early exit, or readiness timeout). Used only for a status
48
+ // log line; the lifecycle never awaits it.
49
+ this.ready = Promise.resolve(false);
50
+ }
51
+
52
+ // ---- pure builders (static so tests assert them without spawning) ----
53
+
54
+ static buildScript(displayRequired, ppid = process.pid) {
55
+ const assert = displayRequired ? ES_SYSTEM_DISPLAY : ES_SYSTEM;
56
+ return [
57
+ // 'Stop' is load-bearing: under Constrained Language Mode / WDAC /
58
+ // AppLocker / EDR, Add-Type (which writes a .cs to %TEMP% and shells to
59
+ // csc.exe) is blocked. With the default 'Continue', a blocked Add-Type
60
+ // would NOT exit -- PowerShell would fall into the infinite stdin loop
61
+ // and leak an immortal ~30-50MB process on every restart. 'Stop' makes
62
+ // the child die immediately so our handler fires and ready -> false.
63
+ `$ErrorActionPreference = 'Stop'`,
64
+ // Tag the command line with the parent PID (a trusted integer, never user
65
+ // input) so a stale/orphaned helper is identifiable for triage:
66
+ // Get-CimInstance Win32_Process -Filter "Name='powershell.exe'"
67
+ `# aod-keepalive ppid=${ppid}`,
68
+ `Add-Type -Name P -Namespace W -MemberDefinition '[System.Runtime.InteropServices.DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint e);'`,
69
+ // exit 1 when the assertion is refused so the helper never blocks on
70
+ // stdin WITHOUT actually holding the assertion (which would otherwise
71
+ // latch us "started" while the machine can still sleep).
72
+ `if ([W.P]::SetThreadExecutionState([uint32]${assert}) -ne 0) { [Console]::Out.WriteLine('OK'); [Console]::Out.Flush() } else { [Console]::Error.WriteLine('SetThreadExecutionState returned 0'); exit 1 }`,
73
+ `while ($null -ne [Console]::In.ReadLine()) {}`,
74
+ `[void][W.P]::SetThreadExecutionState([uint32]${ES_CONTINUOUS})`,
75
+ ].join('\n');
76
+ }
77
+
78
+ static buildArgs(displayRequired, ppid = process.pid) {
79
+ return [
80
+ '-NoProfile',
81
+ '-NonInteractive',
82
+ // GPO machine-wide execution policy can otherwise block inline code /
83
+ // the Add-Type compilation step.
84
+ '-ExecutionPolicy', 'Bypass',
85
+ '-Command', KeepaliveManager.buildScript(displayRequired, ppid),
86
+ ];
87
+ }
88
+
89
+ // Absolute path to in-box Windows PowerShell 5.1 (PATH-hijack hardening --
90
+ // never resolve a bare "powershell.exe" off PATH).
91
+ static powershellPath() {
92
+ const root = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
93
+ return path.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
94
+ }
95
+
96
+ // Acquire the wake assertion. No-op unless enabled on win32. Idempotent.
97
+ // Never throws -- keepalive must never break server startup.
98
+ start() {
99
+ if (this._started) return;
100
+ if (!this._enabled || this._platform !== 'win32') return;
101
+ this._started = true;
102
+
103
+ try {
104
+ const ps = KeepaliveManager.powershellPath();
105
+ const args = KeepaliveManager.buildArgs(this._keepDisplayOn);
106
+ const child = this._spawn(ps, args, {
107
+ stdio: ['pipe', 'pipe', 'pipe'],
108
+ windowsHide: true,
109
+ shell: false,
110
+ });
111
+ this._child = child;
112
+
113
+ // Never let the helper independently keep the parent's event loop alive
114
+ // (or revive the exit-134 native-teardown abort this repo has fought).
115
+ // unref() does NOT affect writability, so stdin stays usable by release()
116
+ // while unreferenced.
117
+ this._safe(() => child.unref());
118
+ this._safe(() => child.stdout && child.stdout.unref && child.stdout.unref());
119
+ this._safe(() => child.stderr && child.stderr.unref && child.stderr.unref());
120
+ this._safe(() => child.stdin && child.stdin.unref && child.stdin.unref());
121
+
122
+ // Capture the first stderr line so the failure warning can distinguish
123
+ // Constrained Language Mode (a PowerShell error) from an AV/EDR kill.
124
+ let firstErr = '';
125
+ if (child.stderr) {
126
+ this._safe(() => child.stderr.setEncoding && child.stderr.setEncoding('utf8'));
127
+ child.stderr.on('data', (d) => { if (!firstErr) firstErr = String(d).split('\n')[0].trim(); });
128
+ child.stderr.on('error', () => {});
129
+ }
130
+
131
+ // Per-start state, captured by the closures below so a stale child's
132
+ // late events can never mutate a newer run's state.
133
+ let settled = false;
134
+ let acquired = false;
135
+ let resolveReady;
136
+ this.ready = new Promise((res) => { resolveReady = res; });
137
+
138
+ // Declared with `let` so finishReady (defined next) can reference it and
139
+ // clear it; the timer is created after finishReady to keep ordering clear.
140
+ let timer = null;
141
+
142
+ const finishReady = (ok) => {
143
+ if (settled) return;
144
+ settled = true;
145
+ if (ok) acquired = true;
146
+ this._safe(() => clearTimeout(timer));
147
+ if (this._readyTimer === timer) this._readyTimer = null;
148
+ resolveReady(ok);
149
+ };
150
+
151
+ timer = setTimeout(() => finishReady(false), this._readyTimeoutMs);
152
+ if (timer.unref) timer.unref();
153
+ this._readyTimer = timer;
154
+
155
+ if (child.stdout) {
156
+ this._safe(() => child.stdout.setEncoding && child.stdout.setEncoding('utf8'));
157
+ let buf = '';
158
+ child.stdout.on('data', (d) => {
159
+ if (settled) return; // stop buffering once readiness is decided
160
+ buf += String(d);
161
+ if (/(^|\n)OK(\r?\n|$)/.test(buf)) finishReady(true);
162
+ });
163
+ child.stdout.on('error', () => {});
164
+ }
165
+
166
+ // Settle on 'close' (after all stdio has flushed) rather than 'exit' so
167
+ // firstErr is populated before the failure warning reads it. 'error'
168
+ // covers a spawn that never produced a process. The this._child === child
169
+ // guard makes a superseded/released child's late events a no-op.
170
+ const onGone = () => {
171
+ if (this._child !== child) return;
172
+ this._child = null;
173
+ this._started = false;
174
+ this._safe(() => clearTimeout(timer));
175
+ if (this._readyTimer === timer) this._readyTimer = null;
176
+ if (!settled) {
177
+ finishReady(false);
178
+ } else if (acquired) {
179
+ // Died AFTER holding the assertion (e.g. an AV/EDR kill hours in) and
180
+ // we did not initiate the release -> the machine can now sleep.
181
+ this._safe(() => this._logger.warn && this._logger.warn(
182
+ '⚠ keepalive: wake assertion lost — the helper exited; the machine may sleep. Restart ai-or-die or pass --no-keepalive.'));
183
+ }
184
+ };
185
+ child.on('error', onGone);
186
+ child.on('close', onGone);
187
+
188
+ // Guarantee release on EVERY exit path, including the ones that bypass
189
+ // close()/release(): uncaughtException and the bin outer-catch both call
190
+ // process.exit(1) without close(). A synchronous 'exit' hook, NOT a
191
+ // SIGINT/SIGTERM handler, so it cannot race the server's single
192
+ // handleShutdown owner -- it only closes a pipe. Re-registered idempotently
193
+ // (removeListener first) and removed in releaseSync to avoid leaking
194
+ // listeners across start/release cycles.
195
+ if (!this._exitHandler) {
196
+ this._exitHandler = () => { this._safe(() => this.releaseSync()); };
197
+ }
198
+ this._safe(() => process.removeListener('exit', this._exitHandler));
199
+ this._safe(() => process.once('exit', this._exitHandler));
200
+
201
+ this.ready.then((ok) => {
202
+ if (ok) {
203
+ this._safe(() => this._logger.log && this._logger.log(
204
+ 'keepalive: holding wake assertion (system sleep prevented)'));
205
+ } else {
206
+ const hint = firstErr || 'powershell.exe unavailable or blocked (Constrained Language Mode / WDAC / AV)';
207
+ this._safe(() => this._logger.warn && this._logger.warn(
208
+ `⚠ keepalive: could not hold the wake assertion; the machine may sleep (${hint}). Disable with --no-keepalive.`));
209
+ // Reap a helper that timed out without exiting (e.g. SetThreadExecutionState
210
+ // returned 0 and it is blocked on stdin) so it cannot leak.
211
+ if (this._child === child) this._safe(() => this.releaseSync());
212
+ }
213
+ }).catch(() => {});
214
+ } catch (err) {
215
+ this._started = false;
216
+ this._child = null;
217
+ this._safe(() => this._logger.debug && this._logger.debug(
218
+ 'keepalive: failed to start (continuing):', err && err.message));
219
+ }
220
+ }
221
+
222
+ // Async convenience wrapper; the real work is synchronous (just close a pipe).
223
+ release() {
224
+ this.releaseSync();
225
+ return Promise.resolve();
226
+ }
227
+
228
+ // Drop the assertion: closing stdin makes the helper hit EOF on ReadLine(),
229
+ // run its explicit clear, and exit; kill() is belt-and-suspenders. Idempotent
230
+ // and safe to call when never started or when the child is already dead (its
231
+ // body is fully guarded so it can never interrupt close()).
232
+ releaseSync() {
233
+ const child = this._child;
234
+ this._child = null;
235
+ this._started = false;
236
+ if (this._readyTimer) { this._safe(() => clearTimeout(this._readyTimer)); this._readyTimer = null; }
237
+ if (this._exitHandler) this._safe(() => process.removeListener('exit', this._exitHandler));
238
+ if (!child) return;
239
+ // end() sends a graceful EOF so the helper's ReadLine() returns null and the
240
+ // script runs its explicit ES_CONTINUOUS clear. Do NOT destroy() right after
241
+ // -- that would abort the pipe before the EOF flushes. kill() is the backstop.
242
+ this._safe(() => { if (child.stdin && !child.stdin.destroyed) child.stdin.end(); });
243
+ this._safe(() => child.kill());
244
+ }
245
+
246
+ _safe(fn) {
247
+ try { return fn(); } catch (_) { /* keepalive must never throw into the caller */ }
248
+ }
249
+ }
250
+
251
+ module.exports = KeepaliveManager;
Binary file
package/src/public/app.js CHANGED
@@ -191,6 +191,18 @@ class ClaudeCodeWebInterface {
191
191
  }
192
192
 
193
193
  await this.loadConfig();
194
+ // Apply per-machine identity (`[HOST] ai-or-die`) to the tab/window
195
+ // title, mobile menu header, aria-label, and PWA meta tags now that
196
+ // this.hostname is populated. Must run before the first notification
197
+ // flash, which saves/restores the then-current document.title.
198
+ if (window.AppIdentity) {
199
+ const identity = window.AppIdentity.formatAppIdentity({ hostname: this.hostname });
200
+ window.AppIdentity.applyAppIdentity(identity);
201
+ // Surface the machine identity on the start screen too.
202
+ const spIdText = document.getElementById('startPromptIdentityText');
203
+ const spId = document.getElementById('startPromptIdentity');
204
+ if (spIdText && spId) { spIdText.textContent = identity; spId.hidden = false; }
205
+ }
194
206
  this.setupTerminal();
195
207
  this._setupExtraKeys();
196
208
  this._setupOrientationHandler();
@@ -1929,19 +1941,34 @@ class ClaudeCodeWebInterface {
1929
1941
  });
1930
1942
  }
1931
1943
 
1932
- // Section collapse/expand (keyboard accessible)
1933
- modal.querySelectorAll('.setting-section-header').forEach((header) => {
1934
- const toggle = () => {
1935
- const section = header.parentElement;
1936
- const isCollapsed = section.classList.toggle('collapsed');
1937
- header.setAttribute('aria-expanded', String(!isCollapsed));
1938
- };
1939
- header.addEventListener('click', toggle);
1940
- header.addEventListener('keydown', (e) => {
1941
- if (e.key === 'Enter' || e.key === ' ') {
1942
- e.preventDefault();
1943
- toggle();
1944
- }
1944
+ // Two-pane settings nav (ARIA tablist): switch panes, roving tabindex,
1945
+ // arrow/Home/End keyboard support. Replaces the old collapsible sections.
1946
+ const settingsTabs = Array.from(modal.querySelectorAll('.settings-tab'));
1947
+ const selectSettingsTab = (tab, focus = true) => {
1948
+ settingsTabs.forEach((t) => {
1949
+ const selected = t === tab;
1950
+ t.setAttribute('aria-selected', String(selected));
1951
+ t.tabIndex = selected ? 0 : -1;
1952
+ const pane = document.getElementById(t.getAttribute('aria-controls'));
1953
+ if (pane) pane.hidden = !selected;
1954
+ });
1955
+ if (focus && tab) tab.focus();
1956
+ };
1957
+ settingsTabs.forEach((tab) => {
1958
+ tab.addEventListener('click', () => selectSettingsTab(tab, false));
1959
+ tab.addEventListener('keydown', (e) => {
1960
+ // Navigate among VISIBLE tabs only (the Install tab is hidden
1961
+ // when running as an installed PWA) so focus never lands on an
1962
+ // invisible element and escapes the modal focus trap.
1963
+ const visible = settingsTabs.filter((t) => t.style.display !== 'none' && !t.hidden);
1964
+ const idx = visible.indexOf(tab);
1965
+ if (idx === -1) return;
1966
+ let next = null;
1967
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = visible[(idx + 1) % visible.length];
1968
+ else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = visible[(idx - 1 + visible.length) % visible.length];
1969
+ else if (e.key === 'Home') next = visible[0];
1970
+ else if (e.key === 'End') next = visible[visible.length - 1];
1971
+ if (next) { e.preventDefault(); selectSettingsTab(next); }
1945
1972
  });
1946
1973
  });
1947
1974
 
@@ -3917,11 +3944,15 @@ class ClaudeCodeWebInterface {
3917
3944
  content.classList.remove('closing');
3918
3945
  overlay.classList.remove('closing');
3919
3946
  overlay.classList.remove('active');
3920
- overlay.style.display = 'none';
3947
+ // Clear (don't set) the inline display so the modal hides via its
3948
+ // base CSS rule (.<modal> { display: none }). Setting an inline
3949
+ // display:none would win over `.active { display: flex }` and
3950
+ // permanently block reopening the modal.
3951
+ overlay.style.removeProperty('display');
3921
3952
  }, 150);
3922
3953
  } else {
3923
3954
  overlay.classList.remove('active');
3924
- overlay.style.display = 'none';
3955
+ overlay.style.removeProperty('display');
3925
3956
  }
3926
3957
  }
3927
3958
 
@@ -4196,10 +4227,17 @@ class ClaudeCodeWebInterface {
4196
4227
  if (installBtn) installBtn.style.display = 'none';
4197
4228
  if (iosInstructions) iosInstructions.style.display = 'none';
4198
4229
 
4199
- // If running inside installed PWA, hide the entire section
4200
- const section = document.querySelector('[data-section="install"]');
4201
- if (section) {
4202
- section.style.display = this._isInstalled ? 'none' : '';
4230
+ // If running inside an installed PWA, hide the Install tab + pane.
4231
+ const installTab = document.getElementById('settingsTab-install');
4232
+ const installPane = document.getElementById('settingsPane-install');
4233
+ if (installTab) installTab.style.display = this._isInstalled ? 'none' : '';
4234
+ if (installPane && this._isInstalled) installPane.hidden = true;
4235
+ // If the Install tab was the active one and is now hidden, fall back to
4236
+ // the first visible tab so the pane area is never left blank/unreachable.
4237
+ if (this._isInstalled && installTab && installTab.getAttribute('aria-selected') === 'true') {
4238
+ const firstVisible = Array.from(document.querySelectorAll('.settings-tab'))
4239
+ .find((t) => t.style.display !== 'none');
4240
+ if (firstVisible) firstVisible.click();
4203
4241
  }
4204
4242
 
4205
4243
  switch (this._installState) {
@@ -133,7 +133,7 @@ class AuthManager {
133
133
  left: 0;
134
134
  right: 0;
135
135
  bottom: 0;
136
- background: rgba(0, 0, 0, 0.95);
136
+ background: var(--overlay-backdrop-strong, rgba(0, 0, 0, 0.95));
137
137
  display: flex;
138
138
  align-items: center;
139
139
  justify-content: center;
@@ -143,12 +143,12 @@ class AuthManager {
143
143
  const loginForm = document.createElement('div');
144
144
  loginForm.style.cssText = `
145
145
  background: var(--bg-secondary, #1c2128);
146
- border: 1px solid var(--border-color, #30363d);
146
+ border: 1px solid var(--border-default, #30363d);
147
147
  border-radius: 12px;
148
148
  padding: 32px;
149
149
  max-width: 400px;
150
150
  width: 90%;
151
- box-shadow: 0 10px 50px rgba(0, 0, 0, 0.5);
151
+ box-shadow: var(--shadow-xl);
152
152
  `;
153
153
 
154
154
  loginForm.innerHTML = `
@@ -157,7 +157,7 @@ class AuthManager {
157
157
  Authentication Required
158
158
  </h2>
159
159
  <p style="color: var(--text-secondary, #8b949e); margin: 0 0 24px 0; font-size: 14px;">
160
- This ai-or-die instance requires authentication.
160
+ This instance requires authentication.
161
161
  </p>
162
162
  <form id="auth-form">
163
163
  <div style="margin-bottom: 16px;">
@@ -172,7 +172,7 @@ class AuthManager {
172
172
  width: 100%;
173
173
  padding: 10px 12px;
174
174
  background: var(--bg-primary, #0d1117);
175
- border: 1px solid var(--border-color, #30363d);
175
+ border: 1px solid var(--border-default, #30363d);
176
176
  border-radius: 6px;
177
177
  color: var(--text-primary, #f0f6fc);
178
178
  font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, 'JetBrains Mono', monospace;
@@ -183,7 +183,7 @@ class AuthManager {
183
183
  required
184
184
  />
185
185
  </div>
186
- <div id="auth-error" style="color: #f85149; margin-bottom: 16px; font-size: 14px; display: none;"></div>
186
+ <div id="auth-error" style="color: var(--status-error, #f85149); margin-bottom: 16px; font-size: 14px; display: none;"></div>
187
187
  <button
188
188
  type="submit"
189
189
  style="
@@ -163,7 +163,7 @@
163
163
  border: 2px solid var(--border);
164
164
  border-radius: 50%;
165
165
  cursor: pointer;
166
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
166
+ box-shadow: var(--shadow-md);
167
167
  transition: all var(--duration-slow, 300ms) var(--ease-default, cubic-bezier(0.4, 0, 0.2, 1));
168
168
  width: 56px;
169
169
  height: 56px;
@@ -173,7 +173,7 @@
173
173
  background-color: var(--bg-tertiary);
174
174
  border-color: var(--accent);
175
175
  transform: translateY(-2px);
176
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
176
+ box-shadow: var(--shadow-lg);
177
177
  }
178
178
 
179
179
  .mode-switcher-btn:active {
@@ -212,7 +212,7 @@
212
212
  border: 2px solid var(--error);
213
213
  border-radius: 50%;
214
214
  cursor: pointer;
215
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
215
+ box-shadow: var(--shadow-md);
216
216
  transition: all var(--duration-slow, 300ms) var(--ease-default, cubic-bezier(0.4, 0, 0.2, 1));
217
217
  width: 56px;
218
218
  height: 56px;
@@ -222,6 +222,7 @@
222
222
  background-color: var(--bg-tertiary);
223
223
  border-color: var(--color-red-400);
224
224
  transform: translateY(-2px);
225
+ /* Intentional red destructive-escape glow (not a neutral elevation shadow). */
225
226
  box-shadow: 0 6px 16px rgba(248, 113, 113, 0.2);
226
227
  }
227
228
 
@@ -58,26 +58,26 @@
58
58
  transform: translateY(-1px);
59
59
  }
60
60
 
61
- /* Per-tool brand color hover tints */
61
+ /* Per-tool brand color hover tints (tokens shared with the tab badges) */
62
62
  .tool-card[data-tool="terminal"]:hover:not(.disabled) {
63
- border-color: rgba(113, 113, 122, 0.5);
64
- background: rgba(113, 113, 122, 0.08);
63
+ border-color: rgba(var(--tool-terminal-rgb), 0.5);
64
+ background: rgba(var(--tool-terminal-rgb), 0.08);
65
65
  }
66
66
  .tool-card[data-tool="claude"]:hover:not(.disabled) {
67
- border-color: rgba(217, 119, 6, 0.5);
68
- background: rgba(217, 119, 6, 0.08);
67
+ border-color: rgba(var(--tool-claude-rgb), 0.5);
68
+ background: rgba(var(--tool-claude-rgb), 0.08);
69
69
  }
70
70
  .tool-card[data-tool="codex"]:hover:not(.disabled) {
71
- border-color: rgba(5, 150, 105, 0.5);
72
- background: rgba(5, 150, 105, 0.08);
71
+ border-color: rgba(var(--tool-codex-rgb), 0.5);
72
+ background: rgba(var(--tool-codex-rgb), 0.08);
73
73
  }
74
74
  .tool-card[data-tool="copilot"]:hover:not(.disabled) {
75
- border-color: rgba(99, 102, 241, 0.5);
76
- background: rgba(99, 102, 241, 0.08);
75
+ border-color: rgba(var(--tool-copilot-rgb), 0.5);
76
+ background: rgba(var(--tool-copilot-rgb), 0.08);
77
77
  }
78
78
  .tool-card[data-tool="gemini"]:hover:not(.disabled) {
79
- border-color: rgba(37, 99, 235, 0.5);
80
- background: rgba(37, 99, 235, 0.08);
79
+ border-color: rgba(var(--tool-gemini-rgb), 0.5);
80
+ background: rgba(var(--tool-gemini-rgb), 0.08);
81
81
  }
82
82
 
83
83
  /* Focus visible for keyboard navigation */
@@ -112,20 +112,20 @@
112
112
 
113
113
  /* Per-tool brand color hover for installable cards (softer than available) */
114
114
  .tool-card.installable[data-tool="claude"]:hover {
115
- border-color: rgba(217, 119, 6, 0.3);
116
- background: rgba(217, 119, 6, 0.04);
115
+ border-color: rgba(var(--tool-claude-rgb), 0.3);
116
+ background: rgba(var(--tool-claude-rgb), 0.04);
117
117
  }
118
118
  .tool-card.installable[data-tool="codex"]:hover {
119
- border-color: rgba(5, 150, 105, 0.3);
120
- background: rgba(5, 150, 105, 0.04);
119
+ border-color: rgba(var(--tool-codex-rgb), 0.3);
120
+ background: rgba(var(--tool-codex-rgb), 0.04);
121
121
  }
122
122
  .tool-card.installable[data-tool="copilot"]:hover {
123
- border-color: rgba(99, 102, 241, 0.3);
124
- background: rgba(99, 102, 241, 0.04);
123
+ border-color: rgba(var(--tool-copilot-rgb), 0.3);
124
+ background: rgba(var(--tool-copilot-rgb), 0.04);
125
125
  }
126
126
  .tool-card.installable[data-tool="gemini"]:hover {
127
- border-color: rgba(37, 99, 235, 0.3);
128
- background: rgba(37, 99, 235, 0.04);
127
+ border-color: rgba(var(--tool-gemini-rgb), 0.3);
128
+ background: rgba(var(--tool-gemini-rgb), 0.04);
129
129
  }
130
130
 
131
131
  /* Expanded state */
@@ -474,6 +474,30 @@
474
474
  margin-bottom: var(--space-2);
475
475
  }
476
476
 
477
+ /* Machine-identity chip — reads like a live terminal status badge */
478
+ .start-prompt-identity {
479
+ display: inline-flex;
480
+ align-items: center;
481
+ gap: var(--space-2);
482
+ margin-bottom: var(--space-3);
483
+ padding: var(--space-1) var(--space-3);
484
+ border: 1px solid var(--border-subtle);
485
+ border-radius: var(--radius-full);
486
+ background: var(--surface-secondary);
487
+ color: var(--text-secondary);
488
+ font-family: var(--font-mono);
489
+ font-size: var(--text-sm);
490
+ font-weight: var(--weight-medium);
491
+ }
492
+ .start-prompt-identity[hidden] { display: none; }
493
+ .start-prompt-identity-dot {
494
+ width: 7px;
495
+ height: 7px;
496
+ border-radius: 50%;
497
+ background: var(--status-success);
498
+ flex: 0 0 auto;
499
+ }
500
+
477
501
  /* Start prompt subtitle */
478
502
  .start-prompt-subtitle {
479
503
  color: var(--text-muted);