@pablopunk/webr 0.2.1 → 0.3.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.
@@ -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
- 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, 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";
3
+ import { ViewTransition, createContext, memo, useCallback, useContext, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Archive, ArchiveRestore, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Bot, Check, ChevronDown, ChevronRight, Circle, CircleAlert, CircleCheck, CircleHelp, Command, Copy, Cpu, Folder, FolderTree, GitBranch, ImagePlus, LayoutList, LoaderCircle, MessageCircleQuestion, Mic, Monitor, Moon, PanelLeft, PanelLeftClose, PanelLeftOpen, 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";
@@ -184,7 +184,8 @@ var shortcutLabels = {
184
184
  previousPane: "Previous pane",
185
185
  toggleSidebar: "Toggle sidebar",
186
186
  newThread: "New thread",
187
- toggleTerminal: "Toggle terminal"
187
+ openTerminal: "New terminal",
188
+ closePane: "Close pane"
188
189
  };
189
190
  var defaultShortcuts = {
190
191
  nextThread: "ctrl+j",
@@ -193,14 +194,15 @@ var defaultShortcuts = {
193
194
  previousPane: "ctrl+h",
194
195
  toggleSidebar: "meta+b",
195
196
  newThread: "ctrl+x",
196
- toggleTerminal: "ctrl+q"
197
+ openTerminal: "ctrl+e",
198
+ closePane: "ctrl+q"
197
199
  };
198
200
  function getShortcuts() {
199
201
  try {
200
202
  const saved = JSON.parse(localStorage.getItem("webr-shortcuts") ?? "{}");
201
203
  return {
202
204
  ...defaultShortcuts,
203
- ...saved
205
+ ...Object.fromEntries(Object.entries(saved).filter(([action]) => Object.hasOwn(defaultShortcuts, action)))
204
206
  };
205
207
  } catch {
206
208
  return defaultShortcuts;
@@ -335,11 +337,15 @@ var renameProject = (project, name) => post$1("/api/projects/rename", {
335
337
  logicalId: project.logicalId ?? project.id,
336
338
  name
337
339
  });
338
- var toggleThreadTerminal = (thread, direction) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/terminal`, {
340
+ var openThreadTerminal = (thread, afterPaneId) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/panes/terminal`, {
339
341
  machineId: thread.machineId,
340
- direction
342
+ afterPaneId
343
+ });
344
+ var closeThreadPane = (thread, paneId, force) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/panes/close`, {
345
+ machineId: thread.machineId,
346
+ paneId,
347
+ force
341
348
  });
342
- var closeThreadTerminal = (thread) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/terminal/close`, { machineId: thread.machineId });
343
349
  var deleteThreadPermanently = (thread) => post$1(`/api/threads/${encodeURIComponent(thread.id)}/delete`, { machineId: thread.machineId });
344
350
  var deleteArchivedThreads = () => post$1("/api/archive/delete");
345
351
  var plural = (count, one, many) => count === 1 ? one : many;
@@ -351,32 +357,15 @@ function deleteWarning(threads) {
351
357
  if (!worktrees) return `${subject} permanently? This closes ${closes}. Your project files stay untouched.`;
352
358
  return `${subject} permanently? This closes ${closes} and removes ${worktrees === count ? plural(count, "its worktree", "their worktrees") : `${worktrees} ${plural(worktrees, "worktree", "worktrees")}`}, including uncommitted changes.`;
353
359
  }
354
- //#endregion
355
- //#region src/lib/terminal-split.ts
356
- var threadAgent = (thread) => thread.panes.find((pane) => pane.kind === "agent") ?? thread.panes[0];
357
- var threadShell = (thread) => {
358
- const main = threadAgent(thread);
359
- return thread.panes.find((pane) => pane.kind === "shell" && pane.id !== main?.id);
360
- };
361
- var peekSide = (shell, area) => shell.x > area.x && shell.y === area.y ? "right" : "bottom";
362
- var terminalDirection = (side) => side === "right" ? "right" : "down";
363
- function hiddenTerminalLayout(thread, layout) {
364
- const shell = threadShell(thread);
365
- const agent = threadAgent(thread);
366
- const shellRect = shell && layout.panes.find((rect) => rect.paneId === shell.id)?.rect;
367
- const otherPanes = thread.panes.filter((pane) => pane.id !== shell?.id && pane.id !== agent?.id);
368
- if (!thread.terminalHidden || !shell || !agent || !shellRect || otherPanes.length) return void 0;
369
- return {
370
- shell,
371
- agent,
372
- side: peekSide(shellRect, layout.area)
373
- };
360
+ var canClosePane = (thread, paneId) => thread.panes.length > 1 && thread.panes.some((pane) => pane.id === paneId && pane.kind !== "agent");
361
+ var neighbourPane = (panes, at) => panes[at - 1] ?? panes[at + 1];
362
+ function paneToFocusAfterClosing(panes, windowStart, paneId) {
363
+ const at = panes.findIndex((pane) => pane.id === paneId);
364
+ return panes.slice(windowStart, windowStart + 2).find((pane) => pane.id !== paneId) ?? neighbourPane(panes, at);
374
365
  }
375
- function currentTerminalDirection(thread, layouts, narrow) {
376
- const shell = threadShell(thread);
377
- const layout = layouts.find((layout) => layout.panes.some((rect) => rect.paneId === shell?.id));
378
- const rect = layout?.panes.find((rect) => rect.paneId === shell?.id)?.rect;
379
- return rect && layout ? terminalDirection(peekSide(rect, layout.area)) : narrow ? "down" : "right";
366
+ function windowStartShowing(count, start, index, size = 2) {
367
+ const shown = index < 0 ? start : index < start ? index : index >= start + size ? index - size + 1 : start;
368
+ return Math.max(0, Math.min(shown, count - size));
380
369
  }
381
370
  function span(name) {
382
371
  const start = performance.now();
@@ -389,7 +378,7 @@ function startPerf() {}
389
378
  //#endregion
390
379
  //#region src/components/CommandPalette.tsx
391
380
  var reportFailure$1 = (error) => alertDialog(error instanceof Error ? error.message : "Herdr could not complete this action.");
392
- function CommandPalette({ open, onClose, projects, threads, thread, focusedPane, mode, onModeChange, onToggleSidebar, onFocusPane, onTerminalAction, onToggleTerminal, onCloseTerminal }) {
381
+ function CommandPalette({ open, onClose, projects, threads, thread, focusedPane, mode, onModeChange, onToggleSidebar, onFocusPane, onTerminalAction, onOpenTerminal, onClosePane }) {
393
382
  const [query, setQuery] = useState("");
394
383
  const [selected, setSelected] = useState(0);
395
384
  const [shortcuts, setShortcuts] = useState(defaultShortcuts);
@@ -483,26 +472,17 @@ function CommandPalette({ open, onClose, projects, threads, thread, focusedPane,
483
472
  navigator.clipboard.writeText(location.href);
484
473
  }
485
474
  },
486
- ...thread.bindingState === "attached" ? threadShell(thread) ? [{
487
- label: "Toggle terminal",
475
+ ...thread.bindingState === "attached" ? [{
476
+ label: "New terminal",
488
477
  icon: /* @__PURE__ */ jsx(SquareTerminal, { size: 16 }),
489
- shortcut: "toggleTerminal",
490
- run: () => onToggleTerminal()
491
- }, {
492
- label: "Close terminal",
478
+ shortcut: "openTerminal",
479
+ run: onOpenTerminal
480
+ }, ...canClosePane(thread, focusedPane) ? [{
481
+ label: "Close pane",
493
482
  icon: /* @__PURE__ */ jsx(X, { size: 16 }),
494
- detail: "Stops its shell",
495
- run: onCloseTerminal
496
- }] : [{
497
- label: "Toggle terminal (side)",
498
- icon: /* @__PURE__ */ jsx(PanelRight, { size: 16 }),
499
- shortcut: "toggleTerminal",
500
- run: () => onToggleTerminal("right")
501
- }, {
502
- label: "Toggle terminal (below)",
503
- icon: /* @__PURE__ */ jsx(PanelBottom, { size: 16 }),
504
- run: () => onToggleTerminal("down")
505
- }] : [],
483
+ shortcut: "closePane",
484
+ run: onClosePane
485
+ }] : []] : [],
506
486
  ...thread.bindingState === "attached" ? [
507
487
  {
508
488
  label: "Request terminal control",
@@ -565,8 +545,8 @@ function CommandPalette({ open, onClose, projects, threads, thread, focusedPane,
565
545
  onToggleSidebar,
566
546
  onFocusPane,
567
547
  onTerminalAction,
568
- onToggleTerminal,
569
- onCloseTerminal
548
+ onOpenTerminal,
549
+ onClosePane
570
550
  ]).filter((command) => `${command.label} ${command.detail ?? ""}`.toLowerCase().includes(query.toLowerCase().trim()));
571
551
  if (!open) return null;
572
552
  const run = (command) => {
@@ -2795,7 +2775,6 @@ function createRuntimeStore(bootstrap) {
2795
2775
  threads: Object.fromEntries(Object.entries(state.threads).map(([id, thread]) => [id, {
2796
2776
  ...thread,
2797
2777
  status: "unknown",
2798
- panes: [],
2799
2778
  bindingState: "detached"
2800
2779
  }]))
2801
2780
  };
@@ -2836,9 +2815,14 @@ function createRuntimeStore(bootstrap) {
2836
2815
  }
2837
2816
  var createUiStore = () => createStore((set) => ({
2838
2817
  focusedPanes: {},
2818
+ paneWindows: {},
2839
2819
  focus: (scope, paneId) => set((state) => ({ focusedPanes: {
2840
2820
  ...state.focusedPanes,
2841
2821
  [scope]: paneId
2822
+ } })),
2823
+ showWindow: (scope, start) => set((state) => ({ paneWindows: {
2824
+ ...state.paneWindows,
2825
+ [scope]: start
2842
2826
  } }))
2843
2827
  }));
2844
2828
  function decodeFrame(packet) {
@@ -3922,6 +3906,7 @@ var loadTerminalFonts = () => typeof document === "undefined" || !document.fonts
3922
3906
  ]).then(() => void 0, () => void 0);
3923
3907
  var FOCUS_IN = "\x1B[I";
3924
3908
  var FOCUS_OUT = "\x1B[O";
3909
+ var finiteSize = (size) => size && Number.isFinite(size.cols) && Number.isFinite(size.rows) ? size : void 0;
3925
3910
  function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }) {
3926
3911
  const host = useRef(null);
3927
3912
  const section = useRef(null);
@@ -4000,11 +3985,12 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
4000
3985
  });
4001
3986
  const measuredViewport = () => {
4002
3987
  try {
4003
- return fit.proposeDimensions();
3988
+ return finiteSize(fit.proposeDimensions());
4004
3989
  } catch {
4005
3990
  return;
4006
3991
  }
4007
3992
  };
3993
+ const hostIsLaidOut = () => !!host.current?.isConnected && host.current.clientWidth > 0 && host.current.clientHeight > 0;
4008
3994
  const viewport = () => {
4009
3995
  const { cols, rows } = measuredViewport() ?? estimatedViewport();
4010
3996
  return {
@@ -4034,13 +4020,14 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
4034
4020
  const resize = new ResizeObserver(() => {
4035
4021
  clearTimeout(timer);
4036
4022
  timer = setTimeout(() => {
4023
+ if (!hostIsLaidOut()) return;
4037
4024
  const { cols, rows } = viewport();
4038
4025
  control.current?.resize(cols, rows);
4039
4026
  }, 150);
4040
4027
  });
4041
4028
  resize.observe(host.current);
4042
4029
  const claimSize = () => {
4043
- if (document.visibilityState !== "visible" || !document.hasFocus()) return;
4030
+ if (document.visibilityState !== "visible" || !document.hasFocus() || !hostIsLaidOut()) return;
4044
4031
  const { cols, rows } = viewport();
4045
4032
  control.current?.resize(cols, rows, true);
4046
4033
  };
@@ -4216,70 +4203,6 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
4216
4203
  });
4217
4204
  }
4218
4205
  //#endregion
4219
- //#region src/components/AdoptTab.tsx
4220
- function AdoptTab({ thread, tabs }) {
4221
- const [tabId, setTabId] = useState("");
4222
- const [error, setError] = useState("");
4223
- const [busy, setBusy] = useState(false);
4224
- const selected = tabs.find((tab) => tab.tabId === tabId && !tab.bound);
4225
- return /* @__PURE__ */ jsxs("form", {
4226
- className: "adoption-picker",
4227
- onSubmit: async (event) => {
4228
- event.preventDefault();
4229
- if (!selected || busy) return;
4230
- setBusy(true);
4231
- 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" })) {
4232
- setBusy(false);
4233
- return;
4234
- }
4235
- setError("");
4236
- try {
4237
- if (!(await fetch(`/api/threads/${encodeURIComponent(thread.id)}/adopt`, {
4238
- method: "POST",
4239
- headers: { "Content-Type": "application/json" },
4240
- body: JSON.stringify({
4241
- machineId: thread.machineId,
4242
- terminalIds: selected.panes.map((pane) => pane.terminalId)
4243
- })
4244
- })).ok) throw new Error("The tab changed or is already bound; select it again after refresh.");
4245
- } catch (error) {
4246
- setError(error instanceof Error ? error.message : "Adoption failed.");
4247
- } finally {
4248
- setBusy(false);
4249
- }
4250
- },
4251
- children: [
4252
- /* @__PURE__ */ jsxs("label", { children: ["Select the current native tab to adopt ", /* @__PURE__ */ jsxs("select", {
4253
- "aria-label": "Native tab to adopt",
4254
- value: tabId,
4255
- onChange: (event) => setTabId(event.target.value),
4256
- children: [/* @__PURE__ */ jsx("option", {
4257
- value: "",
4258
- children: "Select a tab…"
4259
- }), tabs.map((tab) => /* @__PURE__ */ jsxs("option", {
4260
- value: tab.tabId,
4261
- disabled: tab.bound || !tab.panes.length,
4262
- children: [
4263
- tab.label,
4264
- " · ",
4265
- tab.tabId,
4266
- tab.bound ? " · Already bound" : ""
4267
- ]
4268
- }, tab.tabId))]
4269
- })] }),
4270
- selected && /* @__PURE__ */ jsx("p", { children: selected.panes.map((pane) => `${pane.title} (${pane.terminalId})`).join(", ") }),
4271
- /* @__PURE__ */ jsx("button", {
4272
- disabled: !selected || busy,
4273
- children: "Adopt without restarting"
4274
- }),
4275
- error && /* @__PURE__ */ jsx("p", {
4276
- role: "alert",
4277
- children: error
4278
- })
4279
- ]
4280
- });
4281
- }
4282
- //#endregion
4283
4206
  //#region src/components/LaunchProgress.tsx
4284
4207
  function LaunchProgress({ thread }) {
4285
4208
  const failed = launchFailed(thread);
@@ -4327,51 +4250,109 @@ function LaunchProgress({ thread }) {
4327
4250
  });
4328
4251
  }
4329
4252
  //#endregion
4330
- //#region src/components/ThreadView.tsx
4331
- var PEEK_SIZE = "28px";
4332
- var besidePeek = (side) => side === "right" ? {
4333
- left: 0,
4334
- top: 0,
4335
- width: `calc(100% - ${PEEK_SIZE})`,
4336
- height: "100%"
4337
- } : {
4338
- left: 0,
4339
- top: 0,
4340
- width: "100%",
4341
- height: `calc(100% - ${PEEK_SIZE})`
4342
- };
4343
- function TerminalPeek({ side, onOpen }) {
4253
+ //#region src/components/PaneStrip.tsx
4254
+ var PaneIcon = ({ pane }) => pane.kind === "agent" ? /* @__PURE__ */ jsx(Bot, {
4255
+ size: 14,
4256
+ strokeWidth: 1.8,
4257
+ "aria-hidden": "true"
4258
+ }) : /* @__PURE__ */ jsx(TerminalSquare, {
4259
+ size: 14,
4260
+ strokeWidth: 1.8,
4261
+ "aria-hidden": "true"
4262
+ });
4263
+ function CollapsedPane({ pane, side, onOpen }) {
4344
4264
  return /* @__PURE__ */ jsx(Tooltip$1, {
4345
- label: "Show terminal",
4346
- side: side === "right" ? "left" : "top",
4265
+ label: `Show ${pane.title}`,
4266
+ side: side === "left" ? "right" : "left",
4347
4267
  children: /* @__PURE__ */ jsxs("button", {
4348
4268
  type: "button",
4349
- className: `terminal-peek is-${side}`,
4350
- "aria-label": "Show terminal",
4269
+ className: "pane-collapsed",
4270
+ "aria-label": `Show ${pane.title}`,
4351
4271
  onClick: onOpen,
4352
- children: [/* @__PURE__ */ jsx(TerminalSquare, {
4353
- size: 14,
4354
- strokeWidth: 1.8,
4355
- "aria-hidden": "true"
4356
- }), /* @__PURE__ */ jsx("span", { children: "Terminal" })]
4272
+ children: [/* @__PURE__ */ jsx(PaneIcon, { pane }), /* @__PURE__ */ jsx("span", { children: pane.title })]
4357
4273
  })
4358
4274
  });
4359
4275
  }
4360
- function ThreadView({ thread, layouts, tabs, connected, canControl, onFocusPane, focusedPane, onToggleTerminal }) {
4361
- const pane = (item, style) => /* @__PURE__ */ jsx("div", {
4362
- className: "native-pane-position",
4363
- style,
4364
- children: /* @__PURE__ */ jsx(TerminalPane, {
4365
- pane: item,
4366
- machineId: thread.machineId,
4367
- threadId: thread.id,
4368
- active: focusedPane === item.id,
4369
- canControl,
4370
- onFocus: () => onFocusPane(item.id)
4371
- })
4372
- }, `${item.id}:${item.terminalId}`);
4276
+ var stripShape = (panes, windowStart) => JSON.stringify([windowStart, panes.map((pane) => pane.id)]);
4277
+ var panesInShape = (shape, panes) => {
4278
+ const [start, ids] = JSON.parse(shape);
4279
+ return {
4280
+ start,
4281
+ panes: ids.flatMap((id) => panes.filter((pane) => pane.id === id))
4282
+ };
4283
+ };
4284
+ /** Only a change of order or window runs as a transition, because a view transition covers the live terminals with snapshots while it animates. */
4285
+ var useAnimatedShape = (panes, windowStart) => panesInShape(useDeferredValue(stripShape(panes, windowStart)), panes);
4286
+ function PaneStrip({ panes: livePanes, windowStart: liveStart, onFocusPane, renderPane }) {
4287
+ const { panes, start } = useAnimatedShape(livePanes, liveStart);
4288
+ const isVisible = (index) => index >= start && index < start + 2;
4289
+ return /* @__PURE__ */ jsx("div", {
4290
+ className: `pane-strip ${panes.length > 1 ? "is-split" : ""}`,
4291
+ children: panes.map((pane, index) => /* @__PURE__ */ jsx(ViewTransition, {
4292
+ default: "pane-move",
4293
+ enter: "pane-enter",
4294
+ exit: "pane-exit",
4295
+ children: isVisible(index) ? /* @__PURE__ */ jsx("div", {
4296
+ className: "pane-slot",
4297
+ children: renderPane(pane)
4298
+ }) : /* @__PURE__ */ jsx(CollapsedPane, {
4299
+ pane,
4300
+ side: index < start ? "left" : "right",
4301
+ onOpen: () => onFocusPane(pane.id)
4302
+ })
4303
+ }, pane.id))
4304
+ });
4305
+ }
4306
+ function PaneTabs({ panes, focusedPane, onFocusPane, renderPane }) {
4307
+ const shownId = useDeferredValue(focusedPane);
4308
+ const shown = panes.find((pane) => pane.id === shownId) ?? panes[0];
4309
+ return /* @__PURE__ */ jsxs("div", {
4310
+ className: "pane-tabbed",
4311
+ children: [panes.length > 1 && /* @__PURE__ */ jsx("div", {
4312
+ className: "pane-tabs",
4313
+ role: "tablist",
4314
+ "aria-label": "Panes",
4315
+ children: panes.map((pane) => /* @__PURE__ */ jsxs("button", {
4316
+ type: "button",
4317
+ role: "tab",
4318
+ "aria-selected": pane.id === shown?.id,
4319
+ className: "pane-tab",
4320
+ onClick: () => onFocusPane(pane.id),
4321
+ children: [/* @__PURE__ */ jsx(PaneIcon, { pane }), /* @__PURE__ */ jsx("span", { children: pane.title })]
4322
+ }, pane.id))
4323
+ }), shown && /* @__PURE__ */ jsx(ViewTransition, {
4324
+ enter: "pane-enter",
4325
+ exit: "pane-exit",
4326
+ children: /* @__PURE__ */ jsx("div", {
4327
+ className: "pane-slot",
4328
+ role: "tabpanel",
4329
+ children: renderPane(shown)
4330
+ })
4331
+ }, shown.id)]
4332
+ });
4333
+ }
4334
+ //#endregion
4335
+ //#region src/components/ThreadView.tsx
4336
+ function ThreadView({ thread, connected, canControl, onFocusPane, focusedPane, windowStart, narrow }) {
4337
+ const detached = connected && thread.bindingState === "detached";
4338
+ useEffect(() => {
4339
+ if (detached) navigate(`/new?project=${encodeURIComponent(thread.projectId)}`);
4340
+ }, [detached, thread.projectId]);
4341
+ const renderPane = (pane) => pane.terminalId ? /* @__PURE__ */ jsx(TerminalPane, {
4342
+ pane,
4343
+ machineId: thread.machineId,
4344
+ threadId: thread.id,
4345
+ active: focusedPane === pane.id,
4346
+ canControl,
4347
+ onFocus: () => onFocusPane(pane.id)
4348
+ }, pane.terminalId) : null;
4373
4349
  if (connected && (isLaunching(thread) || launchFailed(thread))) return /* @__PURE__ */ jsx(LaunchProgress, { thread });
4374
- const visibleLayouts = layouts.filter((layout) => layout.panes.some((rect) => thread.panes.some((pane) => pane.id === rect.paneId)));
4350
+ const strip = {
4351
+ panes: thread.panes,
4352
+ focusedPane,
4353
+ onFocusPane,
4354
+ renderPane
4355
+ };
4375
4356
  return /* @__PURE__ */ jsxs("main", {
4376
4357
  className: "native-thread-layout",
4377
4358
  "aria-label": `${thread.title} tab`,
@@ -4379,37 +4360,17 @@ function ThreadView({ thread, layouts, tabs, connected, canControl, onFocusPane,
4379
4360
  (!connected || thread.bindingState !== "attached") && /* @__PURE__ */ jsx("p", {
4380
4361
  className: "runtime-notice",
4381
4362
  role: "status",
4382
- 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…"
4363
+ children: !connected ? "Disconnected; input is disabled." : "Waiting for the terminal to attach…"
4383
4364
  }),
4384
4365
  connected && thread.bindingState === "attached" && !canControl && /* @__PURE__ */ jsx("p", {
4385
4366
  className: "runtime-notice",
4386
4367
  role: "status",
4387
4368
  children: "Terminal input is unavailable because this Herdr version is not supported."
4388
4369
  }),
4389
- connected && thread.bindingState === "detached" && /* @__PURE__ */ jsx(AdoptTab, {
4390
- thread,
4391
- tabs
4392
- }, thread.id),
4393
- connected && thread.bindingState === "attached" && visibleLayouts.map((layout) => {
4394
- const hidden = hiddenTerminalLayout(thread, layout);
4395
- const split = !hidden && layout.panes.filter((rect) => thread.panes.some((item) => item.id === rect.paneId)).length > 1;
4396
- return /* @__PURE__ */ jsx("div", {
4397
- className: `native-pane-layout ${split ? "is-split" : ""}`,
4398
- children: hidden ? [pane(hidden.agent, besidePeek(hidden.side)), /* @__PURE__ */ jsx(TerminalPeek, {
4399
- side: hidden.side,
4400
- onOpen: onToggleTerminal
4401
- }, "terminal-peek")] : thread.panes.map((item) => {
4402
- const rect = layout.panes.find((rect) => rect.paneId === item.id)?.rect;
4403
- if (!rect || !layout.area.width || !layout.area.height) return null;
4404
- return pane(item, {
4405
- left: `${(rect.x - layout.area.x) / layout.area.width * 100}%`,
4406
- top: `${(rect.y - layout.area.y) / layout.area.height * 100}%`,
4407
- width: `${rect.width / layout.area.width * 100}%`,
4408
- height: `${rect.height / layout.area.height * 100}%`
4409
- });
4410
- })
4411
- }, `${layout.workspaceId}:${layout.tabId}`);
4412
- })
4370
+ connected && thread.bindingState === "attached" && (narrow ? /* @__PURE__ */ jsx(PaneTabs, { ...strip }) : /* @__PURE__ */ jsx(PaneStrip, {
4371
+ ...strip,
4372
+ windowStart
4373
+ }))
4413
4374
  ]
4414
4375
  });
4415
4376
  }
@@ -4423,6 +4384,30 @@ function appShortcutAction(event, shortcuts) {
4423
4384
  return Object.entries(shortcuts).find(([, combo]) => combo === keyCombo(event))?.[0];
4424
4385
  }
4425
4386
  //#endregion
4387
+ //#region src/client/pane-window.ts
4388
+ var NARROW_SCREEN = "(max-width: 760px)";
4389
+ function usePaneWindowStart(ui, scope, paneIds, focusedPane) {
4390
+ const stored = useStore(ui, (state) => state.paneWindows[scope] ?? 0);
4391
+ const start = windowStartShowing(paneIds.length, stored, paneIds.indexOf(focusedPane));
4392
+ useEffect(() => {
4393
+ if (start !== stored) ui.getState().showWindow(scope, start);
4394
+ }, [
4395
+ ui,
4396
+ scope,
4397
+ start,
4398
+ stored
4399
+ ]);
4400
+ return start;
4401
+ }
4402
+ var subscribeToNarrowScreen = (onChange) => {
4403
+ if (typeof matchMedia !== "function") return () => {};
4404
+ const media = matchMedia(NARROW_SCREEN);
4405
+ media.addEventListener("change", onChange);
4406
+ return () => media.removeEventListener("change", onChange);
4407
+ };
4408
+ var isNarrowScreen = () => typeof matchMedia === "function" && matchMedia(NARROW_SCREEN).matches;
4409
+ var useNarrowScreen = () => useSyncExternalStore(subscribeToNarrowScreen, isNarrowScreen, () => false);
4410
+ //#endregion
4426
4411
  //#region src/client/clear-done-on-visit.ts
4427
4412
  function useClearDoneOnVisit(thread, paneId, enabled) {
4428
4413
  const isDone = thread?.status === "done";
@@ -4515,32 +4500,37 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4515
4500
  window.removeEventListener("popstate", syncCollapsedFromUrl);
4516
4501
  };
4517
4502
  }, []);
4518
- const layouts = thread ? projections[thread.machineId]?.layouts ?? [] : [];
4519
- const reportTerminalFailure = (error) => void alertDialog(error instanceof Error ? error.message : "Herdr could not change the terminal.");
4520
- const toggleTerminal = useCallback((direction) => {
4521
- if (!thread) return;
4522
- const chosen = direction ?? currentTerminalDirection(thread, layouts, window.matchMedia("(max-width: 760px)").matches);
4523
- toggleThreadTerminal(thread, chosen).then(({ paneId, hidden }) => setFocusedPane(hidden ? threadAgent(thread)?.id ?? paneId : paneId), reportTerminalFailure);
4503
+ const reportPaneFailure = (error) => void alertDialog(error instanceof Error ? error.message : "Herdr could not change the pane.");
4504
+ const windowStart = usePaneWindowStart(ui, scope, thread?.panes.map((pane) => pane.id) ?? [], focusedPane);
4505
+ const narrow = useNarrowScreen();
4506
+ const openTerminal = useCallback(() => {
4507
+ if (!thread || !focusedPane) return;
4508
+ openThreadTerminal(thread, focusedPane).then(({ paneId }) => setFocusedPane(paneId), reportPaneFailure);
4524
4509
  }, [
4525
4510
  thread,
4526
- layouts,
4511
+ focusedPane,
4527
4512
  setFocusedPane
4528
4513
  ]);
4529
- const focusPane = useCallback((paneId) => {
4530
- if (thread?.terminalHidden && threadShell(thread)?.id === paneId) toggleTerminal();
4531
- else setFocusedPane(paneId);
4514
+ const closePane = useCallback(async (paneId = focusedPane) => {
4515
+ if (!thread || !canClosePane(thread, paneId)) return;
4516
+ const pane = thread.panes.find((pane) => pane.id === paneId);
4517
+ const next = paneToFocusAfterClosing(thread.panes, windowStart, paneId);
4518
+ try {
4519
+ let result = await closeThreadPane(thread, paneId, false);
4520
+ if (result.busy && await confirmDialog(`${pane.title} is running a process. Close it anyway?`, {
4521
+ confirmLabel: "Close",
4522
+ danger: true
4523
+ })) result = await closeThreadPane(thread, paneId, true);
4524
+ if (result.closed && next) setFocusedPane(next.id);
4525
+ } catch (error) {
4526
+ reportPaneFailure(error);
4527
+ }
4532
4528
  }, [
4533
4529
  thread,
4534
- toggleTerminal,
4530
+ focusedPane,
4531
+ windowStart,
4535
4532
  setFocusedPane
4536
4533
  ]);
4537
- const closeTerminal = useCallback(() => {
4538
- if (!thread) return;
4539
- closeThreadTerminal(thread).then(() => {
4540
- const agent = threadAgent(thread);
4541
- if (agent) setFocusedPane(agent.id);
4542
- }, reportTerminalFailure);
4543
- }, [thread, setFocusedPane]);
4544
4534
  const toggleSidebar = useCallback(() => {
4545
4535
  if (window.matchMedia("(max-width: 760px)").matches) {
4546
4536
  setMobileOpen((value) => !value);
@@ -4573,11 +4563,12 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4573
4563
  if (!action) return;
4574
4564
  event.preventDefault();
4575
4565
  if (action === "toggleSidebar") toggleSidebar();
4576
- if (action === "toggleTerminal") toggleTerminal();
4566
+ if (action === "openTerminal") openTerminal();
4567
+ if (action === "closePane") closePane();
4577
4568
  if (action === "newThread") navigate(`/new${thread ? `?project=${encodeURIComponent(thread.projectId)}` : ""}`);
4578
4569
  if ((action === "nextPane" || action === "previousPane") && thread?.panes.length) {
4579
4570
  const index = thread.panes.findIndex((pane) => pane.id === focusedPane);
4580
- focusPane(thread.panes[(index + (action === "nextPane" ? 1 : -1) + thread.panes.length) % thread.panes.length].id);
4571
+ setFocusedPane(thread.panes[(index + (action === "nextPane" ? 1 : -1) + thread.panes.length) % thread.panes.length].id);
4581
4572
  }
4582
4573
  if ((action === "nextThread" || action === "previousThread") && threads.length) {
4583
4574
  const index = threads.findIndex((item) => item.id === thread?.id);
@@ -4592,8 +4583,8 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4592
4583
  threads,
4593
4584
  paletteOpen,
4594
4585
  toggleSidebar,
4595
- toggleTerminal,
4596
- focusPane,
4586
+ openTerminal,
4587
+ closePane,
4597
4588
  focusedPane,
4598
4589
  setFocusedPane
4599
4590
  ]);
@@ -4619,13 +4610,12 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4619
4610
  children: [
4620
4611
  page === "thread" && thread && /* @__PURE__ */ jsx(ThreadView, {
4621
4612
  thread,
4622
- layouts: projections[thread.machineId]?.layouts ?? [],
4623
- tabs: projections[thread.machineId]?.availableTabs ?? [],
4624
4613
  connected: gatewayConnected && !!projections[thread.machineId]?.connected,
4625
4614
  canControl: !!machines.find((machine) => machine.id === thread.machineId)?.writable,
4626
4615
  focusedPane,
4627
- onFocusPane: focusPane,
4628
- onToggleTerminal: () => toggleTerminal()
4616
+ onFocusPane: setFocusedPane,
4617
+ windowStart,
4618
+ narrow
4629
4619
  }),
4630
4620
  page === "new" && /* @__PURE__ */ jsx(NewThreadView, {
4631
4621
  projects,
@@ -4669,13 +4659,13 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4669
4659
  mode,
4670
4660
  onModeChange: changeMode,
4671
4661
  onToggleSidebar: toggleSidebar,
4672
- onFocusPane: focusPane,
4662
+ onFocusPane: setFocusedPane,
4673
4663
  onTerminalAction: (action) => {
4674
4664
  const pane = thread?.panes.find((pane) => pane.id === focusedPane);
4675
4665
  if (thread && pane?.terminalId) terminals.command(thread.machineId, thread.id, pane.terminalId, action);
4676
4666
  },
4677
- onToggleTerminal: toggleTerminal,
4678
- onCloseTerminal: closeTerminal
4667
+ onOpenTerminal: openTerminal,
4668
+ onClosePane: () => void closePane()
4679
4669
  })
4680
4670
  ]
4681
4671
  });
@@ -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_C2vmjHiY.mjs";
4
+ import { n as $$Base, t as App } from "./App_D7QqB19a.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_C2vmjHiY.mjs";
4
+ import { n as $$Base, t as App } from "./App_D7QqB19a.mjs";
5
5
  //#region src/pages/sessions/[session]/tabs/[tab].astro
6
6
  var _tab__exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$Tab,
@@ -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_C2vmjHiY.mjs";
4
+ import { n as $$Base, t as App } from "./App_D7QqB19a.mjs";
5
5
  //#region src/pages/new.astro
6
6
  var new_exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$New,
@@ -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_C2vmjHiY.mjs";
4
+ import { n as $$Base, t as App } from "./App_D7QqB19a.mjs";
5
5
  //#region src/pages/settings.astro
6
6
  var settings_exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$Settings,