@lelouchhe/webagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/bin/webagent.mjs +23 -0
- package/config.toml +28 -0
- package/dist/icons/icon-180.png +0 -0
- package/dist/icons/icon-192.png +0 -0
- package/dist/icons/icon-512.png +0 -0
- package/dist/icons/icon.svg +4 -0
- package/dist/index.html +46 -0
- package/dist/js/app.mmjqzu9r.js +10 -0
- package/dist/js/commands.mmjqzu9r.js +454 -0
- package/dist/js/connection.mmjqzu9r.js +76 -0
- package/dist/js/events.mmjqzu9r.js +612 -0
- package/dist/js/images.mmjqzu9r.js +58 -0
- package/dist/js/input.mmjqzu9r.js +196 -0
- package/dist/js/render.mmjqzu9r.js +200 -0
- package/dist/js/state.mmjqzu9r.js +176 -0
- package/dist/manifest.json +26 -0
- package/dist/styles.mmjqzu9r.css +555 -0
- package/dist/sw.js +5 -0
- package/package.json +56 -0
- package/src/bridge.ts +317 -0
- package/src/config.ts +65 -0
- package/src/routes.ts +147 -0
- package/src/server.ts +159 -0
- package/src/session-manager.ts +223 -0
- package/src/store.ts +140 -0
- package/src/title-service.ts +81 -0
- package/src/types.ts +81 -0
- package/src/ws-handler.ts +264 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LelouchHe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# WebAgent
|
|
2
|
+
|
|
3
|
+
A terminal-style web UI for ACP-compatible agents.
|
|
4
|
+
|
|
5
|
+
Tech stack: Node.js + TypeScript (`--experimental-strip-types`), real-time WebSocket communication (`ws`), SQLite persistence (`better-sqlite3`), Zod validation.
|
|
6
|
+
|
|
7
|
+
## Screenshots
|
|
8
|
+
|
|
9
|
+
<table>
|
|
10
|
+
<tr>
|
|
11
|
+
<td width="50%">
|
|
12
|
+
<img src="docs/images/overview-chat.png" alt="Desktop chat overview" />
|
|
13
|
+
<br />
|
|
14
|
+
<sub>Streaming chat in the terminal-style desktop layout.</sub>
|
|
15
|
+
</td>
|
|
16
|
+
<td width="50%">
|
|
17
|
+
<img src="docs/images/plan-busy.png" alt="Plan mode while busy" />
|
|
18
|
+
<br />
|
|
19
|
+
<sub>Plan mode highlighted while a turn is still running.</sub>
|
|
20
|
+
</td>
|
|
21
|
+
</tr>
|
|
22
|
+
<tr>
|
|
23
|
+
<td width="50%">
|
|
24
|
+
<img src="docs/images/permission-dialog.png" alt="Permission request dialog" />
|
|
25
|
+
<br />
|
|
26
|
+
<sub>Permission prompts stay inline in the conversation flow.</sub>
|
|
27
|
+
</td>
|
|
28
|
+
<td width="50%">
|
|
29
|
+
<img src="docs/images/bash-output.png" alt="Inline bash command output" />
|
|
30
|
+
<br />
|
|
31
|
+
<sub><code>!<command></code> output streams directly into the session.</sub>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
</table>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="docs/images/mobile-autopilot.png" alt="Mobile autopilot mode" width="320" />
|
|
38
|
+
<br />
|
|
39
|
+
<sub>Compact mobile layout with mode highlighting and terminal-style action keys.</sub>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
## Prerequisites
|
|
43
|
+
|
|
44
|
+
- Node.js 22.6+ (requires `--experimental-strip-types`)
|
|
45
|
+
- An ACP-compatible agent (e.g. [Copilot CLI](https://github.com/github/copilot-cli)) installed and authenticated
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g @lelouchhe/webagent
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or run directly with npx:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx @lelouchhe/webagent
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Run
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
webagent # start with defaults (port 6800)
|
|
63
|
+
webagent --config /path/to/config.toml # start with custom config
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Data (SQLite database, uploaded images) is stored in `./data/` relative to your current working directory by default.
|
|
67
|
+
|
|
68
|
+
### From source
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/LelouchHe/webagent.git
|
|
72
|
+
cd webagent
|
|
73
|
+
npm install
|
|
74
|
+
npm run build # build static assets (public/ → dist/)
|
|
75
|
+
npm start # start on port 6800
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm run dev # port 6801, uses data-dev/, auto-restarts on file changes
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
How you keep the process running in the background is intentionally left to your own environment and preferred process manager.
|
|
85
|
+
|
|
86
|
+
### Configuration
|
|
87
|
+
|
|
88
|
+
Configuration is via TOML files, passed with `--config`:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
webagent --config config.toml
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
| Key | Default | Description |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `port` | `6800` | HTTP/WebSocket server port |
|
|
99
|
+
| `data_dir` | `data` | SQLite + uploads directory |
|
|
100
|
+
| `default_cwd` | `process.cwd()` | Working directory for new sessions |
|
|
101
|
+
| `public_dir` | `dist` | Static assets directory |
|
|
102
|
+
| `agent_cmd` | `copilot --acp` | ACP agent command (binary + args, space-separated) |
|
|
103
|
+
| `limits.bash_output` | `1048576` (1 MB) | Max bash output stored in DB per command |
|
|
104
|
+
| `limits.image_upload` | `10485760` (10 MB) | Max image upload size |
|
|
105
|
+
| `limits.cancel_timeout` | `10000` (10s) | Cancel timeout in ms; 0 disables |
|
|
106
|
+
|
|
107
|
+
To use a different ACP-compatible agent backend:
|
|
108
|
+
|
|
109
|
+
```toml
|
|
110
|
+
agent_cmd = "my-agent --acp"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Features
|
|
114
|
+
|
|
115
|
+
### Chat
|
|
116
|
+
|
|
117
|
+
- Real-time streaming responses with Markdown rendering + syntax highlighting
|
|
118
|
+
- Collapsible thinking process display
|
|
119
|
+
- Tool call display (status animation, expandable details, diff rendering)
|
|
120
|
+
- Agent execution plan display (pending ○ / in-progress ◉ / done ●)
|
|
121
|
+
- Permission confirmation dialog for sensitive operations (Allow / Deny), synced across devices; auto-approved in autopilot mode
|
|
122
|
+
- Smart scroll: force-scrolls on load/switch/send, soft auto-scroll during streaming
|
|
123
|
+
|
|
124
|
+
### Images
|
|
125
|
+
|
|
126
|
+
- Upload images (button or `^U` shortcut)
|
|
127
|
+
- Paste images (Ctrl+V / Cmd+V)
|
|
128
|
+
- Preview before sending + removable, supports multiple images
|
|
129
|
+
- Server-side storage, displayed inline in chat
|
|
130
|
+
|
|
131
|
+
### Bash Execution
|
|
132
|
+
|
|
133
|
+
- `!<command>` to run shell commands directly
|
|
134
|
+
- Real-time output streaming (stderr in red)
|
|
135
|
+
- Collapsible output with exit code display
|
|
136
|
+
- Cancel running processes
|
|
137
|
+
- 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
|
|
138
|
+
|
|
139
|
+
### Session Management
|
|
140
|
+
|
|
141
|
+
- Auto-resumes last session on page open, no manual switching needed
|
|
142
|
+
- After server restart, restores session context via ACP `loadSession` so conversations can continue
|
|
143
|
+
- Auto-generated titles (async, using a fast model)
|
|
144
|
+
- Session history persisted in SQLite, survives restarts
|
|
145
|
+
- `/sessions` lists all sessions (git-branch style, `*` marks current in green)
|
|
146
|
+
- Switching sessions replays full message history
|
|
147
|
+
|
|
148
|
+
### Slash Commands
|
|
149
|
+
|
|
150
|
+
Type `/` to trigger an autocomplete menu (arrow keys to navigate, Tab to select, Esc to close).
|
|
151
|
+
|
|
152
|
+
| Command | Description |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `/new [cwd]` | Create new session (optionally specify working directory) |
|
|
155
|
+
| `/pwd` | Show current working directory |
|
|
156
|
+
| `/model [name]` | View or switch model (fuzzy match, e.g. `/model opus`) |
|
|
157
|
+
| `/mode [name]` | View or switch mode (Agent / Plan / Autopilot) |
|
|
158
|
+
| `/think [level]` | View or switch reasoning effort (low / medium / high) |
|
|
159
|
+
| `/cancel` | Cancel current response |
|
|
160
|
+
| `/switch <title\|id>` | Switch to a session (match by title or ID prefix) |
|
|
161
|
+
| `/delete <title\|id>` | Delete a session |
|
|
162
|
+
| `/prune` | Delete all sessions except current |
|
|
163
|
+
| `/help` | Show help |
|
|
164
|
+
|
|
165
|
+
### Keyboard Shortcuts
|
|
166
|
+
|
|
167
|
+
| Shortcut | Action |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `Enter` | Send message |
|
|
170
|
+
| `Shift+Enter` | New line |
|
|
171
|
+
| `Ctrl+X` | Cancel current response |
|
|
172
|
+
| `Ctrl+M` | Cycle mode (Agent → Plan → Autopilot) |
|
|
173
|
+
| `Ctrl+U` | Upload image |
|
|
174
|
+
|
|
175
|
+
Tap the `❯` prompt indicator to cycle mode. Tap `new` to create a new session (hidden when input has content).
|
|
176
|
+
|
|
177
|
+
### Theme
|
|
178
|
+
|
|
179
|
+
- Dark / light / system, toggle with `◑`
|
|
180
|
+
- Terminal-style UI (monospace font, `>_` logo)
|
|
181
|
+
- Preference saved to localStorage
|
|
182
|
+
|
|
183
|
+
### Other
|
|
184
|
+
|
|
185
|
+
- PWA support (installable to home screen)
|
|
186
|
+
- WebSocket auto-reconnect (3s retry on disconnect)
|
|
187
|
+
- 30s heartbeat keepalive
|
|
188
|
+
- Auto-expanding input box
|
|
189
|
+
- Mobile-friendly layout
|
|
190
|
+
- Multi-client broadcast (events synced across devices)
|
|
191
|
+
|
|
192
|
+
## Testing
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm test # unit + integration
|
|
196
|
+
npm run test:e2e # Playwright browser E2E
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `TEST_SCENARIOS.md` is the scenario-level coverage map for the current suite.
|
|
200
|
+
- Use it when reviewing what is already protected before adding new tests or auditing gaps.
|
|
201
|
+
- 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.
|
|
202
|
+
|
|
203
|
+
## Architecture
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
Browser ←WebSocket→ server.ts ←ACP→ copilot CLI
|
|
207
|
+
├── routes.ts (HTTP handlers)
|
|
208
|
+
├── ws-handler.ts (WS dispatch)
|
|
209
|
+
├── session-manager.ts (state)
|
|
210
|
+
├── title-service.ts (auto-title)
|
|
211
|
+
└── store.ts (SQLite)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- **server.ts** — HTTP/WebSocket server bootstrap
|
|
215
|
+
- **routes.ts** — HTTP request handlers (static files, REST API, image upload)
|
|
216
|
+
- **ws-handler.ts** — WebSocket message dispatch + broadcast
|
|
217
|
+
- **session-manager.ts** — Session state management (live sessions, buffers, bash procs, model cache)
|
|
218
|
+
- **bridge.ts** — ACP bridge, manages agent subprocess, handles permissions and file I/O
|
|
219
|
+
- **store.ts** — SQLite persistence (sessions + events tables, WAL mode)
|
|
220
|
+
- **title-service.ts** — Async session title generation (dedicated Haiku session)
|
|
221
|
+
- **types.ts** — Shared types + Zod schemas for WS messages
|
|
222
|
+
|
|
223
|
+
## ACP Scope and Current Limits
|
|
224
|
+
|
|
225
|
+
WebAgent uses ACP for the core agent loop: session creation / restore, prompt turns, permission requests, streaming updates, model selection, and text file read/write.
|
|
226
|
+
|
|
227
|
+
Current scope in this repo:
|
|
228
|
+
|
|
229
|
+
- Session lifecycle goes through ACP (`newSession`, `loadSession`, `prompt`, `cancel`)
|
|
230
|
+
- The UI renders a subset of ACP session updates: assistant text, thinking text, tool calls, tool call updates, and plans
|
|
231
|
+
- Session history is persisted locally and restored after server restart
|
|
232
|
+
|
|
233
|
+
Current limits:
|
|
234
|
+
|
|
235
|
+
- MCP servers are not forwarded to the agent; sessions are created with an empty `mcpServers` list
|
|
236
|
+
- ACP terminal APIs are not used; `!<command>` runs through the app's own local `bash` bridge instead of an ACP-managed terminal session
|
|
237
|
+
- The web UI does not expose native CLI command surfaces such as `/plan`, `/fleet`, `/mcp`, `/agent`, or `/skills`
|
|
238
|
+
- Autopilot mode is supported: permissions are auto-approved server-side using `allow_once`
|
|
239
|
+
- 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
|
|
240
|
+
- Model switching depends on the agent's ACP implementation and currently uses the SDK's unstable session-model API
|
|
241
|
+
- ACP does not expose context window usage, token counts, or remaining capacity
|
|
242
|
+
- No method to compact or clear session context; only option is to create a new session
|
|
243
|
+
|
|
244
|
+
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.
|
package/bin/webagent.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const server = join(__dirname, "..", "src", "server.ts");
|
|
9
|
+
|
|
10
|
+
const child = spawn(
|
|
11
|
+
process.execPath,
|
|
12
|
+
["--experimental-strip-types", server, ...process.argv.slice(2)],
|
|
13
|
+
{ stdio: "inherit" },
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
17
|
+
process.on(sig, () => child.kill(sig));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
child.on("exit", (code, signal) => {
|
|
21
|
+
if (signal) process.kill(process.pid, signal);
|
|
22
|
+
else process.exit(code ?? 1);
|
|
23
|
+
});
|
package/config.toml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# WebAgent production configuration
|
|
2
|
+
|
|
3
|
+
# HTTP/WebSocket server port (default: 6800)
|
|
4
|
+
port = 6800
|
|
5
|
+
|
|
6
|
+
# SQLite + uploads directory (default: "data")
|
|
7
|
+
data_dir = "data"
|
|
8
|
+
|
|
9
|
+
# Default working directory for new sessions (default: process.cwd()).
|
|
10
|
+
# Leave this commented out to use the process working directory.
|
|
11
|
+
# default_cwd = "/path/to/your/workspace"
|
|
12
|
+
|
|
13
|
+
# Static assets directory (default: "dist")
|
|
14
|
+
public_dir = "dist"
|
|
15
|
+
|
|
16
|
+
# ACP agent command (binary + args, space-separated) (default: "copilot --acp")
|
|
17
|
+
agent_cmd = "copilot --acp"
|
|
18
|
+
|
|
19
|
+
[limits]
|
|
20
|
+
# Max bash output stored in DB per command (bytes, default 1 MB)
|
|
21
|
+
bash_output = 1_048_576
|
|
22
|
+
|
|
23
|
+
# Max image upload size (bytes, default 10 MB)
|
|
24
|
+
image_upload = 10_485_760
|
|
25
|
+
|
|
26
|
+
# Cancel timeout (ms, default 10s). After sending cancel, if the agent
|
|
27
|
+
# does not respond within this time the UI resets to idle. Set to 0 to disable.
|
|
28
|
+
cancel_timeout = 10_000
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>>_</title>
|
|
7
|
+
<link rel="icon" href="/icons/icon.svg" type="image/svg+xml">
|
|
8
|
+
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
|
9
|
+
<link rel="manifest" href="/manifest.json">
|
|
10
|
+
<meta name="theme-color" content="#0d1117">
|
|
11
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
12
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
14
|
+
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.2/dist/purify.min.js"></script>
|
|
15
|
+
<script>document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || 'auto');</script>
|
|
16
|
+
<link rel="stylesheet" href="/styles.mmjqzu9r.css">
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
|
|
20
|
+
<div id="header">
|
|
21
|
+
<div class="header-side header-left">
|
|
22
|
+
<span class="logo">>_</span>
|
|
23
|
+
</div>
|
|
24
|
+
<span id="session-info" class="status"></span>
|
|
25
|
+
<div class="header-side header-right">
|
|
26
|
+
<span id="status" class="status-dot is-disconnected" data-state="disconnected" role="status" aria-live="polite" aria-label="disconnected" title="disconnected"></span>
|
|
27
|
+
<button id="theme-btn" title="Toggle theme">◑</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div id="messages"></div>
|
|
32
|
+
|
|
33
|
+
<div id="attach-preview"></div>
|
|
34
|
+
<div id="input-area">
|
|
35
|
+
<div id="slash-menu"></div>
|
|
36
|
+
<span id="input-prompt" title="Cycle mode (Ctrl+M)">❯ </span>
|
|
37
|
+
<textarea id="input" rows="1" placeholder="Message or ?" autofocus></textarea>
|
|
38
|
+
<button id="new-btn" class="input-btn" title="New session">new</button>
|
|
39
|
+
<button id="attach-btn" class="input-btn" title="Attach image (Ctrl+U)">^U</button>
|
|
40
|
+
<button id="send-btn" class="input-btn" title="Send (Enter)">↵</button>
|
|
41
|
+
<input type="file" id="file-input" accept="image/*" multiple hidden>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<script type="module" src="/js/app.mmjqzu9r.js"></script>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Boot entry point — imports all modules and starts the app
|
|
2
|
+
|
|
3
|
+
import './render.mmjqzu9r.js'; // theme, click-to-collapse listeners
|
|
4
|
+
import './commands.mmjqzu9r.js'; // slash menu listeners
|
|
5
|
+
import './images.mmjqzu9r.js'; // attach/paste listeners
|
|
6
|
+
import './input.mmjqzu9r.js'; // keyboard/send listeners
|
|
7
|
+
import { connect } from './connection.mmjqzu9r.js';
|
|
8
|
+
|
|
9
|
+
connect();
|
|
10
|
+
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js');
|