@carlesandres/house 0.5.1 → 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.1",
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,24 +50,26 @@ 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/",
61
- test: "bun test",
62
- "test:pty": "HOUSE_PTY=1 bun test test/pty/",
63
- "version:set": "bun run dev/set-version.ts",
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",
64
65
  release: "bun run dev/release.ts",
65
- "build:cli": "bun run dev/build-cli.ts",
66
- "build:standalone": "bun run dev/build-standalone.ts",
67
- "build:npm-packages": "bun run dev/build-npm-packages.ts",
68
- "build:themes": "bun run dev/build-themes.ts",
69
- prepack: "bun run build:cli",
70
66
  "repro:pty-codeblocks": "bun run dev/repro-pty-codeblocks.ts",
71
- prepare: "git config core.hooksPath .githooks 2>/dev/null || true"
67
+ start: "bun run src/index.tsx",
68
+ test: "bun test",
69
+ "test:pty": "HOUSE_PTY=1 bun test test/pty/",
70
+ typecheck: "tsc --noEmit",
71
+ "verify:github": "bun test test/github-emulation.test.ts",
72
+ "version:set": "bun run dev/set-version.ts"
72
73
  },
73
74
  optionalDependencies: {
74
75
  "@carlesandres/house-darwin-arm64": "0.5.1",
@@ -85,12 +86,15 @@ var package_default = {
85
86
  ignore: "^7.0.5",
86
87
  marked: "^18.0.4",
87
88
  react: "19.2.6",
88
- scheduler: "0.27.0"
89
+ scheduler: "0.27.0",
90
+ "string-width": "7.2.0",
91
+ "web-tree-sitter": "0.25.10"
89
92
  },
90
93
  devDependencies: {
91
94
  "@effect/language-service": "0.86.2",
92
95
  "@types/bun": "1.3.14",
93
96
  "@types/react": "19.2.15",
97
+ emulate: "0.9.0",
94
98
  oxfmt: "0.51.0",
95
99
  oxlint: "1.66.0",
96
100
  tuistory: "^0.8.0",
@@ -100,18 +104,424 @@ var package_default = {
100
104
 
101
105
  // src/Browser.tsx
102
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
103
516
  import { useKeyboard, useRenderer, useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
104
517
  import { useAtomValue, useAtomSet } from "@effect/atom-react";
105
518
  import { Effect as Effect3 } from "effect";
106
- 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";
107
520
 
108
521
  // src/keymap/browser.ts
109
522
  var JUMP = 8;
110
- var clamp = (n, min, max) => Math.max(min, Math.min(max, n));
111
- var lastIndex = (c) => Math.max(0, c.files.length - 1);
112
523
  var haveFiles = (c) => c.files.length > 0;
113
524
  var hasSelected = (c) => c.hasSelected;
114
- var stepBy = (c, delta) => c.setSelectedIndex((i) => clamp(i + delta, 0, lastIndex(c)));
115
525
  var inSidebar = (c) => c.focus === "sidebar";
116
526
  var filterClosed = (c) => !c.filterOpen;
117
527
  var paletteClosed = (c) => !c.paletteOpen;
@@ -216,7 +626,7 @@ var browserBindings = [
216
626
  description: "Move selection down",
217
627
  keys: ["j", "down"],
218
628
  when: inSidebarWithFiles,
219
- run: (c) => stepBy(c, 1)
629
+ run: (c) => c.moveSelectionBy(1)
220
630
  },
221
631
  {
222
632
  id: "sidebar.up",
@@ -224,7 +634,7 @@ var browserBindings = [
224
634
  description: "Move selection up",
225
635
  keys: ["k", "up"],
226
636
  when: inSidebarWithFiles,
227
- run: (c) => stepBy(c, -1)
637
+ run: (c) => c.moveSelectionBy(-1)
228
638
  },
229
639
  {
230
640
  id: "sidebar.jumpDown",
@@ -232,7 +642,7 @@ var browserBindings = [
232
642
  description: `Jump down ${JUMP}`,
233
643
  keys: ["shift+j"],
234
644
  when: inSidebarWithFiles,
235
- run: (c) => stepBy(c, JUMP)
645
+ run: (c) => c.moveSelectionBy(JUMP)
236
646
  },
237
647
  {
238
648
  id: "sidebar.jumpUp",
@@ -240,7 +650,7 @@ var browserBindings = [
240
650
  description: `Jump up ${JUMP}`,
241
651
  keys: ["shift+k"],
242
652
  when: inSidebarWithFiles,
243
- run: (c) => stepBy(c, -JUMP)
653
+ run: (c) => c.moveSelectionBy(-JUMP)
244
654
  },
245
655
  {
246
656
  id: "sidebar.pageDown",
@@ -248,7 +658,7 @@ var browserBindings = [
248
658
  description: "Page down",
249
659
  keys: ["space", "pagedown", "ctrl+d"],
250
660
  when: inSidebarWithFiles,
251
- run: (c) => stepBy(c, JUMP)
661
+ run: (c) => c.moveSelectionBy(JUMP)
252
662
  },
253
663
  {
254
664
  id: "sidebar.pageUp",
@@ -256,7 +666,7 @@ var browserBindings = [
256
666
  description: "Page up",
257
667
  keys: ["b", "pageup", "ctrl+u"],
258
668
  when: inSidebarWithFiles,
259
- run: (c) => stepBy(c, -JUMP)
669
+ run: (c) => c.moveSelectionBy(-JUMP)
260
670
  },
261
671
  {
262
672
  id: "sidebar.top",
@@ -264,7 +674,7 @@ var browserBindings = [
264
674
  description: "Jump to first file",
265
675
  keys: ["g"],
266
676
  when: inSidebarWithFiles,
267
- run: (c) => c.setSelectedIndex(() => 0)
677
+ run: (c) => c.selectFirst()
268
678
  },
269
679
  {
270
680
  id: "sidebar.bottom",
@@ -272,7 +682,7 @@ var browserBindings = [
272
682
  description: "Jump to last file",
273
683
  keys: ["shift+g"],
274
684
  when: inSidebarWithFiles,
275
- run: (c) => c.setSelectedIndex(() => lastIndex(c))
685
+ run: (c) => c.selectLast()
276
686
  },
277
687
  {
278
688
  id: "sidebar.open",
@@ -304,7 +714,7 @@ var browserBindings = [
304
714
  description: "Prev file",
305
715
  keys: ["["],
306
716
  when: inReaderWithSibling,
307
- run: (c) => stepBy(c, -1)
717
+ run: (c) => c.moveSelectionBy(-1)
308
718
  },
309
719
  {
310
720
  id: "reader.nextFile",
@@ -312,7 +722,7 @@ var browserBindings = [
312
722
  description: "Next file",
313
723
  keys: ["]"],
314
724
  when: inReaderWithSibling,
315
- run: (c) => stepBy(c, 1)
725
+ run: (c) => c.moveSelectionBy(1)
316
726
  },
317
727
  {
318
728
  id: "reader.back",
@@ -615,7 +1025,7 @@ var setActiveTheme = (definition, tone) => {
615
1025
  };
616
1026
 
617
1027
  // src/PromptRow.tsx
618
- import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
1028
+ import { jsxDEV as jsxDEV4 } from "@opentui/react/jsx-dev-runtime";
619
1029
  var PREFIX = "> ";
620
1030
  var CURSOR = "\u258F";
621
1031
  var PromptRow = ({ query, editing, placeholder = "", width }) => {
@@ -624,14 +1034,14 @@ var PromptRow = ({ query, editing, placeholder = "", width }) => {
624
1034
  const bodyFg = editing ? colors.primary : query.length > 0 ? colors.text : colors.textMuted;
625
1035
  const body = rawBody.length <= bodyBudget ? rawBody : editing ? "\u2026" + rawBody.slice(rawBody.length - bodyBudget + 1) : rawBody.slice(0, bodyBudget - 1) + "\u2026";
626
1036
  const prefixFg = editing ? colors.secondary : colors.primary;
627
- return /* @__PURE__ */ jsxDEV("text", {
1037
+ return /* @__PURE__ */ jsxDEV4("text", {
628
1038
  wrapMode: "none",
629
1039
  children: [
630
- /* @__PURE__ */ jsxDEV("span", {
1040
+ /* @__PURE__ */ jsxDEV4("span", {
631
1041
  style: { fg: prefixFg },
632
1042
  children: PREFIX
633
1043
  }, undefined, false, undefined, this),
634
- /* @__PURE__ */ jsxDEV("span", {
1044
+ /* @__PURE__ */ jsxDEV4("span", {
635
1045
  style: { fg: bodyFg },
636
1046
  children: body
637
1047
  }, undefined, false, undefined, this)
@@ -640,7 +1050,7 @@ var PromptRow = ({ query, editing, placeholder = "", width }) => {
640
1050
  };
641
1051
 
642
1052
  // src/CommandPalette.tsx
643
- import { jsxDEV as jsxDEV2 } from "@opentui/react/jsx-dev-runtime";
1053
+ import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
644
1054
  var SCRIM = RGBA.fromInts(0, 0, 0, 150);
645
1055
  var FOOTER_HINT = "\u2191\u2193 select enter run esc close";
646
1056
  var CATEGORY_ORDER = ["Navigation", "View", "File", "Appearance", "App"];
@@ -710,7 +1120,7 @@ var CommandPalette = ({
710
1120
  const titleWidth = Math.max(8, rowWidth - 2 - SHORTCUT_WIDTH - 1);
711
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);
712
1122
  const fitRight = (s, width) => s.length >= width ? s.slice(0, width) : " ".repeat(width - s.length) + s;
713
- return /* @__PURE__ */ jsxDEV2("box", {
1123
+ return /* @__PURE__ */ jsxDEV5("box", {
714
1124
  position: "absolute",
715
1125
  left: 0,
716
1126
  top: 0,
@@ -718,7 +1128,7 @@ var CommandPalette = ({
718
1128
  height: viewportHeight,
719
1129
  zIndex: 20,
720
1130
  style: { backgroundColor: SCRIM },
721
- children: /* @__PURE__ */ jsxDEV2("box", {
1131
+ children: /* @__PURE__ */ jsxDEV5("box", {
722
1132
  position: "absolute",
723
1133
  left,
724
1134
  top,
@@ -735,26 +1145,26 @@ var CommandPalette = ({
735
1145
  backgroundColor: colors.backgroundPanel
736
1146
  },
737
1147
  children: [
738
- /* @__PURE__ */ jsxDEV2(PromptRow, {
1148
+ /* @__PURE__ */ jsxDEV5(PromptRow, {
739
1149
  query,
740
1150
  editing: true,
741
1151
  width: rowWidth
742
1152
  }, undefined, false, undefined, this),
743
- /* @__PURE__ */ jsxDEV2("text", {
1153
+ /* @__PURE__ */ jsxDEV5("text", {
744
1154
  content: " "
745
1155
  }, undefined, false, undefined, this),
746
- commands.length === 0 ? /* @__PURE__ */ jsxDEV2("text", {
1156
+ commands.length === 0 ? /* @__PURE__ */ jsxDEV5("text", {
747
1157
  wrapMode: "none",
748
1158
  content: " (no matches)",
749
1159
  style: { fg: colors.textMuted }
750
1160
  }, undefined, false, undefined, this) : visible.map((row) => {
751
1161
  if (row.kind === "spacer") {
752
- return /* @__PURE__ */ jsxDEV2("text", {
1162
+ return /* @__PURE__ */ jsxDEV5("text", {
753
1163
  content: " "
754
1164
  }, row.key, false, undefined, this);
755
1165
  }
756
1166
  if (row.kind === "header") {
757
- return /* @__PURE__ */ jsxDEV2("text", {
1167
+ return /* @__PURE__ */ jsxDEV5("text", {
758
1168
  wrapMode: "none",
759
1169
  content: fit(row.text, rowWidth),
760
1170
  style: { fg: colors.textMuted, attributes: 1 }
@@ -766,25 +1176,25 @@ var CommandPalette = ({
766
1176
  const titleText = fit(cmd.title, titleWidth);
767
1177
  const shortcutText = cmd.shortcut ? fitRight(cmd.shortcut, SHORTCUT_WIDTH) : " ".repeat(SHORTCUT_WIDTH);
768
1178
  const titleFg = isSelected ? colors.primary : colors.text;
769
- return /* @__PURE__ */ jsxDEV2("text", {
1179
+ return /* @__PURE__ */ jsxDEV5("text", {
770
1180
  wrapMode: "none",
771
1181
  style: isSelected ? { bg: colors.backgroundElement } : {},
772
1182
  children: [
773
- /* @__PURE__ */ jsxDEV2("span", {
1183
+ /* @__PURE__ */ jsxDEV5("span", {
774
1184
  style: { fg: titleFg },
775
1185
  children: `${selector}${titleText} `
776
1186
  }, undefined, false, undefined, this),
777
- /* @__PURE__ */ jsxDEV2("span", {
1187
+ /* @__PURE__ */ jsxDEV5("span", {
778
1188
  style: { fg: colors.textMuted },
779
1189
  children: shortcutText
780
1190
  }, undefined, false, undefined, this)
781
1191
  ]
782
1192
  }, cmd.id, true, undefined, this);
783
1193
  }),
784
- /* @__PURE__ */ jsxDEV2("text", {
1194
+ /* @__PURE__ */ jsxDEV5("text", {
785
1195
  content: " "
786
1196
  }, undefined, false, undefined, this),
787
- /* @__PURE__ */ jsxDEV2("text", {
1197
+ /* @__PURE__ */ jsxDEV5("text", {
788
1198
  wrapMode: "none",
789
1199
  content: fit(FOOTER_HINT, rowWidth),
790
1200
  style: { fg: colors.textMuted }
@@ -952,8 +1362,8 @@ var displayKey = (raw) => {
952
1362
  };
953
1363
 
954
1364
  // src/Spinner.tsx
955
- import { useEffect, useRef, useState } from "react";
956
- 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";
957
1367
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
958
1368
  var FRAME_COUNT = FRAMES.length;
959
1369
  var normalizeFrameIndex = (index) => (index % FRAME_COUNT + FRAME_COUNT) % FRAME_COUNT;
@@ -963,12 +1373,12 @@ var Spinner = ({
963
1373
  initialFrameIndex = 0,
964
1374
  registerTick = null
965
1375
  }) => {
966
- const [index, setIndex] = useState(() => normalizeFrameIndex(initialFrameIndex));
967
- const tickRef = useRef(() => {
1376
+ const [index, setIndex] = useState2(() => normalizeFrameIndex(initialFrameIndex));
1377
+ const tickRef = useRef2(() => {
968
1378
  return;
969
1379
  });
970
1380
  tickRef.current = () => setIndex((prev) => (prev + 1) % FRAME_COUNT);
971
- useEffect(() => {
1381
+ useEffect3(() => {
972
1382
  if (registerTick) {
973
1383
  registerTick(() => tickRef.current());
974
1384
  return;
@@ -978,7 +1388,7 @@ var Spinner = ({
978
1388
  }, intervalMs);
979
1389
  return () => clearInterval(id);
980
1390
  }, [intervalMs, registerTick]);
981
- return /* @__PURE__ */ jsxDEV3("text", {
1391
+ return /* @__PURE__ */ jsxDEV6("text", {
982
1392
  content: FRAMES[index] ?? FRAMES[0],
983
1393
  wrapMode: "none",
984
1394
  style: { fg }
@@ -986,7 +1396,7 @@ var Spinner = ({
986
1396
  };
987
1397
 
988
1398
  // src/StatusIndicator.tsx
989
- import { jsxDEV as jsxDEV4 } from "@opentui/react/jsx-dev-runtime";
1399
+ import { jsxDEV as jsxDEV7 } from "@opentui/react/jsx-dev-runtime";
990
1400
  var statusIndicatorFg = (variant) => {
991
1401
  switch (variant) {
992
1402
  case "info":
@@ -1007,7 +1417,7 @@ var StatusIndicator = ({
1007
1417
  }) => {
1008
1418
  const activeColor = statusIndicatorFg(variant);
1009
1419
  const backgroundColor = active ? activeColor : colors.backgroundElement;
1010
- return /* @__PURE__ */ jsxDEV4("box", {
1420
+ return /* @__PURE__ */ jsxDEV7("box", {
1011
1421
  ...onMouseUp === undefined ? {} : { onMouseUp },
1012
1422
  style: {
1013
1423
  width: 3,
@@ -1015,7 +1425,7 @@ var StatusIndicator = ({
1015
1425
  flexDirection: "row",
1016
1426
  backgroundColor
1017
1427
  },
1018
- children: /* @__PURE__ */ jsxDEV4("text", {
1428
+ children: /* @__PURE__ */ jsxDEV7("text", {
1019
1429
  content: ` ${icon} `,
1020
1430
  wrapMode: "none",
1021
1431
  style: {
@@ -1028,7 +1438,7 @@ var StatusIndicator = ({
1028
1438
  };
1029
1439
 
1030
1440
  // src/Footer.tsx
1031
- import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
1441
+ import { jsxDEV as jsxDEV8 } from "@opentui/react/jsx-dev-runtime";
1032
1442
  var FOOTER_HEIGHT = 1;
1033
1443
  var normalizeStatusLine = (status) => status.replace(/\s+/g, " ").trim();
1034
1444
  var HINT_SEPARATOR = " ";
@@ -1118,17 +1528,17 @@ var Footer = ({
1118
1528
  const statusContent = status ? status.slice(0, Math.max(0, statusBudget - statusChromeWidth)) : "";
1119
1529
  const statusSeparatorContent = status ? STATUS_SEPARATOR.slice(0, Math.max(0, statusBudget - nonWarningStatusPrefixBudget - statusContent.length)) : "";
1120
1530
  const noticeContent = notice ? notice.length > contentBudget ? notice.slice(0, contentBudget) : notice : null;
1121
- const renderIndicators = () => renderedIndicators.map(({ id, ...props }) => /* @__PURE__ */ jsxDEV5(StatusIndicator, {
1531
+ const renderIndicators = () => renderedIndicators.map(({ id, ...props }) => /* @__PURE__ */ jsxDEV8(StatusIndicator, {
1122
1532
  ...props
1123
1533
  }, id, false, undefined, this));
1124
- const renderIndicatorContentGap = () => indicatorContentGapBudget > 0 ? /* @__PURE__ */ jsxDEV5("text", {
1534
+ const renderIndicatorContentGap = () => indicatorContentGapBudget > 0 ? /* @__PURE__ */ jsxDEV8("text", {
1125
1535
  content: " ",
1126
1536
  wrapMode: "none",
1127
1537
  style: { fg: colors.textMuted }
1128
1538
  }, undefined, false, undefined, this) : null;
1129
1539
  const renderHints = () => visibleHints.flatMap((h, i) => {
1130
1540
  const sep = i > 0 ? [
1131
- /* @__PURE__ */ jsxDEV5("text", {
1541
+ /* @__PURE__ */ jsxDEV8("text", {
1132
1542
  content: HINT_SEPARATOR,
1133
1543
  wrapMode: "none",
1134
1544
  style: { fg: colors.textMuted }
@@ -1136,12 +1546,12 @@ var Footer = ({
1136
1546
  ] : [];
1137
1547
  return [
1138
1548
  ...sep,
1139
- /* @__PURE__ */ jsxDEV5("text", {
1549
+ /* @__PURE__ */ jsxDEV8("text", {
1140
1550
  content: h.key,
1141
1551
  wrapMode: "none",
1142
1552
  style: { fg: colors.text }
1143
1553
  }, `k${i}`, false, undefined, this),
1144
- /* @__PURE__ */ jsxDEV5("text", {
1554
+ /* @__PURE__ */ jsxDEV8("text", {
1145
1555
  content: ` ${h.label}`,
1146
1556
  wrapMode: "none",
1147
1557
  style: { fg: colors.textMuted }
@@ -1149,12 +1559,12 @@ var Footer = ({
1149
1559
  ];
1150
1560
  });
1151
1561
  if (noticeContent !== null) {
1152
- return /* @__PURE__ */ jsxDEV5("box", {
1562
+ return /* @__PURE__ */ jsxDEV8("box", {
1153
1563
  style: rowStyle,
1154
1564
  children: [
1155
1565
  renderIndicators(),
1156
1566
  renderIndicatorContentGap(),
1157
- /* @__PURE__ */ jsxDEV5("text", {
1567
+ /* @__PURE__ */ jsxDEV8("text", {
1158
1568
  content: noticeContent,
1159
1569
  wrapMode: "none",
1160
1570
  style: { fg: colors.primary }
@@ -1169,25 +1579,25 @@ var Footer = ({
1169
1579
  ...discoverySpinnerInitialFrameIndex === undefined ? {} : { initialFrameIndex: discoverySpinnerInitialFrameIndex },
1170
1580
  ...discoverySpinnerRegisterTick === undefined ? {} : { registerTick: discoverySpinnerRegisterTick }
1171
1581
  };
1172
- return /* @__PURE__ */ jsxDEV5("box", {
1582
+ return /* @__PURE__ */ jsxDEV8("box", {
1173
1583
  style: rowStyle,
1174
1584
  children: [
1175
1585
  renderIndicators(),
1176
1586
  renderIndicatorContentGap(),
1177
- nonWarningStatusPrefixBudget >= 1 ? /* @__PURE__ */ jsxDEV5(Spinner, {
1587
+ nonWarningStatusPrefixBudget >= 1 ? /* @__PURE__ */ jsxDEV8(Spinner, {
1178
1588
  ...spinnerProps
1179
1589
  }, undefined, false, undefined, this) : null,
1180
- nonWarningStatusPrefixBudget >= 2 ? /* @__PURE__ */ jsxDEV5("text", {
1590
+ nonWarningStatusPrefixBudget >= 2 ? /* @__PURE__ */ jsxDEV8("text", {
1181
1591
  content: " ",
1182
1592
  wrapMode: "none",
1183
1593
  style: { fg: colors.textMuted }
1184
1594
  }, undefined, false, undefined, this) : null,
1185
- isPartialWarning ? null : /* @__PURE__ */ jsxDEV5("text", {
1595
+ isPartialWarning ? null : /* @__PURE__ */ jsxDEV8("text", {
1186
1596
  content: statusContent,
1187
1597
  wrapMode: "none",
1188
1598
  style: { fg: colors.secondary }
1189
1599
  }, undefined, false, undefined, this),
1190
- statusSeparatorContent.length > 0 && /* @__PURE__ */ jsxDEV5("text", {
1600
+ statusSeparatorContent.length > 0 && /* @__PURE__ */ jsxDEV8("text", {
1191
1601
  content: statusSeparatorContent,
1192
1602
  wrapMode: "none",
1193
1603
  style: { fg: colors.textMuted }
@@ -1196,7 +1606,7 @@ var Footer = ({
1196
1606
  ]
1197
1607
  }, undefined, true, undefined, this);
1198
1608
  }
1199
- return /* @__PURE__ */ jsxDEV5("box", {
1609
+ return /* @__PURE__ */ jsxDEV8("box", {
1200
1610
  style: rowStyle,
1201
1611
  children: [
1202
1612
  renderIndicators(),
@@ -1207,7 +1617,7 @@ var Footer = ({
1207
1617
  };
1208
1618
 
1209
1619
  // src/ui/middleTruncate.ts
1210
- var middleTruncate = (value, width, options = {}) => {
1620
+ var middleTruncate2 = (value, width, options = {}) => {
1211
1621
  const ellipsis = options.ellipsis ?? "\u2026";
1212
1622
  if (width <= 0)
1213
1623
  return "";
@@ -1222,7 +1632,7 @@ var middleTruncate = (value, width, options = {}) => {
1222
1632
  };
1223
1633
 
1224
1634
  // src/Header.tsx
1225
- import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
1635
+ import { jsxDEV as jsxDEV9 } from "@opentui/react/jsx-dev-runtime";
1226
1636
  var HEADER_HEIGHT = 1;
1227
1637
  var FILE_SEPARATOR = " \xB7 ";
1228
1638
  var HEADER_HORIZONTAL_PADDING = 2;
@@ -1249,7 +1659,7 @@ var layoutHeaderSegments = ({
1249
1659
  const rootWidth = rootSegment === null ? 0 : usableWidth - joinedLength(segments) - FILE_SEPARATOR.length - HEADER_GROUP_GAP - right.length;
1250
1660
  if (root !== null && root.length > rootWidth && rootWidth >= MIN_TRUNCATED_ROOT_WIDTH) {
1251
1661
  return {
1252
- left: [...segments, { id: "root", text: middleTruncate(root, rootWidth), tone: "muted" }],
1662
+ left: [...segments, { id: "root", text: middleTruncate2(root, rootWidth), tone: "muted" }],
1253
1663
  right
1254
1664
  };
1255
1665
  }
@@ -1264,7 +1674,7 @@ var layoutHeaderSegments = ({
1264
1674
  const fileWidth = usableWidth - BRAND.length - FILE_SEPARATOR.length;
1265
1675
  if (fileWidth > 0) {
1266
1676
  return {
1267
- left: [iconBrand, { id: "file", text: middleTruncate(file, fileWidth), tone: "primary" }],
1677
+ left: [iconBrand, { id: "file", text: middleTruncate2(file, fileWidth), tone: "primary" }],
1268
1678
  right: null
1269
1679
  };
1270
1680
  }
@@ -1278,7 +1688,7 @@ var fits = (segments, right, usableWidth) => {
1278
1688
  };
1279
1689
  var Header = ({ width, currentFile, rootLabel, version = package_default.version }) => {
1280
1690
  const layout = layoutHeaderSegments({ width, currentFile, rootLabel, version });
1281
- return /* @__PURE__ */ jsxDEV6("box", {
1691
+ return /* @__PURE__ */ jsxDEV9("box", {
1282
1692
  style: {
1283
1693
  width,
1284
1694
  height: HEADER_HEIGHT,
@@ -1290,20 +1700,20 @@ var Header = ({ width, currentFile, rootLabel, version = package_default.version
1290
1700
  backgroundColor: colors.backgroundPanel
1291
1701
  },
1292
1702
  children: [
1293
- /* @__PURE__ */ jsxDEV6("text", {
1703
+ /* @__PURE__ */ jsxDEV9("text", {
1294
1704
  wrapMode: "none",
1295
1705
  children: layout.left.map((segment, idx) => [
1296
- idx > 0 ? /* @__PURE__ */ jsxDEV6("span", {
1706
+ idx > 0 ? /* @__PURE__ */ jsxDEV9("span", {
1297
1707
  style: { fg: colors.textMuted },
1298
1708
  children: FILE_SEPARATOR
1299
1709
  }, `${segment.id}-${idx}-separator`, false, undefined, this) : null,
1300
- /* @__PURE__ */ jsxDEV6("span", {
1710
+ /* @__PURE__ */ jsxDEV9("span", {
1301
1711
  style: { fg: segment.tone === "primary" ? colors.text : colors.textMuted },
1302
1712
  children: segment.text
1303
1713
  }, `${segment.id}-${idx}`, false, undefined, this)
1304
1714
  ])
1305
1715
  }, undefined, false, undefined, this),
1306
- layout.right !== null && /* @__PURE__ */ jsxDEV6("text", {
1716
+ layout.right !== null && /* @__PURE__ */ jsxDEV9("text", {
1307
1717
  content: layout.right,
1308
1718
  wrapMode: "none",
1309
1719
  style: { fg: colors.textMuted }
@@ -1510,55 +1920,56 @@ var resolveSidebarWidth = (viewport, preferred) => {
1510
1920
  var defaultPreferredWidth = (viewport) => Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, Math.floor(viewport * 0.25)));
1511
1921
  var canFitInline = (viewport) => viewport >= SIDEBAR_MIN_WIDTH + DIVIDER_WIDTH + READER_MIN_WIDTH;
1512
1922
 
1513
- // src/layout/sidebarEmptyState.ts
1514
- var HEAD_ELISION_PREFIX = "\u2026";
1515
- var fitSidebarEmptyValue = (value, width) => {
1516
- if (value.length <= width)
1517
- return value;
1518
- if (width <= 1)
1519
- return value.slice(value.length - 1);
1520
- return HEAD_ELISION_PREFIX + value.slice(value.length - width + 1);
1521
- };
1522
-
1523
- // src/layout/sidebarRow.ts
1524
- var SIDEBAR_ROW_SEPARATOR = " ";
1525
- var MIN_PARENT_BUDGET = 3;
1526
- var formatSidebarRow = (relativePath, totalWidth) => {
1527
- const slash = relativePath.lastIndexOf("/");
1528
- if (slash < 0) {
1529
- return { basename: fitTail(relativePath, totalWidth), separator: "", parent: "" };
1530
- }
1531
- const basename = relativePath.slice(slash + 1);
1532
- const parentFull = relativePath.slice(0, slash);
1533
- if (basename.length >= totalWidth) {
1534
- return { basename: fitTail(basename, totalWidth), separator: "", parent: "" };
1535
- }
1536
- const remaining = totalWidth - basename.length - SIDEBAR_ROW_SEPARATOR.length;
1537
- if (remaining < MIN_PARENT_BUDGET || parentFull.length === 0) {
1538
- return { basename, separator: "", parent: "" };
1539
- }
1540
- if (parentFull.length <= remaining) {
1541
- return row(basename, parentFull);
1542
- }
1543
- return row(basename, middleTruncate(parentFull, remaining));
1544
- };
1545
- var row = (basename, parent) => ({
1546
- basename,
1547
- separator: SIDEBAR_ROW_SEPARATOR,
1548
- parent
1549
- });
1550
- var fitTail = (s, width) => {
1551
- if (s.length <= width)
1552
- return s;
1553
- if (width <= 1)
1554
- return s.slice(0, 1);
1555
- 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);
1556
1967
  };
1557
1968
 
1558
1969
  // src/StatusPopover.tsx
1559
1970
  import { useTerminalDimensions } from "@opentui/react";
1560
- import { useMemo, useState as useState2 } from "react";
1561
- 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";
1562
1973
  var clamp2 = (n, min, max) => Math.max(min, Math.min(max, n));
1563
1974
  var measureLine = (line) => line.length;
1564
1975
  var wrapLine = (line, width) => {
@@ -1596,7 +2007,7 @@ var StatusPopoverPanel = ({
1596
2007
  while (linesToRender.length < popoverHeight - 2)
1597
2008
  linesToRender.push("");
1598
2009
  const borderColor = statusIndicatorFg(variant);
1599
- return /* @__PURE__ */ jsxDEV7("box", {
2010
+ return /* @__PURE__ */ jsxDEV11("box", {
1600
2011
  position: "absolute",
1601
2012
  left: 1,
1602
2013
  top: Math.max(0, viewportHeight - popoverHeight - 2),
@@ -1610,15 +2021,15 @@ var StatusPopoverPanel = ({
1610
2021
  flexDirection: "column"
1611
2022
  },
1612
2023
  children: [
1613
- /* @__PURE__ */ jsxDEV7("text", {
2024
+ /* @__PURE__ */ jsxDEV11("text", {
1614
2025
  content: ""
1615
2026
  }, undefined, false, undefined, this),
1616
- linesToRender.map((line, i) => /* @__PURE__ */ jsxDEV7("text", {
2027
+ linesToRender.map((line, i) => /* @__PURE__ */ jsxDEV11("text", {
1617
2028
  content: line,
1618
2029
  wrapMode: "none",
1619
2030
  style: { fg: colors.text }
1620
2031
  }, i, false, undefined, this)),
1621
- /* @__PURE__ */ jsxDEV7("text", {
2032
+ /* @__PURE__ */ jsxDEV11("text", {
1622
2033
  content: ""
1623
2034
  }, undefined, false, undefined, this)
1624
2035
  ]
@@ -8363,8 +8774,10 @@ var saveThemePreference = async (record, path = defaultConfigPath()) => {
8363
8774
  };
8364
8775
 
8365
8776
  // src/Browser.tsx
8366
- import { jsxDEV as jsxDEV8, Fragment as Fragment2 } from "@opentui/react/jsx-dev-runtime";
8777
+ import { jsxDEV as jsxDEV12 } from "@opentui/react/jsx-dev-runtime";
8367
8778
  var defaultReadFile = (path) => Effect3.runPromise(readFileText(path));
8779
+ var getFileId = (file) => file.path;
8780
+ var getFilePath = (file) => file.relativePath;
8368
8781
  var clamp3 = (n, min, max) => Math.max(min, Math.min(max, n));
8369
8782
  var nextReaderEmptyStateTipRotation = 0;
8370
8783
  var FILTER_DEBOUNCE_MS = 50;
@@ -8383,43 +8796,6 @@ var floatingOverlayReducer = (state, action) => {
8383
8796
  return state.kind === "status-popover" ? { kind: "status-popover", content: action.content } : state;
8384
8797
  }
8385
8798
  };
8386
- var SidebarEmptyMessage = ({
8387
- label,
8388
- value,
8389
- width,
8390
- withTopSpacer
8391
- }) => /* @__PURE__ */ jsxDEV8(Fragment2, {
8392
- children: [
8393
- withTopSpacer && /* @__PURE__ */ jsxDEV8("text", {
8394
- content: ""
8395
- }, undefined, false, undefined, this),
8396
- /* @__PURE__ */ jsxDEV8("box", {
8397
- style: {
8398
- width,
8399
- flexDirection: "row",
8400
- flexWrap: "wrap",
8401
- justifyContent: "center",
8402
- gap: 1
8403
- },
8404
- children: [
8405
- /* @__PURE__ */ jsxDEV8("text", {
8406
- wrapMode: "none",
8407
- children: /* @__PURE__ */ jsxDEV8("span", {
8408
- style: { fg: colors.textMuted },
8409
- children: label
8410
- }, undefined, false, undefined, this)
8411
- }, undefined, false, undefined, this),
8412
- /* @__PURE__ */ jsxDEV8("text", {
8413
- wrapMode: "none",
8414
- children: /* @__PURE__ */ jsxDEV8("span", {
8415
- style: { fg: colors.textMuted },
8416
- children: `"${fitSidebarEmptyValue(value, width)}"`
8417
- }, undefined, false, undefined, this)
8418
- }, undefined, false, undefined, this)
8419
- ]
8420
- }, undefined, true, undefined, this)
8421
- ]
8422
- }, undefined, true, undefined, this);
8423
8799
  var Browser = ({
8424
8800
  files,
8425
8801
  initialIndex = 0,
@@ -8448,20 +8824,29 @@ var Browser = ({
8448
8824
  const theme = useAtomValue(themeAtom);
8449
8825
  const setTheme = useAtomSet(themeAtom);
8450
8826
  const syntaxStyle = useMemo2(() => SyntaxStyle.fromStyles(colors.syntax), [theme]);
8451
- const [selectedIndex, setSelectedIndex] = useState3(() => clamp3(initialIndex, 0, Math.max(0, files.length - 1)));
8452
- const [wrapEnabled, setWrapEnabled] = useState3(initialWrap);
8453
- const [loaded, setLoaded] = useState3(null);
8454
- const [error, setError] = useState3(null);
8455
- 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);
8456
8831
  const startInFilter = startupFocus === "filter";
8457
8832
  const initialFocus = startupFocus === null ? shown ? "sidebar" : "reader" : startupFocus === "reader" ? "reader" : "sidebar";
8458
- const [focus, setFocus] = useState3(() => initialFocus);
8459
- const [sidebarScroll, setSidebarScroll] = useState3(0);
8460
- const [filterOpen, setFilterOpen] = useState3(startInFilter);
8461
- const [filterInput, setFilterInput] = useState3(initialQuery);
8462
- const [filterApplied, setFilterApplied] = useState3(initialQuery);
8463
- const [floatingOverlay, dispatchFloatingOverlayState] = useReducer(floatingOverlayReducer, noFloatingOverlay);
8464
- 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);
8465
8850
  const dispatchFloatingOverlay = (action) => {
8466
8851
  const next = floatingOverlayReducer(floatingOverlayRef.current, action);
8467
8852
  floatingOverlayRef.current = next;
@@ -8471,29 +8856,27 @@ var Browser = ({
8471
8856
  const paletteOpen = floatingOverlay.kind === "command-palette";
8472
8857
  const activeStatusPopover = floatingOverlay.kind === "status-popover" ? floatingOverlay : null;
8473
8858
  const discoveryWarningStatus = isPartialDiscoveryWarning2(discoveryStatus) ? discoveryStatus : null;
8474
- const [paletteQuery, setPaletteQuery] = useState3("");
8475
- const [paletteIndex, setPaletteIndex] = useState3(0);
8476
- const paletteQueryRef = useRef2("");
8477
- const paletteIndexRef = useRef2(0);
8478
- const [readerEmptyStateTipRotation, setReaderEmptyStateTipRotation] = useState3(() => nextReaderEmptyStateTipRotation);
8479
- const readerEmptyStateVisibleRef = useRef2(false);
8480
- const filterOpenRef = useRef2(startInFilter);
8481
- const filterInputRef = useRef2(initialQuery);
8482
- const filterAppliedRef = useRef2(initialQuery);
8483
- const autoSelectForAppliedFilterRef = useRef2(true);
8484
- const focusRef = useRef2(focus);
8485
- const restoreFilterOnSidebarFocusRef = useRef2(startInFilter);
8486
- 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);
8487
8870
  const pushFooterNotice = (text, ttlMs = 2000) => setFooterNoticeState({ text, ttlMs });
8488
- const serverRef = useRef2(null);
8489
- const pendingSelectionPathRef = useRef2(null);
8490
- useEffect2(() => {
8871
+ const serverRef = useRef3(null);
8872
+ const pendingSelectionPathRef = useRef3(null);
8873
+ useEffect4(() => {
8491
8874
  return () => {
8492
8875
  serverRef.current?.stop();
8493
8876
  serverRef.current = null;
8494
8877
  };
8495
8878
  }, []);
8496
- useEffect2(() => {
8879
+ useEffect4(() => {
8497
8880
  if (footerNotice === null)
8498
8881
  return;
8499
8882
  closeFloatingOverlay();
@@ -8502,7 +8885,7 @@ var Browser = ({
8502
8885
  const timer = setTimeout(() => setFooterNoticeState(null), footerNotice.ttlMs);
8503
8886
  return () => clearTimeout(timer);
8504
8887
  }, [disableFooterNoticeAutoClear, footerNotice]);
8505
- useEffect2(() => {
8888
+ useEffect4(() => {
8506
8889
  if (discoveryWarningStatus === null) {
8507
8890
  if (floatingOverlay.kind === "status-popover")
8508
8891
  closeFloatingOverlay();
@@ -8512,11 +8895,11 @@ var Browser = ({
8512
8895
  dispatchFloatingOverlay({ type: "update-status-popover", content: discoveryWarningStatus });
8513
8896
  }
8514
8897
  }, [discoveryWarningStatus, floatingOverlay]);
8515
- const updateNoticeSeenRef = useRef2(null);
8516
- useEffect2(() => {
8898
+ const updateNoticeSeenRef = useRef3(null);
8899
+ useEffect4(() => {
8517
8900
  focusRef.current = focus;
8518
8901
  }, [focus]);
8519
- useEffect2(() => {
8902
+ useEffect4(() => {
8520
8903
  if (!updateNotice)
8521
8904
  return;
8522
8905
  if (updateNoticeSeenRef.current === updateNotice)
@@ -8551,55 +8934,27 @@ var Browser = ({
8551
8934
  });
8552
8935
  pushFooterNotice(`tone: ${nextTone}`);
8553
8936
  };
8554
- useEffect2(() => {
8555
- if (filterInput === filterApplied)
8556
- return;
8557
- const timer = setTimeout(() => {
8558
- filterAppliedRef.current = filterInput;
8559
- setFilterApplied(filterInput);
8560
- }, filterDebounceMs);
8561
- return () => clearTimeout(timer);
8562
- }, [filterApplied, filterDebounceMs, filterInput]);
8563
- useEffect2(() => {
8564
- autoSelectForAppliedFilterRef.current = true;
8565
- }, [filterApplied]);
8566
- const displayedFiles = useMemo2(() => filterFiles(files, filterApplied), [files, filterApplied]);
8567
8937
  const filterHasNoMatches = filterInput.length > 0 && displayedFiles.length === 0;
8568
- useEffect2(() => {
8569
- if (filterApplied.length === 0)
8570
- return;
8571
- if (!autoSelectForAppliedFilterRef.current)
8572
- return;
8573
- if (displayedFiles.length === 0)
8574
- return;
8575
- setSelectedIndex(0);
8576
- autoSelectForAppliedFilterRef.current = false;
8577
- }, [displayedFiles, filterApplied]);
8578
- useEffect2(() => {
8579
- if (selectedIndex >= displayedFiles.length) {
8580
- setSelectedIndex(displayedFiles.length === 0 ? 0 : displayedFiles.length - 1);
8581
- }
8582
- }, [displayedFiles.length, selectedIndex]);
8583
- useEffect2(() => {
8938
+ useEffect4(() => {
8584
8939
  const target = pendingSelectionPathRef.current;
8585
8940
  if (target === null)
8586
8941
  return;
8587
- const idx = displayedFiles.findIndex((f) => f.path === target);
8588
- if (idx >= 0) {
8589
- setSelectedIndex(idx);
8942
+ if (displayedFiles.some((file) => file.path === target)) {
8943
+ const snapshot = navigator.selectId(target);
8944
+ if (snapshot.selectedFile?.path !== target)
8945
+ return;
8590
8946
  pendingSelectionPathRef.current = null;
8591
8947
  }
8592
8948
  }, [displayedFiles]);
8593
- const selected = displayedFiles[selectedIndex];
8594
- const [renderedPath, setRenderedPath] = useState3(selected?.path ?? null);
8595
- useEffect2(() => {
8949
+ const [renderedPath, setRenderedPath] = useState4(selected?.path ?? null);
8950
+ useEffect4(() => {
8596
8951
  const target = selected?.path ?? null;
8597
8952
  if (target === renderedPath)
8598
8953
  return;
8599
8954
  const timer = setTimeout(() => setRenderedPath(target), renderedPathDebounceMs);
8600
8955
  return () => clearTimeout(timer);
8601
8956
  }, [selected?.path, renderedPath, renderedPathDebounceMs]);
8602
- useEffect2(() => {
8957
+ useEffect4(() => {
8603
8958
  if (!renderedPath) {
8604
8959
  setLoaded(null);
8605
8960
  return;
@@ -8631,10 +8986,20 @@ var Browser = ({
8631
8986
  paletteOpen,
8632
8987
  wrapEnabled,
8633
8988
  setFocus,
8634
- setSelectedIndex: (updater) => {
8989
+ moveSelectionBy: (delta) => {
8990
+ pendingSelectionPathRef.current = null;
8991
+ navigator.cancelAutoSelect();
8992
+ navigator.moveBy(delta);
8993
+ },
8994
+ selectFirst: () => {
8995
+ pendingSelectionPathRef.current = null;
8996
+ navigator.cancelAutoSelect();
8997
+ navigator.selectFirst();
8998
+ },
8999
+ selectLast: () => {
8635
9000
  pendingSelectionPathRef.current = null;
8636
- autoSelectForAppliedFilterRef.current = false;
8637
- setSelectedIndex(updater);
9001
+ navigator.cancelAutoSelect();
9002
+ navigator.selectLast();
8638
9003
  },
8639
9004
  toggleShown: () => {
8640
9005
  if (!canFitInline(width)) {
@@ -8665,10 +9030,9 @@ var Browser = ({
8665
9030
  clearAndOpenFilter: () => {
8666
9031
  closeFloatingOverlay();
8667
9032
  filterInputRef.current = "";
8668
- filterAppliedRef.current = "";
8669
9033
  setFilterInput("");
8670
- setFilterApplied("");
8671
- setSelectedIndex(() => 0);
9034
+ navigator.flushSearch("");
9035
+ navigator.selectFirst();
8672
9036
  focusRef.current = "sidebar";
8673
9037
  if (focus !== "sidebar")
8674
9038
  setFocus("sidebar");
@@ -8687,13 +9051,14 @@ var Browser = ({
8687
9051
  cycleTheme,
8688
9052
  toggleTone,
8689
9053
  toggleAll: () => {
8690
- if (pendingSelectionPathRef.current === null && selected) {
8691
- pendingSelectionPathRef.current = selected.path;
9054
+ const current = navigator.getSnapshot().selectedFile;
9055
+ if (pendingSelectionPathRef.current === null && current) {
9056
+ pendingSelectionPathRef.current = current.path;
8692
9057
  }
8693
9058
  onToggleAll?.();
8694
9059
  },
8695
9060
  serveCurrent: () => {
8696
- const file = displayedFiles[selectedIndex];
9061
+ const file = navigator.getSnapshot().selectedFile;
8697
9062
  if (!file)
8698
9063
  return;
8699
9064
  let handle = serverRef.current;
@@ -8723,7 +9088,7 @@ var Browser = ({
8723
9088
  process.exit(0);
8724
9089
  },
8725
9090
  editCurrent: () => {
8726
- const file = displayedFiles[selectedIndex];
9091
+ const file = navigator.getSnapshot().selectedFile;
8727
9092
  if (!file)
8728
9093
  return;
8729
9094
  const editor = resolveEditor(process.env);
@@ -8773,7 +9138,7 @@ var Browser = ({
8773
9138
  })();
8774
9139
  },
8775
9140
  copyCurrentContents: () => {
8776
- const file = displayedFiles[selectedIndex];
9141
+ const file = navigator.getSnapshot().selectedFile;
8777
9142
  if (!file)
8778
9143
  return;
8779
9144
  (async () => {
@@ -8856,9 +9221,8 @@ var Browser = ({
8856
9221
  }
8857
9222
  if (filterOpenRef.current && focusRef.current === "sidebar") {
8858
9223
  const closeFilter = (commit) => {
8859
- filterAppliedRef.current = filterInputRef.current;
8860
- setFilterApplied(filterInputRef.current);
8861
- const picked = displayedFiles[selectedIndex] ?? null;
9224
+ const snapshot = navigator.flushSearch(filterInputRef.current);
9225
+ const picked = snapshot.selectedFile;
8862
9226
  const effectiveCommit = commit && picked !== null;
8863
9227
  restoreFilterOnSidebarFocusRef.current = false;
8864
9228
  filterOpenRef.current = false;
@@ -8894,10 +9258,9 @@ var Browser = ({
8894
9258
  }
8895
9259
  if (key.ctrl && key.name === "\\") {
8896
9260
  filterInputRef.current = "";
8897
- filterAppliedRef.current = "";
8898
9261
  setFilterInput("");
8899
- setFilterApplied("");
8900
- setSelectedIndex(() => 0);
9262
+ navigator.flushSearch("");
9263
+ navigator.selectFirst();
8901
9264
  return;
8902
9265
  }
8903
9266
  if (key.name === "backspace" || key.name === "delete") {
@@ -8907,15 +9270,15 @@ var Browser = ({
8907
9270
  }
8908
9271
  filterInputRef.current = filterInputRef.current.slice(0, -1);
8909
9272
  setFilterInput(filterInputRef.current);
8910
- setSelectedIndex(() => 0);
9273
+ navigator.selectIndex(0);
8911
9274
  return;
8912
9275
  }
8913
9276
  if (key.name === "up") {
8914
- setSelectedIndex((i) => Math.max(0, i - 1));
9277
+ navigator.moveBy(-1);
8915
9278
  return;
8916
9279
  }
8917
9280
  if (key.name === "down") {
8918
- setSelectedIndex((i) => Math.min(Math.max(0, displayedFiles.length - 1), i + 1));
9281
+ navigator.moveBy(1);
8919
9282
  return;
8920
9283
  }
8921
9284
  if (key.ctrl || key.meta)
@@ -8929,7 +9292,7 @@ var Browser = ({
8929
9292
  if (char !== null) {
8930
9293
  filterInputRef.current = filterInputRef.current + char;
8931
9294
  setFilterInput(filterInputRef.current);
8932
- setSelectedIndex(() => 0);
9295
+ navigator.selectIndex(0);
8933
9296
  }
8934
9297
  return;
8935
9298
  }
@@ -8949,7 +9312,7 @@ var Browser = ({
8949
9312
  const parsedContent = useMemo2(() => parseFrontmatter(content), [content]);
8950
9313
  const readerEmptyStateTitle = filterHasNoMatches ? `No files match: ${filterInput}` : `${BRAND} ${BRAND_NAME}`;
8951
9314
  const readerEmptyStateVisible = error == null && renderedPath == null;
8952
- useEffect2(() => {
9315
+ useEffect4(() => {
8953
9316
  if (disableReaderEmptyStateRotation)
8954
9317
  return;
8955
9318
  if (readerEmptyStateVisible) {
@@ -8962,25 +9325,6 @@ var Browser = ({
8962
9325
  readerEmptyStateVisibleRef.current = false;
8963
9326
  }, [disableReaderEmptyStateRotation, readerEmptyStateVisible]);
8964
9327
  const discoveryActive = discoveryStatus !== null && discoveryStatus.length > 0;
8965
- const filterRowVisible = files.length > 0 || discoveryActive;
8966
- const sidebarBodyHeight = Math.max(1, height - FOOTER_HEIGHT - HEADER_HEIGHT - 2 - (filterRowVisible ? 1 : 0));
8967
- const maxScroll = Math.max(0, displayedFiles.length - sidebarBodyHeight);
8968
- const desiredScroll = (() => {
8969
- let s = sidebarScroll;
8970
- if (selectedIndex < s)
8971
- s = selectedIndex;
8972
- else if (selectedIndex >= s + sidebarBodyHeight)
8973
- s = selectedIndex - sidebarBodyHeight + 1;
8974
- return clamp3(s, 0, maxScroll);
8975
- })();
8976
- useEffect2(() => {
8977
- if (desiredScroll !== sidebarScroll)
8978
- setSidebarScroll(desiredScroll);
8979
- }, [desiredScroll, sidebarScroll]);
8980
- const visibleFiles = displayedFiles.slice(desiredScroll, desiredScroll + sidebarBodyHeight);
8981
- const sidebarPaneWidth = isNarrow ? width : sidebarWidth;
8982
- const sidebarTextWidth = Math.max(4, sidebarPaneWidth - (isNarrow ? 1 : 2));
8983
- const layoutSidebarRow = useCallback((relativePath) => formatSidebarRow(relativePath, sidebarTextWidth), [sidebarTextWidth]);
8984
9328
  const footerProps = {
8985
9329
  bindings: browserBindings,
8986
9330
  ctx,
@@ -9008,74 +9352,17 @@ var Browser = ({
9008
9352
  };
9009
9353
  const readerEmptyStateTips = useMemo2(() => buildReaderEmptyStateTips(browserBindings, ctx), [ctx]);
9010
9354
  const readerEmptyStateTip = useMemo2(() => pickTipByRotation(readerEmptyStateTips, readerEmptyStateTipRotation), [readerEmptyStateTipRotation, readerEmptyStateTips]);
9011
- const sidebarBody = /* @__PURE__ */ jsxDEV8(Fragment2, {
9012
- children: [
9013
- filterRowVisible && /* @__PURE__ */ jsxDEV8(PromptRow, {
9014
- query: filterInput,
9015
- editing: filterOpen,
9016
- placeholder: "type / to filter",
9017
- width: sidebarTextWidth
9018
- }, undefined, false, undefined, this),
9019
- displayedFiles.length === 0 ? /* @__PURE__ */ jsxDEV8(SidebarEmptyMessage, {
9020
- withTopSpacer: filterRowVisible,
9021
- width: sidebarTextWidth,
9022
- label: files.length === 0 ? discoveryActive ? "Scanning" : "No markdown files in" : "No files match",
9023
- value: files.length === 0 ? discoveryActive ? "\u2026" : rootLabel : filterApplied
9024
- }, undefined, false, undefined, this) : visibleFiles.map((file, idx) => {
9025
- const realIdx = desiredScroll + idx;
9026
- const isSelected = realIdx === selectedIndex;
9027
- const { basename: basename2, separator, parent } = layoutSidebarRow(file.relativePath);
9028
- const basenameFg = isSelected ? colors.selectedListItemText : colors.text;
9029
- const rowStyle = {
9030
- width: sidebarTextWidth,
9031
- height: 1,
9032
- flexDirection: "row",
9033
- ...isSelected ? { backgroundColor: colors.backgroundElement } : {}
9034
- };
9035
- return /* @__PURE__ */ jsxDEV8("box", {
9036
- style: rowStyle,
9037
- children: /* @__PURE__ */ jsxDEV8("text", {
9038
- wrapMode: "none",
9039
- children: [
9040
- /* @__PURE__ */ jsxDEV8("span", {
9041
- style: { fg: basenameFg },
9042
- children: basename2
9043
- }, undefined, false, undefined, this),
9044
- parent !== "" && /* @__PURE__ */ jsxDEV8("span", {
9045
- style: { fg: colors.textMuted },
9046
- children: middleTruncate(`${separator}${parent}`, Math.max(0, sidebarTextWidth - basename2.length))
9047
- }, undefined, false, undefined, this)
9048
- ]
9049
- }, undefined, true, undefined, this)
9050
- }, file.path, false, undefined, this);
9051
- })
9052
- ]
9053
- }, undefined, true, undefined, this);
9054
- const sidebarBorderSides = ["top", "bottom", "right"];
9055
9355
  const readerBorderSides = ["top", "bottom"];
9056
- const SIDEBAR_BORDER_CHARS = {
9057
- topLeft: "\u250C",
9058
- topRight: "\u252C",
9059
- bottomLeft: "\u2514",
9060
- bottomRight: "\u2534",
9061
- horizontal: "\u2500",
9062
- vertical: "\u2502",
9063
- topT: "\u252C",
9064
- bottomT: "\u2534",
9065
- leftT: "\u251C",
9066
- rightT: "\u2524",
9067
- cross: "\u253C"
9068
- };
9069
- const INACTIVE_PANE_OPACITY = 0.62;
9070
- return /* @__PURE__ */ jsxDEV8("box", {
9356
+ const INACTIVE_PANE_OPACITY2 = 0.62;
9357
+ return /* @__PURE__ */ jsxDEV12("box", {
9071
9358
  style: { width, height, flexDirection: "column", backgroundColor: colors.backgroundPanel },
9072
9359
  children: [
9073
- /* @__PURE__ */ jsxDEV8(Header, {
9360
+ /* @__PURE__ */ jsxDEV12(Header, {
9074
9361
  width,
9075
9362
  currentFile,
9076
9363
  rootLabel
9077
9364
  }, undefined, false, undefined, this),
9078
- /* @__PURE__ */ jsxDEV8("box", {
9365
+ /* @__PURE__ */ jsxDEV12("box", {
9079
9366
  style: {
9080
9367
  flexDirection: "row",
9081
9368
  flexGrow: 1,
@@ -9083,28 +9370,20 @@ var Browser = ({
9083
9370
  backgroundColor: colors.backgroundPanel
9084
9371
  },
9085
9372
  children: [
9086
- sidebarInline && /* @__PURE__ */ jsxDEV8("box", {
9087
- style: {
9088
- border: isNarrow ? readerBorderSides : sidebarBorderSides,
9089
- borderColor: colors.border,
9090
- ...isNarrow ? { flexGrow: 1, flexShrink: 1 } : { width: sidebarWidth, flexShrink: 0 },
9091
- flexDirection: "column",
9092
- backgroundColor: colors.backgroundPanel
9093
- },
9094
- ...isNarrow ? {} : { customBorderChars: SIDEBAR_BORDER_CHARS },
9095
- children: /* @__PURE__ */ jsxDEV8("box", {
9096
- style: {
9097
- flexGrow: 1,
9098
- flexShrink: 1,
9099
- flexDirection: "column",
9100
- paddingLeft: 1,
9101
- backgroundColor: sidebarActive ? colors.background : colors.backgroundPanel,
9102
- opacity: sidebarActive ? 1 : INACTIVE_PANE_OPACITY
9103
- },
9104
- children: sidebarBody
9105
- }, 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
9106
9385
  }, undefined, false, undefined, this),
9107
- readerVisible && /* @__PURE__ */ jsxDEV8("box", {
9386
+ readerVisible && /* @__PURE__ */ jsxDEV12("box", {
9108
9387
  style: {
9109
9388
  border: readerBorderSides,
9110
9389
  borderColor: colors.border,
@@ -9113,19 +9392,19 @@ var Browser = ({
9113
9392
  flexDirection: "column",
9114
9393
  backgroundColor: colors.backgroundPanel
9115
9394
  },
9116
- children: /* @__PURE__ */ jsxDEV8("box", {
9395
+ children: /* @__PURE__ */ jsxDEV12("box", {
9117
9396
  style: {
9118
9397
  flexGrow: 1,
9119
9398
  flexShrink: 1,
9120
9399
  flexDirection: "column",
9121
9400
  padding: 1,
9122
9401
  backgroundColor: readerActive ? colors.background : colors.backgroundPanel,
9123
- opacity: readerActive ? 1 : INACTIVE_PANE_OPACITY
9402
+ opacity: readerActive ? 1 : INACTIVE_PANE_OPACITY2
9124
9403
  },
9125
- children: error ? /* @__PURE__ */ jsxDEV8("text", {
9404
+ children: error ? /* @__PURE__ */ jsxDEV12("text", {
9126
9405
  content: error,
9127
9406
  style: { fg: colors.error }
9128
- }, undefined, false, undefined, this) : !renderedPath ? /* @__PURE__ */ jsxDEV8("box", {
9407
+ }, undefined, false, undefined, this) : !renderedPath ? /* @__PURE__ */ jsxDEV12("box", {
9129
9408
  style: {
9130
9409
  flexGrow: 1,
9131
9410
  flexShrink: 1,
@@ -9133,22 +9412,22 @@ var Browser = ({
9133
9412
  justifyContent: "center",
9134
9413
  backgroundColor: readerActive ? colors.background : colors.backgroundPanel
9135
9414
  },
9136
- children: /* @__PURE__ */ jsxDEV8("box", {
9415
+ children: /* @__PURE__ */ jsxDEV12("box", {
9137
9416
  style: { flexDirection: "column", gap: 1, alignItems: "center" },
9138
9417
  children: [
9139
- /* @__PURE__ */ jsxDEV8("text", {
9418
+ /* @__PURE__ */ jsxDEV12("text", {
9140
9419
  content: readerEmptyStateTitle,
9141
9420
  wrapMode: "none",
9142
9421
  style: { fg: colors.textMuted }
9143
9422
  }, undefined, false, undefined, this),
9144
- readerEmptyStateTip && /* @__PURE__ */ jsxDEV8("text", {
9423
+ readerEmptyStateTip && /* @__PURE__ */ jsxDEV12("text", {
9145
9424
  content: readerEmptyStateTip.text,
9146
9425
  wrapMode: "none",
9147
9426
  style: { fg: colors.textMuted }
9148
9427
  }, readerEmptyStateTip.id, false, undefined, this)
9149
9428
  ]
9150
9429
  }, undefined, true, undefined, this)
9151
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV8("scrollbox", {
9430
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV12("scrollbox", {
9152
9431
  style: {
9153
9432
  scrollY: true,
9154
9433
  scrollX: false,
@@ -9158,17 +9437,17 @@ var Browser = ({
9158
9437
  },
9159
9438
  focused: readerActive && !paletteOpen,
9160
9439
  children: [
9161
- parsedContent.fields.length > 0 && /* @__PURE__ */ jsxDEV8("box", {
9440
+ parsedContent.fields.length > 0 && /* @__PURE__ */ jsxDEV12("box", {
9162
9441
  style: { flexDirection: "column", marginBottom: 1 },
9163
- children: parsedContent.fields.map((field) => /* @__PURE__ */ jsxDEV8("box", {
9442
+ children: parsedContent.fields.map((field) => /* @__PURE__ */ jsxDEV12("box", {
9164
9443
  style: { flexDirection: "row", gap: 1, flexWrap: "wrap" },
9165
9444
  children: [
9166
- /* @__PURE__ */ jsxDEV8("text", {
9445
+ /* @__PURE__ */ jsxDEV12("text", {
9167
9446
  content: `${field.key}:`,
9168
9447
  wrapMode: "word",
9169
9448
  style: { fg: colors.secondary }
9170
9449
  }, undefined, false, undefined, this),
9171
- /* @__PURE__ */ jsxDEV8("text", {
9450
+ /* @__PURE__ */ jsxDEV12("text", {
9172
9451
  content: field.value,
9173
9452
  wrapMode: "word",
9174
9453
  style: { fg: colors.textMuted }
@@ -9176,7 +9455,7 @@ var Browser = ({
9176
9455
  ]
9177
9456
  }, field.key, true, undefined, this))
9178
9457
  }, undefined, false, undefined, this),
9179
- /* @__PURE__ */ jsxDEV8("markdown", {
9458
+ /* @__PURE__ */ jsxDEV12("markdown", {
9180
9459
  content: parsedContent.body,
9181
9460
  syntaxStyle,
9182
9461
  fg: colors.text,
@@ -9190,17 +9469,17 @@ var Browser = ({
9190
9469
  }, undefined, false, undefined, this)
9191
9470
  ]
9192
9471
  }, undefined, true, undefined, this),
9193
- /* @__PURE__ */ jsxDEV8(Footer, {
9472
+ /* @__PURE__ */ jsxDEV12(Footer, {
9194
9473
  ...footerProps
9195
9474
  }, undefined, false, undefined, this),
9196
- paletteOpen && /* @__PURE__ */ jsxDEV8(CommandPalette, {
9475
+ paletteOpen && /* @__PURE__ */ jsxDEV12(CommandPalette, {
9197
9476
  commands: orderCommandsForPalette(filterCommands(buildCommands(ctx), paletteQuery)),
9198
9477
  query: paletteQuery,
9199
9478
  selectedIndex: paletteIndex,
9200
9479
  viewportWidth: width,
9201
9480
  viewportHeight: height
9202
9481
  }, undefined, false, undefined, this),
9203
- activeStatusPopover && /* @__PURE__ */ jsxDEV8(StatusPopoverPanel, {
9482
+ activeStatusPopover && /* @__PURE__ */ jsxDEV12(StatusPopoverPanel, {
9204
9483
  content: activeStatusPopover.content,
9205
9484
  variant: "warning"
9206
9485
  }, undefined, false, undefined, this)
@@ -9677,13 +9956,13 @@ var subscribeUpdateInfo = (cb) => {
9677
9956
  };
9678
9957
 
9679
9958
  // src/update/useUpdateNotice.ts
9680
- import { useEffect as useEffect3, useState as useState4 } from "react";
9959
+ import { useEffect as useEffect5, useState as useState5 } from "react";
9681
9960
  var useUpdateNotice = () => {
9682
- const [text, setText] = useState4(() => {
9961
+ const [text, setText] = useState5(() => {
9683
9962
  const cur = currentUpdateInfo();
9684
9963
  return cur ? formatFooterNotice(cur) : null;
9685
9964
  });
9686
- useEffect3(() => {
9965
+ useEffect5(() => {
9687
9966
  const unsub = subscribeUpdateInfo((info) => setText(formatFooterNotice(info)));
9688
9967
  return unsub;
9689
9968
  }, []);
@@ -9691,7 +9970,7 @@ var useUpdateNotice = () => {
9691
9970
  };
9692
9971
 
9693
9972
  // src/index.tsx
9694
- import { jsxDEV as jsxDEV9 } from "@opentui/react/jsx-dev-runtime";
9973
+ import { jsxDEV as jsxDEV13 } from "@opentui/react/jsx-dev-runtime";
9695
9974
  var pathIsDirectory = async (path) => {
9696
9975
  try {
9697
9976
  return (await stat(path)).isDirectory();
@@ -9755,14 +10034,14 @@ var DiscoverShell = ({
9755
10034
  startupFocus
9756
10035
  }) => {
9757
10036
  const updateNotice = useUpdateNotice();
9758
- const [show, setShow] = useState5(initialShow);
9759
- const [files, setFiles] = useState5([]);
9760
- const [scanning, setScanning] = useState5(true);
9761
- const [scanError, setScanError] = useState5(null);
9762
- const [skippedDirCount, setSkippedDirCount] = useState5(0);
9763
- const [lastSkippedDir, setLastSkippedDir] = useState5(null);
9764
- const countRef = useRef3(0);
9765
- 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(() => {
9766
10045
  setFiles([]);
9767
10046
  setScanning(true);
9768
10047
  setScanError(null);
@@ -9801,7 +10080,7 @@ var DiscoverShell = ({
9801
10080
  skippedCount: skippedDirCount,
9802
10081
  lastSkippedPath: lastSkippedDir
9803
10082
  }));
9804
- return /* @__PURE__ */ jsxDEV9(Browser, {
10083
+ return /* @__PURE__ */ jsxDEV13(Browser, {
9805
10084
  files,
9806
10085
  initialQuery,
9807
10086
  wrapWidth,
@@ -9981,9 +10260,9 @@ async function runTui({
9981
10260
  console.error(`house: ${err instanceof Error ? err.message : String(err)}`);
9982
10261
  process.exit(1);
9983
10262
  }
9984
- createRoot(renderer).render(/* @__PURE__ */ jsxDEV9(RegistryProvider, {
10263
+ createRoot(renderer).render(/* @__PURE__ */ jsxDEV13(RegistryProvider, {
9985
10264
  initialValues: [[themeAtom, initialTheme]],
9986
- children: /* @__PURE__ */ jsxDEV9(DiscoverShell, {
10265
+ children: /* @__PURE__ */ jsxDEV13(DiscoverShell, {
9987
10266
  target: discoveryRoot,
9988
10267
  initialQuery,
9989
10268
  initialShow: show,