@pablopunk/webr 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
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_CnGYDmxV.mjs";
2
2
  import { t as createComponent } from "./compiler_BiU10Vds.mjs";
3
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, Server, Settings2, SquarePen, SquareTerminal, Sun, TerminalSquare, Trash2, TriangleAlert, X } from "lucide-react";
4
+ import { Archive, ArchiveRestore, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Check, ChevronDown, ChevronRight, Circle, CircleAlert, CircleCheck, CircleHelp, Command, Copy, Cpu, Folder, FolderTree, GitBranch, ImagePlus, LayoutList, LoaderCircle, MessageCircleQuestion, Mic, Monitor, Moon, PanelBottom, PanelLeft, PanelLeftClose, PanelLeftOpen, PanelRight, Pencil, Plus, Search, Server, Settings2, Square, SquarePen, SquareTerminal, Sun, TerminalSquare, Trash2, TriangleAlert, X } from "lucide-react";
5
5
  import { createRoot } from "react-dom/client";
6
6
  import { AlertDialog } from "@base-ui/react/alert-dialog";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -191,7 +191,7 @@ var defaultShortcuts = {
191
191
  previousThread: "ctrl+k",
192
192
  nextPane: "ctrl+l",
193
193
  previousPane: "ctrl+h",
194
- toggleSidebar: "ctrl+b",
194
+ toggleSidebar: "meta+b",
195
195
  newThread: "ctrl+x",
196
196
  toggleTerminal: "ctrl+q"
197
197
  };
@@ -330,6 +330,11 @@ var renameThread = (thread, title) => post$1(`/api/threads/${encodeURIComponent(
330
330
  machineId: thread.machineId,
331
331
  title
332
332
  });
333
+ var renameProject = (project, name) => post$1("/api/projects/rename", {
334
+ machineId: project.machineId ?? "local",
335
+ logicalId: project.logicalId ?? project.id,
336
+ name
337
+ });
333
338
  var toggleThreadTerminal = (thread, direction) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/terminal`, {
334
339
  machineId: thread.machineId,
335
340
  direction
@@ -675,6 +680,10 @@ async function listAuditEvents() {
675
680
  if (!response.ok) throw new Error("Webr could not load recent activity.");
676
681
  return response.json();
677
682
  }
683
+ async function tailscaleStatus() {
684
+ const response = await fetch("/api/remote/tailscale");
685
+ return response.ok ? (await response.json()).status : "unavailable";
686
+ }
678
687
  var revokeDevice = (id) => post(`/api/devices/${encodeURIComponent(id)}/revoke`);
679
688
  var decidePairRequest = (id, decision) => post(`/api/pair/${encodeURIComponent(id)}/${decision}`);
680
689
  async function fetchPending() {
@@ -1398,6 +1407,7 @@ function NewThreadView({ projects, machines, selectedProjectId, onOpenSidebar })
1398
1407
  const [busy, setBusy] = useState(false);
1399
1408
  const [error, setError] = useState("");
1400
1409
  const [creatingWorkspace, setCreatingWorkspace] = useState(false);
1410
+ const projectIdsBeforeCreate = useRef(null);
1401
1411
  const operation = useRef(null);
1402
1412
  const images = usePromptImages({ machineId });
1403
1413
  const baseMachine = machines.find((machine) => machine.id === machineId) ?? {
@@ -1447,6 +1457,14 @@ function NewThreadView({ projects, machines, selectedProjectId, onOpenSidebar })
1447
1457
  projectId,
1448
1458
  catalog.data
1449
1459
  ]);
1460
+ useEffect(() => {
1461
+ const known = projectIdsBeforeCreate.current;
1462
+ const added = known && machineProjects.find((project) => !known.has(project.id));
1463
+ if (added) {
1464
+ projectIdsBeforeCreate.current = null;
1465
+ changeProject(added.id);
1466
+ }
1467
+ }, [machineProjects]);
1450
1468
  useEffect(() => {
1451
1469
  if (!machineId && machines.length) setMachineId(machines.find((machine) => machine.id === "local")?.id ?? machines[0].id);
1452
1470
  if (!projectId && machineProjects.length) setProjectId(machineProjects[0].id);
@@ -1526,7 +1544,10 @@ function NewThreadView({ projects, machines, selectedProjectId, onOpenSidebar })
1526
1544
  /* @__PURE__ */ jsx("p", { children: !machineProjects.length ? "This Herdr session has no projects yet." : "Add a project to this Herdr session." }),
1527
1545
  /* @__PURE__ */ jsx(CreateWorkspace, {
1528
1546
  machineId: machine.id,
1529
- onCreated: () => setCreatingWorkspace(false)
1547
+ onCreated: () => {
1548
+ projectIdsBeforeCreate.current = new Set(machineProjects.map((project) => project.id));
1549
+ setCreatingWorkspace(false);
1550
+ }
1530
1551
  }),
1531
1552
  !!machineProjects.length && /* @__PURE__ */ jsx("button", {
1532
1553
  className: "workspace-cancel",
@@ -1778,11 +1799,16 @@ function InviteDialog({ invite, onClose }) {
1778
1799
  const [urlIndex, setUrlIndex] = useState(0);
1779
1800
  const [copied, setCopied] = useState(false);
1780
1801
  const url = invite.urls[urlIndex];
1781
- const copy = () => writeClipboardText(invite.token).then((ok) => {
1802
+ const copyText = (text) => writeClipboardText(text).then((ok) => {
1782
1803
  if (!ok) return;
1783
1804
  setCopied(true);
1784
1805
  setTimeout(() => setCopied(false), 1500);
1785
1806
  });
1807
+ const copy = () => copyText(invite.token);
1808
+ const selectAndCopyAddress = (index) => {
1809
+ setUrlIndex(index);
1810
+ copyText(invite.urls[index]);
1811
+ };
1786
1812
  return /* @__PURE__ */ jsx(Dialog.Root, {
1787
1813
  open: true,
1788
1814
  onOpenChange: (open) => {
@@ -1812,7 +1838,7 @@ function InviteDialog({ invite, onClose }) {
1812
1838
  type: "button",
1813
1839
  "aria-pressed": index === urlIndex,
1814
1840
  className: index === urlIndex ? "theme-option is-selected" : "theme-option",
1815
- onClick: () => setUrlIndex(index),
1841
+ onClick: () => selectAndCopyAddress(index),
1816
1842
  children: new URL(address).host
1817
1843
  }, address))
1818
1844
  })
@@ -1849,6 +1875,21 @@ function InviteDialog({ invite, onClose }) {
1849
1875
  })] })
1850
1876
  });
1851
1877
  }
1878
+ var TAILSCALE_CALLOUTS = {
1879
+ unavailable: "Install Tailscale on this computer and your other devices to get a private, secure HTTPS address for Webr. HTTPS unlocks features browsers only allow on secure pages, such as the microphone.",
1880
+ "https-disabled": "Tailscale is installed, but HTTPS certificates are off. Turn them on in the Tailscale admin console under DNS, then restart Webr to unlock features that need a secure address, such as the microphone."
1881
+ };
1882
+ function TailscaleCallout() {
1883
+ const [status, setStatus] = useState("ready");
1884
+ useEffect(() => {
1885
+ tailscaleStatus().then(setStatus, () => void 0);
1886
+ }, []);
1887
+ return status === "ready" ? null : /* @__PURE__ */ jsx("p", {
1888
+ className: "tailscale-callout",
1889
+ role: "note",
1890
+ children: TAILSCALE_CALLOUTS[status]
1891
+ });
1892
+ }
1852
1893
  function RemoteAccess() {
1853
1894
  const [devices, setDevices] = useState([]);
1854
1895
  const [invite, setInvite] = useState();
@@ -1881,6 +1922,7 @@ function RemoteAccess() {
1881
1922
  className: "settings-note",
1882
1923
  children: "Other devices ask to connect and you approve them here. This computer never needs a code."
1883
1924
  }),
1925
+ /* @__PURE__ */ jsx(TailscaleCallout, {}),
1884
1926
  /* @__PURE__ */ jsxs("div", {
1885
1927
  className: "settings-row",
1886
1928
  children: [/* @__PURE__ */ jsx("span", { children: "Connect a device" }), /* @__PURE__ */ jsx("button", {
@@ -2259,6 +2301,39 @@ function ThreadTitleEditor({ thread, onDone, onError }) {
2259
2301
  });
2260
2302
  }
2261
2303
  //#endregion
2304
+ //#region src/components/ProjectNameEditor.tsx
2305
+ function ProjectNameEditor({ project, onDone, onError }) {
2306
+ const [name, setName] = useState(project.name);
2307
+ const input = useRef(null);
2308
+ const finished = useRef(false);
2309
+ useEffect(() => {
2310
+ input.current?.focus();
2311
+ input.current?.select();
2312
+ }, []);
2313
+ const finish = (save) => {
2314
+ if (finished.current) return;
2315
+ finished.current = true;
2316
+ onDone();
2317
+ const next = name.trim();
2318
+ if (save && next && next !== project.name) renameProject(project, next).catch(onError);
2319
+ };
2320
+ return /* @__PURE__ */ jsx("input", {
2321
+ ref: input,
2322
+ className: "thread-name-input group-name-input",
2323
+ "aria-label": `Rename ${project.name}`,
2324
+ value: name,
2325
+ maxLength: 90,
2326
+ onChange: (event) => setName(event.target.value),
2327
+ onBlur: () => finish(true),
2328
+ onClick: (event) => event.stopPropagation(),
2329
+ onKeyDown: (event) => {
2330
+ event.stopPropagation();
2331
+ if (event.key === "Enter") finish(true);
2332
+ if (event.key === "Escape") finish(false);
2333
+ }
2334
+ });
2335
+ }
2336
+ //#endregion
2262
2337
  //#region src/components/useFlipList.ts
2263
2338
  var durationMs = 220;
2264
2339
  var itemSelector = "[data-flip-id]";
@@ -2304,6 +2379,10 @@ function groupProjectLocations(projects, currentId) {
2304
2379
  return [...groups.values()];
2305
2380
  }
2306
2381
  //#endregion
2382
+ //#region src/client/home-path.ts
2383
+ var homePrefix = /^\/(?:Users|home)\/[^/]+/;
2384
+ var withTildeHome = (path) => path.replace(homePrefix, "~");
2385
+ //#endregion
2307
2386
  //#region src/components/Sidebar.tsx
2308
2387
  var statusLabel = {
2309
2388
  working: "Working",
@@ -2433,6 +2512,63 @@ function ThreadLink({ thread, project, machineName, current, showProject, editor
2433
2512
  })
2434
2513
  });
2435
2514
  }
2515
+ function ProjectGroupHeader({ project, closed, onToggle }) {
2516
+ const [renaming, setRenaming] = useState(false);
2517
+ const Title = renaming ? "div" : "button";
2518
+ const label = renaming ? /* @__PURE__ */ jsx(ProjectNameEditor, {
2519
+ project,
2520
+ onDone: () => setRenaming(false),
2521
+ onError: reportFailure
2522
+ }) : /* @__PURE__ */ jsx("span", {
2523
+ className: "group-name",
2524
+ children: project.name
2525
+ });
2526
+ return /* @__PURE__ */ jsxs(ContextMenu.Root, { children: [/* @__PURE__ */ jsx(ContextMenu.Trigger, {
2527
+ className: "group-header",
2528
+ children: /* @__PURE__ */ jsxs(Title, {
2529
+ className: "group-title",
2530
+ onClick: renaming ? void 0 : onToggle,
2531
+ "aria-expanded": renaming ? void 0 : !closed,
2532
+ children: [
2533
+ /* @__PURE__ */ jsx("span", {
2534
+ className: "group-chevron",
2535
+ "aria-hidden": "true",
2536
+ children: closed ? /* @__PURE__ */ jsx(ChevronRight, { size: 14 }) : /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
2537
+ }),
2538
+ /* @__PURE__ */ jsx(ProjectIcon, { project }),
2539
+ label
2540
+ ]
2541
+ })
2542
+ }), /* @__PURE__ */ jsx(ContextMenu.Portal, { children: /* @__PURE__ */ jsx(ContextMenu.Positioner, {
2543
+ className: "thread-menu-positioner",
2544
+ children: /* @__PURE__ */ jsxs(ContextMenu.Popup, {
2545
+ className: "thread-menu",
2546
+ children: [
2547
+ /* @__PURE__ */ jsxs(ContextMenu.Item, {
2548
+ className: "thread-menu-item",
2549
+ onClick: () => void navigate(`/new?project=${encodeURIComponent(project.id)}`),
2550
+ children: [/* @__PURE__ */ jsx(Plus, {
2551
+ size: 14,
2552
+ "aria-hidden": "true"
2553
+ }), "New thread"]
2554
+ }),
2555
+ /* @__PURE__ */ jsxs(ContextMenu.Item, {
2556
+ className: "thread-menu-item",
2557
+ onClick: () => setRenaming(true),
2558
+ children: [/* @__PURE__ */ jsx(Pencil, {
2559
+ size: 14,
2560
+ "aria-hidden": "true"
2561
+ }), "Rename project"]
2562
+ }),
2563
+ /* @__PURE__ */ jsx("div", {
2564
+ className: "thread-menu-path",
2565
+ title: project.path,
2566
+ children: withTildeHome(project.path)
2567
+ })
2568
+ ]
2569
+ })
2570
+ }) })] });
2571
+ }
2436
2572
  function ArchivedThreads({ threads, renderThread }) {
2437
2573
  const [open, setOpen] = useState(false);
2438
2574
  const [deleting, setDeleting] = useState(false);
@@ -2597,36 +2733,10 @@ function Sidebar({ projects, threads, archived = [], machines, currentId, mode,
2597
2733
  "aria-label": "Agent threads",
2598
2734
  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", {
2599
2735
  className: "thread-group",
2600
- children: [/* @__PURE__ */ jsxs("div", {
2601
- className: "group-header",
2602
- children: [/* @__PURE__ */ jsxs("button", {
2603
- className: "group-title",
2604
- onClick: () => setClosedProjects((value) => value.includes(id) ? value.filter((item) => item !== id) : [...value, id]),
2605
- "aria-expanded": !closedProjects.includes(id),
2606
- children: [
2607
- /* @__PURE__ */ jsx("span", {
2608
- className: "group-chevron",
2609
- "aria-hidden": "true",
2610
- children: closedProjects.includes(id) ? /* @__PURE__ */ jsx(ChevronRight, { size: 14 }) : /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
2611
- }),
2612
- /* @__PURE__ */ jsx(ProjectIcon, { project }),
2613
- /* @__PURE__ */ jsx("span", {
2614
- className: "group-name",
2615
- children: project.name
2616
- })
2617
- ]
2618
- }), /* @__PURE__ */ jsx(Tooltip$1, {
2619
- label: `New thread in ${project.name}`,
2620
- children: /* @__PURE__ */ jsx("a", {
2621
- className: "group-new-thread",
2622
- href: `/new?project=${encodeURIComponent(project.id)}`,
2623
- "aria-label": `New thread in ${project.name}`,
2624
- children: /* @__PURE__ */ jsx(SquarePen, {
2625
- size: 15,
2626
- "aria-hidden": "true"
2627
- })
2628
- })
2629
- })]
2736
+ children: [/* @__PURE__ */ jsx(ProjectGroupHeader, {
2737
+ project,
2738
+ closed: closedProjects.includes(id),
2739
+ onToggle: () => setClosedProjects((value) => value.includes(id) ? value.filter((item) => item !== id) : [...value, id])
2630
2740
  }), !closedProjects.includes(id) && threads.filter((thread) => locations.includes(thread.projectId)).map((thread) => threadLink(thread, false))]
2631
2741
  }, id)) : threads.map((thread) => threadLink(thread, true)), !threads.length && !archived.length && /* @__PURE__ */ jsx("p", {
2632
2742
  className: "sidebar-empty",
@@ -3306,55 +3416,122 @@ var keys = [
3306
3416
  }
3307
3417
  ];
3308
3418
  var keepKeyboardOpen = (event) => event.preventDefault();
3309
- function TerminalKeyBar({ ctrl, onToggleCtrl, onBytes }) {
3419
+ var pressOnTouchDown = (action) => ({
3420
+ onPointerDown: (event) => {
3421
+ keepKeyboardOpen(event);
3422
+ action();
3423
+ },
3424
+ onMouseDown: keepKeyboardOpen,
3425
+ onTouchStart: keepKeyboardOpen,
3426
+ onClick: (event) => {
3427
+ if (event.detail === 0) action();
3428
+ }
3429
+ });
3430
+ var voiceLabels = {
3431
+ idle: "Dictate",
3432
+ preparing: "Preparing voice input",
3433
+ recording: "Stop dictation",
3434
+ transcribing: "Transcribing"
3435
+ };
3436
+ var voiceIcon = (phase) => phase === "recording" ? /* @__PURE__ */ jsx(Square, {
3437
+ size: 14,
3438
+ fill: "currentColor"
3439
+ }) : phase === "idle" ? /* @__PURE__ */ jsx(Mic, { size: 16 }) : /* @__PURE__ */ jsx(LoaderCircle, {
3440
+ size: 16,
3441
+ className: "is-spinning"
3442
+ });
3443
+ function VoiceButton({ voice }) {
3444
+ return /* @__PURE__ */ jsx("button", {
3445
+ type: "button",
3446
+ tabIndex: -1,
3447
+ "aria-label": voiceLabels[voice.phase],
3448
+ "aria-pressed": voice.phase === "recording",
3449
+ className: `terminal-voice is-${voice.phase}`,
3450
+ ...pressOnTouchDown(voice.toggle),
3451
+ children: voiceIcon(voice.phase)
3452
+ });
3453
+ }
3454
+ function TerminalKeyBar({ ctrl, onToggleCtrl, onBytes, voice }) {
3310
3455
  return /* @__PURE__ */ jsxs("div", {
3311
3456
  className: "terminal-key-bar",
3312
3457
  role: "toolbar",
3313
3458
  "aria-label": "Terminal keys",
3314
- children: [/* @__PURE__ */ jsx("button", {
3315
- type: "button",
3316
- tabIndex: -1,
3317
- "aria-pressed": ctrl,
3318
- className: ctrl ? "is-armed" : "",
3319
- onPointerDown: keepKeyboardOpen,
3320
- onClick: onToggleCtrl,
3321
- children: "Ctrl"
3322
- }), keys.map(({ label, key, name }) => /* @__PURE__ */ jsx("button", {
3323
- type: "button",
3324
- tabIndex: -1,
3325
- "aria-label": name ?? key,
3326
- onPointerDown: keepKeyboardOpen,
3327
- onClick: () => {
3328
- const bytes = terminalKeyBytes({
3329
- key,
3330
- ctrlKey: false,
3331
- altKey: false
3332
- });
3333
- if (bytes) onBytes(bytes);
3334
- },
3335
- children: label
3336
- }, key))]
3459
+ children: [
3460
+ /* @__PURE__ */ jsx("button", {
3461
+ type: "button",
3462
+ tabIndex: -1,
3463
+ "aria-pressed": ctrl,
3464
+ className: ctrl ? "is-armed" : "",
3465
+ ...pressOnTouchDown(onToggleCtrl),
3466
+ children: "Ctrl"
3467
+ }),
3468
+ keys.map(({ label, key, name }) => /* @__PURE__ */ jsx("button", {
3469
+ type: "button",
3470
+ tabIndex: -1,
3471
+ "aria-label": name ?? key,
3472
+ ...pressOnTouchDown(() => {
3473
+ const bytes = terminalKeyBytes({
3474
+ key,
3475
+ ctrlKey: false,
3476
+ altKey: false
3477
+ });
3478
+ if (bytes) onBytes(bytes);
3479
+ }),
3480
+ children: label
3481
+ }, key)),
3482
+ voice?.available && /* @__PURE__ */ jsx(VoiceButton, { voice })
3483
+ ]
3337
3484
  });
3338
3485
  }
3339
3486
  //#endregion
3340
3487
  //#region src/client/visual-viewport.ts
3341
3488
  var COARSE_POINTER = "(pointer: coarse)";
3489
+ var KEYBOARD_MIN_HEIGHT = 120;
3490
+ var KEYBOARD_ANIMATION_MS = 350;
3342
3491
  function trackVisualViewport() {
3343
3492
  const viewport = window.visualViewport;
3344
3493
  if (!viewport || !matchMedia(COARSE_POINTER).matches) return () => {};
3345
3494
  const root = document.documentElement;
3346
- const sync = () => {
3495
+ let tallestViewport = viewport.height;
3496
+ const fitToKeyboard = () => {
3347
3497
  root.style.setProperty("--app-height", `${viewport.height}px`);
3348
3498
  root.style.setProperty("--app-top", `${viewport.offsetTop}px`);
3349
3499
  };
3500
+ const fillScreen = () => {
3501
+ root.style.removeProperty("--app-height");
3502
+ root.style.removeProperty("--app-top");
3503
+ };
3504
+ const sync = () => {
3505
+ tallestViewport = Math.max(tallestViewport, viewport.height, window.innerHeight);
3506
+ const keyboardOpen = tallestViewport - viewport.height > KEYBOARD_MIN_HEIGHT;
3507
+ if (keyboardOpen) fitToKeyboard();
3508
+ else fillScreen();
3509
+ root.toggleAttribute("data-keyboard-open", keyboardOpen);
3510
+ };
3350
3511
  sync();
3512
+ let settle;
3513
+ const syncAfterKeyboardAnimation = () => {
3514
+ clearTimeout(settle);
3515
+ settle = setTimeout(sync, KEYBOARD_ANIMATION_MS);
3516
+ };
3517
+ const forgetTallestViewport = () => {
3518
+ tallestViewport = 0;
3519
+ sync();
3520
+ };
3351
3521
  viewport.addEventListener("resize", sync);
3352
3522
  viewport.addEventListener("scroll", sync);
3523
+ window.addEventListener("orientationchange", forgetTallestViewport);
3524
+ window.addEventListener("focusout", syncAfterKeyboardAnimation);
3525
+ window.addEventListener("focusin", syncAfterKeyboardAnimation);
3353
3526
  return () => {
3527
+ clearTimeout(settle);
3528
+ window.removeEventListener("focusout", syncAfterKeyboardAnimation);
3529
+ window.removeEventListener("focusin", syncAfterKeyboardAnimation);
3530
+ window.removeEventListener("orientationchange", forgetTallestViewport);
3354
3531
  viewport.removeEventListener("resize", sync);
3355
3532
  viewport.removeEventListener("scroll", sync);
3356
- root.style.removeProperty("--app-height");
3357
- root.style.removeProperty("--app-top");
3533
+ fillScreen();
3534
+ root.removeAttribute("data-keyboard-open");
3358
3535
  };
3359
3536
  }
3360
3537
  function matchesCoarsePointer() {
@@ -3528,6 +3705,188 @@ function useTerminalNotice() {
3528
3705
  };
3529
3706
  }
3530
3707
  //#endregion
3708
+ //#region src/client/voice-recorder.ts
3709
+ var TARGET_SAMPLE_RATE = 16e3;
3710
+ var CAPTURE_PROCESSOR = `registerProcessor('webr-capture', class extends AudioWorkletProcessor {
3711
+ process([input]) { if (input?.[0]) this.port.postMessage(input[0].slice()); return true; }
3712
+ });`;
3713
+ var canRecordAudio = () => typeof window !== "undefined" && window.isSecureContext && !!navigator.mediaDevices?.getUserMedia && typeof AudioWorkletNode !== "undefined";
3714
+ var createAudioContext = () => {
3715
+ try {
3716
+ return new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
3717
+ } catch {
3718
+ return new AudioContext();
3719
+ }
3720
+ };
3721
+ function toInt16Pcm(chunks) {
3722
+ const pcm = new Int16Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
3723
+ let offset = 0;
3724
+ for (const chunk of chunks) for (const sample of chunk) pcm[offset++] = Math.max(-1, Math.min(1, sample)) * 32767;
3725
+ return pcm.buffer;
3726
+ }
3727
+ async function loadCaptureProcessor(context) {
3728
+ const url = URL.createObjectURL(new Blob([CAPTURE_PROCESSOR], { type: "text/javascript" }));
3729
+ try {
3730
+ await context.audioWorklet.addModule(url);
3731
+ } finally {
3732
+ URL.revokeObjectURL(url);
3733
+ }
3734
+ }
3735
+ async function startRecording() {
3736
+ const context = createAudioContext();
3737
+ context.resume();
3738
+ const release = (stream) => {
3739
+ stream?.getTracks().forEach((track) => track.stop());
3740
+ context.close();
3741
+ };
3742
+ let stream;
3743
+ try {
3744
+ stream = await navigator.mediaDevices.getUserMedia({ audio: {
3745
+ channelCount: 1,
3746
+ echoCancellation: true,
3747
+ noiseSuppression: true,
3748
+ autoGainControl: true
3749
+ } });
3750
+ await loadCaptureProcessor(context);
3751
+ await context.resume();
3752
+ } catch (error) {
3753
+ release(stream);
3754
+ throw error;
3755
+ }
3756
+ const chunks = [];
3757
+ const source = context.createMediaStreamSource(stream);
3758
+ const capture = new AudioWorkletNode(context, "webr-capture");
3759
+ capture.port.onmessage = (event) => chunks.push(event.data);
3760
+ source.connect(capture);
3761
+ const finish = () => {
3762
+ source.disconnect();
3763
+ capture.disconnect();
3764
+ release(stream);
3765
+ };
3766
+ return {
3767
+ stop: async () => {
3768
+ const sampleRate = context.sampleRate;
3769
+ finish();
3770
+ return {
3771
+ pcm: toInt16Pcm(chunks),
3772
+ sampleRate
3773
+ };
3774
+ },
3775
+ cancel: finish
3776
+ };
3777
+ }
3778
+ //#endregion
3779
+ //#region src/client/voice.ts
3780
+ var voiceErrors = {
3781
+ voice_unavailable: "Voice input is not available on this Webr server.",
3782
+ image_too_large: "The recording is too long."
3783
+ };
3784
+ async function voiceState() {
3785
+ const response = await fetch("/api/voice").catch(() => void 0);
3786
+ return response?.ok ? (await response.json()).state : "unavailable";
3787
+ }
3788
+ async function warmVoice() {
3789
+ const response = await fetch("/api/voice/warm", {
3790
+ method: "POST",
3791
+ headers: { "Content-Type": "application/json" },
3792
+ body: "{}"
3793
+ }).catch(() => void 0);
3794
+ return response?.ok ? (await response.json()).state : "unavailable";
3795
+ }
3796
+ async function transcribe(pcm, sampleRate) {
3797
+ const response = await fetch(`/api/voice/transcribe?sampleRate=${sampleRate}`, {
3798
+ method: "POST",
3799
+ headers: { "Content-Type": "audio/l16" },
3800
+ body: pcm
3801
+ });
3802
+ const result = await response.json().catch(() => ({}));
3803
+ if (!response.ok || result.text === void 0) throw new Error(voiceErrors[result.error ?? ""] ?? "The recording could not be transcribed.");
3804
+ return result.text;
3805
+ }
3806
+ //#endregion
3807
+ //#region src/components/useVoiceInput.ts
3808
+ var DOWNLOAD_POLL_MS = 2e3;
3809
+ var DOWNLOADING = "Downloading the voice model (670 MB, one time). The mic works when it finishes.";
3810
+ var isDownloadPending = (state) => state === "needs-download" || state === "downloading";
3811
+ var microphoneError = (error) => error instanceof DOMException && error.name === "NotAllowedError" ? "Allow microphone access for Webr in your browser settings." : "The microphone could not start.";
3812
+ function useVoiceInput({ send, show }) {
3813
+ const [state, setState] = useState("unavailable");
3814
+ const [phase, setPhase] = useState("idle");
3815
+ const recorder = useRef(void 0);
3816
+ const sendRef = useRef(send);
3817
+ sendRef.current = send;
3818
+ useEffect(() => {
3819
+ if (!canRecordAudio()) return;
3820
+ let cancelled = false;
3821
+ voiceState().then((current) => {
3822
+ if (!cancelled) setState(current);
3823
+ });
3824
+ return () => {
3825
+ cancelled = true;
3826
+ recorder.current?.cancel();
3827
+ };
3828
+ }, []);
3829
+ useEffect(() => {
3830
+ if (phase !== "preparing") return;
3831
+ const poll = setInterval(() => void voiceState().then((current) => {
3832
+ if (isDownloadPending(current)) return;
3833
+ setState(current);
3834
+ setPhase("idle");
3835
+ show(current === "ready" ? "Voice input is ready. Tap the mic to talk." : "The voice model could not be downloaded.", current === "ready" ? "info" : "error", 4e3);
3836
+ }), DOWNLOAD_POLL_MS);
3837
+ return () => clearInterval(poll);
3838
+ }, [phase, show]);
3839
+ const prepare = useCallback(() => {
3840
+ warmVoice();
3841
+ setPhase("preparing");
3842
+ show(DOWNLOADING, "info");
3843
+ }, [show]);
3844
+ const record = useCallback(async () => {
3845
+ setPhase("recording");
3846
+ const starting = startRecording();
3847
+ warmVoice();
3848
+ try {
3849
+ recorder.current = await starting;
3850
+ } catch (error) {
3851
+ setPhase("idle");
3852
+ show(microphoneError(error), "error", 5e3);
3853
+ }
3854
+ }, [show]);
3855
+ const finish = useCallback(async () => {
3856
+ const active = recorder.current;
3857
+ recorder.current = void 0;
3858
+ if (!active) return;
3859
+ setPhase("transcribing");
3860
+ try {
3861
+ const { pcm, sampleRate } = await active.stop();
3862
+ const text = await transcribe(pcm, sampleRate);
3863
+ if (text) sendRef.current(text);
3864
+ else show("No speech was heard.", "info", 2500);
3865
+ } catch (error) {
3866
+ show(error instanceof Error ? error.message : "The recording could not be transcribed.", "error", 5e3);
3867
+ } finally {
3868
+ setPhase("idle");
3869
+ }
3870
+ }, [show]);
3871
+ const toggle = useCallback(() => {
3872
+ if (phase === "idle") {
3873
+ if (isDownloadPending(state)) prepare();
3874
+ else record();
3875
+ } else if (phase === "recording") finish();
3876
+ }, [
3877
+ phase,
3878
+ state,
3879
+ prepare,
3880
+ record,
3881
+ finish
3882
+ ]);
3883
+ return {
3884
+ available: state !== "unavailable",
3885
+ phase,
3886
+ toggle
3887
+ };
3888
+ }
3889
+ //#endregion
3531
3890
  //#region src/components/TerminalPane.tsx
3532
3891
  function themeColors() {
3533
3892
  const styles = getComputedStyle(document.documentElement);
@@ -3586,6 +3945,12 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
3586
3945
  control.current?.input(text, true);
3587
3946
  }
3588
3947
  });
3948
+ const voice = useVoiceInput({
3949
+ show,
3950
+ send: (text) => {
3951
+ control.current?.input(text, true);
3952
+ }
3953
+ });
3589
3954
  const showCopyResult = useRef((copied) => {});
3590
3955
  showCopyResult.current = (copied) => copied ? show("Copied to clipboard", "info", 1200) : show("The browser blocked clipboard access.", "error", 4e3);
3591
3956
  useEffect(() => {
@@ -3844,7 +4209,8 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
3844
4209
  canControl && active && touch && inputFocused && /* @__PURE__ */ jsx(TerminalKeyBar, {
3845
4210
  ctrl,
3846
4211
  onToggleCtrl: () => setCtrl((armed) => !armed),
3847
- onBytes: (bytes) => control.current?.input(bytes)
4212
+ onBytes: (bytes) => control.current?.input(bytes),
4213
+ voice
3848
4214
  })
3849
4215
  ]
3850
4216
  });
@@ -4080,6 +4446,7 @@ function useClearDoneOnVisit(thread, paneId, enabled) {
4080
4446
  }
4081
4447
  //#endregion
4082
4448
  //#region src/components/App.tsx
4449
+ var keepTerminalFocus = (event) => event.preventDefault();
4083
4450
  function App(props) {
4084
4451
  return /* @__PURE__ */ jsx(CrashReport, { children: /* @__PURE__ */ jsx(RuntimeProvider, {
4085
4452
  bootstrap: props.bootstrap,
@@ -4280,12 +4647,14 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4280
4647
  className: "mobile-controls",
4281
4648
  children: [/* @__PURE__ */ jsx("button", {
4282
4649
  "aria-label": "Open sidebar",
4650
+ onPointerDown: keepTerminalFocus,
4283
4651
  onClick: toggleSidebar,
4284
- children: /* @__PURE__ */ jsx(PanelLeft, { size: 16 })
4652
+ children: /* @__PURE__ */ jsx(PanelLeft, { size: 22 })
4285
4653
  }), /* @__PURE__ */ jsx("button", {
4286
4654
  "aria-label": "Open command palette",
4655
+ onPointerDown: keepTerminalFocus,
4287
4656
  onClick: () => setPaletteOpen(true),
4288
- children: /* @__PURE__ */ jsx(Search, { size: 16 })
4657
+ children: /* @__PURE__ */ jsx(Command, { size: 22 })
4289
4658
  })]
4290
4659
  }),
4291
4660
  false,
@@ -1,7 +1,7 @@
1
1
  import { r as __exportAll } from "./rolldown-runtime_BMI-E3GI.mjs";
2
2
  import { S as createAstro, i as renderComponent, u as renderTemplate } from "./server_CnGYDmxV.mjs";
3
3
  import { t as createComponent } from "./compiler_BiU10Vds.mjs";
4
- import { n as $$Base, t as App } from "./App_CmESMUHE.mjs";
4
+ import { n as $$Base, t as App } from "./App_C2vmjHiY.mjs";
5
5
  //#region src/pages/threads/[id].astro
6
6
  var _id__exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$Id,
@@ -1,7 +1,7 @@
1
1
  import { r as __exportAll } from "./rolldown-runtime_BMI-E3GI.mjs";
2
2
  import { S as createAstro, i as renderComponent, u as renderTemplate } from "./server_CnGYDmxV.mjs";
3
3
  import { t as createComponent } from "./compiler_BiU10Vds.mjs";
4
- import { n as $$Base, t as App } from "./App_CmESMUHE.mjs";
4
+ import { n as $$Base, t as App } from "./App_C2vmjHiY.mjs";
5
5
  //#region src/pages/sessions/[session]/tabs/[tab].astro
6
6
  var _tab__exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$Tab,