@pablopunk/webr 0.2.1 → 0.3.2

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.
Binary file
Binary file
Binary file
@@ -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) => {
@@ -1345,7 +1325,10 @@ var behaviorKey = "webr-new-session-behavior";
1345
1325
  var legacyWorktreeKey = "webr-new-worktree";
1346
1326
  var lastWorktreeKey = (machineId, projectId) => `webr-last-worktree:${machineId}:${projectId}`;
1347
1327
  var isBehavior = (value) => value === "main" || value === "worktree" || value === "remember";
1348
- var behaviorFromLegacySetting = () => localStorage.getItem(legacyWorktreeKey) === "false" ? "main" : "worktree";
1328
+ var behaviorFromLegacySetting = () => {
1329
+ const legacy = localStorage.getItem(legacyWorktreeKey);
1330
+ return legacy === null ? "remember" : legacy === "false" ? "main" : "worktree";
1331
+ };
1349
1332
  function readNewSessionBehavior() {
1350
1333
  const stored = localStorage.getItem(behaviorKey);
1351
1334
  return isBehavior(stored) ? stored : behaviorFromLegacySetting();
@@ -2795,7 +2778,6 @@ function createRuntimeStore(bootstrap) {
2795
2778
  threads: Object.fromEntries(Object.entries(state.threads).map(([id, thread]) => [id, {
2796
2779
  ...thread,
2797
2780
  status: "unknown",
2798
- panes: [],
2799
2781
  bindingState: "detached"
2800
2782
  }]))
2801
2783
  };
@@ -2836,9 +2818,14 @@ function createRuntimeStore(bootstrap) {
2836
2818
  }
2837
2819
  var createUiStore = () => createStore((set) => ({
2838
2820
  focusedPanes: {},
2821
+ paneWindows: {},
2839
2822
  focus: (scope, paneId) => set((state) => ({ focusedPanes: {
2840
2823
  ...state.focusedPanes,
2841
2824
  [scope]: paneId
2825
+ } })),
2826
+ showWindow: (scope, start) => set((state) => ({ paneWindows: {
2827
+ ...state.paneWindows,
2828
+ [scope]: start
2842
2829
  } }))
2843
2830
  }));
2844
2831
  function decodeFrame(packet) {
@@ -3914,7 +3901,7 @@ function themeColors() {
3914
3901
  };
3915
3902
  }
3916
3903
  var TERMINAL_FONT_FAMILY = "\"JetBrains Mono\", \"Webr Symbols\", ui-monospace, Menlo, monospace";
3917
- var TERMINAL_FONT_SIZE = 13;
3904
+ var TERMINAL_FONT_SIZE = 15;
3918
3905
  var loadTerminalFonts = () => typeof document === "undefined" || !document.fonts ? Promise.resolve() : Promise.all([
3919
3906
  document.fonts.load(`400 ${TERMINAL_FONT_SIZE}px "JetBrains Mono"`),
3920
3907
  document.fonts.load(`700 ${TERMINAL_FONT_SIZE}px "JetBrains Mono"`),
@@ -3922,6 +3909,7 @@ var loadTerminalFonts = () => typeof document === "undefined" || !document.fonts
3922
3909
  ]).then(() => void 0, () => void 0);
3923
3910
  var FOCUS_IN = "\x1B[I";
3924
3911
  var FOCUS_OUT = "\x1B[O";
3912
+ var finiteSize = (size) => size && Number.isFinite(size.cols) && Number.isFinite(size.rows) ? size : void 0;
3925
3913
  function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }) {
3926
3914
  const host = useRef(null);
3927
3915
  const section = useRef(null);
@@ -4000,11 +3988,12 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
4000
3988
  });
4001
3989
  const measuredViewport = () => {
4002
3990
  try {
4003
- return fit.proposeDimensions();
3991
+ return finiteSize(fit.proposeDimensions());
4004
3992
  } catch {
4005
3993
  return;
4006
3994
  }
4007
3995
  };
3996
+ const hostIsLaidOut = () => !!host.current?.isConnected && host.current.clientWidth > 0 && host.current.clientHeight > 0;
4008
3997
  const viewport = () => {
4009
3998
  const { cols, rows } = measuredViewport() ?? estimatedViewport();
4010
3999
  return {
@@ -4034,13 +4023,14 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
4034
4023
  const resize = new ResizeObserver(() => {
4035
4024
  clearTimeout(timer);
4036
4025
  timer = setTimeout(() => {
4026
+ if (!hostIsLaidOut()) return;
4037
4027
  const { cols, rows } = viewport();
4038
4028
  control.current?.resize(cols, rows);
4039
4029
  }, 150);
4040
4030
  });
4041
4031
  resize.observe(host.current);
4042
4032
  const claimSize = () => {
4043
- if (document.visibilityState !== "visible" || !document.hasFocus()) return;
4033
+ if (document.visibilityState !== "visible" || !document.hasFocus() || !hostIsLaidOut()) return;
4044
4034
  const { cols, rows } = viewport();
4045
4035
  control.current?.resize(cols, rows, true);
4046
4036
  };
@@ -4216,70 +4206,6 @@ function TerminalPane({ pane, machineId, threadId, active, canControl, onFocus }
4216
4206
  });
4217
4207
  }
4218
4208
  //#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
4209
  //#region src/components/LaunchProgress.tsx
4284
4210
  function LaunchProgress({ thread }) {
4285
4211
  const failed = launchFailed(thread);
@@ -4327,51 +4253,109 @@ function LaunchProgress({ thread }) {
4327
4253
  });
4328
4254
  }
4329
4255
  //#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 }) {
4256
+ //#region src/components/PaneStrip.tsx
4257
+ var PaneIcon = ({ pane }) => pane.kind === "agent" ? /* @__PURE__ */ jsx(Bot, {
4258
+ size: 14,
4259
+ strokeWidth: 1.8,
4260
+ "aria-hidden": "true"
4261
+ }) : /* @__PURE__ */ jsx(TerminalSquare, {
4262
+ size: 14,
4263
+ strokeWidth: 1.8,
4264
+ "aria-hidden": "true"
4265
+ });
4266
+ function CollapsedPane({ pane, side, onOpen }) {
4344
4267
  return /* @__PURE__ */ jsx(Tooltip$1, {
4345
- label: "Show terminal",
4346
- side: side === "right" ? "left" : "top",
4268
+ label: `Show ${pane.title}`,
4269
+ side: side === "left" ? "right" : "left",
4347
4270
  children: /* @__PURE__ */ jsxs("button", {
4348
4271
  type: "button",
4349
- className: `terminal-peek is-${side}`,
4350
- "aria-label": "Show terminal",
4272
+ className: "pane-collapsed",
4273
+ "aria-label": `Show ${pane.title}`,
4351
4274
  onClick: onOpen,
4352
- children: [/* @__PURE__ */ jsx(TerminalSquare, {
4353
- size: 14,
4354
- strokeWidth: 1.8,
4355
- "aria-hidden": "true"
4356
- }), /* @__PURE__ */ jsx("span", { children: "Terminal" })]
4275
+ children: [/* @__PURE__ */ jsx(PaneIcon, { pane }), /* @__PURE__ */ jsx("span", { children: pane.title })]
4357
4276
  })
4358
4277
  });
4359
4278
  }
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}`);
4279
+ var stripShape = (panes, windowStart) => JSON.stringify([windowStart, panes.map((pane) => pane.id)]);
4280
+ var panesInShape = (shape, panes) => {
4281
+ const [start, ids] = JSON.parse(shape);
4282
+ return {
4283
+ start,
4284
+ panes: ids.flatMap((id) => panes.filter((pane) => pane.id === id))
4285
+ };
4286
+ };
4287
+ /** Only a change of order or window runs as a transition, because a view transition covers the live terminals with snapshots while it animates. */
4288
+ var useAnimatedShape = (panes, windowStart) => panesInShape(useDeferredValue(stripShape(panes, windowStart)), panes);
4289
+ function PaneStrip({ panes: livePanes, windowStart: liveStart, onFocusPane, renderPane }) {
4290
+ const { panes, start } = useAnimatedShape(livePanes, liveStart);
4291
+ const isVisible = (index) => index >= start && index < start + 2;
4292
+ return /* @__PURE__ */ jsx("div", {
4293
+ className: `pane-strip ${panes.length > 1 ? "is-split" : ""}`,
4294
+ children: panes.map((pane, index) => /* @__PURE__ */ jsx(ViewTransition, {
4295
+ default: "pane-move",
4296
+ enter: "pane-enter",
4297
+ exit: "pane-exit",
4298
+ children: isVisible(index) ? /* @__PURE__ */ jsx("div", {
4299
+ className: "pane-slot",
4300
+ children: renderPane(pane)
4301
+ }) : /* @__PURE__ */ jsx(CollapsedPane, {
4302
+ pane,
4303
+ side: index < start ? "left" : "right",
4304
+ onOpen: () => onFocusPane(pane.id)
4305
+ })
4306
+ }, pane.id))
4307
+ });
4308
+ }
4309
+ function PaneTabs({ panes, focusedPane, onFocusPane, renderPane }) {
4310
+ const shownId = useDeferredValue(focusedPane);
4311
+ const shown = panes.find((pane) => pane.id === shownId) ?? panes[0];
4312
+ return /* @__PURE__ */ jsxs("div", {
4313
+ className: "pane-tabbed",
4314
+ children: [panes.length > 1 && /* @__PURE__ */ jsx("div", {
4315
+ className: "pane-tabs",
4316
+ role: "tablist",
4317
+ "aria-label": "Panes",
4318
+ children: panes.map((pane) => /* @__PURE__ */ jsxs("button", {
4319
+ type: "button",
4320
+ role: "tab",
4321
+ "aria-selected": pane.id === shown?.id,
4322
+ className: "pane-tab",
4323
+ onClick: () => onFocusPane(pane.id),
4324
+ children: [/* @__PURE__ */ jsx(PaneIcon, { pane }), /* @__PURE__ */ jsx("span", { children: pane.title })]
4325
+ }, pane.id))
4326
+ }), shown && /* @__PURE__ */ jsx(ViewTransition, {
4327
+ enter: "pane-enter",
4328
+ exit: "pane-exit",
4329
+ children: /* @__PURE__ */ jsx("div", {
4330
+ className: "pane-slot",
4331
+ role: "tabpanel",
4332
+ children: renderPane(shown)
4333
+ })
4334
+ }, shown.id)]
4335
+ });
4336
+ }
4337
+ //#endregion
4338
+ //#region src/components/ThreadView.tsx
4339
+ function ThreadView({ thread, connected, canControl, onFocusPane, focusedPane, windowStart, narrow }) {
4340
+ const detached = connected && thread.bindingState === "detached";
4341
+ useEffect(() => {
4342
+ if (detached) navigate(`/new?project=${encodeURIComponent(thread.projectId)}`);
4343
+ }, [detached, thread.projectId]);
4344
+ const renderPane = (pane) => pane.terminalId ? /* @__PURE__ */ jsx(TerminalPane, {
4345
+ pane,
4346
+ machineId: thread.machineId,
4347
+ threadId: thread.id,
4348
+ active: focusedPane === pane.id,
4349
+ canControl,
4350
+ onFocus: () => onFocusPane(pane.id)
4351
+ }, pane.terminalId) : null;
4373
4352
  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)));
4353
+ const strip = {
4354
+ panes: thread.panes,
4355
+ focusedPane,
4356
+ onFocusPane,
4357
+ renderPane
4358
+ };
4375
4359
  return /* @__PURE__ */ jsxs("main", {
4376
4360
  className: "native-thread-layout",
4377
4361
  "aria-label": `${thread.title} tab`,
@@ -4379,37 +4363,17 @@ function ThreadView({ thread, layouts, tabs, connected, canControl, onFocusPane,
4379
4363
  (!connected || thread.bindingState !== "attached") && /* @__PURE__ */ jsx("p", {
4380
4364
  className: "runtime-notice",
4381
4365
  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…"
4366
+ children: !connected ? "Disconnected; input is disabled." : "Waiting for the terminal to attach…"
4383
4367
  }),
4384
4368
  connected && thread.bindingState === "attached" && !canControl && /* @__PURE__ */ jsx("p", {
4385
4369
  className: "runtime-notice",
4386
4370
  role: "status",
4387
4371
  children: "Terminal input is unavailable because this Herdr version is not supported."
4388
4372
  }),
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
- })
4373
+ connected && thread.bindingState === "attached" && (narrow ? /* @__PURE__ */ jsx(PaneTabs, { ...strip }) : /* @__PURE__ */ jsx(PaneStrip, {
4374
+ ...strip,
4375
+ windowStart
4376
+ }))
4413
4377
  ]
4414
4378
  });
4415
4379
  }
@@ -4423,6 +4387,30 @@ function appShortcutAction(event, shortcuts) {
4423
4387
  return Object.entries(shortcuts).find(([, combo]) => combo === keyCombo(event))?.[0];
4424
4388
  }
4425
4389
  //#endregion
4390
+ //#region src/client/pane-window.ts
4391
+ var NARROW_SCREEN = "(max-width: 760px)";
4392
+ function usePaneWindowStart(ui, scope, paneIds, focusedPane) {
4393
+ const stored = useStore(ui, (state) => state.paneWindows[scope] ?? 0);
4394
+ const start = windowStartShowing(paneIds.length, stored, paneIds.indexOf(focusedPane));
4395
+ useEffect(() => {
4396
+ if (start !== stored) ui.getState().showWindow(scope, start);
4397
+ }, [
4398
+ ui,
4399
+ scope,
4400
+ start,
4401
+ stored
4402
+ ]);
4403
+ return start;
4404
+ }
4405
+ var subscribeToNarrowScreen = (onChange) => {
4406
+ if (typeof matchMedia !== "function") return () => {};
4407
+ const media = matchMedia(NARROW_SCREEN);
4408
+ media.addEventListener("change", onChange);
4409
+ return () => media.removeEventListener("change", onChange);
4410
+ };
4411
+ var isNarrowScreen = () => typeof matchMedia === "function" && matchMedia(NARROW_SCREEN).matches;
4412
+ var useNarrowScreen = () => useSyncExternalStore(subscribeToNarrowScreen, isNarrowScreen, () => false);
4413
+ //#endregion
4426
4414
  //#region src/client/clear-done-on-visit.ts
4427
4415
  function useClearDoneOnVisit(thread, paneId, enabled) {
4428
4416
  const isDone = thread?.status === "done";
@@ -4515,32 +4503,37 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4515
4503
  window.removeEventListener("popstate", syncCollapsedFromUrl);
4516
4504
  };
4517
4505
  }, []);
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);
4506
+ const reportPaneFailure = (error) => void alertDialog(error instanceof Error ? error.message : "Herdr could not change the pane.");
4507
+ const windowStart = usePaneWindowStart(ui, scope, thread?.panes.map((pane) => pane.id) ?? [], focusedPane);
4508
+ const narrow = useNarrowScreen();
4509
+ const openTerminal = useCallback(() => {
4510
+ if (!thread || !focusedPane) return;
4511
+ openThreadTerminal(thread, focusedPane).then(({ paneId }) => setFocusedPane(paneId), reportPaneFailure);
4524
4512
  }, [
4525
4513
  thread,
4526
- layouts,
4514
+ focusedPane,
4527
4515
  setFocusedPane
4528
4516
  ]);
4529
- const focusPane = useCallback((paneId) => {
4530
- if (thread?.terminalHidden && threadShell(thread)?.id === paneId) toggleTerminal();
4531
- else setFocusedPane(paneId);
4517
+ const closePane = useCallback(async (paneId = focusedPane) => {
4518
+ if (!thread || !canClosePane(thread, paneId)) return;
4519
+ const pane = thread.panes.find((pane) => pane.id === paneId);
4520
+ const next = paneToFocusAfterClosing(thread.panes, windowStart, paneId);
4521
+ try {
4522
+ let result = await closeThreadPane(thread, paneId, false);
4523
+ if (result.busy && await confirmDialog(`${pane.title} is running a process. Close it anyway?`, {
4524
+ confirmLabel: "Close",
4525
+ danger: true
4526
+ })) result = await closeThreadPane(thread, paneId, true);
4527
+ if (result.closed && next) setFocusedPane(next.id);
4528
+ } catch (error) {
4529
+ reportPaneFailure(error);
4530
+ }
4532
4531
  }, [
4533
4532
  thread,
4534
- toggleTerminal,
4533
+ focusedPane,
4534
+ windowStart,
4535
4535
  setFocusedPane
4536
4536
  ]);
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
4537
  const toggleSidebar = useCallback(() => {
4545
4538
  if (window.matchMedia("(max-width: 760px)").matches) {
4546
4539
  setMobileOpen((value) => !value);
@@ -4573,11 +4566,12 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4573
4566
  if (!action) return;
4574
4567
  event.preventDefault();
4575
4568
  if (action === "toggleSidebar") toggleSidebar();
4576
- if (action === "toggleTerminal") toggleTerminal();
4569
+ if (action === "openTerminal") openTerminal();
4570
+ if (action === "closePane") closePane();
4577
4571
  if (action === "newThread") navigate(`/new${thread ? `?project=${encodeURIComponent(thread.projectId)}` : ""}`);
4578
4572
  if ((action === "nextPane" || action === "previousPane") && thread?.panes.length) {
4579
4573
  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);
4574
+ setFocusedPane(thread.panes[(index + (action === "nextPane" ? 1 : -1) + thread.panes.length) % thread.panes.length].id);
4581
4575
  }
4582
4576
  if ((action === "nextThread" || action === "previousThread") && threads.length) {
4583
4577
  const index = threads.findIndex((item) => item.id === thread?.id);
@@ -4592,8 +4586,8 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4592
4586
  threads,
4593
4587
  paletteOpen,
4594
4588
  toggleSidebar,
4595
- toggleTerminal,
4596
- focusPane,
4589
+ openTerminal,
4590
+ closePane,
4597
4591
  focusedPane,
4598
4592
  setFocusedPane
4599
4593
  ]);
@@ -4619,13 +4613,12 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4619
4613
  children: [
4620
4614
  page === "thread" && thread && /* @__PURE__ */ jsx(ThreadView, {
4621
4615
  thread,
4622
- layouts: projections[thread.machineId]?.layouts ?? [],
4623
- tabs: projections[thread.machineId]?.availableTabs ?? [],
4624
4616
  connected: gatewayConnected && !!projections[thread.machineId]?.connected,
4625
4617
  canControl: !!machines.find((machine) => machine.id === thread.machineId)?.writable,
4626
4618
  focusedPane,
4627
- onFocusPane: focusPane,
4628
- onToggleTerminal: () => toggleTerminal()
4619
+ onFocusPane: setFocusedPane,
4620
+ windowStart,
4621
+ narrow
4629
4622
  }),
4630
4623
  page === "new" && /* @__PURE__ */ jsx(NewThreadView, {
4631
4624
  projects,
@@ -4669,13 +4662,13 @@ function RuntimeApp({ page, bootstrap, threadId, projectId }) {
4669
4662
  mode,
4670
4663
  onModeChange: changeMode,
4671
4664
  onToggleSidebar: toggleSidebar,
4672
- onFocusPane: focusPane,
4665
+ onFocusPane: setFocusedPane,
4673
4666
  onTerminalAction: (action) => {
4674
4667
  const pane = thread?.panes.find((pane) => pane.id === focusedPane);
4675
4668
  if (thread && pane?.terminalId) terminals.command(thread.machineId, thread.id, pane.terminalId, action);
4676
4669
  },
4677
- onToggleTerminal: toggleTerminal,
4678
- onCloseTerminal: closeTerminal
4670
+ onOpenTerminal: openTerminal,
4671
+ onClosePane: () => void closePane()
4679
4672
  })
4680
4673
  ]
4681
4674
  });
@@ -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_DlKdj0Cm.mjs";
5
5
  //#region src/pages/threads/[id].astro
6
6
  var _id__exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$Id,