@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 +45 -268
- package/dist/index.html +2 -3
- package/dist/js/app.C4WRSLDF.js +10 -0
- package/dist/{styles.01a9ju9l.css → styles.01a6wdjv.css} +30 -5
- package/lib/bridge.js +284 -0
- package/lib/config.js +62 -0
- package/lib/daemon.js +278 -0
- package/lib/event-handler.js +106 -0
- package/lib/push-service.js +166 -0
- package/lib/routes.js +945 -0
- package/lib/server.js +82 -0
- package/lib/session-manager.js +277 -0
- package/lib/shared/constants.js +17 -0
- package/lib/sse-manager.js +80 -0
- package/lib/store.js +174 -0
- package/lib/title-service.js +74 -0
- package/lib/types.js +13 -0
- package/package.json +6 -4
- package/dist/js/app.IXP5KGP6.js +0 -8
package/README.md
CHANGED
|
@@ -3,304 +3,81 @@
|
|
|
3
3
|
[](https://github.com/LelouchHe/webagent/actions/workflows/ci.yml)
|
|
4
4
|
[](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="
|
|
15
|
-
<img src="docs/images/
|
|
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="
|
|
20
|
-
<img src="docs/images/
|
|
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
|
|
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/
|
|
33
|
-
<br
|
|
34
|
-
<sub><code>!<command></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
|
-
|
|
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
|
-
|
|
37
|
+
## Quick Start
|
|
53
38
|
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
69
|
+
Tech stack: Node.js + TypeScript (`--experimental-strip-types`), SQLite (`better-sqlite3`), Zod validation, esbuild bundling.
|
|
296
70
|
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 */
|