ai-or-die 0.1.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.
Files changed (78) hide show
  1. package/.cursor/commands/commit-push.md +18 -0
  2. package/.github/agents/architect.md +26 -0
  3. package/.github/agents/engineer.md +29 -0
  4. package/.github/agents/qa-reviewer.md +31 -0
  5. package/.github/agents/researcher.md +30 -0
  6. package/.github/agents/troubleshooter.md +33 -0
  7. package/.github/copilot-instructions.md +55 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/build-binaries.yml +76 -0
  10. package/.github/workflows/ci.yml +70 -0
  11. package/.github/workflows/release-on-main.yml +73 -0
  12. package/.prompts/log.md +9 -0
  13. package/AGENTS.md +84 -0
  14. package/CHANGELOG.md +25 -0
  15. package/CLAUDE.md +130 -0
  16. package/CONTRIBUTING.md +76 -0
  17. package/LICENSE +22 -0
  18. package/README.md +165 -0
  19. package/bin/ai-or-die.js +203 -0
  20. package/docs/.nojekyll +1 -0
  21. package/docs/README.md +37 -0
  22. package/docs/adrs/0000-template.md +35 -0
  23. package/docs/adrs/0001-bridge-base-class.md +53 -0
  24. package/docs/adrs/0002-devtunnels-over-ngrok.md +56 -0
  25. package/docs/adrs/0003-multi-tool-architecture.md +71 -0
  26. package/docs/adrs/0004-cross-platform-support.md +101 -0
  27. package/docs/adrs/0005-single-binary-distribution.md +58 -0
  28. package/docs/agent-instructions/00-philosophy.md +55 -0
  29. package/docs/agent-instructions/01-research-and-web.md +49 -0
  30. package/docs/agent-instructions/02-testing-and-validation.md +63 -0
  31. package/docs/agent-instructions/03-tooling-and-pipelines.md +59 -0
  32. package/docs/architecture/bridge-pattern.md +510 -0
  33. package/docs/architecture/overview.md +216 -0
  34. package/docs/architecture/websocket-protocol.md +609 -0
  35. package/docs/history/README.md +26 -0
  36. package/docs/specs/authentication.md +167 -0
  37. package/docs/specs/bridges.md +210 -0
  38. package/docs/specs/client-app.md +308 -0
  39. package/docs/specs/e2e-testing.md +311 -0
  40. package/docs/specs/server.md +334 -0
  41. package/docs/specs/session-store.md +170 -0
  42. package/docs/specs/usage-analytics.md +342 -0
  43. package/nul +0 -0
  44. package/package.json +54 -0
  45. package/scripts/build-sea.js +187 -0
  46. package/scripts/pty-sea-shim.js +21 -0
  47. package/scripts/publish-both.sh +21 -0
  48. package/scripts/release-pr.sh +73 -0
  49. package/scripts/smoke-test-binary.js +190 -0
  50. package/scripts/validate.ps1 +25 -0
  51. package/scripts/validate.sh +16 -0
  52. package/sea-bootstrap.js +54 -0
  53. package/site/ADVANCED_ANALYTICS.md +174 -0
  54. package/site/index.html +151 -0
  55. package/site/script.js +17 -0
  56. package/site/style.css +60 -0
  57. package/src/base-bridge.js +340 -0
  58. package/src/claude-bridge.js +48 -0
  59. package/src/codex-bridge.js +27 -0
  60. package/src/copilot-bridge.js +29 -0
  61. package/src/gemini-bridge.js +26 -0
  62. package/src/public/app.js +2123 -0
  63. package/src/public/auth.js +244 -0
  64. package/src/public/icon-generator.js +26 -0
  65. package/src/public/icons.js +36 -0
  66. package/src/public/index.html +397 -0
  67. package/src/public/manifest.json +45 -0
  68. package/src/public/plan-detector.js +186 -0
  69. package/src/public/service-worker.js +108 -0
  70. package/src/public/session-manager.js +1124 -0
  71. package/src/public/splits.js +574 -0
  72. package/src/public/style.css +2090 -0
  73. package/src/server.js +1269 -0
  74. package/src/terminal-bridge.js +49 -0
  75. package/src/usage-analytics.js +494 -0
  76. package/src/usage-reader.js +895 -0
  77. package/src/utils/auth.js +123 -0
  78. package/src/utils/session-store.js +181 -0
@@ -0,0 +1,167 @@
1
+ # Authentication Specification
2
+
3
+ ## Overview
4
+
5
+ ai-or-die provides optional token-based authentication for both HTTP and WebSocket connections. Authentication is managed by two independent classes: a server-side `AuthManager` utility and a client-side `AuthManager` in the browser.
6
+
7
+ ---
8
+
9
+ ## Server-Side Auth
10
+
11
+ ### AuthManager Class
12
+
13
+ Source: `src/utils/auth.js`
14
+
15
+ A utility class for token management, rate limiting, and Express middleware generation. While the class is available, the server (`src/server.js`) currently implements auth inline rather than delegating to this class.
16
+
17
+ #### Token Management
18
+
19
+ | Method | Description |
20
+ |--------|-------------|
21
+ | `generateToken()` | Generates a 32-byte random hex string via `crypto.randomBytes(32).toString('hex')` |
22
+ | `validateToken(token)` | Checks if `token` is in the internal `Set` |
23
+ | `addToken(token)` | Adds a token to the `Set` |
24
+ | `removeToken(token)` | Removes a token from the `Set` |
25
+ | `clearTokens()` | Clears all stored tokens |
26
+
27
+ #### Middleware Factory
28
+
29
+ `createMiddleware(requiredToken)` returns an Express middleware that:
30
+
31
+ 1. If `requiredToken` is falsy, calls `next()` (no auth required).
32
+ 2. Extracts the token from:
33
+ - `Authorization: Bearer <token>` header, or
34
+ - `?token=<token>` query parameter.
35
+ 3. Compares against `requiredToken`.
36
+ 4. Returns `401 Unauthorized` with `{ error: "Unauthorized", message: "Valid authentication token required" }` on mismatch.
37
+
38
+ #### WebSocket Validator
39
+
40
+ `createWebSocketValidator(requiredToken)` returns a function compatible with the `ws` library's `verifyClient` callback:
41
+
42
+ 1. If `requiredToken` is falsy, returns `true` (no auth required).
43
+ 2. Parses the URL from `info.req.url`.
44
+ 3. Extracts `?token=` query parameter.
45
+ 4. Returns `token === requiredToken`.
46
+
47
+ #### Rate Limiting
48
+
49
+ `rateLimit(identifier, maxRequests = 100, windowMs = 60000)`:
50
+
51
+ 1. Tracks request timestamps per `identifier` (typically IP address).
52
+ 2. Filters to requests within the current window.
53
+ 3. Returns `false` if the limit is exceeded, `true` otherwise.
54
+ 4. Pushes the current timestamp on success.
55
+
56
+ `createRateLimitMiddleware(maxRequests = 100, windowMs = 60000)` returns Express middleware that:
57
+ - Identifies the client by `req.ip || req.connection.remoteAddress`.
58
+ - Returns `429 Too Many Requests` with `retryAfter` header when the limit is hit.
59
+
60
+ `cleanupRateLimit()`:
61
+ - Removes entries older than 1 hour from the rate limiter `Map`.
62
+ - Should be called periodically to prevent memory growth.
63
+
64
+ ---
65
+
66
+ ### Server Auth Implementation
67
+
68
+ Source: `src/server.js` (inline in `setupExpress()`)
69
+
70
+ The server implements authentication directly rather than using the `AuthManager` class:
71
+
72
+ 1. **Pre-auth endpoints** -- `/auth-status` and `/auth-verify` are registered before the auth middleware, making them accessible without a token.
73
+
74
+ 2. **Auth middleware** -- Registered conditionally when `!this.noAuth && this.auth`:
75
+ ```js
76
+ const token = req.headers.authorization || req.query.token;
77
+ if (token !== `Bearer ${this.auth}` && token !== this.auth) {
78
+ return res.status(401).json({ error: 'Unauthorized' });
79
+ }
80
+ ```
81
+ This accepts either:
82
+ - `Authorization: Bearer <token>` header
83
+ - `?token=<token>` query parameter (raw token, no Bearer prefix)
84
+ - `Authorization: <token>` header (raw token, matched directly)
85
+
86
+ 3. **WebSocket auth** -- The `verifyClient` callback on the `ws.Server`:
87
+ ```js
88
+ verifyClient: (info) => {
89
+ if (!this.noAuth && this.auth) {
90
+ const url = new URL(info.req.url, 'ws://localhost');
91
+ const token = url.searchParams.get('token');
92
+ return token === this.auth;
93
+ }
94
+ return true;
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ### CLI Auth Flow
101
+
102
+ Source: `bin/cc-web.js`
103
+
104
+ | CLI Flag | Behavior |
105
+ |----------|----------|
106
+ | `--auth <token>` | Use the provided string as the auth token |
107
+ | `--disable-auth` | Set `noAuth = true`; no token is required |
108
+ | _(neither flag)_ | Auto-generate a random 10-character token using a charset that excludes ambiguous characters (`0`, `O`, `1`, `l`, `I`) |
109
+
110
+ The random token generator:
111
+ ```js
112
+ const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
113
+ ```
114
+ This produces tokens that are easy to read and transcribe (e.g., `kP7nVm3Qxt`).
115
+
116
+ The generated token is printed to the terminal with bold yellow ANSI formatting for visibility.
117
+
118
+ ---
119
+
120
+ ## Client-Side Auth
121
+
122
+ ### AuthManager (Browser)
123
+
124
+ Source: `src/public/auth.js`
125
+
126
+ A global singleton instantiated as `window.authManager`.
127
+
128
+ #### Token Storage
129
+
130
+ Tokens are stored in `sessionStorage` under the key `cc-web-token`. This means:
131
+ - The token persists across page reloads within the same tab.
132
+ - The token is cleared when the tab/browser is closed.
133
+ - Each browser tab can have its own independent auth state.
134
+
135
+ #### Initialization Flow
136
+
137
+ `initialize()` is called on page load:
138
+
139
+ 1. Calls `GET /auth-status` to check if auth is required.
140
+ 2. If auth is required and no stored token exists, calls `showLoginPrompt()`.
141
+ 3. If auth is required and a token exists, calls `POST /auth-verify` to validate it.
142
+ 4. If validation fails, clears the stored token and shows the login prompt.
143
+ 5. Returns `true` if authenticated (or auth not required), `false` if the login prompt was shown.
144
+
145
+ #### Login Prompt
146
+
147
+ `showLoginPrompt()` creates a full-screen overlay (`z-index: 10000`) with:
148
+ - A password input field for the access token.
149
+ - A submit button that calls `verifyToken()`.
150
+ - Error display for invalid tokens.
151
+ - Visual feedback during verification (input disabled, button text changes to "Authenticating...").
152
+ - On success: overlay is removed and `window.location.reload()` triggers full re-initialization.
153
+ - On failure: error message is shown, form is re-enabled.
154
+
155
+ #### Helper Methods
156
+
157
+ | Method | Description |
158
+ |--------|-------------|
159
+ | `getAuthHeaders()` | Returns `{ Authorization: "Bearer <token>" }` or empty object |
160
+ | `getWebSocketUrl(baseUrl)` | Appends `?token=<token>` (or `&token=<token>`) to the WebSocket URL |
161
+ | `logout()` | Clears `sessionStorage`, reloads the page |
162
+
163
+ #### Integration Points
164
+
165
+ - `ClaudeCodeWebInterface.authFetch(url, options)` merges `authManager.getAuthHeaders()` into every fetch request.
166
+ - WebSocket connections use `authManager.getWebSocketUrl()` to append the token to the connection URL.
167
+ - `SessionTabManager.loadSessions()` and `closeSession()` include auth headers via `window.authManager.getAuthHeaders()`.
@@ -0,0 +1,210 @@
1
+ # Bridge Specification
2
+
3
+ Bridges manage the spawning, I/O, and lifecycle of CLI agent processes via `node-pty`. Each bridge class owns a `Map<sessionId, BridgeSession>` of active pty sessions.
4
+
5
+ ---
6
+
7
+ ## Common Interface
8
+
9
+ Every bridge implements the same public API:
10
+
11
+ | Method | Signature | Description |
12
+ |--------|-----------|-------------|
13
+ | `startSession` | `(sessionId, options) => Promise<BridgeSession>` | Spawn the CLI process in a pty and wire up output/exit/error callbacks. Throws if `sessionId` already exists. |
14
+ | `sendInput` | `(sessionId, data) => Promise<void>` | Write raw data to the pty stdin. Throws if session is missing or inactive. |
15
+ | `resize` | `(sessionId, cols, rows) => Promise<void>` | Resize the pty dimensions. Logs a warning on failure instead of throwing. |
16
+ | `stopSession` | `(sessionId) => Promise<void>` | Send `SIGTERM`; after a 5-second grace period, send `SIGKILL`. Removes the session from the internal Map. |
17
+ | `getSession` | `(sessionId) => BridgeSession \| undefined` | Return the raw session object. |
18
+ | `getAllSessions` | `() => Array<{ id, workingDir, created, active }>` | List all sessions with metadata. |
19
+ | `cleanup` | `() => Promise<void>` | Stop all active sessions. |
20
+
21
+ ### BridgeSession Object
22
+
23
+ ```js
24
+ {
25
+ process: IPty, // node-pty process handle
26
+ workingDir: string, // cwd the process was started in
27
+ created: Date,
28
+ active: boolean,
29
+ killTimeout: Timeout|null // handle for the SIGKILL escalation timer
30
+ }
31
+ ```
32
+
33
+ ### startSession Options
34
+
35
+ ```js
36
+ {
37
+ workingDir: string, // defaults to process.cwd()
38
+ dangerouslySkipPermissions: boolean, // defaults to false (Claude/Codex only)
39
+ onOutput: (data: string) => void,
40
+ onExit: (code: number, signal: number) => void,
41
+ onError: (error: Error) => void,
42
+ cols: number, // defaults to 80
43
+ rows: number // defaults to 24
44
+ }
45
+ ```
46
+
47
+ ### PTY Environment
48
+
49
+ All bridges spawn with these environment variables:
50
+
51
+ ```
52
+ TERM=xterm-256color
53
+ FORCE_COLOR=1
54
+ COLORTERM=truecolor
55
+ ```
56
+
57
+ The pty name is set to `xterm-color`.
58
+
59
+ ### Command Discovery
60
+
61
+ Each bridge's constructor calls `findCommand()` which iterates through platform-specific candidate paths. Discovery works differently depending on whether the candidate is an absolute path or a bare command name:
62
+
63
+ - **Absolute paths:** Checked via `fs.existsSync()` only. On Windows, candidates are expanded with `.exe` and `.cmd` suffixes (e.g., `C:\Users\foo\.claude\local\claude` also checks `claude.exe` and `claude.cmd`).
64
+ - **Bare command names:** Checked via `commandExists()`, which runs `which` (Linux/macOS) or `where` (Windows) with a 5-second timeout to prevent hangs on systems with large PATH or network-mapped drives.
65
+
66
+ The first match wins. If none are found, a fallback default command name is used (e.g., `'claude'`).
67
+
68
+ ### Spawn Watchdog
69
+
70
+ After a PTY process is spawned, a 30-second watchdog timer starts. If no data, exit, or error event fires within that period, the process is killed and an error is reported to the caller. This prevents zombie processes on Windows where ConPTY initialization can hang silently. The watchdog is cleared on the first `onData`, `onExit`, or `on('error')` event.
71
+
72
+ ### Availability Check
73
+
74
+ `isAvailable()` returns `true` if the resolved command differs from the fallback default (meaning a specific path was found), or if the fallback command exists on PATH. The server checks `isAvailable()` before attempting to spawn a tool session, returning an immediate error to the client if the CLI is not installed.
75
+
76
+ The result is cached for 60 seconds (`_availableCache` / `_availableCacheTime`) to avoid repeated synchronous `where`/`which` calls that block the Node.js event loop. On Windows, `where.exe` can take several seconds when scanning large PATH variables or network-mapped drives, which would stall all WebSocket message processing during concurrent session starts.
77
+
78
+ ---
79
+
80
+ ## ClaudeBridge
81
+
82
+ Source: `src/claude-bridge.js`
83
+
84
+ ### Command Search Paths
85
+
86
+ ```
87
+ /home/ec2-user/.claude/local/claude
88
+ claude (PATH lookup)
89
+ claude-code (PATH lookup)
90
+ ~/.claude/local/claude
91
+ ~/.local/bin/claude
92
+ /usr/local/bin/claude
93
+ /usr/bin/claude
94
+ ```
95
+
96
+ Fallback: `'claude'`
97
+
98
+ ### CLI Arguments
99
+
100
+ | Flag | Condition |
101
+ |------|-----------|
102
+ | `--dangerously-skip-permissions` | When `options.dangerouslySkipPermissions` is `true` |
103
+
104
+ ### Trust Prompt Auto-Accept
105
+
106
+ The bridge monitors output for the string `"Do you trust the files in this folder?"`. On first detection, it sends `\r` (Enter) after a 500ms delay to auto-confirm the default trust option. This is tracked via a `trustPromptHandled` boolean to prevent duplicate submissions.
107
+
108
+ ### Output Buffer
109
+
110
+ A rolling `dataBuffer` (max 10,000 chars, trimmed to last 5,000) is maintained for trust prompt detection. This buffer is internal to the bridge and separate from the server-level `outputBuffer`.
111
+
112
+ ---
113
+
114
+ ## CodexBridge
115
+
116
+ Source: `src/codex-bridge.js`
117
+
118
+ ### Command Search Paths
119
+
120
+ ```
121
+ ~/.codex/local/codex
122
+ codex (PATH lookup)
123
+ codex-code (PATH lookup)
124
+ ~/.local/bin/codex
125
+ /usr/local/bin/codex
126
+ /usr/bin/codex
127
+ ```
128
+
129
+ Fallback: `'codex'`
130
+
131
+ ### CLI Arguments
132
+
133
+ | Flag | Condition |
134
+ |------|-----------|
135
+ | `--dangerously-bypass-approvals-and-sandbox` | When `options.dangerouslySkipPermissions` is `true` |
136
+
137
+ ### Notes
138
+
139
+ - No trust prompt handling (Codex does not prompt for folder trust).
140
+ - Maintains an internal `dataBuffer` (same 10,000/5,000 limits) for future prompt detection.
141
+
142
+ ---
143
+
144
+ ## AgentBridge
145
+
146
+ Source: `src/agent-bridge.js`
147
+
148
+ ### Command Search Paths
149
+
150
+ ```
151
+ ~/.cursor/local/cursor-agent
152
+ cursor-agent (PATH lookup)
153
+ ~/.local/bin/cursor-agent
154
+ /usr/local/bin/cursor-agent
155
+ /usr/bin/cursor-agent
156
+ ```
157
+
158
+ Fallback: `'cursor-agent'`
159
+
160
+ ### CLI Arguments
161
+
162
+ None. The agent is spawned with an empty args array.
163
+
164
+ ### Notes
165
+
166
+ - No `dangerouslySkipPermissions` option. The AgentBridge `startSession` does not destructure or use that option.
167
+ - Maintains an internal `dataBuffer` (same 10,000/5,000 limits) for future prompt detection.
168
+
169
+ ---
170
+
171
+ ## Planned Bridges
172
+
173
+ The following bridges are planned but not yet implemented. They should follow the same common interface described above.
174
+
175
+ ### BaseBridge (Planned)
176
+
177
+ A shared base class that extracts the duplicated logic from ClaudeBridge, CodexBridge, and AgentBridge:
178
+
179
+ - Cross-platform command discovery (`find*Command`, `commandExists`)
180
+ - Session lifecycle management (`startSession`, `stopSession`, `sendInput`, `resize`)
181
+ - Output buffering with configurable limits
182
+ - Kill timeout escalation (SIGTERM then SIGKILL after 5s)
183
+ - PTY environment setup
184
+
185
+ Each concrete bridge would extend `BaseBridge` and provide:
186
+ - `commandSearchPaths` -- ordered array of paths to search
187
+ - `fallbackCommand` -- default command name
188
+ - `buildArgs(options)` -- returns the CLI arguments array
189
+ - `onDataHook(data, buffer)` -- optional per-bridge output processing (e.g., trust prompt handling)
190
+
191
+ ### CopilotBridge (Planned)
192
+
193
+ - Command: `copilot`
194
+ - Installation: `npm install -g @github/copilot`
195
+ - Search paths: standard locations following the same pattern as existing bridges
196
+
197
+ ### GeminiBridge (Planned)
198
+
199
+ - Command: `gemini`
200
+ - Installation: `npm install -g @google/gemini-cli`
201
+ - Search paths: standard locations following the same pattern as existing bridges
202
+
203
+ ### TerminalBridge (Planned)
204
+
205
+ Opens a raw shell session rather than an AI agent.
206
+
207
+ - Linux/macOS: spawns `$SHELL` (defaults to `/bin/bash`)
208
+ - Windows: spawns `powershell.exe` or `cmd.exe`
209
+ - No `dangerouslySkipPermissions` or AI-specific flags
210
+ - Useful for running manual commands alongside agent sessions
@@ -0,0 +1,308 @@
1
+ # Client Application Specification
2
+
3
+ The frontend is a single-page application served from `src/public/`. It runs entirely in the browser with no build step -- all JavaScript is loaded directly via `<script>` tags.
4
+
5
+ ---
6
+
7
+ ## Dependencies
8
+
9
+ | Library | Version | Source | Purpose |
10
+ |---------|---------|--------|---------|
11
+ | xterm.js | 5.3.0 | unpkg CDN | Terminal emulator component |
12
+ | xterm-addon-fit | 0.8.0 | unpkg CDN | Auto-fit terminal to container |
13
+ | xterm-addon-web-links | 0.9.0 | unpkg CDN | Clickable URLs in terminal output |
14
+ | JetBrains Mono | -- | Google Fonts | Monospace font for terminal |
15
+ | Inter | -- | Google Fonts | UI font for headers, tabs, controls |
16
+
17
+ ---
18
+
19
+ ## ClaudeCodeWebInterface
20
+
21
+ Source: `src/public/app.js` (~2100 lines)
22
+
23
+ The main application controller. Instantiated once on page load.
24
+
25
+ ### Constructor Properties
26
+
27
+ | Property | Type | Description |
28
+ |----------|------|-------------|
29
+ | `terminal` | Terminal | xterm.js instance |
30
+ | `fitAddon` | FitAddon | Auto-resize addon |
31
+ | `webLinksAddon` | WebLinksAddon | Clickable URL addon |
32
+ | `socket` | WebSocket | Active WebSocket connection |
33
+ | `connectionId` | string | Server-assigned connection UUID |
34
+ | `currentClaudeSessionId` | string | Currently joined session ID |
35
+ | `currentClaudeSessionName` | string | Currently joined session name |
36
+ | `reconnectAttempts` | number | Counter for exponential backoff |
37
+ | `maxReconnectAttempts` | number | 5 |
38
+ | `reconnectDelay` | number | 1000ms base delay |
39
+ | `folderMode` | boolean | Always `true` |
40
+ | `currentFolderPath` | string | Current path in folder browser |
41
+ | `claudeSessions` | Array | Cached session list from server |
42
+ | `isCreatingNewSession` | boolean | Flag for new-session folder picker flow |
43
+ | `isMobile` | boolean | Detected via `detectMobile()` |
44
+ | `currentMode` | string | `'chat'` |
45
+ | `planDetector` | PlanDetector | Plan mode detection instance |
46
+ | `aliases` | Object | `{ claude: 'Claude', codex: 'Codex' }` -- populated from `/api/config` |
47
+ | `sessionTabManager` | SessionTabManager | Tab bar controller |
48
+ | `usageStats` | Object | Latest usage data |
49
+ | `sessionTimer` | Object | Session timer data from server |
50
+ | `sessionTimerInterval` | Interval | Client-side timer tick |
51
+ | `splitContainer` | Object | Split/tile view controller |
52
+
53
+ ### Initialization Flow
54
+
55
+ `init()` performs these steps in order:
56
+
57
+ 1. Call `window.authManager.initialize()`. If it returns `false`, the login prompt is displayed and initialization halts.
58
+ 2. Fetch `/api/config` to get folder mode, aliases, and base folder.
59
+ 3. Set up the terminal (xterm.js with fit addon and web links addon).
60
+ 4. Establish WebSocket connection.
61
+ 5. Set up UI event handlers (folder browser, session controls, resize observer).
62
+ 6. Initialize `SessionTabManager`.
63
+ 7. Initialize `PlanDetector`.
64
+ 8. Load existing sessions from the server.
65
+ 9. Start usage polling interval.
66
+
67
+ ### WebSocket Management
68
+
69
+ - **Connection:** Constructs the URL with the session token via `authManager.getWebSocketUrl()`. Reconnects automatically with exponential backoff up to `maxReconnectAttempts`.
70
+ - **Message handling:** Routes incoming messages by `type` field to appropriate handlers (output rendering, session state updates, usage updates, etc.).
71
+ - **Output rendering:** Writes raw terminal data directly to xterm.js via `terminal.write(data)`. Also feeds data to `planDetector.processOutput(data)` and `sessionTabManager.markSessionActivity()`.
72
+
73
+ ### Terminal Configuration
74
+
75
+ ```js
76
+ {
77
+ cursorBlink: true,
78
+ theme: {
79
+ background: '#0d1117',
80
+ foreground: '#f0f6fc',
81
+ cursor: '#f0f6fc',
82
+ // Full 16-color ANSI palette configured
83
+ },
84
+ fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace",
85
+ fontSize: 14, // 13 on mobile
86
+ lineHeight: 1.2,
87
+ scrollback: 10000,
88
+ allowProposedApi: true
89
+ }
90
+ ```
91
+
92
+ The terminal auto-resizes via `FitAddon` triggered by a `ResizeObserver` on the terminal container, debounced to prevent excessive resize messages.
93
+
94
+ ### Folder Browser
95
+
96
+ A modal dialog for selecting working directories:
97
+ - Fetches directory listings from `GET /api/folders?path=...`.
98
+ - Supports navigating up to parent (within `baseFolder` bounds).
99
+ - Supports creating new folders via `POST /api/create-folder`.
100
+ - On selection, either creates a new session or sets the working directory.
101
+
102
+ ### Agent Start Controls
103
+
104
+ When a session has no running agent, the UI presents buttons to start:
105
+ - **Claude** -- sends `{ type: "start_claude" }`
106
+ - **Codex** -- sends `{ type: "start_codex" }`
107
+ - **Copilot** -- sends `{ type: "start_copilot" }`
108
+ - **Gemini** -- sends `{ type: "start_gemini" }`
109
+ - **Terminal** -- sends `{ type: "start_terminal" }`
110
+
111
+ Each button's label uses the configured alias from `/api/config`. Buttons are disabled for tools that are not available on the server.
112
+
113
+ A 45-second client-side timeout acts as a safety net: if no `_started`, `error`, or `exit` message arrives from the server within that window, the loading spinner is replaced with an error message. This prevents the UI from getting permanently stuck if the server fails to respond (e.g., due to a process hang on Windows).
114
+
115
+ ### Usage Dashboard
116
+
117
+ Polls usage data via the WebSocket `get_usage` message at regular intervals. Displays:
118
+ - Session timer (elapsed/remaining in the current session window)
119
+ - Token consumption (input, output, cache)
120
+ - Cost tracking
121
+ - Burn rate and depletion predictions
122
+ - Plan information
123
+
124
+ ### Authenticated Fetch
125
+
126
+ `authFetch(url, options)` wraps `fetch()` to automatically include auth headers:
127
+ ```js
128
+ const authHeaders = window.authManager.getAuthHeaders();
129
+ const mergedOptions = {
130
+ ...options,
131
+ headers: { ...authHeaders, ...(options.headers || {}) }
132
+ };
133
+ return fetch(url, mergedOptions);
134
+ ```
135
+
136
+ ---
137
+
138
+ ## SessionTabManager
139
+
140
+ Source: `src/public/session-manager.js` (~1125 lines)
141
+
142
+ Manages the browser-style tab bar for multi-session support.
143
+
144
+ ### State
145
+
146
+ | Property | Type | Description |
147
+ |----------|------|-------------|
148
+ | `tabs` | `Map<sessionId, HTMLElement>` | Tab DOM elements |
149
+ | `activeSessions` | `Map<sessionId, SessionData>` | Session metadata |
150
+ | `activeTabId` | string | Currently active tab's session ID |
151
+ | `tabOrder` | Array | Visual ordering of tab IDs |
152
+ | `tabHistory` | Array | Most-recently-used ordering (max 50 entries) |
153
+ | `notificationsEnabled` | boolean | Whether desktop notifications are permitted |
154
+
155
+ ### Tab Operations
156
+
157
+ | Operation | Trigger |
158
+ |-----------|---------|
159
+ | Create | "New Tab" button, Ctrl/Cmd+T |
160
+ | Close | Close button, Ctrl/Cmd+W, middle-click |
161
+ | Switch | Tab click, Ctrl/Cmd+Tab (next), Ctrl/Cmd+Shift+Tab (prev), Alt+1-9 |
162
+ | Rename | Double-click tab |
163
+ | Reorder | Drag and drop |
164
+ | Close Others | Right-click context menu |
165
+
166
+ ### Tab Display Name Resolution
167
+
168
+ 1. If the session name is customized (does not start with "Session " containing ":"), use it.
169
+ 2. Otherwise, extract the last path component of `workingDir` as the folder name.
170
+ 3. Fall back to the default session name.
171
+
172
+ ### Drag and Drop
173
+
174
+ Tabs are `draggable="true"`. Reordering uses these events:
175
+ - `dragstart` -- stores the session ID and adds a `.dragging` class.
176
+ - `dragover` -- calculates insertion point via `getDragAfterElement()` (finds the closest tab element based on mouse X position).
177
+ - `dragend` -- syncs the visual order to `tabOrder` and updates overflow menus.
178
+
179
+ ### Mobile Behavior
180
+
181
+ On viewports <= 768px wide:
182
+ - Only the first 2 tabs are visible.
183
+ - An overflow dropdown shows remaining tabs with a count badge.
184
+ - Tabs are automatically reordered by `lastAccessed` timestamp so the most recently used tabs are always visible.
185
+
186
+ ### Status Indicators
187
+
188
+ Each tab has a status dot with states:
189
+ - **idle** -- default, no activity
190
+ - **active** -- agent is producing output (adds `.pulse` animation)
191
+ - **error** -- an error occurred in the session
192
+ - **unread** -- output completed in a background tab (blue indicator)
193
+
194
+ ### Unread Detection
195
+
196
+ When a session transitions from `active` to `idle` in a background tab (not the currently viewed tab), it is marked as unread. This happens via two mechanisms:
197
+
198
+ 1. **Work completion timeout (90s):** If no new output arrives for 90 seconds while the status was `active`, the tab is marked as unread and a notification is sent.
199
+ 2. **Command completion patterns:** Output is checked against regex patterns for common completion indicators:
200
+ - `build successful`, `compilation finished`, `tests passed`
201
+ - `deployment complete`, `npm install completed`
202
+ - `successfully compiled`, `Done in X.Xs`
203
+
204
+ ### Desktop Notifications
205
+
206
+ - Requests permission on first load (deferred by 2 seconds).
207
+ - Shows a prompt banner if permission is `"default"`.
208
+ - Sends `Notification` when the page is not visible and the event is for a background tab.
209
+ - Falls back to in-page toast notifications + vibration on mobile.
210
+ - Notifications auto-close after 5 seconds.
211
+ - Clicking a notification switches to the relevant tab and focuses the window.
212
+
213
+ ### Keyboard Shortcuts
214
+
215
+ | Shortcut | Action |
216
+ |----------|--------|
217
+ | Ctrl/Cmd + T | New tab |
218
+ | Ctrl/Cmd + W | Close current tab |
219
+ | Ctrl/Cmd + Tab | Next tab (uses MRU history first, falls back to visual order) |
220
+ | Ctrl/Cmd + Shift + Tab | Previous tab (visual order) |
221
+ | Alt + 1-9 | Switch to tab by index |
222
+
223
+ ---
224
+
225
+ ## PlanDetector
226
+
227
+ Source: `src/public/plan-detector.js` (~186 lines)
228
+
229
+ Monitors terminal output for plan mode activation and plan content.
230
+
231
+ ### Detection Markers
232
+
233
+ **Plan mode start indicators:**
234
+ - `"Plan mode is active"`
235
+ - `"you MUST NOT make any edits"`
236
+ - `"present your plan by calling the ExitPlanMode tool"`
237
+ - `"Starting plan mode"`
238
+
239
+ **Completed plan patterns:**
240
+ - `## Implementation Plan:`
241
+ - `### <number>. ` (numbered sections)
242
+ - `## Plan:`
243
+ - `### Plan Overview`
244
+ - `## Proposed Solution:`
245
+
246
+ **Plan mode end indicators:**
247
+ - `"User has approved your plan"`
248
+ - `"You can now start coding"`
249
+ - `"Plan mode exited"`
250
+ - `"Exiting plan mode"`
251
+
252
+ ### Processing Flow
253
+
254
+ 1. Raw output is appended to an internal buffer (max 10,000 entries, trimmed to 5,000).
255
+ 2. Recent text is extracted with ANSI codes stripped.
256
+ 3. Checks are performed in order: plan start, completed plan content, plan end.
257
+ 4. Callbacks `onPlanModeChange(isActive)` and `onPlanDetected(plan)` fire on state changes.
258
+
259
+ ### Plan Extraction
260
+
261
+ Three strategies are attempted in order:
262
+ 1. Match `## Implementation Plan:` to a terminal prompt indicator.
263
+ 2. Match a structured plan header with 2+ `###` subsections.
264
+ 3. Match any plan-like content in the last 5,000 characters.
265
+
266
+ The extracted plan text is cleaned of ANSI codes and normalized line endings.
267
+
268
+ ---
269
+
270
+ ## AuthManager (Client)
271
+
272
+ Source: `src/public/auth.js` (~245 lines)
273
+
274
+ See the [Authentication Specification](authentication.md) for full details. Key points:
275
+
276
+ - Global singleton at `window.authManager`.
277
+ - Token persisted in `sessionStorage` (per-tab, cleared on browser close).
278
+ - Full-screen login overlay with password input.
279
+ - `getAuthHeaders()` returns `{ Authorization: "Bearer <token>" }`.
280
+ - `getWebSocketUrl(baseUrl)` appends `?token=<token>` to WebSocket URLs.
281
+
282
+ ---
283
+
284
+ ## PWA Support
285
+
286
+ ### Service Worker
287
+
288
+ Source: `src/public/service-worker.js`
289
+
290
+ - **Cache name:** `claude-code-web-v1`
291
+ - **Precached resources:** `/`, `/index.html`, `/style.css`, `/app.js`, `/session-manager.js`, `/plan-detector.js`
292
+ - **Strategy for API/WebSocket routes:** Network only, with a 503 offline fallback.
293
+ - **Strategy for static assets:** Network first, cache on success, fall back to cache when offline.
294
+ - Activates immediately via `skipWaiting()` + `clients.claim()`.
295
+ - Cleans up old caches on activation.
296
+
297
+ ### Manifest
298
+
299
+ Source: `src/public/manifest.json`
300
+
301
+ Provides installable PWA metadata. Icons are dynamically generated SVGs served by the Express server at `/icon-{size}.png`.
302
+
303
+ ### Dynamic Icon Generation
304
+
305
+ The server generates SVG icons at sizes 16, 32, 144, 180, 192, and 512 pixels:
306
+ - Dark background (`#1a1a1a`) with rounded corners.
307
+ - Monospace "CC" text in orange (`#ff6b00`).
308
+ - Served as `image/svg+xml` with a 1-year `Cache-Control`.