@lelouchhe/webagent 0.1.10 → 0.2.2

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 CHANGED
@@ -3,304 +3,81 @@
3
3
  [![CI](https://github.com/LelouchHe/webagent/actions/workflows/ci.yml/badge.svg)](https://github.com/LelouchHe/webagent/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/@lelouchhe/webagent)](https://www.npmjs.com/package/@lelouchhe/webagent)
5
5
 
6
- A terminal-style web UI for ACP-compatible agents.
7
-
8
- Tech stack: Node.js + TypeScript (`--experimental-strip-types`), REST + SSE real-time communication, SQLite persistence (`better-sqlite3`), Zod validation, esbuild (frontend bundling).
9
-
10
- ## Screenshots
6
+ A terminal-style web UI for [ACP](https://agentclientprotocol.com/)-compatible agents — Copilot CLI, Claude Code, Gemini CLI, and [more](docs/configuration.md#acp-compatible-agents).
11
7
 
12
8
  <table>
13
9
  <tr>
14
- <td width="50%">
15
- <img src="docs/images/overview-chat.png" alt="Desktop chat overview" />
16
- <br />
17
- <sub>Streaming chat in the terminal-style desktop layout.</sub>
10
+ <td width="60%">
11
+ <img src="docs/images/chat-desktop.png" alt="Desktop chat with tool calls and diffs" />
18
12
  </td>
19
- <td width="50%">
20
- <img src="docs/images/plan-busy.png" alt="Plan mode while busy" />
21
- <br />
22
- <sub>Plan mode highlighted while a turn is still running.</sub>
13
+ <td width="40%">
14
+ <img src="docs/images/mobile-chat.png" alt="Mobile layout" />
23
15
  </td>
24
16
  </tr>
17
+ </table>
18
+
19
+ <details>
20
+ <summary>More screenshots</summary>
21
+
22
+ <table>
25
23
  <tr>
26
24
  <td width="50%">
27
- <img src="docs/images/permission-dialog.png" alt="Permission request dialog" />
28
- <br />
29
- <sub>Permission prompts stay inline in the conversation flow.</sub>
25
+ <img src="docs/images/permission.png" alt="Permission dialog" />
26
+ <br /><sub>Inline permission prompts, synced across devices.</sub>
30
27
  </td>
31
28
  <td width="50%">
32
- <img src="docs/images/bash-output.png" alt="Inline bash command output" />
33
- <br />
34
- <sub><code>!&lt;command&gt;</code> output streams directly into the session.</sub>
29
+ <img src="docs/images/slash-menu.png" alt="Slash command menu" />
30
+ <br /><sub>Slash command autocomplete menu.</sub>
35
31
  </td>
36
32
  </tr>
37
33
  </table>
38
34
 
39
- <p align="center">
40
- <img src="docs/images/mobile-autopilot.png" alt="Mobile autopilot mode" width="320" />
41
- <br />
42
- <sub>Compact mobile layout with mode highlighting and terminal-style action keys.</sub>
43
- </p>
44
-
45
- ## Prerequisites
46
-
47
- - Node.js 22.6+ (requires `--experimental-strip-types`)
48
- - An ACP-compatible agent installed and authenticated
49
-
50
- ### ACP-Compatible Agents
35
+ </details>
51
36
 
52
- WebAgent works with any agent that implements the [Agent Client Protocol](https://agentclientprotocol.com/). Some options:
37
+ ## Quick Start
53
38
 
54
- | Agent | Command | Notes |
55
- |---|---|---|
56
- | [Copilot CLI](https://github.com/github/copilot-cli) | `copilot --acp` | Default. GitHub's AI pair programmer |
57
- | [Claude Code](https://docs.anthropic.com/en/docs/agents/claude-code) | `claude --acp` | Anthropic's coding agent |
58
- | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini --acp` | Google's Gemini models |
59
- | [OpenCode](https://opencode.ai/) | `opencode --acp` | Open-source, extensible |
60
-
61
- See the [ACP Registry](https://agentclientprotocol.com/get-started/agents) for the full list. To use a different agent, set `agent_cmd` in your config:
62
-
63
- ```toml
64
- agent_cmd = "claude --acp"
65
- ```
66
-
67
- ## Install
39
+ **Prerequisites:** Node.js 22.6+, an ACP-compatible agent installed and authenticated.
68
40
 
69
41
  ```bash
70
42
  npm install -g @lelouchhe/webagent
43
+ webagent # start on port 6800
44
+ webagent --config /path/to/config.toml # custom config
71
45
  ```
72
46
 
73
- Or run directly with npx:
74
-
75
- ```bash
76
- npx @lelouchhe/webagent
77
- ```
78
-
79
- ## Run
80
-
81
- ```bash
82
- webagent # start with defaults (port 6800)
83
- webagent --config /path/to/config.toml # start with custom config
84
- ```
85
-
86
- Data (SQLite database, uploaded images) is stored in `./data/` relative to your current working directory by default.
87
-
88
- ### From source
89
-
90
- ```bash
91
- git clone https://github.com/LelouchHe/webagent.git
92
- cd webagent
93
- npm install
94
- npm run build # bundle frontend TS → dist/ (esbuild)
95
- npm start # start on port 6800
96
- ```
97
-
98
- ### Development
99
-
100
- ```bash
101
- npm run dev # port 6801, esbuild watch + server auto-restart on file changes
102
- ```
103
-
104
- ### Service management
105
-
106
- WebAgent includes a built-in daemon with crash recovery:
107
-
108
- ```bash
109
- webagent start --config config.toml # start as background daemon
110
- webagent stop # stop the daemon
111
- webagent restart # atomic restart (Unix) / stop+start (Windows)
112
- webagent status # show running state
113
- ```
114
-
115
- The daemon writes a PID file (`webagent.pid`) and log file (`webagent.log`) in the current directory. Run all commands from the same directory.
116
-
117
- For auto-start on boot, see [docs/autostart.md](docs/autostart.md) (launchd, systemd, crontab, and Windows Task Scheduler examples).
118
-
119
- ### Configuration
120
-
121
- Configuration is via TOML files, passed with `--config`:
122
-
123
- ```bash
124
- webagent --config config.toml
125
- ```
126
-
127
- If no `--config` is provided, all settings use built-in defaults. See `config.toml` for the checked-in default settings and `config.dev.toml` for development.
128
-
129
- | Key | Default | Description |
130
- |---|---|---|
131
- | `port` | `6800` | HTTP server port |
132
- | `data_dir` | `data` | SQLite + uploads directory |
133
- | `default_cwd` | `process.cwd()` | Working directory for new sessions |
134
- | `public_dir` | `dist` | Static assets directory |
135
- | `agent_cmd` | `copilot --acp` | ACP agent command (binary + args, space-separated) |
136
- | `limits.bash_output` | `1048576` (1 MB) | Max bash output stored in DB per command |
137
- | `limits.image_upload` | `10485760` (10 MB) | Max image upload size |
138
- | `limits.cancel_timeout` | `10000` (10s) | Cancel timeout in ms; 0 disables |
139
- | `push.vapid_subject` | `mailto:webagent@localhost` | VAPID subject for Web Push (email or URL) |
140
-
141
- To use a different ACP-compatible agent backend:
142
-
143
- ```toml
144
- agent_cmd = "my-agent --acp"
145
- ```
146
-
147
- ## Features
148
-
149
- ### Chat
150
-
151
- - Real-time streaming responses with Markdown rendering + syntax highlighting
152
- - Collapsible thinking process display
153
- - Tool call display (status animation, expandable details, diff rendering)
154
- - Agent execution plan display (pending ○ / in-progress ◉ / done ●)
155
- - Permission confirmation dialog for sensitive operations (Allow / Deny), synced across devices; auto-approved in autopilot mode
156
- - Smart scroll: force-scrolls on load/switch/send, soft auto-scroll during streaming
157
-
158
- ### Images
159
-
160
- - Upload images (button or `^U` shortcut)
161
- - Paste images (Ctrl+V / Cmd+V)
162
- - Preview before sending + removable, supports multiple images
163
- - Server-side storage, displayed inline in chat
164
-
165
- ### Bash Execution
166
-
167
- - `!<command>` to run shell commands directly
168
- - Real-time output streaming (stderr in red)
169
- - Collapsible output with exit code display
170
- - Cancel running processes
171
- - Cancel is session-scoped inside WebAgent: it stops the current ACP turn plus WebAgent-owned session work (like local `!` bash), but it cannot stop host-level tasks started outside the WebAgent server/runtime
172
-
173
- ### Session Management
174
-
175
- - Auto-resumes last session on page open, no manual switching needed
176
- - After server restart, restores session context via ACP `loadSession` so conversations can continue
177
- - Auto-generated titles (async, using a fast model)
178
- - Session history persisted in SQLite, survives restarts
179
- - `/switch` lists all sessions (git-branch style, `*` marks current in green)
180
- - Switching sessions replays full message history
181
-
182
- ### Slash Commands
183
-
184
- Type `/` to trigger an autocomplete menu with arrow keys to navigate, Esc to close.
185
-
186
- | Key | In menu | Without menu |
187
- |---|---|---|
188
- | `Tab` | Fill selected item into input | — |
189
- | `Enter` | Send current input | Send current input |
190
- | Click/Tap | Fill and send (Tab + Enter) | — |
191
-
192
- Commands with submenus (`/model`, `/mode`, `/think`, `/notify`, `/switch`, `/new`) show a picker after typing the command and a space. Tab completes the selection into the input so you can review or edit before pressing Enter to send.
193
-
194
- | Command | Description |
195
- |---|---|
196
- | `/new [cwd]` | Create new session (optionally specify working directory) |
197
- | `/pwd` | Show current working directory |
198
- | `/model [name]` | View or switch model (fuzzy match, e.g. `/model opus`) |
199
- | `/mode [name]` | View or switch mode (Agent / Plan / Autopilot) |
200
- | `/think [level]` | View or switch reasoning effort (low / medium / high) |
201
- | `/notify [on\|off]` | Toggle push notifications for background alerts |
202
- | `/cancel` | Cancel current response |
203
- | `/switch <title\|id>` | Switch to a session (match by title or ID prefix) |
204
- | `/rename <new title>` | Rename current session |
205
- | `/exit` | Close current session (delete + switch to previous) |
206
- | `/prune` | Delete all sessions except current |
207
-
208
- Type `?` for inline help listing all commands and shortcuts.
209
-
210
- ### Keyboard Shortcuts
211
-
212
- | Shortcut | Action |
213
- |---|---|
214
- | `Enter` | Send message |
215
- | `Shift+Enter` | New line |
216
- | `Ctrl+X` | Cancel current response |
217
- | `Ctrl+M` | Cycle mode (Agent → Plan → Autopilot) |
218
- | `Ctrl+U` | Upload image |
219
-
220
- Tap the `❯` prompt indicator to cycle mode. Tap `new` to create a new session (hidden when input has content).
221
-
222
- ### Theme
47
+ Or run directly: `npx @lelouchhe/webagent`
223
48
 
224
- - Dark / light / system, toggle with `◑`
225
- - Terminal-style UI (monospace font, `>_` logo)
226
- - Preference saved to localStorage
227
-
228
- ### Other
229
-
230
- - PWA support (installable to home screen)
231
- - Web Push notifications — background alerts when no browser tab is visible (use `/notify on`)
232
- - SSE auto-reconnect (3s retry on disconnect)
233
- - 30s heartbeat keepalive
234
- - Auto-expanding input box
235
- - Mobile-friendly layout
236
- - Multi-client broadcast (events synced across devices)
237
-
238
- ## Testing
239
-
240
- ```bash
241
- npm test # unit + integration
242
- npm run test:e2e # Playwright browser E2E
243
- ```
244
-
245
- - `TEST_SCENARIOS.md` is the scenario-level coverage map for the current suite.
246
- - Use it when reviewing what is already protected before adding new tests or auditing gaps.
247
- - The E2E suite now covers session lifecycle, reconnect/restart recovery, permissions, cancel flows, bash lifecycle, media persistence, slash-menu UX, config persistence/inheritance, and multi-client config behavior.
49
+ Data (SQLite database, uploaded images) is stored in `./data/` by default. See [Configuration & Operations](docs/configuration.md) for daemon mode, TOML settings, and agent setup.
248
50
 
249
51
  ## Architecture
250
52
 
251
53
  ```
252
- Browser REST+SSE server.ts ACP copilot CLI
253
- ├── routes.ts (HTTP handlers)
254
- ├── event-handler.ts (ACP event routing)
255
- ├── session-manager.ts (state)
256
- ├── title-service.ts (auto-title)
257
- ├── push-service.ts (Web Push)
258
- ├── daemon.ts (background service)
259
- └── store.ts (SQLite)
54
+ Browser ←── REST + SSE ──→ Server ←── ACP ──→ Agent CLI
55
+ (thin client) (Node.js) (copilot/claude/gemini)
260
56
  ```
261
57
 
262
- - **server.ts**HTTP server bootstrap
263
- - **routes.ts** — HTTP request handlers (static files, REST API, image upload, push subscription)
264
- - **event-handler.ts** — ACP event routing + SSE broadcast
265
- - **session-manager.ts** — Session state management (live sessions, buffers, bash procs, model cache)
266
- - **bridge.ts** — ACP bridge, manages agent subprocess, handles permissions and file I/O
267
- - **store.ts** — SQLite persistence (sessions, events, push subscriptions; WAL mode)
268
- - **title-service.ts** — Async session title generation (dedicated Haiku session)
269
- - **push-service.ts** — Web Push notifications (VAPID keys, subscriptions, visibility-gated delivery)
270
- - **daemon.ts** — Background service management (start/stop/status/restart) with supervisor
271
- - **types.ts** — Shared types + Zod schemas
272
- - **shared/constants.ts** — Constants shared between frontend and backend (tool icons, plan status icons)
273
- - **public/js/*.ts** — Frontend TypeScript source, bundled by esbuild into a single `dist/js/app.[hash].js`
274
-
275
- ## ACP Scope and Current Limits
276
-
277
- WebAgent uses ACP for the core agent loop: session creation / restore, prompt turns, permission requests, streaming updates, model selection, and text file read/write.
278
-
279
- Current scope in this repo:
58
+ The frontend is a standard browser client that talks to the server over REST + SSE. The API is the boundary anyone can build their own client.
280
59
 
281
- - Session lifecycle goes through ACP (`newSession`, `loadSession`, `prompt`, `cancel`)
282
- - The UI renders a subset of ACP session updates: assistant text, thinking text, tool calls, tool call updates, and plans
283
- - Session history is persisted locally and restored after server restart
284
-
285
- ### Client extensions
286
-
287
- ACP allows the client to inject extra capabilities into the agent on top of its native baseline. WebAgent currently provides:
288
-
289
- | Extension | Status | Notes |
290
- |---|---|---|
291
- | `fs` (readTextFile / writeTextFile) | ✅ Implemented | Agent can read/write files through the client |
292
- | `terminal` | Declared but not wired | `!<command>` runs via the app's own local bash bridge, not ACP `terminal/*` |
293
- | `mcpServers` | `[]` (no extras) | Agent's own MCP servers (e.g. GitHub MCP) work normally; passing `[]` means the client isn't providing additional ones |
60
+ | Module | Role |
61
+ |---|---|
62
+ | `routes.ts` | REST API + static files ([full API reference](docs/api.md)) |
63
+ | `event-handler.ts` | ACP event routing → SSE broadcast |
64
+ | `session-manager.ts` | Session state, buffers, bash processes |
65
+ | `bridge.ts` | ACP bridge — agent subprocess lifecycle |
66
+ | `store.ts` | SQLite persistence (WAL mode) |
67
+ | `daemon.ts` | Background service with crash recovery |
294
68
 
295
- Passing `mcpServers: []` does **not** disable MCP — the agent loads its own configured MCP servers independently. The parameter is for the client to provide _additional_ servers the agent wouldn't have on its own.
69
+ Tech stack: Node.js + TypeScript (`--experimental-strip-types`), SQLite (`better-sqlite3`), Zod validation, esbuild bundling.
296
70
 
297
- ### Current limits
71
+ Frontend source lives in `public/js/*.ts`, bundled by esbuild into a single content-hashed JS file. See [Client Architecture](docs/client-architecture.md).
298
72
 
299
- - The web UI does not expose native CLI command surfaces such as `/plan`, `/fleet`, `/mcp`, `/agent`, or `/skills`
300
- - Autopilot mode is supported: permissions are auto-approved server-side using `allow_once`
301
- - Event handling is intentionally narrower than a native CLI client; only selected ACP updates are rendered/persisted, and the silent title-generation session suppresses normal UI events
302
- - Model switching depends on the agent's ACP implementation and currently uses the SDK's unstable session-model API
303
- - ACP does not expose context window usage, token counts, or remaining capacity
304
- - No method to compact or clear session context; only option is to create a new session
73
+ ## Documentation
305
74
 
306
- In practice, this means WebAgent provides a browser UI for the core ACP chat/session workflow, but not the full product surface of direct Copilot CLI or Claude Code in a terminal.
75
+ | Document | Contents |
76
+ |---|---|
77
+ | **[Features](docs/features.md)** | Chat, images, bash, sessions, slash commands, keyboard shortcuts, themes |
78
+ | **[Configuration & Operations](docs/configuration.md)** | TOML config, daemon commands, agent setup, upgrading |
79
+ | **[API Reference](docs/api.md)** | REST endpoints, SSE events, implementation details |
80
+ | **[ACP Integration](docs/acp.md)** | Client extensions, protocol scope, current limits |
81
+ | **[Client Architecture](docs/client-architecture.md)** | Frontend modules, data flow, conventions |
82
+ | **[Development](docs/development.md)** | Building from source, dev mode, testing, publishing |
83
+ | **[Auto-Start on Boot](docs/autostart.md)** | launchd, systemd, crontab, Windows Task Scheduler |
package/dist/index.html CHANGED
@@ -13,7 +13,7 @@
13
13
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
14
  <script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.2/dist/purify.min.js"></script>
15
15
  <script>document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || 'auto');</script>
16
- <link rel="stylesheet" href="/styles.01a9ju9l.css">
16
+ <link rel="stylesheet" href="/styles.01a6wdjv.css">
17
17
  </head>
18
18
  <body>
19
19
 
@@ -35,13 +35,12 @@
35
35
  <div id="slash-menu"></div>
36
36
  <span id="input-prompt" title="Cycle mode (Ctrl+M)">❯ </span>
37
37
  <textarea id="input" rows="1" placeholder="Message or ?"></textarea>
38
- <button id="new-btn" class="input-btn" title="New session">new</button>
39
38
  <button id="attach-btn" class="input-btn" title="Attach image (Ctrl+U)">^U</button>
40
39
  <button id="send-btn" class="input-btn" title="Send (Enter)">↵</button>
41
40
  <input type="file" id="file-input" accept="image/*" multiple hidden>
42
41
  </div>
43
42
  <div id="status-bar"></div>
44
43
 
45
- <script src="/js/app.IXP5KGP6.js"></script>
44
+ <script src="/js/app.C4WRSLDF.js"></script>
46
45
  </body>
47
46
  </html>
@@ -0,0 +1,10 @@
1
+ var ue=class extends Error{name="ApiError";status;constructor(t,s){super(s),this.status=t}};async function U(e,t){let s=await fetch(e,t);if(!s.ok){let r=`HTTP ${s.status}`;try{let a=await s.json();a.error&&(r=String(a.error))}catch{}throw new ue(s.status,r)}let i=await s.text();if(i)return JSON.parse(i)}function q(e,t){return U(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}function _e(e){let t={};return e?.cwd&&(t.cwd=e.cwd),e?.inheritFromSessionId&&(t.inheritFromSessionId=e.inheritFromSessionId),q("/api/v1/sessions",t)}function pe(e){return U("/api/v1/sessions/"+e,{method:"DELETE"})}function te(){return U("/api/v1/sessions")}function $(e){return U("/api/v1/sessions/"+e)}function fe(e,t,s){let i={text:t};return s?.length&&(i.images=s),q("/api/v1/sessions/"+e+"/prompt",i)}function Ne(e){return q("/api/v1/sessions/"+e+"/cancel",{})}function ne(e,t,s){return q("/api/v1/sessions/"+e+"/permissions/"+t,{optionId:s})}function se(e,t){return q("/api/v1/sessions/"+e+"/permissions/"+t,{denied:!0})}function K(e,t,s){let i=t.replace(/_/g,"-");return U("/api/v1/sessions/"+e+"/"+i,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:s})})}function qe(e,t){return U("/api/v1/sessions/"+e+"/title",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})}function Be(e,t){return q("/api/v1/sessions/"+e+"/bash",{command:t})}function V(e,t,s){let i={visible:t};return s&&(i.sessionId=s),q("/api/beta/clients/"+e+"/visibility",i)}var T=e=>document.querySelector(e),o={messages:T("#messages"),input:T("#input"),sendBtn:T("#send-btn"),prompt:T("#input-prompt"),status:T("#status"),sessionInfo:T("#session-info"),attachBtn:T("#attach-btn"),fileInput:T("#file-input"),attachPreview:T("#attach-preview"),themeBtn:T("#theme-btn"),slashMenu:T("#slash-menu"),inputArea:T("#input-area"),statusBar:T("#status-bar")},n={eventSource:null,clientId:null,sessionId:null,sessionSwitchGen:0,sessionCwd:null,sessionTitle:null,awaitingNewSession:!1,configOptions:[],currentAssistantEl:null,currentAssistantText:"",currentThinkingEl:null,currentThinkingText:"",busy:!1,pendingImages:[],currentBashEl:null,followMessages:!0,pendingToolCallIds:new Set,pendingPermissionRequestIds:new Set,pendingPromptDone:!1,turnEnded:!1,newTurnStarted:!1,sentMessageForSession:null,sentBashForSession:null,cancelTimeout:1e4,serverVersion:null,agentName:null,agentVersion:null,_cancelTimerId:null,_onCancelTimeout:null,lastEventSeq:0,oldestLoadedSeq:0,hasMoreHistory:!1,loadingOlderEvents:!1,replayInProgress:!1,replayTarget:null,replayQueue:[],unconfirmedPermissions:new Map},dt={disconnected:"is-disconnected",connecting:"is-connecting",connected:"is-connected"};function B(e,t=e){o.status.textContent="",o.status.className=`status-dot ${dt[e]}`,o.status.dataset.state=e,o.status.setAttribute("aria-label",t),o.status.setAttribute("title",t)}function x(e){return n.configOptions.find(t=>t.id===e)}function ie(e){return x(e)?.currentValue??null}function Oe(e,t){let s=x(e);s&&(s.currentValue=t)}function me(e){n.configOptions=e,_(),O()}function _(){o.inputArea.classList.remove("plan-mode","autopilot-mode");let e=ie("mode")||"";e.includes("#plan")?o.inputArea.classList.add("plan-mode"):e.includes("#autopilot")&&o.inputArea.classList.add("autopilot-mode")}function O(){if(!o.statusBar)return;let e=ie("model"),t=n.sessionCwd||"";if(o.statusBar.textContent="",t){e&&o.statusBar.appendChild(document.createTextNode(e+" \xB7 "));let s=document.createElement("span");s.className="status-cwd",s.textContent=t,o.statusBar.appendChild(s)}else e&&(o.statusBar.textContent=e)}function I(e){n.busy=e,e?(o.sendBtn.textContent="^C",o.sendBtn.title="Cancel (Ctrl+C)",o.sendBtn.classList.add("cancel"),o.prompt.classList.add("busy")):(o.sendBtn.textContent="\u21B5",o.sendBtn.title="Send (Enter)",o.sendBtn.classList.remove("cancel"),o.prompt.classList.remove("busy"))}function M({cwd:e,inheritFromSessionId:t=n.sessionId}={}){n.awaitingNewSession=!0,_e({cwd:e,inheritFromSessionId:t}).catch(()=>{})}var Ae=[];function Re(e){Ae.push(e)}function y(){for(let e of Ae)e();o.messages.innerHTML="",n.currentAssistantEl=null,n.currentAssistantText="",n.currentThinkingEl=null,n.currentThinkingText="",n.pendingImages.length=0,n.followMessages=!0,n.pendingToolCallIds.clear(),n.pendingPermissionRequestIds.clear(),n.unconfirmedPermissions.clear(),n.pendingPromptDone=!1,n.turnEnded=!1,n.newTurnStarted=!1,n._cancelTimerId=null,n.lastEventSeq=0,n.oldestLoadedSeq=0,n.hasMoreHistory=!1,n.loadingOlderEvents=!1,n.replayInProgress=!1,n.replayQueue=[],o.attachPreview.innerHTML="",o.attachPreview.classList.remove("active"),o.input.disabled=!1,o.sendBtn.disabled=!1,o.input.placeholder="Message or ?",I(!1),n.sessionTitle=null,n.sessionCwd=null,n.configOptions=[],oe(null,null),o.statusBar&&(o.statusBar.textContent="")}function G(){return!n.busy||!n.sessionId?!1:(Ne(n.sessionId).catch(()=>{}),J(),n.cancelTimeout>0&&(n._cancelTimerId=setTimeout(()=>{n._cancelTimerId=null,n.busy&&(n.turnEnded=!0,I(!1),n._onCancelTimeout?.())},n.cancelTimeout)),!0)}function J(){n._cancelTimerId!=null&&(clearTimeout(n._cancelTimerId),n._cancelTimerId=null)}function De(){return location.hash.slice(1)||null}function Fe(e){history.replaceState(null,"",`#${e}`)}function oe(e,t){o.sessionInfo.textContent=t||(e?e.slice(0,8)+"\u2026":""),document.title=t||">_"}B("disconnected");marked.setOptions({breaks:!0,gfm:!0});function re(e){return DOMPurify.sanitize(marked.parse(e))}function R(e,t){let s=document.createElement("div");return s.className=`msg ${e}`,s.innerHTML=e==="user"?p(t).replace(/\n/g,"<br>"):re(t),E(s),s}function c(e){let t=document.createElement("div");return t.className="system-msg",t.textContent=e,E(t),t}function P(){n.currentAssistantEl=null,n.currentAssistantText=""}function k(){if(n.currentThinkingEl){let e=n.currentThinkingEl.querySelector("summary");e.textContent="\u283F thought",e.classList.remove("active"),e.style.animation="none",n.currentThinkingEl=null,n.currentThinkingText=""}}var A=null,ut=80;function he(e){return e.scrollHeight-e.scrollTop-e.clientHeight<ut}function pt(){n.followMessages=he(o.messages)}o.messages.addEventListener("scroll",pt);function ft(){return n.followMessages||he(o.messages)}function E(e,t=!1){if(n.replayTarget)return n.replayTarget.appendChild(e),e;let s=t||ft();return o.messages.appendChild(e),S(s),e}function je(){D(),A=document.createElement("div"),A.id="waiting",A.innerHTML='<span class="cursor">\u258C</span>',E(A,!0)}function D(){A&&(A.remove(),A=null)}var ge=!1;function S(e){let t=o.messages;if(e||n.followMessages){typeof requestAnimationFrame=="function"?ge||(ge=!0,requestAnimationFrame(()=>{ge=!1,t.scrollTop=t.scrollHeight})):t.scrollTop=t.scrollHeight,n.followMessages=!0;return}n.followMessages=he(t)}function p(e){let t=document.createElement("div");return t.textContent=e,t.innerHTML}function We(e){if(!e)return"";let t=new Date(e.endsWith("Z")?e:e+"Z"),s=i=>String(i).padStart(2,"0");return`${t.getFullYear()}-${s(t.getMonth()+1)}-${s(t.getDate())} ${s(t.getHours())}:${s(t.getMinutes())}`}function ye(e){if(typeof e=="string"&&e.includes("*** Begin Patch")){let t=e.split(`
2
+ `),s=[];for(let i of t)i.startsWith("*** Begin Patch")||i.startsWith("*** End Patch")||(i.startsWith("*** Update File:")||i.startsWith("*** Add File:")||i.startsWith("*** Delete File:")?s.push(`<span class="diff-file">${p(i)}</span>`):i.startsWith("@@")?s.push(`<span class="diff-hunk">${p(i)}</span>`):i.startsWith("-")?s.push(`<span class="diff-del">${p(i)}</span>`):i.startsWith("+")?s.push(`<span class="diff-add">${p(i)}</span>`):s.push(p(i)));return s.join(`
3
+ `)}if(e&&typeof e=="object"){let t=[];if(e.path&&t.push(`<span class="diff-file">*** ${p(e.path)}</span>`),e.old_str!=null)for(let s of String(e.old_str).split(`
4
+ `))t.push(`<span class="diff-del">- ${p(s)}</span>`);if(e.new_str!=null)for(let s of String(e.new_str).split(`
5
+ `))t.push(`<span class="diff-add">+ ${p(s)}</span>`);if(e.file_text!=null)for(let s of String(e.file_text).split(`
6
+ `))t.push(`<span class="diff-add">+ ${p(s)}</span>`);return t.length>(e.path?1:0)?t.join(`
7
+ `):null}return null}function Q(e,t=!1){let s=document.createElement("div");return s.className="bash-block",s.innerHTML=`<span class="bash-cmd${t?" running":""}">${p(e)}</span><div class="bash-output"></div>`,s.querySelector(".bash-cmd").addEventListener("click",()=>{let i=s.querySelector(".bash-output");i.style.display==="none"?i.style.display="block":i.classList.contains("has-content")&&(i.style.display="none")}),E(s),t&&(n.currentBashEl=s),s}function Y(e,t,s){if(!e)return;let i=e.querySelector(".bash-cmd");i.classList.remove("running");let r="";if(s?r=`[signal: ${s}]`:t!==0&&t!=null&&(r=`[exit: ${t}]`),r){let a=document.createElement("span");a.className=`bash-exit ${t===0?"ok":"fail"}`,a.textContent=r,i.after(a)}e===n.currentBashEl&&(n.currentBashEl=null)}var mt={auto:"\u25D1",light:"\u2600",dark:"\u263E"},Ue=["auto","light","dark"];function Ke(){return localStorage.getItem("theme")||"auto"}function Ve(e){document.documentElement.setAttribute("data-theme",e),o.themeBtn.textContent=mt[e],o.themeBtn.title=`Theme: ${e}`,localStorage.setItem("theme",e)}o.themeBtn.onclick=()=>{let e=Ke();Ve(Ue[(Ue.indexOf(e)+1)%3])};Ve(Ke());var we={task_complete:"\u2714",read:"cat",edit:"edit",execute:"exec",search:"find",delete:"rm"},ve="run",Ie={pending:"\u25CB",in_progress:"\u25C9",completed:"\u25CF"};function Ge(e){return n.replayTarget?.querySelector(`[id="${e}"]`)??document.getElementById(e)}function gt(e){return n.replayTarget?.querySelector(e)??document.querySelector(e)}var Je="webagent_notify_tip_shown",Qe="webagent_notify_tip_denied_shown";function ht(){if(typeof Notification>"u"||n.replayInProgress)return;let e=Notification.permission;if(e!=="granted"){if(e==="denied"){if(localStorage.getItem(Qe))return;localStorage.setItem(Qe,"1"),c("tip: notifications are blocked \u2014 allow in browser site settings to enable");return}localStorage.getItem(Je)||(localStorage.setItem(Je,"1"),c("tip: use /notify to enable background notifications"))}}function ae(){n.pendingPromptDone&&(n.pendingToolCallIds.size>0||n.pendingPermissionRequestIds.size>0||(D(),k(),P(),I(!1),n.pendingPromptDone=!1,ht()))}function yt(){for(let e of n.pendingToolCallIds){let t=document.getElementById(`tc-${e}`);if(!t)continue;t.className="tool-call failed";let s=t.querySelector(".icon");s&&(s.textContent="\u2717")}for(let e of n.pendingPermissionRequestIds){let t=document.querySelector(`.permission[data-request-id="${e}"]`);if(!t||!t.querySelector("button"))continue;let i=t.querySelector(".title")?.textContent||"\u26BF";t.innerHTML=`<span style="opacity:0.5">${p(i)} \u2014 cancelled</span>`}n.pendingToolCallIds.clear(),n.pendingPermissionRequestIds.clear()}function wt(){for(let e of n.pendingToolCallIds){let t=document.getElementById(`tc-${e}`);if(!t)continue;t.className="tool-call completed";let s=t.querySelector(".icon");s&&(s.textContent="\u2713")}n.pendingToolCallIds.clear(),n.pendingPermissionRequestIds.clear()}function be(e){if(Array.isArray(e))return{events:e,streaming:{thinking:!1,assistant:!1}};let t=e;return{events:t.events??[],streaming:t.streaming??{thinking:!1,assistant:!1},total:typeof t.total=="number"?t.total:void 0,hasMore:typeof t.hasMore=="boolean"?t.hasMore:void 0}}var Ye=200;async function N(e){n.replayInProgress=!0,n.replayQueue=[];try{let t=await fetch(`/api/v1/sessions/${e}/events?limit=${Ye}`);if(!t.ok)return!1;let s=await t.json(),{events:i,streaming:r,hasMore:a}=be(s),l=document.createDocumentFragment(),d=Ee(i);n.replayTarget=l;for(let u=0;u<i.length;u++){let f=JSON.parse(i[u].data);ke(i[u].type,f,i,u,d)}return n.replayTarget=null,o.messages.style.display="none",o.messages.appendChild(l),o.messages.style.display="",i.length&&(n.lastEventSeq=i[i.length-1].seq,n.oldestLoadedSeq=i[0].seq),n.hasMoreHistory=a===!0,n.hasMoreHistory&&vt(),ze(),Se(i,r),!0}catch{return!1}finally{n.replayTarget=null,n.replayInProgress=!1,Xe()}}function Se(e,t){if(t.thinking&&e.length){for(let s=e.length-1;s>=0;s--)if(e[s].type==="thinking"){let i=JSON.parse(e[s].data),r=o.messages.querySelectorAll(".thinking"),a=r[r.length-1];if(a){n.currentThinkingEl=a,n.currentThinkingText=i.text;let l=a.querySelector("summary");l&&(l.textContent="\u283F thinking...",l.classList.add("active"))}break}}if(t.assistant&&e.length){for(let s=e.length-1;s>=0;s--)if(e[s].type==="assistant_message"){let i=JSON.parse(e[s].data),r=o.messages.querySelectorAll(".msg.assistant"),a=r[r.length-1];a&&(n.currentAssistantEl=a,n.currentAssistantText=i.text);break}}}function ze(){let e=o.messages.querySelector("[data-sync-boundary]");e&&e.removeAttribute("data-sync-boundary");let t=o.messages.lastElementChild;t&&t.setAttribute("data-sync-boundary","")}async function Te(e){n.replayInProgress=!0,n.replayQueue=[];try{let t=`/api/v1/sessions/${e}/events?after=${n.lastEventSeq}`,s=await fetch(t);if(!s.ok)return!1;let i=await s.json(),{events:r,streaming:a}=be(i),l=o.messages.querySelector("[data-sync-boundary]");if(l)for(;l.nextElementSibling;)l.nextElementSibling.remove();if(n.currentAssistantEl=null,n.currentAssistantText="",n.currentThinkingEl=null,n.currentThinkingText="",n.currentBashEl=null,r.length===0)return Se(r,a),!0;let d=document.createDocumentFragment(),u=Ee(r);n.replayTarget=d;for(let f=0;f<r.length;f++){let g=JSON.parse(r[f].data);ke(r[f].type,g,r,f,u)}return n.replayTarget=null,o.messages.appendChild(d),n.lastEventSeq=r[r.length-1].seq,ze(),Se(r,a),!0}catch{return!1}finally{n.replayTarget=null,n.replayInProgress=!1,Xe()}}var z=null;function vt(){le();let e=document.createElement("div");e.id="history-sentinel",e.className="history-sentinel",e.textContent="\u2191 loading\u2026",o.messages.prepend(e),typeof IntersectionObserver=="function"&&(z=new IntersectionObserver(t=>{t[0]?.isIntersecting&&!n.loadingOlderEvents&&n.hasMoreHistory&&n.sessionId&&It(n.sessionId)},{root:o.messages,rootMargin:"200px 0px 0px 0px"}),z.observe(e))}function le(){z&&(z.disconnect(),z=null),document.getElementById("history-sentinel")?.remove()}Re(le);async function It(e){if(n.loadingOlderEvents||!n.hasMoreHistory||n.oldestLoadedSeq<=0)return!1;n.loadingOlderEvents=!0;try{let t=await fetch(`/api/v1/sessions/${e}/events?limit=${Ye}&before=${n.oldestLoadedSeq}`);if(!t.ok||e!==n.sessionId)return!1;let s=await t.json(),{events:i,hasMore:r}=be(s);if(i.length===0)return n.hasMoreHistory=!1,le(),!0;let a=document.createDocumentFragment(),l=Ee(i);n.replayTarget=a;for(let g=0;g<i.length;g++){let v=JSON.parse(i[g].data);ke(i[g].type,v,i,g,l)}n.replayTarget=null;let d=o.messages,u=d.scrollHeight,f=document.getElementById("history-sentinel");return f?f.after(a):d.prepend(a),d.scrollTop+=d.scrollHeight-u,n.oldestLoadedSeq=i[0].seq,n.hasMoreHistory=r===!0,n.hasMoreHistory||le(),!0}catch{return!1}finally{n.loadingOlderEvents=!1}}function Ze(){for(let[e,t]of n.unconfirmedPermissions){let s=document.querySelector(`.permission[data-request-id="${e}"]`);if(!s||!s.querySelector("button")){n.unconfirmedPermissions.delete(e);continue}t.denied?se(t.sessionId,e).catch(()=>{}):ne(t.sessionId,e,t.optionId).catch(()=>{});let i=s.dataset.title?`\u26BF ${p(s.dataset.title)}`:"\u26BF";s.innerHTML=`<span style="opacity:0.5">${i} \u2014 ${p(t.optionName)}</span>`,n.unconfirmedPermissions.delete(e)}}function Ee(e){let t=new Set;for(let s of e)s.type==="permission_response"&&t.add(JSON.parse(s.data).requestId);return{toolCalls:new Map,permissions:new Map,resolvedPermissions:t,currentBashEl:null}}function ke(e,t,s,i,r){switch(e){case"user_message":{let a=R("user",t.text);if(t.images)for(let l of t.images){let d=document.createElement("img");d.className="user-image",d.src=l.path,a.appendChild(d)}break}case"assistant_message":{let l=(n.replayTarget||o.messages).lastElementChild;if(l&&l.classList.contains("msg")&&l.classList.contains("assistant")){let f=(l.getAttribute("data-raw")||"")+t.text;l.setAttribute("data-raw",f),l.innerHTML=re(f);break}R("assistant",t.text).setAttribute("data-raw",t.text);break}case"thinking":{let l=(n.replayTarget||o.messages).lastElementChild;if(l&&l.classList.contains("thinking")){let u=l.querySelector(".thinking-content");if(u){u.textContent+=`
8
+ `+t.text;break}}let d=document.createElement("details");d.className="thinking",d.innerHTML=`<summary>\u283F thought</summary><div class="thinking-content">${p(t.text)}</div>`,E(d);break}case"tool_call":{let a=we[t.kind]||ve,l=document.createElement("div");l.className="tool-call",l.id=`tc-${t.id}`,l.dataset.kind=t.kind;let d=`<span class="icon">${a}</span> ${p(t.title)}`,u=t.rawInput;u&&u.command?d+=`<span class="tc-detail">$ ${p(u.command)}</span>`:u&&u.path&&(d+=`<span class="tc-detail">${p(u.path)}</span>`),l.innerHTML=d;let f=t.kind==="edit"?ye(u):null;if(f){let v=document.createElement("details");v.innerHTML=`<summary>diff</summary><div class="diff-view">${f}</div>`,l.appendChild(v)}let g=l.querySelector(".tc-detail");g&&l.addEventListener("click",v=>{v.target.closest("details")||g.classList.toggle("expanded")}),E(l),r&&r.toolCalls.set(t.id,l);break}case"tool_call_update":{let a=r?r.toolCalls.get(t.id):Ge(`tc-${t.id}`);if(a){let l=t.status==="completed"?"\u2713":t.status==="failed"?"\u2717":"\u2026";a.className=`tool-call ${t.status}`;let d=a.querySelector(".icon");if(d&&(d.textContent=l),t.content&&t.content.length&&!a.querySelector("details")&&!a.querySelector(".tc-summary")){let u=t.content.map(f=>f.type==="terminal"?`[terminal ${f.terminalId}]`:f.content?.text?f.content.text:Array.isArray(f.content)?f.content.map(g=>g.text||"").join(""):"").filter(Boolean).join(`
9
+ `);if(u)if(a.dataset.kind==="task_complete"){let f=document.createElement("div");f.className="tc-summary",f.textContent=u,a.appendChild(f)}else{let f=document.createElement("details");f.innerHTML=`<summary>output</summary><div class="tc-content">${p(u)}</div>`,a.appendChild(f)}}}(t.status==="completed"||t.status==="failed")&&n.pendingToolCallIds.delete(t.id);break}case"plan":{let a=document.createElement("div");a.className="plan",a.innerHTML='<div class="plan-title">\u2015 plan</div>'+(t.entries||[]).map(l=>`<div class="plan-entry">${Ie[l.status]||"?"} ${p(l.content)}</div>`).join(""),E(a);break}case"permission_request":{let a=document.createElement("div");a.className="permission",a.dataset.requestId=t.requestId,a.dataset.title=t.title||"",a.innerHTML=`<span class="title" style="opacity:0.5">\u26BF ${p(t.title)}</span> `,!(r?r.resolvedPermissions.has(t.requestId):s&&s.slice(i+1).some(d=>d.type==="permission_response"&&JSON.parse(d.data).requestId===t.requestId))&&t.options&&(a.querySelector(".title").style.opacity="1",t.options.forEach(d=>{let u=document.createElement("button"),f=(d.kind||"").includes("allow");u.className=f?"allow":"deny",u.textContent=d.name,u.onclick=()=>{(d.kind||"").includes("reject")||(d.kind||"").includes("deny")?se(n.sessionId,t.requestId).catch(()=>{}):ne(n.sessionId,t.requestId,d.optionId).catch(()=>{}),a.innerHTML=`<span style="opacity:0.5">\u26BF ${p(t.title)} \u2014 ${p(d.name)}</span>`},a.appendChild(u)})),E(a),r&&r.permissions.set(t.requestId,a);break}case"permission_response":{let a=r?r.permissions.get(t.requestId):gt(`.permission[data-request-id="${t.requestId}"]`);if(a){let l=a.dataset.title?`\u26BF ${a.dataset.title}`:"\u26BF",d=t.optionName||(t.denied?"denied":"allowed");a.innerHTML=`<span style="opacity:0.5">${p(l)} \u2014 ${p(d)}</span>`}break}case"bash_command":{let a=Q(t.command,!1);r?r.currentBashEl=a:a.id="bash-replay-pending";break}case"bash_result":{let a=r?r.currentBashEl:Ge("bash-replay-pending");if(a){if(r||a.removeAttribute("id"),t.output){let l=a.querySelector(".bash-output");l&&(l.textContent=t.output,l.classList.add("has-content"))}Y(a,t.code,t.signal),r&&(r.currentBashEl=null)}break}case"prompt_done":n.pendingToolCallIds.clear(),n.pendingPermissionRequestIds.clear(),n.pendingPromptDone=!1,I(!1);break}}function Xe(){let e=n.replayQueue;n.replayQueue=[];for(let t of e)St(t)||C(t)}function St(e){switch(e.type){case"tool_call":return!!document.getElementById(`tc-${e.id}`);case"permission_request":return!!document.querySelector(`.permission[data-request-id="${e.requestId}"]`);case"thought_chunk":return!!n.currentThinkingEl;case"message_chunk":return!!n.currentAssistantEl;default:return!1}}function C(e){if(n.replayInProgress){console.log("[handleEvent-DEBUG] QUEUED (replayInProgress):",e.type),n.replayQueue.push(e);return}if(!(e.sessionId&&e.type!=="session_created"&&e.type!=="session_deleted"&&(!n.sessionId||e.sessionId!==n.sessionId)))switch(e.type){case"connected":e.cancelTimeout!=null&&(n.cancelTimeout=e.cancelTimeout);break;case"session_created":if(!n.awaitingNewSession&&n.sessionId&&e.sessionId!==n.sessionId)break;if(n.awaitingNewSession=!1,n.sessionId=e.sessionId,n.sessionCwd=e.cwd||n.sessionCwd,n.sessionTitle=e.title||null,e.configOptions?.length&&me(e.configOptions),Fe(n.sessionId),n.clientId&&V(n.clientId,!document.hidden,n.sessionId).catch(()=>{}),oe(n.sessionId,n.sessionTitle),B("connected","connected"),o.input.disabled=!1,o.sendBtn.disabled=!1,o.input.placeholder="Message or ?",I(!!e.busyKind),n.newTurnStarted=!1,e.busyKind==="bash"){let t=document.getElementById("bash-replay-pending");t&&(t.removeAttribute("id"),t.querySelector(".bash-cmd")?.classList.add("running"),n.currentBashEl=t)}else n.currentBashEl=null;o.messages.children.length===0&&c(`Session created: ${n.sessionTitle||e.sessionId.slice(0,8)+"\u2026"}`),O();break;case"user_message":{if(n.sentMessageForSession===e.sessionId){n.sentMessageForSession=null;break}if(k(),P(),n.newTurnStarted=!0,n.turnEnded=!1,e.sessionId===n.sessionId){let t=R("user",e.text);if(e.images)for(let s of e.images){let i=document.createElement("img");i.className="user-image",i.src=s.path,t.appendChild(i)}}break}case"message_chunk":if(n.turnEnded)break;D(),k(),n.currentAssistantEl||(n.currentAssistantEl=R("assistant",""),n.currentAssistantText=""),n.currentAssistantText+=e.text,n.currentAssistantEl.innerHTML=re(n.currentAssistantText),S();break;case"thought_chunk":if(n.turnEnded)break;D(),n.currentThinkingEl||(n.currentThinkingEl=document.createElement("details"),n.currentThinkingEl.className="thinking",n.currentThinkingEl.innerHTML='<summary class="active">\u283F thinking...</summary><div class="thinking-content"></div>',n.currentThinkingText="",E(n.currentThinkingEl)),n.currentThinkingText+=e.text,n.currentThinkingEl.querySelector(".thinking-content").textContent=n.currentThinkingText,S();break;case"tool_call":{if(n.turnEnded)break;n.pendingToolCallIds.add(e.id),I(!0),D(),k(),P();let t=we[e.kind]||ve,s=document.createElement("div");s.className="tool-call",s.id=`tc-${e.id}`,s.dataset.kind=e.kind;let i=`<span class="icon">${t}</span> ${p(e.title)}`,r=e.rawInput;r&&r.command?i+=`<span class="tc-detail">$ ${p(r.command)}</span>`:r&&r.path&&(i+=`<span class="tc-detail">${p(r.path)}</span>`),s.innerHTML=i;let a=e.kind==="edit"?ye(r):null;if(a){let d=document.createElement("details");d.innerHTML=`<summary>diff</summary><div class="diff-view">${a}</div>`,s.appendChild(d)}let l=s.querySelector(".tc-detail");l&&s.addEventListener("click",d=>{d.target.closest("details")||l.classList.toggle("expanded")}),E(s);break}case"tool_call_update":{let t=document.getElementById(`tc-${e.id}`);if((e.status==="completed"||e.status==="failed")&&n.pendingToolCallIds.delete(e.id),t){let s=e.status==="completed"?"\u2713":e.status==="failed"?"\u2717":"\u2026";t.className=`tool-call ${e.status}`;let i=t.querySelector(".icon");if(i&&(i.textContent=s),e.content&&e.content.length&&!t.querySelector("details")&&!t.querySelector(".tc-summary")){let r=e.content.map(a=>a.type==="terminal"?`[terminal ${a.terminalId}]`:a.content?.text?a.content.text:Array.isArray(a.content)?a.content.map(l=>l.text||"").join(""):"").filter(Boolean).join(`
10
+ `);if(r)if(t.dataset.kind==="task_complete"){let a=document.createElement("div");a.className="tc-summary",a.textContent=r,t.appendChild(a)}else{let a=document.createElement("details");a.innerHTML=`<summary>output</summary><div class="tc-content">${p(r)}</div>`,t.appendChild(a)}}}ae(),S();break}case"plan":{k(),P();let t=document.createElement("div");t.className="plan",t.innerHTML='<div class="plan-title">\u2015 plan</div>'+e.entries.map(s=>`<div class="plan-entry">${Ie[s.status]||"?"} ${p(s.content)}</div>`).join(""),E(t);break}case"permission_request":{if(n.turnEnded||document.querySelector(`.permission[data-request-id="${e.requestId}"]`))break;n.pendingPermissionRequestIds.add(e.requestId),I(!0),k();let t=document.createElement("div");t.className="permission",t.dataset.requestId=e.requestId,t.dataset.title=e.title||"",t.innerHTML=`<span class="title">\u26BF ${p(e.title)}</span> `,e.options.forEach(s=>{let i=document.createElement("button"),r=(s.kind||"").includes("allow");i.className=r?"allow":"deny",i.textContent=s.name,i.onclick=()=>{let a=(s.kind||"").includes("reject")||(s.kind||"").includes("deny");a?se(n.sessionId,e.requestId).catch(()=>{}):ne(n.sessionId,e.requestId,s.optionId).catch(()=>{}),n.pendingPermissionRequestIds.delete(e.requestId),n.unconfirmedPermissions.set(e.requestId,{sessionId:n.sessionId,optionId:s.optionId,optionName:s.name,denied:a}),t.innerHTML=`<span style="opacity:0.5">\u26BF ${p(e.title)} \u2014 ${p(s.name)}</span>`,ae()},t.appendChild(i)}),E(t);break}case"permission_resolved":{n.pendingPermissionRequestIds.delete(e.requestId),n.unconfirmedPermissions.delete(e.requestId);let t=document.querySelector(`.permission[data-request-id="${e.requestId}"]`);if(e.sessionId===n.sessionId&&t){let s=t.dataset.title?`\u26BF ${t.dataset.title}`:"\u26BF",i=e.optionName||(e.denied?"denied":"allowed");t.innerHTML=`<span style="opacity:0.5">${p(s)} \u2014 ${p(i)}</span>`}ae();break}case"bash_command":{if(n.sentBashForSession===e.sessionId){n.sentBashForSession=null;break}e.sessionId===n.sessionId&&(Q(e.command,!0),I(!0));break}case"bash_output":{if(e.sessionId!==n.sessionId)break;if(n.currentBashEl){let t=n.currentBashEl.querySelector(".bash-output");if(e.stream==="stderr"){let s=document.createElement("span");s.className="stderr",s.textContent=e.text,t.appendChild(s)}else t.appendChild(document.createTextNode(e.text));t.classList.add("has-content"),t.scrollTop=t.scrollHeight,S()}break}case"bash_done":{if(e.sessionId!==n.sessionId)break;Y(n.currentBashEl,e.code,e.signal),e.error&&c(`err: ${e.error}`),I(!1);break}case"prompt_done":{if(J(),e.stopReason==="cancelled"&&n.newTurnStarted){n.newTurnStarted=!1,k(),P();break}n.newTurnStarted=!1,e.stopReason==="cancelled"?yt():wt(),n.turnEnded=!0,n.pendingPromptDone=!0,ae();break}case"session_deleted":e.sessionId===n.sessionId&&(c("warn: This session has been deleted."),o.input.disabled=!0,o.sendBtn.disabled=!0,o.input.placeholder="Session deleted");break;case"session_expired":y(),c("warn: Previous session expired, created new one."),M();break;case"config_set":{Oe(e.configId,e.value);let t=x(e.configId),s=t?.name||e.configId,i=t?.options.find(r=>r.value===e.value)?.name||e.value;c(`ok: ${s}: ${i}`),e.configId==="mode"&&_(),O();break}case"config_option_update":e.configOptions?.length&&me(e.configOptions);break;case"session_title_updated":e.sessionId===n.sessionId&&(n.sessionTitle=e.title,oe(n.sessionId,n.sessionTitle));break;case"error":n.awaitingNewSession=!1,n.pendingToolCallIds.clear(),n.pendingPermissionRequestIds.clear(),n.pendingPromptDone=!1,D(),k(),P(),c(`err: ${e.message}`),I(!1);break}}async function bt(){try{let e=await navigator.serviceWorker?.ready;if(!e)return;let t=await fetch("/api/beta/push/vapid-key");if(!t.ok)return;let{publicKey:s}=await t.json(),r=(await e.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:Et(s)})).toJSON();await fetch("/api/beta/push/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({endpoint:r.endpoint,keys:r.keys,clientId:n.clientId})})}catch(e){console.error("[push] subscribe failed:",e)}}async function Tt(){try{let e=await navigator.serviceWorker?.ready;if(!e)return;let t=await e.pushManager.getSubscription();if(!t)return;let s=t.endpoint;await t.unsubscribe(),await fetch("/api/beta/push/unsubscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({endpoint:s})})}catch(e){console.error("[push] unsubscribe failed:",e)}}function Et(e){let t="=".repeat((4-e.length%4)%4),s=(e+t).replace(/-/g,"+").replace(/_/g,"/"),i=atob(s),r=new Uint8Array(i.length);for(let a=0;a<i.length;a++)r[a]=i.charCodeAt(a);return r}async function Ce(){try{let e=await navigator.serviceWorker?.ready;return e?await e.pushManager.getSubscription()!==null:!1}catch{return!1}}async function Me(e){let t=e.split(/\s+/),s=t[0].toLowerCase(),i=t.slice(1).join(" ").trim();switch(s){case"/new":return y(),c("Creating new session\u2026"),M({cwd:i||n.sessionCwd}),!0;case"/pwd":return c(`\u{1F4C1} ${n.sessionCwd||"unknown"}`),!0;case"/rename":{if(!n.sessionId)return c("err: No active session"),!0;if(!i)return c(`Current: ${n.sessionTitle||"(untitled)"}`),c("Usage: /rename <new title>"),!0;try{await qe(n.sessionId,i),c(`Renamed \u2192 ${i}`)}catch{c("err: Failed to rename session")}return!0}case"/sessions":return c("Removed. Use /switch to see all sessions."),!0;case"/exit":{if(!n.sessionId)return c("warn: No active session"),!0;let r=n.sessionId;try{let l=(await te()).find(d=>d.id!==r);if(n.busy&&G(),pe(r).catch(()=>{}),L=null,l){n.sessionSwitchGen++;let d=n.sessionSwitchGen;y(),n.sessionId=null;let[u]=await Promise.all([$(l.id),N(l.id)]);if(d!==n.sessionSwitchGen)return!0;C({type:"session_created",sessionId:u.id,cwd:u.cwd,title:u.title,configOptions:u.configOptions,busyKind:u.busyKind}),S(!0)}else y(),n.sessionId=null,c("Creating new session\u2026"),M({inheritFromSessionId:null})}catch{c("err: Failed to exit session")}return!0}case"/prune":{try{let l=(await(await fetch("/api/v1/sessions")).json()).filter(d=>d.id!==n.sessionId);if(l.length===0)return c("No other sessions to prune."),!0;for(let d of l)pe(d.id).catch(()=>{});L=null,c(`Pruned ${l.length} session(s).`)}catch{c("err: Failed to prune sessions")}return!0}case"/switch":{if(!i)return c("Usage: /switch <title or id prefix>"),!0;try{let a=await(await fetch("/api/v1/sessions")).json(),l=i.toLowerCase(),d=a.find(g=>g.id.startsWith(i)||g.title&&g.title.toLowerCase().includes(l));if(!d)return c(`err: No session matching "${i}"`),!0;n.sessionSwitchGen++;let u=n.sessionSwitchGen;y(),n.sessionId=null;let[f]=await Promise.all([$(d.id),N(d.id)]);if(u!==n.sessionSwitchGen)return!0;C({type:"session_created",sessionId:f.id,cwd:f.cwd,title:f.title,configOptions:f.configOptions,busyKind:f.busyKind}),S(!0)}catch{y(),n.sessionId=null,c("err: Failed to switch session")}return!0}case"/cancel":return n.busy?(G(),c("^C")):c("Nothing to cancel."),!0;case"/help":case"?":{let r=[];n.serverVersion&&r.push(`WebAgent ${n.serverVersion}`),n.agentName&&n.agentVersion&&r.push(`${n.agentName} ${n.agentVersion}`),r.length&&c(r.join(" \xB7 ")),c("? \u2014 Show help"),c("/help \u2014 Show help (alias)"),c("!<command> \u2014 Run bash command");for(let a of et){let l=a.args?`${a.cmd} ${a.args}`:a.cmd;c(`${l} \u2014 ${a.desc}`)}c("--- Shortcuts ---");for(let a of kt)c(`${a.key} \u2014 ${a.desc}`);c("--- Tips ---");for(let a of Ct)c(a.text);return!0}case"/model":case"/mode":case"/think":{let a={"/model":"model","/mode":"mode","/think":"reasoning_effort"}[s],l=x(a);if(!i){let v=l?.options.find(ee=>ee.value===l.currentValue)?.name||l?.currentValue||"unknown";return c(`${l?.name||a}: ${v}`),c(`Type ${s} + space to pick from list`),!0}if(!l)return c(`err: ${s.slice(1)} is not available.`),!0;let d=i.trim(),u=v=>v.toLowerCase().replace(/[\s_]+/g,"-"),f=u(d),g=l.options.find(v=>u(v.value)===f||u(v.name)===f);if(!g){let v=l.options.filter(ee=>u(ee.value).includes(f)||u(ee.name).includes(f));if(v.length===1)g=v[0];else if(v.length>1)return c(`err: Ambiguous "${i}". Type ${s} + space to see options.`),!0}return g?(l.currentValue=g.value,_(),O(),c(`${l.name} \u2192 ${g.name}`),await K(n.sessionId,a,g.value).catch(()=>{}),!0):(c(`err: Unknown "${i}". Type ${s} + space to see options.`),!0)}case"/notify":{if(typeof Notification>"u")return c("err: notifications not supported in this browser"),!0;let r=i.toLowerCase();if(r==="on"){if(Notification.permission==="denied")return c("notify: blocked \u2014 allow in browser site settings to enable"),!0;if(Notification.permission!=="granted"&&await Notification.requestPermission()!=="granted")return c("notify: blocked \u2014 allow in browser site settings to enable"),!0;let l=await Ce();return await bt(),c(l?"notify: already enabled":"notify: enabled"),!0}if(r==="off")return await Tt(),c("notify: disabled"),!0;let a=Notification.permission;return a==="denied"?c("notify: blocked \u2014 allow in browser site settings to enable"):a==="granted"&&await Ce()?c("notify: enabled"):c("notify: off \u2014 use /notify on to enable"),!0}default:return!1}}var et=[{cmd:"/cancel",args:"",desc:"Cancel current response"},{cmd:"/exit",args:"",desc:"Close current session"},{cmd:"/mode",args:"[name]",desc:"Pick or switch mode"},{cmd:"/model",args:"[name]",desc:"Pick or switch model"},{cmd:"/new",args:"[cwd]",desc:"New session"},{cmd:"/notify",args:"[on|off]",desc:"Toggle background notifications"},{cmd:"/prune",args:"",desc:"Delete all sessions except current"},{cmd:"/pwd",args:"",desc:"Show working directory"},{cmd:"/rename",args:"<new title>",desc:"Rename current session"},{cmd:"/switch",args:"<title|id>",desc:"Switch to session"},{cmd:"/think",args:"[level]",desc:"Pick or switch reasoning effort"}],kt=[{key:"Enter",desc:"Send message"},{key:"Shift+Enter",desc:"New line"},{key:"^C",desc:"Cancel current response"},{key:"^M",desc:"Cycle mode (Agent \u2192 Plan \u2192 Autopilot)"},{key:"^U",desc:"Upload image"}],Ct=[{text:"Tap \u276F prompt to cycle mode"}],b=-1,m=[],w="commands",Z=null,L=null,W=null,xe=!1;function Le(){let e=o.input.value;if(W!==null){if(e===W)return;W=null}let t=e.match(/^\/new /);if(t){let l=e.slice(t[0].length).toLowerCase();Mt(l);return}let s=e.match(/^\/switch /);if(s){let l=e.slice(s[0].length).toLowerCase();xt(l,"switch");return}let i=e.match(/^\/(model|mode|think) /);if(i){let d={model:"model",mode:"mode",think:"reasoning_effort"}[i[1]],u=e.slice(i[0].length).toLowerCase();Lt(d,u);return}let r=e.match(/^\/notify /);if(r){let l=e.slice(r[0].length).toLowerCase();Pt(l);return}if(!e.startsWith("/")||e.includes(" ")){h();return}w="commands";let a=e.toLowerCase();if(m=et.filter(l=>l.cmd.startsWith(a)),m.length===0){h();return}b=0,F(),o.slashMenu.classList.add("active")}async function xt(e,t="switch"){if(!L)try{L=await(await fetch("/api/v1/sessions")).json(),setTimeout(()=>{L=null},5e3)}catch{return}if(w=t,m=L.filter(i=>e?i.title&&i.title.toLowerCase().includes(e)||i.id.startsWith(e):!0),m.length===0){h();return}b=0,F(),o.slashMenu.classList.add("active")}async function Mt(e){if(!L)try{L=await(await fetch("/api/v1/sessions")).json(),setTimeout(()=>{L=null},5e3)}catch{return}w="new";let t=new Map;for(let i of L){let r=t.get(i.cwd);(!r||(i.last_active_at||i.created_at)>r.time)&&t.set(i.cwd,{cwd:i.cwd,time:i.last_active_at||i.created_at})}let s=[...t.values()].sort((i,r)=>r.time.localeCompare(i.time));if(e&&(s=s.filter(i=>i.cwd.toLowerCase().includes(e))),m=s,m.length===0){h();return}b=0,F(),o.slashMenu.classList.add("active")}function Lt(e,t){let s=x(e);if(!s){h();return}if(w="config",Z=e,m=s.options.filter(i=>t?i.value.toLowerCase().includes(t)||i.name.toLowerCase().includes(t):!0),m.length===0){h();return}b=0,F(),o.slashMenu.classList.add("active")}var $t=[{value:"on",name:"on",desc:"Enable background notifications"},{value:"off",name:"off",desc:"Disable background notifications"}];async function Pt(e){if(w="notify",m=$t.filter(i=>e?i.value.includes(e)||i.name.includes(e):!0),m.length===0){h();return}xe=await Ce();let t=xe?"on":"off",s=m.findIndex(i=>i.value===t);b=s>=0?s:0,F(),o.slashMenu.classList.add("active")}function F(){if(w==="new"){let t=(n.sessionCwd||"").toLowerCase();o.slashMenu.innerHTML=m.map((s,i)=>{let r=s.cwd.toLowerCase()===t;return`<div class="slash-item${i===b?" selected":""}" data-idx="${i}"><span class="slash-cmd"${r?' style="color:var(--green)"':""}>${p((r?"* ":" ")+s.cwd)}</span></div>`}).join("")}else if(w==="config"){let t=ie(Z)?.toLowerCase()||"";o.slashMenu.innerHTML=m.map((s,i)=>{let r=s.value.toLowerCase()===t;return`<div class="slash-item${i===b?" selected":""}" data-idx="${i}"><span class="slash-cmd"${r?' style="color:var(--green)"':""}>${p((r?"* ":" ")+s.name)}</span></div>`}).join("")}else if(w==="notify"){let t=xe?"on":"off";o.slashMenu.innerHTML=m.map((s,i)=>{let r=s.value===t;return`<div class="slash-item${i===b?" selected":""}" data-idx="${i}"><span class="slash-cmd"${r?' style="color:var(--green)"':""}>${p((r?"* ":" ")+s.name)}</span><span class="slash-desc">${p(s.desc)}</span></div>`}).join("")}else w==="switch"?o.slashMenu.innerHTML=m.map((t,s)=>{let i=t.id===n.sessionId,r=i?"* ":" ",a=t.title||t.id.slice(0,8)+"\u2026",l=We(t.last_active_at||t.created_at);return`<div class="slash-item${s===b?" selected":""}" data-idx="${s}"><span class="slash-cmd"${i?' style="color:var(--green)"':""}>${p(r+a)}</span><span class="slash-desc">${p(t.cwd)} (${p(l)})</span></div>`}).join(""):o.slashMenu.innerHTML=m.map((t,s)=>{let i=t.args?`${t.cmd} ${t.args}`:t.cmd;return`<div class="slash-item${s===b?" selected":""}" data-idx="${s}"><span class="slash-cmd">${p(i)}</span><span class="slash-desc">${p(t.desc)}</span></div>`}).join("");let e=o.slashMenu.querySelector(".selected");e&&e.scrollIntoView({block:"nearest"})}function h(){o.slashMenu.classList.remove("active"),b=-1,m=[],w="commands",W=o.input.value}function Ht(e){if(!(e<0||e>=m.length)){if(w==="commands"){let t=m[e];o.input.value=t.cmd+(t.args?" ":""),h(),o.input.focus(),["/new","/switch","/model","/mode","/think","/notify"].includes(t.cmd)&&(W=null,Le())}else if(w==="config"){let t=m[e],s={model:"/model",mode:"/mode",reasoning_effort:"/think"}[Z]||`/${Z}`;o.input.value=`${s} ${t.name}`,h(),o.input.focus()}else if(w==="notify"){let t=m[e];o.input.value=`/notify ${t.value}`,h(),o.input.focus()}else if(w==="new"){let t=m[e];o.input.value=`/new ${t.cwd}`,h(),o.input.focus()}else if(w==="switch"){let t=m[e];o.input.value=`/switch ${t.title||t.id}`,h(),o.input.focus()}}}async function _t(e){if(!(e<0||e>=m.length))if(w==="new"){let t=m[e];o.input.value="",h(),y(),c("Creating new session\u2026"),M({cwd:t.cwd})}else if(w==="config"){let t=m[e],s=Z,i=x(s);o.input.value="",h(),i&&(i.currentValue=t.value),_(),O(),c(`${i?.name||s} \u2192 ${t.name}`),await K(n.sessionId,s,t.value).catch(()=>{})}else if(w==="switch"){let t=m[e];o.input.value="",h(),y(),n.sessionId=null,c("Switching\u2026"),Promise.all([$(t.id),N(t.id)]).then(([s,i])=>{C({type:"session_created",sessionId:s.id,cwd:s.cwd,title:s.title,configOptions:s.configOptions,busyKind:s.busyKind}),i&&S(!0)}).catch(()=>{y(),n.sessionId=null,c("err: Failed to switch session")})}else if(w==="notify"){let t=m[e];o.input.value=`/notify ${t.value}`,h(),Me(o.input.value),o.input.value=""}else{let t=m[e];o.input.value=t.cmd+(t.args?" ":""),h(),o.input.focus(),["/new","/switch","/model","/mode","/think","/notify"].includes(t.cmd)&&(W=null,Le())}}function tt(e){return o.slashMenu.classList.contains("active")?e.key==="ArrowDown"?(b=(b+1)%m.length,F(),!0):e.key==="ArrowUp"?(b=(b-1+m.length)%m.length,F(),!0):e.key==="Tab"?(Ht(b),!0):!1:!1}o.slashMenu.addEventListener("mousedown",e=>{e.preventDefault();let t=e.target.closest(".slash-item");t&&_t(Number(t.dataset.idx))});o.input.addEventListener("input",()=>{Le(),o.inputArea.classList.toggle("bash-mode",o.input.value.startsWith("!"))});function nt(e){return new Promise(t=>{let s=new FileReader;s.onload=()=>{let i=s.result.split(",")[1];t({data:i,mimeType:e.type,previewUrl:s.result})},s.readAsDataURL(e)})}function st(e){n.pendingImages.push(e),ce(),o.input.focus()}function ce(){if(o.attachPreview.innerHTML="",n.pendingImages.length===0){o.attachPreview.classList.remove("active");return}o.attachPreview.classList.add("active"),n.pendingImages.forEach((e,t)=>{let s=document.createElement("span");s.className="attach-thumb",s.innerHTML=`<img src="${e.previewUrl}"><button class="remove">\xD7</button>`,s.querySelector(".remove").addEventListener("click",()=>{n.pendingImages.splice(t,1),ce()}),o.attachPreview.appendChild(s)})}o.attachBtn.onclick=()=>o.fileInput.click();o.fileInput.onchange=async()=>{for(let e of o.fileInput.files)e.type.startsWith("image/")&&st(await nt(e));o.fileInput.value=""};o.input.addEventListener("paste",async e=>{for(let t of e.clipboardData.items)t.type.startsWith("image/")&&(e.preventDefault(),st(await nt(t.getAsFile())))});var H=document.createElement("div");H.id="lightbox-overlay";var X=document.createElement("img");H.appendChild(X);document.body.appendChild(H);var de=1,Nt=.5,qt=5;function Bt(){return H.classList.contains("active")}function Ot(e){X.src=e,de=1,X.style.transform="",H.classList.add("active")}function it(){H.classList.remove("active"),X.src=""}document.getElementById("messages").addEventListener("click",e=>{let t=e.target;t.tagName==="IMG"&&t.classList.contains("user-image")&&Ot(t.src)});H.addEventListener("click",e=>{e.target===H&&it()});document.addEventListener("keydown",e=>{e.key==="Escape"&&Bt()&&it()});H.addEventListener("wheel",e=>{e.preventDefault();let t=e.deltaY>0?-.15:.15;de=Math.min(qt,Math.max(Nt,de+t)),X.style.transform=`scale(${de})`},{passive:!1});function $e(){return n.clientId!==null}n._onCancelTimeout=()=>c("warn: Agent not responding to cancel");function ot(){let e=o.input.value.trim();if(!e&&n.pendingImages.length===0)return;if((e.startsWith("/")||e==="?"||e.startsWith("? "))&&n.pendingImages.length===0){o.input.value="",o.input.style.height="auto",lt(),Me(e);return}if(e.startsWith("!")&&n.pendingImages.length===0){let i=e.slice(1).trim();if(!i)return;if(!n.sessionId){c("warn: Session not ready yet, please wait\u2026");return}if(!$e()){c("warn: Not connected, please retry");return}o.input.value="",o.input.style.height="auto",o.inputArea.classList.remove("bash-mode"),Q(i,!0),n.sentBashForSession=n.sessionId,Be(n.sessionId,i).catch(()=>{}),I(!0);return}if(n.busy)return;if(o.input.value="",o.input.style.height="auto",o.inputArea.classList.remove("bash-mode"),!n.sessionId){c("warn: Session not ready yet, please wait\u2026");return}if(!$e()){c("warn: Not connected, please retry");return}let t=R("user",e||"(image)");for(let i of n.pendingImages){let r=document.createElement("img");r.className="user-image",r.src=i.previewUrl,t.appendChild(r)}let s=n.pendingImages.slice();n.pendingImages.length=0,ce(),s.length>0?Promise.all(s.map(i=>fetch(`/api/v1/sessions/${n.sessionId}/images`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:i.data,mimeType:i.mimeType})}).then(r=>r.json()).then(r=>({data:i.data,mimeType:i.mimeType,path:r.url})))).then(i=>{if(!$e()){t.remove(),c("warn: Not connected, please retry"),I(!1);return}fe(n.sessionId,e||"What is in this image?",i.map(r=>({data:r.data,mimeType:r.mimeType,path:r.path}))).catch(()=>{})}):fe(n.sessionId,e).catch(()=>{}),n.turnEnded=!1,n.sentMessageForSession=n.sessionId,I(!0),je()}function rt(){G()&&c("^C")}function at(){let e=o.input.value.trim();return e.startsWith("/")||e.startsWith("!")||e==="?"||e.startsWith("? ")}function lt(){n.busy&&(at()?(o.sendBtn.textContent="\u21B5",o.sendBtn.title="Send (Enter)",o.sendBtn.classList.remove("cancel")):(o.sendBtn.textContent="^C",o.sendBtn.title="Cancel (Ctrl+C)",o.sendBtn.classList.add("cancel")))}o.sendBtn.onclick=()=>{n.busy&&!at()?rt():ot()};o.input.addEventListener("keydown",e=>{if(tt(e)){e.preventDefault();return}if(e.key==="Enter"&&!e.shiftKey){e.preventDefault(),h(),ot();return}if(e.key==="u"&&(e.ctrlKey||e.metaKey)&&!e.shiftKey){e.preventDefault(),o.fileInput.click();return}});document.addEventListener("keydown",e=>{if(e.key==="c"&&(e.ctrlKey||e.metaKey)&&!e.shiftKey&&n.busy&&!(window.getSelection()?.toString()||o.input.selectionStart!==o.input.selectionEnd)){e.preventDefault(),rt();return}e.key==="Escape"&&o.slashMenu.classList.contains("active")&&(e.preventDefault(),h(),o.input.focus())});function ct(){let e=x("mode");if(!e||!e.options.length)return;let t=e.options.findIndex(i=>i.value===e.currentValue),s=e.options[(t+1)%e.options.length];e.currentValue=s.value,K(n.sessionId,"mode",s.value).catch(()=>{}),c(`Mode \u2192 ${s.name}`),_()}document.addEventListener("keydown",e=>{e.key==="m"&&(e.ctrlKey||e.metaKey)&&!e.shiftKey&&(e.preventDefault(),ct())});o.prompt.addEventListener("click",ct);o.input.addEventListener("input",lt);o.input.addEventListener("input",()=>{o.input.style.height="auto",o.input.style.height=Math.min(o.input.scrollHeight,200)+"px"});async function At(e){try{let t=await navigator.serviceWorker?.ready;if(!t)return;let s=await t.pushManager.getSubscription();if(!s)return;await fetch("/api/beta/push/register-client",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({clientId:e,endpoint:s.endpoint})})}catch{}}function He(){B("connecting","connecting");let e=new EventSource("/api/v1/events/stream");n.eventSource=e,e.onmessage=async t=>{let s=JSON.parse(t.data);if(s.type==="connected"){n.clientId=s.clientId,V(s.clientId,!document.hidden,n.sessionId??void 0).catch(()=>{}),At(s.clientId);return}C(s)},e.onerror=()=>{e.close(),Dt(),setTimeout(He,3e3)},Rt()}async function Rt(){B("connecting","session loading");let e=n.sessionSwitchGen,t=De();if(t&&t===n.sessionId&&n.lastEventSeq>0){if(await Pe(t,!0,e),e!==n.sessionSwitchGen)return;Ze(),S(!1);return}if(t){if(y(),await Pe(t,!1,e),e!==n.sessionSwitchGen)return;S(!0);return}try{let s=await te();if(e!==n.sessionSwitchGen)return;if(s.length>0){if(y(),await Pe(s[0].id,!1,e),e!==n.sessionSwitchGen)return;S(!0);return}}catch{}e===n.sessionSwitchGen&&M()}async function Pe(e,t,s){if(t){try{let i=await $(e);if(s!==n.sessionSwitchGen)return;C({type:"session_created",sessionId:i.id,cwd:i.cwd,title:i.title,configOptions:i.configOptions,busyKind:i.busyKind})}catch{if(s!==n.sessionSwitchGen)return;y(),c("warn: Previous session expired, created new one."),M();return}if(s!==n.sessionSwitchGen)return;await Te(e)}else{n.sessionId=null;let i=N(e),r;try{let[a,l]=await Promise.all([$(e),i]);if(s!==n.sessionSwitchGen)return;r=a,l||c("warn: Failed to load history.")}catch{if(s!==n.sessionSwitchGen)return;y(),c("warn: Previous session expired, created new one."),M();return}C({type:"session_created",sessionId:r.id,cwd:r.cwd,title:r.title,configOptions:r.configOptions,busyKind:r.busyKind})}}function Dt(){B("disconnected","disconnected"),n.eventSource=null,n.clientId=null,k(),P(),n.currentBashEl&&Y(n.currentBashEl,null,"disconnected"),n.pendingToolCallIds.clear(),n.pendingPermissionRequestIds.clear(),n.pendingPromptDone=!1,n.turnEnded=!1,J(),I(!1)}document.addEventListener("visibilitychange",()=>{n.clientId&&V(n.clientId,!document.hidden,n.sessionId??void 0).catch(()=>{}),!document.hidden&&n.sessionId&&n.lastEventSeq>0&&!n.replayInProgress&&Te(n.sessionId).then(()=>S(!1))});He();fetch("/api/v1/version").then(e=>e.json()).then(e=>{typeof e.server=="string"&&(n.serverVersion=e.server);let t=e.agent;t&&(n.agentName=t.name??null,n.agentVersion=t.version??null)}).catch(()=>{});"serviceWorker"in navigator&&(navigator.serviceWorker.register("/sw.js"),navigator.serviceWorker.addEventListener("message",e=>{if(e.data?.type==="navigate"&&e.data.sessionId){let t=e.data.sessionId;if(n.sessionId===t)return;n.sessionSwitchGen++;let s=n.sessionSwitchGen;history.replaceState(null,"",`#${t}`),y(),n.sessionId=null,c("Switching\u2026"),Promise.all([$(t),N(t)]).then(([i,r])=>{s===n.sessionSwitchGen&&(C({type:"session_created",sessionId:i.id,cwd:i.cwd,title:i.title,configOptions:i.configOptions,busyKind:i.busyKind}),r&&S(!0))}).catch(()=>{y(),n.sessionId=null,c("err: Failed to switch session")})}}));
@@ -262,6 +262,11 @@ body {
262
262
  color: var(--text);
263
263
  cursor: default;
264
264
  }
265
+ .tool-call .tc-summary {
266
+ margin: 4px 0 4px 16px;
267
+ font-size: 14px;
268
+ color: var(--text);
269
+ }
265
270
 
266
271
  /* Diff view for edit tool calls */
267
272
  .diff-view {
@@ -471,10 +476,6 @@ body {
471
476
  color: var(--text);
472
477
  border-color: var(--text-dim);
473
478
  }
474
- #new-btn {
475
- font-size: 11px;
476
- letter-spacing: 0.2px;
477
- }
478
479
  #send-btn.cancel {
479
480
  color: var(--red);
480
481
  border-color: var(--red);
@@ -530,7 +531,6 @@ body {
530
531
  .slash-item:hover .slash-desc, .slash-item.selected .slash-desc { color: rgba(255,255,255,0.8); }
531
532
 
532
533
  /* Image attach */
533
- #new-btn.hidden { display: none; }
534
534
  #attach-preview {
535
535
  display: none;
536
536
  padding: 4px 16px;
@@ -571,6 +571,31 @@ body {
571
571
  border: 1px solid var(--border);
572
572
  display: block;
573
573
  margin-top: 4px;
574
+ cursor: zoom-in;
575
+ }
576
+
577
+ /* Image lightbox overlay */
578
+ #lightbox-overlay {
579
+ position: fixed;
580
+ inset: 0;
581
+ z-index: 9999;
582
+ background: rgba(0, 0, 0, 0.85);
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ cursor: zoom-out;
587
+ visibility: hidden;
588
+ pointer-events: none;
589
+ }
590
+ #lightbox-overlay.active {
591
+ visibility: visible;
592
+ pointer-events: auto;
593
+ }
594
+ #lightbox-overlay img {
595
+ max-width: 90vw;
596
+ max-height: 90vh;
597
+ border-radius: 6px;
598
+ cursor: default;
574
599
  }
575
600
 
576
601
  /* History pagination sentinel */