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 +9 -1
- package/package.json +1 -1
- package/src/keepalive-manager.js +251 -0
- package/src/public/app-identity.js +0 -0
- package/src/public/app.js +57 -19
- package/src/public/auth.js +6 -6
- package/src/public/components/buttons.css +4 -3
- package/src/public/components/cards.css +43 -19
- package/src/public/components/controls.css +422 -0
- package/src/public/components/file-browser.css +4 -4
- package/src/public/components/input-overlay.css +1 -1
- package/src/public/components/menus.css +4 -4
- package/src/public/components/modals.css +9 -9
- package/src/public/components/sticky-note.css +9 -10
- package/src/public/components/tabs.css +8 -1
- package/src/public/components/terminal.css +3 -3
- package/src/public/index.html +177 -140
- package/src/public/service-worker.js +8 -3
- package/src/public/session-manager.js +20 -15
- package/src/public/tokens.css +23 -0
- package/src/server.js +70 -7
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
|
@@ -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
|
-
//
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
if (
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
4200
|
-
const
|
|
4201
|
-
|
|
4202
|
-
|
|
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) {
|
package/src/public/auth.js
CHANGED
|
@@ -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-
|
|
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:
|
|
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
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
64
|
-
background: rgba(
|
|
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(
|
|
68
|
-
background: rgba(
|
|
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(
|
|
72
|
-
background: rgba(
|
|
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(
|
|
76
|
-
background: rgba(
|
|
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(
|
|
80
|
-
background: rgba(
|
|
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(
|
|
116
|
-
background: rgba(
|
|
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(
|
|
120
|
-
background: rgba(
|
|
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(
|
|
124
|
-
background: rgba(
|
|
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(
|
|
128
|
-
background: rgba(
|
|
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);
|