@rubytech/create-realagent 1.0.684 → 1.0.685

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/dist/index.js CHANGED
@@ -1731,28 +1731,33 @@ function installTerminalService() {
1731
1731
  console.error(` WARNING: failed to write ~/.tmux.conf: ${err instanceof Error ? err.message : String(err)}`);
1732
1732
  }
1733
1733
  }
1734
- // Install and enable the maxy-ttyd.service --user unit. Independent of
1735
- // BRAND.serviceName a single device runs one admin terminal regardless of
1736
- // brand, because the unit binds to 127.0.0.1:7681 which only one process can
1737
- // hold anyway. On a multi-brand device, the first brand's install writes the
1738
- // unit and every subsequent install is a no-op (idempotent overwrite).
1734
+ // Install and enable the per-brand ttyd --user unit (Task 662). The unit
1735
+ // file name and the loopback port are both brand-scoped so two brands
1736
+ // installed on the same device each run their own ttyd process without
1737
+ // contending for either the filesystem path or the TCP port:
1738
+ // `${BRAND.hostname}-ttyd.service` + 127.0.0.1:${BRAND.ttydPort}
1739
1739
  const systemdUserDir = resolve(homeDir, ".config/systemd/user");
1740
1740
  mkdirSync(systemdUserDir, { recursive: true });
1741
+ const ttydUnitShort = `${BRAND.hostname}-ttyd`;
1742
+ const ttydUnitName = `${ttydUnitShort}.service`;
1741
1743
  // Skip systemd-unit install if the ttyd binary is not in place — enabling
1742
1744
  // a unit whose ExecStart points at a missing file just churns systemd with
1743
1745
  // restart failures.
1744
1746
  if (!ttydReady) {
1745
- console.error(" Skipping maxy-ttyd.service install — ttyd binary not present. Admin terminal will be unavailable until remediated.");
1747
+ console.error(` Skipping ${ttydUnitName} install — ttyd binary not present. Admin terminal will be unavailable until remediated.`);
1746
1748
  return;
1747
1749
  }
1748
- const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-ttyd.service");
1749
- const ttydUnitDest = join(systemdUserDir, "maxy-ttyd.service");
1750
+ const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/ttyd.service.template");
1751
+ const ttydUnitDest = join(systemdUserDir, ttydUnitName);
1750
1752
  try {
1751
1753
  if (existsSync(ttydUnitTemplate)) {
1752
- writeFileSync(ttydUnitDest, readFileSync(ttydUnitTemplate, "utf-8"));
1754
+ const ttydServiceContent = readFileSync(ttydUnitTemplate, "utf-8")
1755
+ .replace(/__TTYD_PORT__/g, String(TTYD_PORT));
1756
+ writeFileSync(ttydUnitDest, ttydServiceContent);
1757
+ logFile(` ${ttydUnitName}: TTYD_PORT=${TTYD_PORT}`);
1753
1758
  }
1754
1759
  else {
1755
- console.error(` WARNING: maxy-ttyd.service template missing at ${ttydUnitTemplate} — admin terminal will not work`);
1760
+ console.error(` WARNING: ttyd.service.template missing at ${ttydUnitTemplate} — admin terminal will not work`);
1756
1761
  return;
1757
1762
  }
1758
1763
  }
@@ -1761,9 +1766,9 @@ function installTerminalService() {
1761
1766
  return;
1762
1767
  }
1763
1768
  spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
1764
- spawnSync("systemctl", ["--user", "enable", "maxy-ttyd"], { stdio: "inherit" });
1765
- spawnSync("systemctl", ["--user", "restart", "maxy-ttyd"], { stdio: "inherit" });
1766
- console.log(" maxy-ttyd.service enabled — admin terminal available on 127.0.0.1:7681");
1769
+ spawnSync("systemctl", ["--user", "enable", ttydUnitShort], { stdio: "inherit" });
1770
+ spawnSync("systemctl", ["--user", "restart", ttydUnitShort], { stdio: "inherit" });
1771
+ console.log(` ${ttydUnitName} enabled — admin terminal available on 127.0.0.1:${TTYD_PORT}`);
1767
1772
  }
1768
1773
  function installService() {
1769
1774
  log("12", TOTAL, `Starting ${BRAND.productName}...`);
@@ -1847,10 +1852,12 @@ function installService() {
1847
1852
  // non-default --port.
1848
1853
  const MAXY_UI_INTERNAL_PORT = PORT + 1;
1849
1854
  const neo4jServiceDep = NEO4J_DEDICATED ? `neo4j-${BRAND.hostname}.service` : "neo4j.service";
1855
+ const edgeUnitShort = `${BRAND.hostname}-edge`;
1856
+ const edgeUnitName = `${edgeUnitShort}.service`;
1850
1857
  const serviceFile = `[Unit]
1851
1858
  Description=${BRAND.productName} AI Assistant
1852
- After=${neo4jServiceDep} maxy-edge.service
1853
- Wants=maxy-edge.service
1859
+ After=${neo4jServiceDep} ${edgeUnitName}
1860
+ Wants=${edgeUnitName}
1854
1861
 
1855
1862
  [Service]
1856
1863
  Type=notify
@@ -1879,23 +1886,31 @@ StandardError=append:${persistDir}/logs/server.log
1879
1886
  WantedBy=default.target
1880
1887
  `;
1881
1888
  writeFileSync(join(serviceDir, BRAND.serviceName), serviceFile);
1882
- // Task 647 — maxy-edge.service: the always-on front door that owns the
1883
- // public port (PORT) and the VNC stack (Xtigervnc + websockify). Its
1884
- // lifecycle is independent of maxy-ui, so an in-place upgrade triggered
1885
- // from the admin terminal can restart maxy-ui without disconnecting the
1886
- // browser's remote terminal WebSocket.
1887
- const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-edge.service");
1889
+ // Task 647 — the edge service: always-on front door that owns the public
1890
+ // port (PORT) and the VNC stack (Xtigervnc + websockify). Its lifecycle is
1891
+ // independent of the main brand service, so an in-place upgrade triggered
1892
+ // from the admin terminal can restart the main brand service without
1893
+ // disconnecting the browser's remote terminal WebSocket.
1894
+ //
1895
+ // Task 662: the unit is per-brand so two brands on the same device each
1896
+ // own their own edge listener on their own EDGE_PORT — installing brand B
1897
+ // never rewrites brand A's unit or steals brand A's public port. Upgrades
1898
+ // from pre-662 installs require the manual recovery paragraph in
1899
+ // .docs/deployment.md before re-running this installer; auto-migration is
1900
+ // intentionally scoped out.
1901
+ const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/edge.service.template");
1888
1902
  if (existsSync(edgeTemplatePath)) {
1889
1903
  const edgeServiceContent = readFileSync(edgeTemplatePath, "utf-8")
1890
1904
  .replace(/__INSTALL_DIR__/g, INSTALL_DIR)
1891
1905
  .replace(/__EDGE_PORT__/g, String(PORT))
1892
1906
  .replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
1907
+ .replace(/__TTYD_PORT__/g, String(TTYD_PORT))
1893
1908
  .replace(/__PERSIST_DIR__/g, persistDir);
1894
- writeFileSync(join(serviceDir, "maxy-edge.service"), edgeServiceContent);
1895
- logFile(` maxy-edge.service: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}`);
1909
+ writeFileSync(join(serviceDir, edgeUnitName), edgeServiceContent);
1910
+ logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT} TTYD_PORT=${TTYD_PORT}`);
1896
1911
  }
1897
1912
  else {
1898
- console.error(` WARNING: maxy-edge.service template missing at ${edgeTemplatePath} — remote terminal will disconnect during upgrade`);
1913
+ console.error(` WARNING: edge.service.template missing at ${edgeTemplatePath} — remote terminal will disconnect during upgrade`);
1899
1914
  }
1900
1915
  // Task 560: the unit declares Environment=PATH=%h/.local/bin:... so the graph
1901
1916
  // MCP shim's spawn("uvx", ...) resolves against uv's install location. Without
@@ -1952,17 +1967,17 @@ WantedBy=multi-user.target
1952
1967
  catch { /* not critical */ }
1953
1968
  // Reload and (re)start.
1954
1969
  //
1955
- // Task 647 ordering: on upgrades, the old maxy-ui still holds the public
1956
- // port (PORT). Stop it FIRST so maxy-edge can bind that socket; starting
1957
- // maxy-edge first would race against the old maxy-ui and fail with EADDRINUSE.
1958
- // Fresh installs: the stop is a no-op.
1970
+ // Task 647 ordering: on upgrades, the old main brand service still holds
1971
+ // the public port (PORT). Stop it FIRST so the edge can bind that socket;
1972
+ // starting the edge first would race against the old main brand service
1973
+ // and fail with EADDRINUSE. Fresh installs: the stop is a no-op.
1959
1974
  const unitName = BRAND.serviceName.replace(".service", "");
1960
1975
  spawnSync("systemctl", ["--user", "stop", unitName], { stdio: "inherit" });
1961
1976
  spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
1962
- spawnSync("systemctl", ["--user", "enable", "maxy-edge"], { stdio: "inherit" });
1977
+ spawnSync("systemctl", ["--user", "enable", edgeUnitShort], { stdio: "inherit" });
1963
1978
  spawnSync("systemctl", ["--user", "enable", unitName], { stdio: "inherit" });
1964
- // maxy-edge first: binds public port + starts VNC stack. Then maxy-ui.
1965
- spawnSync("systemctl", ["--user", "restart", "maxy-edge"], { stdio: "inherit" });
1979
+ // edge first: binds public port + starts VNC stack. Then main brand service.
1980
+ spawnSync("systemctl", ["--user", "restart", edgeUnitShort], { stdio: "inherit" });
1966
1981
  spawnSync("systemctl", ["--user", "restart", unitName], { stdio: "inherit" });
1967
1982
  // Wait for the server to come up
1968
1983
  console.log(" Waiting for web server...");
@@ -2312,6 +2327,28 @@ else {
2312
2327
  }
2313
2328
  // Dedicated = port differs from the default shared instance
2314
2329
  const NEO4J_DEDICATED = NEO4J_PORT !== DEFAULT_NEO4J_PORT;
2330
+ // ---------------------------------------------------------------------------
2331
+ // TTYD port — per-brand loopback so two brands on the same device each run
2332
+ // their own ttyd unit on a distinct port (Task 662). Edge service proxies
2333
+ // /ttyd to this port via Environment=TTYD_PORT.
2334
+ //
2335
+ // Priority: --ttyd-port flag > BRAND.ttydPort > 7681. Same pattern as
2336
+ // NEO4J_PORT (Task 659).
2337
+ // ---------------------------------------------------------------------------
2338
+ const DEFAULT_TTYD_PORT = 7681;
2339
+ let TTYD_PORT = BRAND.ttydPort ?? DEFAULT_TTYD_PORT;
2340
+ let TTYD_PORT_SOURCE = BRAND.ttydPort ? "brand.json" : "default";
2341
+ const ttydPortIdx = _args.indexOf("--ttyd-port");
2342
+ if (ttydPortIdx !== -1) {
2343
+ const raw = _args[ttydPortIdx + 1];
2344
+ const parsed = raw ? parseInt(raw, 10) : NaN;
2345
+ if (isNaN(parsed) || parsed < 1024 || parsed > 65535) {
2346
+ console.error(`Setup failed: --ttyd-port requires a numeric value between 1024 and 65535 (got: ${raw ?? "nothing"})`);
2347
+ process.exit(1);
2348
+ }
2349
+ TTYD_PORT = parsed;
2350
+ TTYD_PORT_SOURCE = "--ttyd-port flag";
2351
+ }
2315
2352
  const PKG_VERSION = JSON.parse(readFileSync(resolve(import.meta.dirname, "../package.json"), "utf-8")).version;
2316
2353
  initLogging();
2317
2354
  console.log("================================================================");
@@ -2324,6 +2361,7 @@ if (HOSTNAME_FLAG)
2324
2361
  console.log(` Display: ${DISPLAY_MODE} (${DISPLAY_MODE_SOURCE})`);
2325
2362
  console.log(` Embed model: ${EMBED_MODEL} (${EMBED_DIMS} dims, ${EMBED_SOURCE})`);
2326
2363
  console.log(` Neo4j: ${NEO4J_DEDICATED ? "dedicated" : "shared"} on bolt://localhost:${NEO4J_PORT} (${NEO4J_PORT_SOURCE})`);
2364
+ console.log(` ttyd: 127.0.0.1:${TTYD_PORT} (${TTYD_PORT_SOURCE})`);
2327
2365
  console.log("");
2328
2366
  logDiagnostics("pre-flight");
2329
2367
  logFile(` Neo4j instance: ${NEO4J_DEDICATED ? "dedicated" : "shared"} on bolt://localhost:${NEO4J_PORT}`);
package/dist/uninstall.js CHANGED
@@ -85,14 +85,23 @@ export function isMaxyInstalled() {
85
85
  * present — its runtime still depends on those singletons.
86
86
  *
87
87
  * Detection: any `.service` file in `~/.config/systemd/user/` whose name is
88
- * not this brand's `BRAND.serviceName`. This mirrors the install-time check
89
- * at index.ts:489 which uses the same signal for apt/hostname behavior. */
88
+ * not this brand's `BRAND.serviceName` and not this brand's own edge/ttyd
89
+ * unit (Task 662 each brand owns its own per-brand edge + ttyd unit, so
90
+ * those files must not register as peer evidence).
91
+ *
92
+ * Legacy pre-662 shared `maxy-edge.service` / `maxy-ttyd.service` are
93
+ * intentionally counted as peer evidence on uninstall: on a dual-brand
94
+ * pre-662 device the shared unit belonged to whichever brand last ran the
95
+ * installer — treating it as peer evidence keeps the uninstaller from
96
+ * purging device-wide singletons the other brand still depends on. */
90
97
  function peerBrandPresent() {
91
98
  const systemdUserDir = resolve(HOME, ".config/systemd/user");
92
99
  if (!existsSync(systemdUserDir))
93
100
  return false;
101
+ const thisBrandEdge = `${BRAND.hostname}-edge.service`;
102
+ const thisBrandTtyd = `${BRAND.hostname}-ttyd.service`;
94
103
  try {
95
- return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && !f.startsWith("maxy-edge") && !f.startsWith("maxy-ttyd"));
104
+ return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && f !== thisBrandEdge && f !== thisBrandTtyd);
96
105
  }
97
106
  catch {
98
107
  return false;
@@ -111,14 +120,25 @@ function stopServices() {
111
120
  catch {
112
121
  console.log(` ${BRAND.serviceName} not running`);
113
122
  }
114
- // Task 647: maxy-edge owns the public port and VNC stack. Stop it after
115
- // maxy-ui so the edge's ExecStopPost (vnc.sh stop) runs cleanly.
123
+ // Task 647: the edge service owns the public port and VNC stack. Stop it
124
+ // after the main brand service so the edge's ExecStopPost (vnc.sh stop)
125
+ // runs cleanly. Task 662: the edge unit is per-brand (`<hostname>-edge`),
126
+ // plus the ttyd unit (`<hostname>-ttyd`) — both must be stopped.
127
+ const edgeUnitShort = `${BRAND.hostname}-edge`;
128
+ const ttydUnitShort = `${BRAND.hostname}-ttyd`;
116
129
  try {
117
- spawnSync("systemctl", ["--user", "stop", "maxy-edge"], { stdio: "pipe", timeout: 15_000 });
118
- console.log(" Stopped maxy-edge");
130
+ spawnSync("systemctl", ["--user", "stop", edgeUnitShort], { stdio: "pipe", timeout: 15_000 });
131
+ console.log(` Stopped ${edgeUnitShort}`);
119
132
  }
120
133
  catch {
121
- console.log(" maxy-edge not running");
134
+ console.log(` ${edgeUnitShort} not running`);
135
+ }
136
+ try {
137
+ spawnSync("systemctl", ["--user", "stop", ttydUnitShort], { stdio: "pipe", timeout: 15_000 });
138
+ console.log(` Stopped ${ttydUnitShort}`);
139
+ }
140
+ catch {
141
+ console.log(` ${ttydUnitShort} not running`);
122
142
  }
123
143
  // Stop Neo4j — dedicated branded instance if this brand uses one, else shared.
124
144
  // Brand isolation (Task 659): never stop `neo4j.service` when this brand runs
@@ -551,19 +571,27 @@ function removeSystemdService() {
551
571
  console.log(` Failed to remove service file: ${err instanceof Error ? err.message : String(err)}`);
552
572
  }
553
573
  }
554
- // Task 647: remove maxy-edge.service alongside maxy-ui.
555
- try {
556
- spawnSync("systemctl", ["--user", "disable", "maxy-edge"], { stdio: "pipe" });
557
- }
558
- catch { /* ignore */ }
559
- const edgeServiceFile = resolve(HOME, ".config/systemd/user/maxy-edge.service");
560
- if (existsSync(edgeServiceFile)) {
574
+ // Task 647 + 662: remove this brand's per-brand edge + ttyd units alongside
575
+ // the main brand unit. Peer brand's edge/ttyd units live at different
576
+ // filenames (`<peer-hostname>-edge.service`) and are never touched here.
577
+ // Legacy pre-662 shared units left on disk by a previous installer are
578
+ // cleaned up by the installer pre-hygiene loop, not here.
579
+ const thisBrandEdge = `${BRAND.hostname}-edge`;
580
+ const thisBrandTtyd = `${BRAND.hostname}-ttyd`;
581
+ for (const unit of [thisBrandEdge, thisBrandTtyd]) {
561
582
  try {
562
- rmSync(edgeServiceFile);
563
- console.log(" Removed maxy-edge.service");
583
+ spawnSync("systemctl", ["--user", "disable", unit], { stdio: "pipe" });
564
584
  }
565
- catch (err) {
566
- console.log(` Failed to remove maxy-edge.service: ${err instanceof Error ? err.message : String(err)}`);
585
+ catch { /* ignore */ }
586
+ const unitFile = resolve(HOME, `.config/systemd/user/${unit}.service`);
587
+ if (existsSync(unitFile)) {
588
+ try {
589
+ rmSync(unitFile);
590
+ console.log(` Removed ${unit}.service`);
591
+ }
592
+ catch (err) {
593
+ console.log(` Failed to remove ${unit}.service: ${err instanceof Error ? err.message : String(err)}`);
594
+ }
567
595
  }
568
596
  }
569
597
  // Reload daemon
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.684",
3
+ "version": "1.0.685",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -7,6 +7,7 @@
7
7
  "tagline": "Built for agents. By agents.",
8
8
  "domain": "realagent.network",
9
9
  "neo4jPort": 7688,
10
+ "ttydPort": 7682,
10
11
 
11
12
  "defaultColors": {
12
13
  "primary": "#7C8C72",
@@ -68,19 +68,29 @@ The logs will show which service failed to start and why. Common causes:
68
68
 
69
69
  ## Systemd units on each device
70
70
 
71
- Each Maxy device runs one `--user` systemd unit:
71
+ Each installed brand runs three per-brand `--user` systemd units (Task 662 — unit filenames are prefixed with the brand's `hostname` so two brands on the same device never share a unit file):
72
72
 
73
- - `maxy-ui.service` — the admin + public HTTP server on `127.0.0.1:19199`. Restarted by the upgrade flow; short downtime is expected during steps 8→12 of an upgrade.
74
- - `maxy-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to `maxy-ui`, handles `/websockify` (VNC) and `/ttyd` (admin terminal) WebSocket upgrades locally. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
75
- - `maxy-ttyd.service` — `ttyd` bound to `127.0.0.1:7681`, running `tmux new-session -A -s maxy-pty`. Owns the byte-stream admin terminal rendered by xterm.js in the header overlay and the Software Update modal (Task 657). Independent of `maxy-ui` and `maxy-edge`; outlives service restarts so scrollback is preserved.
73
+ - `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19199`. Restarted by the upgrade flow; short downtime is expected during steps 8→12 of an upgrade.
74
+ - `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service, handles `/websockify` (VNC) and `/ttyd` (admin terminal) WebSocket upgrades locally. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
75
+ - `{hostname}-ttyd.service` — `ttyd` bound to `127.0.0.1:{BRAND.ttydPort}` (7681 Maxy, 7682 Real Agent), running `tmux new-session -A -s maxy-pty`. Owns the byte-stream admin terminal rendered by xterm.js in the header overlay and the Software Update modal (Task 657). Independent of the main brand service and the edge; outlives service restarts so scrollback is preserved.
76
76
 
77
- If the admin terminal fails to open, check `sudo tail -n 50 ~/.maxy/logs/edge-boot.log` — the `ttyd-ws-upgrade` / `ttyd-proxy-open` / `ttyd-proxy-close` lines carry a `corrId` that ties the full session lifecycle together. For unit health, `systemctl --user status maxy-ttyd` + `journalctl --user -u maxy-ttyd`.
77
+ If the admin terminal fails to open, check `sudo tail -n 50 ~/{configDir}/logs/edge-boot.log` — the `ttyd-ws-upgrade` / `ttyd-proxy-open` / `ttyd-proxy-close` lines carry a `corrId` that ties the full session lifecycle together. For unit health, `systemctl --user status {hostname}-ttyd` + `journalctl --user -u {hostname}-ttyd`.
78
+
79
+ **Pre-Task-662 upgrade** — devices that ran an installer before Task 662 have legacy shared `maxy-edge.service` and `maxy-ttyd.service` units on disk, which will collide with the new per-brand units. Before re-running any installer on such a device:
80
+
81
+ ```bash
82
+ systemctl --user stop maxy-edge maxy-ttyd 2>/dev/null || true
83
+ systemctl --user disable maxy-edge maxy-ttyd 2>/dev/null || true
84
+ rm -f ~/.config/systemd/user/maxy-edge.service \
85
+ ~/.config/systemd/user/maxy-ttyd.service
86
+ systemctl --user daemon-reload
87
+ ```
78
88
 
79
89
  ## Running multiple brands on one device
80
90
 
81
91
  A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.
82
92
 
83
- - **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688).
93
+ - **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge + ttyd systemd units (`maxy-edge.service` + `maxy-ttyd.service` vs `realagent-edge.service` + `realagent-ttyd.service`), its own ttyd loopback port (7681 vs 7682), and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688).
84
94
  - **Shared:** both brands share the system Chromium/VNC stack, the Ollama model server, and the `cloudflared` command itself. Browser automation is serialised — one admin session at a time across both brands.
85
95
 
86
96
  To install a second brand on a device that already runs the first, just run the other installer. No flags needed for isolation:
@@ -1,14 +1,15 @@
1
1
  [Unit]
2
- Description=Maxy Edge (public port + VNC transport — independent of maxy-ui)
3
- # No ordering dependency on maxy-ui: the edge is the long-running front door.
4
- # maxy-ui.service declares Wants/After this unit so maxy-ui only starts once
5
- # the edge is listening, but this unit has no such reciprocal dependency.
2
+ Description=Edge service (public port + VNC + ttyd transport — independent of the main brand service)
3
+ # No ordering dependency on the main brand service: the edge is the long-running
4
+ # front door. The main brand unit declares Wants/After this unit so it only
5
+ # starts once the edge is listening, but this unit has no such reciprocal
6
+ # dependency.
6
7
 
7
8
  [Service]
8
9
  Type=simple
9
10
  # ExecStartPre owns the VNC stack so an `npx -y @rubytech/create-maxy@latest`
10
- # run from the admin terminal can restart maxy-ui without taking Xtigervnc,
11
- # websockify, or the upgrade shell down with it. Task 647.
11
+ # run from the admin terminal can restart the main brand service without
12
+ # taking Xtigervnc, websockify, or the upgrade shell down with it. Task 647.
12
13
  ExecStartPre=/bin/bash __INSTALL_DIR__/platform/scripts/vnc.sh start
13
14
  ExecStart=/usr/bin/node __INSTALL_DIR__/server/maxy-edge.js
14
15
  ExecStopPost=/bin/bash __INSTALL_DIR__/platform/scripts/vnc.sh stop
@@ -23,6 +24,7 @@ Environment=MAXY_UI_HOST=127.0.0.1
23
24
  Environment=MAXY_UI_PORT=__MAXY_UI_PORT__
24
25
  Environment=WEBSOCKIFY_HOST=127.0.0.1
25
26
  Environment=WEBSOCKIFY_PORT=6080
27
+ Environment=TTYD_PORT=__TTYD_PORT__
26
28
  Environment=DISPLAY=:99
27
29
  Environment=MAXY_PLATFORM_ROOT=__INSTALL_DIR__/platform
28
30
  Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
@@ -1,5 +1,5 @@
1
1
  [Unit]
2
- Description=Maxy admin terminal (ttyd + tmux) — persistent PTY for admin UI
2
+ Description=Admin terminal (ttyd + tmux) — persistent PTY for admin UI
3
3
  After=default.target
4
4
 
5
5
  [Service]
@@ -9,14 +9,19 @@ Type=simple
9
9
  # because Debian Bookworm's apt does NOT carry a ttyd package. /usr/local/bin
10
10
  # is the standard location for binaries installed outside the distro package
11
11
  # manager; /usr/bin is reserved for apt-owned files.
12
- # -p 7681 listen on 127.0.0.1:7681 (same-origin proxy in maxy-ui binds to it)
12
+ # -p __TTYD_PORT__ listen on 127.0.0.1:<per-brand-port> (the edge service
13
+ # proxies /ttyd to it via TTYD_PORT env)
13
14
  # -i 127.0.0.1 reject non-loopback connections at the ttyd layer as well
14
15
  # -W writable (allow client → server input bytes)
15
16
  # tmux new-session -A -s maxy-pty attach if session exists, create otherwise.
16
- # Lifetime = user session / device lifetime. Outlives maxy-ui restarts because
17
- # this unit has no After=maxy-ui.service and no Requires= — independent.
17
+ # Lifetime = user session / device lifetime. Outlives main brand service
18
+ # restarts because this unit has no After/Requires on it — independent.
19
+ # Known shared-user gap: the `maxy-pty` session name is identical across
20
+ # brands, so two brands running as the same linux user share one tmux
21
+ # session. Tracked in Task 663; structurally removed by Task 660 (per-user
22
+ # linux brand isolation).
18
23
  # -x 200 -y 50 initial geometry; xterm.js fit-addon drives runtime resizes.
19
- ExecStart=/usr/local/bin/ttyd -p 7681 -i 127.0.0.1 -W tmux new-session -A -s maxy-pty -x 200 -y 50
24
+ ExecStart=/usr/local/bin/ttyd -p __TTYD_PORT__ -i 127.0.0.1 -W tmux new-session -A -s maxy-pty -x 200 -y 50
20
25
  Environment=TERM=xterm-256color
21
26
  Restart=always
22
27
  RestartSec=2
@@ -8,7 +8,7 @@ import {
8
8
  vncLog
9
9
  } from "./chunk-3RBKKDHC.js";
10
10
 
11
- // server/maxy-edge.ts
11
+ // server/edge.ts
12
12
  import { createServer, request as httpRequest } from "http";
13
13
  import { createConnection as createConnection3 } from "net";
14
14
  import { readFileSync, existsSync, watchFile } from "fs";
@@ -628,7 +628,7 @@ Content-Length: 0\r
628
628
  socket.destroy();
629
629
  }
630
630
 
631
- // server/maxy-edge.ts
631
+ // server/edge.ts
632
632
  var PLATFORM_ROOT = process.env.MAXY_PLATFORM_ROOT || "";
633
633
  var BRAND_JSON_PATH = PLATFORM_ROOT ? join(PLATFORM_ROOT, "config", "brand.json") : "";
634
634
  var BRAND = { configDir: ".maxy" };
@@ -359,4 +359,4 @@ ${JSON.stringify(u.input,null,2)}`,f=ae.get(r),p=o?void 0:f??t;return(0,V.jsx)(c
359
359
  \x1B[32m[terminal] reattached\x1B[0m\r
360
360
  `),console.info(`[terminal] reattached`)):(l.current=!0,h.current?.());try{n.fit(),e.readyState===WebSocket.OPEN&&e.send(wE(t.cols,t.rows))}catch{}},onClose:e=>{e!==1e3&&l.current&&t.write(`\r
361
361
  \x1B[33m[terminal] server restart detected — reconnecting…\x1B[0m\r
362
- `)},onUnavailable:()=>{_.current?.()}});c.current=r;let i=t.onData(e=>{let t=c.current?.getSocket();!t||t.readyState!==WebSocket.OPEN||t.send(CE(e))}),v=()=>{try{n.fit();let e=c.current?.getSocket();e&&e.readyState===WebSocket.OPEN&&e.send(wE(t.cols,t.rows))}catch{}};return window.addEventListener(`resize`,v),()=>{window.removeEventListener(`resize`,v);try{i.dispose()}catch{}r.dispose();try{t.dispose()}catch{}o.current=null,s.current=null,c.current=null,f.current=0,p.current=0,m.current=0,l.current=!1,d.current=!1,u.current=!1}},[e]),(0,V.jsx)(`div`,{ref:a,className:`remote-terminal`,style:{width:`100%`,height:`100%`,minHeight:360,background:`#0a0a0a`}})});function DE(e){let{show:t,onClose:n,conversationsLoading:r,conversationsError:a,conversationsList:o,setConversationsList:s,sessionKey:c,isStreaming:l,resetConversation:u,setMessages:d,setConversationId:f}=e,[p,m]=(0,B.useState)(null),[g,v]=(0,B.useState)(null),[y,b]=(0,B.useState)(``),[x,S]=(0,B.useState)(!1),C=(0,B.useRef)(null);(0,B.useEffect)(()=>{t||(m(null),v(null),b(``),S(!1))},[t]),(0,B.useEffect)(()=>{g?.phase===`editing`&&(C.current?.focus(),C.current?.select())},[g]);function w(){m(null),v(null),b(``),S(!1)}async function T(e){if(c){w(),m(e),v({phase:`loading`});try{let t=await fetch(`/api/admin/sessions/${encodeURIComponent(e)}/label?session_key=${encodeURIComponent(c)}`,{method:`POST`});if(!t.ok){v({phase:`error`,reason:(await t.json().catch(()=>({}))).error||`Request failed`});return}let n=await t.json();n.label?(v({phase:`editing`}),b(n.label)):v({phase:`error`,reason:`Not enough message content to suggest a name`})}catch(e){console.error(`[admin-modal] label suggestion failed:`,e),v({phase:`error`,reason:e instanceof Error?e.message:String(e)})}}}async function E(e){if(!(!c||!y.trim()||x)){S(!0);try{let t=await fetch(`/api/admin/sessions/${encodeURIComponent(e)}/label`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({label:y.trim(),session_key:c})});if(!t.ok){v({phase:`error`,reason:(await t.json().catch(()=>({}))).error||`Rename failed`}),S(!1);return}s(t=>t.map(t=>t.conversationId===e?{...t,name:y.trim()}:t)),w()}catch(e){console.error(`[admin-modal] label rename failed:`,e),v({phase:`error`,reason:e instanceof Error?e.message:String(e)}),S(!1)}}}return t?(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:n,children:(0,V.jsxs)(`div`,{className:`claude-info-modal conversations-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(ze,{size:12}),`Conversations`,(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:n,children:`✕`})]}),(0,V.jsxs)(`div`,{className:`conversations-list`,children:[r&&(0,V.jsxs)(`div`,{className:`conversations-empty`,children:[(0,V.jsx)(ie,{size:14,className:`spin`}),` Loading…`]}),a&&(0,V.jsx)(`div`,{className:`conversations-empty conversations-error`,children:a}),!r&&!a&&o.length===0&&(0,V.jsx)(`div`,{className:`conversations-empty`,children:`No recent conversations`}),!r&&!a&&o.map(e=>{let t=p===e.conversationId,r=t&&g?.phase===`loading`,a=t&&g?.phase===`editing`,o=t&&g?.phase===`error`;return(0,V.jsxs)(`div`,{className:`conversations-row`,children:[(0,V.jsxs)(`div`,{className:`conversations-row-info`,children:[r&&(0,V.jsxs)(`span`,{className:`conversations-row-name conversations-edit-loading`,children:[(0,V.jsx)(ie,{size:12,className:`spin`}),` Generating suggestion…`]}),a&&(0,V.jsxs)(`div`,{className:`conversations-edit-inline`,children:[(0,V.jsx)(`input`,{ref:C,type:`text`,className:`conversations-edit-input`,value:y,onChange:e=>b(e.target.value),onKeyDown:t=>{t.key===`Enter`&&y.trim()&&E(e.conversationId),t.key===`Escape`&&w()},maxLength:200}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-confirm`,title:`Confirm rename`,disabled:!y.trim()||x,onClick:()=>E(e.conversationId),children:x?(0,V.jsx)(ie,{size:13,className:`spin`}):(0,V.jsx)(h,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-cancel`,title:`Cancel`,onClick:w,children:(0,V.jsx)(fe,{size:13})})]}),o&&(0,V.jsxs)(`div`,{className:`conversations-edit-inline`,children:[(0,V.jsx)(`span`,{className:`conversations-edit-error`,children:g.reason}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-retry`,title:`Retry`,onClick:()=>T(e.conversationId),children:(0,V.jsx)(de,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-cancel`,title:`Cancel`,onClick:w,children:(0,V.jsx)(fe,{size:13})})]}),!t&&(0,V.jsxs)(V.Fragment,{children:[(0,V.jsx)(`span`,{className:`conversations-row-name`,children:e.name||e.conversationId.slice(0,12)+`…`}),(0,V.jsxs)(`span`,{className:`conversations-row-meta`,children:[(0,V.jsx)(`span`,{className:`conversations-row-id`,children:e.conversationId.slice(0,8)}),(0,V.jsxs)(`span`,{className:`conversations-row-time`,children:[(0,V.jsx)(Se,{size:10}),(()=>{try{let t=new Date(e.updatedAt),n=Date.now()-t.getTime();return n<6e4?`just now`:n<36e5?`${Math.floor(n/6e4)}m ago`:n<864e5?`${Math.floor(n/36e5)}h ago`:`${Math.floor(n/864e5)}d ago`}catch{return``}})()]})]})]})]}),(0,V.jsx)(`div`,{className:`conversations-row-actions`,children:!t&&(0,V.jsxs)(V.Fragment,{children:[(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-rename`,title:`Rename conversation`,onClick:()=>T(e.conversationId),children:(0,V.jsx)(qe,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-play`,title:`Resume conversation`,disabled:l,onClick:()=>{l||!c||(n(),u(),fetch(`/api/admin/sessions/${encodeURIComponent(e.conversationId)}/resume?session_key=${encodeURIComponent(c)}`,{method:`POST`}).then(e=>e.ok?e.json():Promise.reject(Error(`Failed to resume conversation`))).then(e=>{e.conversationId&&f(e.conversationId);let t=(e.messages??[]).map(e=>({role:e.role===`user`?`admin`:`maxy`,content:e.role===`user`?e.content:void 0,events:e.role===`assistant`?[{type:`text`,content:e.content}]:void 0,timestamp:new Date(e.createdAt).getTime()||0,historical:!0}));t.length>0&&d(t)}).catch(e=>{console.error(`[chat] conversation resume failed:`,e),d([{role:`maxy`,events:[{type:`text`,content:`Could not load conversation history.`}],timestamp:Date.now()-1,historical:!0}])}))},children:(0,V.jsx)(_,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-delete`,title:`Delete conversation`,onClick:()=>{s(t=>t.filter(t=>t.conversationId!==e.conversationId)),c&&fetch(`/api/admin/sessions/${encodeURIComponent(e.conversationId)}?session_key=${encodeURIComponent(c)}`,{method:`DELETE`}).catch(e=>console.error(`[chat] conversation delete failed:`,e))},children:(0,V.jsx)(i,{size:13})})]})})]},e.conversationId)})]})]})}):null}function OE({src:e,onClose:t}){return e?(0,V.jsxs)(`div`,{className:`attachment-lightbox`,onClick:t,children:[(0,V.jsx)(`button`,{className:`attachment-lightbox-close`,onClick:t,"aria-label":`Close`,children:`×`}),(0,V.jsx)(`img`,{src:e,alt:`Attachment`,onClick:e=>e.stopPropagation()})]}):null}function kE({show:e,onClose:t,onConfirm:n}){return e?(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:t,children:(0,V.jsxs)(`div`,{className:`claude-info-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(`span`,{children:`Compact conversation?`}),(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:t,"aria-label":`Close`,children:`✕`})]}),(0,V.jsx)(`div`,{className:`claude-info-section`,style:{padding:`12px 14px`,fontSize:`11px`,color:`var(--text-secondary)`},children:`Summarises the conversation history to free up context window space. The session continues with a compressed summary.`}),(0,V.jsxs)(`div`,{className:`claude-info-section`,style:{display:`flex`,gap:`8px`,padding:`10px 14px`},children:[(0,V.jsx)(T,{variant:`secondary`,size:`sm`,style:{background:`var(--accent)`,flex:1},onClick:()=>{t(),n()},children:`Yes, compact`}),(0,V.jsx)(T,{variant:`secondary`,size:`sm`,style:{flex:1},onClick:t,children:`No`})]})]})}):null}function AE(e){let{show:t,onClose:n,claudeInfo:r,messages:i,sessionElapsed:a,conversationId:o,contextModeHint:s,handleContextModeToggle:c}=e;if(!t)return null;let l=i.flatMap(e=>e.events?.filter(e=>e.type===`usage`)??[]),u=l.at(-1),d=u?.peak_request_pct==null?u?.context_window?Math.round((u.input_tokens+u.cache_creation_tokens+u.cache_read_tokens)/u.context_window*100):0:Math.round(u.peak_request_pct*100),f=l.reduce((e,t)=>e+t.input_tokens+t.cache_creation_tokens+t.cache_read_tokens+t.output_tokens,0),p=i.flatMap(e=>e.events?.filter(e=>e.type===`rate_limit`)??[]).at(-1),m=l.reduce((e,t)=>e+(t.total_cost_usd??0),0),h=r?.account?.subscriptionType,g=e=>{let t=e*1e3-Date.now();if(t<=0)return`now`;let n=Math.floor(t/36e5),r=Math.floor(t%36e5/6e4);return n>0?`${n}h ${r}m`:`${r}m`};return(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:n,children:(0,V.jsxs)(`div`,{className:`claude-info-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`claude-info-icon`}),(0,V.jsx)(`span`,{children:`Claude Code`}),(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:n,"aria-label":`Close`,children:`✕`})]}),(0,V.jsxs)(`div`,{className:`claude-info-section`,children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Version`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:r?r.version:`…`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Email`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:r?.account?.email??`…`})]})]}),(h||p||m>0)&&(0,V.jsxs)(`div`,{className:`claude-info-section`,children:[h&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Plan`}),(0,V.jsx)(`span`,{className:`claude-info-value`,style:{textTransform:`capitalize`},children:h})]}),p&&(0,V.jsxs)(V.Fragment,{children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Usage`}),(0,V.jsxs)(`span`,{className:`claude-info-value`,children:[Math.round(p.utilization*100),`%`]})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Resets in`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:g(p.resetsAt)})]}),p.isUsingOverage&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Overage`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:`Active`})]})]}),m>0&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Session cost`}),(0,V.jsxs)(`span`,{className:`claude-info-value`,children:[`$`,m<.01?m.toFixed(4):m.toFixed(2)]})]})]}),(0,V.jsxs)(`div`,{className:`claude-info-section`,children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Model`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:r?.model??`…`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Context mode`}),(0,V.jsxs)(`span`,{className:`claude-info-value claude-info-toggle`,role:`button`,tabIndex:0,onClick:c,onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),c())},children:[r?.contextMode??`…`,s?` (next turn)`:``]})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Context used`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:d>0?`${d}%`:`—`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Tokens`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:f>0?ct(f):`—`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Session`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:st(a)})]}),o&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Conversation`}),(0,V.jsx)(`span`,{className:`claude-info-value`,style:{fontFamily:`monospace`,fontSize:10},children:o.slice(0,8)})]})]})]})})}var jE=5e3;function ME({show:e,onClose:t,versionInfo:n,sessionKey:r,onUpgradeComplete:i}){let[a,o]=(0,B.useState)(!1),[s,c]=(0,B.useState)(!1),[l,u]=(0,B.useState)(null),d=(0,B.useRef)(null),f=(0,B.useRef)(null),p=n?.latest??null,m=(0,B.useCallback)(()=>{f.current&&=(clearInterval(f.current),null)},[]);(0,B.useEffect)(()=>{e||(m(),o(!1),c(!1),u(null))},[e,m]),(0,B.useEffect)(()=>m,[m]),(0,B.useEffect)(()=>{if(!(!e||!a||s||!p))return f.current=setInterval(async()=>{try{let e=await fetch(`/api/admin/version`,{cache:`no-store`});if(!e.ok)return;(await e.json()).installed===p&&(m(),c(!0),i(),setTimeout(()=>window.location.reload(),2e3))}catch{}},jE),m},[e,a,s,p,i,m]);let h=(0,B.useRef)(!1),g=(0,B.useCallback)(()=>{!p||a||(h.current=!0,o(!0),u(null),PE(d.current)&&(h.current=!1))},[p,a]),_=(0,B.useCallback)(()=>{h.current&&PE(d.current)&&(h.current=!1)},[]),v=(0,B.useCallback)(()=>{u("Terminal transport unavailable — check `systemctl --user status maxy-ttyd`")},[]);return e?(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:()=>{(!a||s)&&t()},children:(0,V.jsxs)(`div`,{className:`claude-info-modal update-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(le,{size:12}),`Software Update`,(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:t,children:`✕`})]}),(0,V.jsxs)(`div`,{className:`update-modal-body`,children:[(0,V.jsxs)(`div`,{className:`update-modal-versions`,children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Installed`}),(0,V.jsxs)(`span`,{className:`claude-info-value`,children:[`v`,n?.installed??`…`]})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Available`}),(0,V.jsxs)(`span`,{className:`claude-info-value update-version-new`,children:[`v`,p??`…`]})]})]}),l&&(0,V.jsx)(`div`,{className:`update-modal-launch-error`,role:`alert`,children:l}),!a&&!s&&(0,V.jsx)(`button`,{type:`button`,className:`update-modal-btn`,onClick:g,disabled:!p,children:l?`Try again`:`Upgrade`}),a&&!s&&(0,V.jsx)(`div`,{className:`update-modal-terminal`,style:{height:360,marginTop:10,background:`#0a0a0a`,borderRadius:4,overflow:`hidden`},children:(0,V.jsx)(EE,{ref:d,corrId:`upgrade-modal`,onReady:_,onUnavailable:v})}),s&&(0,V.jsxs)(`div`,{className:`update-modal-result`,children:[(0,V.jsx)(R,{size:14,className:`update-success-icon`}),(0,V.jsxs)(`span`,{children:[`Upgraded to v`,p,` — reloading…`]})]})]})]})}):null}var NE=`@rubytech/create-maxy`;function PE(e){return e?e.send(`sudo -v\nnpx -y ${NE}@latest\n`):!1}function FE({onClose:e}){ot();let[t,n]=(0,B.useState)(!1),r=(0,B.useRef)(null),i=(0,B.useRef)(null),a=(0,B.useCallback)(()=>{i.current&&=(clearInterval(i.current),null),r.current&&!r.current.closed&&r.current.close(),r.current=null,n(!1)},[]),o=(0,B.useCallback)(()=>{let t=`/vnc-popout.html?title=${encodeURIComponent(v.productName)}`,o=window.open(t,`maxy-vnc-overlay-popout`,`width=1024,height=768`);o&&(r.current=o,n(!0),i.current=setInterval(()=>{r.current?.closed&&(a(),e())},500))},[a,e]),s=(0,B.useCallback)(()=>{a(),e()},[a,e]);(0,B.useEffect)(()=>{if(t)return;let e=e=>{e.key===`Escape`&&s()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t,s]),(0,B.useEffect)(()=>()=>a(),[a]);let c=Lt({handlePopout:o,disabled:t});return t?(0,V.jsxs)(`div`,{className:`browser-overlay-popout`,children:[(0,V.jsx)(O,{className:`browser-viewer__icon`}),(0,V.jsx)(`span`,{className:`browser-viewer__title`,children:v.productName}),(0,V.jsx)(`span`,{className:`browser-viewer__popout-label`,children:`Popped out`}),(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:We,onClick:()=>{a()},"aria-label":`Pop back in`}),(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:fe,onClick:s,"aria-label":`Close`})]}):(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen`,children:[(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen__bar`,...c,children:[(0,V.jsx)(O,{className:`browser-viewer__icon`}),(0,V.jsx)(`span`,{className:`browser-viewer-fullscreen__title`,children:v.productName}),(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen__actions`,children:[(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:De,onClick:o,"aria-label":`Pop out`}),(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:fe,onClick:s,"aria-label":`Close`})]})]}),(0,V.jsx)(`iframe`,{src:`/vnc-viewer.html`,className:`browser-viewer-fullscreen__iframe`,title:v.productName})]})}function IE({onClose:e}){let t=(0,B.useCallback)(()=>{e()},[e]);return(0,B.useEffect)(()=>{let e=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t]),(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen`,children:[(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen__bar`,children:[(0,V.jsx)(Qe,{className:`browser-viewer__icon`}),(0,V.jsx)(`span`,{className:`browser-viewer-fullscreen__title`,children:v.productName}),(0,V.jsx)(`div`,{className:`browser-viewer-fullscreen__actions`,children:(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:fe,onClick:t,"aria-label":`Close`})})]}),(0,V.jsx)(`div`,{className:`browser-viewer-fullscreen__iframe`,style:{padding:8,background:`#0a0a0a`},children:(0,V.jsx)(EE,{corrId:`header-overlay`,onUnavailable:()=>{t()}})})]})}function LE(){ot();let e=(0,B.useRef)(null),[t,n]=(0,B.useState)(null),[r,a]=(0,B.useState)(!1),[o,s]=(0,B.useState)(!1),[c,u]=(0,B.useState)(``),[d,f]=(0,B.useState)(!1),[p,m]=(0,B.useState)(null),[g,_]=(0,B.useState)(!1),[y,b]=(0,B.useState)(null),[x,S]=(0,B.useState)(null),[w,T]=(0,B.useState)(!1),[E,D]=(0,B.useState)([]),[k,A]=(0,B.useState)(!1),[j,M]=(0,B.useState)(null),[N,P]=(0,B.useState)(null),[ee,F]=(0,B.useState)(!1),[te,I]=(0,B.useState)(null),[ne,ae]=(0,B.useState)(!1),[le,ue]=(0,B.useState)(!1),[de,pe]=(0,B.useState)(!1),me=(0,B.useRef)(!1),he=(0,B.useRef)(!1),ge=(0,B.useRef)(!1),_e=(0,B.useRef)(null),ve=l(),ye=lt(),R=ht(),be=wt(),z=Ct({sessionKey:R.sessionKey,setSessionKey:R.setSessionKey,setAppState:R.setAppState,setConversationId:R.setConversationId,startElapsedTimer:ye.startElapsedTimer,stopElapsedTimer:ye.stopElapsedTimer,resetTimerState:ye.resetTimerState,pausedElapsedRef:ye.pausedElapsedRef,expandAllDefaultRef:R.expandAllDefaultRef,setExpandAll:R.setExpandAll,getPendingFiles:()=>be.pendingFiles,clearPendingFiles:be.clearFiles,inputRef:e});(0,B.useEffect)(()=>{u(window.location.hostname.startsWith(`admin.`)?window.location.origin.replace(`admin.`,`public.`):window.location.origin)},[]),(0,B.useEffect)(()=>{function e(e){e.key===`Escape`&&(t?n(null):ve.selectionMode&&ve.exitSelection())}return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t,ve.selectionMode]),(0,B.useEffect)(()=>{(R.appState===`set-pin`||R.appState===`enter-pin`)&&setTimeout(()=>R.pinInputRef.current?.focus(),100),R.appState===`chat`&&setTimeout(()=>e.current?.focus(),100)},[R.appState,z.greetingGeneration]),(0,B.useEffect)(()=>{if(R.appState!==`chat`)return;history.pushState({maxyChat:!0},``);function e(){console.debug(`[history-guard] popstate absorbed`),history.pushState({maxyChat:!0},``)}return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)},[R.appState]);let xe=(0,B.useRef)(z.greetingGeneration);return(0,B.useEffect)(()=>{z.greetingGeneration!==xe.current&&(xe.current=z.greetingGeneration,R.onboardingComplete===!1&&(me.current=!1))},[z.greetingGeneration,R.onboardingComplete]),(0,B.useEffect)(()=>{R.appState===`chat`&&R.onboardingComplete===!1&&(me.current||(me.current=!0,z.sendSystemPrompt(`[Begin onboarding.]`)))},[R.appState,R.onboardingComplete,z.greetingGeneration]),(0,B.useEffect)(()=>{if(!d)return;let e=!1;return fetch(`/api/admin/version`).then(e=>e.json()).then(t=>{e||P(t)}).catch(e=>{console.error(`[admin/version] menu-open client fetch failed:`,e)}),()=>{e=!0}},[d]),(0,B.useEffect)(()=>{if(R.appState!==`chat`){he.current=!1,ge.current=!1;return}if(he.current)return;he.current=!0;let e=!1;return fetch(`/api/admin/version`).then(e=>e.json()).then(t=>{e||(P(t),!(!t.updateAvailable||ge.current)&&(I(t),F(!0),fetch(`/api/admin/version/alert-surfaced`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({installed:t.installed,latest:t.latest})}).catch(e=>{console.error(`[admin/version] alert-surfaced signal failed:`,e)})))}).catch(e=>{console.error(`[admin/version] session-start client fetch failed:`,e)}),()=>{e=!0}},[R.appState]),(0,B.useEffect)(()=>{if(!d){m(null),b(null),S(null),P(null);return}function e(e){_e.current&&!_e.current.contains(e.target)&&f(!1)}return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[d]),(0,B.useEffect)(()=>{if(typeof BroadcastChannel>`u`)return;let e=new BroadcastChannel(`platform-onboarding`);return e.onmessage=e=>{e.data?.type===`remote-password-set`&&z.sendMessageRef.current(`I've set the remote password`)},()=>e.close()},[]),R.appState===`loading`?(0,V.jsx)(`div`,{className:`chat-page admin-page`,children:(0,V.jsx)(`header`,{className:`chat-header`,children:(0,V.jsx)(`img`,{src:re,alt:v.productName,className:`chat-logo`})})}):R.appState===`set-pin`?(0,V.jsx)(Et,{pin:R.pin,setPin:R.setPin,confirmPin:R.confirmPin,setConfirmPin:R.setConfirmPin,showPin:R.showPin,setShowPin:R.setShowPin,pinLoading:R.pinLoading,pinError:R.pinError,pinInputRef:R.pinInputRef,confirmPinInputRef:R.confirmPinInputRef,setPinFormRef:R.setPinFormRef,onSubmit:R.handleSetPin}):R.appState===`onboarding-choice`?(0,V.jsx)(At,{setAppState:R.setAppState,setOnboardingComplete:R.setOnboardingComplete}):R.appState===`connect-claude`?(0,V.jsx)(kt,{authPolling:R.authPolling,setAuthPolling:R.setAuthPolling,authLoading:R.authLoading,setAuthLoading:R.setAuthLoading,pinError:R.pinError,setPinError:R.setPinError,setAppState:R.setAppState}):R.appState===`enter-pin`?(0,V.jsx)(Dt,{pin:R.pin,setPin:R.setPin,showPin:R.showPin,setShowPin:R.setShowPin,pinLoading:R.pinLoading,pinError:R.pinError,pinInputRef:R.pinInputRef,onSubmit:R.handleLogin,onChangePin:R.handleChangePin}):R.appState===`account-picker`?(0,V.jsx)(Ot,{accounts:R.accounts,loading:R.accountPickerLoading,error:R.pinError,onSelect:R.handleAccountSelect}):(0,V.jsx)(C,{value:{onShowVnc:()=>ae(!0)},children:(0,V.jsxs)(`div`,{className:`chat-page admin-page`,children:[(0,V.jsxs)(`header`,{className:`chat-header`,children:[(0,V.jsx)(`img`,{src:re,alt:v.productName,className:`chat-logo`}),(!v.logoContainsName||R.businessName)&&(0,V.jsx)(`div`,{children:(0,V.jsx)(`h1`,{className:`chat-tagline`,children:R.businessName||v.productName})}),(0,V.jsxs)(`div`,{className:`chat-burger-wrap`,ref:_e,children:[(0,V.jsx)(`button`,{type:`button`,className:`chat-burger`,onClick:()=>f(e=>!e),"aria-label":`Menu`,"aria-haspopup":`true`,"aria-expanded":d,children:(0,V.jsx)(se,{size:20})}),d&&(0,V.jsxs)(`div`,{className:`chat-menu`,children:[(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:async()=>{if(!(ne||le)){ue(!0),f(!1);try{let e=await(await fetch(`/api/admin/browser/launch`,{method:`POST`})).json().catch(()=>({ok:!1,error:`Invalid response`}));if(!e.ok)throw Error(e.error??`Failed to launch browser`);ae(!0)}catch(e){console.error(`[browser] Launch failed:`,e),alert(e instanceof Error?e.message:`Failed to launch browser`)}finally{ue(!1)}}},disabled:le,children:[(0,V.jsx)(O,{size:14}),` Browser`,le&&(0,V.jsx)(ie,{size:12,className:`spin`})]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{de||(f(!1),pe(!0))},children:[(0,V.jsx)(Qe,{size:14}),` Terminal`]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{f(!1),window.location.href=`/data`},children:[(0,V.jsx)(oe,{size:14}),` Data`]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{f(!1),window.location.href=`/graph`},children:[(0,V.jsx)(ce,{size:14}),` Graph`]}),(0,V.jsx)(`div`,{className:`chat-menu-divider`}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:async()=>{if(p!==null){m(null),S(null);return}_(!0),b(null);try{let e=await fetch(`/api/admin/agents`);if(!e.ok)throw Error(`Failed to load agents`);m((await e.json()).agents??[])}catch(e){console.error(`[admin] agent list failed:`,e),b(e instanceof Error?e.message:String(e))}finally{_(!1)}},children:[(0,V.jsx)(De,{size:14}),` Public`,g&&(0,V.jsx)(ie,{size:12,className:`spin`})]}),p!==null&&(0,V.jsxs)(`div`,{className:`chat-menu-agents`,children:[y&&(0,V.jsx)(`span`,{className:`chat-menu-agent-error`,children:y}),p.length===0&&!y&&(0,V.jsx)(`span`,{className:`chat-menu-agent-empty`,children:`No public agents configured`}),p.map(e=>(0,V.jsxs)(`div`,{className:`chat-menu-item chat-menu-agent-item`,children:[(0,V.jsx)(`span`,{className:`agent-status-dot ${e.status}`}),(0,V.jsxs)(`span`,{className:`agent-text`,children:[(0,V.jsx)(`span`,{className:`agent-display-name`,children:e.displayName}),(0,V.jsxs)(`span`,{className:`agent-slug`,children:[`/`,e.slug]})]}),x===e.slug?(0,V.jsxs)(`span`,{className:`agent-actions agent-confirm`,children:[(0,V.jsx)(`button`,{type:`button`,className:`agent-action-btn agent-confirm-yes`,title:`Confirm delete`,onClick:()=>{S(null),m(t=>t?.filter(t=>t.slug!==e.slug)??null),fetch(`/api/admin/agents/${encodeURIComponent(e.slug)}`,{method:`DELETE`}).catch(e=>console.error(`[admin/agents] delete failed:`,e))},children:(0,V.jsx)(h,{size:12})}),(0,V.jsx)(`button`,{type:`button`,className:`agent-action-btn agent-confirm-no`,title:`Cancel`,onClick:()=>S(null),children:(0,V.jsx)(fe,{size:12})})]}):(0,V.jsxs)(`span`,{className:`agent-actions`,children:[(0,V.jsx)(`a`,{href:`${c}/${e.slug}`,target:`_blank`,rel:`noopener noreferrer`,className:`agent-action-btn`,title:`Open agent`,onClick:()=>f(!1),children:(0,V.jsx)(De,{size:12})}),(0,V.jsx)(`button`,{type:`button`,className:`agent-action-btn agent-delete-btn`,title:`Delete agent`,onClick:()=>S(e.slug),children:(0,V.jsx)(i,{size:12})})]})]},e.slug))]}),(0,V.jsx)(`div`,{className:`chat-menu-divider`}),N?.updateAvailable?(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-version`,onClick:()=>{N.latest&&(I(N),f(!1),F(!0))},children:[(0,V.jsx)(Pe,{size:14}),(0,V.jsxs)(`span`,{className:`version-installed`,children:[`v`,N.installed,(0,V.jsx)(`span`,{className:`version-update-dot`})]})]}):(0,V.jsxs)(`div`,{className:`chat-menu-version chat-menu-version-passive`,children:[(0,V.jsx)(Pe,{size:14}),(0,V.jsxs)(`span`,{className:`version-installed`,children:[N?`v${N.installed}`:`…`,N&&!N.updateAvailable&&(0,V.jsx)(`span`,{className:`version-uptodate-dot`})]})]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{f(!1),R.handleLogout(),z.setMessages([])},children:[(0,V.jsx)(L,{size:14}),` Log out`]})]})]})]}),(0,V.jsxs)(`div`,{className:`chat-content`,children:[(0,V.jsx)(uE,{messages:z.messages,isStreaming:z.isStreaming,elapsedSeconds:ye.elapsedSeconds,sessionTurnStart:ye.sessionTurnStart,expandAll:R.expandAll,setExpandAll:R.setExpandAll,conversationCompacted:z.conversationCompacted,isCompacting:z.isCompacting,setShowCompactConfirm:s,handleComponentSubmit:z.handleComponentSubmit,effectiveSubmitted:z.effectiveSubmitted,selectionMode:ve.selectionMode,selectedItems:ve.selectedItems,toggleSelectItem:ve.toggleSelectItem,sessionKey:R.sessionKey,setLightboxSrc:n,messageQueue:z.messageQueue,messageQueueRef:z.messageQueueRef,setQueue:z.setQueue,sendMessage:z.sendMessage,appState:R.appState,onboardingComplete:R.onboardingComplete}),(0,V.jsx)(dE,{offer:z.topicChangeOffer,onFreshStart:z.handleTopicFreshStart,onContinue:z.handleTopicContinue,disabled:z.isStreaming}),(0,V.jsx)(mE,{selectionMode:ve.selectionMode,selectedItems:ve.selectedItems,copyToast:ve.copyToast||null,exitSelection:ve.exitSelection,enterSelection:ve.enterSelection,copySelected:ve.copySelected,showCopyToast:ve.showCopyToast,input:z.input,setInput:z.setInput,inputRef:e,pendingFiles:be.pendingFiles,setPendingFiles:be.setPendingFiles,isDragOver:be.isDragOver,attachError:be.attachError,fileInputRef:be.fileInputRef,addFiles:be.addFiles,removeFile:be.removeFile,onDragOver:be.onDragOver,onDragLeave:be.onDragLeave,onDrop:be.onDrop,isStreaming:z.isStreaming,wasPaused:z.wasPaused,messages:z.messages,messageQueueRef:z.messageQueueRef,setQueue:z.setQueue,sendMessage:z.sendMessage,stopStreaming:z.stopStreaming,disconnecting:R.disconnecting,handleDisconnect:R.handleDisconnect,claudeInfo:R.claudeInfo,setClaudeInfo:R.setClaudeInfo,setShowClaudeInfo:a,conversationId:R.conversationId,onNewConversation:z.startNewConversation,newConversationDisabled:z.isStreaming||z.isCompacting,onOpenConversations:async()=>{T(!0),A(!0),M(null);try{let e=await fetch(`/api/admin/sessions?session_key=${encodeURIComponent(R.sessionKey)}`);if(!e.ok)throw Error(`Failed to load conversations`);D((await e.json()).sessions??[])}catch(e){console.error(`[admin] conversation list failed:`,e),M(e instanceof Error?e.message:String(e))}finally{A(!1)}}})]}),ve.copyToast&&(0,V.jsx)(`span`,{className:`copy-toast${ve.copyToast===`failed`?` copy-toast-failed`:``}`,children:ve.copyToast===`copied`?`Copied`:`Copy failed`}),(0,V.jsx)(DE,{show:w,onClose:()=>T(!1),conversationsLoading:k,conversationsError:j,conversationsList:E,setConversationsList:D,sessionKey:R.sessionKey,isStreaming:z.isStreaming,resetConversation:z.resetConversation,setMessages:z.setMessages,setConversationId:R.setConversationId}),(0,V.jsx)(OE,{src:t,onClose:()=>n(null)}),(0,V.jsx)(kE,{show:o,onClose:()=>s(!1),onConfirm:z.handleCompactNow}),(0,V.jsx)(AE,{show:r,onClose:()=>{a(!1),R.setClaudeInfo(null)},claudeInfo:R.claudeInfo,messages:z.messages,sessionElapsed:ye.sessionElapsed,conversationId:R.conversationId,contextModeHint:R.contextModeHint,handleContextModeToggle:R.handleContextModeToggle}),(0,V.jsx)(ME,{show:ee,onClose:()=>{F(!1),ge.current=!0},versionInfo:te,sessionKey:R.sessionKey,onUpgradeComplete:()=>P(null)}),ne&&(0,V.jsx)(FE,{onClose:()=>ae(!1)}),de&&(0,V.jsx)(IE,{onClose:()=>pe(!1)})]})})}(0,at.createRoot)(document.getElementById(`root`)).render((0,V.jsx)(LE,{}));
362
+ `)},onUnavailable:()=>{_.current?.()}});c.current=r;let i=t.onData(e=>{let t=c.current?.getSocket();!t||t.readyState!==WebSocket.OPEN||t.send(CE(e))}),v=()=>{try{n.fit();let e=c.current?.getSocket();e&&e.readyState===WebSocket.OPEN&&e.send(wE(t.cols,t.rows))}catch{}};return window.addEventListener(`resize`,v),()=>{window.removeEventListener(`resize`,v);try{i.dispose()}catch{}r.dispose();try{t.dispose()}catch{}o.current=null,s.current=null,c.current=null,f.current=0,p.current=0,m.current=0,l.current=!1,d.current=!1,u.current=!1}},[e]),(0,V.jsx)(`div`,{ref:a,className:`remote-terminal`,style:{width:`100%`,height:`100%`,minHeight:360,background:`#0a0a0a`}})});function DE(e){let{show:t,onClose:n,conversationsLoading:r,conversationsError:a,conversationsList:o,setConversationsList:s,sessionKey:c,isStreaming:l,resetConversation:u,setMessages:d,setConversationId:f}=e,[p,m]=(0,B.useState)(null),[g,v]=(0,B.useState)(null),[y,b]=(0,B.useState)(``),[x,S]=(0,B.useState)(!1),C=(0,B.useRef)(null);(0,B.useEffect)(()=>{t||(m(null),v(null),b(``),S(!1))},[t]),(0,B.useEffect)(()=>{g?.phase===`editing`&&(C.current?.focus(),C.current?.select())},[g]);function w(){m(null),v(null),b(``),S(!1)}async function T(e){if(c){w(),m(e),v({phase:`loading`});try{let t=await fetch(`/api/admin/sessions/${encodeURIComponent(e)}/label?session_key=${encodeURIComponent(c)}`,{method:`POST`});if(!t.ok){v({phase:`error`,reason:(await t.json().catch(()=>({}))).error||`Request failed`});return}let n=await t.json();n.label?(v({phase:`editing`}),b(n.label)):v({phase:`error`,reason:`Not enough message content to suggest a name`})}catch(e){console.error(`[admin-modal] label suggestion failed:`,e),v({phase:`error`,reason:e instanceof Error?e.message:String(e)})}}}async function E(e){if(!(!c||!y.trim()||x)){S(!0);try{let t=await fetch(`/api/admin/sessions/${encodeURIComponent(e)}/label`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({label:y.trim(),session_key:c})});if(!t.ok){v({phase:`error`,reason:(await t.json().catch(()=>({}))).error||`Rename failed`}),S(!1);return}s(t=>t.map(t=>t.conversationId===e?{...t,name:y.trim()}:t)),w()}catch(e){console.error(`[admin-modal] label rename failed:`,e),v({phase:`error`,reason:e instanceof Error?e.message:String(e)}),S(!1)}}}return t?(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:n,children:(0,V.jsxs)(`div`,{className:`claude-info-modal conversations-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(ze,{size:12}),`Conversations`,(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:n,children:`✕`})]}),(0,V.jsxs)(`div`,{className:`conversations-list`,children:[r&&(0,V.jsxs)(`div`,{className:`conversations-empty`,children:[(0,V.jsx)(ie,{size:14,className:`spin`}),` Loading…`]}),a&&(0,V.jsx)(`div`,{className:`conversations-empty conversations-error`,children:a}),!r&&!a&&o.length===0&&(0,V.jsx)(`div`,{className:`conversations-empty`,children:`No recent conversations`}),!r&&!a&&o.map(e=>{let t=p===e.conversationId,r=t&&g?.phase===`loading`,a=t&&g?.phase===`editing`,o=t&&g?.phase===`error`;return(0,V.jsxs)(`div`,{className:`conversations-row`,children:[(0,V.jsxs)(`div`,{className:`conversations-row-info`,children:[r&&(0,V.jsxs)(`span`,{className:`conversations-row-name conversations-edit-loading`,children:[(0,V.jsx)(ie,{size:12,className:`spin`}),` Generating suggestion…`]}),a&&(0,V.jsxs)(`div`,{className:`conversations-edit-inline`,children:[(0,V.jsx)(`input`,{ref:C,type:`text`,className:`conversations-edit-input`,value:y,onChange:e=>b(e.target.value),onKeyDown:t=>{t.key===`Enter`&&y.trim()&&E(e.conversationId),t.key===`Escape`&&w()},maxLength:200}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-confirm`,title:`Confirm rename`,disabled:!y.trim()||x,onClick:()=>E(e.conversationId),children:x?(0,V.jsx)(ie,{size:13,className:`spin`}):(0,V.jsx)(h,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-cancel`,title:`Cancel`,onClick:w,children:(0,V.jsx)(fe,{size:13})})]}),o&&(0,V.jsxs)(`div`,{className:`conversations-edit-inline`,children:[(0,V.jsx)(`span`,{className:`conversations-edit-error`,children:g.reason}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-retry`,title:`Retry`,onClick:()=>T(e.conversationId),children:(0,V.jsx)(de,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-cancel`,title:`Cancel`,onClick:w,children:(0,V.jsx)(fe,{size:13})})]}),!t&&(0,V.jsxs)(V.Fragment,{children:[(0,V.jsx)(`span`,{className:`conversations-row-name`,children:e.name||e.conversationId.slice(0,12)+`…`}),(0,V.jsxs)(`span`,{className:`conversations-row-meta`,children:[(0,V.jsx)(`span`,{className:`conversations-row-id`,children:e.conversationId.slice(0,8)}),(0,V.jsxs)(`span`,{className:`conversations-row-time`,children:[(0,V.jsx)(Se,{size:10}),(()=>{try{let t=new Date(e.updatedAt),n=Date.now()-t.getTime();return n<6e4?`just now`:n<36e5?`${Math.floor(n/6e4)}m ago`:n<864e5?`${Math.floor(n/36e5)}h ago`:`${Math.floor(n/864e5)}d ago`}catch{return``}})()]})]})]})]}),(0,V.jsx)(`div`,{className:`conversations-row-actions`,children:!t&&(0,V.jsxs)(V.Fragment,{children:[(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-rename`,title:`Rename conversation`,onClick:()=>T(e.conversationId),children:(0,V.jsx)(qe,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-play`,title:`Resume conversation`,disabled:l,onClick:()=>{l||!c||(n(),u(),fetch(`/api/admin/sessions/${encodeURIComponent(e.conversationId)}/resume?session_key=${encodeURIComponent(c)}`,{method:`POST`}).then(e=>e.ok?e.json():Promise.reject(Error(`Failed to resume conversation`))).then(e=>{e.conversationId&&f(e.conversationId);let t=(e.messages??[]).map(e=>({role:e.role===`user`?`admin`:`maxy`,content:e.role===`user`?e.content:void 0,events:e.role===`assistant`?[{type:`text`,content:e.content}]:void 0,timestamp:new Date(e.createdAt).getTime()||0,historical:!0}));t.length>0&&d(t)}).catch(e=>{console.error(`[chat] conversation resume failed:`,e),d([{role:`maxy`,events:[{type:`text`,content:`Could not load conversation history.`}],timestamp:Date.now()-1,historical:!0}])}))},children:(0,V.jsx)(_,{size:13})}),(0,V.jsx)(`button`,{type:`button`,className:`conversations-action conversations-delete`,title:`Delete conversation`,onClick:()=>{s(t=>t.filter(t=>t.conversationId!==e.conversationId)),c&&fetch(`/api/admin/sessions/${encodeURIComponent(e.conversationId)}?session_key=${encodeURIComponent(c)}`,{method:`DELETE`}).catch(e=>console.error(`[chat] conversation delete failed:`,e))},children:(0,V.jsx)(i,{size:13})})]})})]},e.conversationId)})]})]})}):null}function OE({src:e,onClose:t}){return e?(0,V.jsxs)(`div`,{className:`attachment-lightbox`,onClick:t,children:[(0,V.jsx)(`button`,{className:`attachment-lightbox-close`,onClick:t,"aria-label":`Close`,children:`×`}),(0,V.jsx)(`img`,{src:e,alt:`Attachment`,onClick:e=>e.stopPropagation()})]}):null}function kE({show:e,onClose:t,onConfirm:n}){return e?(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:t,children:(0,V.jsxs)(`div`,{className:`claude-info-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(`span`,{children:`Compact conversation?`}),(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:t,"aria-label":`Close`,children:`✕`})]}),(0,V.jsx)(`div`,{className:`claude-info-section`,style:{padding:`12px 14px`,fontSize:`11px`,color:`var(--text-secondary)`},children:`Summarises the conversation history to free up context window space. The session continues with a compressed summary.`}),(0,V.jsxs)(`div`,{className:`claude-info-section`,style:{display:`flex`,gap:`8px`,padding:`10px 14px`},children:[(0,V.jsx)(T,{variant:`secondary`,size:`sm`,style:{background:`var(--accent)`,flex:1},onClick:()=>{t(),n()},children:`Yes, compact`}),(0,V.jsx)(T,{variant:`secondary`,size:`sm`,style:{flex:1},onClick:t,children:`No`})]})]})}):null}function AE(e){let{show:t,onClose:n,claudeInfo:r,messages:i,sessionElapsed:a,conversationId:o,contextModeHint:s,handleContextModeToggle:c}=e;if(!t)return null;let l=i.flatMap(e=>e.events?.filter(e=>e.type===`usage`)??[]),u=l.at(-1),d=u?.peak_request_pct==null?u?.context_window?Math.round((u.input_tokens+u.cache_creation_tokens+u.cache_read_tokens)/u.context_window*100):0:Math.round(u.peak_request_pct*100),f=l.reduce((e,t)=>e+t.input_tokens+t.cache_creation_tokens+t.cache_read_tokens+t.output_tokens,0),p=i.flatMap(e=>e.events?.filter(e=>e.type===`rate_limit`)??[]).at(-1),m=l.reduce((e,t)=>e+(t.total_cost_usd??0),0),h=r?.account?.subscriptionType,g=e=>{let t=e*1e3-Date.now();if(t<=0)return`now`;let n=Math.floor(t/36e5),r=Math.floor(t%36e5/6e4);return n>0?`${n}h ${r}m`:`${r}m`};return(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:n,children:(0,V.jsxs)(`div`,{className:`claude-info-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`claude-info-icon`}),(0,V.jsx)(`span`,{children:`Claude Code`}),(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:n,"aria-label":`Close`,children:`✕`})]}),(0,V.jsxs)(`div`,{className:`claude-info-section`,children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Version`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:r?r.version:`…`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Email`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:r?.account?.email??`…`})]})]}),(h||p||m>0)&&(0,V.jsxs)(`div`,{className:`claude-info-section`,children:[h&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Plan`}),(0,V.jsx)(`span`,{className:`claude-info-value`,style:{textTransform:`capitalize`},children:h})]}),p&&(0,V.jsxs)(V.Fragment,{children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Usage`}),(0,V.jsxs)(`span`,{className:`claude-info-value`,children:[Math.round(p.utilization*100),`%`]})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Resets in`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:g(p.resetsAt)})]}),p.isUsingOverage&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Overage`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:`Active`})]})]}),m>0&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Session cost`}),(0,V.jsxs)(`span`,{className:`claude-info-value`,children:[`$`,m<.01?m.toFixed(4):m.toFixed(2)]})]})]}),(0,V.jsxs)(`div`,{className:`claude-info-section`,children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Model`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:r?.model??`…`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Context mode`}),(0,V.jsxs)(`span`,{className:`claude-info-value claude-info-toggle`,role:`button`,tabIndex:0,onClick:c,onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),c())},children:[r?.contextMode??`…`,s?` (next turn)`:``]})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Context used`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:d>0?`${d}%`:`—`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Tokens`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:f>0?ct(f):`—`})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Session`}),(0,V.jsx)(`span`,{className:`claude-info-value`,children:st(a)})]}),o&&(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Conversation`}),(0,V.jsx)(`span`,{className:`claude-info-value`,style:{fontFamily:`monospace`,fontSize:10},children:o.slice(0,8)})]})]})]})})}var jE=5e3;function ME({show:e,onClose:t,versionInfo:n,sessionKey:r,onUpgradeComplete:i}){let[a,o]=(0,B.useState)(!1),[s,c]=(0,B.useState)(!1),[l,u]=(0,B.useState)(null),d=(0,B.useRef)(null),f=(0,B.useRef)(null),p=n?.latest??null,m=(0,B.useCallback)(()=>{f.current&&=(clearInterval(f.current),null)},[]);(0,B.useEffect)(()=>{e||(m(),o(!1),c(!1),u(null))},[e,m]),(0,B.useEffect)(()=>m,[m]),(0,B.useEffect)(()=>{if(!(!e||!a||s||!p))return f.current=setInterval(async()=>{try{let e=await fetch(`/api/admin/version`,{cache:`no-store`});if(!e.ok)return;(await e.json()).installed===p&&(m(),c(!0),i(),setTimeout(()=>window.location.reload(),2e3))}catch{}},jE),m},[e,a,s,p,i,m]);let h=(0,B.useRef)(!1),g=(0,B.useCallback)(()=>{!p||a||(h.current=!0,o(!0),u(null),PE(d.current)&&(h.current=!1))},[p,a]),_=(0,B.useCallback)(()=>{h.current&&PE(d.current)&&(h.current=!1)},[]),y=(0,B.useCallback)(()=>{u(`Terminal transport unavailable — check \`systemctl --user status ${v.hostname}-ttyd\``)},[]);return e?(0,V.jsx)(`div`,{className:`claude-info-overlay`,onClick:()=>{(!a||s)&&t()},children:(0,V.jsxs)(`div`,{className:`claude-info-modal update-modal`,onClick:e=>e.stopPropagation(),children:[(0,V.jsxs)(`div`,{className:`claude-info-header`,children:[(0,V.jsx)(le,{size:12}),`Software Update`,(0,V.jsx)(`button`,{className:`claude-info-close`,onClick:t,children:`✕`})]}),(0,V.jsxs)(`div`,{className:`update-modal-body`,children:[(0,V.jsxs)(`div`,{className:`update-modal-versions`,children:[(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Installed`}),(0,V.jsxs)(`span`,{className:`claude-info-value`,children:[`v`,n?.installed??`…`]})]}),(0,V.jsxs)(`div`,{className:`claude-info-row`,children:[(0,V.jsx)(`span`,{className:`claude-info-label`,children:`Available`}),(0,V.jsxs)(`span`,{className:`claude-info-value update-version-new`,children:[`v`,p??`…`]})]})]}),l&&(0,V.jsx)(`div`,{className:`update-modal-launch-error`,role:`alert`,children:l}),!a&&!s&&(0,V.jsx)(`button`,{type:`button`,className:`update-modal-btn`,onClick:g,disabled:!p,children:l?`Try again`:`Upgrade`}),a&&!s&&(0,V.jsx)(`div`,{className:`update-modal-terminal`,style:{height:360,marginTop:10,background:`#0a0a0a`,borderRadius:4,overflow:`hidden`},children:(0,V.jsx)(EE,{ref:d,corrId:`upgrade-modal`,onReady:_,onUnavailable:y})}),s&&(0,V.jsxs)(`div`,{className:`update-modal-result`,children:[(0,V.jsx)(R,{size:14,className:`update-success-icon`}),(0,V.jsxs)(`span`,{children:[`Upgraded to v`,p,` — reloading…`]})]})]})]})}):null}var NE=`@rubytech/create-maxy`;function PE(e){return e?e.send(`sudo -v\nnpx -y ${NE}@latest\n`):!1}function FE({onClose:e}){ot();let[t,n]=(0,B.useState)(!1),r=(0,B.useRef)(null),i=(0,B.useRef)(null),a=(0,B.useCallback)(()=>{i.current&&=(clearInterval(i.current),null),r.current&&!r.current.closed&&r.current.close(),r.current=null,n(!1)},[]),o=(0,B.useCallback)(()=>{let t=`/vnc-popout.html?title=${encodeURIComponent(v.productName)}`,o=window.open(t,`maxy-vnc-overlay-popout`,`width=1024,height=768`);o&&(r.current=o,n(!0),i.current=setInterval(()=>{r.current?.closed&&(a(),e())},500))},[a,e]),s=(0,B.useCallback)(()=>{a(),e()},[a,e]);(0,B.useEffect)(()=>{if(t)return;let e=e=>{e.key===`Escape`&&s()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t,s]),(0,B.useEffect)(()=>()=>a(),[a]);let c=Lt({handlePopout:o,disabled:t});return t?(0,V.jsxs)(`div`,{className:`browser-overlay-popout`,children:[(0,V.jsx)(O,{className:`browser-viewer__icon`}),(0,V.jsx)(`span`,{className:`browser-viewer__title`,children:v.productName}),(0,V.jsx)(`span`,{className:`browser-viewer__popout-label`,children:`Popped out`}),(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:We,onClick:()=>{a()},"aria-label":`Pop back in`}),(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:fe,onClick:s,"aria-label":`Close`})]}):(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen`,children:[(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen__bar`,...c,children:[(0,V.jsx)(O,{className:`browser-viewer__icon`}),(0,V.jsx)(`span`,{className:`browser-viewer-fullscreen__title`,children:v.productName}),(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen__actions`,children:[(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:De,onClick:o,"aria-label":`Pop out`}),(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:fe,onClick:s,"aria-label":`Close`})]})]}),(0,V.jsx)(`iframe`,{src:`/vnc-viewer.html`,className:`browser-viewer-fullscreen__iframe`,title:v.productName})]})}function IE({onClose:e}){let t=(0,B.useCallback)(()=>{e()},[e]);return(0,B.useEffect)(()=>{let e=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t]),(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen`,children:[(0,V.jsxs)(`div`,{className:`browser-viewer-fullscreen__bar`,children:[(0,V.jsx)(Qe,{className:`browser-viewer__icon`}),(0,V.jsx)(`span`,{className:`browser-viewer-fullscreen__title`,children:v.productName}),(0,V.jsx)(`div`,{className:`browser-viewer-fullscreen__actions`,children:(0,V.jsx)(T,{variant:`ghost`,size:`sm`,icon:fe,onClick:t,"aria-label":`Close`})})]}),(0,V.jsx)(`div`,{className:`browser-viewer-fullscreen__iframe`,style:{padding:8,background:`#0a0a0a`},children:(0,V.jsx)(EE,{corrId:`header-overlay`,onUnavailable:()=>{t()}})})]})}function LE(){ot();let e=(0,B.useRef)(null),[t,n]=(0,B.useState)(null),[r,a]=(0,B.useState)(!1),[o,s]=(0,B.useState)(!1),[c,u]=(0,B.useState)(``),[d,f]=(0,B.useState)(!1),[p,m]=(0,B.useState)(null),[g,_]=(0,B.useState)(!1),[y,b]=(0,B.useState)(null),[x,S]=(0,B.useState)(null),[w,T]=(0,B.useState)(!1),[E,D]=(0,B.useState)([]),[k,A]=(0,B.useState)(!1),[j,M]=(0,B.useState)(null),[N,P]=(0,B.useState)(null),[ee,F]=(0,B.useState)(!1),[te,I]=(0,B.useState)(null),[ne,ae]=(0,B.useState)(!1),[le,ue]=(0,B.useState)(!1),[de,pe]=(0,B.useState)(!1),me=(0,B.useRef)(!1),he=(0,B.useRef)(!1),ge=(0,B.useRef)(!1),_e=(0,B.useRef)(null),ve=l(),ye=lt(),R=ht(),be=wt(),z=Ct({sessionKey:R.sessionKey,setSessionKey:R.setSessionKey,setAppState:R.setAppState,setConversationId:R.setConversationId,startElapsedTimer:ye.startElapsedTimer,stopElapsedTimer:ye.stopElapsedTimer,resetTimerState:ye.resetTimerState,pausedElapsedRef:ye.pausedElapsedRef,expandAllDefaultRef:R.expandAllDefaultRef,setExpandAll:R.setExpandAll,getPendingFiles:()=>be.pendingFiles,clearPendingFiles:be.clearFiles,inputRef:e});(0,B.useEffect)(()=>{u(window.location.hostname.startsWith(`admin.`)?window.location.origin.replace(`admin.`,`public.`):window.location.origin)},[]),(0,B.useEffect)(()=>{function e(e){e.key===`Escape`&&(t?n(null):ve.selectionMode&&ve.exitSelection())}return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[t,ve.selectionMode]),(0,B.useEffect)(()=>{(R.appState===`set-pin`||R.appState===`enter-pin`)&&setTimeout(()=>R.pinInputRef.current?.focus(),100),R.appState===`chat`&&setTimeout(()=>e.current?.focus(),100)},[R.appState,z.greetingGeneration]),(0,B.useEffect)(()=>{if(R.appState!==`chat`)return;history.pushState({maxyChat:!0},``);function e(){console.debug(`[history-guard] popstate absorbed`),history.pushState({maxyChat:!0},``)}return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)},[R.appState]);let xe=(0,B.useRef)(z.greetingGeneration);return(0,B.useEffect)(()=>{z.greetingGeneration!==xe.current&&(xe.current=z.greetingGeneration,R.onboardingComplete===!1&&(me.current=!1))},[z.greetingGeneration,R.onboardingComplete]),(0,B.useEffect)(()=>{R.appState===`chat`&&R.onboardingComplete===!1&&(me.current||(me.current=!0,z.sendSystemPrompt(`[Begin onboarding.]`)))},[R.appState,R.onboardingComplete,z.greetingGeneration]),(0,B.useEffect)(()=>{if(!d)return;let e=!1;return fetch(`/api/admin/version`).then(e=>e.json()).then(t=>{e||P(t)}).catch(e=>{console.error(`[admin/version] menu-open client fetch failed:`,e)}),()=>{e=!0}},[d]),(0,B.useEffect)(()=>{if(R.appState!==`chat`){he.current=!1,ge.current=!1;return}if(he.current)return;he.current=!0;let e=!1;return fetch(`/api/admin/version`).then(e=>e.json()).then(t=>{e||(P(t),!(!t.updateAvailable||ge.current)&&(I(t),F(!0),fetch(`/api/admin/version/alert-surfaced`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({installed:t.installed,latest:t.latest})}).catch(e=>{console.error(`[admin/version] alert-surfaced signal failed:`,e)})))}).catch(e=>{console.error(`[admin/version] session-start client fetch failed:`,e)}),()=>{e=!0}},[R.appState]),(0,B.useEffect)(()=>{if(!d){m(null),b(null),S(null),P(null);return}function e(e){_e.current&&!_e.current.contains(e.target)&&f(!1)}return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[d]),(0,B.useEffect)(()=>{if(typeof BroadcastChannel>`u`)return;let e=new BroadcastChannel(`platform-onboarding`);return e.onmessage=e=>{e.data?.type===`remote-password-set`&&z.sendMessageRef.current(`I've set the remote password`)},()=>e.close()},[]),R.appState===`loading`?(0,V.jsx)(`div`,{className:`chat-page admin-page`,children:(0,V.jsx)(`header`,{className:`chat-header`,children:(0,V.jsx)(`img`,{src:re,alt:v.productName,className:`chat-logo`})})}):R.appState===`set-pin`?(0,V.jsx)(Et,{pin:R.pin,setPin:R.setPin,confirmPin:R.confirmPin,setConfirmPin:R.setConfirmPin,showPin:R.showPin,setShowPin:R.setShowPin,pinLoading:R.pinLoading,pinError:R.pinError,pinInputRef:R.pinInputRef,confirmPinInputRef:R.confirmPinInputRef,setPinFormRef:R.setPinFormRef,onSubmit:R.handleSetPin}):R.appState===`onboarding-choice`?(0,V.jsx)(At,{setAppState:R.setAppState,setOnboardingComplete:R.setOnboardingComplete}):R.appState===`connect-claude`?(0,V.jsx)(kt,{authPolling:R.authPolling,setAuthPolling:R.setAuthPolling,authLoading:R.authLoading,setAuthLoading:R.setAuthLoading,pinError:R.pinError,setPinError:R.setPinError,setAppState:R.setAppState}):R.appState===`enter-pin`?(0,V.jsx)(Dt,{pin:R.pin,setPin:R.setPin,showPin:R.showPin,setShowPin:R.setShowPin,pinLoading:R.pinLoading,pinError:R.pinError,pinInputRef:R.pinInputRef,onSubmit:R.handleLogin,onChangePin:R.handleChangePin}):R.appState===`account-picker`?(0,V.jsx)(Ot,{accounts:R.accounts,loading:R.accountPickerLoading,error:R.pinError,onSelect:R.handleAccountSelect}):(0,V.jsx)(C,{value:{onShowVnc:()=>ae(!0)},children:(0,V.jsxs)(`div`,{className:`chat-page admin-page`,children:[(0,V.jsxs)(`header`,{className:`chat-header`,children:[(0,V.jsx)(`img`,{src:re,alt:v.productName,className:`chat-logo`}),(!v.logoContainsName||R.businessName)&&(0,V.jsx)(`div`,{children:(0,V.jsx)(`h1`,{className:`chat-tagline`,children:R.businessName||v.productName})}),(0,V.jsxs)(`div`,{className:`chat-burger-wrap`,ref:_e,children:[(0,V.jsx)(`button`,{type:`button`,className:`chat-burger`,onClick:()=>f(e=>!e),"aria-label":`Menu`,"aria-haspopup":`true`,"aria-expanded":d,children:(0,V.jsx)(se,{size:20})}),d&&(0,V.jsxs)(`div`,{className:`chat-menu`,children:[(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:async()=>{if(!(ne||le)){ue(!0),f(!1);try{let e=await(await fetch(`/api/admin/browser/launch`,{method:`POST`})).json().catch(()=>({ok:!1,error:`Invalid response`}));if(!e.ok)throw Error(e.error??`Failed to launch browser`);ae(!0)}catch(e){console.error(`[browser] Launch failed:`,e),alert(e instanceof Error?e.message:`Failed to launch browser`)}finally{ue(!1)}}},disabled:le,children:[(0,V.jsx)(O,{size:14}),` Browser`,le&&(0,V.jsx)(ie,{size:12,className:`spin`})]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{de||(f(!1),pe(!0))},children:[(0,V.jsx)(Qe,{size:14}),` Terminal`]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{f(!1),window.location.href=`/data`},children:[(0,V.jsx)(oe,{size:14}),` Data`]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{f(!1),window.location.href=`/graph`},children:[(0,V.jsx)(ce,{size:14}),` Graph`]}),(0,V.jsx)(`div`,{className:`chat-menu-divider`}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:async()=>{if(p!==null){m(null),S(null);return}_(!0),b(null);try{let e=await fetch(`/api/admin/agents`);if(!e.ok)throw Error(`Failed to load agents`);m((await e.json()).agents??[])}catch(e){console.error(`[admin] agent list failed:`,e),b(e instanceof Error?e.message:String(e))}finally{_(!1)}},children:[(0,V.jsx)(De,{size:14}),` Public`,g&&(0,V.jsx)(ie,{size:12,className:`spin`})]}),p!==null&&(0,V.jsxs)(`div`,{className:`chat-menu-agents`,children:[y&&(0,V.jsx)(`span`,{className:`chat-menu-agent-error`,children:y}),p.length===0&&!y&&(0,V.jsx)(`span`,{className:`chat-menu-agent-empty`,children:`No public agents configured`}),p.map(e=>(0,V.jsxs)(`div`,{className:`chat-menu-item chat-menu-agent-item`,children:[(0,V.jsx)(`span`,{className:`agent-status-dot ${e.status}`}),(0,V.jsxs)(`span`,{className:`agent-text`,children:[(0,V.jsx)(`span`,{className:`agent-display-name`,children:e.displayName}),(0,V.jsxs)(`span`,{className:`agent-slug`,children:[`/`,e.slug]})]}),x===e.slug?(0,V.jsxs)(`span`,{className:`agent-actions agent-confirm`,children:[(0,V.jsx)(`button`,{type:`button`,className:`agent-action-btn agent-confirm-yes`,title:`Confirm delete`,onClick:()=>{S(null),m(t=>t?.filter(t=>t.slug!==e.slug)??null),fetch(`/api/admin/agents/${encodeURIComponent(e.slug)}`,{method:`DELETE`}).catch(e=>console.error(`[admin/agents] delete failed:`,e))},children:(0,V.jsx)(h,{size:12})}),(0,V.jsx)(`button`,{type:`button`,className:`agent-action-btn agent-confirm-no`,title:`Cancel`,onClick:()=>S(null),children:(0,V.jsx)(fe,{size:12})})]}):(0,V.jsxs)(`span`,{className:`agent-actions`,children:[(0,V.jsx)(`a`,{href:`${c}/${e.slug}`,target:`_blank`,rel:`noopener noreferrer`,className:`agent-action-btn`,title:`Open agent`,onClick:()=>f(!1),children:(0,V.jsx)(De,{size:12})}),(0,V.jsx)(`button`,{type:`button`,className:`agent-action-btn agent-delete-btn`,title:`Delete agent`,onClick:()=>S(e.slug),children:(0,V.jsx)(i,{size:12})})]})]},e.slug))]}),(0,V.jsx)(`div`,{className:`chat-menu-divider`}),N?.updateAvailable?(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-version`,onClick:()=>{N.latest&&(I(N),f(!1),F(!0))},children:[(0,V.jsx)(Pe,{size:14}),(0,V.jsxs)(`span`,{className:`version-installed`,children:[`v`,N.installed,(0,V.jsx)(`span`,{className:`version-update-dot`})]})]}):(0,V.jsxs)(`div`,{className:`chat-menu-version chat-menu-version-passive`,children:[(0,V.jsx)(Pe,{size:14}),(0,V.jsxs)(`span`,{className:`version-installed`,children:[N?`v${N.installed}`:`…`,N&&!N.updateAvailable&&(0,V.jsx)(`span`,{className:`version-uptodate-dot`})]})]}),(0,V.jsxs)(`button`,{type:`button`,className:`chat-menu-item`,onClick:()=>{f(!1),R.handleLogout(),z.setMessages([])},children:[(0,V.jsx)(L,{size:14}),` Log out`]})]})]})]}),(0,V.jsxs)(`div`,{className:`chat-content`,children:[(0,V.jsx)(uE,{messages:z.messages,isStreaming:z.isStreaming,elapsedSeconds:ye.elapsedSeconds,sessionTurnStart:ye.sessionTurnStart,expandAll:R.expandAll,setExpandAll:R.setExpandAll,conversationCompacted:z.conversationCompacted,isCompacting:z.isCompacting,setShowCompactConfirm:s,handleComponentSubmit:z.handleComponentSubmit,effectiveSubmitted:z.effectiveSubmitted,selectionMode:ve.selectionMode,selectedItems:ve.selectedItems,toggleSelectItem:ve.toggleSelectItem,sessionKey:R.sessionKey,setLightboxSrc:n,messageQueue:z.messageQueue,messageQueueRef:z.messageQueueRef,setQueue:z.setQueue,sendMessage:z.sendMessage,appState:R.appState,onboardingComplete:R.onboardingComplete}),(0,V.jsx)(dE,{offer:z.topicChangeOffer,onFreshStart:z.handleTopicFreshStart,onContinue:z.handleTopicContinue,disabled:z.isStreaming}),(0,V.jsx)(mE,{selectionMode:ve.selectionMode,selectedItems:ve.selectedItems,copyToast:ve.copyToast||null,exitSelection:ve.exitSelection,enterSelection:ve.enterSelection,copySelected:ve.copySelected,showCopyToast:ve.showCopyToast,input:z.input,setInput:z.setInput,inputRef:e,pendingFiles:be.pendingFiles,setPendingFiles:be.setPendingFiles,isDragOver:be.isDragOver,attachError:be.attachError,fileInputRef:be.fileInputRef,addFiles:be.addFiles,removeFile:be.removeFile,onDragOver:be.onDragOver,onDragLeave:be.onDragLeave,onDrop:be.onDrop,isStreaming:z.isStreaming,wasPaused:z.wasPaused,messages:z.messages,messageQueueRef:z.messageQueueRef,setQueue:z.setQueue,sendMessage:z.sendMessage,stopStreaming:z.stopStreaming,disconnecting:R.disconnecting,handleDisconnect:R.handleDisconnect,claudeInfo:R.claudeInfo,setClaudeInfo:R.setClaudeInfo,setShowClaudeInfo:a,conversationId:R.conversationId,onNewConversation:z.startNewConversation,newConversationDisabled:z.isStreaming||z.isCompacting,onOpenConversations:async()=>{T(!0),A(!0),M(null);try{let e=await fetch(`/api/admin/sessions?session_key=${encodeURIComponent(R.sessionKey)}`);if(!e.ok)throw Error(`Failed to load conversations`);D((await e.json()).sessions??[])}catch(e){console.error(`[admin] conversation list failed:`,e),M(e instanceof Error?e.message:String(e))}finally{A(!1)}}})]}),ve.copyToast&&(0,V.jsx)(`span`,{className:`copy-toast${ve.copyToast===`failed`?` copy-toast-failed`:``}`,children:ve.copyToast===`copied`?`Copied`:`Copy failed`}),(0,V.jsx)(DE,{show:w,onClose:()=>T(!1),conversationsLoading:k,conversationsError:j,conversationsList:E,setConversationsList:D,sessionKey:R.sessionKey,isStreaming:z.isStreaming,resetConversation:z.resetConversation,setMessages:z.setMessages,setConversationId:R.setConversationId}),(0,V.jsx)(OE,{src:t,onClose:()=>n(null)}),(0,V.jsx)(kE,{show:o,onClose:()=>s(!1),onConfirm:z.handleCompactNow}),(0,V.jsx)(AE,{show:r,onClose:()=>{a(!1),R.setClaudeInfo(null)},claudeInfo:R.claudeInfo,messages:z.messages,sessionElapsed:ye.sessionElapsed,conversationId:R.conversationId,contextModeHint:R.contextModeHint,handleContextModeToggle:R.handleContextModeToggle}),(0,V.jsx)(ME,{show:ee,onClose:()=>{F(!1),ge.current=!0},versionInfo:te,sessionKey:R.sessionKey,onUpgradeComplete:()=>P(null)}),ne&&(0,V.jsx)(FE,{onClose:()=>ae(!1)}),de&&(0,V.jsx)(IE,{onClose:()=>pe(!1)})]})})}(0,at.createRoot)(document.getElementById(`root`)).render((0,V.jsx)(LE,{}));
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Real Agent</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/admin-WQxJgaus.js"></script>
8
+ <script type="module" crossorigin src="/assets/admin-BFmYXz1V.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-DD-I1_y5.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/jsx-runtime-BVKWELH6.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-qlgyTAkD.js">