@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.
- package/README.md +30 -1
- package/dist/client/_astro/App.1VFY1ClT.css +1 -0
- package/dist/client/_astro/App.CGyf2WDA.js +4 -0
- package/dist/server/chunks/{App_CmESMUHE.mjs → App_C2vmjHiY.mjs} +434 -65
- package/dist/server/chunks/{_id__CQzMt2xL.mjs → _id__BimjoFCd.mjs} +1 -1
- package/dist/server/chunks/{_tab__D-smYrCo.mjs → _tab__WmsbMlfC.mjs} +1 -1
- package/dist/server/chunks/{new_NXgn74q9.mjs → new_bTXSW7C3.mjs} +1 -1
- package/dist/server/chunks/{settings_D1jcCWzt.mjs → settings_DwzWmJBe.mjs} +1 -1
- package/dist/server/entry.mjs +5 -5
- package/package.json +4 -1
- package/server/cli.ts +0 -3
- package/server/plugin/stop.ts +10 -1
- package/server/run.ts +10 -3
- package/server/ssr.ts +4 -3
- package/src/server/auth/access.ts +1 -1
- package/src/server/auth/auth.ts +12 -4
- package/src/server/dev-hmr-proxy.ts +16 -0
- package/src/server/host.ts +11 -2
- package/src/server/runtime/manager.ts +7 -1
- package/src/server/runtime/reconcile.ts +5 -2
- package/src/server/tailscale-serve.ts +42 -0
- package/src/server/transport/icons.ts +1 -1
- package/src/server/voice/model.ts +35 -0
- package/src/server/voice/routes.ts +29 -0
- package/src/server/voice/transcriber.ts +80 -0
- package/src/server/voice/worker.mjs +23 -0
- package/dist/client/_astro/App.1PYvyCIo.css +0 -1
- package/dist/client/_astro/App.B3aX6cef.js +0 -2
|
@@ -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: "
|
|
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: () =>
|
|
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
|
|
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: () =>
|
|
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__ */
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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
|
-
|
|
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: [
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3357
|
-
root.
|
|
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:
|
|
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(
|
|
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 "./
|
|
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 "./
|
|
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,
|