@pablopunk/webr 0.1.1

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 (126) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +17 -0
  3. package/bin/webr.mjs +11 -0
  4. package/dist/client/_astro/App.BxlQhLhN.css +1 -0
  5. package/dist/client/_astro/App.CzxEZ_Dw.js +2 -0
  6. package/dist/client/_astro/ClientRouter.astro_astro_type_script_index_0_lang.CTgtIFyY.js +1 -0
  7. package/dist/client/_astro/addon-fit.jgKqzwky.js +1 -0
  8. package/dist/client/_astro/addon-web-links.C9lyQRxI.js +1 -0
  9. package/dist/client/_astro/client.BDkzSRUN.js +1 -0
  10. package/dist/client/_astro/client.Bbg9OBwT.js +1 -0
  11. package/dist/client/_astro/client.CLSsMr2g.js +9 -0
  12. package/dist/client/_astro/jetbrains-mono.CFJaiWaY.woff2 +0 -0
  13. package/dist/client/_astro/symbols-nerd.CEdkKAK8.woff2 +0 -0
  14. package/dist/client/_astro/xterm.B-l5AIf6.js +7 -0
  15. package/dist/client/apple-touch-icon.png +0 -0
  16. package/dist/client/brand-mark.png +0 -0
  17. package/dist/client/favicon-16.png +0 -0
  18. package/dist/client/favicon-32.png +0 -0
  19. package/dist/client/favicon.ico +0 -0
  20. package/dist/client/icon-192.png +0 -0
  21. package/dist/client/icon-512.png +0 -0
  22. package/dist/client/icon-maskable-512.png +0 -0
  23. package/dist/client/manifest.webmanifest +30 -0
  24. package/dist/server/chunks/App_BmsGAwGf.mjs +4314 -0
  25. package/dist/server/chunks/_id__u0ueX6GO.mjs +34 -0
  26. package/dist/server/chunks/_tab__DL6wyr-j.mjs +35 -0
  27. package/dist/server/chunks/_virtual_astro_server-island-manifest_C1Q2srgE.mjs +5 -0
  28. package/dist/server/chunks/_virtual_astro_session-driver_DS5V7T-N.mjs +6 -0
  29. package/dist/server/chunks/compiler_CpXox_xS.mjs +35 -0
  30. package/dist/server/chunks/console_CucE1SwS.mjs +170 -0
  31. package/dist/server/chunks/errors_BgIp4W2B.mjs +383 -0
  32. package/dist/server/chunks/index_DyyPce_k.mjs +22 -0
  33. package/dist/server/chunks/new_G7XChx5p.mjs +33 -0
  34. package/dist/server/chunks/node_BlixNyYV.mjs +1816 -0
  35. package/dist/server/chunks/noop-entrypoint_Z3zFhrGC.mjs +4 -0
  36. package/dist/server/chunks/remote_BxuVLM0r.mjs +169 -0
  37. package/dist/server/chunks/server_DS45LC4q.mjs +2496 -0
  38. package/dist/server/chunks/settings_DLV2Js3e.mjs +31 -0
  39. package/dist/server/chunks/sharp_CgNvPCpY.mjs +152 -0
  40. package/dist/server/chunks/vnode-children_CRmaSs5G.mjs +24 -0
  41. package/dist/server/entry.mjs +5837 -0
  42. package/dist/server/virtual_astro_middleware.mjs +4 -0
  43. package/drizzle/0000_gateway.sql +13 -0
  44. package/drizzle/0001_device_sessions.sql +9 -0
  45. package/drizzle/0002_audit_events.sql +7 -0
  46. package/drizzle/meta/_journal.json +1 -0
  47. package/package.json +88 -0
  48. package/server/cli.ts +93 -0
  49. package/server/command.ts +9 -0
  50. package/server/entry.ts +3 -0
  51. package/server/herdr-check.ts +37 -0
  52. package/server/home.ts +4 -0
  53. package/server/log-file.ts +52 -0
  54. package/server/package-info.ts +6 -0
  55. package/server/plugin/config.ts +14 -0
  56. package/server/plugin/dev.ts +27 -0
  57. package/server/plugin/files.ts +10 -0
  58. package/server/plugin/herdr.ts +33 -0
  59. package/server/plugin/index.ts +68 -0
  60. package/server/plugin/probe.ts +17 -0
  61. package/server/plugin/stop.ts +39 -0
  62. package/server/port.ts +8 -0
  63. package/server/run.ts +35 -0
  64. package/server/ssr.ts +23 -0
  65. package/server/start.ts +5 -0
  66. package/server/update/hint.ts +26 -0
  67. package/server/update/index.ts +33 -0
  68. package/server/update/latest.ts +33 -0
  69. package/server/update/package-manager.ts +24 -0
  70. package/server/update/versions.ts +15 -0
  71. package/src/lib/avatars.ts +15 -0
  72. package/src/lib/launch.ts +16 -0
  73. package/src/lib/machines.ts +41 -0
  74. package/src/lib/mock-store.ts +82 -0
  75. package/src/lib/models.ts +139 -0
  76. package/src/lib/project-icons.ts +120 -0
  77. package/src/lib/terminal-split.ts +23 -0
  78. package/src/lib/uuid.ts +10 -0
  79. package/src/server/auth/access.ts +22 -0
  80. package/src/server/auth/audit.ts +15 -0
  81. package/src/server/auth/auth.ts +134 -0
  82. package/src/server/auth/pair-page.ts +85 -0
  83. package/src/server/auth/pairing.ts +91 -0
  84. package/src/server/auth/presence.ts +8 -0
  85. package/src/server/auth/sessions.ts +48 -0
  86. package/src/server/directories.ts +21 -0
  87. package/src/server/dist.ts +3 -0
  88. package/src/server/host.ts +166 -0
  89. package/src/server/perf-log.ts +23 -0
  90. package/src/server/protocol/deadlines.ts +24 -0
  91. package/src/server/protocol/native.ts +10 -0
  92. package/src/server/protocol/ndjson.ts +22 -0
  93. package/src/server/protocol/socket.ts +81 -0
  94. package/src/server/protocol/validate.ts +17 -0
  95. package/src/server/public-urls.ts +17 -0
  96. package/src/server/runtime/herdr-actions.ts +122 -0
  97. package/src/server/runtime/launch.ts +55 -0
  98. package/src/server/runtime/manager.ts +187 -0
  99. package/src/server/runtime/reconcile.ts +51 -0
  100. package/src/server/runtime/supervisor.ts +92 -0
  101. package/src/server/runtime/target.ts +37 -0
  102. package/src/server/runtime/workspace.ts +24 -0
  103. package/src/server/storage/database.ts +88 -0
  104. package/src/server/storage/drizzle-node-sqlite.ts +18 -0
  105. package/src/server/storage/node-sqlite-client.ts +44 -0
  106. package/src/server/storage/schema.ts +25 -0
  107. package/src/server/tailscale-host.ts +17 -0
  108. package/src/server/terminal/cli.ts +44 -0
  109. package/src/server/terminal/hub.ts +140 -0
  110. package/src/server/terminal/writer.ts +38 -0
  111. package/src/server/transport/herdr-target.ts +148 -0
  112. package/src/server/transport/icons.ts +65 -0
  113. package/src/server/transport/identity.ts +5 -0
  114. package/src/server/transport/local-session.ts +65 -0
  115. package/src/server/transport/model-discovery.ts +18 -0
  116. package/src/server/transport/native-locations.ts +17 -0
  117. package/src/server/transport/process.ts +21 -0
  118. package/src/server/transport/registry.ts +27 -0
  119. package/src/server/transport/ssh.ts +47 -0
  120. package/src/server/uploads.ts +40 -0
  121. package/src/server/websockets.ts +62 -0
  122. package/src/shared/agent-kinds.ts +4 -0
  123. package/src/shared/frame.ts +38 -0
  124. package/src/shared/projects.ts +1 -0
  125. package/src/shared/retry.ts +1 -0
  126. package/src/shared/runtime.ts +33 -0
@@ -0,0 +1,4314 @@
1
+ import { S as createAstro, f as renderHead, i as renderComponent, m as createRenderInstruction, p as addAttribute, s as renderSlot, u as renderTemplate } from "./server_DS45LC4q.mjs";
2
+ import { t as createComponent } from "./compiler_CpXox_xS.mjs";
3
+ import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { Archive, ArchiveRestore, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Check, ChevronDown, ChevronRight, Circle, CircleAlert, CircleCheck, CircleHelp, Copy, Cpu, Folder, FolderTree, GitBranch, ImagePlus, LayoutList, LoaderCircle, MessageCircleQuestion, Monitor, Moon, PanelBottom, PanelLeft, PanelLeftClose, PanelLeftOpen, PanelRight, Plus, Search, Settings2, SquarePen, SquareTerminal, Sun, TerminalSquare, Trash2, TriangleAlert, X } from "lucide-react";
5
+ import { createRoot } from "react-dom/client";
6
+ import { AlertDialog } from "@base-ui/react/alert-dialog";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { Tooltip } from "@base-ui/react/tooltip";
9
+ import { BotAvatar, botAvatarTypes } from "bot-avatars";
10
+ import { Combobox } from "@base-ui/react/combobox";
11
+ import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
12
+ import { Dialog } from "@base-ui/react/dialog";
13
+ import { encode } from "uqr";
14
+ import { ContextMenu } from "@base-ui/react/context-menu";
15
+ import { Menu } from "@base-ui/react/menu";
16
+ import { useStore } from "zustand";
17
+ import { createStore } from "zustand/vanilla";
18
+ import { useShallow } from "zustand/react/shallow";
19
+ //#region node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/runtime/server/render/script.js
20
+ async function renderScript(result, id) {
21
+ const inlined = result.inlinedScripts.get(id);
22
+ let content = "";
23
+ if (inlined != null) {
24
+ if (inlined) content = `<script type="module">${inlined}<\/script>`;
25
+ } else {
26
+ const resolved = await result.resolve(id);
27
+ content = `<script type="module" src="${result.userAssetsBase ? (result.base === "/" ? "" : result.base) + result.userAssetsBase : ""}${resolved}"><\/script>`;
28
+ }
29
+ return createRenderInstruction({
30
+ type: "script",
31
+ id,
32
+ content
33
+ });
34
+ }
35
+ //#endregion
36
+ //#region node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/components/ClientRouter.astro
37
+ createAstro("https://astro.build");
38
+ var $$ClientRouter = createComponent(($$result, $$props, $$slots) => {
39
+ const Astro = $$result.createAstro($$props, $$slots);
40
+ Astro.self = $$ClientRouter;
41
+ const { fallback = "animate" } = Astro.props;
42
+ return renderTemplate`<meta name="astro-view-transitions-enabled" content="true"><meta name="astro-view-transitions-fallback"${addAttribute(fallback, "content")}>${renderScript($$result, "/Users/pablopunk/src/herdr-web/node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/components/ClientRouter.astro?astro&type=script&index=0&lang.ts")}`;
43
+ }, "/Users/pablopunk/src/herdr-web/node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/components/ClientRouter.astro", void 0);
44
+ //#endregion
45
+ //#region src/assets/fonts/jetbrains-mono.woff2?url
46
+ var jetbrains_mono_default = "/_astro/jetbrains-mono.CFJaiWaY.woff2";
47
+ //#endregion
48
+ //#region src/layouts/Base.astro
49
+ createAstro("https://astro.build");
50
+ var $$Base = createComponent(($$result, $$props, $$slots) => {
51
+ const Astro = $$result.createAstro($$props, $$slots);
52
+ Astro.self = $$Base;
53
+ const { title = "Webr" } = Astro.props;
54
+ return renderTemplate`<html lang="en" data-theme="light"><head>${renderComponent($$result, "ClientRouter", $$ClientRouter, {})}<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="color-scheme" content="light dark"><meta name="theme-color" media="(prefers-color-scheme: light)" content="#f8f8f7"><meta name="theme-color" media="(prefers-color-scheme: dark)" content="#121313"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-title" content="Webr"><meta name="apple-mobile-web-app-status-bar-style" content="default"><link rel="icon" href="/favicon.ico" sizes="48x48"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png"><link rel="apple-touch-icon" href="/apple-touch-icon.png"><link rel="manifest" href="/manifest.webmanifest"><title>${title} · Webr</title><link rel="preload" as="font" type="font/woff2"${addAttribute(jetbrains_mono_default, "href")} crossorigin><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"><script>
55
+ function restoreWebrTheme() {
56
+ let preference = 'system';
57
+ try { preference = localStorage.getItem('webr-theme') || 'system'; } catch {}
58
+ document.documentElement.dataset.theme = preference === 'system'
59
+ ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
60
+ : preference;
61
+ }
62
+ restoreWebrTheme();
63
+ document.addEventListener('astro:before-swap', (event) => { event.newDocument.documentElement.dataset.theme = document.documentElement.dataset.theme; });
64
+ document.addEventListener('astro:after-swap', restoreWebrTheme);
65
+ <\/script>${renderHead($$result)}</head><body>${renderSlot($$result, $$slots["default"])}</body></html>`;
66
+ }, "/Users/pablopunk/src/herdr-web/src/layouts/Base.astro", void 0);
67
+ //#endregion
68
+ //#region src/components/dialogs.tsx
69
+ var root;
70
+ var showing;
71
+ var show = (request) => {
72
+ root ??= createRoot(document.body.appendChild(document.createElement("div")));
73
+ showing?.resolve(false);
74
+ showing = request;
75
+ const settle = (accepted) => {
76
+ if (showing !== request) return;
77
+ showing = void 0;
78
+ root?.render(null);
79
+ request.resolve(accepted);
80
+ };
81
+ root.render(/* @__PURE__ */ jsx(DialogView, {
82
+ request,
83
+ settle
84
+ }));
85
+ };
86
+ function DialogView({ request, settle }) {
87
+ return /* @__PURE__ */ jsx(AlertDialog.Root, {
88
+ open: true,
89
+ onOpenChange: (open) => {
90
+ if (!open) settle(false);
91
+ },
92
+ children: /* @__PURE__ */ jsxs(AlertDialog.Portal, { children: [/* @__PURE__ */ jsx(AlertDialog.Backdrop, { className: "dialog-backdrop" }), /* @__PURE__ */ jsxs(AlertDialog.Popup, {
93
+ className: "dialog",
94
+ children: [/* @__PURE__ */ jsx(AlertDialog.Description, {
95
+ className: "dialog-message",
96
+ children: request.message
97
+ }), /* @__PURE__ */ jsxs("div", {
98
+ className: "dialog-actions",
99
+ children: [request.cancelLabel && /* @__PURE__ */ jsx(AlertDialog.Close, {
100
+ className: "dialog-button",
101
+ children: request.cancelLabel
102
+ }), /* @__PURE__ */ jsx("button", {
103
+ type: "button",
104
+ autoFocus: true,
105
+ className: `dialog-button is-primary ${request.danger ? "is-danger" : ""}`,
106
+ onClick: () => settle(true),
107
+ children: request.confirmLabel
108
+ })]
109
+ })]
110
+ })] })
111
+ });
112
+ }
113
+ var confirmDialog = (message, { confirmLabel = "Confirm", danger = false } = {}) => new Promise((resolve) => show({
114
+ message,
115
+ confirmLabel,
116
+ cancelLabel: "Cancel",
117
+ danger,
118
+ resolve
119
+ }));
120
+ var alertDialog = (message) => new Promise((resolve) => show({
121
+ message,
122
+ confirmLabel: "OK",
123
+ resolve: () => resolve()
124
+ }));
125
+ //#endregion
126
+ //#region src/components/Tooltip.tsx
127
+ function Tooltip$1({ label, side = "top", children }) {
128
+ return /* @__PURE__ */ jsxs(Tooltip.Root, { children: [/* @__PURE__ */ jsx(Tooltip.Trigger, { render: children }), /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsx(Tooltip.Positioner, {
129
+ className: "tooltip-positioner",
130
+ side,
131
+ sideOffset: 8,
132
+ collisionPadding: 8,
133
+ children: /* @__PURE__ */ jsx(Tooltip.Popup, {
134
+ className: "tooltip-popup",
135
+ children: label
136
+ })
137
+ }) })] });
138
+ }
139
+ //#endregion
140
+ //#region src/lib/avatars.ts
141
+ var GOLDEN_ANGLE_DEGREES = 137.508;
142
+ var hueFarFromThePreviousThread = (index) => (index * GOLDEN_ANGLE_DEGREES % 360).toFixed(3);
143
+ function avatarForThread(thread) {
144
+ const index = thread.avatarIndex;
145
+ return {
146
+ type: botAvatarTypes[index % botAvatarTypes.length],
147
+ color: `hsl(${hueFarFromThePreviousThread(index)} 72% 58%)`,
148
+ seed: index * .61803398875 % 1
149
+ };
150
+ }
151
+ //#endregion
152
+ //#region src/components/ThreadAvatar.tsx
153
+ var TIME_FOR_A_JUMP_TO_LAND_MS = 2e3;
154
+ var FACE_FORWARD = 0;
155
+ function usePauseAfterLanding(working) {
156
+ const [paused, setPaused] = useState(!working);
157
+ useEffect(() => {
158
+ if (working) {
159
+ setPaused(false);
160
+ return;
161
+ }
162
+ const timer = setTimeout(() => setPaused(true), TIME_FOR_A_JUMP_TO_LAND_MS);
163
+ return () => clearTimeout(timer);
164
+ }, [working]);
165
+ return paused;
166
+ }
167
+ function ThreadAvatar({ thread, working, size }) {
168
+ const paused = usePauseAfterLanding(working);
169
+ const remountIntoTheRestPose = paused ? "rest" : "moving";
170
+ return /* @__PURE__ */ jsx(BotAvatar, {
171
+ ...avatarForThread(thread),
172
+ state: working ? "working" : "default",
173
+ size,
174
+ paused,
175
+ turn: working ? void 0 : FACE_FORWARD
176
+ }, remountIntoTheRestPose);
177
+ }
178
+ //#endregion
179
+ //#region src/components/shortcuts.ts
180
+ var shortcutLabels = {
181
+ nextThread: "Next thread",
182
+ previousThread: "Previous thread",
183
+ nextPane: "Next pane",
184
+ previousPane: "Previous pane",
185
+ toggleSidebar: "Toggle sidebar",
186
+ newThread: "New thread",
187
+ toggleTerminal: "Toggle terminal"
188
+ };
189
+ var defaultShortcuts = {
190
+ nextThread: "ctrl+j",
191
+ previousThread: "ctrl+k",
192
+ nextPane: "ctrl+l",
193
+ previousPane: "ctrl+h",
194
+ toggleSidebar: "ctrl+b",
195
+ newThread: "ctrl+x",
196
+ toggleTerminal: "ctrl+q"
197
+ };
198
+ function getShortcuts() {
199
+ try {
200
+ const saved = JSON.parse(localStorage.getItem("webr-shortcuts") ?? "{}");
201
+ return {
202
+ ...defaultShortcuts,
203
+ ...saved
204
+ };
205
+ } catch {
206
+ return defaultShortcuts;
207
+ }
208
+ }
209
+ function shortcutKeys(combo) {
210
+ const keys = {
211
+ ctrl: "⌃",
212
+ alt: "⌥",
213
+ meta: "⌘",
214
+ shift: "⇧",
215
+ space: "Space",
216
+ escape: "Esc"
217
+ };
218
+ return combo.split("+").map((key) => keys[key] ?? key.toUpperCase());
219
+ }
220
+ function formatShortcut(combo) {
221
+ return shortcutKeys(combo).join("");
222
+ }
223
+ function keyCombo(event) {
224
+ const modifiers = [
225
+ event.ctrlKey && "ctrl",
226
+ event.altKey && "alt",
227
+ event.metaKey && "meta",
228
+ event.shiftKey && "shift"
229
+ ].filter(Boolean);
230
+ const key = event.key.toLowerCase() === " " ? "space" : event.key.toLowerCase();
231
+ if ([
232
+ "control",
233
+ "alt",
234
+ "meta",
235
+ "shift"
236
+ ].includes(key)) return "";
237
+ return [...modifiers, key].join("+");
238
+ }
239
+ function isReservedShortcut(combo) {
240
+ return /^(meta\+(w|t|r|l|n)|ctrl\+(w|t|r|n))$/.test(combo) || !/(ctrl|alt|meta)\+/.test(combo);
241
+ }
242
+ //#endregion
243
+ //#region src/components/ThemeControl.tsx
244
+ function applyTheme(preference) {
245
+ const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
246
+ document.documentElement.dataset.theme = preference === "system" ? dark ? "dark" : "light" : preference;
247
+ }
248
+ function ThemeControl({ expanded = false }) {
249
+ const [preference, setPreference] = useState("system");
250
+ useEffect(() => {
251
+ const saved = localStorage.getItem("webr-theme");
252
+ const initial = saved === "light" || saved === "dark" ? saved : "system";
253
+ setPreference(initial);
254
+ applyTheme(initial);
255
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
256
+ const onChange = () => {
257
+ const current = localStorage.getItem("webr-theme") || "system";
258
+ setPreference(current);
259
+ applyTheme(current);
260
+ };
261
+ media.addEventListener("change", onChange);
262
+ window.addEventListener("webr-theme-change", onChange);
263
+ return () => {
264
+ media.removeEventListener("change", onChange);
265
+ window.removeEventListener("webr-theme-change", onChange);
266
+ };
267
+ }, []);
268
+ const select = (value) => {
269
+ setPreference(value);
270
+ localStorage.setItem("webr-theme", value);
271
+ applyTheme(value);
272
+ };
273
+ return /* @__PURE__ */ jsx("div", {
274
+ className: expanded ? "theme-control theme-control--expanded" : "theme-control",
275
+ role: "group",
276
+ "aria-label": "Color theme",
277
+ children: [
278
+ ["system", Monitor],
279
+ ["light", Sun],
280
+ ["dark", Moon]
281
+ ].map(([value, Icon]) => /* @__PURE__ */ jsx(Tooltip$1, {
282
+ label: `${value[0].toUpperCase()}${value.slice(1)} theme`,
283
+ children: /* @__PURE__ */ jsxs("button", {
284
+ type: "button",
285
+ "aria-label": `${value} theme`,
286
+ "aria-pressed": preference === value,
287
+ className: preference === value ? "theme-option is-selected" : "theme-option",
288
+ onClick: () => select(value),
289
+ children: [/* @__PURE__ */ jsx(Icon, {
290
+ size: 15,
291
+ strokeWidth: 1.8
292
+ }), expanded && /* @__PURE__ */ jsx("span", { children: value })]
293
+ })
294
+ }, value))
295
+ });
296
+ }
297
+ //#endregion
298
+ //#region node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/transitions/router.js
299
+ var navigateOnServerWarned = false;
300
+ async function navigate(href, options) {
301
+ if (!navigateOnServerWarned) {
302
+ const warning = /* @__PURE__ */ new Error("The view transitions client API was called during a server side render. This may be unintentional as the navigate() function is expected to be called in response to user interactions. Please make sure that your usage is correct.");
303
+ warning.name = "Warning";
304
+ console.warn(warning);
305
+ navigateOnServerWarned = true;
306
+ }
307
+ }
308
+ //#endregion
309
+ //#region src/client/thread-actions.ts
310
+ var actionErrors = {
311
+ machine_disconnected: "The machine is not connected.",
312
+ launch_in_progress: "Wait for the launch to finish, then try again.",
313
+ thread_not_found: "This thread does not exist anymore."
314
+ };
315
+ async function post$1(path, body = {}) {
316
+ const response = await fetch(path, {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/json" },
319
+ body: JSON.stringify(body)
320
+ });
321
+ const result = await response.json().catch(() => ({}));
322
+ if (!response.ok) throw new Error(actionErrors[result.error ?? ""] ?? "Herdr could not complete this action.");
323
+ return result;
324
+ }
325
+ var setThreadArchived = (thread, archived) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/archive`, {
326
+ machineId: thread.machineId,
327
+ archived
328
+ });
329
+ var renameThread = (thread, title) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/rename`, {
330
+ machineId: thread.machineId,
331
+ title
332
+ });
333
+ var toggleThreadTerminal = (thread, direction) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/terminal`, {
334
+ machineId: thread.machineId,
335
+ direction
336
+ });
337
+ var closeThreadTerminal = (thread) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/terminal/close`, { machineId: thread.machineId });
338
+ var deleteThreadPermanently = (thread) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/delete`, { machineId: thread.machineId });
339
+ var deleteArchivedThreads = () => post$1("/api/archive/delete");
340
+ var plural = (count, one, many) => count === 1 ? one : many;
341
+ function deleteWarning(threads) {
342
+ const count = threads.length;
343
+ const worktrees = threads.filter((thread) => thread.ownsWorktree).length;
344
+ const subject = plural(count, "Delete this thread", `Delete ${count} threads`);
345
+ const closes = plural(count, "its terminal", "their terminals");
346
+ if (!worktrees) return `${subject} permanently? This closes ${closes}. Your project files stay untouched.`;
347
+ return `${subject} permanently? This closes ${closes} and removes ${worktrees === count ? plural(count, "its worktree", "their worktrees") : `${worktrees} ${plural(worktrees, "worktree", "worktrees")}`}, including uncommitted changes.`;
348
+ }
349
+ //#endregion
350
+ //#region src/lib/terminal-split.ts
351
+ var threadAgent = (thread) => thread.panes.find((pane) => pane.kind === "agent") ?? thread.panes[0];
352
+ var threadShell = (thread) => {
353
+ const main = threadAgent(thread);
354
+ return thread.panes.find((pane) => pane.kind === "shell" && pane.id !== main?.id);
355
+ };
356
+ var peekSide = (shell, area) => shell.x > area.x && shell.y === area.y ? "right" : "bottom";
357
+ var terminalDirection = (side) => side === "right" ? "right" : "down";
358
+ function hiddenTerminalLayout(thread, layout) {
359
+ const shell = threadShell(thread);
360
+ const agent = threadAgent(thread);
361
+ const shellRect = shell && layout.panes.find((rect) => rect.paneId === shell.id)?.rect;
362
+ const otherPanes = thread.panes.filter((pane) => pane.id !== shell?.id && pane.id !== agent?.id);
363
+ if (!thread.terminalHidden || !shell || !agent || !shellRect || otherPanes.length) return void 0;
364
+ return {
365
+ shell,
366
+ agent,
367
+ side: peekSide(shellRect, layout.area)
368
+ };
369
+ }
370
+ function currentTerminalDirection(thread, layouts, narrow) {
371
+ const shell = threadShell(thread);
372
+ const layout = layouts.find((layout) => layout.panes.some((rect) => rect.paneId === shell?.id));
373
+ const rect = layout?.panes.find((rect) => rect.paneId === shell?.id)?.rect;
374
+ return rect && layout ? terminalDirection(peekSide(rect, layout.area)) : narrow ? "down" : "right";
375
+ }
376
+ function span(name) {
377
+ const start = performance.now();
378
+ return () => {
379
+ return performance.now() - start;
380
+ };
381
+ }
382
+ var perfNow = () => performance.now();
383
+ function startPerf() {}
384
+ //#endregion
385
+ //#region src/components/CommandPalette.tsx
386
+ var reportFailure$1 = (error) => alertDialog(error instanceof Error ? error.message : "Herdr could not complete this action.");
387
+ function CommandPalette({ open, onClose, projects, threads, thread, focusedPane, mode, onModeChange, onToggleSidebar, onFocusPane, onTerminalAction, onToggleTerminal, onCloseTerminal }) {
388
+ const [query, setQuery] = useState("");
389
+ const [selected, setSelected] = useState(0);
390
+ const [shortcuts, setShortcuts] = useState(defaultShortcuts);
391
+ const input = useRef(null);
392
+ useEffect(() => {
393
+ if (!open) return;
394
+ setQuery("");
395
+ setSelected(0);
396
+ setShortcuts(getShortcuts());
397
+ requestAnimationFrame(() => input.current?.focus());
398
+ }, [open]);
399
+ const navigateThread = (offset) => {
400
+ const index = threads.findIndex((item) => item.id === thread?.id);
401
+ const target = threads[((index === -1 && offset < 0 ? 0 : index) + offset + threads.length) % threads.length];
402
+ if (target) navigate(`/threads/${encodeURIComponent(target.id)}`);
403
+ };
404
+ const focusPane = (offset) => {
405
+ if (thread?.panes.length) onFocusPane(thread.panes[(thread.panes.findIndex((pane) => pane.id === focusedPane) + offset + thread.panes.length) % thread.panes.length].id);
406
+ };
407
+ const matching = useMemo(() => [
408
+ {
409
+ label: "New thread",
410
+ icon: /* @__PURE__ */ jsx(SquarePen, { size: 16 }),
411
+ shortcut: "newThread",
412
+ run: () => {
413
+ navigate(`/new${thread ? `?project=${encodeURIComponent(thread.projectId)}` : ""}`);
414
+ }
415
+ },
416
+ {
417
+ label: "Settings",
418
+ icon: /* @__PURE__ */ jsx(Settings2, { size: 16 }),
419
+ run: () => {
420
+ navigate("/settings");
421
+ }
422
+ },
423
+ ...[],
424
+ {
425
+ label: "Toggle sidebar",
426
+ icon: /* @__PURE__ */ jsx(PanelLeft, { size: 16 }),
427
+ shortcut: "toggleSidebar",
428
+ run: onToggleSidebar
429
+ },
430
+ ...threads.length ? [{
431
+ label: "Next thread",
432
+ icon: /* @__PURE__ */ jsx(ArrowDown, { size: 16 }),
433
+ shortcut: "nextThread",
434
+ run: () => navigateThread(1)
435
+ }, {
436
+ label: "Previous thread",
437
+ icon: /* @__PURE__ */ jsx(ArrowUp, { size: 16 }),
438
+ shortcut: "previousThread",
439
+ run: () => navigateThread(-1)
440
+ }] : [],
441
+ {
442
+ label: "Group threads by project",
443
+ icon: /* @__PURE__ */ jsx(FolderTree, { size: 16 }),
444
+ detail: mode === "projects" ? "Selected" : void 0,
445
+ run: () => onModeChange("projects")
446
+ },
447
+ {
448
+ label: "Show all threads",
449
+ icon: /* @__PURE__ */ jsx(LayoutList, { size: 16 }),
450
+ detail: mode === "threads" ? "Selected" : void 0,
451
+ run: () => onModeChange("threads")
452
+ },
453
+ ...[
454
+ "system",
455
+ "light",
456
+ "dark"
457
+ ].map((theme) => ({
458
+ label: `${theme[0].toUpperCase()}${theme.slice(1)} theme`,
459
+ icon: theme === "system" ? /* @__PURE__ */ jsx(Monitor, { size: 16 }) : theme === "light" ? /* @__PURE__ */ jsx(Sun, { size: 16 }) : /* @__PURE__ */ jsx(Moon, { size: 16 }),
460
+ run: () => {
461
+ localStorage.setItem("webr-theme", theme);
462
+ applyTheme(theme);
463
+ window.dispatchEvent(new Event("webr-theme-change"));
464
+ }
465
+ })),
466
+ ...thread ? [
467
+ {
468
+ label: thread.archivedAt ? "Unarchive thread" : "Archive thread",
469
+ icon: thread.archivedAt ? /* @__PURE__ */ jsx(ArchiveRestore, { size: 16 }) : /* @__PURE__ */ jsx(Archive, { size: 16 }),
470
+ run: () => {
471
+ setThreadArchived(thread, !thread.archivedAt).catch(reportFailure$1);
472
+ }
473
+ },
474
+ {
475
+ label: "Copy thread link",
476
+ icon: /* @__PURE__ */ jsx(Copy, { size: 16 }),
477
+ run: () => {
478
+ navigator.clipboard.writeText(location.href);
479
+ }
480
+ },
481
+ ...thread.bindingState === "attached" ? threadShell(thread) ? [{
482
+ label: "Toggle terminal",
483
+ icon: /* @__PURE__ */ jsx(SquareTerminal, { size: 16 }),
484
+ shortcut: "toggleTerminal",
485
+ run: () => onToggleTerminal()
486
+ }, {
487
+ label: "Close terminal",
488
+ icon: /* @__PURE__ */ jsx(X, { size: 16 }),
489
+ detail: "Stops its shell",
490
+ run: onCloseTerminal
491
+ }] : [{
492
+ label: "Toggle terminal (side)",
493
+ icon: /* @__PURE__ */ jsx(PanelRight, { size: 16 }),
494
+ shortcut: "toggleTerminal",
495
+ run: () => onToggleTerminal("right")
496
+ }, {
497
+ label: "Toggle terminal (below)",
498
+ icon: /* @__PURE__ */ jsx(PanelBottom, { size: 16 }),
499
+ run: () => onToggleTerminal("down")
500
+ }] : [],
501
+ ...thread.bindingState === "attached" ? [
502
+ {
503
+ label: "Request terminal control",
504
+ icon: /* @__PURE__ */ jsx(TerminalSquare, { size: 16 }),
505
+ run: () => onTerminalAction("control")
506
+ },
507
+ {
508
+ label: "Release terminal control",
509
+ icon: /* @__PURE__ */ jsx(TerminalSquare, { size: 16 }),
510
+ run: () => onTerminalAction("release")
511
+ },
512
+ {
513
+ label: "Take over terminal control",
514
+ icon: /* @__PURE__ */ jsx(TerminalSquare, { size: 16 }),
515
+ run: () => {
516
+ confirmDialog("Replace the active controller of this terminal?", { confirmLabel: "Take over" }).then((accepted) => {
517
+ if (accepted) onTerminalAction("takeover");
518
+ });
519
+ }
520
+ }
521
+ ] : [],
522
+ {
523
+ label: "Next pane",
524
+ icon: /* @__PURE__ */ jsx(ArrowRight, { size: 16 }),
525
+ shortcut: "nextPane",
526
+ run: () => focusPane(1)
527
+ },
528
+ {
529
+ label: "Previous pane",
530
+ icon: /* @__PURE__ */ jsx(ArrowLeft, { size: 16 }),
531
+ shortcut: "previousPane",
532
+ run: () => focusPane(-1)
533
+ },
534
+ ...thread.panes.map((pane, index) => ({
535
+ label: `Focus ${pane.title}`,
536
+ icon: /* @__PURE__ */ jsx(TerminalSquare, { size: 16 }),
537
+ detail: `Pane ${index + 1}`,
538
+ run: () => onFocusPane(pane.id)
539
+ }))
540
+ ] : [],
541
+ ...threads.map((item) => ({
542
+ label: item.title,
543
+ icon: /* @__PURE__ */ jsx(ThreadAvatar, {
544
+ thread: item,
545
+ working: item.status === "working",
546
+ size: 22
547
+ }),
548
+ detail: projects.find((project) => project.id === item.projectId)?.name,
549
+ run: () => {
550
+ navigate(`/threads/${encodeURIComponent(item.id)}`);
551
+ }
552
+ }))
553
+ ], [
554
+ projects,
555
+ threads,
556
+ thread,
557
+ focusedPane,
558
+ mode,
559
+ onModeChange,
560
+ onToggleSidebar,
561
+ onFocusPane,
562
+ onTerminalAction,
563
+ onToggleTerminal,
564
+ onCloseTerminal
565
+ ]).filter((command) => `${command.label} ${command.detail ?? ""}`.toLowerCase().includes(query.toLowerCase().trim()));
566
+ if (!open) return null;
567
+ const run = (command) => {
568
+ onClose();
569
+ command.run();
570
+ };
571
+ return /* @__PURE__ */ jsx("div", {
572
+ className: "palette-overlay",
573
+ onMouseDown: (event) => {
574
+ if (event.target === event.currentTarget) onClose();
575
+ },
576
+ children: /* @__PURE__ */ jsxs("div", {
577
+ className: "palette",
578
+ role: "dialog",
579
+ "aria-modal": "true",
580
+ "aria-label": "Command palette",
581
+ onKeyDown: (event) => {
582
+ if (event.key === "Escape") {
583
+ event.preventDefault();
584
+ onClose();
585
+ }
586
+ if (event.key === "ArrowDown") {
587
+ event.preventDefault();
588
+ setSelected((value) => (value + 1) % (matching.length || 1));
589
+ }
590
+ if (event.key === "ArrowUp") {
591
+ event.preventDefault();
592
+ setSelected((value) => (value - 1 + (matching.length || 1)) % (matching.length || 1));
593
+ }
594
+ if (event.key === "Enter" && matching.length) {
595
+ event.preventDefault();
596
+ run(matching[Math.min(selected, matching.length - 1)]);
597
+ }
598
+ if (event.key === "Tab") {
599
+ event.preventDefault();
600
+ input.current?.focus();
601
+ }
602
+ },
603
+ children: [
604
+ /* @__PURE__ */ jsx("input", {
605
+ ref: input,
606
+ "aria-label": "Search commands and threads",
607
+ placeholder: "Search threads and actions…",
608
+ value: query,
609
+ onChange: (event) => {
610
+ setQuery(event.target.value);
611
+ setSelected(0);
612
+ }
613
+ }),
614
+ /* @__PURE__ */ jsxs("div", {
615
+ className: "palette-results",
616
+ role: "listbox",
617
+ "aria-label": "Results",
618
+ children: [matching.map((command, index) => /* @__PURE__ */ jsxs("button", {
619
+ role: "option",
620
+ "aria-selected": selected === index,
621
+ className: selected === index ? "is-selected" : "",
622
+ onMouseEnter: () => setSelected(index),
623
+ onClick: () => run(command),
624
+ children: [
625
+ /* @__PURE__ */ jsx("span", {
626
+ className: "palette-icon",
627
+ "aria-hidden": "true",
628
+ children: command.icon
629
+ }),
630
+ /* @__PURE__ */ jsx("span", {
631
+ className: "palette-label",
632
+ children: command.label
633
+ }),
634
+ command.detail && /* @__PURE__ */ jsx("small", { children: command.detail }),
635
+ command.shortcut && /* @__PURE__ */ jsx(Tooltip$1, {
636
+ label: shortcuts[command.shortcut],
637
+ children: /* @__PURE__ */ jsx("kbd", { children: formatShortcut(shortcuts[command.shortcut]) })
638
+ })
639
+ ]
640
+ }, `${command.label}-${index}`)), !matching.length && /* @__PURE__ */ jsx("p", { children: "No results" })]
641
+ }),
642
+ /* @__PURE__ */ jsxs("div", {
643
+ className: "palette-help",
644
+ children: [
645
+ "↑↓ Navigate ",
646
+ /* @__PURE__ */ jsx("span", { children: "↵ Select" }),
647
+ " ",
648
+ /* @__PURE__ */ jsx("span", { children: "Esc Close" })
649
+ ]
650
+ })
651
+ ]
652
+ })
653
+ });
654
+ }
655
+ //#endregion
656
+ //#region src/client/pairing.ts
657
+ var POLL_INTERVAL_MS = 2e3;
658
+ var post = (path) => fetch(path, {
659
+ method: "POST",
660
+ headers: { "Content-Type": "application/json" },
661
+ body: "{}"
662
+ });
663
+ async function createInvite() {
664
+ const response = await post("/api/pair/invites");
665
+ if (!response.ok) throw new Error("Webr could not create a code.");
666
+ return response.json();
667
+ }
668
+ async function listDevices() {
669
+ const response = await fetch("/api/devices");
670
+ if (!response.ok) throw new Error("Webr could not list devices.");
671
+ return response.json();
672
+ }
673
+ async function listAuditEvents() {
674
+ const response = await fetch("/api/audit");
675
+ if (!response.ok) throw new Error("Webr could not load recent activity.");
676
+ return response.json();
677
+ }
678
+ var revokeDevice = (id) => post(`/api/devices/${encodeURIComponent(id)}/revoke`);
679
+ var decidePairRequest = (id, decision) => post(`/api/pair/${encodeURIComponent(id)}/${decision}`);
680
+ async function fetchPending() {
681
+ try {
682
+ const response = await fetch("/api/pair/pending");
683
+ return response.ok ? await response.json() : [];
684
+ } catch {
685
+ return [];
686
+ }
687
+ }
688
+ var notificationsGranted = () => typeof Notification !== "undefined" && Notification.permission === "granted";
689
+ var canWatchInBackground = () => document.visibilityState === "visible" || notificationsGranted();
690
+ function notifyHiddenTab(requests, alreadyNotified) {
691
+ if (document.visibilityState === "visible" || !notificationsGranted()) return;
692
+ for (const request of requests) {
693
+ if (alreadyNotified.has(request.id)) continue;
694
+ alreadyNotified.add(request.id);
695
+ new Notification("A device wants to connect to Webr", {
696
+ body: `${request.deviceName} · ${request.source}`,
697
+ tag: request.id
698
+ });
699
+ }
700
+ }
701
+ function usePendingPairRequests() {
702
+ const [pending, setPending] = useState([]);
703
+ const notified = useRef(/* @__PURE__ */ new Set());
704
+ const refresh = useCallback(async () => {
705
+ if (!canWatchInBackground()) return;
706
+ const requests = await fetchPending();
707
+ notifyHiddenTab(requests, notified.current);
708
+ setPending(requests);
709
+ }, []);
710
+ useEffect(() => {
711
+ refresh();
712
+ const timer = setInterval(() => void refresh(), POLL_INTERVAL_MS);
713
+ document.addEventListener("visibilitychange", refresh);
714
+ return () => {
715
+ clearInterval(timer);
716
+ document.removeEventListener("visibilitychange", refresh);
717
+ };
718
+ }, [refresh]);
719
+ return {
720
+ pending,
721
+ decide: useCallback(async (id, decision) => {
722
+ setPending((current) => current.filter((request) => request.id !== id));
723
+ await decidePairRequest(id, decision).catch(() => void 0);
724
+ await refresh();
725
+ }, [refresh])
726
+ };
727
+ }
728
+ function relativeTime$1(timestamp, now = Date.now()) {
729
+ const seconds = Math.round((timestamp - now) / 1e3);
730
+ const [unit, size] = [
731
+ ["day", 86400],
732
+ ["hour", 3600],
733
+ ["minute", 60]
734
+ ].find(([, size]) => Math.abs(seconds) >= size) ?? ["second", 1];
735
+ return Math.abs(seconds) < 45 ? "just now" : new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(Math.round(seconds / size), unit);
736
+ }
737
+ var notificationsSupported = () => typeof Notification !== "undefined";
738
+ var notificationPermission = () => notificationsSupported() ? Notification.permission : "denied";
739
+ var requestNotificationPermission = () => Notification.requestPermission();
740
+ function describeAuditEvent({ kind, deviceName, source }) {
741
+ const device = deviceName ?? "A device";
742
+ const where = source ? ` from ${source}` : "";
743
+ return {
744
+ pair_requested: `${device} asked to connect${where}`,
745
+ pair_approved: `${device} was approved${where}`,
746
+ pair_denied: `${device} was declined${where}`,
747
+ invite_created: `A connection code was created${where}`,
748
+ invite_redeemed: `${device} connected with a code${where}`,
749
+ device_revoked: `${device} was disconnected`,
750
+ session_expired: `${device} was signed out after being idle`
751
+ }[kind];
752
+ }
753
+ function relativeFuture(timestamp, now = Date.now()) {
754
+ return timestamp <= now ? "expired" : new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(...futureUnit(timestamp - now));
755
+ }
756
+ function futureUnit(milliseconds) {
757
+ const days = Math.round(milliseconds / 864e5);
758
+ return days >= 1 ? [days, "day"] : [Math.max(1, Math.round(milliseconds / 36e5)), "hour"];
759
+ }
760
+ //#endregion
761
+ //#region src/components/PairingPrompt.tsx
762
+ function PairingPrompt() {
763
+ const { pending, decide } = usePendingPairRequests();
764
+ const request = pending[0];
765
+ return /* @__PURE__ */ jsx(AlertDialog.Root, {
766
+ open: !!request,
767
+ onOpenChange: (open) => {
768
+ if (!open && request) decide(request.id, "deny");
769
+ },
770
+ children: /* @__PURE__ */ jsxs(AlertDialog.Portal, { children: [/* @__PURE__ */ jsx(AlertDialog.Backdrop, { className: "dialog-backdrop" }), /* @__PURE__ */ jsx(AlertDialog.Popup, {
771
+ className: "dialog pairing-prompt",
772
+ children: request && /* @__PURE__ */ jsxs(Fragment$1, { children: [
773
+ /* @__PURE__ */ jsxs(AlertDialog.Title, {
774
+ className: "pairing-title",
775
+ children: [
776
+ "Allow ",
777
+ request.deviceName,
778
+ " to connect?"
779
+ ]
780
+ }),
781
+ /* @__PURE__ */ jsxs(AlertDialog.Description, {
782
+ className: "dialog-message",
783
+ children: [
784
+ "Requested from ",
785
+ request.source,
786
+ ". Only approve if this code matches the one shown on that device."
787
+ ]
788
+ }),
789
+ /* @__PURE__ */ jsx("div", {
790
+ className: "pairing-code",
791
+ "aria-label": `Code ${request.code}`,
792
+ children: request.code
793
+ }),
794
+ pending.length > 1 && /* @__PURE__ */ jsxs("p", {
795
+ className: "settings-note",
796
+ children: [pending.length - 1, " more waiting"]
797
+ }),
798
+ /* @__PURE__ */ jsxs("div", {
799
+ className: "dialog-actions",
800
+ children: [/* @__PURE__ */ jsx("button", {
801
+ type: "button",
802
+ className: "dialog-button",
803
+ onClick: () => void decide(request.id, "deny"),
804
+ children: "Deny"
805
+ }), /* @__PURE__ */ jsx("button", {
806
+ type: "button",
807
+ autoFocus: true,
808
+ className: "dialog-button is-primary",
809
+ onClick: () => void decide(request.id, "approve"),
810
+ children: "Approve"
811
+ })]
812
+ })
813
+ ] })
814
+ })] })
815
+ });
816
+ }
817
+ //#endregion
818
+ //#region src/components/CrashReport.tsx
819
+ var CrashReport = ({ children }) => /* @__PURE__ */ jsx(Fragment$1, { children });
820
+ //#endregion
821
+ //#region src/client/image-attach.ts
822
+ var attachableTypes = [
823
+ "image/png",
824
+ "image/jpeg",
825
+ "image/gif",
826
+ "image/webp"
827
+ ];
828
+ var uploadErrors = {
829
+ image_too_large: "This image is larger than 20 MB.",
830
+ unsupported_image: "Only PNG, JPEG, GIF and WebP images can be attached.",
831
+ uploads_unsupported_target: "Images can be attached only on the Local machine for now.",
832
+ machine_disconnected: "The machine is not connected."
833
+ };
834
+ var isAttachableImage = (file) => attachableTypes.includes(file.type);
835
+ var imageFilesFrom = (data) => data?.files ? [...data.files].filter(isAttachableImage) : [];
836
+ var carriesFiles = (data) => !!data?.types && [...data.types].includes("Files");
837
+ var escapePathForTerminal = (path) => path.replace(/([\s\\'"()&;$`!*?[\]{}<>|#~])/g, "\\$1");
838
+ async function uploadImage(machineId, file) {
839
+ if (!isAttachableImage(file)) throw new Error(uploadErrors.unsupported_image);
840
+ if (file.size > 20971520) throw new Error(uploadErrors.image_too_large);
841
+ const response = await fetch(`/api/uploads?machineId=${encodeURIComponent(machineId)}`, {
842
+ method: "POST",
843
+ headers: { "Content-Type": file.type },
844
+ body: file
845
+ });
846
+ const result = await response.json().catch(() => ({}));
847
+ if (!response.ok || !result.path) throw new Error(uploadErrors[result.error ?? ""] ?? "The image could not be attached.");
848
+ return result.path;
849
+ }
850
+ var THUMBNAIL_SIZE = 160;
851
+ var readAsDataUrl = (file) => new Promise((done, fail) => {
852
+ const reader = new FileReader();
853
+ reader.onload = () => done(String(reader.result));
854
+ reader.onerror = () => fail(reader.error);
855
+ reader.readAsDataURL(file);
856
+ });
857
+ async function thumbnailDataUrl(file) {
858
+ if (typeof createImageBitmap !== "function") return readAsDataUrl(file);
859
+ const bitmap = await createImageBitmap(file);
860
+ const scale = Math.min(1, THUMBNAIL_SIZE / Math.max(bitmap.width, bitmap.height));
861
+ const canvas = document.createElement("canvas");
862
+ canvas.width = Math.max(1, Math.round(bitmap.width * scale));
863
+ canvas.height = Math.max(1, Math.round(bitmap.height * scale));
864
+ canvas.getContext("2d")?.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
865
+ bitmap.close();
866
+ return canvas.toDataURL("image/jpeg", .8);
867
+ }
868
+ async function uploadImagesAsTerminalText(machineId, files) {
869
+ const paths = [];
870
+ for (const file of files) paths.push(await uploadImage(machineId, file));
871
+ return paths.map(escapePathForTerminal).join(" ") + " ";
872
+ }
873
+ //#endregion
874
+ //#region src/lib/uuid.ts
875
+ var hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
876
+ function uuid() {
877
+ if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
878
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
879
+ bytes[6] = bytes[6] & 15 | 64;
880
+ bytes[8] = bytes[8] & 63 | 128;
881
+ const text = hex(bytes);
882
+ return `${text.slice(0, 8)}-${text.slice(8, 12)}-${text.slice(12, 16)}-${text.slice(16, 20)}-${text.slice(20)}`;
883
+ }
884
+ //#endregion
885
+ //#region src/components/usePromptImages.ts
886
+ var ONLY_IMAGES$1 = "Only PNG, JPEG, GIF and WebP images can be attached.";
887
+ var promptWithImages = (prompt, images) => [prompt.trim(), ...images.map((image) => escapePathForTerminal(image.path))].filter(Boolean).join(" ");
888
+ function usePromptImages({ machineId }) {
889
+ const [dragging, setDragging] = useState(false);
890
+ const [attachments, setAttachments] = useState([]);
891
+ const [uploading, setUploading] = useState(0);
892
+ const [error, setError] = useState("");
893
+ const depth = useRef(0);
894
+ const attach = async (files) => {
895
+ if (!files.length) {
896
+ setError(ONLY_IMAGES$1);
897
+ return;
898
+ }
899
+ setError("");
900
+ setUploading((count) => count + files.length);
901
+ for (const file of files) try {
902
+ const path = await uploadImage(machineId, file);
903
+ const preview = await thumbnailDataUrl(file);
904
+ setAttachments((current) => [...current, {
905
+ id: uuid(),
906
+ name: file.name || "image",
907
+ preview,
908
+ path
909
+ }]);
910
+ } catch (cause) {
911
+ setError(cause instanceof Error ? cause.message : "The image could not be attached.");
912
+ } finally {
913
+ setUploading((count) => count - 1);
914
+ }
915
+ };
916
+ const remove = (id) => setAttachments((current) => current.filter((image) => image.id !== id));
917
+ return {
918
+ dragging,
919
+ attachments,
920
+ setAttachments,
921
+ uploading,
922
+ error,
923
+ remove,
924
+ handlers: {
925
+ onDragEnter: (event) => {
926
+ if (!carriesFiles(event.dataTransfer)) return;
927
+ event.preventDefault();
928
+ depth.current += 1;
929
+ setDragging(true);
930
+ },
931
+ onDragOver: (event) => {
932
+ if (!carriesFiles(event.dataTransfer)) return;
933
+ event.preventDefault();
934
+ event.dataTransfer.dropEffect = "copy";
935
+ },
936
+ onDragLeave: (event) => {
937
+ if (!carriesFiles(event.dataTransfer)) return;
938
+ depth.current = Math.max(0, depth.current - 1);
939
+ if (!depth.current) setDragging(false);
940
+ },
941
+ onDrop: (event) => {
942
+ if (!carriesFiles(event.dataTransfer)) return;
943
+ event.preventDefault();
944
+ depth.current = 0;
945
+ setDragging(false);
946
+ attach(imageFilesFrom(event.dataTransfer));
947
+ },
948
+ onPaste: (event) => {
949
+ const images = imageFilesFrom(event.clipboardData);
950
+ if (!images.length) return;
951
+ event.preventDefault();
952
+ attach(images);
953
+ }
954
+ }
955
+ };
956
+ }
957
+ //#endregion
958
+ //#region src/components/ComposerCombobox.tsx
959
+ function ComposerCombobox({ label, value, options, onChange, icon, allowCustom = false, action }) {
960
+ const [query, setQuery] = useState("");
961
+ const selected = options.find((option) => option.value === value) ?? {
962
+ value,
963
+ label: value
964
+ };
965
+ const search = query.trim();
966
+ const items = options.filter((option) => option.label.toLowerCase().includes(search.toLowerCase()));
967
+ const custom = allowCustom && search && !options.some((option) => option.value === search) ? {
968
+ value: search,
969
+ label: `Use “${search}”`
970
+ } : null;
971
+ return /* @__PURE__ */ jsxs(Combobox.Root, {
972
+ items: custom ? [...items, custom] : items,
973
+ value: selected,
974
+ filter: null,
975
+ inputValue: query,
976
+ onInputValueChange: setQuery,
977
+ onOpenChange: () => setQuery(""),
978
+ itemToStringLabel: (option) => option.label,
979
+ isItemEqualToValue: (a, b) => a.value === b.value,
980
+ onValueChange: (option) => {
981
+ if (option && !option.disabled) onChange(option.value);
982
+ },
983
+ children: [/* @__PURE__ */ jsx(Tooltip$1, {
984
+ label: `${label}: ${selected.label}`,
985
+ children: /* @__PURE__ */ jsxs(Combobox.Trigger, {
986
+ className: "composer-select",
987
+ "aria-label": `${label}: ${selected.label}`,
988
+ children: [
989
+ /* @__PURE__ */ jsx("span", {
990
+ className: "composer-select-icon",
991
+ "aria-hidden": "true",
992
+ children: selected.icon ?? icon
993
+ }),
994
+ /* @__PURE__ */ jsx("span", {
995
+ className: "composer-select-label",
996
+ children: selected.shortLabel ?? (selected.label || label)
997
+ }),
998
+ /* @__PURE__ */ jsx(ChevronDown, {
999
+ className: "composer-select-caret",
1000
+ size: 11,
1001
+ "aria-hidden": "true"
1002
+ })
1003
+ ]
1004
+ })
1005
+ }), /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsx(Combobox.Positioner, {
1006
+ className: "composer-select-positioner",
1007
+ sideOffset: 6,
1008
+ align: "start",
1009
+ collisionPadding: 12,
1010
+ children: /* @__PURE__ */ jsxs(Combobox.Popup, {
1011
+ className: "composer-select-popup",
1012
+ "aria-label": `${label} options`,
1013
+ children: [
1014
+ /* @__PURE__ */ jsxs("div", {
1015
+ className: "composer-select-search",
1016
+ children: [/* @__PURE__ */ jsx(Search, {
1017
+ size: 14,
1018
+ "aria-hidden": "true"
1019
+ }), /* @__PURE__ */ jsx(Combobox.Input, {
1020
+ "aria-label": `Search ${label.toLowerCase()}`,
1021
+ placeholder: `Search ${label.toLowerCase()}…`,
1022
+ maxLength: allowCustom ? 120 : void 0
1023
+ })]
1024
+ }),
1025
+ /* @__PURE__ */ jsx(Combobox.Empty, {
1026
+ className: "composer-select-empty",
1027
+ children: "No matches"
1028
+ }),
1029
+ /* @__PURE__ */ jsx(Combobox.List, {
1030
+ className: "composer-select-list",
1031
+ children: (option) => /* @__PURE__ */ jsxs(Combobox.Item, {
1032
+ value: option,
1033
+ disabled: option.disabled,
1034
+ className: "composer-select-option",
1035
+ children: [
1036
+ option.icon && /* @__PURE__ */ jsx("span", {
1037
+ "aria-hidden": "true",
1038
+ children: option.icon
1039
+ }),
1040
+ /* @__PURE__ */ jsx("span", {
1041
+ className: "composer-select-option-label",
1042
+ children: option.label
1043
+ }),
1044
+ /* @__PURE__ */ jsx(Combobox.ItemIndicator, {
1045
+ className: "composer-select-check",
1046
+ children: /* @__PURE__ */ jsx(Check, { size: 14 })
1047
+ })
1048
+ ]
1049
+ }, option.value)
1050
+ }),
1051
+ action && /* @__PURE__ */ jsxs("button", {
1052
+ type: "button",
1053
+ className: "composer-select-action",
1054
+ onClick: action.onClick,
1055
+ children: [action.icon, /* @__PURE__ */ jsx("span", { children: action.label })]
1056
+ })
1057
+ ]
1058
+ })
1059
+ }) })]
1060
+ });
1061
+ }
1062
+ //#endregion
1063
+ //#region src/components/ProjectIcon.tsx
1064
+ function ProjectIcon({ project }) {
1065
+ const image = useRef(null);
1066
+ const [loaded, setLoaded] = useState(false);
1067
+ const [failed, setFailed] = useState(false);
1068
+ useEffect(() => {
1069
+ if (image.current?.complete) {
1070
+ setLoaded(image.current.naturalWidth > 0);
1071
+ setFailed(image.current.naturalWidth === 0);
1072
+ }
1073
+ }, [project.iconUrl]);
1074
+ return /* @__PURE__ */ jsxs("span", {
1075
+ className: `thread-project ${loaded ? "has-icon" : ""}`,
1076
+ style: { "--project-color": project.color },
1077
+ "aria-hidden": "true",
1078
+ children: [/* @__PURE__ */ jsx("span", {
1079
+ className: "thread-project-letter",
1080
+ children: project.initial
1081
+ }), project.iconUrl && !failed && /* @__PURE__ */ jsx("img", {
1082
+ ref: image,
1083
+ src: project.iconUrl,
1084
+ alt: "",
1085
+ decoding: "async",
1086
+ onLoad: () => setLoaded(true),
1087
+ onError: () => setFailed(true)
1088
+ })]
1089
+ });
1090
+ }
1091
+ //#endregion
1092
+ //#region src/components/ProjectPicker.tsx
1093
+ function ProjectPicker({ projects, projectId, onChange, onAddProject }) {
1094
+ return /* @__PURE__ */ jsx(ComposerCombobox, {
1095
+ label: "Project",
1096
+ value: projectId,
1097
+ onChange,
1098
+ icon: /* @__PURE__ */ jsx(Folder, { size: 14 }),
1099
+ action: onAddProject ? {
1100
+ label: "Add project",
1101
+ icon: /* @__PURE__ */ jsx(Plus, { size: 14 }),
1102
+ onClick: onAddProject
1103
+ } : void 0,
1104
+ options: projects.map((project) => ({
1105
+ value: project.id,
1106
+ label: project.name,
1107
+ icon: /* @__PURE__ */ jsx(ProjectIcon, { project }, project.id)
1108
+ }))
1109
+ });
1110
+ }
1111
+ //#endregion
1112
+ //#region src/components/LaunchSelectors.tsx
1113
+ function LaunchSelectors({ machine, machines, projects, projectId, harness, model, onMachineChange, onProjectChange, onHarnessChange, onModelChange, onAddProject }) {
1114
+ const models = machine.harnesses.find((choice) => choice.id === harness)?.models ?? [];
1115
+ return /* @__PURE__ */ jsxs("div", {
1116
+ className: "composer-selectors",
1117
+ children: [
1118
+ /* @__PURE__ */ jsx(ComposerCombobox, {
1119
+ label: "Machine",
1120
+ value: machine.id,
1121
+ onChange: onMachineChange,
1122
+ icon: /* @__PURE__ */ jsx(Monitor, { size: 14 }),
1123
+ options: machines.map((choice) => ({
1124
+ value: choice.id,
1125
+ label: `${choice.name}${choice.connected ? "" : " · Not connected"}`,
1126
+ shortLabel: choice.name,
1127
+ disabled: !choice.connected
1128
+ }))
1129
+ }),
1130
+ /* @__PURE__ */ jsx(ProjectPicker, {
1131
+ projects,
1132
+ projectId,
1133
+ onChange: onProjectChange,
1134
+ onAddProject
1135
+ }, machine.id),
1136
+ /* @__PURE__ */ jsx(ComposerCombobox, {
1137
+ label: "Harness",
1138
+ value: harness,
1139
+ onChange: onHarnessChange,
1140
+ icon: /* @__PURE__ */ jsx(TerminalSquare, { size: 14 }),
1141
+ options: machine.harnesses.map((choice) => ({
1142
+ value: choice.id,
1143
+ label: choice.name,
1144
+ shortLabel: choice.name === "Claude Code" ? "Claude" : choice.name
1145
+ }))
1146
+ }),
1147
+ /* @__PURE__ */ jsx(ComposerCombobox, {
1148
+ label: "Model",
1149
+ value: model,
1150
+ onChange: onModelChange,
1151
+ icon: /* @__PURE__ */ jsx(Cpu, { size: 14 }),
1152
+ allowCustom: machine.harnesses.find((choice) => choice.id === harness)?.customModels ?? false,
1153
+ options: models.map((name) => ({
1154
+ value: name,
1155
+ label: name,
1156
+ shortLabel: name.split("/").at(-1)
1157
+ }))
1158
+ }, `${machine.id}:${harness}`)
1159
+ ]
1160
+ });
1161
+ }
1162
+ //#endregion
1163
+ //#region src/client/catalog.ts
1164
+ function useMachineCatalog(machineId, connected, configVersion = 1, session = "", projectId = "") {
1165
+ return useQuery({
1166
+ queryKey: [
1167
+ "machine-catalog",
1168
+ machineId,
1169
+ session,
1170
+ configVersion,
1171
+ projectId
1172
+ ],
1173
+ enabled: !!machineId && connected,
1174
+ queryFn: async ({ signal }) => {
1175
+ const response = await fetch("/api/catalog/" + encodeURIComponent(machineId) + (projectId ? "?projectId=" + encodeURIComponent(projectId) : ""), { signal });
1176
+ if (!response.ok) throw new Error("Machine catalog is not available.");
1177
+ return await response.json();
1178
+ }
1179
+ });
1180
+ }
1181
+ //#endregion
1182
+ //#region src/client/directory-suggestions.ts
1183
+ var looksLikePath = (value) => value.startsWith("/") || value.startsWith("~");
1184
+ var onlyStrings = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1185
+ function useDirectorySuggestions(machineId, prefix) {
1186
+ const [suggestions, setSuggestions] = useState([]);
1187
+ useEffect(() => {
1188
+ if (!looksLikePath(prefix)) {
1189
+ setSuggestions([]);
1190
+ return;
1191
+ }
1192
+ const controller = new AbortController();
1193
+ fetch(`/api/directories?machineId=${encodeURIComponent(machineId)}&prefix=${encodeURIComponent(prefix)}`, { signal: controller.signal }).then((response) => response.ok ? response.json() : []).then((body) => setSuggestions(onlyStrings(body))).catch(() => {});
1194
+ return () => controller.abort();
1195
+ }, [machineId, prefix]);
1196
+ return suggestions;
1197
+ }
1198
+ //#endregion
1199
+ //#region src/components/CreateWorkspace.tsx
1200
+ var isAbsoluteOrHome = (path) => path.startsWith("/") || path.startsWith("~/");
1201
+ function CreateWorkspace({ machineId, onCreated }) {
1202
+ const [path, setPath] = useState("");
1203
+ const [label, setLabel] = useState("");
1204
+ const [busy, setBusy] = useState(false);
1205
+ const [error, setError] = useState("");
1206
+ const [highlighted, setHighlighted] = useState(0);
1207
+ const [open, setOpen] = useState(false);
1208
+ const suggestions = useDirectorySuggestions(machineId, path);
1209
+ const showing = open && suggestions.length > 0 && !(suggestions.length === 1 && suggestions[0] === path);
1210
+ const accept = (value) => {
1211
+ setPath(value);
1212
+ setHighlighted(0);
1213
+ setOpen(true);
1214
+ };
1215
+ const onKeyDown = (event) => {
1216
+ if (!showing) return;
1217
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1218
+ event.preventDefault();
1219
+ setHighlighted((index) => (index + (event.key === "ArrowDown" ? 1 : suggestions.length - 1)) % suggestions.length);
1220
+ } else if (event.key === "Tab") {
1221
+ event.preventDefault();
1222
+ accept(suggestions[highlighted] ?? suggestions[0]);
1223
+ } else if (event.key === "Enter") {
1224
+ event.preventDefault();
1225
+ accept(suggestions[highlighted] ?? suggestions[0]);
1226
+ setOpen(false);
1227
+ } else if (event.key === "Escape") setOpen(false);
1228
+ };
1229
+ const operation = useRef(null);
1230
+ const submit = async (event) => {
1231
+ event.preventDefault();
1232
+ setBusy(true);
1233
+ setError("");
1234
+ const payload = JSON.stringify({
1235
+ machineId,
1236
+ path,
1237
+ label
1238
+ });
1239
+ if (operation.current?.payload !== payload) operation.current = {
1240
+ payload,
1241
+ key: uuid()
1242
+ };
1243
+ try {
1244
+ const response = await fetch("/api/workspaces", {
1245
+ method: "POST",
1246
+ headers: {
1247
+ "Content-Type": "application/json",
1248
+ "Idempotency-Key": operation.current.key
1249
+ },
1250
+ body: payload
1251
+ });
1252
+ const result = await response.json();
1253
+ if (!response.ok) throw new Error(result.error ?? "Could not create workspace");
1254
+ if (result.state === "unknown") throw new Error("The result is not known; check Herdr before you create another workspace.");
1255
+ onCreated();
1256
+ } catch (cause) {
1257
+ setError(cause instanceof Error ? cause.message : "Could not create workspace");
1258
+ } finally {
1259
+ setBusy(false);
1260
+ }
1261
+ };
1262
+ return /* @__PURE__ */ jsxs("form", {
1263
+ className: "thread-composer workspace-composer",
1264
+ onSubmit: submit,
1265
+ "aria-label": "Create workspace",
1266
+ "aria-busy": busy,
1267
+ children: [
1268
+ /* @__PURE__ */ jsxs("label", {
1269
+ className: "path-field",
1270
+ children: [
1271
+ "Project folder",
1272
+ /* @__PURE__ */ jsx("input", {
1273
+ value: path,
1274
+ onChange: (event) => {
1275
+ setPath(event.target.value);
1276
+ setHighlighted(0);
1277
+ setOpen(true);
1278
+ },
1279
+ onKeyDown,
1280
+ onBlur: () => setOpen(false),
1281
+ placeholder: "/absolute/path/to/project",
1282
+ autoComplete: "off",
1283
+ spellCheck: false,
1284
+ role: "combobox",
1285
+ "aria-expanded": showing,
1286
+ "aria-autocomplete": "list",
1287
+ required: true,
1288
+ pattern: "(/|~/).*"
1289
+ }),
1290
+ showing && /* @__PURE__ */ jsx("ul", {
1291
+ className: "path-suggestions",
1292
+ role: "listbox",
1293
+ children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx("li", {
1294
+ role: "option",
1295
+ "aria-selected": index === highlighted,
1296
+ "data-highlighted": index === highlighted || void 0,
1297
+ onMouseDown: (event) => {
1298
+ event.preventDefault();
1299
+ accept(suggestion);
1300
+ },
1301
+ children: suggestion
1302
+ }, suggestion))
1303
+ })
1304
+ ]
1305
+ }),
1306
+ /* @__PURE__ */ jsxs("label", { children: ["Workspace name", /* @__PURE__ */ jsx("input", {
1307
+ value: label,
1308
+ onChange: (event) => setLabel(event.target.value),
1309
+ required: true,
1310
+ maxLength: 80
1311
+ })] }),
1312
+ /* @__PURE__ */ jsx("button", {
1313
+ type: "submit",
1314
+ disabled: busy || !isAbsoluteOrHome(path) || !label.trim(),
1315
+ children: busy ? "Creating…" : "Create workspace"
1316
+ }),
1317
+ error && /* @__PURE__ */ jsx("p", {
1318
+ className: "form-error",
1319
+ role: "alert",
1320
+ children: error
1321
+ })
1322
+ ]
1323
+ });
1324
+ }
1325
+ //#endregion
1326
+ //#region src/client/new-session-behavior.ts
1327
+ var newSessionBehaviorLabels = {
1328
+ main: "Main checkout",
1329
+ worktree: "New worktree",
1330
+ remember: "Remember by project"
1331
+ };
1332
+ var behaviorKey = "webr-new-session-behavior";
1333
+ var legacyWorktreeKey = "webr-new-worktree";
1334
+ var lastWorktreeKey = (machineId, projectId) => `webr-last-worktree:${machineId}:${projectId}`;
1335
+ var isBehavior = (value) => value === "main" || value === "worktree" || value === "remember";
1336
+ var behaviorFromLegacySetting = () => localStorage.getItem(legacyWorktreeKey) === "false" ? "main" : "worktree";
1337
+ function readNewSessionBehavior() {
1338
+ const stored = localStorage.getItem(behaviorKey);
1339
+ return isBehavior(stored) ? stored : behaviorFromLegacySetting();
1340
+ }
1341
+ function writeNewSessionBehavior(behavior) {
1342
+ localStorage.setItem(behaviorKey, behavior);
1343
+ }
1344
+ function recordWorktreeChoice(machineId, projectId, worktree) {
1345
+ localStorage.setItem(lastWorktreeKey(machineId, projectId), String(worktree));
1346
+ }
1347
+ var rememberedWorktreeChoice = (machineId, projectId) => localStorage.getItem(lastWorktreeKey(machineId, projectId)) !== "false";
1348
+ function initialWorktreeChoice(machineId, projectId) {
1349
+ const behavior = readNewSessionBehavior();
1350
+ return behavior === "remember" ? rememberedWorktreeChoice(machineId, projectId) : behavior === "worktree";
1351
+ }
1352
+ //#endregion
1353
+ //#region src/client/new-thread-draft.ts
1354
+ var draftKey = "webr-new-thread-draft";
1355
+ var isImage = (value) => !!value && typeof value === "object" && [
1356
+ "id",
1357
+ "name",
1358
+ "preview",
1359
+ "path"
1360
+ ].every((field) => typeof value[field] === "string");
1361
+ function readNewThreadDraft() {
1362
+ try {
1363
+ const stored = JSON.parse(localStorage.getItem(draftKey) ?? "{}");
1364
+ return {
1365
+ prompt: typeof stored.prompt === "string" ? stored.prompt : "",
1366
+ images: Array.isArray(stored.images) ? stored.images.filter(isImage) : []
1367
+ };
1368
+ } catch {
1369
+ return {
1370
+ prompt: "",
1371
+ images: []
1372
+ };
1373
+ }
1374
+ }
1375
+ function writeNewThreadDraft(draft) {
1376
+ try {
1377
+ if (!draft.prompt && !draft.images.length) localStorage.removeItem(draftKey);
1378
+ else localStorage.setItem(draftKey, JSON.stringify(draft));
1379
+ } catch {}
1380
+ }
1381
+ var clearNewThreadDraft = () => writeNewThreadDraft({
1382
+ prompt: "",
1383
+ images: []
1384
+ });
1385
+ //#endregion
1386
+ //#region src/components/NewThreadView.tsx
1387
+ var harnessPreference = (machineId, projectId) => `webr-last-harness:${machineId}:${projectId}`;
1388
+ function NewThreadView({ projects, machines, selectedProjectId, onOpenSidebar }) {
1389
+ const [projectId, setProjectId] = useState(projects.find((project) => project.id === selectedProjectId)?.id ?? projects[0]?.id ?? "");
1390
+ const [machineId, setMachineId] = useState(machines.find((machine) => selectedProjectId && Object.hasOwn(machine.projectPaths, selectedProjectId))?.id ?? machines[0]?.id ?? "");
1391
+ const [harness, setHarness] = useState("");
1392
+ const [model, setModel] = useState("Default");
1393
+ const [worktree, setWorktree] = useState(true);
1394
+ const [prompt, setPrompt] = useState("");
1395
+ const [busy, setBusy] = useState(false);
1396
+ const [error, setError] = useState("");
1397
+ const [creatingWorkspace, setCreatingWorkspace] = useState(false);
1398
+ const operation = useRef(null);
1399
+ const images = usePromptImages({ machineId });
1400
+ const baseMachine = machines.find((machine) => machine.id === machineId) ?? {
1401
+ id: "",
1402
+ name: "No target",
1403
+ connected: false,
1404
+ session: "",
1405
+ projectPaths: {},
1406
+ harnesses: []
1407
+ };
1408
+ const catalog = useMachineCatalog(machineId, baseMachine.connected, baseMachine.configVersion, baseMachine.session, projectId);
1409
+ const machine = {
1410
+ ...baseMachine,
1411
+ harnesses: baseMachine.connected ? catalog.data?.harnesses ?? [] : []
1412
+ };
1413
+ const machineProjects = projects.filter((project) => Object.hasOwn(machine.projectPaths, project.id)).map((project) => ({
1414
+ ...project,
1415
+ path: machine.projectPaths[project.id],
1416
+ iconUrl: machineId === "local" ? project.iconUrl : void 0
1417
+ }));
1418
+ const selectedHarness = machine.harnesses.find((choice) => choice.id === harness);
1419
+ const canSubmit = !!(prompt.trim() && !images.uploading && model.trim() && machine.connected && machineProjects.some((project) => project.id === projectId) && selectedHarness?.launchEnabled) && !busy;
1420
+ const draftRestored = useRef(false);
1421
+ useEffect(() => {
1422
+ const draft = readNewThreadDraft();
1423
+ setPrompt(draft.prompt);
1424
+ images.setAttachments(draft.images);
1425
+ draftRestored.current = true;
1426
+ }, []);
1427
+ useEffect(() => {
1428
+ if (draftRestored.current) writeNewThreadDraft({
1429
+ prompt,
1430
+ images: images.attachments
1431
+ });
1432
+ }, [prompt, images.attachments]);
1433
+ useEffect(() => {
1434
+ setWorktree(initialWorktreeChoice(machineId, projectId));
1435
+ }, [machineId, projectId]);
1436
+ useEffect(() => {
1437
+ if (!projectId || !catalog.data) return;
1438
+ const choices = catalog.data.harnesses;
1439
+ const previous = localStorage.getItem(harnessPreference(machineId, projectId));
1440
+ setHarness(choices.find((choice) => choice.id === previous)?.id ?? choices.find((choice) => choice.launchEnabled)?.id ?? choices[0]?.id ?? "");
1441
+ setModel("Default");
1442
+ }, [
1443
+ machineId,
1444
+ projectId,
1445
+ catalog.data
1446
+ ]);
1447
+ useEffect(() => {
1448
+ if (!machineId && machines.length) setMachineId(machines.find((machine) => machine.id === "local")?.id ?? machines[0].id);
1449
+ if (!projectId && machineProjects.length) setProjectId(machineProjects[0].id);
1450
+ }, [
1451
+ machines,
1452
+ machineId,
1453
+ machineProjects,
1454
+ projectId
1455
+ ]);
1456
+ const changeMachine = (id) => {
1457
+ const next = machines.find((machine) => machine.id === id);
1458
+ if (!next?.connected) return;
1459
+ setMachineId(id);
1460
+ if (!Object.hasOwn(next.projectPaths, projectId)) setProjectId(projects.find((project) => Object.hasOwn(next.projectPaths, project.id))?.id ?? "");
1461
+ setHarness("");
1462
+ setModel("Default");
1463
+ setError("");
1464
+ };
1465
+ const changeProject = (id) => {
1466
+ setProjectId(id);
1467
+ setHarness("");
1468
+ setModel("Default");
1469
+ setError("");
1470
+ };
1471
+ const changeHarness = (id) => {
1472
+ setHarness(id);
1473
+ setModel("Default");
1474
+ if (projectId) localStorage.setItem(harnessPreference(machineId, projectId), id);
1475
+ };
1476
+ const submit = async (event) => {
1477
+ event.preventDefault();
1478
+ if (!canSubmit) return;
1479
+ setBusy(true);
1480
+ setError("");
1481
+ const payload = JSON.stringify({
1482
+ prompt: promptWithImages(prompt, images.attachments),
1483
+ projectId,
1484
+ machineId,
1485
+ agent: harness,
1486
+ model: model.trim(),
1487
+ worktree
1488
+ });
1489
+ if (operation.current?.payload !== payload) operation.current = {
1490
+ payload,
1491
+ key: uuid()
1492
+ };
1493
+ try {
1494
+ const result = await fetch("/api/threads", {
1495
+ method: "POST",
1496
+ headers: {
1497
+ "Content-Type": "application/json",
1498
+ "Idempotency-Key": operation.current.key
1499
+ },
1500
+ body: payload
1501
+ });
1502
+ const response = await result.json();
1503
+ if (!result.ok) throw new Error(response.error || "Could not create thread");
1504
+ recordWorktreeChoice(machineId, projectId, worktree);
1505
+ clearNewThreadDraft();
1506
+ await navigate(`/threads/${encodeURIComponent(response.id)}`);
1507
+ } catch (cause) {
1508
+ setError(cause instanceof Error ? cause.message : "Could not create thread");
1509
+ setBusy(false);
1510
+ }
1511
+ };
1512
+ return /* @__PURE__ */ jsxs("main", {
1513
+ className: "new-thread-page",
1514
+ "aria-label": "New thread",
1515
+ children: [
1516
+ /* @__PURE__ */ jsx("button", {
1517
+ className: "mobile-menu",
1518
+ onClick: onOpenSidebar,
1519
+ children: "Threads"
1520
+ }),
1521
+ /* @__PURE__ */ jsx("h1", { children: "What do you want to build today?" }),
1522
+ machine.connected && machine.id === "local" && (!machineProjects.length || creatingWorkspace) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1523
+ /* @__PURE__ */ jsx("p", { children: !machineProjects.length ? "This Herdr session has no projects yet." : "Add a project to this Herdr session." }),
1524
+ /* @__PURE__ */ jsx(CreateWorkspace, {
1525
+ machineId: machine.id,
1526
+ onCreated: () => setCreatingWorkspace(false)
1527
+ }),
1528
+ !!machineProjects.length && /* @__PURE__ */ jsx("button", {
1529
+ className: "workspace-cancel",
1530
+ onClick: () => setCreatingWorkspace(false),
1531
+ children: "Cancel"
1532
+ })
1533
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
1534
+ /* @__PURE__ */ jsxs("form", {
1535
+ className: `thread-composer ${images.dragging ? "is-dropping" : ""}`,
1536
+ onSubmit: submit,
1537
+ "aria-busy": busy,
1538
+ ...images.handlers,
1539
+ children: [
1540
+ (images.attachments.length > 0 || images.uploading > 0) && /* @__PURE__ */ jsxs("ul", {
1541
+ className: "composer-images",
1542
+ "aria-label": "Attached images",
1543
+ children: [images.attachments.map((image) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("img", {
1544
+ src: image.preview,
1545
+ alt: image.name
1546
+ }), /* @__PURE__ */ jsx(Tooltip$1, {
1547
+ label: "Remove image",
1548
+ children: /* @__PURE__ */ jsx("button", {
1549
+ type: "button",
1550
+ "aria-label": `Remove ${image.name}`,
1551
+ onClick: () => images.remove(image.id),
1552
+ children: /* @__PURE__ */ jsx(X, {
1553
+ size: 12,
1554
+ strokeWidth: 2.4
1555
+ })
1556
+ })
1557
+ })] }, image.id)), Array.from({ length: images.uploading }, (_, index) => /* @__PURE__ */ jsx("li", {
1558
+ className: "is-uploading",
1559
+ "aria-label": "Uploading image"
1560
+ }, "uploading-" + index))]
1561
+ }),
1562
+ /* @__PURE__ */ jsx("textarea", {
1563
+ autoFocus: true,
1564
+ required: true,
1565
+ maxLength: 8e3,
1566
+ value: prompt,
1567
+ onChange: (event) => setPrompt(event.target.value),
1568
+ onKeyDown: (event) => {
1569
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
1570
+ event.preventDefault();
1571
+ event.currentTarget.form?.requestSubmit();
1572
+ }
1573
+ },
1574
+ "aria-label": "Thread prompt",
1575
+ placeholder: "Write a prompt, or drop an image…"
1576
+ }),
1577
+ /* @__PURE__ */ jsxs("div", {
1578
+ className: "composer-toolbar",
1579
+ children: [
1580
+ /* @__PURE__ */ jsx(LaunchSelectors, {
1581
+ machine,
1582
+ machines,
1583
+ projects: machineProjects,
1584
+ projectId,
1585
+ harness,
1586
+ model,
1587
+ onMachineChange: changeMachine,
1588
+ onProjectChange: changeProject,
1589
+ onHarnessChange: changeHarness,
1590
+ onModelChange: setModel,
1591
+ onAddProject: machine.connected && machine.id === "local" ? () => setCreatingWorkspace(true) : void 0
1592
+ }),
1593
+ /* @__PURE__ */ jsx(Tooltip$1, {
1594
+ label: worktree ? "new worktree" : "current checkout",
1595
+ children: /* @__PURE__ */ jsxs("label", {
1596
+ className: "worktree-option",
1597
+ children: [/* @__PURE__ */ jsx("input", {
1598
+ type: "checkbox",
1599
+ checked: worktree,
1600
+ onChange: (event) => setWorktree(event.target.checked),
1601
+ "aria-label": "Create a new worktree"
1602
+ }), /* @__PURE__ */ jsx(GitBranch, {
1603
+ size: 15,
1604
+ "aria-hidden": "true"
1605
+ })]
1606
+ })
1607
+ }),
1608
+ /* @__PURE__ */ jsx("div", {
1609
+ className: "composer-actions",
1610
+ children: /* @__PURE__ */ jsx(Tooltip$1, {
1611
+ label: "Create thread",
1612
+ children: /* @__PURE__ */ jsx("button", {
1613
+ className: "composer-send",
1614
+ type: "submit",
1615
+ disabled: !canSubmit,
1616
+ "aria-label": "Create thread",
1617
+ children: busy ? "…" : /* @__PURE__ */ jsx(ArrowUp, {
1618
+ size: 18,
1619
+ strokeWidth: 2.2
1620
+ })
1621
+ })
1622
+ })
1623
+ })
1624
+ ]
1625
+ })
1626
+ ]
1627
+ }),
1628
+ (selectedHarness?.reason || !machines.length || machine.error || catalog.isError) && /* @__PURE__ */ jsx("p", {
1629
+ className: "composer-preview-note",
1630
+ children: selectedHarness?.reason ?? (!machines.length ? "No target is configured." : machine.error ? `Machine is not connected: ${machine.error.replaceAll("_", " ")}.` : "The machine catalog is not available.")
1631
+ }),
1632
+ images.error && /* @__PURE__ */ jsx("p", {
1633
+ className: "form-error",
1634
+ role: "alert",
1635
+ children: images.error
1636
+ }),
1637
+ error && /* @__PURE__ */ jsx("p", {
1638
+ className: "form-error",
1639
+ role: "alert",
1640
+ children: error
1641
+ })
1642
+ ] })
1643
+ ]
1644
+ });
1645
+ }
1646
+ //#endregion
1647
+ //#region src/client/terminal-clipboard.ts
1648
+ var OSC_CLIPBOARD = 52;
1649
+ var OSC_52_CLIPBOARD_READ_REQUEST = "?";
1650
+ function decodeOsc52ClipboardWrite(data) {
1651
+ const payload = data.slice(data.indexOf(";") + 1);
1652
+ if (!data.includes(";") || !payload || payload === OSC_52_CLIPBOARD_READ_REQUEST) return void 0;
1653
+ try {
1654
+ return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(atob(payload), (char) => char.charCodeAt(0)));
1655
+ } catch {
1656
+ return;
1657
+ }
1658
+ }
1659
+ function copyThroughDocumentCopyEvent(text) {
1660
+ const fill = (event) => {
1661
+ event.clipboardData?.setData("text/plain", text);
1662
+ event.preventDefault();
1663
+ };
1664
+ document.addEventListener("copy", fill, true);
1665
+ try {
1666
+ return document.execCommand("copy");
1667
+ } catch {
1668
+ return false;
1669
+ } finally {
1670
+ document.removeEventListener("copy", fill, true);
1671
+ }
1672
+ }
1673
+ async function writeClipboardText(text) {
1674
+ try {
1675
+ await navigator.clipboard.writeText(text);
1676
+ return true;
1677
+ } catch {
1678
+ return copyThroughDocumentCopyEvent(text);
1679
+ }
1680
+ }
1681
+ var hasOwnTextSelection = (target) => {
1682
+ if ((target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) && target.selectionStart !== target.selectionEnd) return true;
1683
+ return !!document.getSelection()?.toString();
1684
+ };
1685
+ function bindTerminalClipboard(host, terminal, canWriteFromTerminalOutput, onCopied) {
1686
+ const copySelectionOnRelease = () => {
1687
+ if (terminal.hasSelection()) writeClipboardText(terminal.getSelection()).then(onCopied);
1688
+ };
1689
+ const copySelectionOnShortcut = (event) => {
1690
+ if (event.defaultPrevented || !terminal.hasSelection() || hasOwnTextSelection(event.target)) return;
1691
+ event.clipboardData?.setData("text/plain", terminal.getSelection());
1692
+ event.preventDefault();
1693
+ onCopied(true);
1694
+ };
1695
+ const osc52 = terminal.parser.registerOscHandler(OSC_CLIPBOARD, (data) => {
1696
+ const text = decodeOsc52ClipboardWrite(data);
1697
+ if (text !== void 0 && canWriteFromTerminalOutput()) writeClipboardText(text).then(onCopied);
1698
+ return true;
1699
+ });
1700
+ host.addEventListener("mouseup", copySelectionOnRelease);
1701
+ window.addEventListener("copy", copySelectionOnShortcut);
1702
+ return () => {
1703
+ osc52.dispose();
1704
+ host.removeEventListener("mouseup", copySelectionOnRelease);
1705
+ window.removeEventListener("copy", copySelectionOnShortcut);
1706
+ };
1707
+ }
1708
+ //#endregion
1709
+ //#region src/components/QrCode.tsx
1710
+ var QUIET_ZONE = 2;
1711
+ function QrCode({ value, label }) {
1712
+ const { data, size } = encode(value);
1713
+ const modules = data.flatMap((row, y) => row.flatMap((dark, x) => dark ? [`M${x + QUIET_ZONE} ${y + QUIET_ZONE}h1v1h-1z`] : []));
1714
+ const span = size + 4;
1715
+ return /* @__PURE__ */ jsxs("svg", {
1716
+ className: "qr-code",
1717
+ role: "img",
1718
+ "aria-label": label,
1719
+ viewBox: `0 0 ${span} ${span}`,
1720
+ shapeRendering: "crispEdges",
1721
+ children: [/* @__PURE__ */ jsx("rect", {
1722
+ width: span,
1723
+ height: span,
1724
+ fill: "#fff"
1725
+ }), /* @__PURE__ */ jsx("path", {
1726
+ d: modules.join(""),
1727
+ fill: "#000"
1728
+ })]
1729
+ });
1730
+ }
1731
+ //#endregion
1732
+ //#region src/components/NotificationSetting.tsx
1733
+ function NotificationSetting() {
1734
+ const [permission, setPermission] = useState(notificationPermission);
1735
+ if (!notificationsSupported() || permission !== "default") return null;
1736
+ return /* @__PURE__ */ jsxs("div", {
1737
+ className: "settings-row",
1738
+ children: [/* @__PURE__ */ jsxs("span", { children: ["Notify me about new requests", /* @__PURE__ */ jsx("small", {
1739
+ className: "device-meta",
1740
+ children: "Shown when Webr is open in a hidden tab."
1741
+ })] }), /* @__PURE__ */ jsx("button", {
1742
+ type: "button",
1743
+ className: "dialog-button",
1744
+ onClick: () => void requestNotificationPermission().then(setPermission),
1745
+ children: "Turn on"
1746
+ })]
1747
+ });
1748
+ }
1749
+ //#endregion
1750
+ //#region src/components/RemoteActivity.tsx
1751
+ var VISIBLE_EVENTS = 8;
1752
+ function RemoteActivity({ refreshKey }) {
1753
+ const [events, setEvents] = useState([]);
1754
+ useEffect(() => {
1755
+ listAuditEvents().then(setEvents, () => void 0);
1756
+ }, [refreshKey]);
1757
+ if (!events.length) return null;
1758
+ return /* @__PURE__ */ jsxs("div", {
1759
+ className: "remote-activity",
1760
+ children: [/* @__PURE__ */ jsx("h3", {
1761
+ className: "settings-subtitle",
1762
+ children: "Recent activity"
1763
+ }), events.slice(0, VISIBLE_EVENTS).map((event) => /* @__PURE__ */ jsxs("p", {
1764
+ className: "activity-row",
1765
+ children: [/* @__PURE__ */ jsx("span", { children: describeAuditEvent(event) }), /* @__PURE__ */ jsx("time", {
1766
+ dateTime: new Date(event.at).toISOString(),
1767
+ children: relativeTime$1(event.at)
1768
+ })]
1769
+ }, event.id))]
1770
+ });
1771
+ }
1772
+ //#endregion
1773
+ //#region src/components/RemoteAccess.tsx
1774
+ function InviteDialog({ invite, onClose }) {
1775
+ const [urlIndex, setUrlIndex] = useState(0);
1776
+ const [copied, setCopied] = useState(false);
1777
+ const url = invite.urls[urlIndex];
1778
+ const copy = () => writeClipboardText(invite.token).then((ok) => {
1779
+ if (!ok) return;
1780
+ setCopied(true);
1781
+ setTimeout(() => setCopied(false), 1500);
1782
+ });
1783
+ return /* @__PURE__ */ jsx(Dialog.Root, {
1784
+ open: true,
1785
+ onOpenChange: (open) => {
1786
+ if (!open) onClose();
1787
+ },
1788
+ children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: "dialog-backdrop" }), /* @__PURE__ */ jsxs(Dialog.Popup, {
1789
+ className: "dialog pairing-invite",
1790
+ children: [
1791
+ /* @__PURE__ */ jsx(Dialog.Title, {
1792
+ className: "pairing-title",
1793
+ children: "Connect a device"
1794
+ }),
1795
+ url ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1796
+ /* @__PURE__ */ jsx(Dialog.Description, {
1797
+ className: "dialog-message",
1798
+ children: "Scan with the device’s camera, or open Webr there and enter the code."
1799
+ }),
1800
+ /* @__PURE__ */ jsx(QrCode, {
1801
+ value: url,
1802
+ label: "QR code to connect a device"
1803
+ }),
1804
+ invite.urls.length > 1 && /* @__PURE__ */ jsx("div", {
1805
+ className: "theme-control theme-control--labels",
1806
+ role: "group",
1807
+ "aria-label": "Address",
1808
+ children: invite.urls.map((address, index) => /* @__PURE__ */ jsx("button", {
1809
+ type: "button",
1810
+ "aria-pressed": index === urlIndex,
1811
+ className: index === urlIndex ? "theme-option is-selected" : "theme-option",
1812
+ onClick: () => setUrlIndex(index),
1813
+ children: new URL(address).host
1814
+ }, address))
1815
+ })
1816
+ ] }) : /* @__PURE__ */ jsxs(Dialog.Description, {
1817
+ className: "dialog-message",
1818
+ children: [
1819
+ "Webr only listens on this computer. Restart it with ",
1820
+ /* @__PURE__ */ jsx("code", { children: "--lan" }),
1821
+ " or ",
1822
+ /* @__PURE__ */ jsx("code", { children: "--origin <url>" }),
1823
+ " so other devices can reach it."
1824
+ ]
1825
+ }),
1826
+ /* @__PURE__ */ jsxs("div", {
1827
+ className: "pairing-token",
1828
+ children: [/* @__PURE__ */ jsx("code", { children: invite.token }), /* @__PURE__ */ jsx("button", {
1829
+ type: "button",
1830
+ className: "dialog-button",
1831
+ onClick: () => void copy(),
1832
+ "aria-label": "Copy code",
1833
+ children: copied ? /* @__PURE__ */ jsx(Check, { size: 14 }) : /* @__PURE__ */ jsx(Copy, { size: 14 })
1834
+ })]
1835
+ }),
1836
+ /* @__PURE__ */ jsx("p", {
1837
+ className: "settings-note",
1838
+ children: "Works once and expires in 5 minutes."
1839
+ }),
1840
+ /* @__PURE__ */ jsx("div", {
1841
+ className: "dialog-actions",
1842
+ children: /* @__PURE__ */ jsx(Dialog.Close, {
1843
+ className: "dialog-button is-primary",
1844
+ children: "Done"
1845
+ })
1846
+ })
1847
+ ]
1848
+ })] })
1849
+ });
1850
+ }
1851
+ function RemoteAccess() {
1852
+ const [devices, setDevices] = useState([]);
1853
+ const [invite, setInvite] = useState();
1854
+ const [error, setError] = useState("");
1855
+ const refresh = useCallback(() => listDevices().then(setDevices, () => void 0), []);
1856
+ useEffect(() => {
1857
+ refresh();
1858
+ }, [refresh]);
1859
+ const showInvite = () => createInvite().then((created) => {
1860
+ setError("");
1861
+ setInvite(created);
1862
+ }, (failure) => setError(failure.message));
1863
+ const revoke = async (device) => {
1864
+ if (!await confirmDialog(`Disconnect ${device.name}? It will need to be approved again.`, {
1865
+ confirmLabel: "Disconnect",
1866
+ danger: true
1867
+ })) return;
1868
+ await revokeDevice(device.id);
1869
+ if (device.current) location.reload();
1870
+ else await refresh();
1871
+ };
1872
+ return /* @__PURE__ */ jsxs("section", {
1873
+ id: "remote-access",
1874
+ children: [
1875
+ /* @__PURE__ */ jsx("div", {
1876
+ className: "settings-section-title",
1877
+ children: /* @__PURE__ */ jsx("h2", { children: "Remote access" })
1878
+ }),
1879
+ /* @__PURE__ */ jsx("p", {
1880
+ className: "settings-note",
1881
+ children: "Other devices ask to connect and you approve them here. This computer never needs a code."
1882
+ }),
1883
+ /* @__PURE__ */ jsxs("div", {
1884
+ className: "settings-row",
1885
+ children: [/* @__PURE__ */ jsx("span", { children: "Connect a device" }), /* @__PURE__ */ jsx("button", {
1886
+ type: "button",
1887
+ className: "dialog-button",
1888
+ onClick: () => void showInvite(),
1889
+ children: "Show code"
1890
+ })]
1891
+ }),
1892
+ /* @__PURE__ */ jsx(NotificationSetting, {}),
1893
+ error && /* @__PURE__ */ jsx("p", {
1894
+ role: "alert",
1895
+ className: "form-error",
1896
+ children: error
1897
+ }),
1898
+ devices.map((device) => /* @__PURE__ */ jsxs("div", {
1899
+ className: "settings-row",
1900
+ children: [/* @__PURE__ */ jsxs("span", { children: [
1901
+ device.name,
1902
+ device.current && /* @__PURE__ */ jsx("em", {
1903
+ className: "device-tag",
1904
+ children: "This device"
1905
+ }),
1906
+ /* @__PURE__ */ jsxs("small", {
1907
+ className: "device-meta",
1908
+ children: [
1909
+ "Active ",
1910
+ relativeTime$1(device.lastSeenAt),
1911
+ " · Signs out ",
1912
+ relativeFuture(device.expiresAt),
1913
+ " if unused"
1914
+ ]
1915
+ })
1916
+ ] }), /* @__PURE__ */ jsx("button", {
1917
+ type: "button",
1918
+ className: "dialog-button",
1919
+ onClick: () => void revoke(device),
1920
+ children: "Disconnect"
1921
+ })]
1922
+ }, device.id)),
1923
+ /* @__PURE__ */ jsx(RemoteActivity, { refreshKey: devices }),
1924
+ invite && /* @__PURE__ */ jsx(InviteDialog, {
1925
+ invite,
1926
+ onClose: () => setInvite(void 0)
1927
+ })
1928
+ ]
1929
+ });
1930
+ }
1931
+ //#endregion
1932
+ //#region src/components/SettingsView.tsx
1933
+ function SettingsView({ onOpenSidebar }) {
1934
+ const [behavior, setBehavior] = useState("worktree");
1935
+ const [shortcuts, setShortcuts] = useState(defaultShortcuts);
1936
+ const [recording, setRecording] = useState(null);
1937
+ const [error, setError] = useState("");
1938
+ useEffect(() => {
1939
+ setBehavior(readNewSessionBehavior());
1940
+ setShortcuts(getShortcuts());
1941
+ }, []);
1942
+ const recordKey = (event, action) => {
1943
+ event.preventDefault();
1944
+ event.stopPropagation();
1945
+ if (event.key === "Escape") {
1946
+ setRecording(null);
1947
+ setError("");
1948
+ return;
1949
+ }
1950
+ const combo = keyCombo(event);
1951
+ if (!combo) return;
1952
+ if (isReservedShortcut(combo) || combo === "meta+k") {
1953
+ setError("Choose a shortcut with a modifier that does not conflict with the browser or ⌘K.");
1954
+ return;
1955
+ }
1956
+ if (Object.entries(shortcuts).some(([name, key]) => name !== action && key === combo)) {
1957
+ setError("That shortcut is already in use.");
1958
+ return;
1959
+ }
1960
+ const next = {
1961
+ ...shortcuts,
1962
+ [action]: combo
1963
+ };
1964
+ setShortcuts(next);
1965
+ localStorage.setItem("webr-shortcuts", JSON.stringify(next));
1966
+ setRecording(null);
1967
+ setError("");
1968
+ };
1969
+ return /* @__PURE__ */ jsxs("main", {
1970
+ className: "form-page settings-page",
1971
+ children: [
1972
+ /* @__PURE__ */ jsx("button", {
1973
+ className: "mobile-menu",
1974
+ onClick: onOpenSidebar,
1975
+ children: "Threads"
1976
+ }),
1977
+ /* @__PURE__ */ jsx("h1", { children: "Settings" }),
1978
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h2", { children: "Appearance" }), /* @__PURE__ */ jsxs("div", {
1979
+ className: "settings-row",
1980
+ children: [/* @__PURE__ */ jsx("span", { children: "Theme" }), /* @__PURE__ */ jsx(ThemeControl, { expanded: true })]
1981
+ })] }),
1982
+ /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h2", { children: "New threads" }), /* @__PURE__ */ jsxs("div", {
1983
+ className: "settings-row",
1984
+ children: [/* @__PURE__ */ jsx("span", { children: "New session behavior" }), /* @__PURE__ */ jsx("div", {
1985
+ className: "theme-control theme-control--labels",
1986
+ role: "group",
1987
+ "aria-label": "New session behavior",
1988
+ children: Object.keys(newSessionBehaviorLabels).map((choice) => /* @__PURE__ */ jsx("button", {
1989
+ type: "button",
1990
+ "aria-pressed": behavior === choice,
1991
+ className: behavior === choice ? "theme-option is-selected" : "theme-option",
1992
+ onClick: () => {
1993
+ setBehavior(choice);
1994
+ writeNewSessionBehavior(choice);
1995
+ },
1996
+ children: newSessionBehaviorLabels[choice]
1997
+ }, choice))
1998
+ })]
1999
+ })] }),
2000
+ /* @__PURE__ */ jsx(RemoteAccess, {}),
2001
+ /* @__PURE__ */ jsxs("section", {
2002
+ id: "shortcuts",
2003
+ children: [
2004
+ /* @__PURE__ */ jsxs("div", {
2005
+ className: "settings-section-title",
2006
+ children: [/* @__PURE__ */ jsx("h2", { children: "Keyboard shortcuts" }), /* @__PURE__ */ jsx("button", {
2007
+ onClick: () => {
2008
+ setShortcuts(defaultShortcuts);
2009
+ localStorage.removeItem("webr-shortcuts");
2010
+ setRecording(null);
2011
+ setError("");
2012
+ },
2013
+ children: "Reset"
2014
+ })]
2015
+ }),
2016
+ /* @__PURE__ */ jsx("p", {
2017
+ className: "settings-note",
2018
+ children: "Press ⌘K for all actions and threads."
2019
+ }),
2020
+ Object.keys(shortcutLabels).map((action) => /* @__PURE__ */ jsxs("div", {
2021
+ className: "settings-row",
2022
+ children: [/* @__PURE__ */ jsx("span", { children: shortcutLabels[action] }), /* @__PURE__ */ jsx("button", {
2023
+ className: "shortcut-key",
2024
+ "data-recording": recording === action,
2025
+ onClick: () => {
2026
+ setRecording(action);
2027
+ setError("");
2028
+ },
2029
+ onKeyDown: (event) => recording === action && recordKey(event, action),
2030
+ "aria-label": `Change ${shortcutLabels[action]} shortcut`,
2031
+ children: recording === action ? "Press keys…" : shortcutKeys(shortcuts[action]).map((key, index) => /* @__PURE__ */ jsx("kbd", { children: key }, index))
2032
+ })]
2033
+ }, action)),
2034
+ error && /* @__PURE__ */ jsx("p", {
2035
+ role: "alert",
2036
+ className: "form-error",
2037
+ children: error
2038
+ })
2039
+ ]
2040
+ })
2041
+ ]
2042
+ });
2043
+ }
2044
+ //#endregion
2045
+ //#region src/lib/models.ts
2046
+ var harnessNames = {
2047
+ claude: "Claude Code",
2048
+ codex: "Codex",
2049
+ opencode: "OpenCode",
2050
+ pi: "Pi"
2051
+ };
2052
+ function harnessName(agent) {
2053
+ return Object.hasOwn(harnessNames, agent) ? harnessNames[agent] : agent;
2054
+ }
2055
+ function relativeTime(value) {
2056
+ const minutes = Math.max(0, Math.floor((Date.now() - new Date(value).getTime()) / 6e4));
2057
+ if (minutes < 1) return "now";
2058
+ if (minutes < 60) return `${minutes}m`;
2059
+ if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
2060
+ return `${Math.floor(minutes / 1440)}d`;
2061
+ }
2062
+ //#endregion
2063
+ //#region src/lib/launch.ts
2064
+ var launchSteps = [
2065
+ {
2066
+ id: "validate",
2067
+ label: "Checking the machine",
2068
+ active: "Checking the machine…"
2069
+ },
2070
+ {
2071
+ id: "checkout",
2072
+ label: "Creating the worktree",
2073
+ active: "Creating the worktree…"
2074
+ },
2075
+ {
2076
+ id: "start",
2077
+ label: "Starting the agent",
2078
+ active: "Starting the agent…"
2079
+ },
2080
+ {
2081
+ id: "prompt",
2082
+ label: "Sending your prompt",
2083
+ active: "Sending your prompt…"
2084
+ }
2085
+ ];
2086
+ var RECENT_FAILURE_MS = 6e5;
2087
+ var launchStepIndex = (step) => step === "checkout_complete" ? 2 : Math.max(0, launchSteps.findIndex((item) => item.id === step));
2088
+ var isLaunching = (thread) => ["pending", "running"].includes(thread.operation?.state ?? "") && !thread.panes.some((pane) => pane.kind === "agent");
2089
+ var launchFailed = (thread) => ["failed", "unknown"].includes(thread.operation?.state ?? "") && !thread.panes.some((pane) => pane.kind === "agent");
2090
+ var launchLabel = (thread) => launchSteps[launchStepIndex(thread.operation?.step ?? "validate")].active;
2091
+ var failedRecently = (thread, now) => now - Date.parse(thread.updatedAt) < RECENT_FAILURE_MS;
2092
+ var isListedThread = (thread, now = Date.now()) => thread.panes.some((pane) => pane.kind === "agent") || isLaunching(thread) || launchFailed(thread) && failedRecently(thread, now);
2093
+ //#endregion
2094
+ //#region src/components/OpenWorktree.tsx
2095
+ var projectQuery = (project) => new URLSearchParams({
2096
+ machineId: project.machineId ?? "local",
2097
+ projectId: project.id
2098
+ });
2099
+ var worktreeName = (worktree) => worktree.branch ?? worktree.label;
2100
+ var folderName = (worktree) => worktree.path.split("/").at(-1) ?? "";
2101
+ var matchesSearch = (worktree, query) => `${worktreeName(worktree)} ${folderName(worktree)}`.toLowerCase().includes(query.trim().toLowerCase());
2102
+ var fetchWorktrees = async (project, signal) => {
2103
+ const response = await fetch(`/api/worktrees?${projectQuery(project)}`, { signal });
2104
+ if (!response.ok) throw new Error("Could not list worktrees.");
2105
+ return await response.json();
2106
+ };
2107
+ var openWorktree = async (project, worktree) => {
2108
+ const response = await fetch("/api/worktrees/open", {
2109
+ method: "POST",
2110
+ headers: { "Content-Type": "application/json" },
2111
+ body: JSON.stringify({
2112
+ machineId: project.machineId ?? "local",
2113
+ projectId: project.id,
2114
+ path: worktree.path
2115
+ })
2116
+ });
2117
+ if (!response.ok) throw new Error("Could not open this worktree.");
2118
+ return (await response.json()).id;
2119
+ };
2120
+ var openAndShow = async (project, worktree) => {
2121
+ try {
2122
+ await navigate(`/threads/${encodeURIComponent(await openWorktree(project, worktree))}`);
2123
+ } catch (error) {
2124
+ alertDialog(error instanceof Error ? error.message : "Could not open this worktree.");
2125
+ }
2126
+ };
2127
+ var keepTypingInsideInput = (event) => {
2128
+ if (event.key.length === 1 || event.key === "Backspace") event.stopPropagation();
2129
+ };
2130
+ function WorktreeSearch({ project }) {
2131
+ const [query, setQuery] = useState("");
2132
+ const worktrees = useQuery({
2133
+ queryKey: [
2134
+ "worktrees",
2135
+ project.machineId,
2136
+ project.id
2137
+ ],
2138
+ queryFn: ({ signal }) => fetchWorktrees(project, signal),
2139
+ staleTime: 0,
2140
+ gcTime: 0
2141
+ });
2142
+ const matches = (worktrees.data ?? []).filter((worktree) => matchesSearch(worktree, query));
2143
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
2144
+ className: "composer-select-search",
2145
+ children: [/* @__PURE__ */ jsx(Search, {
2146
+ size: 14,
2147
+ "aria-hidden": "true"
2148
+ }), /* @__PURE__ */ jsx("input", {
2149
+ autoFocus: true,
2150
+ "aria-label": "Search worktrees",
2151
+ placeholder: "Search worktrees…",
2152
+ value: query,
2153
+ onChange: (event) => setQuery(event.target.value),
2154
+ onKeyDown: keepTypingInsideInput
2155
+ })]
2156
+ }), /* @__PURE__ */ jsxs("div", {
2157
+ className: "worktree-menu-list",
2158
+ children: [
2159
+ matches.map((worktree) => /* @__PURE__ */ jsx(Tooltip$1, {
2160
+ label: worktree.path,
2161
+ side: "right",
2162
+ children: /* @__PURE__ */ jsxs(Menu.Item, {
2163
+ className: "thread-menu-item",
2164
+ onClick: () => void openAndShow(project, worktree),
2165
+ children: [/* @__PURE__ */ jsx(GitBranch, {
2166
+ size: 14,
2167
+ "aria-hidden": "true"
2168
+ }), /* @__PURE__ */ jsxs("span", {
2169
+ className: "worktree-menu-name",
2170
+ children: [worktreeName(worktree), /* @__PURE__ */ jsx("small", { children: folderName(worktree) })]
2171
+ })]
2172
+ })
2173
+ }, worktree.path)),
2174
+ worktrees.isError && /* @__PURE__ */ jsx("p", {
2175
+ className: "worktree-menu-note",
2176
+ role: "alert",
2177
+ children: "Could not list worktrees."
2178
+ }),
2179
+ worktrees.data && !matches.length && /* @__PURE__ */ jsx("p", {
2180
+ className: "worktree-menu-note",
2181
+ children: worktrees.data.length ? "No matches" : "Every worktree already has a thread"
2182
+ })
2183
+ ]
2184
+ })] });
2185
+ }
2186
+ function OpenWorktree({ projects }) {
2187
+ return /* @__PURE__ */ jsxs(Menu.Root, { children: [/* @__PURE__ */ jsx(Tooltip$1, {
2188
+ label: "Open existing worktree",
2189
+ children: /* @__PURE__ */ jsx(Menu.Trigger, {
2190
+ className: "sidebar-open-worktree",
2191
+ "aria-label": "Open existing worktree",
2192
+ disabled: !projects.length,
2193
+ children: /* @__PURE__ */ jsx(Search, {
2194
+ size: 16,
2195
+ "aria-hidden": "true"
2196
+ })
2197
+ })
2198
+ }), /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(Menu.Positioner, {
2199
+ className: "thread-menu-positioner",
2200
+ sideOffset: 6,
2201
+ align: "start",
2202
+ children: /* @__PURE__ */ jsx(Menu.Popup, {
2203
+ className: "thread-menu",
2204
+ children: projects.map((project) => /* @__PURE__ */ jsxs(Menu.SubmenuRoot, { children: [/* @__PURE__ */ jsxs(Menu.SubmenuTrigger, {
2205
+ className: "thread-menu-item",
2206
+ children: [
2207
+ /* @__PURE__ */ jsx(ProjectIcon, { project }),
2208
+ /* @__PURE__ */ jsx("span", { children: project.name }),
2209
+ /* @__PURE__ */ jsx(ChevronRight, {
2210
+ size: 14,
2211
+ "aria-hidden": "true",
2212
+ className: "worktree-menu-chevron"
2213
+ })
2214
+ ]
2215
+ }), /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(Menu.Positioner, {
2216
+ className: "thread-menu-positioner",
2217
+ sideOffset: 4,
2218
+ alignOffset: -4,
2219
+ children: /* @__PURE__ */ jsx(Menu.Popup, {
2220
+ className: "thread-menu worktree-menu",
2221
+ children: /* @__PURE__ */ jsx(WorktreeSearch, { project })
2222
+ })
2223
+ }) })] }, `${project.machineId}:${project.id}`))
2224
+ })
2225
+ }) })] });
2226
+ }
2227
+ //#endregion
2228
+ //#region src/components/ThreadTitleEditor.tsx
2229
+ function ThreadTitleEditor({ thread, onDone, onError }) {
2230
+ const [title, setTitle] = useState(thread.title);
2231
+ const input = useRef(null);
2232
+ const finished = useRef(false);
2233
+ useEffect(() => {
2234
+ input.current?.focus();
2235
+ input.current?.select();
2236
+ }, []);
2237
+ const finish = (save) => {
2238
+ if (finished.current) return;
2239
+ finished.current = true;
2240
+ onDone();
2241
+ const next = title.trim();
2242
+ if (save && next && next !== thread.title) renameThread(thread, next).catch(onError);
2243
+ };
2244
+ return /* @__PURE__ */ jsx("input", {
2245
+ ref: input,
2246
+ className: "thread-name-input",
2247
+ "aria-label": `Rename ${thread.title}`,
2248
+ value: title,
2249
+ maxLength: 90,
2250
+ onChange: (event) => setTitle(event.target.value),
2251
+ onBlur: () => finish(true),
2252
+ onClick: (event) => event.stopPropagation(),
2253
+ onKeyDown: (event) => {
2254
+ event.stopPropagation();
2255
+ if (event.key === "Enter") finish(true);
2256
+ if (event.key === "Escape") finish(false);
2257
+ }
2258
+ });
2259
+ }
2260
+ //#endregion
2261
+ //#region src/components/useFlipList.ts
2262
+ var durationMs = 220;
2263
+ var itemSelector = "[data-flip-id]";
2264
+ var contentTops = (container) => {
2265
+ const origin = container.getBoundingClientRect().top - container.scrollTop;
2266
+ return new Map([...container.querySelectorAll(itemSelector)].map((element) => [element.dataset.flipId, {
2267
+ element,
2268
+ top: element.getBoundingClientRect().top - origin
2269
+ }]));
2270
+ };
2271
+ function useFlipList(container) {
2272
+ const previous = useRef(/* @__PURE__ */ new Map());
2273
+ useLayoutEffect(() => {
2274
+ if (!container.current) return;
2275
+ const current = contentTops(container.current);
2276
+ for (const [id, { element, top }] of current) {
2277
+ const before = previous.current.get(id);
2278
+ if (before === void 0 || Math.abs(before - top) < 1) continue;
2279
+ element.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], {
2280
+ duration: durationMs,
2281
+ easing: "cubic-bezier(.2, .8, .2, 1)"
2282
+ });
2283
+ }
2284
+ previous.current = new Map([...current].map(([id, { top }]) => [id, top]));
2285
+ });
2286
+ }
2287
+ //#endregion
2288
+ //#region src/client/project-groups.ts
2289
+ function groupProjectLocations(projects, currentId) {
2290
+ const groups = /* @__PURE__ */ new Map();
2291
+ for (const project of projects) {
2292
+ const id = project.logicalId ?? project.id;
2293
+ const group = groups.get(id);
2294
+ if (group) {
2295
+ group.locations.push(project.id);
2296
+ if (project.id === currentId) group.project = project;
2297
+ } else groups.set(id, {
2298
+ id,
2299
+ project,
2300
+ locations: [project.id]
2301
+ });
2302
+ }
2303
+ return [...groups.values()];
2304
+ }
2305
+ //#endregion
2306
+ //#region src/components/Sidebar.tsx
2307
+ var statusLabel = {
2308
+ working: "Working",
2309
+ blocked: "Needs input",
2310
+ done: "Done",
2311
+ idle: "Idle",
2312
+ unknown: "Unknown"
2313
+ };
2314
+ var statusBadges = {
2315
+ blocked: MessageCircleQuestion,
2316
+ done: CircleCheck,
2317
+ unknown: CircleHelp
2318
+ };
2319
+ var reportFailure = (error) => alertDialog(error instanceof Error ? error.message : "Herdr could not complete this action.");
2320
+ var confirmDelete = async (thread) => {
2321
+ if (await confirmDialog(deleteWarning([thread]), {
2322
+ confirmLabel: "Delete",
2323
+ danger: true
2324
+ })) await deleteThreadPermanently(thread).catch(reportFailure);
2325
+ };
2326
+ var ThreadRow = memo(function ThreadRow({ thread, project, machineName, current, showProject }) {
2327
+ const archived = !!thread.archivedAt;
2328
+ const ArchiveIcon = archived ? ArchiveRestore : Archive;
2329
+ const archiveLabel = archived ? `Unarchive ${thread.title}` : `Archive ${thread.title}`;
2330
+ const [renaming, setRenaming] = useState(false);
2331
+ const editor = renaming ? /* @__PURE__ */ jsx(ThreadTitleEditor, {
2332
+ thread,
2333
+ onDone: () => setRenaming(false),
2334
+ onError: reportFailure
2335
+ }) : void 0;
2336
+ return /* @__PURE__ */ jsxs(ContextMenu.Root, { children: [/* @__PURE__ */ jsxs(ContextMenu.Trigger, {
2337
+ className: "thread-item",
2338
+ "data-flip-id": thread.id,
2339
+ onDoubleClick: (event) => {
2340
+ event.preventDefault();
2341
+ setRenaming(true);
2342
+ },
2343
+ children: [/* @__PURE__ */ jsx(ThreadLink, {
2344
+ thread,
2345
+ project,
2346
+ machineName,
2347
+ current,
2348
+ showProject,
2349
+ editor
2350
+ }), /* @__PURE__ */ jsx(Tooltip$1, {
2351
+ label: archived ? "Unarchive" : "Archive",
2352
+ children: /* @__PURE__ */ jsx("button", {
2353
+ type: "button",
2354
+ className: "thread-archive",
2355
+ "aria-label": archiveLabel,
2356
+ onClick: () => void setThreadArchived(thread, !archived).catch(reportFailure),
2357
+ children: /* @__PURE__ */ jsx(ArchiveIcon, {
2358
+ size: 15,
2359
+ strokeWidth: 1.8,
2360
+ "aria-hidden": "true"
2361
+ })
2362
+ })
2363
+ })]
2364
+ }), /* @__PURE__ */ jsx(ContextMenu.Portal, { children: /* @__PURE__ */ jsx(ContextMenu.Positioner, {
2365
+ className: "thread-menu-positioner",
2366
+ children: /* @__PURE__ */ jsx(ContextMenu.Popup, {
2367
+ className: "thread-menu",
2368
+ children: /* @__PURE__ */ jsxs(ContextMenu.Item, {
2369
+ className: "thread-menu-item is-danger",
2370
+ onClick: () => void confirmDelete(thread),
2371
+ children: [/* @__PURE__ */ jsx(Trash2, {
2372
+ size: 14,
2373
+ "aria-hidden": "true"
2374
+ }), "Delete permanently"]
2375
+ })
2376
+ })
2377
+ }) })] });
2378
+ });
2379
+ function ThreadLink({ thread, project, machineName, current, showProject, editor }) {
2380
+ const launching = isLaunching(thread);
2381
+ const failed = launchFailed(thread);
2382
+ const StatusIcon = failed ? TriangleAlert : launching ? void 0 : statusBadges[thread.status];
2383
+ const needsAttention = failed || thread.status === "blocked" || thread.status === "done";
2384
+ const status = launching ? launchLabel(thread) : failed ? "Launch stopped" : statusLabel[thread.status];
2385
+ const context = launching || failed ? status : showProject ? project?.name : harnessName(thread.agent);
2386
+ const Row = editor ? "div" : "a";
2387
+ return /* @__PURE__ */ jsx(Tooltip$1, {
2388
+ label: `${thread.title} · ${project?.name ?? ""} · ${machineName} · ${harnessName(thread.agent)} · ${thread.model} · ${status}`,
2389
+ side: "right",
2390
+ children: /* @__PURE__ */ jsxs(Row, {
2391
+ className: `thread-row ${current ? "is-current" : ""} ${needsAttention ? `needs-attention status-${failed ? "blocked" : thread.status}` : ""}`,
2392
+ href: editor ? void 0 : `/threads/${encodeURIComponent(thread.id)}`,
2393
+ "aria-current": current ? "page" : void 0,
2394
+ "aria-label": `${thread.title} · ${project?.name ?? ""} · ${machineName} · ${status}`,
2395
+ children: [
2396
+ /* @__PURE__ */ jsxs("span", {
2397
+ className: "thread-avatar",
2398
+ "aria-hidden": "true",
2399
+ children: [/* @__PURE__ */ jsx(ThreadAvatar, {
2400
+ thread,
2401
+ working: thread.status === "working" || launching,
2402
+ size: 24
2403
+ }), StatusIcon && /* @__PURE__ */ jsx(StatusIcon, {
2404
+ className: `thread-status status-${failed ? "blocked" : thread.status}`,
2405
+ size: 14,
2406
+ strokeWidth: 2.2
2407
+ })]
2408
+ }),
2409
+ /* @__PURE__ */ jsxs("span", {
2410
+ className: "thread-copy",
2411
+ children: [/* @__PURE__ */ jsxs("span", {
2412
+ className: "thread-context",
2413
+ children: [
2414
+ showProject && project && !launching && !failed && /* @__PURE__ */ jsx(ProjectIcon, { project }),
2415
+ thread.worktree && !launching && !failed && /* @__PURE__ */ jsx(GitBranch, {
2416
+ className: "thread-worktree",
2417
+ size: 11,
2418
+ "aria-label": "Worktree"
2419
+ }),
2420
+ context
2421
+ ]
2422
+ }), editor ?? /* @__PURE__ */ jsx("span", {
2423
+ className: "thread-name",
2424
+ children: thread.title
2425
+ })]
2426
+ }),
2427
+ /* @__PURE__ */ jsx("span", {
2428
+ className: "thread-time",
2429
+ children: relativeTime(thread.updatedAt)
2430
+ })
2431
+ ]
2432
+ })
2433
+ });
2434
+ }
2435
+ function ArchivedThreads({ threads, renderThread }) {
2436
+ const [open, setOpen] = useState(false);
2437
+ const [deleting, setDeleting] = useState(false);
2438
+ const deleteAll = async () => {
2439
+ if (!await confirmDialog(deleteWarning(threads), {
2440
+ confirmLabel: "Delete all",
2441
+ danger: true
2442
+ })) return;
2443
+ setDeleting(true);
2444
+ try {
2445
+ const result = await deleteArchivedThreads();
2446
+ if (result.failed.length) await alertDialog(`${result.failed.length} archived ${result.failed.length === 1 ? "thread" : "threads"} could not be deleted.`);
2447
+ } catch (error) {
2448
+ reportFailure(error);
2449
+ } finally {
2450
+ setDeleting(false);
2451
+ }
2452
+ };
2453
+ return /* @__PURE__ */ jsxs("section", {
2454
+ className: "thread-group archived-threads",
2455
+ "aria-label": "Archived threads",
2456
+ children: [/* @__PURE__ */ jsxs("div", {
2457
+ className: "group-header",
2458
+ children: [/* @__PURE__ */ jsxs("button", {
2459
+ className: "group-title",
2460
+ onClick: () => setOpen((value) => !value),
2461
+ "aria-expanded": open,
2462
+ children: [
2463
+ /* @__PURE__ */ jsx("span", {
2464
+ className: "group-chevron",
2465
+ "aria-hidden": "true",
2466
+ children: open ? /* @__PURE__ */ jsx(ChevronDown, { size: 14 }) : /* @__PURE__ */ jsx(ChevronRight, { size: 14 })
2467
+ }),
2468
+ /* @__PURE__ */ jsx(Archive, {
2469
+ size: 14,
2470
+ "aria-hidden": "true"
2471
+ }),
2472
+ /* @__PURE__ */ jsx("span", {
2473
+ className: "group-name",
2474
+ children: "Archived"
2475
+ }),
2476
+ /* @__PURE__ */ jsx("span", {
2477
+ className: "group-count",
2478
+ children: threads.length
2479
+ })
2480
+ ]
2481
+ }), /* @__PURE__ */ jsx("button", {
2482
+ type: "button",
2483
+ className: "archived-delete-all",
2484
+ onClick: () => void deleteAll(),
2485
+ disabled: deleting,
2486
+ children: deleting ? "Deleting…" : "Delete all"
2487
+ })]
2488
+ }), open && threads.map(renderThread)]
2489
+ });
2490
+ }
2491
+ function Sidebar({ projects, threads, archived = [], machines, currentId, mode, onModeChange, collapsed, mobileOpen, onCollapse, onCloseMobile, onOpenPalette }) {
2492
+ const [closedProjects, setClosedProjects] = useState([]);
2493
+ const currentProjectId = threads.find((thread) => thread.id === currentId)?.projectId;
2494
+ const projectGroups = groupProjectLocations(projects, currentProjectId);
2495
+ const navigation = useRef(null);
2496
+ useFlipList(navigation);
2497
+ const threadLink = (thread, showProject) => {
2498
+ const project = projects.find((item) => item.id === thread.projectId);
2499
+ return /* @__PURE__ */ jsx(ThreadRow, {
2500
+ thread,
2501
+ project,
2502
+ machineName: machines.find((machine) => machine.id === thread.machineId)?.name ?? thread.machineId,
2503
+ current: currentId === thread.id,
2504
+ showProject
2505
+ }, thread.id);
2506
+ };
2507
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [mobileOpen && /* @__PURE__ */ jsx("button", {
2508
+ className: "sidebar-scrim",
2509
+ "aria-label": "Close sidebar",
2510
+ onClick: onCloseMobile
2511
+ }), /* @__PURE__ */ jsxs("aside", {
2512
+ className: `sidebar ${collapsed ? "is-collapsed" : ""} ${mobileOpen ? "is-mobile-open" : ""}`,
2513
+ "aria-label": "Threads",
2514
+ onClick: (event) => {
2515
+ if (mobileOpen && event.target.closest("a[href]")) onCloseMobile();
2516
+ },
2517
+ children: [
2518
+ /* @__PURE__ */ jsxs("div", {
2519
+ className: "sidebar-header",
2520
+ children: [
2521
+ !collapsed && /* @__PURE__ */ jsxs("span", {
2522
+ className: "brand",
2523
+ children: [/* @__PURE__ */ jsx("img", {
2524
+ src: "/brand-mark.png",
2525
+ alt: "",
2526
+ width: 20,
2527
+ height: 20
2528
+ }), "webr"]
2529
+ }),
2530
+ /* @__PURE__ */ jsx("button", {
2531
+ className: "icon-button sidebar-collapse",
2532
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
2533
+ onClick: onCollapse,
2534
+ children: collapsed ? /* @__PURE__ */ jsx(PanelLeftOpen, { size: 16 }) : /* @__PURE__ */ jsx(PanelLeftClose, { size: 16 })
2535
+ }),
2536
+ /* @__PURE__ */ jsx("button", {
2537
+ className: "icon-button mobile-close",
2538
+ "aria-label": "Close sidebar",
2539
+ onClick: onCloseMobile,
2540
+ children: /* @__PURE__ */ jsx(X, { size: 16 })
2541
+ })
2542
+ ]
2543
+ }),
2544
+ /* @__PURE__ */ jsxs("div", {
2545
+ className: "sidebar-actions",
2546
+ children: [
2547
+ /* @__PURE__ */ jsx(Tooltip$1, {
2548
+ label: "New thread",
2549
+ children: /* @__PURE__ */ jsxs("a", {
2550
+ className: "sidebar-new-thread",
2551
+ href: `/new${currentProjectId ? `?project=${encodeURIComponent(currentProjectId)}` : ""}`,
2552
+ "aria-label": "New thread",
2553
+ children: [/* @__PURE__ */ jsx(SquarePen, {
2554
+ size: 16,
2555
+ "aria-hidden": "true"
2556
+ }), !collapsed && /* @__PURE__ */ jsx("span", { children: "New thread" })]
2557
+ })
2558
+ }),
2559
+ /* @__PURE__ */ jsx(OpenWorktree, { projects }),
2560
+ !collapsed && /* @__PURE__ */ jsxs("div", {
2561
+ className: "sidebar-view-switch",
2562
+ role: "group",
2563
+ "aria-label": "Sidebar layout",
2564
+ children: [/* @__PURE__ */ jsx(Tooltip$1, {
2565
+ label: "Show threads",
2566
+ children: /* @__PURE__ */ jsx("button", {
2567
+ type: "button",
2568
+ "aria-label": "Show threads",
2569
+ "aria-pressed": mode === "threads",
2570
+ onClick: () => onModeChange("threads"),
2571
+ children: /* @__PURE__ */ jsx(LayoutList, {
2572
+ size: 16,
2573
+ "aria-hidden": "true"
2574
+ })
2575
+ })
2576
+ }), /* @__PURE__ */ jsx(Tooltip$1, {
2577
+ label: "Group by project",
2578
+ children: /* @__PURE__ */ jsx("button", {
2579
+ type: "button",
2580
+ "aria-label": "Group by project",
2581
+ "aria-pressed": mode === "projects",
2582
+ onClick: () => onModeChange("projects"),
2583
+ children: /* @__PURE__ */ jsx(FolderTree, {
2584
+ size: 16,
2585
+ "aria-hidden": "true"
2586
+ })
2587
+ })
2588
+ })]
2589
+ })
2590
+ ]
2591
+ }),
2592
+ !collapsed && /* @__PURE__ */ jsxs(Fragment$1, { children: [
2593
+ /* @__PURE__ */ jsxs("nav", {
2594
+ ref: navigation,
2595
+ className: "thread-navigation",
2596
+ "aria-label": "Agent threads",
2597
+ children: [mode === "projects" ? projectGroups.filter((group) => threads.some((thread) => group.locations.includes(thread.projectId))).sort((a, b) => (threads.find((thread) => b.locations.includes(thread.projectId))?.updatedAt ?? "").localeCompare(threads.find((thread) => a.locations.includes(thread.projectId))?.updatedAt ?? "")).map(({ id, project, locations }) => /* @__PURE__ */ jsxs("section", {
2598
+ className: "thread-group",
2599
+ children: [/* @__PURE__ */ jsxs("div", {
2600
+ className: "group-header",
2601
+ children: [/* @__PURE__ */ jsxs("button", {
2602
+ className: "group-title",
2603
+ onClick: () => setClosedProjects((value) => value.includes(id) ? value.filter((item) => item !== id) : [...value, id]),
2604
+ "aria-expanded": !closedProjects.includes(id),
2605
+ children: [
2606
+ /* @__PURE__ */ jsx("span", {
2607
+ className: "group-chevron",
2608
+ "aria-hidden": "true",
2609
+ children: closedProjects.includes(id) ? /* @__PURE__ */ jsx(ChevronRight, { size: 14 }) : /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
2610
+ }),
2611
+ /* @__PURE__ */ jsx(ProjectIcon, { project }),
2612
+ /* @__PURE__ */ jsx("span", {
2613
+ className: "group-name",
2614
+ children: project.name
2615
+ })
2616
+ ]
2617
+ }), /* @__PURE__ */ jsx(Tooltip$1, {
2618
+ label: `New thread in ${project.name}`,
2619
+ children: /* @__PURE__ */ jsx("a", {
2620
+ className: "group-new-thread",
2621
+ href: `/new?project=${encodeURIComponent(project.id)}`,
2622
+ "aria-label": `New thread in ${project.name}`,
2623
+ children: /* @__PURE__ */ jsx(SquarePen, {
2624
+ size: 15,
2625
+ "aria-hidden": "true"
2626
+ })
2627
+ })
2628
+ })]
2629
+ }), !closedProjects.includes(id) && threads.filter((thread) => locations.includes(thread.projectId)).map((thread) => threadLink(thread, false))]
2630
+ }, id)) : threads.map((thread) => threadLink(thread, true)), !threads.length && !archived.length && /* @__PURE__ */ jsx("p", {
2631
+ className: "sidebar-empty",
2632
+ children: "No agents yet. Press ⌘K to start one."
2633
+ })]
2634
+ }),
2635
+ !!archived.length && /* @__PURE__ */ jsx(ArchivedThreads, {
2636
+ threads: archived,
2637
+ renderThread: (thread) => threadLink(thread, true)
2638
+ }),
2639
+ /* @__PURE__ */ jsxs("div", {
2640
+ className: "sidebar-footer",
2641
+ children: [/* @__PURE__ */ jsxs("a", {
2642
+ className: "sidebar-settings",
2643
+ href: "/settings",
2644
+ children: [/* @__PURE__ */ jsx(Settings2, { size: 15 }), " Settings"]
2645
+ }), /* @__PURE__ */ jsx(Tooltip$1, {
2646
+ label: "Open command palette",
2647
+ children: /* @__PURE__ */ jsx("button", {
2648
+ type: "button",
2649
+ className: "sidebar-palette",
2650
+ "aria-label": "Open command palette",
2651
+ onClick: onOpenPalette,
2652
+ children: /* @__PURE__ */ jsx("kbd", { children: "⌘K" })
2653
+ })
2654
+ })]
2655
+ })
2656
+ ] })
2657
+ ]
2658
+ })] });
2659
+ }
2660
+ //#endregion
2661
+ //#region src/client/store.ts
2662
+ var equal = (left, right) => JSON.stringify(left) === JSON.stringify(right);
2663
+ var share = (previous, next) => previous && equal(previous, next) ? previous : next;
2664
+ function createRuntimeStore(bootstrap) {
2665
+ const retired = /* @__PURE__ */ new Map();
2666
+ const awaiting = /* @__PURE__ */ new Set();
2667
+ const store = createStore((set, get) => ({
2668
+ projections: {},
2669
+ threads: {},
2670
+ projects: {},
2671
+ threadIds: [],
2672
+ projectIds: [],
2673
+ connected: false,
2674
+ connection: (connected) => set((state) => {
2675
+ if (connected) return { connected };
2676
+ for (const id of Object.keys(state.projections)) awaiting.add(id);
2677
+ return {
2678
+ connected,
2679
+ projections: Object.fromEntries(Object.entries(state.projections).map(([id, projection]) => [id, {
2680
+ ...projection,
2681
+ connected: false,
2682
+ freshAt: null
2683
+ }])),
2684
+ threads: Object.fromEntries(Object.entries(state.threads).map(([id, thread]) => [id, {
2685
+ ...thread,
2686
+ status: "unknown",
2687
+ panes: [],
2688
+ bindingState: "detached"
2689
+ }]))
2690
+ };
2691
+ }),
2692
+ install: (projection) => {
2693
+ const old = get();
2694
+ const previous = old.projections[projection.machineId];
2695
+ const generations = retired.get(projection.machineId) ?? /* @__PURE__ */ new Set();
2696
+ if (generations.has(projection.generation) || !awaiting.has(projection.machineId) && previous?.generation === projection.generation && previous.revision >= projection.revision) return;
2697
+ awaiting.delete(projection.machineId);
2698
+ if (previous && previous.generation !== projection.generation) generations.add(previous.generation);
2699
+ if (generations.size > 8) generations.delete(generations.values().next().value);
2700
+ retired.set(projection.machineId, generations);
2701
+ const threads = Object.fromEntries(Object.entries(old.threads).filter(([, thread]) => thread.machineId !== projection.machineId));
2702
+ for (const thread of projection.threads) threads[thread.id] = share(old.threads[thread.id], {
2703
+ ...thread,
2704
+ panes: thread.panes.map((pane) => share(old.threads[thread.id]?.panes.find((oldPane) => oldPane.id === pane.id && oldPane.terminalId === pane.terminalId), pane))
2705
+ });
2706
+ const projections = {
2707
+ ...old.projections,
2708
+ [projection.machineId]: projection
2709
+ };
2710
+ const projects = {};
2711
+ for (const entry of Object.values(projections)) for (const project of entry.projects) projects[project.id] = share(old.projects[project.id], project);
2712
+ const threadIds = Object.values(threads).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((thread) => thread.id);
2713
+ const projectIds = Object.keys(projects);
2714
+ set({
2715
+ projections,
2716
+ threads,
2717
+ projects,
2718
+ threadIds: share(old.threadIds, threadIds),
2719
+ projectIds: share(old.projectIds, projectIds)
2720
+ });
2721
+ }
2722
+ }));
2723
+ for (const projection of bootstrap.projections) store.getState().install(projection);
2724
+ return store;
2725
+ }
2726
+ var createUiStore = () => createStore((set) => ({
2727
+ focusedPanes: {},
2728
+ focus: (scope, paneId) => set((state) => ({ focusedPanes: {
2729
+ ...state.focusedPanes,
2730
+ [scope]: paneId
2731
+ } }))
2732
+ }));
2733
+ function decodeFrame(packet) {
2734
+ if (packet.length < 32) throw new Error("short_frame");
2735
+ const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
2736
+ const seq = Number(view.getBigUint64(12));
2737
+ if (view.getUint32(0) !== 1212437041 || view.getUint32(24) !== packet.length - 32 || packet.length - 32 > 262144 || !Number.isSafeInteger(seq) || view.getUint8(28) > 1 || view.getUint8(29) || view.getUint16(30)) throw new Error("invalid_frame");
2738
+ const frame = {
2739
+ streamId: view.getUint32(4),
2740
+ generation: view.getUint32(8),
2741
+ seq,
2742
+ width: view.getUint16(20),
2743
+ height: view.getUint16(22),
2744
+ full: !!view.getUint8(28),
2745
+ bytes: packet.subarray(32)
2746
+ };
2747
+ if (!frame.streamId || !frame.generation || !frame.width || frame.width > 500 || !frame.height || frame.height > 300) throw new Error("invalid_dimensions");
2748
+ return frame;
2749
+ }
2750
+ var FrameSequence = class {
2751
+ seq;
2752
+ accept(frame) {
2753
+ if (this.seq === void 0 && !frame.full) throw new Error("baseline_required");
2754
+ if (this.seq !== void 0 && frame.seq !== this.seq + 1) throw new Error("sequence_gap");
2755
+ this.seq = frame.seq;
2756
+ }
2757
+ };
2758
+ //#endregion
2759
+ //#region src/client/terminal-renderer.ts
2760
+ var TerminalRenderer = class {
2761
+ terminal;
2762
+ acknowledge;
2763
+ budget;
2764
+ frames = [];
2765
+ bytes = 0;
2766
+ writing = false;
2767
+ closed = false;
2768
+ constructor(terminal, acknowledge, budget = 524288) {
2769
+ this.terminal = terminal;
2770
+ this.acknowledge = acknowledge;
2771
+ this.budget = budget;
2772
+ }
2773
+ push(frame) {
2774
+ if (this.closed) return;
2775
+ if (this.bytes + frame.bytes.length > this.budget) throw new Error("renderer_overload");
2776
+ this.bytes += frame.bytes.length;
2777
+ this.frames.push(frame);
2778
+ this.frames.length;
2779
+ this.bytes / 1024;
2780
+ this.flush();
2781
+ }
2782
+ close() {
2783
+ this.closed = true;
2784
+ this.frames = [];
2785
+ this.bytes = 0;
2786
+ }
2787
+ flush() {
2788
+ if (this.closed || this.writing || !this.frames.length) return;
2789
+ this.writing = true;
2790
+ const frame = this.frames.shift();
2791
+ this.terminal.resize(frame.width, frame.height);
2792
+ const bytes = frame.full ? new Uint8Array(frame.bytes.length + 2) : frame.bytes;
2793
+ if (frame.full) {
2794
+ bytes.set([27, 99]);
2795
+ bytes.set(frame.bytes, 2);
2796
+ }
2797
+ const writeStart = perfNow();
2798
+ this.terminal.write(bytes, () => {
2799
+ if (this.closed) return;
2800
+ perfNow() - writeStart;
2801
+ this.bytes -= frame.bytes.length;
2802
+ this.writing = false;
2803
+ this.acknowledge(frame);
2804
+ this.flush();
2805
+ });
2806
+ }
2807
+ };
2808
+ //#endregion
2809
+ //#region src/shared/retry.ts
2810
+ var retryDelay = (attempt, random = Math.random) => Math.min(3e4, Math.round(Math.min(25e3, 1e3 * 2 ** Math.min(attempt, 5)) * (.8 + random() * .4)));
2811
+ //#endregion
2812
+ //#region src/client/terminal-manager.ts
2813
+ var conflictMessage = "Another controller owns this terminal.";
2814
+ var BrowserTerminalManager = class {
2815
+ install;
2816
+ onConnection;
2817
+ metadata;
2818
+ binary;
2819
+ stopped = true;
2820
+ epoch = 0;
2821
+ nextId = 1;
2822
+ panes = /* @__PURE__ */ new Map();
2823
+ reconnect;
2824
+ instance = "";
2825
+ failures = 0;
2826
+ connectedAt = 0;
2827
+ scrollSentAt = /* @__PURE__ */ new Map();
2828
+ inputSentAt = /* @__PURE__ */ new Map();
2829
+ openedAt = /* @__PURE__ */ new Map();
2830
+ constructor(install, onConnection) {
2831
+ this.install = install;
2832
+ this.onConnection = onConnection;
2833
+ }
2834
+ start() {
2835
+ this.stopped = false;
2836
+ this.connect();
2837
+ }
2838
+ stop() {
2839
+ this.stopped = true;
2840
+ clearTimeout(this.reconnect);
2841
+ this.disconnect();
2842
+ }
2843
+ mount(pane) {
2844
+ const id = this.nextId++;
2845
+ this.panes.set(id, {
2846
+ ...pane,
2847
+ generation: 1,
2848
+ sequence: new FrameSequence(),
2849
+ writable: false,
2850
+ accepted: false,
2851
+ baseline: false,
2852
+ pending: 0,
2853
+ resizing: false,
2854
+ attempts: 0
2855
+ });
2856
+ this.open(id);
2857
+ return {
2858
+ close: () => {
2859
+ const current = this.panes.get(id);
2860
+ if (current) {
2861
+ this.send({
2862
+ type: "release",
2863
+ streamId: id,
2864
+ generation: current.generation
2865
+ });
2866
+ current.renderer?.close();
2867
+ }
2868
+ this.panes.delete(id);
2869
+ },
2870
+ input: (text, paste = false) => {
2871
+ const current = this.panes.get(id);
2872
+ if (current?.writable) {
2873
+ this.inputSentAt.set(id, perfNow());
2874
+ this.send({
2875
+ type: "input",
2876
+ streamId: id,
2877
+ generation: current.generation,
2878
+ text,
2879
+ paste
2880
+ });
2881
+ }
2882
+ },
2883
+ control: (takeover = false) => {
2884
+ const current = this.panes.get(id);
2885
+ if (current && (current.mode !== "control" || takeover)) {
2886
+ this.send({
2887
+ type: "release",
2888
+ streamId: id,
2889
+ generation: current.generation
2890
+ });
2891
+ current.reason = void 0;
2892
+ current.mode = "control";
2893
+ ++current.generation;
2894
+ current.sequence = new FrameSequence();
2895
+ this.open(id, takeover);
2896
+ }
2897
+ },
2898
+ observe: () => {
2899
+ const current = this.panes.get(id);
2900
+ if (current && current.mode !== "observe") {
2901
+ this.send({
2902
+ type: "release",
2903
+ streamId: id,
2904
+ generation: current.generation
2905
+ });
2906
+ current.mode = "observe";
2907
+ ++current.generation;
2908
+ current.sequence = new FrameSequence();
2909
+ this.open(id);
2910
+ }
2911
+ },
2912
+ resize: (cols, rows, claim = false) => {
2913
+ const current = this.panes.get(id);
2914
+ if (!current || cols === current.cols && rows === current.rows && !(claim && current.mode === "control" && current.accepted)) return;
2915
+ current.cols = cols;
2916
+ current.rows = rows;
2917
+ if (current.mode === "control" && current.accepted) {
2918
+ current.writable = false;
2919
+ current.baseline = false;
2920
+ current.resizing = true;
2921
+ current.onState("Waiting for the resized full baseline.", false);
2922
+ this.send({
2923
+ type: "resize",
2924
+ streamId: id,
2925
+ generation: current.generation,
2926
+ cols,
2927
+ rows
2928
+ });
2929
+ } else {
2930
+ this.send({
2931
+ type: "release",
2932
+ streamId: id,
2933
+ generation: current.generation
2934
+ });
2935
+ ++current.generation;
2936
+ current.sequence = new FrameSequence();
2937
+ this.open(id);
2938
+ }
2939
+ },
2940
+ scroll: (direction, lines, column, row) => {
2941
+ const current = this.panes.get(id);
2942
+ if (current?.writable) {
2943
+ this.scrollSentAt.set(id, perfNow());
2944
+ this.send({
2945
+ type: "scroll",
2946
+ streamId: id,
2947
+ generation: current.generation,
2948
+ direction,
2949
+ lines,
2950
+ column,
2951
+ row
2952
+ });
2953
+ }
2954
+ },
2955
+ mouse: (action, button, column, row, modifiers) => {
2956
+ const current = this.panes.get(id);
2957
+ if (current?.writable) this.send({
2958
+ type: "mouse",
2959
+ streamId: id,
2960
+ generation: current.generation,
2961
+ action,
2962
+ button,
2963
+ column,
2964
+ row,
2965
+ modifiers
2966
+ });
2967
+ }
2968
+ };
2969
+ }
2970
+ command(machineId, threadId, terminalId, action) {
2971
+ const entry = [...this.panes.entries()].find(([, pane]) => pane.machineId === machineId && pane.threadId === threadId && pane.terminalId === terminalId);
2972
+ if (!entry) return;
2973
+ const [id, pane] = entry;
2974
+ pane.writable = false;
2975
+ pane.reason = void 0;
2976
+ this.send({
2977
+ type: "release",
2978
+ streamId: id,
2979
+ generation: pane.generation
2980
+ });
2981
+ pane.mode = action === "release" ? "observe" : "control";
2982
+ ++pane.generation;
2983
+ pane.sequence = new FrameSequence();
2984
+ this.open(id, action === "takeover");
2985
+ }
2986
+ send(action) {
2987
+ if (this.binary?.readyState !== WebSocket.OPEN || this.metadata?.readyState !== WebSocket.OPEN) return false;
2988
+ if (this.binary.bufferedAmount > 32768) {
2989
+ this.disconnect();
2990
+ return false;
2991
+ }
2992
+ this.binary.send(JSON.stringify(action));
2993
+ return true;
2994
+ }
2995
+ open(id, takeover = false) {
2996
+ const pane = this.panes.get(id);
2997
+ if (!pane) return;
2998
+ pane.writable = false;
2999
+ pane.accepted = false;
3000
+ pane.baseline = false;
3001
+ pane.pending = 0;
3002
+ pane.resizing = false;
3003
+ pane.renderer?.close();
3004
+ pane.renderer = new TerminalRenderer(pane.terminal, (frame) => {
3005
+ if (this.panes.get(id) !== pane || pane.generation !== frame.generation) return;
3006
+ if (!this.send({
3007
+ type: "ack",
3008
+ streamId: id,
3009
+ generation: frame.generation,
3010
+ seq: frame.seq
3011
+ })) return;
3012
+ --pane.pending;
3013
+ if (frame.full && (!pane.resizing || frame.width === pane.cols && frame.height === pane.rows)) {
3014
+ pane.baseline = true;
3015
+ pane.resizing = false;
3016
+ const openedAt = this.openedAt.get(id);
3017
+ if (openedAt !== void 0) {
3018
+ perfNow() - openedAt;
3019
+ this.openedAt.delete(id);
3020
+ }
3021
+ }
3022
+ const ready = pane.mode === "control" && pane.accepted && pane.baseline;
3023
+ pane.writable = ready;
3024
+ if (frame.full) pane.onState(ready ? "Input control is active." : pane.reason ?? "Read-only", ready);
3025
+ });
3026
+ pane.onState(pane.mode === "control" ? "Waiting for terminal control." : pane.reason ?? "Read-only", false);
3027
+ this.openedAt.set(id, perfNow());
3028
+ this.send({
3029
+ type: "open",
3030
+ streamId: id,
3031
+ generation: pane.generation,
3032
+ machineId: pane.machineId,
3033
+ threadId: pane.threadId,
3034
+ terminalId: pane.terminalId,
3035
+ mode: pane.mode,
3036
+ cols: pane.cols,
3037
+ rows: pane.rows,
3038
+ takeover
3039
+ });
3040
+ }
3041
+ connect() {
3042
+ if (this.stopped) return;
3043
+ this.instance = uuid();
3044
+ const epoch = ++this.epoch;
3045
+ const base = (location.protocol === "https:" ? "wss://" : "ws://") + location.host;
3046
+ const metadata = new WebSocket(base + "/api/ws/metadata?instance=" + this.instance);
3047
+ const binary = new WebSocket(base + "/api/ws/terminal?instance=" + this.instance);
3048
+ this.metadata = metadata;
3049
+ this.binary = binary;
3050
+ binary.binaryType = "arraybuffer";
3051
+ const opened = () => {
3052
+ if (epoch !== this.epoch || metadata.readyState !== 1 || binary.readyState !== 1) return;
3053
+ this.connectedAt = Date.now();
3054
+ this.onConnection(true);
3055
+ for (const [id, pane] of this.panes) {
3056
+ ++pane.generation;
3057
+ pane.sequence = new FrameSequence();
3058
+ pane.attempts = 0;
3059
+ this.open(id);
3060
+ }
3061
+ };
3062
+ metadata.onopen = opened;
3063
+ binary.onopen = opened;
3064
+ metadata.onmessage = (event) => {
3065
+ if (epoch !== this.epoch) return;
3066
+ try {
3067
+ const record = JSON.parse(event.data);
3068
+ if (record.type === "projection") this.install(record.projection);
3069
+ } catch {
3070
+ this.disconnect();
3071
+ }
3072
+ };
3073
+ binary.onmessage = (event) => {
3074
+ if (epoch !== this.epoch) return;
3075
+ try {
3076
+ if (typeof event.data === "string") {
3077
+ const record = JSON.parse(event.data);
3078
+ const pane = this.panes.get(record.streamId);
3079
+ if (!pane || pane.generation !== record.generation) return;
3080
+ if (record.type === "stream.opened") {
3081
+ pane.accepted = !!record.writable && pane.mode === "control";
3082
+ if (pane.accepted) {
3083
+ pane.baseline = false;
3084
+ pane.resizing = true;
3085
+ this.send({
3086
+ type: "resize",
3087
+ streamId: record.streamId,
3088
+ generation: record.generation,
3089
+ cols: pane.cols,
3090
+ rows: pane.rows
3091
+ });
3092
+ }
3093
+ pane.writable = pane.accepted && pane.baseline;
3094
+ pane.onState(pane.writable ? "Input control is active." : pane.reason ?? (pane.accepted ? "Waiting for the full baseline; input is disabled." : "Read-only"), pane.writable);
3095
+ }
3096
+ if (record.type === "stream.closed" || record.type === "stream.error") {
3097
+ pane.writable = false;
3098
+ pane.accepted = false;
3099
+ pane.baseline = false;
3100
+ pane.renderer?.close();
3101
+ pane.onState(record.reason === "controller_conflict" ? conflictMessage : String(record.reason).replaceAll("_", " "), false);
3102
+ if (record.type === "stream.error" && pane.mode === "control") {
3103
+ pane.reason = record.reason === "controller_conflict" ? conflictMessage : String(record.reason).replaceAll("_", " ");
3104
+ pane.mode = "observe";
3105
+ ++pane.generation;
3106
+ pane.sequence = new FrameSequence();
3107
+ this.open(record.streamId);
3108
+ return;
3109
+ }
3110
+ if (record.reason === "stream_resync_required" && pane.attempts++ < 2) {
3111
+ ++pane.generation;
3112
+ pane.sequence = new FrameSequence();
3113
+ pane.mode = "observe";
3114
+ this.open(record.streamId);
3115
+ }
3116
+ }
3117
+ return;
3118
+ }
3119
+ const decodeStart = perfNow();
3120
+ const frame = decodeFrame(new Uint8Array(event.data));
3121
+ event.data.byteLength;
3122
+ perfNow() - decodeStart;
3123
+ const typedAt = this.inputSentAt.get(frame.streamId);
3124
+ if (typedAt !== void 0) {
3125
+ perfNow() - typedAt;
3126
+ this.inputSentAt.delete(frame.streamId);
3127
+ }
3128
+ const scrolledAt = this.scrollSentAt.get(frame.streamId);
3129
+ if (scrolledAt !== void 0) {
3130
+ perfNow() - scrolledAt;
3131
+ this.scrollSentAt.delete(frame.streamId);
3132
+ }
3133
+ const pane = this.panes.get(frame.streamId);
3134
+ if (!pane || pane.generation !== frame.generation) return;
3135
+ pane.sequence.accept(frame);
3136
+ ++pane.pending;
3137
+ pane.pending;
3138
+ pane.renderer?.push(frame);
3139
+ } catch {
3140
+ this.disconnect();
3141
+ }
3142
+ };
3143
+ const lost = () => {
3144
+ if (epoch === this.epoch) this.disconnect();
3145
+ };
3146
+ metadata.onclose = lost;
3147
+ metadata.onerror = lost;
3148
+ binary.onclose = lost;
3149
+ binary.onerror = lost;
3150
+ }
3151
+ disconnect() {
3152
+ if (this.connectedAt && Date.now() - this.connectedAt >= 3e4) this.failures = 0;
3153
+ this.connectedAt = 0;
3154
+ ++this.epoch;
3155
+ this.metadata?.close();
3156
+ this.binary?.close();
3157
+ this.metadata = void 0;
3158
+ this.binary = void 0;
3159
+ this.onConnection(false);
3160
+ for (const pane of this.panes.values()) {
3161
+ pane.renderer?.close();
3162
+ pane.writable = false;
3163
+ pane.mode = "observe";
3164
+ pane.onState("Disconnected; input was not saved.", false);
3165
+ }
3166
+ clearTimeout(this.reconnect);
3167
+ if (!this.stopped) this.reconnect = setTimeout(() => this.connect(), retryDelay(this.failures++));
3168
+ }
3169
+ };
3170
+ //#endregion
3171
+ //#region src/client/provider.tsx
3172
+ var Context = createContext(null);
3173
+ function RuntimeProvider({ bootstrap, children }) {
3174
+ const [value] = useState(() => {
3175
+ const runtime = createRuntimeStore(bootstrap);
3176
+ return {
3177
+ runtime,
3178
+ ui: createUiStore(),
3179
+ terminals: new BrowserTerminalManager((projection) => {
3180
+ const done = span("projectionInstallMs");
3181
+ runtime.getState().install(projection);
3182
+ done();
3183
+ }, (connected) => runtime.getState().connection(connected))
3184
+ };
3185
+ });
3186
+ const [query] = useState(() => new QueryClient({ defaultOptions: { queries: {
3187
+ staleTime: 15e3,
3188
+ retry: false
3189
+ } } }));
3190
+ useEffect(() => {
3191
+ value.terminals.start();
3192
+ return () => value.terminals.stop();
3193
+ }, [value]);
3194
+ return /* @__PURE__ */ jsx(Context.Provider, {
3195
+ value,
3196
+ children: /* @__PURE__ */ jsx(QueryClientProvider, {
3197
+ client: query,
3198
+ children
3199
+ })
3200
+ });
3201
+ }
3202
+ function useRuntime() {
3203
+ const value = useContext(Context);
3204
+ if (!value) throw new Error("Runtime provider is missing");
3205
+ return value;
3206
+ }
3207
+ function useRuntimeSelector(selector) {
3208
+ return useStore(useRuntime().runtime, selector);
3209
+ }
3210
+ //#endregion
3211
+ //#region src/components/TerminalInput.tsx
3212
+ var specialKeys = {
3213
+ Enter: "\r",
3214
+ Backspace: "",
3215
+ Tab: " ",
3216
+ Escape: "\x1B",
3217
+ ArrowUp: "\x1B[A",
3218
+ ArrowDown: "\x1B[B",
3219
+ ArrowRight: "\x1B[C",
3220
+ ArrowLeft: "\x1B[D"
3221
+ };
3222
+ function terminalKeyBytes(event) {
3223
+ const bytes = event.ctrlKey && /^[a-z]$/i.test(event.key) ? String.fromCharCode(event.key.toUpperCase().charCodeAt(0) - 64) : specialKeys[event.key];
3224
+ const newlineInsteadOfSubmit = event.key === "Enter" && event.shiftKey;
3225
+ return bytes && (event.altKey || newlineInsteadOfSubmit ? "\x1B" + bytes : bytes);
3226
+ }
3227
+ var controlCharacter = (text) => /^[a-z@\[\\\]^_]$/i.test(text) ? String.fromCharCode(text.toUpperCase().charCodeAt(0) & 31) : void 0;
3228
+ function TerminalInput({ onInput, onFocusChange }) {
3229
+ const [draft, setDraft] = useState("");
3230
+ const composing = useRef(false);
3231
+ const committed = useRef(null);
3232
+ return /* @__PURE__ */ jsx("textarea", {
3233
+ className: "terminal-input-capture",
3234
+ value: draft,
3235
+ "aria-label": "Terminal input",
3236
+ rows: 1,
3237
+ onFocus: () => onFocusChange?.(true),
3238
+ onBlur: () => onFocusChange?.(false),
3239
+ onCompositionStart: () => {
3240
+ composing.current = true;
3241
+ committed.current = null;
3242
+ },
3243
+ onCompositionEnd: (event) => {
3244
+ composing.current = false;
3245
+ const text = event.data;
3246
+ committed.current = text;
3247
+ if (text) onInput(text);
3248
+ setDraft("");
3249
+ event.currentTarget.value = "";
3250
+ },
3251
+ onChange: (event) => {
3252
+ if (composing.current) {
3253
+ setDraft(event.target.value);
3254
+ return;
3255
+ }
3256
+ const text = event.target.value;
3257
+ if (text && text !== committed.current) onInput(text);
3258
+ committed.current = null;
3259
+ setDraft("");
3260
+ event.currentTarget.value = "";
3261
+ },
3262
+ onPaste: (event) => {
3263
+ event.preventDefault();
3264
+ committed.current = null;
3265
+ const text = event.clipboardData.getData("text/plain");
3266
+ if (text) onInput(text, true);
3267
+ },
3268
+ onKeyDown: (event) => {
3269
+ if (event.defaultPrevented || event.nativeEvent.isComposing || composing.current || event.metaKey) return;
3270
+ committed.current = null;
3271
+ const bytes = terminalKeyBytes(event);
3272
+ if (bytes) {
3273
+ event.preventDefault();
3274
+ onInput(bytes);
3275
+ }
3276
+ }
3277
+ });
3278
+ }
3279
+ //#endregion
3280
+ //#region src/components/TerminalKeyBar.tsx
3281
+ var keys = [
3282
+ {
3283
+ label: "Tab",
3284
+ key: "Tab"
3285
+ },
3286
+ {
3287
+ label: /* @__PURE__ */ jsx(ArrowLeft, { size: 16 }),
3288
+ key: "ArrowLeft",
3289
+ name: "Left"
3290
+ },
3291
+ {
3292
+ label: /* @__PURE__ */ jsx(ArrowDown, { size: 16 }),
3293
+ key: "ArrowDown",
3294
+ name: "Down"
3295
+ },
3296
+ {
3297
+ label: /* @__PURE__ */ jsx(ArrowUp, { size: 16 }),
3298
+ key: "ArrowUp",
3299
+ name: "Up"
3300
+ },
3301
+ {
3302
+ label: /* @__PURE__ */ jsx(ArrowRight, { size: 16 }),
3303
+ key: "ArrowRight",
3304
+ name: "Right"
3305
+ }
3306
+ ];
3307
+ var keepKeyboardOpen = (event) => event.preventDefault();
3308
+ function TerminalKeyBar({ ctrl, onToggleCtrl, onBytes }) {
3309
+ return /* @__PURE__ */ jsxs("div", {
3310
+ className: "terminal-key-bar",
3311
+ role: "toolbar",
3312
+ "aria-label": "Terminal keys",
3313
+ children: [/* @__PURE__ */ jsx("button", {
3314
+ type: "button",
3315
+ tabIndex: -1,
3316
+ "aria-pressed": ctrl,
3317
+ className: ctrl ? "is-armed" : "",
3318
+ onPointerDown: keepKeyboardOpen,
3319
+ onClick: onToggleCtrl,
3320
+ children: "Ctrl"
3321
+ }), keys.map(({ label, key, name }) => /* @__PURE__ */ jsx("button", {
3322
+ type: "button",
3323
+ tabIndex: -1,
3324
+ "aria-label": name ?? key,
3325
+ onPointerDown: keepKeyboardOpen,
3326
+ onClick: () => {
3327
+ const bytes = terminalKeyBytes({
3328
+ key,
3329
+ ctrlKey: false,
3330
+ altKey: false
3331
+ });
3332
+ if (bytes) onBytes(bytes);
3333
+ },
3334
+ children: label
3335
+ }, key))]
3336
+ });
3337
+ }
3338
+ //#endregion
3339
+ //#region src/client/visual-viewport.ts
3340
+ var COARSE_POINTER = "(pointer: coarse)";
3341
+ function trackVisualViewport() {
3342
+ const viewport = window.visualViewport;
3343
+ if (!viewport || !matchMedia(COARSE_POINTER).matches) return () => {};
3344
+ const root = document.documentElement;
3345
+ const sync = () => {
3346
+ root.style.setProperty("--app-height", `${viewport.height}px`);
3347
+ root.style.setProperty("--app-top", `${viewport.offsetTop}px`);
3348
+ };
3349
+ sync();
3350
+ viewport.addEventListener("resize", sync);
3351
+ viewport.addEventListener("scroll", sync);
3352
+ return () => {
3353
+ viewport.removeEventListener("resize", sync);
3354
+ viewport.removeEventListener("scroll", sync);
3355
+ root.style.removeProperty("--app-height");
3356
+ root.style.removeProperty("--app-top");
3357
+ };
3358
+ }
3359
+ function matchesCoarsePointer() {
3360
+ return typeof matchMedia === "function" && matchMedia(COARSE_POINTER).matches;
3361
+ }
3362
+ //#endregion
3363
+ //#region src/client/terminal-gestures.ts
3364
+ var MAX_LINES_PER_MESSAGE = 100;
3365
+ var FALLBACK_CELL_HEIGHT = 20;
3366
+ var SCROLL_SPEED = 10;
3367
+ var flickGain = (pixels) => SCROLL_SPEED * (1 + Math.min(2, Math.abs(pixels) / 60));
3368
+ function bindTerminalGestures(host, screen, control, writable) {
3369
+ const pointer = (event) => {
3370
+ if (!writable() || !event.altKey || event.shiftKey) return;
3371
+ const { element, cols, rows } = screen();
3372
+ const rect = element?.getBoundingClientRect();
3373
+ if (!rect?.width || !rect.height) return;
3374
+ const column = Math.floor((event.clientX - rect.left) / rect.width * cols);
3375
+ const row = Math.floor((event.clientY - rect.top) / rect.height * rows);
3376
+ if (column < 0 || row < 0 || column >= cols || row >= rows) return;
3377
+ event.preventDefault();
3378
+ const button = event.button === 2 || event.buttons & 2 ? "right" : event.button === 1 || event.buttons & 4 ? "middle" : "left";
3379
+ control()?.mouse(event.type === "pointerdown" ? "down" : event.type === "pointerup" ? "up" : event.buttons ? "drag" : "move", button, column, row, event.ctrlKey ? 2 : 0);
3380
+ };
3381
+ let pendingPixels = 0;
3382
+ let frame = 0;
3383
+ let pointerCell;
3384
+ const cellUnderPointer = (event) => {
3385
+ const { element, cols, rows } = screen();
3386
+ const rect = element?.getBoundingClientRect();
3387
+ if (!rect?.width || !rect.height) return void 0;
3388
+ return {
3389
+ column: Math.min(cols, Math.max(1, Math.floor((event.clientX - rect.left) / rect.width * cols) + 1)),
3390
+ row: Math.min(rows, Math.max(1, Math.floor((event.clientY - rect.top) / rect.height * rows) + 1))
3391
+ };
3392
+ };
3393
+ const cellHeight = () => {
3394
+ const { element, rows } = screen();
3395
+ const height = element?.getBoundingClientRect().height;
3396
+ return height && rows ? height / rows : FALLBACK_CELL_HEIGHT;
3397
+ };
3398
+ const flushScroll = () => {
3399
+ frame = 0;
3400
+ const cell = cellHeight();
3401
+ const lines = Math.trunc(pendingPixels / cell);
3402
+ if (!lines) return;
3403
+ pendingPixels -= lines * cell;
3404
+ control()?.scroll(lines < 0 ? "up" : "down", Math.min(MAX_LINES_PER_MESSAGE, Math.abs(lines)), pointerCell?.column, pointerCell?.row);
3405
+ };
3406
+ const wheel = (event) => {
3407
+ if (!writable() || !event.deltaY) return;
3408
+ pointerCell = cellUnderPointer(event);
3409
+ event.preventDefault();
3410
+ const pixels = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? event.deltaY * cellHeight() : event.deltaY;
3411
+ if (pixels * pendingPixels < 0) pendingPixels = 0;
3412
+ pendingPixels += pixels * flickGain(pixels);
3413
+ frame ||= requestAnimationFrame(flushScroll);
3414
+ };
3415
+ for (const type of [
3416
+ "pointerdown",
3417
+ "pointerup",
3418
+ "pointermove"
3419
+ ]) host.addEventListener(type, pointer);
3420
+ host.addEventListener("wheel", wheel, { passive: false });
3421
+ return () => {
3422
+ for (const type of [
3423
+ "pointerdown",
3424
+ "pointerup",
3425
+ "pointermove"
3426
+ ]) host.removeEventListener(type, pointer);
3427
+ host.removeEventListener("wheel", wheel);
3428
+ cancelAnimationFrame(frame);
3429
+ };
3430
+ }
3431
+ //#endregion
3432
+ //#region src/components/useImageAttachments.ts
3433
+ var ONLY_IMAGES = "Only PNG, JPEG, GIF and WebP images can be attached.";
3434
+ var attachingLabel = (count) => count === 1 ? "Attaching image…" : `Attaching ${count} images…`;
3435
+ var attachedLabel = (count) => count === 1 ? "Image attached" : `${count} images attached`;
3436
+ function useImageAttachments({ machineId, active, writable, send, show }) {
3437
+ const [dragging, setDragging] = useState(false);
3438
+ const sendRef = useRef(send);
3439
+ sendRef.current = send;
3440
+ const attach = useCallback(async (files) => {
3441
+ if (!files.length) {
3442
+ show(ONLY_IMAGES, "error", 4e3);
3443
+ return;
3444
+ }
3445
+ if (!writable.current) {
3446
+ show("Wait until the terminal accepts input, then attach the image again.", "error", 4e3);
3447
+ return;
3448
+ }
3449
+ show(attachingLabel(files.length), "info");
3450
+ try {
3451
+ sendRef.current(await uploadImagesAsTerminalText(machineId, files));
3452
+ show(attachedLabel(files.length), "info", 1500);
3453
+ } catch (error) {
3454
+ show(error instanceof Error ? error.message : "The image could not be attached.", "error", 5e3);
3455
+ }
3456
+ }, [
3457
+ machineId,
3458
+ show,
3459
+ writable
3460
+ ]);
3461
+ useEffect(() => {
3462
+ if (!active) return;
3463
+ let depth = 0;
3464
+ const enter = (event) => {
3465
+ if (!carriesFiles(event.dataTransfer)) return;
3466
+ event.preventDefault();
3467
+ depth += 1;
3468
+ setDragging(true);
3469
+ };
3470
+ const over = (event) => {
3471
+ if (!carriesFiles(event.dataTransfer)) return;
3472
+ event.preventDefault();
3473
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
3474
+ };
3475
+ const leave = (event) => {
3476
+ if (!carriesFiles(event.dataTransfer)) return;
3477
+ depth = Math.max(0, depth - 1);
3478
+ if (!depth) setDragging(false);
3479
+ };
3480
+ const drop = (event) => {
3481
+ if (!carriesFiles(event.dataTransfer)) return;
3482
+ event.preventDefault();
3483
+ depth = 0;
3484
+ setDragging(false);
3485
+ attach(imageFilesFrom(event.dataTransfer));
3486
+ };
3487
+ window.addEventListener("dragenter", enter);
3488
+ window.addEventListener("dragover", over);
3489
+ window.addEventListener("dragleave", leave);
3490
+ window.addEventListener("drop", drop);
3491
+ return () => {
3492
+ window.removeEventListener("dragenter", enter);
3493
+ window.removeEventListener("dragover", over);
3494
+ window.removeEventListener("dragleave", leave);
3495
+ window.removeEventListener("drop", drop);
3496
+ setDragging(false);
3497
+ };
3498
+ }, [active, attach]);
3499
+ return {
3500
+ dragging,
3501
+ attach
3502
+ };
3503
+ }
3504
+ //#endregion
3505
+ //#region src/client/terminal-links.ts
3506
+ function openLinkOnModifierClick(event, uri) {
3507
+ if (!event.metaKey && !event.ctrlKey) return;
3508
+ window.open(uri, "_blank", "noopener,noreferrer");
3509
+ }
3510
+ //#endregion
3511
+ //#region src/components/useTerminalNotice.ts
3512
+ function useTerminalNotice() {
3513
+ const [notice, setNotice] = useState(null);
3514
+ const hide = useRef(void 0);
3515
+ const show = useCallback((text, tone, durationMs) => {
3516
+ clearTimeout(hide.current);
3517
+ setNotice({
3518
+ text,
3519
+ tone
3520
+ });
3521
+ if (durationMs) hide.current = setTimeout(() => setNotice(null), durationMs);
3522
+ }, []);
3523
+ useEffect(() => () => clearTimeout(hide.current), []);
3524
+ return {
3525
+ notice,
3526
+ show
3527
+ };
3528
+ }
3529
+ //#endregion
3530
+ //#region src/components/TerminalPane.tsx
3531
+ function themeColors() {
3532
+ const styles = getComputedStyle(document.documentElement);
3533
+ return {
3534
+ background: styles.getPropertyValue("--terminal-bg").trim(),
3535
+ foreground: styles.getPropertyValue("--terminal-ink").trim(),
3536
+ cursor: styles.getPropertyValue("--terminal-cursor").trim(),
3537
+ selectionBackground: styles.getPropertyValue("--terminal-selection").trim(),
3538
+ black: "#222222",
3539
+ red: "#df7774",
3540
+ green: "#8ebea1",
3541
+ yellow: "#d4b581",
3542
+ blue: "#81aee0",
3543
+ magenta: "#ac9dd5",
3544
+ cyan: "#8dc6cf",
3545
+ white: "#d5d5d5",
3546
+ brightBlack: "#777777",
3547
+ brightRed: "#f18f89",
3548
+ brightGreen: "#a1d8b6",
3549
+ brightYellow: "#eed1a2",
3550
+ brightBlue: "#a7c7ef",
3551
+ brightMagenta: "#c6b6e8",
3552
+ brightCyan: "#abdee4",
3553
+ brightWhite: "#ffffff"
3554
+ };
3555
+ }
3556
+ var TERMINAL_FONT_FAMILY = "\"JetBrains Mono\", \"Webr Symbols\", ui-monospace, Menlo, monospace";
3557
+ var TERMINAL_FONT_SIZE = 13;
3558
+ var loadTerminalFonts = () => typeof document === "undefined" || !document.fonts ? Promise.resolve() : Promise.all([
3559
+ document.fonts.load(`400 ${TERMINAL_FONT_SIZE}px "JetBrains Mono"`),
3560
+ document.fonts.load(`700 ${TERMINAL_FONT_SIZE}px "JetBrains Mono"`),
3561
+ document.fonts.load(`${TERMINAL_FONT_SIZE}px "Webr Symbols"`, "")
3562
+ ]).then(() => void 0, () => void 0);
3563
+ var FOCUS_IN = "\x1B[I";
3564
+ var FOCUS_OUT = "\x1B[O";
3565
+ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }) {
3566
+ const host = useRef(null);
3567
+ const section = useRef(null);
3568
+ const term = useRef(null);
3569
+ const control = useRef(null);
3570
+ const { terminals } = useRuntime();
3571
+ const [message, setMessage] = useState("Connecting…");
3572
+ const [writable, setWritable] = useState(false);
3573
+ const writableRef = useRef(false);
3574
+ const [inputFocused, setInputFocused] = useState(false);
3575
+ const [ctrl, setCtrl] = useState(false);
3576
+ const [touch, setTouch] = useState(false);
3577
+ useEffect(() => setTouch(matchesCoarsePointer()), []);
3578
+ const { notice, show } = useTerminalNotice();
3579
+ const images = useImageAttachments({
3580
+ machineId,
3581
+ active,
3582
+ writable: writableRef,
3583
+ show,
3584
+ send: (text) => {
3585
+ control.current?.input(text, true);
3586
+ }
3587
+ });
3588
+ const showCopyResult = useRef((copied) => {});
3589
+ showCopyResult.current = (copied) => copied ? show("Copied to clipboard", "info", 1200) : show("The browser blocked clipboard access.", "error", 4e3);
3590
+ useEffect(() => {
3591
+ if (!host.current) return;
3592
+ let disposed = false;
3593
+ let cleanup = () => {};
3594
+ Promise.all([
3595
+ import("@xterm/xterm"),
3596
+ import("@xterm/addon-fit"),
3597
+ import("@xterm/addon-web-links"),
3598
+ loadTerminalFonts()
3599
+ ]).then(([xterm, { FitAddon }, { WebLinksAddon }]) => {
3600
+ if (disposed || !host.current) return;
3601
+ const terminal = new xterm.Terminal({
3602
+ fontFamily: TERMINAL_FONT_FAMILY,
3603
+ fontSize: TERMINAL_FONT_SIZE,
3604
+ lineHeight: 1.2,
3605
+ letterSpacing: 0,
3606
+ cursorBlink: true,
3607
+ cursorInactiveStyle: "block",
3608
+ allowTransparency: false,
3609
+ scrollback: 0,
3610
+ theme: themeColors(),
3611
+ disableStdin: true,
3612
+ linkHandler: { activate: () => {} }
3613
+ });
3614
+ terminal.open(host.current);
3615
+ terminal.attachCustomWheelEventHandler(() => false);
3616
+ term.current = terminal;
3617
+ const redirectFocus = () => {
3618
+ if (writableRef.current) host.current?.parentElement?.querySelector(".terminal-input-capture")?.focus();
3619
+ };
3620
+ terminal.textarea?.addEventListener("focus", redirectFocus);
3621
+ const clipboard = bindTerminalClipboard(host.current, terminal, () => writableRef.current, (copied) => showCopyResult.current(copied));
3622
+ const links = terminal.parser.registerOscHandler(8, () => true);
3623
+ if (!pane.terminalId) {
3624
+ clipboard();
3625
+ terminal.dispose();
3626
+ return;
3627
+ }
3628
+ const fit = new FitAddon();
3629
+ terminal.loadAddon(fit);
3630
+ terminal.loadAddon(new WebLinksAddon(openLinkOnModifierClick));
3631
+ const estimatedViewport = () => ({
3632
+ cols: Math.floor((host.current?.clientWidth ?? 640) / 8),
3633
+ rows: Math.floor((host.current?.clientHeight ?? 400) / 20)
3634
+ });
3635
+ const measuredViewport = () => {
3636
+ try {
3637
+ return fit.proposeDimensions();
3638
+ } catch {
3639
+ return;
3640
+ }
3641
+ };
3642
+ const viewport = () => {
3643
+ const { cols, rows } = measuredViewport() ?? estimatedViewport();
3644
+ return {
3645
+ cols: Math.max(2, Math.min(500, cols)),
3646
+ rows: Math.max(1, Math.min(300, rows))
3647
+ };
3648
+ };
3649
+ control.current = terminals.mount({
3650
+ machineId,
3651
+ threadId,
3652
+ terminalId: pane.terminalId,
3653
+ terminal,
3654
+ mode: canControl ? "control" : "observe",
3655
+ ...viewport(),
3656
+ onState: (message, writable) => {
3657
+ setMessage(message);
3658
+ setWritable(writable);
3659
+ writableRef.current = writable;
3660
+ }
3661
+ });
3662
+ const gestures = bindTerminalGestures(host.current, () => ({
3663
+ element: terminal.element?.querySelector(".xterm-screen") ?? void 0,
3664
+ cols: terminal.cols,
3665
+ rows: terminal.rows
3666
+ }), () => control.current, () => writableRef.current);
3667
+ let timer;
3668
+ const resize = new ResizeObserver(() => {
3669
+ clearTimeout(timer);
3670
+ timer = setTimeout(() => {
3671
+ const { cols, rows } = viewport();
3672
+ control.current?.resize(cols, rows);
3673
+ }, 150);
3674
+ });
3675
+ resize.observe(host.current);
3676
+ const claimSize = () => {
3677
+ if (document.visibilityState !== "visible" || !document.hasFocus()) return;
3678
+ const { cols, rows } = viewport();
3679
+ control.current?.resize(cols, rows, true);
3680
+ };
3681
+ window.addEventListener("focus", claimSize);
3682
+ document.addEventListener("visibilitychange", claimSize);
3683
+ const colorObserver = new MutationObserver(() => {
3684
+ terminal.options.theme = themeColors();
3685
+ });
3686
+ colorObserver.observe(document.documentElement, {
3687
+ attributes: true,
3688
+ attributeFilter: ["data-theme"]
3689
+ });
3690
+ cleanup = () => {
3691
+ clearTimeout(timer);
3692
+ control.current?.close();
3693
+ control.current = null;
3694
+ writableRef.current = false;
3695
+ gestures();
3696
+ window.removeEventListener("focus", claimSize);
3697
+ document.removeEventListener("visibilitychange", claimSize);
3698
+ terminal.textarea?.removeEventListener("focus", redirectFocus);
3699
+ clipboard();
3700
+ links.dispose();
3701
+ resize.disconnect();
3702
+ colorObserver.disconnect();
3703
+ terminal.dispose();
3704
+ term.current = null;
3705
+ };
3706
+ }).catch(() => setMessage("The terminal could not load."));
3707
+ return () => {
3708
+ disposed = true;
3709
+ cleanup();
3710
+ };
3711
+ }, [
3712
+ pane.id,
3713
+ pane.terminalId,
3714
+ machineId,
3715
+ threadId,
3716
+ terminals,
3717
+ canControl
3718
+ ]);
3719
+ const sendInput = (text, paste) => {
3720
+ if (!writableRef.current) return;
3721
+ if (!ctrl || paste) {
3722
+ control.current?.input(text, paste);
3723
+ return;
3724
+ }
3725
+ setCtrl(false);
3726
+ control.current?.input(controlCharacter(text) ?? text);
3727
+ };
3728
+ const showCursorAsFocused = () => term.current?.focus();
3729
+ const focusInput = () => {
3730
+ if (!writable || term.current?.hasSelection()) return;
3731
+ showCursorAsFocused();
3732
+ host.current?.parentElement?.querySelector(".terminal-input-capture")?.focus();
3733
+ };
3734
+ useEffect(() => {
3735
+ if (active) focusInput();
3736
+ }, [active, writable]);
3737
+ useEffect(() => {
3738
+ if (term.current) term.current.options.cursorInactiveStyle = active ? "block" : "outline";
3739
+ }, [active, writable]);
3740
+ useEffect(() => {
3741
+ if (!active || !writable) return;
3742
+ const unfocused = (event) => event.target === document.body || event.target === document.documentElement || section.current?.contains(event.target);
3743
+ const onKeyDown = (event) => {
3744
+ if (event.defaultPrevented || event.metaKey || event.isComposing || !unfocused(event) || event.target.classList?.contains("terminal-input-capture")) return;
3745
+ const bytes = terminalKeyBytes(event);
3746
+ if (bytes) {
3747
+ event.preventDefault();
3748
+ event.stopPropagation();
3749
+ control.current?.input(bytes);
3750
+ } else if (event.key.length === 1 && !event.ctrlKey) focusInput();
3751
+ };
3752
+ const onPaste = (event) => {
3753
+ const fromInput = event.target.classList?.contains("terminal-input-capture");
3754
+ if (!unfocused(event) && !fromInput) return;
3755
+ const pastedImages = imageFilesFrom(event.clipboardData);
3756
+ if (pastedImages.length) {
3757
+ event.preventDefault();
3758
+ event.stopPropagation();
3759
+ images.attach(pastedImages);
3760
+ return;
3761
+ }
3762
+ const text = event.clipboardData?.getData("text/plain");
3763
+ if (!text || fromInput) return;
3764
+ event.preventDefault();
3765
+ event.stopPropagation();
3766
+ control.current?.input(text, true);
3767
+ };
3768
+ window.addEventListener("keydown", onKeyDown, true);
3769
+ window.addEventListener("paste", onPaste, true);
3770
+ return () => {
3771
+ window.removeEventListener("keydown", onKeyDown, true);
3772
+ window.removeEventListener("paste", onPaste, true);
3773
+ };
3774
+ }, [
3775
+ active,
3776
+ writable,
3777
+ images.attach
3778
+ ]);
3779
+ useEffect(() => {
3780
+ const refocus = () => {
3781
+ if (active) focusInput();
3782
+ };
3783
+ window.addEventListener("focus", refocus);
3784
+ return () => window.removeEventListener("focus", refocus);
3785
+ }, [active, writable]);
3786
+ return /* @__PURE__ */ jsxs("section", {
3787
+ ref: section,
3788
+ className: `terminal-pane ${active ? "is-active" : ""}`,
3789
+ "aria-label": `${pane.title} terminal`,
3790
+ onPointerDownCapture: () => {
3791
+ if (!active) onFocus();
3792
+ },
3793
+ onFocusCapture: () => {
3794
+ if (!active) onFocus();
3795
+ },
3796
+ onClick: () => {
3797
+ if (canControl && !writableRef.current) control.current?.control();
3798
+ focusInput();
3799
+ },
3800
+ children: [
3801
+ /* @__PURE__ */ jsx("span", {
3802
+ className: "terminal-status",
3803
+ role: "status",
3804
+ children: message
3805
+ }),
3806
+ message === "Another controller owns this terminal." && /* @__PURE__ */ jsxs("div", {
3807
+ className: "terminal-control-conflict",
3808
+ children: ["Another Herdr client controls this terminal.", /* @__PURE__ */ jsx("button", {
3809
+ type: "button",
3810
+ onClick: (event) => {
3811
+ event.stopPropagation();
3812
+ confirmDialog("Replace the other Herdr client that controls this terminal? Its input stops working.", { confirmLabel: "Take over" }).then((accepted) => {
3813
+ if (accepted) control.current?.control(true);
3814
+ });
3815
+ },
3816
+ children: "Take over"
3817
+ })]
3818
+ }),
3819
+ /* @__PURE__ */ jsx("div", {
3820
+ ref: host,
3821
+ className: "terminal-host"
3822
+ }),
3823
+ images.dragging && /* @__PURE__ */ jsxs("div", {
3824
+ className: "terminal-drop-overlay",
3825
+ "aria-hidden": "true",
3826
+ children: [/* @__PURE__ */ jsx(ImagePlus, {
3827
+ size: 22,
3828
+ strokeWidth: 1.6
3829
+ }), /* @__PURE__ */ jsx("span", { children: "Drop image to attach" })]
3830
+ }),
3831
+ notice && /* @__PURE__ */ jsx("div", {
3832
+ className: `terminal-attach-notice is-${notice.tone}`,
3833
+ role: notice.tone === "error" ? "alert" : "status",
3834
+ children: notice.text
3835
+ }),
3836
+ (canControl || writable) && /* @__PURE__ */ jsx(TerminalInput, {
3837
+ onInput: sendInput,
3838
+ onFocusChange: (focused) => {
3839
+ setInputFocused(focused);
3840
+ if (term.current?.modes.sendFocusMode) control.current?.input(focused ? FOCUS_IN : FOCUS_OUT);
3841
+ }
3842
+ }),
3843
+ canControl && active && touch && inputFocused && /* @__PURE__ */ jsx(TerminalKeyBar, {
3844
+ ctrl,
3845
+ onToggleCtrl: () => setCtrl((armed) => !armed),
3846
+ onBytes: (bytes) => control.current?.input(bytes)
3847
+ })
3848
+ ]
3849
+ });
3850
+ }
3851
+ //#endregion
3852
+ //#region src/components/AdoptTab.tsx
3853
+ function AdoptTab({ thread, tabs }) {
3854
+ const [tabId, setTabId] = useState("");
3855
+ const [error, setError] = useState("");
3856
+ const [busy, setBusy] = useState(false);
3857
+ const selected = tabs.find((tab) => tab.tabId === tabId && !tab.bound);
3858
+ return /* @__PURE__ */ jsxs("form", {
3859
+ className: "adoption-picker",
3860
+ onSubmit: async (event) => {
3861
+ event.preventDefault();
3862
+ if (!selected || busy) return;
3863
+ setBusy(true);
3864
+ if (!await confirmDialog(`Bind this thread to ${selected.label} (${selected.tabId}) and its ${selected.panes.length} current terminals without sending the old prompt?`, { confirmLabel: "Bind" })) {
3865
+ setBusy(false);
3866
+ return;
3867
+ }
3868
+ setError("");
3869
+ try {
3870
+ if (!(await fetch(`/api/threads/${encodeURIComponent(thread.id)}/adopt`, {
3871
+ method: "POST",
3872
+ headers: { "Content-Type": "application/json" },
3873
+ body: JSON.stringify({
3874
+ machineId: thread.machineId,
3875
+ terminalIds: selected.panes.map((pane) => pane.terminalId)
3876
+ })
3877
+ })).ok) throw new Error("The tab changed or is already bound; select it again after refresh.");
3878
+ } catch (error) {
3879
+ setError(error instanceof Error ? error.message : "Adoption failed.");
3880
+ } finally {
3881
+ setBusy(false);
3882
+ }
3883
+ },
3884
+ children: [
3885
+ /* @__PURE__ */ jsxs("label", { children: ["Select the current native tab to adopt ", /* @__PURE__ */ jsxs("select", {
3886
+ "aria-label": "Native tab to adopt",
3887
+ value: tabId,
3888
+ onChange: (event) => setTabId(event.target.value),
3889
+ children: [/* @__PURE__ */ jsx("option", {
3890
+ value: "",
3891
+ children: "Select a tab…"
3892
+ }), tabs.map((tab) => /* @__PURE__ */ jsxs("option", {
3893
+ value: tab.tabId,
3894
+ disabled: tab.bound || !tab.panes.length,
3895
+ children: [
3896
+ tab.label,
3897
+ " · ",
3898
+ tab.tabId,
3899
+ tab.bound ? " · Already bound" : ""
3900
+ ]
3901
+ }, tab.tabId))]
3902
+ })] }),
3903
+ selected && /* @__PURE__ */ jsx("p", { children: selected.panes.map((pane) => `${pane.title} (${pane.terminalId})`).join(", ") }),
3904
+ /* @__PURE__ */ jsx("button", {
3905
+ disabled: !selected || busy,
3906
+ children: "Adopt without restarting"
3907
+ }),
3908
+ error && /* @__PURE__ */ jsx("p", {
3909
+ role: "alert",
3910
+ children: error
3911
+ })
3912
+ ]
3913
+ });
3914
+ }
3915
+ //#endregion
3916
+ //#region src/components/LaunchProgress.tsx
3917
+ function LaunchProgress({ thread }) {
3918
+ const failed = launchFailed(thread);
3919
+ const current = launchStepIndex(thread.operation?.step ?? "validate");
3920
+ return /* @__PURE__ */ jsxs("main", {
3921
+ className: "launch-progress",
3922
+ "aria-label": "Launch progress",
3923
+ role: "status",
3924
+ children: [
3925
+ /* @__PURE__ */ jsx("h1", { children: failed ? "The launch stopped" : "Starting your agent" }),
3926
+ /* @__PURE__ */ jsx("blockquote", {
3927
+ className: "launch-prompt",
3928
+ children: thread.prompt || thread.title
3929
+ }),
3930
+ /* @__PURE__ */ jsx("ol", {
3931
+ className: "launch-steps",
3932
+ children: launchSteps.map((step, index) => {
3933
+ const state = index < current ? "done" : index === current ? failed ? "failed" : "active" : "waiting";
3934
+ const Icon = state === "done" ? CircleCheck : state === "failed" ? CircleAlert : state === "active" ? LoaderCircle : Circle;
3935
+ return /* @__PURE__ */ jsxs("li", {
3936
+ className: `launch-step is-${state}`,
3937
+ "aria-current": state === "active" ? "step" : void 0,
3938
+ children: [/* @__PURE__ */ jsx(Icon, {
3939
+ size: 16,
3940
+ strokeWidth: 1.8,
3941
+ "aria-hidden": "true"
3942
+ }), step.label]
3943
+ }, step.id);
3944
+ })
3945
+ }),
3946
+ failed && /* @__PURE__ */ jsxs("div", {
3947
+ className: "launch-failure",
3948
+ role: "alert",
3949
+ children: [/* @__PURE__ */ jsxs("p", { children: [
3950
+ "Herdr stopped while ",
3951
+ launchSteps[current].label.toLowerCase(),
3952
+ ". The worktree may already exist, so check it in Herdr before you try again."
3953
+ ] }), /* @__PURE__ */ jsx("a", {
3954
+ className: "launch-retry",
3955
+ href: `/new?project=${encodeURIComponent(thread.projectId)}`,
3956
+ children: "Start a new thread"
3957
+ })]
3958
+ })
3959
+ ]
3960
+ });
3961
+ }
3962
+ //#endregion
3963
+ //#region src/components/ThreadView.tsx
3964
+ var PEEK_SIZE = "28px";
3965
+ var besidePeek = (side) => side === "right" ? {
3966
+ left: 0,
3967
+ top: 0,
3968
+ width: `calc(100% - ${PEEK_SIZE})`,
3969
+ height: "100%"
3970
+ } : {
3971
+ left: 0,
3972
+ top: 0,
3973
+ width: "100%",
3974
+ height: `calc(100% - ${PEEK_SIZE})`
3975
+ };
3976
+ function TerminalPeek({ side, onOpen }) {
3977
+ return /* @__PURE__ */ jsx(Tooltip$1, {
3978
+ label: "Show terminal",
3979
+ side: side === "right" ? "left" : "top",
3980
+ children: /* @__PURE__ */ jsxs("button", {
3981
+ type: "button",
3982
+ className: `terminal-peek is-${side}`,
3983
+ "aria-label": "Show terminal",
3984
+ onClick: onOpen,
3985
+ children: [/* @__PURE__ */ jsx(TerminalSquare, {
3986
+ size: 14,
3987
+ strokeWidth: 1.8,
3988
+ "aria-hidden": "true"
3989
+ }), /* @__PURE__ */ jsx("span", { children: "Terminal" })]
3990
+ })
3991
+ });
3992
+ }
3993
+ function ThreadView({ thread, layouts, tabs, connected, canControl, onFocusPane, focusedPane, onToggleTerminal }) {
3994
+ const pane = (item, style) => /* @__PURE__ */ jsx("div", {
3995
+ className: "native-pane-position",
3996
+ style,
3997
+ children: /* @__PURE__ */ jsx(TerminalPane, {
3998
+ pane: item,
3999
+ machineId: thread.machineId,
4000
+ threadId: thread.id,
4001
+ active: focusedPane === item.id,
4002
+ canControl,
4003
+ onFocus: () => onFocusPane(item.id)
4004
+ })
4005
+ }, `${item.id}:${item.terminalId}`);
4006
+ if (connected && (isLaunching(thread) || launchFailed(thread))) return /* @__PURE__ */ jsx(LaunchProgress, { thread });
4007
+ const visibleLayouts = layouts.filter((layout) => layout.panes.some((rect) => thread.panes.some((pane) => pane.id === rect.paneId)));
4008
+ return /* @__PURE__ */ jsxs("main", {
4009
+ className: "native-thread-layout",
4010
+ "aria-label": `${thread.title} tab`,
4011
+ children: [
4012
+ (!connected || thread.bindingState !== "attached") && /* @__PURE__ */ jsx("p", {
4013
+ className: "runtime-notice",
4014
+ role: "status",
4015
+ children: !connected ? "Disconnected; input is disabled." : thread.bindingState === "detached" ? "The saved terminal is not present; explicit adoption is required." : "Waiting for the terminal to attach…"
4016
+ }),
4017
+ connected && thread.bindingState === "attached" && !canControl && /* @__PURE__ */ jsx("p", {
4018
+ className: "runtime-notice",
4019
+ role: "status",
4020
+ children: "Terminal input is unavailable because this Herdr version is not supported."
4021
+ }),
4022
+ connected && thread.bindingState === "detached" && /* @__PURE__ */ jsx(AdoptTab, {
4023
+ thread,
4024
+ tabs
4025
+ }, thread.id),
4026
+ connected && thread.bindingState === "attached" && visibleLayouts.map((layout) => {
4027
+ const hidden = hiddenTerminalLayout(thread, layout);
4028
+ const split = !hidden && layout.panes.filter((rect) => thread.panes.some((item) => item.id === rect.paneId)).length > 1;
4029
+ return /* @__PURE__ */ jsx("div", {
4030
+ className: `native-pane-layout ${split ? "is-split" : ""}`,
4031
+ children: hidden ? [pane(hidden.agent, besidePeek(hidden.side)), /* @__PURE__ */ jsx(TerminalPeek, {
4032
+ side: hidden.side,
4033
+ onOpen: onToggleTerminal
4034
+ }, "terminal-peek")] : thread.panes.map((item) => {
4035
+ const rect = layout.panes.find((rect) => rect.paneId === item.id)?.rect;
4036
+ if (!rect || !layout.area.width || !layout.area.height) return null;
4037
+ return pane(item, {
4038
+ left: `${(rect.x - layout.area.x) / layout.area.width * 100}%`,
4039
+ top: `${(rect.y - layout.area.y) / layout.area.height * 100}%`,
4040
+ width: `${rect.width / layout.area.width * 100}%`,
4041
+ height: `${rect.height / layout.area.height * 100}%`
4042
+ });
4043
+ })
4044
+ }, `${layout.workspaceId}:${layout.tabId}`);
4045
+ })
4046
+ ]
4047
+ });
4048
+ }
4049
+ //#endregion
4050
+ //#region src/client/keyboard.ts
4051
+ function appShortcutAction(event, shortcuts) {
4052
+ if (event.repeat || event.defaultPrevented || event.isComposing) return;
4053
+ const target = event.target;
4054
+ if (target?.closest("[data-recording=\"true\"]")) return;
4055
+ if (target?.closest("input, textarea, select, [contenteditable=\"true\"]") && !target.closest(".xterm, .terminal-input-capture")) return;
4056
+ return Object.entries(shortcuts).find(([, combo]) => combo === keyCombo(event))?.[0];
4057
+ }
4058
+ //#endregion
4059
+ //#region src/client/clear-done-on-visit.ts
4060
+ function useClearDoneOnVisit(thread, paneId, enabled) {
4061
+ const isDone = thread?.status === "done";
4062
+ useEffect(() => {
4063
+ if (!thread || !paneId || !isDone || !enabled) return;
4064
+ fetch(`/api/threads/${encodeURIComponent(thread.id)}/focus`, {
4065
+ method: "POST",
4066
+ headers: { "Content-Type": "application/json" },
4067
+ body: JSON.stringify({
4068
+ machineId: thread.machineId,
4069
+ paneId
4070
+ })
4071
+ }).catch(() => void 0);
4072
+ }, [
4073
+ thread?.id,
4074
+ thread?.machineId,
4075
+ paneId,
4076
+ isDone,
4077
+ enabled
4078
+ ]);
4079
+ }
4080
+ //#endregion
4081
+ //#region src/components/App.tsx
4082
+ function App(props) {
4083
+ return /* @__PURE__ */ jsx(CrashReport, { children: /* @__PURE__ */ jsx(RuntimeProvider, {
4084
+ bootstrap: props.bootstrap,
4085
+ children: /* @__PURE__ */ jsx(RuntimeApp, { ...props })
4086
+ }) });
4087
+ }
4088
+ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4089
+ const { ui, terminals } = useRuntime();
4090
+ const projects = useRuntimeSelector(useShallow((state) => state.projectIds.map((id) => state.projects[id])));
4091
+ const threads = useRuntimeSelector(useShallow((state) => state.threadIds.map((id) => state.threads[id]).filter((item) => !item.archivedAt && isListedThread(item))));
4092
+ const archived = useRuntimeSelector(useShallow((state) => state.threadIds.map((id) => state.threads[id]).filter((item) => !!item.archivedAt)));
4093
+ const thread = useRuntimeSelector((state) => state.threads[threadId ?? ""]);
4094
+ const projections = useRuntimeSelector((state) => state.projections);
4095
+ const gatewayConnected = useRuntimeSelector((state) => state.connected);
4096
+ const machines = Object.values(projections).flatMap((projection) => {
4097
+ const machine = projection.machine ?? bootstrap.machines.find((machine) => machine.id === projection.machineId);
4098
+ return machine ? [{
4099
+ ...machine,
4100
+ connected: gatewayConnected && projection.connected,
4101
+ error: projection.error
4102
+ }] : [];
4103
+ });
4104
+ const [collapsed, setCollapsed] = useState(false);
4105
+ const [mobileOpen, setMobileOpen] = useState(false);
4106
+ const [paletteOpen, setPaletteOpen] = useState(false);
4107
+ useEffect(startPerf, []);
4108
+ useEffect(trackVisualViewport, []);
4109
+ const [mode, setMode] = useState("threads");
4110
+ const scope = `${thread?.machineId ?? ""}:${thread?.id ?? ""}`;
4111
+ const storedFocus = useStore(ui, (state) => state.focusedPanes[scope]);
4112
+ const focusedPane = thread?.panes.some((pane) => pane.id === storedFocus) ? storedFocus : thread?.panes[0]?.id ?? "";
4113
+ const setFocusedPane = useCallback((paneId) => ui.getState().focus(scope, paneId), [ui, scope]);
4114
+ useClearDoneOnVisit(page === "thread" ? thread : void 0, focusedPane, gatewayConnected && !!machines.find((machine) => machine.id === thread?.machineId)?.writable);
4115
+ const shownThreadId = useRef(void 0);
4116
+ useEffect(() => {
4117
+ if (thread) shownThreadId.current = thread.id;
4118
+ else if (page === "thread" && shownThreadId.current === threadId) navigate("/");
4119
+ }, [
4120
+ thread,
4121
+ page,
4122
+ threadId
4123
+ ]);
4124
+ useEffect(() => {
4125
+ const keepFileDropsInsideTheApp = (event) => {
4126
+ if (carriesFiles(event.dataTransfer)) event.preventDefault();
4127
+ };
4128
+ window.addEventListener("dragover", keepFileDropsInsideTheApp);
4129
+ window.addEventListener("drop", keepFileDropsInsideTheApp);
4130
+ return () => {
4131
+ window.removeEventListener("dragover", keepFileDropsInsideTheApp);
4132
+ window.removeEventListener("drop", keepFileDropsInsideTheApp);
4133
+ };
4134
+ }, []);
4135
+ useEffect(() => {
4136
+ const syncCollapsedFromUrl = () => setCollapsed(new URLSearchParams(window.location.search).get("sidebar") === "collapsed");
4137
+ syncCollapsedFromUrl();
4138
+ window.addEventListener("popstate", syncCollapsedFromUrl);
4139
+ setMode(localStorage.getItem("webr-sidebar-mode") === "projects" ? "projects" : "threads");
4140
+ const media = matchMedia("(prefers-color-scheme: dark)");
4141
+ const onChange = () => {
4142
+ if ((localStorage.getItem("webr-theme") ?? "system") === "system") applyTheme("system");
4143
+ };
4144
+ media.addEventListener("change", onChange);
4145
+ return () => {
4146
+ media.removeEventListener("change", onChange);
4147
+ window.removeEventListener("popstate", syncCollapsedFromUrl);
4148
+ };
4149
+ }, []);
4150
+ const layouts = thread ? projections[thread.machineId]?.layouts ?? [] : [];
4151
+ const reportTerminalFailure = (error) => void alertDialog(error instanceof Error ? error.message : "Herdr could not change the terminal.");
4152
+ const toggleTerminal = useCallback((direction) => {
4153
+ if (!thread) return;
4154
+ const chosen = direction ?? currentTerminalDirection(thread, layouts, window.matchMedia("(max-width: 760px)").matches);
4155
+ toggleThreadTerminal(thread, chosen).then(({ paneId, hidden }) => setFocusedPane(hidden ? threadAgent(thread)?.id ?? paneId : paneId), reportTerminalFailure);
4156
+ }, [
4157
+ thread,
4158
+ layouts,
4159
+ setFocusedPane
4160
+ ]);
4161
+ const focusPane = useCallback((paneId) => {
4162
+ if (thread?.terminalHidden && threadShell(thread)?.id === paneId) toggleTerminal();
4163
+ else setFocusedPane(paneId);
4164
+ }, [
4165
+ thread,
4166
+ toggleTerminal,
4167
+ setFocusedPane
4168
+ ]);
4169
+ const closeTerminal = useCallback(() => {
4170
+ if (!thread) return;
4171
+ closeThreadTerminal(thread).then(() => {
4172
+ const agent = threadAgent(thread);
4173
+ if (agent) setFocusedPane(agent.id);
4174
+ }, reportTerminalFailure);
4175
+ }, [thread, setFocusedPane]);
4176
+ const toggleSidebar = useCallback(() => {
4177
+ if (window.matchMedia("(max-width: 760px)").matches) {
4178
+ setMobileOpen((value) => !value);
4179
+ return;
4180
+ }
4181
+ setCollapsed((value) => {
4182
+ const next = !value;
4183
+ const url = new URL(window.location.href);
4184
+ if (next) url.searchParams.set("sidebar", "collapsed");
4185
+ else url.searchParams.delete("sidebar");
4186
+ window.history.replaceState(window.history.state, "", url);
4187
+ return next;
4188
+ });
4189
+ }, []);
4190
+ const changeMode = useCallback((value) => {
4191
+ setMode(value);
4192
+ localStorage.setItem("webr-sidebar-mode", value);
4193
+ }, []);
4194
+ useEffect(() => {
4195
+ const onKey = (event) => {
4196
+ if (event.repeat || event.isComposing) return;
4197
+ if (event.target?.closest("[data-recording=\"true\"]")) return;
4198
+ if (event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "k") {
4199
+ event.preventDefault();
4200
+ setPaletteOpen((value) => !value);
4201
+ return;
4202
+ }
4203
+ if (paletteOpen || event.defaultPrevented) return;
4204
+ const action = appShortcutAction(event, getShortcuts());
4205
+ if (!action) return;
4206
+ event.preventDefault();
4207
+ if (action === "toggleSidebar") toggleSidebar();
4208
+ if (action === "toggleTerminal") toggleTerminal();
4209
+ if (action === "newThread") navigate(`/new${thread ? `?project=${encodeURIComponent(thread.projectId)}` : ""}`);
4210
+ if ((action === "nextPane" || action === "previousPane") && thread?.panes.length) {
4211
+ const index = thread.panes.findIndex((pane) => pane.id === focusedPane);
4212
+ focusPane(thread.panes[(index + (action === "nextPane" ? 1 : -1) + thread.panes.length) % thread.panes.length].id);
4213
+ }
4214
+ if ((action === "nextThread" || action === "previousThread") && threads.length) {
4215
+ const index = threads.findIndex((item) => item.id === thread?.id);
4216
+ const next = action === "nextThread" ? (index + 1) % threads.length : (index - 1 + threads.length) % threads.length;
4217
+ navigate(`/threads/${encodeURIComponent(threads[next].id)}`);
4218
+ }
4219
+ };
4220
+ window.addEventListener("keydown", onKey, true);
4221
+ return () => window.removeEventListener("keydown", onKey, true);
4222
+ }, [
4223
+ thread,
4224
+ threads,
4225
+ paletteOpen,
4226
+ toggleSidebar,
4227
+ toggleTerminal,
4228
+ focusPane,
4229
+ focusedPane,
4230
+ setFocusedPane
4231
+ ]);
4232
+ return /* @__PURE__ */ jsxs("div", {
4233
+ className: `app-shell ${page === "thread" ? "is-thread" : ""}`,
4234
+ children: [
4235
+ /* @__PURE__ */ jsx(Sidebar, {
4236
+ projects,
4237
+ threads,
4238
+ archived,
4239
+ machines,
4240
+ currentId: thread?.id,
4241
+ mode,
4242
+ onModeChange: changeMode,
4243
+ collapsed,
4244
+ mobileOpen,
4245
+ onCollapse: toggleSidebar,
4246
+ onCloseMobile: () => setMobileOpen(false),
4247
+ onOpenPalette: () => setPaletteOpen(true)
4248
+ }),
4249
+ /* @__PURE__ */ jsxs("div", {
4250
+ className: "main-panel",
4251
+ children: [
4252
+ page === "thread" && thread && /* @__PURE__ */ jsx(ThreadView, {
4253
+ thread,
4254
+ layouts: projections[thread.machineId]?.layouts ?? [],
4255
+ tabs: projections[thread.machineId]?.availableTabs ?? [],
4256
+ connected: gatewayConnected && !!projections[thread.machineId]?.connected,
4257
+ canControl: !!machines.find((machine) => machine.id === thread.machineId)?.writable,
4258
+ focusedPane,
4259
+ onFocusPane: focusPane,
4260
+ onToggleTerminal: () => toggleTerminal()
4261
+ }),
4262
+ page === "new" && /* @__PURE__ */ jsx(NewThreadView, {
4263
+ projects,
4264
+ machines,
4265
+ selectedProjectId: projectId,
4266
+ onOpenSidebar: toggleSidebar
4267
+ }),
4268
+ page === "settings" && /* @__PURE__ */ jsx(SettingsView, { onOpenSidebar: toggleSidebar }),
4269
+ page === "missing" && /* @__PURE__ */ jsxs("main", {
4270
+ className: "not-found",
4271
+ children: [/* @__PURE__ */ jsx("h1", { children: "Thread not found" }), /* @__PURE__ */ jsx("a", {
4272
+ href: "/",
4273
+ children: "Open a thread"
4274
+ })]
4275
+ })
4276
+ ]
4277
+ }),
4278
+ page === "thread" && /* @__PURE__ */ jsxs("div", {
4279
+ className: "mobile-controls",
4280
+ children: [/* @__PURE__ */ jsx("button", {
4281
+ "aria-label": "Open sidebar",
4282
+ onClick: toggleSidebar,
4283
+ children: /* @__PURE__ */ jsx(PanelLeft, { size: 16 })
4284
+ }), /* @__PURE__ */ jsx("button", {
4285
+ "aria-label": "Open command palette",
4286
+ onClick: () => setPaletteOpen(true),
4287
+ children: /* @__PURE__ */ jsx(Search, { size: 16 })
4288
+ })]
4289
+ }),
4290
+ false,
4291
+ /* @__PURE__ */ jsx(PairingPrompt, {}),
4292
+ /* @__PURE__ */ jsx(CommandPalette, {
4293
+ open: paletteOpen,
4294
+ onClose: () => setPaletteOpen(false),
4295
+ projects,
4296
+ threads,
4297
+ thread,
4298
+ focusedPane,
4299
+ mode,
4300
+ onModeChange: changeMode,
4301
+ onToggleSidebar: toggleSidebar,
4302
+ onFocusPane: focusPane,
4303
+ onTerminalAction: (action) => {
4304
+ const pane = thread?.panes.find((pane) => pane.id === focusedPane);
4305
+ if (thread && pane?.terminalId) terminals.command(thread.machineId, thread.id, pane.terminalId, action);
4306
+ },
4307
+ onToggleTerminal: toggleTerminal,
4308
+ onCloseTerminal: closeTerminal
4309
+ })
4310
+ ]
4311
+ });
4312
+ }
4313
+ //#endregion
4314
+ export { $$Base as n, App as t };