@carlesandres/house 0.5.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -9,17 +9,18 @@ import { createCliRenderer } from "@opentui/core";
9
9
  import { createRoot } from "@opentui/react";
10
10
  import { RegistryProvider } from "@effect/atom-react";
11
11
  import { Cause, Duration, Effect as Effect5, Fiber, Stream as Stream2 } from "effect";
12
- import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
12
+ import { useEffect as useEffect6, useRef as useRef4, useState as useState6 } from "react";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "@carlesandres/house",
16
- version: "0.5.0",
16
+ version: "0.5.2",
17
17
  description: "TUI-first markdown reader on opentui",
18
18
  type: "module",
19
19
  license: "MIT",
20
20
  repository: {
21
21
  type: "git",
22
- url: "git+https://github.com/carlesandres/house.git"
22
+ url: "git+https://github.com/carlesandres/house.git",
23
+ directory: "apps/house"
23
24
  },
24
25
  bugs: {
25
26
  url: "https://github.com/carlesandres/house/issues"
@@ -38,8 +39,6 @@ var package_default = {
38
39
  files: [
39
40
  "dist/bin.js",
40
41
  "dist/index.js",
41
- "README.md",
42
- "LICENSE",
43
42
  "CHANGELOG.md"
44
43
  ],
45
44
  publishConfig: {
@@ -51,29 +50,32 @@ var package_default = {
51
50
  house: "dist/bin.js"
52
51
  },
53
52
  scripts: {
53
+ build: "bun run build:cli",
54
+ "build:cli": "bun run dev/build-cli.ts",
55
+ "build:npm-main": "bun run dev/build-npm-main.ts",
56
+ "build:npm-packages": "bun run dev/build-npm-packages.ts",
57
+ "build:standalone": "bun run dev/build-standalone.ts",
58
+ "build:themes": "bun run dev/build-themes.ts",
54
59
  dev: "bun --watch src/index.tsx",
55
- start: "bun run src/index.tsx",
56
- "record-demo": "bash recordings/record-demo.sh",
57
- typecheck: "tsc --noEmit",
58
- lint: "oxlint --tsconfig tsconfig.json src/",
59
60
  format: "oxfmt src/",
60
61
  "format:check": "oxfmt --check src/",
62
+ lint: "oxlint --tsconfig tsconfig.json src/ test/ dev/",
63
+ "npm:pack": "bun run build:npm-main && npm pack --dry-run dist/npm/main",
64
+ "record-demo": "bash recordings/record-demo.sh",
65
+ release: "bun run dev/release.ts",
66
+ "repro:pty-codeblocks": "bun run dev/repro-pty-codeblocks.ts",
67
+ start: "bun run src/index.tsx",
61
68
  test: "bun test",
62
69
  "test:pty": "HOUSE_PTY=1 bun test test/pty/",
63
- "version:set": "bun run dev/set-version.ts",
64
- "build:cli": "bun run dev/build-cli.ts",
65
- "build:standalone": "bun run dev/build-standalone.ts",
66
- "build:npm-packages": "bun run dev/build-npm-packages.ts",
67
- "build:themes": "bun run dev/build-themes.ts",
68
- prepack: "bun run build:cli",
69
- "repro:pty-codeblocks": "bun run dev/repro-pty-codeblocks.ts",
70
- prepare: "git config core.hooksPath .githooks 2>/dev/null || true"
70
+ typecheck: "tsc --noEmit",
71
+ "verify:github": "bun test test/github-emulation.test.ts",
72
+ "version:set": "bun run dev/set-version.ts"
71
73
  },
72
74
  optionalDependencies: {
73
- "@carlesandres/house-darwin-arm64": "0.5.0",
74
- "@carlesandres/house-darwin-x64": "0.5.0",
75
- "@carlesandres/house-linux-arm64": "0.5.0",
76
- "@carlesandres/house-linux-x64": "0.5.0"
75
+ "@carlesandres/house-darwin-arm64": "0.5.1",
76
+ "@carlesandres/house-darwin-x64": "0.5.1",
77
+ "@carlesandres/house-linux-arm64": "0.5.1",
78
+ "@carlesandres/house-linux-x64": "0.5.1"
77
79
  },
78
80
  dependencies: {
79
81
  "@effect/atom-react": "4.0.0-beta.60",
@@ -84,12 +86,15 @@ var package_default = {
84
86
  ignore: "^7.0.5",
85
87
  marked: "^18.0.4",
86
88
  react: "19.2.6",
87
- scheduler: "0.27.0"
89
+ scheduler: "0.27.0",
90
+ "string-width": "7.2.0",
91
+ "web-tree-sitter": "0.25.10"
88
92
  },
89
93
  devDependencies: {
90
94
  "@effect/language-service": "0.86.2",
91
95
  "@types/bun": "1.3.14",
92
96
  "@types/react": "19.2.15",
97
+ emulate: "0.9.0",
93
98
  oxfmt: "0.51.0",
94
99
  oxlint: "1.66.0",
95
100
  tuistory: "^0.8.0",
@@ -99,18 +104,424 @@ var package_default = {
99
104
 
100
105
  // src/Browser.tsx
101
106
  import { SyntaxStyle } from "@opentui/core";
107
+
108
+ // ../../packages/ui/src/file-navigator/FileNavigator.tsx
109
+ import { useEffect, useState } from "react";
110
+
111
+ // ../../packages/ui/src/file-navigator/emptyState.tsx
112
+ import { jsxDEV, Fragment } from "@opentui/react/jsx-dev-runtime";
113
+ var fitValue = (value, width) => {
114
+ if (value.length <= width)
115
+ return value;
116
+ if (width <= 1)
117
+ return value.slice(value.length - 1);
118
+ return "\u2026" + value.slice(value.length - width + 1);
119
+ };
120
+ var NavigatorEmptyState = ({
121
+ emptyState,
122
+ width,
123
+ theme,
124
+ withTopSpacer
125
+ }) => /* @__PURE__ */ jsxDEV(Fragment, {
126
+ children: [
127
+ withTopSpacer && /* @__PURE__ */ jsxDEV("text", {
128
+ content: ""
129
+ }, undefined, false, undefined, this),
130
+ /* @__PURE__ */ jsxDEV("box", {
131
+ style: {
132
+ width,
133
+ flexDirection: "row",
134
+ flexWrap: "wrap",
135
+ justifyContent: "center",
136
+ gap: 1
137
+ },
138
+ children: [
139
+ /* @__PURE__ */ jsxDEV("text", {
140
+ content: emptyState.label,
141
+ wrapMode: "none",
142
+ style: { fg: theme.textMuted }
143
+ }, undefined, false, undefined, this),
144
+ /* @__PURE__ */ jsxDEV("text", {
145
+ content: `"${fitValue(emptyState.value, width)}"`,
146
+ wrapMode: "none",
147
+ style: { fg: theme.textMuted }
148
+ }, undefined, false, undefined, this)
149
+ ]
150
+ }, undefined, true, undefined, this)
151
+ ]
152
+ }, undefined, true, undefined, this);
153
+
154
+ // ../../packages/ui/src/file-navigator/row.tsx
155
+ import { jsxDEV as jsxDEV2 } from "@opentui/react/jsx-dev-runtime";
156
+ var SEPARATOR = " ";
157
+ var MIN_PARENT_BUDGET = 3;
158
+ var middleTruncate = (value, width) => {
159
+ if (width <= 0)
160
+ return "";
161
+ if (value.length <= width)
162
+ return value;
163
+ if (width <= 1)
164
+ return value.slice(0, width);
165
+ const available = width - 1;
166
+ const left = Math.ceil(available / 2);
167
+ const right = Math.floor(available / 2);
168
+ return value.slice(0, left) + "\u2026" + value.slice(value.length - right);
169
+ };
170
+ var fitTail = (value, width) => {
171
+ if (value.length <= width)
172
+ return value;
173
+ if (width <= 1)
174
+ return value.slice(0, 1);
175
+ return value.slice(0, width - 1) + "\u2026";
176
+ };
177
+ var formatFileRow = (path, width) => {
178
+ const slash = path.lastIndexOf("/");
179
+ if (slash < 0)
180
+ return { basename: fitTail(path, width), parent: "", separator: "" };
181
+ const basename = path.slice(slash + 1);
182
+ const parent = path.slice(0, slash);
183
+ if (basename.length >= width) {
184
+ return { basename: fitTail(basename, width), parent: "", separator: "" };
185
+ }
186
+ const remaining = width - basename.length - SEPARATOR.length;
187
+ if (remaining < MIN_PARENT_BUDGET || parent.length === 0) {
188
+ return { basename, parent: "", separator: "" };
189
+ }
190
+ return {
191
+ basename,
192
+ parent: middleTruncate(parent, remaining),
193
+ separator: SEPARATOR
194
+ };
195
+ };
196
+ var DefaultFileRow = ({
197
+ path,
198
+ selected,
199
+ width,
200
+ theme
201
+ }) => {
202
+ const { basename, parent, separator } = formatFileRow(path, width);
203
+ return /* @__PURE__ */ jsxDEV2("text", {
204
+ wrapMode: "none",
205
+ children: [
206
+ /* @__PURE__ */ jsxDEV2("span", {
207
+ style: { fg: selected ? theme.selectedListItemText : theme.text },
208
+ children: basename
209
+ }, undefined, false, undefined, this),
210
+ parent !== "" && /* @__PURE__ */ jsxDEV2("span", {
211
+ style: { fg: theme.textMuted },
212
+ children: `${separator}${parent}`
213
+ }, undefined, false, undefined, this)
214
+ ]
215
+ }, undefined, true, undefined, this);
216
+ };
217
+
218
+ // ../../packages/ui/src/file-navigator/FileNavigator.tsx
219
+ import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
220
+ var INLINE_BORDER_SIDES = ["top", "bottom", "right"];
221
+ var STACKED_BORDER_SIDES = ["top", "bottom"];
222
+ var INLINE_BORDER_CHARS = {
223
+ topLeft: "\u250C",
224
+ topRight: "\u252C",
225
+ bottomLeft: "\u2514",
226
+ bottomRight: "\u2534",
227
+ horizontal: "\u2500",
228
+ vertical: "\u2502",
229
+ topT: "\u252C",
230
+ bottomT: "\u2534",
231
+ leftT: "\u251C",
232
+ rightT: "\u2524",
233
+ cross: "\u253C"
234
+ };
235
+ var INACTIVE_PANE_OPACITY = 0.62;
236
+ var FileNavigator = ({
237
+ controller,
238
+ width,
239
+ paneHeight,
240
+ variant,
241
+ active,
242
+ visible,
243
+ theme,
244
+ header,
245
+ emptyState,
246
+ renderFile
247
+ }) => {
248
+ const [scroll, setScroll] = useState(0);
249
+ const hasHeader = header !== undefined && header !== null && header !== false;
250
+ const visibleRows = Math.max(1, paneHeight - 2 - (hasHeader ? 1 : 0));
251
+ const maxScroll = Math.max(0, controller.filteredFiles.length - visibleRows);
252
+ const selectedIndex = controller.selectedIndex;
253
+ const desiredScroll = (() => {
254
+ let next = scroll;
255
+ if (selectedIndex !== null) {
256
+ if (selectedIndex < next)
257
+ next = selectedIndex;
258
+ else if (selectedIndex >= next + visibleRows)
259
+ next = selectedIndex - visibleRows + 1;
260
+ }
261
+ return Math.max(0, Math.min(maxScroll, next));
262
+ })();
263
+ useEffect(() => {
264
+ if (desiredScroll !== scroll)
265
+ setScroll(desiredScroll);
266
+ }, [desiredScroll, scroll]);
267
+ const rowWidth = Math.max(4, width - (variant === "inline" ? 2 : 1));
268
+ const visibleFiles = controller.filteredFiles.slice(desiredScroll, desiredScroll + visibleRows);
269
+ if (!visible)
270
+ return null;
271
+ return /* @__PURE__ */ jsxDEV3("box", {
272
+ style: {
273
+ border: variant === "inline" ? INLINE_BORDER_SIDES : STACKED_BORDER_SIDES,
274
+ borderColor: theme.border,
275
+ width,
276
+ height: paneHeight,
277
+ ...variant === "inline" ? { flexShrink: 0 } : { flexGrow: 1, flexShrink: 1 },
278
+ flexDirection: "column",
279
+ backgroundColor: theme.backgroundPanel
280
+ },
281
+ ...variant === "inline" ? { customBorderChars: INLINE_BORDER_CHARS } : {},
282
+ children: /* @__PURE__ */ jsxDEV3("box", {
283
+ style: {
284
+ flexGrow: 1,
285
+ flexShrink: 1,
286
+ flexDirection: "column",
287
+ paddingLeft: 1,
288
+ backgroundColor: active ? theme.background : theme.backgroundPanel,
289
+ opacity: active ? 1 : INACTIVE_PANE_OPACITY
290
+ },
291
+ children: [
292
+ header,
293
+ controller.filteredFiles.length === 0 ? emptyState && /* @__PURE__ */ jsxDEV3(NavigatorEmptyState, {
294
+ emptyState,
295
+ width: rowWidth,
296
+ theme,
297
+ withTopSpacer: hasHeader
298
+ }, undefined, false, undefined, this) : visibleFiles.map((file, offset) => {
299
+ const index = desiredScroll + offset;
300
+ const selected = index === controller.selectedIndex;
301
+ return /* @__PURE__ */ jsxDEV3("box", {
302
+ style: {
303
+ width: rowWidth,
304
+ height: 1,
305
+ flexDirection: "row",
306
+ ...selected ? { backgroundColor: theme.backgroundElement } : {}
307
+ },
308
+ children: renderFile ? renderFile({ file, index, selected, width: rowWidth, theme }) : /* @__PURE__ */ jsxDEV3(DefaultFileRow, {
309
+ path: controller.getPath(file),
310
+ selected,
311
+ width: rowWidth,
312
+ theme
313
+ }, undefined, false, undefined, this)
314
+ }, controller.getId(file), false, undefined, this);
315
+ })
316
+ ]
317
+ }, undefined, true, undefined, this)
318
+ }, undefined, false, undefined, this);
319
+ };
320
+ // ../../packages/ui/src/file-navigator/useFileNavigator.ts
321
+ import { useEffect as useEffect2, useLayoutEffect, useReducer, useRef } from "react";
322
+ var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
323
+ var assertUniqueIds = (files, getId) => {
324
+ const ids = new Set;
325
+ for (const file of files) {
326
+ const id = getId(file);
327
+ if (ids.has(id)) {
328
+ throw new Error(`useFileNavigator: duplicate file ID ${JSON.stringify(id)}`);
329
+ }
330
+ ids.add(id);
331
+ }
332
+ };
333
+ var indexOfId = (files, id, getId) => files.findIndex((file) => getId(file) === id);
334
+ var reconcile = (state, filteredFiles, getId) => {
335
+ if (filteredFiles.length === 0) {
336
+ return { ...state, filteredFiles, selectedId: null };
337
+ }
338
+ if (state.autoSelect) {
339
+ return {
340
+ ...state,
341
+ filteredFiles,
342
+ selectedId: getId(filteredFiles[0]),
343
+ priorIndex: 0,
344
+ autoSelect: false
345
+ };
346
+ }
347
+ if (state.selectedId !== null) {
348
+ const retainedIndex = indexOfId(filteredFiles, state.selectedId, getId);
349
+ if (retainedIndex >= 0) {
350
+ return { ...state, filteredFiles, priorIndex: retainedIndex };
351
+ }
352
+ }
353
+ const nextIndex = state.selectedId === null ? 0 : clamp(state.priorIndex, 0, Math.max(0, filteredFiles.length - 1));
354
+ return {
355
+ ...state,
356
+ filteredFiles,
357
+ selectedId: getId(filteredFiles[nextIndex]),
358
+ priorIndex: nextIndex
359
+ };
360
+ };
361
+ var snapshotOf = (state, getId) => {
362
+ if (state.selectedId === null) {
363
+ return {
364
+ appliedQuery: state.appliedQuery,
365
+ filteredFiles: state.filteredFiles,
366
+ selectedFile: null,
367
+ selectedIndex: null
368
+ };
369
+ }
370
+ const selectedIndex = indexOfId(state.filteredFiles, state.selectedId, getId);
371
+ const selectedFile = selectedIndex < 0 ? null : state.filteredFiles[selectedIndex] ?? null;
372
+ return {
373
+ appliedQuery: state.appliedQuery,
374
+ filteredFiles: state.filteredFiles,
375
+ selectedFile,
376
+ selectedIndex: selectedFile === null ? null : selectedIndex
377
+ };
378
+ };
379
+ var initialState = (options) => {
380
+ assertUniqueIds(options.files, options.getId);
381
+ const filteredFiles = options.filter(options.files, options.query);
382
+ if (filteredFiles.length === 0) {
383
+ return {
384
+ appliedQuery: options.query,
385
+ filteredFiles,
386
+ selectedId: null,
387
+ priorIndex: 0,
388
+ autoSelect: options.query.length > 0
389
+ };
390
+ }
391
+ const initialIndex = options.query.length === 0 && options.initialSelectedId != null ? indexOfId(filteredFiles, options.initialSelectedId, options.getId) : -1;
392
+ const selectedIndex = initialIndex >= 0 ? initialIndex : 0;
393
+ return {
394
+ appliedQuery: options.query,
395
+ filteredFiles,
396
+ selectedId: options.getId(filteredFiles[selectedIndex]),
397
+ priorIndex: selectedIndex,
398
+ autoSelect: false
399
+ };
400
+ };
401
+ var useFileNavigator = ({
402
+ files,
403
+ query,
404
+ getId,
405
+ getPath,
406
+ filter,
407
+ initialSelectedId,
408
+ debounceMs = 50
409
+ }) => {
410
+ const stateRef = useRef(null);
411
+ let currentState = stateRef.current;
412
+ if (currentState === null) {
413
+ currentState = initialState({
414
+ files,
415
+ query,
416
+ getId,
417
+ getPath,
418
+ filter,
419
+ ...initialSelectedId === undefined ? {} : { initialSelectedId },
420
+ debounceMs
421
+ });
422
+ stateRef.current = currentState;
423
+ }
424
+ const [, render] = useReducer((version) => version + 1, 0);
425
+ const timerRef = useRef(null);
426
+ const filesRef = useRef(files);
427
+ const getIdRef = useRef(getId);
428
+ const getPathRef = useRef(getPath);
429
+ const filterRef = useRef(filter);
430
+ const revisionRef = useRef(0);
431
+ const renderRevision = revisionRef.current;
432
+ assertUniqueIds(files, getId);
433
+ const renderCandidate = reconcile(currentState, filter(files, currentState.appliedQuery), getId);
434
+ const cancelTimer = () => {
435
+ if (timerRef.current === null)
436
+ return;
437
+ clearTimeout(timerRef.current);
438
+ timerRef.current = null;
439
+ };
440
+ const getSnapshot = () => snapshotOf(stateRef.current, getIdRef.current);
441
+ const applySearch = (nextQuery) => {
442
+ cancelTimer();
443
+ assertUniqueIds(filesRef.current, getIdRef.current);
444
+ const current = stateRef.current;
445
+ const queryChanged = current.appliedQuery !== nextQuery;
446
+ stateRef.current = reconcile({
447
+ ...current,
448
+ appliedQuery: nextQuery,
449
+ autoSelect: queryChanged ? true : current.autoSelect
450
+ }, filterRef.current(filesRef.current, nextQuery), getIdRef.current);
451
+ revisionRef.current += 1;
452
+ render();
453
+ return getSnapshot();
454
+ };
455
+ const selectIndex = (index) => {
456
+ const current = stateRef.current;
457
+ if (current.filteredFiles.length === 0) {
458
+ stateRef.current = { ...current, selectedId: null };
459
+ } else {
460
+ const nextIndex = clamp(index, 0, current.filteredFiles.length - 1);
461
+ stateRef.current = {
462
+ ...current,
463
+ selectedId: getIdRef.current(current.filteredFiles[nextIndex]),
464
+ priorIndex: nextIndex
465
+ };
466
+ }
467
+ revisionRef.current += 1;
468
+ render();
469
+ return getSnapshot();
470
+ };
471
+ const selectId = (id) => {
472
+ const current = stateRef.current;
473
+ const index = indexOfId(current.filteredFiles, id, getIdRef.current);
474
+ if (index < 0)
475
+ return getSnapshot();
476
+ stateRef.current = { ...current, selectedId: id, priorIndex: index };
477
+ revisionRef.current += 1;
478
+ render();
479
+ return getSnapshot();
480
+ };
481
+ useLayoutEffect(() => {
482
+ filesRef.current = files;
483
+ getIdRef.current = getId;
484
+ getPathRef.current = getPath;
485
+ filterRef.current = filter;
486
+ stateRef.current = revisionRef.current === renderRevision ? renderCandidate : reconcile(stateRef.current, filter(files, stateRef.current.appliedQuery), getId);
487
+ revisionRef.current += 1;
488
+ });
489
+ useEffect2(() => {
490
+ cancelTimer();
491
+ if (query === stateRef.current.appliedQuery)
492
+ return;
493
+ timerRef.current = setTimeout(() => applySearch(query), debounceMs);
494
+ return cancelTimer;
495
+ }, [debounceMs, query]);
496
+ useEffect2(() => cancelTimer, []);
497
+ const snapshot = snapshotOf(renderCandidate, getId);
498
+ return {
499
+ ...snapshot,
500
+ getId,
501
+ getPath,
502
+ getSnapshot,
503
+ flushSearch: applySearch,
504
+ cancelAutoSelect: () => {
505
+ stateRef.current = { ...stateRef.current, autoSelect: false };
506
+ revisionRef.current += 1;
507
+ },
508
+ selectIndex,
509
+ selectId,
510
+ moveBy: (delta) => selectIndex((getSnapshot().selectedIndex ?? 0) + delta),
511
+ selectFirst: () => selectIndex(0),
512
+ selectLast: () => selectIndex(stateRef.current.filteredFiles.length - 1)
513
+ };
514
+ };
515
+ // src/Browser.tsx
102
516
  import { useKeyboard, useRenderer, useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
103
517
  import { useAtomValue, useAtomSet } from "@effect/atom-react";
104
518
  import { Effect as Effect3 } from "effect";
105
- import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useReducer, useRef as useRef2, useState as useState3 } from "react";
519
+ import { useEffect as useEffect4, useMemo as useMemo2, useReducer as useReducer2, useRef as useRef3, useState as useState4 } from "react";
106
520
 
107
521
  // src/keymap/browser.ts
108
522
  var JUMP = 8;
109
- var clamp = (n, min, max) => Math.max(min, Math.min(max, n));
110
- var lastIndex = (c) => Math.max(0, c.files.length - 1);
111
523
  var haveFiles = (c) => c.files.length > 0;
112
524
  var hasSelected = (c) => c.hasSelected;
113
- var stepBy = (c, delta) => c.setSelectedIndex((i) => clamp(i + delta, 0, lastIndex(c)));
114
525
  var inSidebar = (c) => c.focus === "sidebar";
115
526
  var filterClosed = (c) => !c.filterOpen;
116
527
  var paletteClosed = (c) => !c.paletteOpen;
@@ -215,7 +626,7 @@ var browserBindings = [
215
626
  description: "Move selection down",
216
627
  keys: ["j", "down"],
217
628
  when: inSidebarWithFiles,
218
- run: (c) => stepBy(c, 1)
629
+ run: (c) => c.moveSelectionBy(1)
219
630
  },
220
631
  {
221
632
  id: "sidebar.up",
@@ -223,7 +634,7 @@ var browserBindings = [
223
634
  description: "Move selection up",
224
635
  keys: ["k", "up"],
225
636
  when: inSidebarWithFiles,
226
- run: (c) => stepBy(c, -1)
637
+ run: (c) => c.moveSelectionBy(-1)
227
638
  },
228
639
  {
229
640
  id: "sidebar.jumpDown",
@@ -231,7 +642,7 @@ var browserBindings = [
231
642
  description: `Jump down ${JUMP}`,
232
643
  keys: ["shift+j"],
233
644
  when: inSidebarWithFiles,
234
- run: (c) => stepBy(c, JUMP)
645
+ run: (c) => c.moveSelectionBy(JUMP)
235
646
  },
236
647
  {
237
648
  id: "sidebar.jumpUp",
@@ -239,7 +650,7 @@ var browserBindings = [
239
650
  description: `Jump up ${JUMP}`,
240
651
  keys: ["shift+k"],
241
652
  when: inSidebarWithFiles,
242
- run: (c) => stepBy(c, -JUMP)
653
+ run: (c) => c.moveSelectionBy(-JUMP)
243
654
  },
244
655
  {
245
656
  id: "sidebar.pageDown",
@@ -247,7 +658,7 @@ var browserBindings = [
247
658
  description: "Page down",
248
659
  keys: ["space", "pagedown", "ctrl+d"],
249
660
  when: inSidebarWithFiles,
250
- run: (c) => stepBy(c, JUMP)
661
+ run: (c) => c.moveSelectionBy(JUMP)
251
662
  },
252
663
  {
253
664
  id: "sidebar.pageUp",
@@ -255,7 +666,7 @@ var browserBindings = [
255
666
  description: "Page up",
256
667
  keys: ["b", "pageup", "ctrl+u"],
257
668
  when: inSidebarWithFiles,
258
- run: (c) => stepBy(c, -JUMP)
669
+ run: (c) => c.moveSelectionBy(-JUMP)
259
670
  },
260
671
  {
261
672
  id: "sidebar.top",
@@ -263,7 +674,7 @@ var browserBindings = [
263
674
  description: "Jump to first file",
264
675
  keys: ["g"],
265
676
  when: inSidebarWithFiles,
266
- run: (c) => c.setSelectedIndex(() => 0)
677
+ run: (c) => c.selectFirst()
267
678
  },
268
679
  {
269
680
  id: "sidebar.bottom",
@@ -271,7 +682,7 @@ var browserBindings = [
271
682
  description: "Jump to last file",
272
683
  keys: ["shift+g"],
273
684
  when: inSidebarWithFiles,
274
- run: (c) => c.setSelectedIndex(() => lastIndex(c))
685
+ run: (c) => c.selectLast()
275
686
  },
276
687
  {
277
688
  id: "sidebar.open",
@@ -303,7 +714,7 @@ var browserBindings = [
303
714
  description: "Prev file",
304
715
  keys: ["["],
305
716
  when: inReaderWithSibling,
306
- run: (c) => stepBy(c, -1)
717
+ run: (c) => c.moveSelectionBy(-1)
307
718
  },
308
719
  {
309
720
  id: "reader.nextFile",
@@ -311,7 +722,7 @@ var browserBindings = [
311
722
  description: "Next file",
312
723
  keys: ["]"],
313
724
  when: inReaderWithSibling,
314
- run: (c) => stepBy(c, 1)
725
+ run: (c) => c.moveSelectionBy(1)
315
726
  },
316
727
  {
317
728
  id: "reader.back",
@@ -614,7 +1025,7 @@ var setActiveTheme = (definition, tone) => {
614
1025
  };
615
1026
 
616
1027
  // src/PromptRow.tsx
617
- import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
1028
+ import { jsxDEV as jsxDEV4 } from "@opentui/react/jsx-dev-runtime";
618
1029
  var PREFIX = "> ";
619
1030
  var CURSOR = "\u258F";
620
1031
  var PromptRow = ({ query, editing, placeholder = "", width }) => {
@@ -623,14 +1034,14 @@ var PromptRow = ({ query, editing, placeholder = "", width }) => {
623
1034
  const bodyFg = editing ? colors.primary : query.length > 0 ? colors.text : colors.textMuted;
624
1035
  const body = rawBody.length <= bodyBudget ? rawBody : editing ? "\u2026" + rawBody.slice(rawBody.length - bodyBudget + 1) : rawBody.slice(0, bodyBudget - 1) + "\u2026";
625
1036
  const prefixFg = editing ? colors.secondary : colors.primary;
626
- return /* @__PURE__ */ jsxDEV("text", {
1037
+ return /* @__PURE__ */ jsxDEV4("text", {
627
1038
  wrapMode: "none",
628
1039
  children: [
629
- /* @__PURE__ */ jsxDEV("span", {
1040
+ /* @__PURE__ */ jsxDEV4("span", {
630
1041
  style: { fg: prefixFg },
631
1042
  children: PREFIX
632
1043
  }, undefined, false, undefined, this),
633
- /* @__PURE__ */ jsxDEV("span", {
1044
+ /* @__PURE__ */ jsxDEV4("span", {
634
1045
  style: { fg: bodyFg },
635
1046
  children: body
636
1047
  }, undefined, false, undefined, this)
@@ -639,7 +1050,7 @@ var PromptRow = ({ query, editing, placeholder = "", width }) => {
639
1050
  };
640
1051
 
641
1052
  // src/CommandPalette.tsx
642
- import { jsxDEV as jsxDEV2 } from "@opentui/react/jsx-dev-runtime";
1053
+ import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
643
1054
  var SCRIM = RGBA.fromInts(0, 0, 0, 150);
644
1055
  var FOOTER_HINT = "\u2191\u2193 select enter run esc close";
645
1056
  var CATEGORY_ORDER = ["Navigation", "View", "File", "Appearance", "App"];
@@ -709,7 +1120,7 @@ var CommandPalette = ({
709
1120
  const titleWidth = Math.max(8, rowWidth - 2 - SHORTCUT_WIDTH - 1);
710
1121
  const fit = (s, width) => s.length === width ? s : s.length > width ? s.slice(0, Math.max(0, width - 1)) + "\u2026" : s + " ".repeat(width - s.length);
711
1122
  const fitRight = (s, width) => s.length >= width ? s.slice(0, width) : " ".repeat(width - s.length) + s;
712
- return /* @__PURE__ */ jsxDEV2("box", {
1123
+ return /* @__PURE__ */ jsxDEV5("box", {
713
1124
  position: "absolute",
714
1125
  left: 0,
715
1126
  top: 0,
@@ -717,7 +1128,7 @@ var CommandPalette = ({
717
1128
  height: viewportHeight,
718
1129
  zIndex: 20,
719
1130
  style: { backgroundColor: SCRIM },
720
- children: /* @__PURE__ */ jsxDEV2("box", {
1131
+ children: /* @__PURE__ */ jsxDEV5("box", {
721
1132
  position: "absolute",
722
1133
  left,
723
1134
  top,
@@ -734,26 +1145,26 @@ var CommandPalette = ({
734
1145
  backgroundColor: colors.backgroundPanel
735
1146
  },
736
1147
  children: [
737
- /* @__PURE__ */ jsxDEV2(PromptRow, {
1148
+ /* @__PURE__ */ jsxDEV5(PromptRow, {
738
1149
  query,
739
1150
  editing: true,
740
1151
  width: rowWidth
741
1152
  }, undefined, false, undefined, this),
742
- /* @__PURE__ */ jsxDEV2("text", {
1153
+ /* @__PURE__ */ jsxDEV5("text", {
743
1154
  content: " "
744
1155
  }, undefined, false, undefined, this),
745
- commands.length === 0 ? /* @__PURE__ */ jsxDEV2("text", {
1156
+ commands.length === 0 ? /* @__PURE__ */ jsxDEV5("text", {
746
1157
  wrapMode: "none",
747
1158
  content: " (no matches)",
748
1159
  style: { fg: colors.textMuted }
749
1160
  }, undefined, false, undefined, this) : visible.map((row) => {
750
1161
  if (row.kind === "spacer") {
751
- return /* @__PURE__ */ jsxDEV2("text", {
1162
+ return /* @__PURE__ */ jsxDEV5("text", {
752
1163
  content: " "
753
1164
  }, row.key, false, undefined, this);
754
1165
  }
755
1166
  if (row.kind === "header") {
756
- return /* @__PURE__ */ jsxDEV2("text", {
1167
+ return /* @__PURE__ */ jsxDEV5("text", {
757
1168
  wrapMode: "none",
758
1169
  content: fit(row.text, rowWidth),
759
1170
  style: { fg: colors.textMuted, attributes: 1 }
@@ -765,25 +1176,25 @@ var CommandPalette = ({
765
1176
  const titleText = fit(cmd.title, titleWidth);
766
1177
  const shortcutText = cmd.shortcut ? fitRight(cmd.shortcut, SHORTCUT_WIDTH) : " ".repeat(SHORTCUT_WIDTH);
767
1178
  const titleFg = isSelected ? colors.primary : colors.text;
768
- return /* @__PURE__ */ jsxDEV2("text", {
1179
+ return /* @__PURE__ */ jsxDEV5("text", {
769
1180
  wrapMode: "none",
770
1181
  style: isSelected ? { bg: colors.backgroundElement } : {},
771
1182
  children: [
772
- /* @__PURE__ */ jsxDEV2("span", {
1183
+ /* @__PURE__ */ jsxDEV5("span", {
773
1184
  style: { fg: titleFg },
774
1185
  children: `${selector}${titleText} `
775
1186
  }, undefined, false, undefined, this),
776
- /* @__PURE__ */ jsxDEV2("span", {
1187
+ /* @__PURE__ */ jsxDEV5("span", {
777
1188
  style: { fg: colors.textMuted },
778
1189
  children: shortcutText
779
1190
  }, undefined, false, undefined, this)
780
1191
  ]
781
1192
  }, cmd.id, true, undefined, this);
782
1193
  }),
783
- /* @__PURE__ */ jsxDEV2("text", {
1194
+ /* @__PURE__ */ jsxDEV5("text", {
784
1195
  content: " "
785
1196
  }, undefined, false, undefined, this),
786
- /* @__PURE__ */ jsxDEV2("text", {
1197
+ /* @__PURE__ */ jsxDEV5("text", {
787
1198
  wrapMode: "none",
788
1199
  content: fit(FOOTER_HINT, rowWidth),
789
1200
  style: { fg: colors.textMuted }
@@ -951,8 +1362,8 @@ var displayKey = (raw) => {
951
1362
  };
952
1363
 
953
1364
  // src/Spinner.tsx
954
- import { useEffect, useRef, useState } from "react";
955
- import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
1365
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
1366
+ import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
956
1367
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
957
1368
  var FRAME_COUNT = FRAMES.length;
958
1369
  var normalizeFrameIndex = (index) => (index % FRAME_COUNT + FRAME_COUNT) % FRAME_COUNT;
@@ -962,12 +1373,12 @@ var Spinner = ({
962
1373
  initialFrameIndex = 0,
963
1374
  registerTick = null
964
1375
  }) => {
965
- const [index, setIndex] = useState(() => normalizeFrameIndex(initialFrameIndex));
966
- const tickRef = useRef(() => {
1376
+ const [index, setIndex] = useState2(() => normalizeFrameIndex(initialFrameIndex));
1377
+ const tickRef = useRef2(() => {
967
1378
  return;
968
1379
  });
969
1380
  tickRef.current = () => setIndex((prev) => (prev + 1) % FRAME_COUNT);
970
- useEffect(() => {
1381
+ useEffect3(() => {
971
1382
  if (registerTick) {
972
1383
  registerTick(() => tickRef.current());
973
1384
  return;
@@ -977,7 +1388,7 @@ var Spinner = ({
977
1388
  }, intervalMs);
978
1389
  return () => clearInterval(id);
979
1390
  }, [intervalMs, registerTick]);
980
- return /* @__PURE__ */ jsxDEV3("text", {
1391
+ return /* @__PURE__ */ jsxDEV6("text", {
981
1392
  content: FRAMES[index] ?? FRAMES[0],
982
1393
  wrapMode: "none",
983
1394
  style: { fg }
@@ -985,7 +1396,7 @@ var Spinner = ({
985
1396
  };
986
1397
 
987
1398
  // src/StatusIndicator.tsx
988
- import { jsxDEV as jsxDEV4 } from "@opentui/react/jsx-dev-runtime";
1399
+ import { jsxDEV as jsxDEV7 } from "@opentui/react/jsx-dev-runtime";
989
1400
  var statusIndicatorFg = (variant) => {
990
1401
  switch (variant) {
991
1402
  case "info":
@@ -1006,7 +1417,7 @@ var StatusIndicator = ({
1006
1417
  }) => {
1007
1418
  const activeColor = statusIndicatorFg(variant);
1008
1419
  const backgroundColor = active ? activeColor : colors.backgroundElement;
1009
- return /* @__PURE__ */ jsxDEV4("box", {
1420
+ return /* @__PURE__ */ jsxDEV7("box", {
1010
1421
  ...onMouseUp === undefined ? {} : { onMouseUp },
1011
1422
  style: {
1012
1423
  width: 3,
@@ -1014,7 +1425,7 @@ var StatusIndicator = ({
1014
1425
  flexDirection: "row",
1015
1426
  backgroundColor
1016
1427
  },
1017
- children: /* @__PURE__ */ jsxDEV4("text", {
1428
+ children: /* @__PURE__ */ jsxDEV7("text", {
1018
1429
  content: ` ${icon} `,
1019
1430
  wrapMode: "none",
1020
1431
  style: {
@@ -1027,7 +1438,7 @@ var StatusIndicator = ({
1027
1438
  };
1028
1439
 
1029
1440
  // src/Footer.tsx
1030
- import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
1441
+ import { jsxDEV as jsxDEV8 } from "@opentui/react/jsx-dev-runtime";
1031
1442
  var FOOTER_HEIGHT = 1;
1032
1443
  var normalizeStatusLine = (status) => status.replace(/\s+/g, " ").trim();
1033
1444
  var HINT_SEPARATOR = " ";
@@ -1117,17 +1528,17 @@ var Footer = ({
1117
1528
  const statusContent = status ? status.slice(0, Math.max(0, statusBudget - statusChromeWidth)) : "";
1118
1529
  const statusSeparatorContent = status ? STATUS_SEPARATOR.slice(0, Math.max(0, statusBudget - nonWarningStatusPrefixBudget - statusContent.length)) : "";
1119
1530
  const noticeContent = notice ? notice.length > contentBudget ? notice.slice(0, contentBudget) : notice : null;
1120
- const renderIndicators = () => renderedIndicators.map(({ id, ...props }) => /* @__PURE__ */ jsxDEV5(StatusIndicator, {
1531
+ const renderIndicators = () => renderedIndicators.map(({ id, ...props }) => /* @__PURE__ */ jsxDEV8(StatusIndicator, {
1121
1532
  ...props
1122
1533
  }, id, false, undefined, this));
1123
- const renderIndicatorContentGap = () => indicatorContentGapBudget > 0 ? /* @__PURE__ */ jsxDEV5("text", {
1534
+ const renderIndicatorContentGap = () => indicatorContentGapBudget > 0 ? /* @__PURE__ */ jsxDEV8("text", {
1124
1535
  content: " ",
1125
1536
  wrapMode: "none",
1126
1537
  style: { fg: colors.textMuted }
1127
1538
  }, undefined, false, undefined, this) : null;
1128
1539
  const renderHints = () => visibleHints.flatMap((h, i) => {
1129
1540
  const sep = i > 0 ? [
1130
- /* @__PURE__ */ jsxDEV5("text", {
1541
+ /* @__PURE__ */ jsxDEV8("text", {
1131
1542
  content: HINT_SEPARATOR,
1132
1543
  wrapMode: "none",
1133
1544
  style: { fg: colors.textMuted }
@@ -1135,12 +1546,12 @@ var Footer = ({
1135
1546
  ] : [];
1136
1547
  return [
1137
1548
  ...sep,
1138
- /* @__PURE__ */ jsxDEV5("text", {
1549
+ /* @__PURE__ */ jsxDEV8("text", {
1139
1550
  content: h.key,
1140
1551
  wrapMode: "none",
1141
1552
  style: { fg: colors.text }
1142
1553
  }, `k${i}`, false, undefined, this),
1143
- /* @__PURE__ */ jsxDEV5("text", {
1554
+ /* @__PURE__ */ jsxDEV8("text", {
1144
1555
  content: ` ${h.label}`,
1145
1556
  wrapMode: "none",
1146
1557
  style: { fg: colors.textMuted }
@@ -1148,12 +1559,12 @@ var Footer = ({
1148
1559
  ];
1149
1560
  });
1150
1561
  if (noticeContent !== null) {
1151
- return /* @__PURE__ */ jsxDEV5("box", {
1562
+ return /* @__PURE__ */ jsxDEV8("box", {
1152
1563
  style: rowStyle,
1153
1564
  children: [
1154
1565
  renderIndicators(),
1155
1566
  renderIndicatorContentGap(),
1156
- /* @__PURE__ */ jsxDEV5("text", {
1567
+ /* @__PURE__ */ jsxDEV8("text", {
1157
1568
  content: noticeContent,
1158
1569
  wrapMode: "none",
1159
1570
  style: { fg: colors.primary }
@@ -1168,25 +1579,25 @@ var Footer = ({
1168
1579
  ...discoverySpinnerInitialFrameIndex === undefined ? {} : { initialFrameIndex: discoverySpinnerInitialFrameIndex },
1169
1580
  ...discoverySpinnerRegisterTick === undefined ? {} : { registerTick: discoverySpinnerRegisterTick }
1170
1581
  };
1171
- return /* @__PURE__ */ jsxDEV5("box", {
1582
+ return /* @__PURE__ */ jsxDEV8("box", {
1172
1583
  style: rowStyle,
1173
1584
  children: [
1174
1585
  renderIndicators(),
1175
1586
  renderIndicatorContentGap(),
1176
- nonWarningStatusPrefixBudget >= 1 ? /* @__PURE__ */ jsxDEV5(Spinner, {
1587
+ nonWarningStatusPrefixBudget >= 1 ? /* @__PURE__ */ jsxDEV8(Spinner, {
1177
1588
  ...spinnerProps
1178
1589
  }, undefined, false, undefined, this) : null,
1179
- nonWarningStatusPrefixBudget >= 2 ? /* @__PURE__ */ jsxDEV5("text", {
1590
+ nonWarningStatusPrefixBudget >= 2 ? /* @__PURE__ */ jsxDEV8("text", {
1180
1591
  content: " ",
1181
1592
  wrapMode: "none",
1182
1593
  style: { fg: colors.textMuted }
1183
1594
  }, undefined, false, undefined, this) : null,
1184
- isPartialWarning ? null : /* @__PURE__ */ jsxDEV5("text", {
1595
+ isPartialWarning ? null : /* @__PURE__ */ jsxDEV8("text", {
1185
1596
  content: statusContent,
1186
1597
  wrapMode: "none",
1187
1598
  style: { fg: colors.secondary }
1188
1599
  }, undefined, false, undefined, this),
1189
- statusSeparatorContent.length > 0 && /* @__PURE__ */ jsxDEV5("text", {
1600
+ statusSeparatorContent.length > 0 && /* @__PURE__ */ jsxDEV8("text", {
1190
1601
  content: statusSeparatorContent,
1191
1602
  wrapMode: "none",
1192
1603
  style: { fg: colors.textMuted }
@@ -1195,7 +1606,7 @@ var Footer = ({
1195
1606
  ]
1196
1607
  }, undefined, true, undefined, this);
1197
1608
  }
1198
- return /* @__PURE__ */ jsxDEV5("box", {
1609
+ return /* @__PURE__ */ jsxDEV8("box", {
1199
1610
  style: rowStyle,
1200
1611
  children: [
1201
1612
  renderIndicators(),
@@ -1206,7 +1617,7 @@ var Footer = ({
1206
1617
  };
1207
1618
 
1208
1619
  // src/ui/middleTruncate.ts
1209
- var middleTruncate = (value, width, options = {}) => {
1620
+ var middleTruncate2 = (value, width, options = {}) => {
1210
1621
  const ellipsis = options.ellipsis ?? "\u2026";
1211
1622
  if (width <= 0)
1212
1623
  return "";
@@ -1221,7 +1632,7 @@ var middleTruncate = (value, width, options = {}) => {
1221
1632
  };
1222
1633
 
1223
1634
  // src/Header.tsx
1224
- import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
1635
+ import { jsxDEV as jsxDEV9 } from "@opentui/react/jsx-dev-runtime";
1225
1636
  var HEADER_HEIGHT = 1;
1226
1637
  var FILE_SEPARATOR = " \xB7 ";
1227
1638
  var HEADER_HORIZONTAL_PADDING = 2;
@@ -1248,7 +1659,7 @@ var layoutHeaderSegments = ({
1248
1659
  const rootWidth = rootSegment === null ? 0 : usableWidth - joinedLength(segments) - FILE_SEPARATOR.length - HEADER_GROUP_GAP - right.length;
1249
1660
  if (root !== null && root.length > rootWidth && rootWidth >= MIN_TRUNCATED_ROOT_WIDTH) {
1250
1661
  return {
1251
- left: [...segments, { id: "root", text: middleTruncate(root, rootWidth), tone: "muted" }],
1662
+ left: [...segments, { id: "root", text: middleTruncate2(root, rootWidth), tone: "muted" }],
1252
1663
  right
1253
1664
  };
1254
1665
  }
@@ -1263,7 +1674,7 @@ var layoutHeaderSegments = ({
1263
1674
  const fileWidth = usableWidth - BRAND.length - FILE_SEPARATOR.length;
1264
1675
  if (fileWidth > 0) {
1265
1676
  return {
1266
- left: [iconBrand, { id: "file", text: middleTruncate(file, fileWidth), tone: "primary" }],
1677
+ left: [iconBrand, { id: "file", text: middleTruncate2(file, fileWidth), tone: "primary" }],
1267
1678
  right: null
1268
1679
  };
1269
1680
  }
@@ -1277,7 +1688,7 @@ var fits = (segments, right, usableWidth) => {
1277
1688
  };
1278
1689
  var Header = ({ width, currentFile, rootLabel, version = package_default.version }) => {
1279
1690
  const layout = layoutHeaderSegments({ width, currentFile, rootLabel, version });
1280
- return /* @__PURE__ */ jsxDEV6("box", {
1691
+ return /* @__PURE__ */ jsxDEV9("box", {
1281
1692
  style: {
1282
1693
  width,
1283
1694
  height: HEADER_HEIGHT,
@@ -1289,20 +1700,20 @@ var Header = ({ width, currentFile, rootLabel, version = package_default.version
1289
1700
  backgroundColor: colors.backgroundPanel
1290
1701
  },
1291
1702
  children: [
1292
- /* @__PURE__ */ jsxDEV6("text", {
1703
+ /* @__PURE__ */ jsxDEV9("text", {
1293
1704
  wrapMode: "none",
1294
1705
  children: layout.left.map((segment, idx) => [
1295
- idx > 0 ? /* @__PURE__ */ jsxDEV6("span", {
1706
+ idx > 0 ? /* @__PURE__ */ jsxDEV9("span", {
1296
1707
  style: { fg: colors.textMuted },
1297
1708
  children: FILE_SEPARATOR
1298
1709
  }, `${segment.id}-${idx}-separator`, false, undefined, this) : null,
1299
- /* @__PURE__ */ jsxDEV6("span", {
1710
+ /* @__PURE__ */ jsxDEV9("span", {
1300
1711
  style: { fg: segment.tone === "primary" ? colors.text : colors.textMuted },
1301
1712
  children: segment.text
1302
1713
  }, `${segment.id}-${idx}`, false, undefined, this)
1303
1714
  ])
1304
1715
  }, undefined, false, undefined, this),
1305
- layout.right !== null && /* @__PURE__ */ jsxDEV6("text", {
1716
+ layout.right !== null && /* @__PURE__ */ jsxDEV9("text", {
1306
1717
  content: layout.right,
1307
1718
  wrapMode: "none",
1308
1719
  style: { fg: colors.textMuted }
@@ -1509,55 +1920,56 @@ var resolveSidebarWidth = (viewport, preferred) => {
1509
1920
  var defaultPreferredWidth = (viewport) => Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, Math.floor(viewport * 0.25)));
1510
1921
  var canFitInline = (viewport) => viewport >= SIDEBAR_MIN_WIDTH + DIVIDER_WIDTH + READER_MIN_WIDTH;
1511
1922
 
1512
- // src/layout/sidebarEmptyState.ts
1513
- var HEAD_ELISION_PREFIX = "\u2026";
1514
- var fitSidebarEmptyValue = (value, width) => {
1515
- if (value.length <= width)
1516
- return value;
1517
- if (width <= 1)
1518
- return value.slice(value.length - 1);
1519
- return HEAD_ELISION_PREFIX + value.slice(value.length - width + 1);
1520
- };
1521
-
1522
- // src/layout/sidebarRow.ts
1523
- var SIDEBAR_ROW_SEPARATOR = " ";
1524
- var MIN_PARENT_BUDGET = 3;
1525
- var formatSidebarRow = (relativePath, totalWidth) => {
1526
- const slash = relativePath.lastIndexOf("/");
1527
- if (slash < 0) {
1528
- return { basename: fitTail(relativePath, totalWidth), separator: "", parent: "" };
1529
- }
1530
- const basename = relativePath.slice(slash + 1);
1531
- const parentFull = relativePath.slice(0, slash);
1532
- if (basename.length >= totalWidth) {
1533
- return { basename: fitTail(basename, totalWidth), separator: "", parent: "" };
1534
- }
1535
- const remaining = totalWidth - basename.length - SIDEBAR_ROW_SEPARATOR.length;
1536
- if (remaining < MIN_PARENT_BUDGET || parentFull.length === 0) {
1537
- return { basename, separator: "", parent: "" };
1538
- }
1539
- if (parentFull.length <= remaining) {
1540
- return row(basename, parentFull);
1541
- }
1542
- return row(basename, middleTruncate(parentFull, remaining));
1543
- };
1544
- var row = (basename, parent) => ({
1545
- basename,
1546
- separator: SIDEBAR_ROW_SEPARATOR,
1547
- parent
1548
- });
1549
- var fitTail = (s, width) => {
1550
- if (s.length <= width)
1551
- return s;
1552
- if (width <= 1)
1553
- return s.slice(0, 1);
1554
- return s.slice(0, width - 1) + "\u2026";
1923
+ // src/Sidebar.tsx
1924
+ import { jsxDEV as jsxDEV10 } from "@opentui/react/jsx-dev-runtime";
1925
+ var Sidebar = ({
1926
+ files,
1927
+ controller,
1928
+ filterInput,
1929
+ filterOpen,
1930
+ discoveryActive,
1931
+ rootLabel,
1932
+ viewportHeight,
1933
+ paneWidth,
1934
+ narrow,
1935
+ active,
1936
+ visible
1937
+ }) => {
1938
+ const headerVisible = files.length > 0 || discoveryActive;
1939
+ const emptyState = controller.filteredFiles.length > 0 ? undefined : files.length === 0 ? discoveryActive ? { label: "Scanning", value: "\u2026" } : { label: "No markdown files in", value: rootLabel } : { label: "No files match", value: controller.appliedQuery };
1940
+ const rowWidth = Math.max(4, paneWidth - (narrow ? 1 : 2));
1941
+ return /* @__PURE__ */ jsxDEV10(FileNavigator, {
1942
+ controller,
1943
+ width: paneWidth,
1944
+ paneHeight: Math.max(1, viewportHeight - HEADER_HEIGHT - FOOTER_HEIGHT),
1945
+ variant: narrow ? "stacked" : "inline",
1946
+ active,
1947
+ visible,
1948
+ theme: {
1949
+ background: colors.background,
1950
+ backgroundPanel: colors.backgroundPanel,
1951
+ backgroundElement: colors.backgroundElement,
1952
+ text: colors.text,
1953
+ textMuted: colors.textMuted,
1954
+ border: colors.border,
1955
+ selectedListItemText: colors.selectedListItemText
1956
+ },
1957
+ ...headerVisible ? {
1958
+ header: /* @__PURE__ */ jsxDEV10(PromptRow, {
1959
+ query: filterInput,
1960
+ editing: filterOpen,
1961
+ placeholder: "type / to filter",
1962
+ width: rowWidth
1963
+ }, undefined, false, undefined, this)
1964
+ } : {},
1965
+ ...emptyState === undefined ? {} : { emptyState }
1966
+ }, undefined, false, undefined, this);
1555
1967
  };
1556
1968
 
1557
1969
  // src/StatusPopover.tsx
1558
1970
  import { useTerminalDimensions } from "@opentui/react";
1559
- import { useMemo, useState as useState2 } from "react";
1560
- import { jsxDEV as jsxDEV7, Fragment } from "@opentui/react/jsx-dev-runtime";
1971
+ import { useMemo, useState as useState3 } from "react";
1972
+ import { jsxDEV as jsxDEV11, Fragment as Fragment2 } from "@opentui/react/jsx-dev-runtime";
1561
1973
  var clamp2 = (n, min, max) => Math.max(min, Math.min(max, n));
1562
1974
  var measureLine = (line) => line.length;
1563
1975
  var wrapLine = (line, width) => {
@@ -1595,7 +2007,7 @@ var StatusPopoverPanel = ({
1595
2007
  while (linesToRender.length < popoverHeight - 2)
1596
2008
  linesToRender.push("");
1597
2009
  const borderColor = statusIndicatorFg(variant);
1598
- return /* @__PURE__ */ jsxDEV7("box", {
2010
+ return /* @__PURE__ */ jsxDEV11("box", {
1599
2011
  position: "absolute",
1600
2012
  left: 1,
1601
2013
  top: Math.max(0, viewportHeight - popoverHeight - 2),
@@ -1609,15 +2021,15 @@ var StatusPopoverPanel = ({
1609
2021
  flexDirection: "column"
1610
2022
  },
1611
2023
  children: [
1612
- /* @__PURE__ */ jsxDEV7("text", {
2024
+ /* @__PURE__ */ jsxDEV11("text", {
1613
2025
  content: ""
1614
2026
  }, undefined, false, undefined, this),
1615
- linesToRender.map((line, i) => /* @__PURE__ */ jsxDEV7("text", {
2027
+ linesToRender.map((line, i) => /* @__PURE__ */ jsxDEV11("text", {
1616
2028
  content: line,
1617
2029
  wrapMode: "none",
1618
2030
  style: { fg: colors.text }
1619
2031
  }, i, false, undefined, this)),
1620
- /* @__PURE__ */ jsxDEV7("text", {
2032
+ /* @__PURE__ */ jsxDEV11("text", {
1621
2033
  content: ""
1622
2034
  }, undefined, false, undefined, this)
1623
2035
  ]
@@ -8362,8 +8774,10 @@ var saveThemePreference = async (record, path = defaultConfigPath()) => {
8362
8774
  };
8363
8775
 
8364
8776
  // src/Browser.tsx
8365
- import { jsxDEV as jsxDEV8, Fragment as Fragment2 } from "@opentui/react/jsx-dev-runtime";
8777
+ import { jsxDEV as jsxDEV12 } from "@opentui/react/jsx-dev-runtime";
8366
8778
  var defaultReadFile = (path) => Effect3.runPromise(readFileText(path));
8779
+ var getFileId = (file) => file.path;
8780
+ var getFilePath = (file) => file.relativePath;
8367
8781
  var clamp3 = (n, min, max) => Math.max(min, Math.min(max, n));
8368
8782
  var nextReaderEmptyStateTipRotation = 0;
8369
8783
  var FILTER_DEBOUNCE_MS = 50;
@@ -8382,43 +8796,6 @@ var floatingOverlayReducer = (state, action) => {
8382
8796
  return state.kind === "status-popover" ? { kind: "status-popover", content: action.content } : state;
8383
8797
  }
8384
8798
  };
8385
- var SidebarEmptyMessage = ({
8386
- label,
8387
- value,
8388
- width,
8389
- withTopSpacer
8390
- }) => /* @__PURE__ */ jsxDEV8(Fragment2, {
8391
- children: [
8392
- withTopSpacer && /* @__PURE__ */ jsxDEV8("text", {
8393
- content: ""
8394
- }, undefined, false, undefined, this),
8395
- /* @__PURE__ */ jsxDEV8("box", {
8396
- style: {
8397
- width,
8398
- flexDirection: "row",
8399
- flexWrap: "wrap",
8400
- justifyContent: "center",
8401
- gap: 1
8402
- },
8403
- children: [
8404
- /* @__PURE__ */ jsxDEV8("text", {
8405
- wrapMode: "none",
8406
- children: /* @__PURE__ */ jsxDEV8("span", {
8407
- style: { fg: colors.textMuted },
8408
- children: label
8409
- }, undefined, false, undefined, this)
8410
- }, undefined, false, undefined, this),
8411
- /* @__PURE__ */ jsxDEV8("text", {
8412
- wrapMode: "none",
8413
- children: /* @__PURE__ */ jsxDEV8("span", {
8414
- style: { fg: colors.textMuted },
8415
- children: `"${fitSidebarEmptyValue(value, width)}"`
8416
- }, undefined, false, undefined, this)
8417
- }, undefined, false, undefined, this)
8418
- ]
8419
- }, undefined, true, undefined, this)
8420
- ]
8421
- }, undefined, true, undefined, this);
8422
8799
  var Browser = ({
8423
8800
  files,
8424
8801
  initialIndex = 0,
@@ -8447,20 +8824,29 @@ var Browser = ({
8447
8824
  const theme = useAtomValue(themeAtom);
8448
8825
  const setTheme = useAtomSet(themeAtom);
8449
8826
  const syntaxStyle = useMemo2(() => SyntaxStyle.fromStyles(colors.syntax), [theme]);
8450
- const [selectedIndex, setSelectedIndex] = useState3(() => clamp3(initialIndex, 0, Math.max(0, files.length - 1)));
8451
- const [wrapEnabled, setWrapEnabled] = useState3(initialWrap);
8452
- const [loaded, setLoaded] = useState3(null);
8453
- const [error, setError] = useState3(null);
8454
- const [shown, setShown] = useState3(true);
8827
+ const [wrapEnabled, setWrapEnabled] = useState4(initialWrap);
8828
+ const [loaded, setLoaded] = useState4(null);
8829
+ const [error, setError] = useState4(null);
8830
+ const [shown, setShown] = useState4(true);
8455
8831
  const startInFilter = startupFocus === "filter";
8456
8832
  const initialFocus = startupFocus === null ? shown ? "sidebar" : "reader" : startupFocus === "reader" ? "reader" : "sidebar";
8457
- const [focus, setFocus] = useState3(() => initialFocus);
8458
- const [sidebarScroll, setSidebarScroll] = useState3(0);
8459
- const [filterOpen, setFilterOpen] = useState3(startInFilter);
8460
- const [filterInput, setFilterInput] = useState3(initialQuery);
8461
- const [filterApplied, setFilterApplied] = useState3(initialQuery);
8462
- const [floatingOverlay, dispatchFloatingOverlayState] = useReducer(floatingOverlayReducer, noFloatingOverlay);
8463
- const floatingOverlayRef = useRef2(noFloatingOverlay);
8833
+ const [focus, setFocus] = useState4(() => initialFocus);
8834
+ const [filterOpen, setFilterOpen] = useState4(startInFilter);
8835
+ const [filterInput, setFilterInput] = useState4(initialQuery);
8836
+ const initialSelectedIdRef = useRef3(files.length === 0 ? null : files[clamp3(initialIndex, 0, Math.max(0, files.length - 1))]?.path ?? null);
8837
+ const navigator = useFileNavigator({
8838
+ files,
8839
+ query: filterInput,
8840
+ getId: getFileId,
8841
+ getPath: getFilePath,
8842
+ filter: filterFiles,
8843
+ initialSelectedId: initialSelectedIdRef.current,
8844
+ debounceMs: filterDebounceMs
8845
+ });
8846
+ const displayedFiles = navigator.filteredFiles;
8847
+ const selected = navigator.selectedFile;
8848
+ const [floatingOverlay, dispatchFloatingOverlayState] = useReducer2(floatingOverlayReducer, noFloatingOverlay);
8849
+ const floatingOverlayRef = useRef3(noFloatingOverlay);
8464
8850
  const dispatchFloatingOverlay = (action) => {
8465
8851
  const next = floatingOverlayReducer(floatingOverlayRef.current, action);
8466
8852
  floatingOverlayRef.current = next;
@@ -8470,29 +8856,27 @@ var Browser = ({
8470
8856
  const paletteOpen = floatingOverlay.kind === "command-palette";
8471
8857
  const activeStatusPopover = floatingOverlay.kind === "status-popover" ? floatingOverlay : null;
8472
8858
  const discoveryWarningStatus = isPartialDiscoveryWarning2(discoveryStatus) ? discoveryStatus : null;
8473
- const [paletteQuery, setPaletteQuery] = useState3("");
8474
- const [paletteIndex, setPaletteIndex] = useState3(0);
8475
- const paletteQueryRef = useRef2("");
8476
- const paletteIndexRef = useRef2(0);
8477
- const [readerEmptyStateTipRotation, setReaderEmptyStateTipRotation] = useState3(() => nextReaderEmptyStateTipRotation);
8478
- const readerEmptyStateVisibleRef = useRef2(false);
8479
- const filterOpenRef = useRef2(startInFilter);
8480
- const filterInputRef = useRef2(initialQuery);
8481
- const filterAppliedRef = useRef2(initialQuery);
8482
- const autoSelectForAppliedFilterRef = useRef2(true);
8483
- const focusRef = useRef2(focus);
8484
- const restoreFilterOnSidebarFocusRef = useRef2(startInFilter);
8485
- const [footerNotice, setFooterNoticeState] = useState3(null);
8859
+ const [paletteQuery, setPaletteQuery] = useState4("");
8860
+ const [paletteIndex, setPaletteIndex] = useState4(0);
8861
+ const paletteQueryRef = useRef3("");
8862
+ const paletteIndexRef = useRef3(0);
8863
+ const [readerEmptyStateTipRotation, setReaderEmptyStateTipRotation] = useState4(() => nextReaderEmptyStateTipRotation);
8864
+ const readerEmptyStateVisibleRef = useRef3(false);
8865
+ const filterOpenRef = useRef3(startInFilter);
8866
+ const filterInputRef = useRef3(initialQuery);
8867
+ const focusRef = useRef3(focus);
8868
+ const restoreFilterOnSidebarFocusRef = useRef3(startInFilter);
8869
+ const [footerNotice, setFooterNoticeState] = useState4(null);
8486
8870
  const pushFooterNotice = (text, ttlMs = 2000) => setFooterNoticeState({ text, ttlMs });
8487
- const serverRef = useRef2(null);
8488
- const pendingSelectionPathRef = useRef2(null);
8489
- useEffect2(() => {
8871
+ const serverRef = useRef3(null);
8872
+ const pendingSelectionPathRef = useRef3(null);
8873
+ useEffect4(() => {
8490
8874
  return () => {
8491
8875
  serverRef.current?.stop();
8492
8876
  serverRef.current = null;
8493
8877
  };
8494
8878
  }, []);
8495
- useEffect2(() => {
8879
+ useEffect4(() => {
8496
8880
  if (footerNotice === null)
8497
8881
  return;
8498
8882
  closeFloatingOverlay();
@@ -8501,7 +8885,7 @@ var Browser = ({
8501
8885
  const timer = setTimeout(() => setFooterNoticeState(null), footerNotice.ttlMs);
8502
8886
  return () => clearTimeout(timer);
8503
8887
  }, [disableFooterNoticeAutoClear, footerNotice]);
8504
- useEffect2(() => {
8888
+ useEffect4(() => {
8505
8889
  if (discoveryWarningStatus === null) {
8506
8890
  if (floatingOverlay.kind === "status-popover")
8507
8891
  closeFloatingOverlay();
@@ -8511,11 +8895,11 @@ var Browser = ({
8511
8895
  dispatchFloatingOverlay({ type: "update-status-popover", content: discoveryWarningStatus });
8512
8896
  }
8513
8897
  }, [discoveryWarningStatus, floatingOverlay]);
8514
- const updateNoticeSeenRef = useRef2(null);
8515
- useEffect2(() => {
8898
+ const updateNoticeSeenRef = useRef3(null);
8899
+ useEffect4(() => {
8516
8900
  focusRef.current = focus;
8517
8901
  }, [focus]);
8518
- useEffect2(() => {
8902
+ useEffect4(() => {
8519
8903
  if (!updateNotice)
8520
8904
  return;
8521
8905
  if (updateNoticeSeenRef.current === updateNotice)
@@ -8550,55 +8934,27 @@ var Browser = ({
8550
8934
  });
8551
8935
  pushFooterNotice(`tone: ${nextTone}`);
8552
8936
  };
8553
- useEffect2(() => {
8554
- if (filterInput === filterApplied)
8555
- return;
8556
- const timer = setTimeout(() => {
8557
- filterAppliedRef.current = filterInput;
8558
- setFilterApplied(filterInput);
8559
- }, filterDebounceMs);
8560
- return () => clearTimeout(timer);
8561
- }, [filterApplied, filterDebounceMs, filterInput]);
8562
- useEffect2(() => {
8563
- autoSelectForAppliedFilterRef.current = true;
8564
- }, [filterApplied]);
8565
- const displayedFiles = useMemo2(() => filterFiles(files, filterApplied), [files, filterApplied]);
8566
8937
  const filterHasNoMatches = filterInput.length > 0 && displayedFiles.length === 0;
8567
- useEffect2(() => {
8568
- if (filterApplied.length === 0)
8569
- return;
8570
- if (!autoSelectForAppliedFilterRef.current)
8571
- return;
8572
- if (displayedFiles.length === 0)
8573
- return;
8574
- setSelectedIndex(0);
8575
- autoSelectForAppliedFilterRef.current = false;
8576
- }, [displayedFiles, filterApplied]);
8577
- useEffect2(() => {
8578
- if (selectedIndex >= displayedFiles.length) {
8579
- setSelectedIndex(displayedFiles.length === 0 ? 0 : displayedFiles.length - 1);
8580
- }
8581
- }, [displayedFiles.length, selectedIndex]);
8582
- useEffect2(() => {
8938
+ useEffect4(() => {
8583
8939
  const target = pendingSelectionPathRef.current;
8584
8940
  if (target === null)
8585
8941
  return;
8586
- const idx = displayedFiles.findIndex((f) => f.path === target);
8587
- if (idx >= 0) {
8588
- setSelectedIndex(idx);
8942
+ if (displayedFiles.some((file) => file.path === target)) {
8943
+ const snapshot = navigator.selectId(target);
8944
+ if (snapshot.selectedFile?.path !== target)
8945
+ return;
8589
8946
  pendingSelectionPathRef.current = null;
8590
8947
  }
8591
8948
  }, [displayedFiles]);
8592
- const selected = displayedFiles[selectedIndex];
8593
- const [renderedPath, setRenderedPath] = useState3(selected?.path ?? null);
8594
- useEffect2(() => {
8949
+ const [renderedPath, setRenderedPath] = useState4(selected?.path ?? null);
8950
+ useEffect4(() => {
8595
8951
  const target = selected?.path ?? null;
8596
8952
  if (target === renderedPath)
8597
8953
  return;
8598
8954
  const timer = setTimeout(() => setRenderedPath(target), renderedPathDebounceMs);
8599
8955
  return () => clearTimeout(timer);
8600
8956
  }, [selected?.path, renderedPath, renderedPathDebounceMs]);
8601
- useEffect2(() => {
8957
+ useEffect4(() => {
8602
8958
  if (!renderedPath) {
8603
8959
  setLoaded(null);
8604
8960
  return;
@@ -8630,10 +8986,20 @@ var Browser = ({
8630
8986
  paletteOpen,
8631
8987
  wrapEnabled,
8632
8988
  setFocus,
8633
- setSelectedIndex: (updater) => {
8989
+ moveSelectionBy: (delta) => {
8990
+ pendingSelectionPathRef.current = null;
8991
+ navigator.cancelAutoSelect();
8992
+ navigator.moveBy(delta);
8993
+ },
8994
+ selectFirst: () => {
8634
8995
  pendingSelectionPathRef.current = null;
8635
- autoSelectForAppliedFilterRef.current = false;
8636
- setSelectedIndex(updater);
8996
+ navigator.cancelAutoSelect();
8997
+ navigator.selectFirst();
8998
+ },
8999
+ selectLast: () => {
9000
+ pendingSelectionPathRef.current = null;
9001
+ navigator.cancelAutoSelect();
9002
+ navigator.selectLast();
8637
9003
  },
8638
9004
  toggleShown: () => {
8639
9005
  if (!canFitInline(width)) {
@@ -8664,10 +9030,9 @@ var Browser = ({
8664
9030
  clearAndOpenFilter: () => {
8665
9031
  closeFloatingOverlay();
8666
9032
  filterInputRef.current = "";
8667
- filterAppliedRef.current = "";
8668
9033
  setFilterInput("");
8669
- setFilterApplied("");
8670
- setSelectedIndex(() => 0);
9034
+ navigator.flushSearch("");
9035
+ navigator.selectFirst();
8671
9036
  focusRef.current = "sidebar";
8672
9037
  if (focus !== "sidebar")
8673
9038
  setFocus("sidebar");
@@ -8686,13 +9051,14 @@ var Browser = ({
8686
9051
  cycleTheme,
8687
9052
  toggleTone,
8688
9053
  toggleAll: () => {
8689
- if (pendingSelectionPathRef.current === null && selected) {
8690
- pendingSelectionPathRef.current = selected.path;
9054
+ const current = navigator.getSnapshot().selectedFile;
9055
+ if (pendingSelectionPathRef.current === null && current) {
9056
+ pendingSelectionPathRef.current = current.path;
8691
9057
  }
8692
9058
  onToggleAll?.();
8693
9059
  },
8694
9060
  serveCurrent: () => {
8695
- const file = displayedFiles[selectedIndex];
9061
+ const file = navigator.getSnapshot().selectedFile;
8696
9062
  if (!file)
8697
9063
  return;
8698
9064
  let handle = serverRef.current;
@@ -8722,7 +9088,7 @@ var Browser = ({
8722
9088
  process.exit(0);
8723
9089
  },
8724
9090
  editCurrent: () => {
8725
- const file = displayedFiles[selectedIndex];
9091
+ const file = navigator.getSnapshot().selectedFile;
8726
9092
  if (!file)
8727
9093
  return;
8728
9094
  const editor = resolveEditor(process.env);
@@ -8772,7 +9138,7 @@ var Browser = ({
8772
9138
  })();
8773
9139
  },
8774
9140
  copyCurrentContents: () => {
8775
- const file = displayedFiles[selectedIndex];
9141
+ const file = navigator.getSnapshot().selectedFile;
8776
9142
  if (!file)
8777
9143
  return;
8778
9144
  (async () => {
@@ -8855,9 +9221,8 @@ var Browser = ({
8855
9221
  }
8856
9222
  if (filterOpenRef.current && focusRef.current === "sidebar") {
8857
9223
  const closeFilter = (commit) => {
8858
- filterAppliedRef.current = filterInputRef.current;
8859
- setFilterApplied(filterInputRef.current);
8860
- const picked = displayedFiles[selectedIndex] ?? null;
9224
+ const snapshot = navigator.flushSearch(filterInputRef.current);
9225
+ const picked = snapshot.selectedFile;
8861
9226
  const effectiveCommit = commit && picked !== null;
8862
9227
  restoreFilterOnSidebarFocusRef.current = false;
8863
9228
  filterOpenRef.current = false;
@@ -8893,10 +9258,9 @@ var Browser = ({
8893
9258
  }
8894
9259
  if (key.ctrl && key.name === "\\") {
8895
9260
  filterInputRef.current = "";
8896
- filterAppliedRef.current = "";
8897
9261
  setFilterInput("");
8898
- setFilterApplied("");
8899
- setSelectedIndex(() => 0);
9262
+ navigator.flushSearch("");
9263
+ navigator.selectFirst();
8900
9264
  return;
8901
9265
  }
8902
9266
  if (key.name === "backspace" || key.name === "delete") {
@@ -8906,15 +9270,15 @@ var Browser = ({
8906
9270
  }
8907
9271
  filterInputRef.current = filterInputRef.current.slice(0, -1);
8908
9272
  setFilterInput(filterInputRef.current);
8909
- setSelectedIndex(() => 0);
9273
+ navigator.selectIndex(0);
8910
9274
  return;
8911
9275
  }
8912
9276
  if (key.name === "up") {
8913
- setSelectedIndex((i) => Math.max(0, i - 1));
9277
+ navigator.moveBy(-1);
8914
9278
  return;
8915
9279
  }
8916
9280
  if (key.name === "down") {
8917
- setSelectedIndex((i) => Math.min(Math.max(0, displayedFiles.length - 1), i + 1));
9281
+ navigator.moveBy(1);
8918
9282
  return;
8919
9283
  }
8920
9284
  if (key.ctrl || key.meta)
@@ -8928,7 +9292,7 @@ var Browser = ({
8928
9292
  if (char !== null) {
8929
9293
  filterInputRef.current = filterInputRef.current + char;
8930
9294
  setFilterInput(filterInputRef.current);
8931
- setSelectedIndex(() => 0);
9295
+ navigator.selectIndex(0);
8932
9296
  }
8933
9297
  return;
8934
9298
  }
@@ -8948,7 +9312,7 @@ var Browser = ({
8948
9312
  const parsedContent = useMemo2(() => parseFrontmatter(content), [content]);
8949
9313
  const readerEmptyStateTitle = filterHasNoMatches ? `No files match: ${filterInput}` : `${BRAND} ${BRAND_NAME}`;
8950
9314
  const readerEmptyStateVisible = error == null && renderedPath == null;
8951
- useEffect2(() => {
9315
+ useEffect4(() => {
8952
9316
  if (disableReaderEmptyStateRotation)
8953
9317
  return;
8954
9318
  if (readerEmptyStateVisible) {
@@ -8961,25 +9325,6 @@ var Browser = ({
8961
9325
  readerEmptyStateVisibleRef.current = false;
8962
9326
  }, [disableReaderEmptyStateRotation, readerEmptyStateVisible]);
8963
9327
  const discoveryActive = discoveryStatus !== null && discoveryStatus.length > 0;
8964
- const filterRowVisible = files.length > 0 || discoveryActive;
8965
- const sidebarBodyHeight = Math.max(1, height - FOOTER_HEIGHT - HEADER_HEIGHT - 2 - (filterRowVisible ? 1 : 0));
8966
- const maxScroll = Math.max(0, displayedFiles.length - sidebarBodyHeight);
8967
- const desiredScroll = (() => {
8968
- let s = sidebarScroll;
8969
- if (selectedIndex < s)
8970
- s = selectedIndex;
8971
- else if (selectedIndex >= s + sidebarBodyHeight)
8972
- s = selectedIndex - sidebarBodyHeight + 1;
8973
- return clamp3(s, 0, maxScroll);
8974
- })();
8975
- useEffect2(() => {
8976
- if (desiredScroll !== sidebarScroll)
8977
- setSidebarScroll(desiredScroll);
8978
- }, [desiredScroll, sidebarScroll]);
8979
- const visibleFiles = displayedFiles.slice(desiredScroll, desiredScroll + sidebarBodyHeight);
8980
- const sidebarPaneWidth = isNarrow ? width : sidebarWidth;
8981
- const sidebarTextWidth = Math.max(4, sidebarPaneWidth - (isNarrow ? 1 : 2));
8982
- const layoutSidebarRow = useCallback((relativePath) => formatSidebarRow(relativePath, sidebarTextWidth), [sidebarTextWidth]);
8983
9328
  const footerProps = {
8984
9329
  bindings: browserBindings,
8985
9330
  ctx,
@@ -9007,74 +9352,17 @@ var Browser = ({
9007
9352
  };
9008
9353
  const readerEmptyStateTips = useMemo2(() => buildReaderEmptyStateTips(browserBindings, ctx), [ctx]);
9009
9354
  const readerEmptyStateTip = useMemo2(() => pickTipByRotation(readerEmptyStateTips, readerEmptyStateTipRotation), [readerEmptyStateTipRotation, readerEmptyStateTips]);
9010
- const sidebarBody = /* @__PURE__ */ jsxDEV8(Fragment2, {
9011
- children: [
9012
- filterRowVisible && /* @__PURE__ */ jsxDEV8(PromptRow, {
9013
- query: filterInput,
9014
- editing: filterOpen,
9015
- placeholder: "type / to filter",
9016
- width: sidebarTextWidth
9017
- }, undefined, false, undefined, this),
9018
- displayedFiles.length === 0 ? /* @__PURE__ */ jsxDEV8(SidebarEmptyMessage, {
9019
- withTopSpacer: filterRowVisible,
9020
- width: sidebarTextWidth,
9021
- label: files.length === 0 ? discoveryActive ? "Scanning" : "No markdown files in" : "No files match",
9022
- value: files.length === 0 ? discoveryActive ? "\u2026" : rootLabel : filterApplied
9023
- }, undefined, false, undefined, this) : visibleFiles.map((file, idx) => {
9024
- const realIdx = desiredScroll + idx;
9025
- const isSelected = realIdx === selectedIndex;
9026
- const { basename: basename2, separator, parent } = layoutSidebarRow(file.relativePath);
9027
- const basenameFg = isSelected ? colors.selectedListItemText : colors.text;
9028
- const rowStyle = {
9029
- width: sidebarTextWidth,
9030
- height: 1,
9031
- flexDirection: "row",
9032
- ...isSelected ? { backgroundColor: colors.backgroundElement } : {}
9033
- };
9034
- return /* @__PURE__ */ jsxDEV8("box", {
9035
- style: rowStyle,
9036
- children: /* @__PURE__ */ jsxDEV8("text", {
9037
- wrapMode: "none",
9038
- children: [
9039
- /* @__PURE__ */ jsxDEV8("span", {
9040
- style: { fg: basenameFg },
9041
- children: basename2
9042
- }, undefined, false, undefined, this),
9043
- parent !== "" && /* @__PURE__ */ jsxDEV8("span", {
9044
- style: { fg: colors.textMuted },
9045
- children: middleTruncate(`${separator}${parent}`, Math.max(0, sidebarTextWidth - basename2.length))
9046
- }, undefined, false, undefined, this)
9047
- ]
9048
- }, undefined, true, undefined, this)
9049
- }, file.path, false, undefined, this);
9050
- })
9051
- ]
9052
- }, undefined, true, undefined, this);
9053
- const sidebarBorderSides = ["top", "bottom", "right"];
9054
9355
  const readerBorderSides = ["top", "bottom"];
9055
- const SIDEBAR_BORDER_CHARS = {
9056
- topLeft: "\u250C",
9057
- topRight: "\u252C",
9058
- bottomLeft: "\u2514",
9059
- bottomRight: "\u2534",
9060
- horizontal: "\u2500",
9061
- vertical: "\u2502",
9062
- topT: "\u252C",
9063
- bottomT: "\u2534",
9064
- leftT: "\u251C",
9065
- rightT: "\u2524",
9066
- cross: "\u253C"
9067
- };
9068
- const INACTIVE_PANE_OPACITY = 0.62;
9069
- return /* @__PURE__ */ jsxDEV8("box", {
9356
+ const INACTIVE_PANE_OPACITY2 = 0.62;
9357
+ return /* @__PURE__ */ jsxDEV12("box", {
9070
9358
  style: { width, height, flexDirection: "column", backgroundColor: colors.backgroundPanel },
9071
9359
  children: [
9072
- /* @__PURE__ */ jsxDEV8(Header, {
9360
+ /* @__PURE__ */ jsxDEV12(Header, {
9073
9361
  width,
9074
9362
  currentFile,
9075
9363
  rootLabel
9076
9364
  }, undefined, false, undefined, this),
9077
- /* @__PURE__ */ jsxDEV8("box", {
9365
+ /* @__PURE__ */ jsxDEV12("box", {
9078
9366
  style: {
9079
9367
  flexDirection: "row",
9080
9368
  flexGrow: 1,
@@ -9082,28 +9370,20 @@ var Browser = ({
9082
9370
  backgroundColor: colors.backgroundPanel
9083
9371
  },
9084
9372
  children: [
9085
- sidebarInline && /* @__PURE__ */ jsxDEV8("box", {
9086
- style: {
9087
- border: isNarrow ? readerBorderSides : sidebarBorderSides,
9088
- borderColor: colors.border,
9089
- ...isNarrow ? { flexGrow: 1, flexShrink: 1 } : { width: sidebarWidth, flexShrink: 0 },
9090
- flexDirection: "column",
9091
- backgroundColor: colors.backgroundPanel
9092
- },
9093
- ...isNarrow ? {} : { customBorderChars: SIDEBAR_BORDER_CHARS },
9094
- children: /* @__PURE__ */ jsxDEV8("box", {
9095
- style: {
9096
- flexGrow: 1,
9097
- flexShrink: 1,
9098
- flexDirection: "column",
9099
- paddingLeft: 1,
9100
- backgroundColor: sidebarActive ? colors.background : colors.backgroundPanel,
9101
- opacity: sidebarActive ? 1 : INACTIVE_PANE_OPACITY
9102
- },
9103
- children: sidebarBody
9104
- }, undefined, false, undefined, this)
9373
+ /* @__PURE__ */ jsxDEV12(Sidebar, {
9374
+ files,
9375
+ controller: navigator,
9376
+ filterInput,
9377
+ filterOpen,
9378
+ discoveryActive,
9379
+ rootLabel,
9380
+ viewportHeight: height,
9381
+ paneWidth: isNarrow ? width : sidebarWidth,
9382
+ narrow: isNarrow,
9383
+ active: sidebarActive,
9384
+ visible: sidebarInline
9105
9385
  }, undefined, false, undefined, this),
9106
- readerVisible && /* @__PURE__ */ jsxDEV8("box", {
9386
+ readerVisible && /* @__PURE__ */ jsxDEV12("box", {
9107
9387
  style: {
9108
9388
  border: readerBorderSides,
9109
9389
  borderColor: colors.border,
@@ -9112,19 +9392,19 @@ var Browser = ({
9112
9392
  flexDirection: "column",
9113
9393
  backgroundColor: colors.backgroundPanel
9114
9394
  },
9115
- children: /* @__PURE__ */ jsxDEV8("box", {
9395
+ children: /* @__PURE__ */ jsxDEV12("box", {
9116
9396
  style: {
9117
9397
  flexGrow: 1,
9118
9398
  flexShrink: 1,
9119
9399
  flexDirection: "column",
9120
9400
  padding: 1,
9121
9401
  backgroundColor: readerActive ? colors.background : colors.backgroundPanel,
9122
- opacity: readerActive ? 1 : INACTIVE_PANE_OPACITY
9402
+ opacity: readerActive ? 1 : INACTIVE_PANE_OPACITY2
9123
9403
  },
9124
- children: error ? /* @__PURE__ */ jsxDEV8("text", {
9404
+ children: error ? /* @__PURE__ */ jsxDEV12("text", {
9125
9405
  content: error,
9126
9406
  style: { fg: colors.error }
9127
- }, undefined, false, undefined, this) : !renderedPath ? /* @__PURE__ */ jsxDEV8("box", {
9407
+ }, undefined, false, undefined, this) : !renderedPath ? /* @__PURE__ */ jsxDEV12("box", {
9128
9408
  style: {
9129
9409
  flexGrow: 1,
9130
9410
  flexShrink: 1,
@@ -9132,22 +9412,22 @@ var Browser = ({
9132
9412
  justifyContent: "center",
9133
9413
  backgroundColor: readerActive ? colors.background : colors.backgroundPanel
9134
9414
  },
9135
- children: /* @__PURE__ */ jsxDEV8("box", {
9415
+ children: /* @__PURE__ */ jsxDEV12("box", {
9136
9416
  style: { flexDirection: "column", gap: 1, alignItems: "center" },
9137
9417
  children: [
9138
- /* @__PURE__ */ jsxDEV8("text", {
9418
+ /* @__PURE__ */ jsxDEV12("text", {
9139
9419
  content: readerEmptyStateTitle,
9140
9420
  wrapMode: "none",
9141
9421
  style: { fg: colors.textMuted }
9142
9422
  }, undefined, false, undefined, this),
9143
- readerEmptyStateTip && /* @__PURE__ */ jsxDEV8("text", {
9423
+ readerEmptyStateTip && /* @__PURE__ */ jsxDEV12("text", {
9144
9424
  content: readerEmptyStateTip.text,
9145
9425
  wrapMode: "none",
9146
9426
  style: { fg: colors.textMuted }
9147
9427
  }, readerEmptyStateTip.id, false, undefined, this)
9148
9428
  ]
9149
9429
  }, undefined, true, undefined, this)
9150
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV8("scrollbox", {
9430
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV12("scrollbox", {
9151
9431
  style: {
9152
9432
  scrollY: true,
9153
9433
  scrollX: false,
@@ -9157,17 +9437,17 @@ var Browser = ({
9157
9437
  },
9158
9438
  focused: readerActive && !paletteOpen,
9159
9439
  children: [
9160
- parsedContent.fields.length > 0 && /* @__PURE__ */ jsxDEV8("box", {
9440
+ parsedContent.fields.length > 0 && /* @__PURE__ */ jsxDEV12("box", {
9161
9441
  style: { flexDirection: "column", marginBottom: 1 },
9162
- children: parsedContent.fields.map((field) => /* @__PURE__ */ jsxDEV8("box", {
9442
+ children: parsedContent.fields.map((field) => /* @__PURE__ */ jsxDEV12("box", {
9163
9443
  style: { flexDirection: "row", gap: 1, flexWrap: "wrap" },
9164
9444
  children: [
9165
- /* @__PURE__ */ jsxDEV8("text", {
9445
+ /* @__PURE__ */ jsxDEV12("text", {
9166
9446
  content: `${field.key}:`,
9167
9447
  wrapMode: "word",
9168
9448
  style: { fg: colors.secondary }
9169
9449
  }, undefined, false, undefined, this),
9170
- /* @__PURE__ */ jsxDEV8("text", {
9450
+ /* @__PURE__ */ jsxDEV12("text", {
9171
9451
  content: field.value,
9172
9452
  wrapMode: "word",
9173
9453
  style: { fg: colors.textMuted }
@@ -9175,7 +9455,7 @@ var Browser = ({
9175
9455
  ]
9176
9456
  }, field.key, true, undefined, this))
9177
9457
  }, undefined, false, undefined, this),
9178
- /* @__PURE__ */ jsxDEV8("markdown", {
9458
+ /* @__PURE__ */ jsxDEV12("markdown", {
9179
9459
  content: parsedContent.body,
9180
9460
  syntaxStyle,
9181
9461
  fg: colors.text,
@@ -9189,17 +9469,17 @@ var Browser = ({
9189
9469
  }, undefined, false, undefined, this)
9190
9470
  ]
9191
9471
  }, undefined, true, undefined, this),
9192
- /* @__PURE__ */ jsxDEV8(Footer, {
9472
+ /* @__PURE__ */ jsxDEV12(Footer, {
9193
9473
  ...footerProps
9194
9474
  }, undefined, false, undefined, this),
9195
- paletteOpen && /* @__PURE__ */ jsxDEV8(CommandPalette, {
9475
+ paletteOpen && /* @__PURE__ */ jsxDEV12(CommandPalette, {
9196
9476
  commands: orderCommandsForPalette(filterCommands(buildCommands(ctx), paletteQuery)),
9197
9477
  query: paletteQuery,
9198
9478
  selectedIndex: paletteIndex,
9199
9479
  viewportWidth: width,
9200
9480
  viewportHeight: height
9201
9481
  }, undefined, false, undefined, this),
9202
- activeStatusPopover && /* @__PURE__ */ jsxDEV8(StatusPopoverPanel, {
9482
+ activeStatusPopover && /* @__PURE__ */ jsxDEV12(StatusPopoverPanel, {
9203
9483
  content: activeStatusPopover.content,
9204
9484
  variant: "warning"
9205
9485
  }, undefined, false, undefined, this)
@@ -9676,13 +9956,13 @@ var subscribeUpdateInfo = (cb) => {
9676
9956
  };
9677
9957
 
9678
9958
  // src/update/useUpdateNotice.ts
9679
- import { useEffect as useEffect3, useState as useState4 } from "react";
9959
+ import { useEffect as useEffect5, useState as useState5 } from "react";
9680
9960
  var useUpdateNotice = () => {
9681
- const [text, setText] = useState4(() => {
9961
+ const [text, setText] = useState5(() => {
9682
9962
  const cur = currentUpdateInfo();
9683
9963
  return cur ? formatFooterNotice(cur) : null;
9684
9964
  });
9685
- useEffect3(() => {
9965
+ useEffect5(() => {
9686
9966
  const unsub = subscribeUpdateInfo((info) => setText(formatFooterNotice(info)));
9687
9967
  return unsub;
9688
9968
  }, []);
@@ -9690,7 +9970,7 @@ var useUpdateNotice = () => {
9690
9970
  };
9691
9971
 
9692
9972
  // src/index.tsx
9693
- import { jsxDEV as jsxDEV9 } from "@opentui/react/jsx-dev-runtime";
9973
+ import { jsxDEV as jsxDEV13 } from "@opentui/react/jsx-dev-runtime";
9694
9974
  var pathIsDirectory = async (path) => {
9695
9975
  try {
9696
9976
  return (await stat(path)).isDirectory();
@@ -9754,14 +10034,14 @@ var DiscoverShell = ({
9754
10034
  startupFocus
9755
10035
  }) => {
9756
10036
  const updateNotice = useUpdateNotice();
9757
- const [show, setShow] = useState5(initialShow);
9758
- const [files, setFiles] = useState5([]);
9759
- const [scanning, setScanning] = useState5(true);
9760
- const [scanError, setScanError] = useState5(null);
9761
- const [skippedDirCount, setSkippedDirCount] = useState5(0);
9762
- const [lastSkippedDir, setLastSkippedDir] = useState5(null);
9763
- const countRef = useRef3(0);
9764
- useEffect4(() => {
10037
+ const [show, setShow] = useState6(initialShow);
10038
+ const [files, setFiles] = useState6([]);
10039
+ const [scanning, setScanning] = useState6(true);
10040
+ const [scanError, setScanError] = useState6(null);
10041
+ const [skippedDirCount, setSkippedDirCount] = useState6(0);
10042
+ const [lastSkippedDir, setLastSkippedDir] = useState6(null);
10043
+ const countRef = useRef4(0);
10044
+ useEffect6(() => {
9765
10045
  setFiles([]);
9766
10046
  setScanning(true);
9767
10047
  setScanError(null);
@@ -9800,7 +10080,7 @@ var DiscoverShell = ({
9800
10080
  skippedCount: skippedDirCount,
9801
10081
  lastSkippedPath: lastSkippedDir
9802
10082
  }));
9803
- return /* @__PURE__ */ jsxDEV9(Browser, {
10083
+ return /* @__PURE__ */ jsxDEV13(Browser, {
9804
10084
  files,
9805
10085
  initialQuery,
9806
10086
  wrapWidth,
@@ -9980,9 +10260,9 @@ async function runTui({
9980
10260
  console.error(`house: ${err instanceof Error ? err.message : String(err)}`);
9981
10261
  process.exit(1);
9982
10262
  }
9983
- createRoot(renderer).render(/* @__PURE__ */ jsxDEV9(RegistryProvider, {
10263
+ createRoot(renderer).render(/* @__PURE__ */ jsxDEV13(RegistryProvider, {
9984
10264
  initialValues: [[themeAtom, initialTheme]],
9985
- children: /* @__PURE__ */ jsxDEV9(DiscoverShell, {
10265
+ children: /* @__PURE__ */ jsxDEV13(DiscoverShell, {
9986
10266
  target: discoveryRoot,
9987
10267
  initialQuery,
9988
10268
  initialShow: show,