@lelouchhe/webagent 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +58 -23
  2. package/bin/webagent.mjs +119 -8
  3. package/config.toml +96 -3
  4. package/dist/index.html +64 -41
  5. package/dist/js/app.GSAIYHML.js +4 -0
  6. package/dist/js/chunk.AJZBJBMO.js +1 -0
  7. package/dist/js/chunk.CGWFHJI2.js +76 -0
  8. package/dist/js/chunk.D4ZYHJAM.js +1 -0
  9. package/dist/js/chunk.VZXGXFNN.js +5 -0
  10. package/dist/js/login.PYIK52HN.js +1 -0
  11. package/dist/js/viewer.6DT53STL.js +1 -0
  12. package/dist/login.html +49 -0
  13. package/dist/share-viewer.00gubshk.css +114 -0
  14. package/dist/share-viewer.html +53 -0
  15. package/dist/styles.012p32dz.css +1443 -0
  16. package/dist/sw.js +79 -27
  17. package/dist/theme-init.js +6 -0
  18. package/lib/agent-detect.js +110 -0
  19. package/lib/atomic-write.js +50 -0
  20. package/lib/attachment-dispatch.js +86 -0
  21. package/lib/attachment-interceptor.js +130 -0
  22. package/lib/attachment-labels.js +139 -0
  23. package/lib/attachments.js +154 -0
  24. package/lib/auth-middleware.js +102 -0
  25. package/lib/auth-store.js +269 -0
  26. package/lib/auth.js +89 -0
  27. package/lib/bootstrap.js +70 -0
  28. package/lib/bridge.js +244 -93
  29. package/lib/client-registry.js +60 -0
  30. package/lib/config.js +123 -9
  31. package/lib/daemon.js +175 -41
  32. package/lib/event-handler.js +209 -91
  33. package/lib/log-fmt.js +67 -0
  34. package/lib/log.js +83 -0
  35. package/lib/message-cleanup.js +48 -0
  36. package/lib/mode-bucket.js +62 -0
  37. package/lib/preflight.js +195 -0
  38. package/lib/push-service.js +338 -45
  39. package/lib/routes.js +1202 -144
  40. package/lib/server.js +149 -33
  41. package/lib/session-manager.js +164 -18
  42. package/lib/session-state.js +160 -0
  43. package/lib/sessions-anchor.js +28 -0
  44. package/lib/share/cleanup.js +45 -0
  45. package/lib/share/routes.js +972 -0
  46. package/lib/share/sanitize.js +179 -0
  47. package/lib/sse-manager.js +94 -8
  48. package/lib/sse-ticket.js +45 -0
  49. package/lib/startup-checks.js +94 -0
  50. package/lib/store.js +624 -30
  51. package/lib/title-service.js +42 -9
  52. package/lib/tokens.js +50 -0
  53. package/lib/types.js +23 -0
  54. package/package.json +38 -4
  55. package/dist/js/app.2562YGRO.js +0 -10
  56. package/dist/styles.008ve1hx.css +0 -669
  57. package/lib/shared/constants.js +0 -17
package/README.md CHANGED
@@ -5,6 +5,24 @@
5
5
 
6
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).
7
7
 
8
+ WebAgent is a thin browser client + Node.js server that lets you drive any ACP agent from a desktop browser, phone, or PWA. Sessions, permissions, and notifications stay in sync across devices; nothing leaves your machine.
9
+
10
+ ## Highlights
11
+
12
+ - **Zero-config first run** — `npx @lelouchhe/webagent` and you're online. Auto-detects the ACP agent on your `PATH`, mints an admin token on first start, persists everything in `./data/`.
13
+ - **Multi-device, real-time** — REST + SSE keeps sessions, permissions, and bash output synced. Approve a permission on your laptop, see it confirmed on your phone.
14
+ - **Web Push notifications** — Get pinged on `prompt_done`, `permission_request`, or `bash_done` when the tab isn't focused. Smart per-session suppression: if any device is actively viewing session X, no buzz from session X.
15
+ - **PWA + mobile-friendly** — Installable to iOS / Android home screen. Mobile-first input, attach via paste/upload, dark-mode native.
16
+ - **Attachments** — Drag, paste, or `^U` any file (images, code, PDFs, …). Server sniffs real MIME from content, so agents reliably read it.
17
+ - **Inline bash** — `!ls -la` runs directly in your session's cwd, output streams in real time, cancellable.
18
+ - **Sessions that survive everything** — SQLite-persisted history, auto-resume on page open, auto-restore via ACP `loadSession` after server restart, auto-generated titles via a fast model.
19
+ - **Rich slash menu** — `/new`, `/switch`, `/model`, `/mode`, `/think`, `/notify`, `/inbox`, `/share`, `/token`, `/log` — autocomplete with Tab, submenus for pickable values.
20
+ - **Public share links** — `/share` snapshots a session into a sanitized read-only viewer at `/s/<token>` for show-and-tell.
21
+ - **Daemon mode with crash recovery** — `webagent start` runs as a background service with PID file, log rotation, and exponential-backoff restart on crash.
22
+ - **Built-in security** — Bearer token auth, per-device tokens, signed image URLs, strict CSP, single-operator threat model.
23
+
24
+ See [Features](docs/features.md) for the full tour.
25
+
8
26
  <table>
9
27
  <tr>
10
28
  <td width="60%">
@@ -36,17 +54,28 @@ A terminal-style web UI for [ACP](https://agentclientprotocol.com/)-compatible a
36
54
 
37
55
  ## Quick Start
38
56
 
39
- **Prerequisites:** Node.js 22.6+, an ACP-compatible agent installed and authenticated.
57
+ **Prerequisites:** Node.js 22.6+, an ACP-compatible agent installed and authenticated (Copilot CLI, Claude Code adapter, Gemini CLI, etc.).
40
58
 
41
59
  ```bash
42
- npm install -g @lelouchhe/webagent
43
- webagent # start on port 6800
44
- webagent --config /path/to/config.toml # custom config
60
+ npx @lelouchhe/webagent # zero-install, runs on port 6800
61
+ # or
62
+ npm install -g @lelouchhe/webagent && webagent
45
63
  ```
46
64
 
47
- Or run directly: `npx @lelouchhe/webagent`
65
+ On first run, the server prints a one-time admin token in the startup
66
+ diagnostic. Open `http://localhost:6800`, paste the token into the
67
+ login form, done. The token persists in `data/auth.json` — subsequent
68
+ runs skip the prompt.
69
+
70
+ Other ways to start:
71
+
72
+ ```bash
73
+ webagent start # background daemon (same first-run UX in your terminal)
74
+ webagent --config /path/to/config.toml # custom config (`webagent config init` to scaffold one)
75
+ webagent --create-token laptop # mint extra tokens for other devices / CI
76
+ ```
48
77
 
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.
78
+ Data (SQLite database, uploaded files) lives in `./data/` by default. See [Configuration & Operations](docs/configuration.md) for daemon mode, TOML settings, and agent setup.
50
79
 
51
80
  ## Architecture
52
81
 
@@ -57,14 +86,14 @@ Browser ←── REST + SSE ──→ Server ←── ACP ──→ Agent CLI
57
86
 
58
87
  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.
59
88
 
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 |
89
+ | Module | Role |
90
+ | -------------------- | ----------------------------------------------------------- |
91
+ | `routes.ts` | REST API + static files ([full API reference](docs/api.md)) |
92
+ | `event-handler.ts` | ACP event routing → SSE broadcast |
93
+ | `session-manager.ts` | Session state, buffers, bash processes |
94
+ | `bridge.ts` | ACP bridge — agent subprocess lifecycle |
95
+ | `store.ts` | SQLite persistence (WAL mode) |
96
+ | `daemon.ts` | Background service with crash recovery |
68
97
 
69
98
  Tech stack: Node.js + TypeScript (`--experimental-strip-types`), SQLite (`better-sqlite3`), Zod validation, esbuild bundling.
70
99
 
@@ -72,12 +101,18 @@ Frontend source lives in `public/js/*.ts`, bundled by esbuild into a single cont
72
101
 
73
102
  ## Documentation
74
103
 
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 |
104
+ | Document | Contents |
105
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------ |
106
+ | **[Features](docs/features.md)** | Chat, attachments, bash, sessions, slash commands, keyboard shortcuts, themes |
107
+ | **[Configuration & Operations](docs/configuration.md)** | TOML config, daemon commands, agent setup, upgrading |
108
+ | **[Security](docs/security.md)** | Bearer auth, token storage, SSE ticket, signed image URLs, CSP, data layout |
109
+ | **[API Reference](docs/api.md)** | REST endpoints, SSE events, implementation details |
110
+ | **[Attachments](docs/uploads.md)** | Upload pipeline, on-disk layout, lifecycle, permission auto-approve, observability |
111
+ | **[ACP Integration](docs/acp.md)** | Client extensions, protocol scope, current limits |
112
+ | **[Client Architecture](docs/client-architecture.md)** | Frontend modules, data flow, conventions |
113
+ | **[Slash Menu](docs/slash-menu.md)** | Walker pipeline, `CmdNode` tree, Tab/Enter/Click contract, how to add commands |
114
+ | **[Messages / Inbox](docs/messages.md)** | `/inbox` slash command, POST ingress, bound vs unbound messages |
115
+ | **[Share Links](docs/share.md)** | Public read-only session snapshots via `/share` + `/s/<token>` |
116
+ | **[Database Schema](docs/schema.md)** | SQLite tables, indexes, FK policy, cascade/lifecycle rules, migrations |
117
+ | **[Development](docs/development.md)** | Building from source, dev mode, testing, publishing |
118
+ | **[Auto-Start on Boot](docs/autostart.md)** | launchd, systemd, crontab, Windows Task Scheduler |
package/bin/webagent.mjs CHANGED
@@ -3,9 +3,121 @@
3
3
  import { spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join } from "node:path";
6
+ import { copyFileSync, existsSync } from "node:fs";
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
 
10
+ // ---- Subcommand: `webagent config <init|show>` ------------------------------
11
+ //
12
+ // `config init` copies the package's bundled config.toml (the same
13
+ // well-commented file that documents every key + its default) to
14
+ // ./config.toml. Refuses to overwrite without --force. The bundled
15
+ // file is the single source of truth, so what users get always matches
16
+ // the schema (test/config-coverage.test.ts guards that alignment).
17
+ //
18
+ // `config show` dumps the effective merged configuration (defaults
19
+ // overlaid with whatever --config provided) as TOML to stdout. Useful
20
+ // for "what is actually in effect right now".
21
+ if (process.argv[2] === "config") {
22
+ const sub = process.argv[3];
23
+ if (sub === "init") {
24
+ const force = process.argv.includes("--force");
25
+ const src = fileURLToPath(new URL("../config.toml", import.meta.url));
26
+ const dst = join(process.cwd(), "config.toml");
27
+ if (existsSync(dst) && !force) {
28
+ console.error(
29
+ `config.toml already exists at ${dst}. Use --force to overwrite.`,
30
+ );
31
+ process.exit(1);
32
+ }
33
+ try {
34
+ copyFileSync(src, dst);
35
+ } catch (err) {
36
+ console.error(`Failed to write ${dst}:`, err.message ?? err);
37
+ process.exit(1);
38
+ }
39
+ console.log(`wrote ${dst}`);
40
+ console.log(`edit it, then run: webagent --config config.toml`);
41
+ process.exit(0);
42
+ }
43
+ if (sub === "show") {
44
+ // Silence loadConfig's [config] log so stdout contains only TOML.
45
+ const origLog = console.log;
46
+ console.log = () => {};
47
+ const cfgUrl = new URL("../lib/config.js", import.meta.url).href;
48
+ const tomlUrl = "smol-toml";
49
+ const { loadConfig } = await import(cfgUrl);
50
+ const { stringify } = await import(tomlUrl);
51
+ const cfg = loadConfig();
52
+ console.log = origLog;
53
+ process.stdout.write(stringify(cfg));
54
+ process.exit(0);
55
+ }
56
+ console.error("Usage: webagent config <init|show> [--force]");
57
+ process.exit(64);
58
+ }
59
+
60
+ // ---- One-shot: --create-token <name> ---------------------------------------
61
+ //
62
+ // Provision an admin-scope token without launching the server. Run this
63
+ // once on first install (or when adding a new client device). The raw
64
+ // token is printed to stdout exactly once; we do not store it anywhere
65
+ // retrievable.
66
+ {
67
+ const argv = process.argv.slice(2);
68
+ const idx = argv.indexOf("--create-token");
69
+ if (idx !== -1) {
70
+ const name = argv[idx + 1];
71
+ if (!name || name.startsWith("--")) {
72
+ console.error("Usage: webagent --create-token <name> [--config <path>]");
73
+ process.exit(64);
74
+ }
75
+ const cfgIdx = argv.indexOf("--config");
76
+ if (cfgIdx !== -1 && argv[cfgIdx + 1]) {
77
+ // loadConfig() reads --config from process.argv directly
78
+ process.argv = [
79
+ process.argv[0],
80
+ process.argv[1],
81
+ "--config",
82
+ argv[cfgIdx + 1],
83
+ ];
84
+ } else {
85
+ process.argv = [process.argv[0], process.argv[1]];
86
+ }
87
+ // Silence loadConfig's [config] log so stdout contains only the raw
88
+ // token (machine-parseable). Errors still go to stderr.
89
+ const origLog = console.log;
90
+ console.log = () => {};
91
+ const cfgUrl = new URL("../lib/config.js", import.meta.url).href;
92
+ const storeUrl = new URL("../lib/auth-store.js", import.meta.url).href;
93
+ const { loadConfig } = await import(cfgUrl);
94
+ const { AuthStore } = await import(storeUrl);
95
+ const cfg = loadConfig();
96
+ console.log = origLog;
97
+ const store = new AuthStore(join(cfg.data_dir, "auth.json"));
98
+ await store.load();
99
+ try {
100
+ const created = await store.addToken(name, "admin");
101
+ // First line: raw token (machine-readable). Followed by a newline so
102
+ // `webagent --create-token foo | tr -d '\n' | pbcopy` works.
103
+ process.stdout.write(created.token + "\n");
104
+ console.error(
105
+ `\nCreated token '${name}' (admin scope). Save it now — it will not be shown again.`,
106
+ );
107
+ console.error(
108
+ `If the server is already running, send SIGHUP so it picks up the new token:`,
109
+ );
110
+ console.error(` kill -HUP $(pgrep -f 'lib/server.js')`);
111
+ await store.close();
112
+ process.exit(0);
113
+ } catch (err) {
114
+ console.error("Failed to create token:", err.message ?? err);
115
+ await store.close().catch(() => {});
116
+ process.exit(1);
117
+ }
118
+ }
119
+ }
120
+
9
121
  // ---- Service management subcommands (start/stop/status/restart) -----------
10
122
 
11
123
  const SUBCOMMANDS = new Set(["start", "stop", "status", "restart"]);
@@ -19,15 +131,14 @@ if (cmd && SUBCOMMANDS.has(cmd)) {
19
131
  // ---- Direct server launch (foreground) ----------------------------------
20
132
  const server = join(__dirname, "..", "lib", "server.js");
21
133
 
22
- const child = spawn(
23
- process.execPath,
24
- [server, ...process.argv.slice(2)],
25
- { stdio: "inherit" },
26
- );
134
+ const child = spawn(process.execPath, [server, ...process.argv.slice(2)], {
135
+ stdio: "inherit",
136
+ });
27
137
 
28
- const signals = process.platform === "win32"
29
- ? ["SIGINT", "SIGTERM"]
30
- : ["SIGINT", "SIGTERM", "SIGHUP"];
138
+ const signals =
139
+ process.platform === "win32"
140
+ ? ["SIGINT", "SIGTERM"]
141
+ : ["SIGINT", "SIGTERM", "SIGHUP"];
31
142
  for (const sig of signals) {
32
143
  process.on(sig, () => child.kill(sig));
33
144
  }
package/config.toml CHANGED
@@ -13,16 +13,25 @@ data_dir = "data"
13
13
  # Static assets directory (default: "dist")
14
14
  public_dir = "dist"
15
15
 
16
- # ACP agent command (binary + args, space-separated) (default: "copilot --acp")
17
- agent_cmd = "copilot --acp"
16
+ # ACP agent command (binary + args, space-separated).
17
+ # Default "auto" scans PATH for known ACP-ready agents (in priority order):
18
+ # copilot, gemini, opencode, claude-agent-acp, codex-acp, qwen
19
+ # Set explicitly to override:
20
+ # agent_cmd = "claude-agent-acp"
21
+ # agent_cmd = "codex-acp"
22
+ # agent_cmd = "gemini --acp"
23
+ agent_cmd = "auto"
18
24
 
19
25
  [limits]
20
26
  # Max bash output stored in DB per command (bytes, default 1 MB)
21
27
  bash_output = 1_048_576
22
28
 
23
- # Max image upload size (bytes, default 10 MB)
29
+ # Max image upload size (bytes, default 10 MB). Applies to image/* attachments.
24
30
  image_upload = 10_485_760
25
31
 
32
+ # Max non-image upload size (bytes, default 50 MB). Applies to all other mime types.
33
+ file_upload = 52_428_800
34
+
26
35
  # Cancel timeout (ms, default 10s). After sending cancel, if the agent
27
36
  # does not respond within this time the UI resets to idle. Set to 0 to disable.
28
37
  cancel_timeout = 10_000
@@ -35,3 +44,87 @@ recent_paths_ttl = 30
35
44
 
36
45
  [push]
37
46
  vapid_subject = "mailto:noreply@example.com"
47
+ # Kill switch for cross-device global visibility suppression. When true,
48
+ # if any client is actively viewing session X, push for session X is
49
+ # suppressed for every device. Flip to false to rollback the feature
50
+ # without a code change.
51
+ global_visibility_suppression = true
52
+
53
+ [title]
54
+ # Models matched (by case-insensitive substring) against the agent's
55
+ # reported availableModels for the title-generation sub-session. The first
56
+ # pattern that matches any model id wins; that model is set via
57
+ # setConfigOption. No match → skip the call and inherit the agent's
58
+ # default model (currentModelId).
59
+ #
60
+ # Default list targets the cheap/fast tier across major providers:
61
+ # - "haiku" → Anthropic (claude-haiku-*)
62
+ # - "flash-lite" → Google Gemini (gemini-*-flash-lite) [must precede "flash"]
63
+ # - "nano" → OpenAI (gpt-*-nano), Gemini Nano
64
+ # - "mini" → OpenAI (gpt-*-mini, 4o-mini), Mistral
65
+ # - "flash" → Google Gemini (gemini-*-flash)
66
+ # - "lite" → Cohere, generic
67
+ #
68
+ # Set model = [] to disable matching entirely and always inherit the
69
+ # agent's default model. To pin one specific model, use a single-element
70
+ # array, e.g. model = ["claude-haiku-4.5"].
71
+ model = ["haiku", "flash-lite", "nano", "mini", "flash", "lite"]
72
+
73
+ [debug]
74
+ # Frontend log level. One of: off | debug | info | warn | error.
75
+ # Default "off". Users can override per page-load via ?debug=<level> in
76
+ # the URL, or at runtime via the /debug slash command. level != off
77
+ # emits to both DevTools console and the conversation-flow DOM.
78
+ level = "off"
79
+
80
+ [messages]
81
+ # Days before an unprocessed unbound inbox message is auto-cleaned
82
+ # (default 30). 0 = keep forever. Bound messages (to:"session:<id>")
83
+ # are stored on the owning session and are not subject to this TTL.
84
+ unprocessed_ttl_days = 30
85
+
86
+ [share]
87
+ # Public read-only session share links. Default off. Dogfood manually
88
+ # flips `enabled = true` after CF Access bypass + Rate Limiting are
89
+ # configured. See docs/share.md.
90
+
91
+ # Master kill switch. When false, all /api/v1/sessions/*/share* and
92
+ # /s/* routes return 410, and /share slash commands are hidden.
93
+ enabled = false
94
+
95
+ # Global default TTL for public share links (hours). 0 = never expire
96
+ # (default). Values >0 are clamped to 168 (7d) at startup with a WARN.
97
+ # Per-share overrides live in the shares.ttl_hours column.
98
+ ttl_hours = 0
99
+
100
+ # true emits Content-Security-Policy on /s/* + /api/v1/shared/*.
101
+ # Set false to fall back to Content-Security-Policy-Report-Only as an
102
+ # emergency kill switch without touching code.
103
+ csp_enforce = true
104
+
105
+ # Public viewer origin; null = same as webagent host. Set when viewer is
106
+ # fronted by a separate CF Worker route (e.g. "https://share.example.com").
107
+ viewer_origin = ""
108
+
109
+ # Sanitizer internal-domain allowlist; substrings matched (case-insensitive)
110
+ # get rewritten to `<internal-host>` before publishing. Example:
111
+ # internal_hosts = ["corp.example.com", "internal-db"]
112
+ internal_hosts = []
113
+
114
+ [auth]
115
+ # Bearer-token auth knobs.
116
+ #
117
+ # first_run_bootstrap controls the zero-config first-run UX. When true
118
+ # (default), if auth.json does NOT exist AND stdin is a TTY, the server
119
+ # auto-mints a one-time admin token and prints it as part of the
120
+ # startup-doctor stream. Operator copies the token from terminal
121
+ # scrollback and pastes it into the /login form.
122
+ #
123
+ # Set to false when deploying behind a supervisor that provisions auth.json
124
+ # out-of-band (CI, Ansible, k8s init container) — server falls back to
125
+ # "exit 78 + use --create-token" behavior.
126
+ #
127
+ # Note: this only kicks in for missing auth.json. If the file exists but
128
+ # the token list is empty (manual edit, parse failure), server still exits
129
+ # 78 — that's a config anomaly, not a fresh install.
130
+ first_run_bootstrap = true
package/dist/index.html CHANGED
@@ -1,47 +1,70 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
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" media="(prefers-color-scheme: dark)">
11
- <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
12
- <meta name="apple-mobile-web-app-capable" content="yes">
13
- <meta name="apple-mobile-web-app-status-bar-style" content="default">
14
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
15
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.2/dist/purify.min.js"></script>
16
- <script>document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || 'auto');</script>
17
- <link rel="stylesheet" href="/styles.008ve1hx.css">
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
11
+ name="theme-color"
12
+ content="#0d1117"
13
+ media="(prefers-color-scheme: dark)"
14
+ />
15
+ <meta
16
+ name="theme-color"
17
+ content="#ffffff"
18
+ media="(prefers-color-scheme: light)"
19
+ />
20
+ <meta name="mobile-web-app-capable" content="yes" />
21
+ <!-- Keep apple-prefixed version: iOS Safari still only recognizes this one
22
+ for standalone PWA mode. Chrome's deprecation warning is satisfied by
23
+ the standard version above. Remove once iOS supports the standard. -->
24
+ <meta name="apple-mobile-web-app-capable" content="yes" />
25
+ <meta name="apple-mobile-web-app-status-bar-style" content="default" />
26
+ <script src="/theme-init.js"></script>
27
+ <link rel="stylesheet" href="/styles.012p32dz.css" />
28
+ <link rel="modulepreload" href="/js/chunk.D4ZYHJAM.js">
29
+ <link rel="modulepreload" href="/js/chunk.CGWFHJI2.js">
30
+ <link rel="modulepreload" href="/js/chunk.AJZBJBMO.js">
18
31
  </head>
19
- <body>
32
+ <body>
33
+ <div id="header">
34
+ <div class="header-side header-left">
35
+ <span class="logo">>_</span>
36
+ </div>
37
+ <span id="session-info" class="status"></span>
38
+ <div class="header-side header-right">
39
+ <span
40
+ id="status"
41
+ class="status-dot is-disconnected"
42
+ data-state="disconnected"
43
+ role="status"
44
+ aria-live="polite"
45
+ aria-label="disconnected"
46
+ title="disconnected"
47
+ ></span>
48
+ <button id="theme-btn" title="Toggle theme">◑</button>
49
+ </div>
50
+ </div>
20
51
 
21
- <div id="header">
22
- <div class="header-side header-left">
23
- <span class="logo">>_</span>
24
- </div>
25
- <span id="session-info" class="status"></span>
26
- <div class="header-side header-right">
27
- <span id="status" class="status-dot is-disconnected" data-state="disconnected" role="status" aria-live="polite" aria-label="disconnected" title="disconnected"></span>
28
- <button id="theme-btn" title="Toggle theme">◑</button>
29
- </div>
30
- </div>
52
+ <div id="messages"></div>
31
53
 
32
- <div id="messages"></div>
54
+ <div id="attach-preview"></div>
55
+ <div id="input-area">
56
+ <div id="slash-menu"></div>
57
+ <span id="mode-pill"></span>
58
+ <span id="input-prompt" title="Cycle mode (Ctrl+M)"></span>
59
+ <textarea id="input" rows="1" placeholder="Message or ?"></textarea>
60
+ <button id="attach-btn" class="input-btn" title="Attach image (Ctrl+U)">
61
+ ^U
62
+ </button>
63
+ <button id="send-btn" class="input-btn" title="Send (Enter)">↵</button>
64
+ <input type="file" id="file-input" multiple hidden />
65
+ </div>
66
+ <div id="status-bar"></div>
33
67
 
34
- <div id="attach-preview"></div>
35
- <div id="input-area">
36
- <div id="slash-menu"></div>
37
- <span id="input-prompt" title="Cycle mode (Ctrl+M)">❯ </span>
38
- <textarea id="input" rows="1" placeholder="Message or ?"></textarea>
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
- <div id="status-bar"></div>
44
-
45
- <script src="/js/app.2562YGRO.js"></script>
46
- </body>
68
+ <script type="module" src="/js/app.GSAIYHML.js"></script>
69
+ </body>
47
70
  </html>