@carlesandres/house 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2711,14 +2711,14 @@ var require_filesystem = __commonJS((exports, module) => {
2711
2711
  fs.close(fd, () => {});
2712
2712
  return buffer.subarray(0, bytesRead);
2713
2713
  };
2714
- var readFile3 = (path) => new Promise((resolve3, reject) => {
2714
+ var readFile3 = (path) => new Promise((resolve4, reject) => {
2715
2715
  fs.open(path, "r", (err, fd) => {
2716
2716
  if (err) {
2717
2717
  reject(err);
2718
2718
  } else {
2719
2719
  const buffer = Buffer.alloc(MAX_LENGTH);
2720
2720
  fs.read(fd, buffer, 0, MAX_LENGTH, 0, (_, bytesRead) => {
2721
- resolve3(buffer.subarray(0, bytesRead));
2721
+ resolve4(buffer.subarray(0, bytesRead));
2722
2722
  fs.close(fd, () => {});
2723
2723
  });
2724
2724
  }
@@ -2779,10 +2779,10 @@ var require_detect_libc = __commonJS((exports, module) => {
2779
2779
  var commandOut = "";
2780
2780
  var safeCommand = () => {
2781
2781
  if (!commandOut) {
2782
- return new Promise((resolve3) => {
2782
+ return new Promise((resolve4) => {
2783
2783
  childProcess.exec(command, (err, out) => {
2784
2784
  commandOut = err ? " " : out;
2785
- resolve3(commandOut);
2785
+ resolve4(commandOut);
2786
2786
  });
2787
2787
  });
2788
2788
  }
@@ -3055,9 +3055,9 @@ var require_watcher = __commonJS((exports) => {
3055
3055
  });
3056
3056
 
3057
3057
  // src/index.tsx
3058
- import { stat as stat2 } from "fs/promises";
3058
+ import { stat as stat3 } from "fs/promises";
3059
3059
  import { homedir as homedir3 } from "os";
3060
- import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
3060
+ import { basename as basename4, dirname as dirname5, isAbsolute as isAbsolute3, relative as relative5, resolve as resolve4 } from "path";
3061
3061
  import { createCliRenderer } from "@opentui/core";
3062
3062
  import { createRoot } from "@opentui/react";
3063
3063
  import { RegistryProvider } from "@effect/atom-react";
@@ -3066,7 +3066,7 @@ import { useState as useState6 } from "react";
3066
3066
  // package.json
3067
3067
  var package_default = {
3068
3068
  name: "@carlesandres/house",
3069
- version: "0.6.0",
3069
+ version: "0.8.0",
3070
3070
  description: "TUI-first markdown reader on opentui",
3071
3071
  type: "module",
3072
3072
  license: "MIT",
@@ -3160,15 +3160,287 @@ var package_default = {
3160
3160
  };
3161
3161
 
3162
3162
  // src/Browser.tsx
3163
- import { existsSync } from "fs";
3164
- import { join as join3 } from "path";
3163
+ import { join as join4 } from "path";
3165
3164
  import { SyntaxStyle } from "@opentui/core";
3165
+
3166
+ // ../../packages/options/src/footer.ts
3167
+ var footerActivate = (spec) => {
3168
+ if (spec.footer?.activate !== undefined)
3169
+ return spec.footer.activate;
3170
+ if (spec.type === "boolean")
3171
+ return "toggle";
3172
+ if (spec.type === "string" && spec.choices !== undefined && spec.choices.length > 0) {
3173
+ return "cycle";
3174
+ }
3175
+ return;
3176
+ };
3177
+ var footerKeys = (specs) => {
3178
+ const keys = [];
3179
+ for (const key of Object.keys(specs)) {
3180
+ if (specs[key]?.footer !== undefined)
3181
+ keys.push(key);
3182
+ }
3183
+ return keys;
3184
+ };
3185
+ var nextFooterValue = (spec, current) => {
3186
+ const activate = footerActivate(spec);
3187
+ if (activate === "toggle") {
3188
+ if (spec.type !== "boolean") {
3189
+ throw new Error('footer activate "toggle" requires a boolean option');
3190
+ }
3191
+ return !current;
3192
+ }
3193
+ if (activate === "cycle") {
3194
+ if (spec.type !== "string" || spec.choices === undefined || spec.choices.length === 0) {
3195
+ throw new Error('footer activate "cycle" requires a string option with choices');
3196
+ }
3197
+ const choices = spec.choices;
3198
+ const currentIndex = choices.indexOf(current);
3199
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % choices.length;
3200
+ return choices[nextIndex];
3201
+ }
3202
+ throw new Error("option has no footer activate strategy");
3203
+ };
3204
+ var footerControlActive = (spec, value) => {
3205
+ if (spec.type === "boolean")
3206
+ return Boolean(value);
3207
+ return true;
3208
+ };
3209
+ var footerControlGlyph = (spec, value) => {
3210
+ const footer = spec.footer;
3211
+ if (footer === undefined) {
3212
+ throw new Error("footerControlGlyph requires a footer-opted spec");
3213
+ }
3214
+ if (typeof value === "string" && footer.labels !== undefined) {
3215
+ const label = footer.labels[value];
3216
+ if (label !== undefined && label.length > 0)
3217
+ return label;
3218
+ }
3219
+ return footer.icon;
3220
+ };
3221
+ var validateFooterSpecs = (specs) => {
3222
+ for (const key of Object.keys(specs)) {
3223
+ const spec = specs[key];
3224
+ const footer = spec.footer;
3225
+ if (footer === undefined)
3226
+ continue;
3227
+ if (footer.icon.length === 0) {
3228
+ throw new Error(`option ${JSON.stringify(key)}: footer.icon must be non-empty`);
3229
+ }
3230
+ const activate = footerActivate(spec);
3231
+ if (activate === undefined) {
3232
+ throw new Error(`option ${JSON.stringify(key)}: footer requires activate "toggle" or "cycle" ` + "(booleans toggle; strings with choices cycle; other types need an explicit strategy)");
3233
+ }
3234
+ if (activate === "toggle" && spec.type !== "boolean") {
3235
+ throw new Error(`option ${JSON.stringify(key)}: footer activate "toggle" requires type "boolean"`);
3236
+ }
3237
+ if (activate === "cycle") {
3238
+ if (spec.type !== "string" || spec.choices === undefined || spec.choices.length === 0) {
3239
+ throw new Error(`option ${JSON.stringify(key)}: footer activate "cycle" requires string choices`);
3240
+ }
3241
+ const labels = footer.labels;
3242
+ if (labels === undefined) {
3243
+ throw new Error(`option ${JSON.stringify(key)}: footer.labels is required for choice options`);
3244
+ }
3245
+ for (const choice of spec.choices) {
3246
+ const label = labels[choice];
3247
+ if (label === undefined || label.length === 0) {
3248
+ throw new Error(`option ${JSON.stringify(key)}: footer.labels missing entry for ${JSON.stringify(choice)}`);
3249
+ }
3250
+ }
3251
+ }
3252
+ }
3253
+ };
3254
+
3255
+ // ../../packages/options/src/decode.ts
3256
+ var isPresent = (value) => value !== undefined && value !== null;
3257
+ var stringifyReceived = (value) => {
3258
+ if (typeof value === "string")
3259
+ return JSON.stringify(value);
3260
+ if (typeof value === "number" || typeof value === "boolean")
3261
+ return JSON.stringify(value);
3262
+ if (value === null)
3263
+ return "null";
3264
+ return typeof value;
3265
+ };
3266
+ var formatResolveError = (error, filePath) => {
3267
+ if (error.layer === "file") {
3268
+ const where = filePath === undefined ? "" : ` in ${filePath}`;
3269
+ return `invalid value for ${error.key}${where}: ${error.message}`;
3270
+ }
3271
+ return `${error.key}: ${error.message}, got ${stringifyReceived(error.received)}`;
3272
+ };
3273
+ var fail = (key, layer, message, received) => ({
3274
+ key,
3275
+ layer,
3276
+ message,
3277
+ received
3278
+ });
3279
+ var booleanMessage = "expected true or false";
3280
+ var numberMessage = (spec) => {
3281
+ if (spec.integer === true && spec.min === 1 && spec.max === undefined) {
3282
+ return "expected a positive integer";
3283
+ }
3284
+ if (spec.integer === true)
3285
+ return "expected an integer";
3286
+ return "expected a number";
3287
+ };
3288
+ var inRange = (value, spec) => {
3289
+ if (spec.min !== undefined && value < spec.min)
3290
+ return false;
3291
+ if (spec.max !== undefined && value > spec.max)
3292
+ return false;
3293
+ if (spec.integer === true && !Number.isSafeInteger(value))
3294
+ return false;
3295
+ if (!Number.isFinite(value))
3296
+ return false;
3297
+ return true;
3298
+ };
3299
+ var decodeBoolean = (key, layer, value) => {
3300
+ if (layer === "env") {
3301
+ if (value === "true")
3302
+ return { ok: true, value: true };
3303
+ if (value === "false")
3304
+ return { ok: true, value: false };
3305
+ return { ok: false, error: fail(key, layer, booleanMessage, value) };
3306
+ }
3307
+ if (typeof value === "boolean")
3308
+ return { ok: true, value };
3309
+ return { ok: false, error: fail(key, layer, booleanMessage, value) };
3310
+ };
3311
+ var decodeNumber = (key, layer, value, spec) => {
3312
+ const message = numberMessage(spec);
3313
+ if (layer === "env") {
3314
+ if (typeof value !== "string" || !/^-?\d+(\.\d+)?$/.test(value)) {
3315
+ return { ok: false, error: fail(key, layer, message, value) };
3316
+ }
3317
+ if (spec.integer === true && !/^-?\d+$/.test(value)) {
3318
+ return { ok: false, error: fail(key, layer, message, value) };
3319
+ }
3320
+ const parsed = spec.integer === true ? Number.parseInt(value, 10) : Number.parseFloat(value);
3321
+ if (!inRange(parsed, spec))
3322
+ return { ok: false, error: fail(key, layer, message, value) };
3323
+ return { ok: true, value: parsed };
3324
+ }
3325
+ if (typeof value !== "number" || !inRange(value, spec)) {
3326
+ return { ok: false, error: fail(key, layer, message, value) };
3327
+ }
3328
+ return { ok: true, value };
3329
+ };
3330
+ var decodeString = (key, layer, value, spec) => {
3331
+ if (typeof value !== "string") {
3332
+ const message = spec.choices === undefined ? "expected a string" : `expected one of ${spec.choices.join(", ")}`;
3333
+ return { ok: false, error: fail(key, layer, message, value) };
3334
+ }
3335
+ if (spec.choices !== undefined && !spec.choices.includes(value)) {
3336
+ return {
3337
+ ok: false,
3338
+ error: fail(key, layer, `expected one of ${spec.choices.join(", ")}`, value)
3339
+ };
3340
+ }
3341
+ return { ok: true, value };
3342
+ };
3343
+ var decodeValue = (key, spec, layer, value) => {
3344
+ if (spec.type === "boolean")
3345
+ return decodeBoolean(key, layer, value);
3346
+ if (spec.type === "number")
3347
+ return decodeNumber(key, layer, value, spec);
3348
+ return decodeString(key, layer, value, spec);
3349
+ };
3350
+
3351
+ // ../../packages/options/src/resolve.ts
3352
+ var LAYERS = ["cli", "env", "file"];
3353
+ var resolveOptions = (specs, layers = {}) => {
3354
+ const values = {};
3355
+ for (const key of Object.keys(specs)) {
3356
+ const spec = specs[key];
3357
+ let found = false;
3358
+ for (const layer of LAYERS) {
3359
+ const source = layers[layer];
3360
+ if (source === undefined)
3361
+ continue;
3362
+ const raw = source[key];
3363
+ if (!isPresent(raw))
3364
+ continue;
3365
+ const decoded = decodeValue(key, spec, layer, raw);
3366
+ if (!decoded.ok)
3367
+ return decoded;
3368
+ values[key] = decoded.value;
3369
+ found = true;
3370
+ break;
3371
+ }
3372
+ if (!found)
3373
+ values[key] = spec.default;
3374
+ }
3375
+ return { ok: true, value: values };
3376
+ };
3377
+ var catalogDefaults = (specs) => {
3378
+ const values = {};
3379
+ for (const key of Object.keys(specs))
3380
+ values[key] = specs[key].default;
3381
+ return values;
3382
+ };
3383
+
3384
+ // ../../packages/options/src/session.ts
3385
+ var persistPolicy = (persist) => persist ?? "session";
3386
+ var createSession = (specs, values, options = {}) => {
3387
+ let snapshot = values;
3388
+ const listeners = new Set;
3389
+ const get = (key) => snapshot[key];
3390
+ const set = async (key, value) => {
3391
+ const spec = specs[key];
3392
+ if (spec === undefined) {
3393
+ throw new Error(`unknown option ${JSON.stringify(key)}`);
3394
+ }
3395
+ const decoded = decodeValue(key, spec, "cli", value);
3396
+ if (!decoded.ok) {
3397
+ throw new Error(`${decoded.error.key}: ${decoded.error.message}`);
3398
+ }
3399
+ if (Object.is(snapshot[key], decoded.value))
3400
+ return;
3401
+ snapshot = { ...snapshot, [key]: decoded.value };
3402
+ for (const listener of listeners)
3403
+ listener(snapshot);
3404
+ if (persistPolicy(spec.persist) === "file" && options.persist !== undefined) {
3405
+ await options.persist({
3406
+ key,
3407
+ value: decoded.value,
3408
+ values: snapshot
3409
+ });
3410
+ }
3411
+ };
3412
+ return {
3413
+ get,
3414
+ set,
3415
+ subscribe: (listener) => {
3416
+ listeners.add(listener);
3417
+ return () => {
3418
+ listeners.delete(listener);
3419
+ };
3420
+ },
3421
+ get values() {
3422
+ return snapshot;
3423
+ }
3424
+ };
3425
+ };
3426
+
3427
+ // ../../packages/options/src/define.ts
3428
+ var defineOptions = (specs) => {
3429
+ validateFooterSpecs(specs);
3430
+ return {
3431
+ specs,
3432
+ defaults: catalogDefaults(specs),
3433
+ resolve: (layers) => resolveOptions(specs, layers),
3434
+ createSession: (values, options) => createSession(specs, values, options)
3435
+ };
3436
+ };
3437
+ // src/Browser.tsx
3166
3438
  import { useKeyboard, useRenderer, useTerminalDimensions as useTerminalDimensions2 } from "@opentui/react";
3167
3439
  import { useAtomValue, useAtomSet } from "@effect/atom-react";
3168
3440
  import { Effect as Effect3 } from "effect";
3169
3441
  import {
3170
3442
  useEffect as useEffect4,
3171
- useLayoutEffect as useLayoutEffect2,
3443
+ useLayoutEffect as useLayoutEffect3,
3172
3444
  useMemo as useMemo2,
3173
3445
  useReducer as useReducer2,
3174
3446
  useRef as useRef3,
@@ -3238,7 +3510,7 @@ var browserBindings = [
3238
3510
  group: "Global",
3239
3511
  description: "Command palette",
3240
3512
  hint: "palette",
3241
- keys: ["ctrl+p"],
3513
+ keys: ["ctrl+p", "?"],
3242
3514
  hintWhen: () => true,
3243
3515
  when: paletteClosed,
3244
3516
  run: (c) => c.openPalette()
@@ -3373,6 +3645,14 @@ var browserBindings = [
3373
3645
  keys: ["shift+n"],
3374
3646
  run: (c) => c.openNewFilePrompt()
3375
3647
  },
3648
+ {
3649
+ id: "file.rename",
3650
+ group: "File",
3651
+ description: "Rename selected file",
3652
+ keys: ["shift+r"],
3653
+ when: hasSelected,
3654
+ run: (c) => c.openRenamePrompt()
3655
+ },
3376
3656
  {
3377
3657
  id: "reader.prevFile",
3378
3658
  group: "File",
@@ -3442,6 +3722,11 @@ var annotations = {
3442
3722
  category: "File",
3443
3723
  keywords: ["new", "create", "nvim", "vim", "editor"]
3444
3724
  },
3725
+ "file.rename": {
3726
+ title: "Rename\u2026",
3727
+ category: "File",
3728
+ keywords: ["rename", "mv", "basename"]
3729
+ },
3445
3730
  "theme.next": { category: "Appearance" },
3446
3731
  "theme.prev": { category: "Appearance" },
3447
3732
  "theme.toneToggle": { title: "Toggle dark/light tone", category: "Appearance" },
@@ -3695,43 +3980,54 @@ var setActiveTheme = (definition, tone) => {
3695
3980
  };
3696
3981
 
3697
3982
  // src/PromptRow.tsx
3983
+ import { useLayoutEffect } from "react";
3698
3984
  import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
3699
3985
  var PREFIX = "> ";
3700
- var CURSOR = "\u258F";
3701
3986
  var PromptRow = ({
3702
3987
  query,
3703
3988
  editing,
3704
3989
  placeholder = "",
3705
3990
  showPlaceholderWhileEditing = false,
3706
- width
3991
+ width,
3992
+ onInput,
3993
+ onEditingReady
3707
3994
  }) => {
3708
3995
  const bodyBudget = Math.max(1, width - PREFIX.length);
3709
- const placeholderWhileEditing = editing && query.length === 0 && showPlaceholderWhileEditing && placeholder.length > 0;
3710
- const rawBody = editing ? `${query}${CURSOR}` : query.length > 0 ? query : placeholder;
3711
- const bodyFg = editing ? colors.primary : query.length > 0 ? colors.text : colors.textMuted;
3712
- const body = rawBody.length <= bodyBudget ? rawBody : editing ? "\u2026" + rawBody.slice(rawBody.length - bodyBudget + 1) : rawBody.slice(0, bodyBudget - 1) + "\u2026";
3713
3996
  const prefixFg = editing ? colors.secondary : colors.primary;
3714
- if (placeholderWhileEditing) {
3715
- const placeholderBudget = Math.max(1, bodyBudget - CURSOR.length);
3716
- const placeholderBody = placeholder.length <= placeholderBudget ? placeholder : "\u2026" + placeholder.slice(placeholder.length - placeholderBudget + 1);
3717
- return /* @__PURE__ */ jsxDEV("text", {
3718
- wrapMode: "none",
3997
+ useLayoutEffect(() => {
3998
+ onEditingReady?.(editing);
3999
+ return () => onEditingReady?.(false);
4000
+ }, [editing, onEditingReady]);
4001
+ if (editing) {
4002
+ return /* @__PURE__ */ jsxDEV("box", {
4003
+ style: { flexDirection: "row", width, height: 1, flexShrink: 0 },
3719
4004
  children: [
3720
- /* @__PURE__ */ jsxDEV("span", {
3721
- style: { fg: prefixFg },
3722
- children: PREFIX
3723
- }, undefined, false, undefined, this),
3724
- /* @__PURE__ */ jsxDEV("span", {
3725
- style: { fg: colors.textMuted },
3726
- children: placeholderBody
4005
+ /* @__PURE__ */ jsxDEV("text", {
4006
+ wrapMode: "none",
4007
+ children: /* @__PURE__ */ jsxDEV("span", {
4008
+ style: { fg: prefixFg },
4009
+ children: PREFIX
4010
+ }, undefined, false, undefined, this)
3727
4011
  }, undefined, false, undefined, this),
3728
- /* @__PURE__ */ jsxDEV("span", {
3729
- style: { fg: colors.primary },
3730
- children: CURSOR
4012
+ /* @__PURE__ */ jsxDEV("input", {
4013
+ focused: true,
4014
+ value: query,
4015
+ placeholder: showPlaceholderWhileEditing ? placeholder : "",
4016
+ ...onInput === undefined ? {} : { onInput },
4017
+ backgroundColor: "transparent",
4018
+ focusedBackgroundColor: "transparent",
4019
+ textColor: colors.primary,
4020
+ focusedTextColor: colors.primary,
4021
+ cursorColor: colors.primary,
4022
+ placeholderColor: colors.textMuted,
4023
+ width: bodyBudget
3731
4024
  }, undefined, false, undefined, this)
3732
4025
  ]
3733
4026
  }, undefined, true, undefined, this);
3734
4027
  }
4028
+ const rawBody = query.length > 0 ? query : placeholder;
4029
+ const bodyFg = query.length > 0 ? colors.text : colors.textMuted;
4030
+ const body = rawBody.length <= bodyBudget ? rawBody : rawBody.slice(0, bodyBudget - 1) + "\u2026";
3735
4031
  return /* @__PURE__ */ jsxDEV("text", {
3736
4032
  wrapMode: "none",
3737
4033
  children: [
@@ -3791,7 +4087,9 @@ var CommandPalette = ({
3791
4087
  query,
3792
4088
  selectedIndex,
3793
4089
  viewportWidth,
3794
- viewportHeight
4090
+ viewportHeight,
4091
+ onQueryChange,
4092
+ onInputReady
3795
4093
  }) => {
3796
4094
  const overlayWidth = Math.min(viewportWidth - 4, 64);
3797
4095
  const rows = buildRows(commands);
@@ -3846,7 +4144,9 @@ var CommandPalette = ({
3846
4144
  /* @__PURE__ */ jsxDEV2(PromptRow, {
3847
4145
  query,
3848
4146
  editing: true,
3849
- width: rowWidth
4147
+ width: rowWidth,
4148
+ onInput: onQueryChange,
4149
+ ...onInputReady === undefined ? {} : { onEditingReady: onInputReady }
3850
4150
  }, undefined, false, undefined, this),
3851
4151
  /* @__PURE__ */ jsxDEV2("text", {
3852
4152
  content: " "
@@ -3904,6 +4204,23 @@ var CommandPalette = ({
3904
4204
 
3905
4205
  // src/PromptModal.tsx
3906
4206
  import { RGBA as RGBA2 } from "@opentui/core";
4207
+
4208
+ // src/ui/middleTruncate.ts
4209
+ var middleTruncate = (value, width, options = {}) => {
4210
+ const ellipsis = options.ellipsis ?? "\u2026";
4211
+ if (width <= 0)
4212
+ return "";
4213
+ if (value.length <= width)
4214
+ return value;
4215
+ if (width <= ellipsis.length)
4216
+ return value.slice(0, width);
4217
+ const available = width - ellipsis.length;
4218
+ const left = Math.ceil(available / 2);
4219
+ const right = Math.floor(available / 2);
4220
+ return value.slice(0, left) + ellipsis + value.slice(value.length - right);
4221
+ };
4222
+
4223
+ // src/PromptModal.tsx
3907
4224
  import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
3908
4225
  var SCRIM2 = RGBA2.fromInts(0, 0, 0, 150);
3909
4226
  var PromptStatusLine = ({
@@ -3924,13 +4241,18 @@ var PromptModal = ({
3924
4241
  placeholder,
3925
4242
  hints,
3926
4243
  status,
4244
+ context,
3927
4245
  viewportWidth,
3928
- viewportHeight
4246
+ viewportHeight,
4247
+ onQueryChange,
4248
+ onInputReady,
4249
+ inputEnabled = true
3929
4250
  }) => {
3930
4251
  const overlayWidth = Math.min(viewportWidth - 4, 64);
3931
4252
  const statusLines = status?.lines ?? [];
3932
4253
  const statusHeight = Math.max(1, statusLines.length);
3933
- const overlayHeight = 2 + 1 + statusHeight + 1;
4254
+ const contextHeight = context !== undefined && context.length > 0 ? 1 : 0;
4255
+ const overlayHeight = 2 + contextHeight + 1 + statusHeight + 1;
3934
4256
  const left = Math.max(0, Math.floor((viewportWidth - overlayWidth) / 2));
3935
4257
  const top = Math.max(0, Math.floor((viewportHeight - overlayHeight) / 2));
3936
4258
  const rowWidth = Math.max(4, overlayWidth - 4);
@@ -3960,12 +4282,19 @@ var PromptModal = ({
3960
4282
  backgroundColor: colors.backgroundPanel
3961
4283
  },
3962
4284
  children: [
4285
+ context !== undefined && context.length > 0 && /* @__PURE__ */ jsxDEV3("text", {
4286
+ wrapMode: "none",
4287
+ content: middleTruncate(context, rowWidth),
4288
+ style: { fg: colors.textMuted }
4289
+ }, undefined, false, undefined, this),
3963
4290
  /* @__PURE__ */ jsxDEV3(PromptRow, {
3964
4291
  query,
3965
- editing: true,
4292
+ editing: inputEnabled,
3966
4293
  placeholder,
3967
4294
  showPlaceholderWhileEditing: true,
3968
- width: rowWidth
4295
+ width: rowWidth,
4296
+ onInput: onQueryChange,
4297
+ ...onInputReady === undefined ? {} : { onEditingReady: onInputReady }
3969
4298
  }, undefined, false, undefined, this),
3970
4299
  status === null || statusLines.length === 0 ? /* @__PURE__ */ jsxDEV3("text", {
3971
4300
  content: " "
@@ -4034,7 +4363,7 @@ var BRAND = "\u2302";
4034
4363
  var BRAND_NAME = "house";
4035
4364
 
4036
4365
  // src/Footer.tsx
4037
- import stringWidth from "string-width";
4366
+ import stringWidth2 from "string-width";
4038
4367
 
4039
4368
  // src/keymap/displayKey.ts
4040
4369
  var displayKey = (raw) => {
@@ -4089,7 +4418,9 @@ var Spinner = ({
4089
4418
  };
4090
4419
 
4091
4420
  // src/StatusIndicator.tsx
4421
+ import stringWidth from "string-width";
4092
4422
  import { jsxDEV as jsxDEV5 } from "@opentui/react/jsx-dev-runtime";
4423
+ var statusIndicatorWidth = (icon) => 2 + Math.max(1, stringWidth(icon));
4093
4424
  var statusIndicatorFg = (variant) => {
4094
4425
  switch (variant) {
4095
4426
  case "info":
@@ -4108,18 +4439,20 @@ var StatusIndicator = ({
4108
4439
  active = true,
4109
4440
  onMouseUp
4110
4441
  }) => {
4442
+ const glyph = icon.toLocaleUpperCase();
4111
4443
  const activeColor = statusIndicatorFg(variant);
4112
4444
  const backgroundColor = active ? activeColor : colors.backgroundElement;
4445
+ const width = statusIndicatorWidth(glyph);
4113
4446
  return /* @__PURE__ */ jsxDEV5("box", {
4114
4447
  ...onMouseUp === undefined ? {} : { onMouseUp },
4115
4448
  style: {
4116
- width: 3,
4449
+ width,
4117
4450
  height: 1,
4118
4451
  flexDirection: "row",
4119
4452
  backgroundColor
4120
4453
  },
4121
4454
  children: /* @__PURE__ */ jsxDEV5("text", {
4122
- content: ` ${icon} `,
4455
+ content: ` ${glyph} `,
4123
4456
  wrapMode: "none",
4124
4457
  style: {
4125
4458
  fg: active ? colors.backgroundPanel : colors.textMuted,
@@ -4135,7 +4468,7 @@ import { jsxDEV as jsxDEV6 } from "@opentui/react/jsx-dev-runtime";
4135
4468
  var FOOTER_HEIGHT = 1;
4136
4469
  var normalizeStatusLine = (status) => status.replace(/\s+/g, " ").trim();
4137
4470
  var HINT_SEPARATOR = " ";
4138
- var hintWidth = (h) => stringWidth(h.key) + 1 + stringWidth(h.label);
4471
+ var hintWidth = (h) => stringWidth2(h.key) + 1 + stringWidth2(h.label);
4139
4472
  var formatHint = (b) => {
4140
4473
  if (!b.hint)
4141
4474
  return null;
@@ -4162,6 +4495,8 @@ var fitHints = (hints, width) => {
4162
4495
  return [{ key: first.key.slice(0, width), label: "" }];
4163
4496
  };
4164
4497
  var STATUS_SEPARATOR = " \xB7 ";
4498
+ var INDICATOR_SEPARATOR = " ";
4499
+ var INDICATOR_SEPARATOR_WIDTH = 1;
4165
4500
  var NON_WARNING_STATUS_PREFIX_WIDTH = 2;
4166
4501
  var isPartialDiscoveryWarning = (status) => status?.startsWith("scan incomplete:") ?? false;
4167
4502
  var Footer = ({
@@ -4209,7 +4544,9 @@ var Footer = ({
4209
4544
  ] : [],
4210
4545
  ...indicators
4211
4546
  ];
4212
- const indicatorBudget = Math.min(usableWidth, renderedIndicators.length * 3);
4547
+ const indicatorWidths = renderedIndicators.map((indicator) => statusIndicatorWidth(indicator.icon));
4548
+ const indicatorSeparatorBudget = renderedIndicators.length > 1 ? (renderedIndicators.length - 1) * INDICATOR_SEPARATOR_WIDTH : 0;
4549
+ const indicatorBudget = Math.min(usableWidth, indicatorWidths.reduce((sum, width2) => sum + width2, 0) + indicatorSeparatorBudget);
4213
4550
  const hasContentAfterIndicators = notice !== null || status !== null || hints.length > 0;
4214
4551
  const indicatorContentGapBudget = renderedIndicators.length > 0 && hasContentAfterIndicators && indicatorBudget < usableWidth ? 1 : 0;
4215
4552
  const contentBudget = Math.max(0, usableWidth - indicatorBudget - indicatorContentGapBudget);
@@ -4221,9 +4558,21 @@ var Footer = ({
4221
4558
  const statusContent = status ? status.slice(0, Math.max(0, statusBudget - statusChromeWidth)) : "";
4222
4559
  const statusSeparatorContent = status ? STATUS_SEPARATOR.slice(0, Math.max(0, statusBudget - nonWarningStatusPrefixBudget - statusContent.length)) : "";
4223
4560
  const noticeContent = notice ? notice.length > contentBudget ? notice.slice(0, contentBudget) : notice : null;
4224
- const renderIndicators = () => renderedIndicators.map(({ id, ...props }) => /* @__PURE__ */ jsxDEV6(StatusIndicator, {
4225
- ...props
4226
- }, id, false, undefined, this));
4561
+ const renderIndicators = () => renderedIndicators.flatMap(({ id, ...props }, index) => {
4562
+ const chip = /* @__PURE__ */ jsxDEV6(StatusIndicator, {
4563
+ ...props
4564
+ }, id, false, undefined, this);
4565
+ if (index === 0)
4566
+ return [chip];
4567
+ return [
4568
+ /* @__PURE__ */ jsxDEV6("text", {
4569
+ content: INDICATOR_SEPARATOR,
4570
+ wrapMode: "none",
4571
+ style: { fg: colors.textMuted }
4572
+ }, `indicator-sep-${id}`, false, undefined, this),
4573
+ chip
4574
+ ];
4575
+ });
4227
4576
  const renderIndicatorContentGap = () => indicatorContentGapBudget > 0 ? /* @__PURE__ */ jsxDEV6("text", {
4228
4577
  content: " ",
4229
4578
  wrapMode: "none",
@@ -4309,21 +4658,6 @@ var Footer = ({
4309
4658
  }, undefined, true, undefined, this);
4310
4659
  };
4311
4660
 
4312
- // src/ui/middleTruncate.ts
4313
- var middleTruncate = (value, width, options = {}) => {
4314
- const ellipsis = options.ellipsis ?? "\u2026";
4315
- if (width <= 0)
4316
- return "";
4317
- if (value.length <= width)
4318
- return value;
4319
- if (width <= ellipsis.length)
4320
- return value.slice(0, width);
4321
- const available = width - ellipsis.length;
4322
- const left = Math.ceil(available / 2);
4323
- const right = Math.floor(available / 2);
4324
- return value.slice(0, left) + ellipsis + value.slice(value.length - right);
4325
- };
4326
-
4327
4661
  // src/Header.tsx
4328
4662
  import { jsxDEV as jsxDEV7 } from "@opentui/react/jsx-dev-runtime";
4329
4663
  var HEADER_HEIGHT = 1;
@@ -4578,12 +4912,95 @@ var readFileText = (path) => Effect.tryPromise({
4578
4912
  catch: (cause) => new FileReadError({ path, cause })
4579
4913
  });
4580
4914
 
4915
+ // src/io/renameFile.ts
4916
+ import { rename, readdir, stat } from "fs/promises";
4917
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
4918
+ var isPathInsideRoot = (root, absolutePath) => {
4919
+ const resolvedRoot = resolve(root);
4920
+ const resolvedPath = resolve(absolutePath);
4921
+ const rel = relative(resolvedRoot, resolvedPath);
4922
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
4923
+ };
4924
+ var uniqueTempBasename = async (entries, finalBasename) => {
4925
+ const stem = finalBasename.endsWith(".md") ? finalBasename.slice(0, -3) : finalBasename;
4926
+ const lower = new Set(entries.map((e) => e.toLowerCase()));
4927
+ for (let i = 0;i < 1000; i++) {
4928
+ const candidate = `${stem}.house-rename-${process.pid}-${Date.now()}-${i}.tmp`;
4929
+ if (!lower.has(candidate.toLowerCase()))
4930
+ return candidate;
4931
+ }
4932
+ throw new Error("could not allocate a temporary rename name");
4933
+ };
4934
+ var renameMarkdownFile = async (request) => {
4935
+ const discoveryRoot = resolve(request.discoveryRoot);
4936
+ const sourcePath = resolve(request.sourcePath);
4937
+ const parentDir = resolve(request.parentDir);
4938
+ const newBasename = request.newBasename;
4939
+ const destPath = join(parentDir, newBasename);
4940
+ const sourceBasename = basename(sourcePath);
4941
+ if (sourceBasename === newBasename && dirname(sourcePath) === parentDir) {
4942
+ return { ok: true, from: sourcePath, to: destPath, noop: true };
4943
+ }
4944
+ let sourceStat;
4945
+ try {
4946
+ sourceStat = await stat(sourcePath);
4947
+ } catch {
4948
+ return { ok: false, reason: "target-missing", message: "target missing" };
4949
+ }
4950
+ if (!sourceStat.isFile()) {
4951
+ return { ok: false, reason: "target-changed", message: "target changed" };
4952
+ }
4953
+ if (!isPathInsideRoot(discoveryRoot, sourcePath)) {
4954
+ return { ok: false, reason: "target-changed", message: "target changed" };
4955
+ }
4956
+ if (dirname(sourcePath) !== parentDir) {
4957
+ return { ok: false, reason: "target-changed", message: "target changed" };
4958
+ }
4959
+ let entries;
4960
+ try {
4961
+ entries = await readdir(parentDir);
4962
+ } catch (err) {
4963
+ const message = err instanceof Error ? err.message : String(err);
4964
+ return { ok: false, reason: "io", message: `rename failed: ${message}` };
4965
+ }
4966
+ if (!entries.includes(sourceBasename)) {
4967
+ return { ok: false, reason: "target-changed", message: "target changed" };
4968
+ }
4969
+ const exactDest = entries.includes(newBasename);
4970
+ const caseFoldDest = entries.find((e) => e.toLowerCase() === newBasename.toLowerCase());
4971
+ const caseOnly = sourceBasename.toLowerCase() === newBasename.toLowerCase() && sourceBasename !== newBasename;
4972
+ if (exactDest || caseFoldDest !== undefined && caseFoldDest !== sourceBasename) {
4973
+ return { ok: false, reason: "already-exists", message: "already exists" };
4974
+ }
4975
+ try {
4976
+ if (caseOnly) {
4977
+ const tempBasename = await uniqueTempBasename(entries, newBasename);
4978
+ const tempPath = join(parentDir, tempBasename);
4979
+ await rename(sourcePath, tempPath);
4980
+ try {
4981
+ await rename(tempPath, destPath);
4982
+ } catch (err) {
4983
+ try {
4984
+ await rename(tempPath, sourcePath);
4985
+ } catch {}
4986
+ throw err;
4987
+ }
4988
+ } else {
4989
+ await rename(sourcePath, destPath);
4990
+ }
4991
+ return { ok: true, from: sourcePath, to: destPath };
4992
+ } catch (err) {
4993
+ const message = err instanceof Error ? err.message : String(err);
4994
+ return { ok: false, reason: "io", message: `rename failed: ${message}` };
4995
+ }
4996
+ };
4997
+
4581
4998
  // ../../packages/ui/src/file-navigator/FilesystemFileNavigator.tsx
4582
4999
  import {
4583
5000
  forwardRef,
4584
5001
  useEffect as useEffect3,
4585
5002
  useImperativeHandle,
4586
- useLayoutEffect,
5003
+ useLayoutEffect as useLayoutEffect2,
4587
5004
  useReducer,
4588
5005
  useRef as useRef2
4589
5006
  } from "react";
@@ -4688,23 +5105,23 @@ var Sidebar = ({
4688
5105
  };
4689
5106
  // ../../packages/ui/src/file-navigator/core/engine.ts
4690
5107
  import { platform } from "os";
4691
- import { relative as relative2, resolve as resolve2 } from "path";
5108
+ import { relative as relative3, resolve as resolve3 } from "path";
4692
5109
 
4693
5110
  // ../../packages/ui/src/file-navigator/core/scanner.ts
4694
- import { basename, extname, join, relative, resolve, sep } from "path";
4695
- import { readFile as readFile2, readdir, realpath, stat } from "fs/promises";
5111
+ import { basename as basename2, extname, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
5112
+ import { readFile as readFile2, readdir as readdir2, realpath, stat as stat2 } from "fs/promises";
4696
5113
  import ignore from "ignore";
4697
5114
  var DEFAULT_BATCH_SIZE = 64;
4698
- var slash = (value) => value.split(sep).join("/");
5115
+ var slash = (value) => value.split(sep2).join("/");
4699
5116
  var compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
4700
5117
  var missing = (error) => typeof error === "object" && error !== null && ("code" in error) && error.code === "ENOENT";
4701
5118
  var inside = (root, path) => {
4702
- const candidate = relative(root, path);
4703
- return candidate === "" || !candidate.startsWith(`..${sep}`) && candidate !== "..";
5119
+ const candidate = relative2(root, path);
5120
+ return candidate === "" || !candidate.startsWith(`..${sep2}`) && candidate !== "..";
4704
5121
  };
4705
5122
  var thenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
4706
5123
  var readStat = async (path) => {
4707
- const value = await stat(path);
5124
+ const value = await stat2(path);
4708
5125
  return { size: value.size, mtimeMs: value.mtimeMs };
4709
5126
  };
4710
5127
  var filterPresentIgnoreFiles = (ignoreFiles, present) => {
@@ -4713,8 +5130,8 @@ var filterPresentIgnoreFiles = (ignoreFiles, present) => {
4713
5130
  };
4714
5131
  var frozenRecord = (path, root, metadata) => Object.freeze({
4715
5132
  absolutePath: path,
4716
- relativePath: slash(relative(root, path)),
4717
- basename: basename(path),
5133
+ relativePath: slash(relative2(root, path)),
5134
+ basename: basename2(path),
4718
5135
  extension: extname(path).toLowerCase(),
4719
5136
  size: metadata.size,
4720
5137
  mtimeMs: metadata.mtimeMs
@@ -4722,7 +5139,7 @@ var frozenRecord = (path, root, metadata) => Object.freeze({
4722
5139
  var ignored = (path, directory, levels) => {
4723
5140
  let result = false;
4724
5141
  for (const level of levels) {
4725
- const candidate = slash(relative(level.directory, path));
5142
+ const candidate = slash(relative2(level.directory, path));
4726
5143
  if (!candidate || candidate === ".." || candidate.startsWith("../"))
4727
5144
  continue;
4728
5145
  const match = level.matcher.test(directory ? `${candidate}/` : candidate);
@@ -4741,7 +5158,7 @@ var loadLevels = async (physical, lexical, ignoreFiles, parent) => {
4741
5158
  ...levels,
4742
5159
  {
4743
5160
  directory: lexical,
4744
- matcher: ignore().add(await readFile2(join(physical, filename), "utf8"))
5161
+ matcher: ignore().add(await readFile2(join2(physical, filename), "utf8"))
4745
5162
  }
4746
5163
  ];
4747
5164
  } catch (error) {
@@ -4755,10 +5172,10 @@ var abort = (signal) => {
4755
5172
  if (signal?.aborted)
4756
5173
  throw new DOMException("scan aborted", "AbortError");
4757
5174
  };
4758
- var normalizeRoot = (input) => resolve(input);
5175
+ var normalizeRoot = (input) => resolve2(input);
4759
5176
  var physicalWatchRoot = async (input) => {
4760
5177
  const root = normalizeRoot(input);
4761
- const rootStat = await stat(root);
5178
+ const rootStat = await stat2(root);
4762
5179
  if (!rootStat.isDirectory())
4763
5180
  throw new Error(`root is not a directory: ${root}`);
4764
5181
  return realpath(root);
@@ -4852,7 +5269,7 @@ var scanFiles = async (input, policyInput, options = {}) => {
4852
5269
  const beforeRead = options.barrier?.("before-read", lexical);
4853
5270
  if (thenable(beforeRead))
4854
5271
  await beforeRead;
4855
- const entries = (await readdir(physical, { withFileTypes: true })).sort((a, b) => compare(a.name, b.name));
5272
+ const entries = (await readdir2(physical, { withFileTypes: true })).sort((a, b) => compare(a.name, b.name));
4856
5273
  const afterRead = options.barrier?.("after-read", lexical);
4857
5274
  if (thenable(afterRead))
4858
5275
  await afterRead;
@@ -4867,8 +5284,8 @@ var scanFiles = async (input, policyInput, options = {}) => {
4867
5284
  for (const entry of files) {
4868
5285
  if (options.topologyOnly)
4869
5286
  continue;
4870
- const path = join(physical, entry.name);
4871
- const lexicalPath = join(lexical, entry.name);
5287
+ const path = join2(physical, entry.name);
5288
+ const lexicalPath = join2(lexical, entry.name);
4872
5289
  try {
4873
5290
  if (controlFiles.has(entry.name) || ignored(lexicalPath, false, levels))
4874
5291
  continue;
@@ -4889,18 +5306,18 @@ var scanFiles = async (input, policyInput, options = {}) => {
4889
5306
  abort(options.signal);
4890
5307
  if (controlFiles.has(entry.name))
4891
5308
  continue;
4892
- const physicalPath = join(physical, entry.name);
4893
- const lexicalPath = join(lexical, entry.name);
5309
+ const physicalPath = join2(physical, entry.name);
5310
+ const lexicalPath = join2(lexical, entry.name);
4894
5311
  try {
4895
5312
  if (entry.isSymbolicLink() && !policy.followSymlinks)
4896
5313
  continue;
4897
5314
  const target = entry.isSymbolicLink() ? await realpath(physicalPath) : physicalPath;
4898
- const targetStat = await stat(target);
5315
+ const targetStat = await stat2(target);
4899
5316
  if (targetStat.isDirectory()) {
4900
5317
  if (ignored(lexicalPath, true, levels) || ancestry.has(target))
4901
5318
  continue;
4902
5319
  if ((targetStat.mode & 511) === 0) {
4903
- options.onDiagnostic?.(new Error(`skipped directory: ${basename(lexicalPath)}`));
5320
+ options.onDiagnostic?.(new Error(`skipped directory: ${basename2(lexicalPath)}`));
4904
5321
  continue;
4905
5322
  }
4906
5323
  try {
@@ -4915,7 +5332,7 @@ var scanFiles = async (input, policyInput, options = {}) => {
4915
5332
  } catch (error) {
4916
5333
  if (missing(error))
4917
5334
  continue;
4918
- options.onDiagnostic?.(new Error(`skipped directory: ${basename(lexicalPath)}`));
5335
+ options.onDiagnostic?.(new Error(`skipped directory: ${basename2(lexicalPath)}`));
4919
5336
  }
4920
5337
  } else if (targetStat.isFile() && !entry.isDirectory()) {
4921
5338
  if (!options.topologyOnly) {
@@ -4927,7 +5344,7 @@ var scanFiles = async (input, policyInput, options = {}) => {
4927
5344
  } catch (error) {
4928
5345
  if (missing(error))
4929
5346
  continue;
4930
- options.onDiagnostic?.(new Error(`skipped directory: ${basename(lexicalPath)}`));
5347
+ options.onDiagnostic?.(new Error(`skipped directory: ${basename2(lexicalPath)}`));
4931
5348
  }
4932
5349
  }
4933
5350
  };
@@ -5022,7 +5439,7 @@ var projectFiles = (files, query, order = "tree", search = fuzzySearch) => {
5022
5439
  // ../../packages/ui/src/file-navigator/core/engine.ts
5023
5440
  var STREAM_SNAPSHOT = 256;
5024
5441
  var errorValue = (value) => value instanceof Error ? value : new Error(String(value));
5025
- var afterPaint = (value) => Promise.resolve(value).then(() => new Promise((resolve3) => setImmediate(resolve3)));
5442
+ var afterPaint = (value) => Promise.resolve(value).then(() => new Promise((resolve4) => setImmediate(resolve4)));
5026
5443
  var byPath = (files) => new Map(files.map((file) => [file.relativePath, file]));
5027
5444
  var publication = (previous, next) => {
5028
5445
  const added = [], changed = [], removed = [];
@@ -5491,13 +5908,13 @@ class FileNavigatorCore {
5491
5908
  if (path === this.#root)
5492
5909
  return "";
5493
5910
  if (path.startsWith(this.#root + "/"))
5494
- return relative2(this.#root, resolve2(path));
5911
+ return relative3(this.#root, resolve3(path));
5495
5912
  if (generation)
5496
5913
  for (const directory of generation.watchDirectories)
5497
5914
  for (const lexical of directory.lexicalPaths) {
5498
- const suffix = relative2(directory.physicalPath, path);
5915
+ const suffix = relative3(directory.physicalPath, path);
5499
5916
  if (suffix === "" || !suffix.startsWith("..") && !suffix.startsWith("/"))
5500
- return relative2(this.#root, resolve2(lexical, suffix));
5917
+ return relative3(this.#root, resolve3(lexical, suffix));
5501
5918
  }
5502
5919
  return path;
5503
5920
  }
@@ -5592,17 +6009,17 @@ var formatFileRow = (path, width) => {
5592
6009
  const slash2 = path.lastIndexOf("/");
5593
6010
  if (slash2 < 0)
5594
6011
  return { basename: fitTail(path, width), parent: "", separator: "" };
5595
- const basename2 = path.slice(slash2 + 1);
6012
+ const basename3 = path.slice(slash2 + 1);
5596
6013
  const parent = path.slice(0, slash2);
5597
- if (basename2.length >= width) {
5598
- return { basename: fitTail(basename2, width), parent: "", separator: "" };
6014
+ if (basename3.length >= width) {
6015
+ return { basename: fitTail(basename3, width), parent: "", separator: "" };
5599
6016
  }
5600
- const remaining = width - basename2.length - SEPARATOR.length;
6017
+ const remaining = width - basename3.length - SEPARATOR.length;
5601
6018
  if (remaining < MIN_PARENT_BUDGET || parent.length === 0) {
5602
- return { basename: basename2, parent: "", separator: "" };
6019
+ return { basename: basename3, parent: "", separator: "" };
5603
6020
  }
5604
6021
  return {
5605
- basename: basename2,
6022
+ basename: basename3,
5606
6023
  parent: middleTruncate2(parent, remaining),
5607
6024
  separator: SEPARATOR
5608
6025
  };
@@ -5613,13 +6030,13 @@ var DefaultFileRow = ({
5613
6030
  width,
5614
6031
  theme
5615
6032
  }) => {
5616
- const { basename: basename2, parent, separator } = formatFileRow(path, width);
6033
+ const { basename: basename3, parent, separator } = formatFileRow(path, width);
5617
6034
  return /* @__PURE__ */ jsxDEV9("text", {
5618
6035
  wrapMode: "none",
5619
6036
  children: [
5620
6037
  /* @__PURE__ */ jsxDEV9("span", {
5621
6038
  style: { fg: selected ? theme.selectedListItemText : theme.text },
5622
- children: basename2
6039
+ children: basename3
5623
6040
  }, undefined, false, undefined, this),
5624
6041
  parent !== "" && /* @__PURE__ */ jsxDEV9("span", {
5625
6042
  style: { fg: theme.textMuted },
@@ -5698,7 +6115,7 @@ var Component = (props, ref) => {
5698
6115
  const timerRef = useRef2(null);
5699
6116
  const redrawRef = useRef2(() => {});
5700
6117
  const flushQueryRef = useRef2(() => snapshotOf(null, normalizedRoot, true, null));
5701
- useLayoutEffect(() => {
6118
+ useLayoutEffect2(() => {
5702
6119
  activeRootRef.current = normalizedRoot;
5703
6120
  callbacks.current = { onSelectionChange, onSelectedFileInvalidated, onDiagnostic, onSnapshot };
5704
6121
  }, [normalizedRoot, onSelectionChange, onSelectedFileInvalidated, onDiagnostic, onSnapshot]);
@@ -5964,6 +6381,7 @@ var queryWouldShowRelativePath = (query, relativePath) => {
5964
6381
 
5965
6382
  // src/new-file/resolveName.ts
5966
6383
  var MD_SUFFIX = /\.md$/i;
6384
+ var pathShapedError = (mode) => mode === "rename" ? "name must be a single file name" : "name must be a single file in the discovery root";
5967
6385
  var hasControlChar = (raw) => {
5968
6386
  for (let i = 0;i < raw.length; i++) {
5969
6387
  const code = raw.charCodeAt(i);
@@ -5972,14 +6390,14 @@ var hasControlChar = (raw) => {
5972
6390
  }
5973
6391
  return false;
5974
6392
  };
5975
- var resolveNewFileName = (raw) => {
6393
+ var resolveMarkdownBasename = (raw, mode = "new-file") => {
5976
6394
  if (hasControlChar(raw))
5977
6395
  return { ok: false, error: "name contains invalid characters" };
5978
6396
  const trimmed = raw.trim();
5979
6397
  if (trimmed.length === 0)
5980
6398
  return { ok: false, error: "name required" };
5981
6399
  if (trimmed.includes("/") || trimmed.includes("\\") || trimmed === "." || trimmed === "..") {
5982
- return { ok: false, error: "name must be a single file in the discovery root" };
6400
+ return { ok: false, error: pathShapedError(mode) };
5983
6401
  }
5984
6402
  if (trimmed.startsWith("."))
5985
6403
  return { ok: false, error: "hidden names aren't supported yet" };
@@ -5992,14 +6410,59 @@ var resolveNewFileName = (raw) => {
5992
6410
  warnings: ["extension will be saved as .md"]
5993
6411
  };
5994
6412
  }
5995
- const basename2 = `${trimmed}.md`;
6413
+ const basename3 = `${trimmed}.md`;
5996
6414
  return {
5997
6415
  ok: true,
5998
- basename: basename2,
5999
- warnings: trimmed.includes(".") ? [`will be created as ${basename2}`] : []
6416
+ basename: basename3,
6417
+ warnings: trimmed.includes(".") ? [`will be created as ${basename3}`] : []
6000
6418
  };
6001
6419
  };
6002
6420
 
6421
+ // src/prompts/helpers.ts
6422
+ import { readdirSync } from "fs";
6423
+ import { dirname as dirname2 } from "path";
6424
+ var parentRelativeOf = (relativePath) => {
6425
+ const idx = relativePath.lastIndexOf("/");
6426
+ return idx === -1 ? "" : relativePath.slice(0, idx);
6427
+ };
6428
+ var destinationRelativePath = (parentRelative, newBasename) => parentRelative.length === 0 ? newBasename : `${parentRelative}/${newBasename}`;
6429
+ var captureActionTarget = (file) => ({
6430
+ absolutePath: file.absolutePath,
6431
+ parentDir: dirname2(file.absolutePath),
6432
+ relativePath: file.relativePath,
6433
+ basename: file.basename,
6434
+ parentRelative: parentRelativeOf(file.relativePath)
6435
+ });
6436
+ var siblingExistsExact = (parentDir, name, exceptBasename) => {
6437
+ try {
6438
+ const entries = readdirSync(parentDir);
6439
+ return entries.includes(name) && name !== exceptBasename;
6440
+ } catch {
6441
+ return false;
6442
+ }
6443
+ };
6444
+ var promptLiveStatus = (raw, mode, query, matchRelativePath, destExists) => {
6445
+ const resolved = resolveMarkdownBasename(raw, mode);
6446
+ if (!resolved.ok) {
6447
+ if (resolved.error === "name required")
6448
+ return null;
6449
+ return { kind: "error", lines: [resolved.error] };
6450
+ }
6451
+ const lines = [...resolved.warnings];
6452
+ if (query.length > 0 && !queryWouldShowRelativePath(query, matchRelativePath)) {
6453
+ lines.push(`filter will change to ${resolved.basename}`);
6454
+ }
6455
+ if (destExists)
6456
+ lines.push("already exists");
6457
+ if (lines.length === 0)
6458
+ return null;
6459
+ return { kind: "warning", lines };
6460
+ };
6461
+ var retargetPreviewIfNeeded = (preview, sourcePath, destPath) => {
6462
+ if (preview?.currentTarget() === sourcePath)
6463
+ preview.setTarget(destPath);
6464
+ };
6465
+
6003
6466
  // src/keymap/keymap.ts
6004
6467
  var parseChord = (raw) => {
6005
6468
  const parts = raw.toLowerCase().split("+");
@@ -6075,7 +6538,9 @@ var Sidebar2 = ({
6075
6538
  onSnapshot,
6076
6539
  onSelectedFileInvalidated,
6077
6540
  onDiagnostic,
6078
- order = "recently-modified"
6541
+ order = "recently-modified",
6542
+ onFilterInput,
6543
+ onFilterEditingReady
6079
6544
  }) => {
6080
6545
  const headerVisible = snapshot.files.length > 0 || discoveryActive;
6081
6546
  const emptyState = snapshot.filteredFiles.length > 0 ? undefined : snapshot.files.length === 0 ? discoveryActive ? { label: "Scanning", value: "\u2026" } : { label: "No markdown files in", value: rootLabel } : { label: "No files match", value: snapshot.appliedQuery };
@@ -6101,7 +6566,9 @@ var Sidebar2 = ({
6101
6566
  query: filterInput,
6102
6567
  editing: filterOpen,
6103
6568
  placeholder: "type / to filter",
6104
- width: rowWidth
6569
+ width: rowWidth,
6570
+ ...onFilterInput === undefined ? {} : { onInput: onFilterInput },
6571
+ ...onFilterEditingReady === undefined ? {} : { onEditingReady: onFilterEditingReady }
6105
6572
  }, undefined, false, undefined, this)
6106
6573
  } : {},
6107
6574
  ...emptyState === undefined ? {} : {
@@ -6267,7 +6734,7 @@ var openInBrowser = (url) => {
6267
6734
  };
6268
6735
 
6269
6736
  // src/serve/server.ts
6270
- import { basename as basename2 } from "path";
6737
+ import { basename as basename3 } from "path";
6271
6738
  import { watch } from "fs";
6272
6739
  import { readFile as readFile3 } from "fs/promises";
6273
6740
 
@@ -6503,7 +6970,7 @@ var startServer = ({ path, port = 0 }) => {
6503
6970
  }
6504
6971
  try {
6505
6972
  const md = await readFile3(target, "utf8");
6506
- const html = renderHtml(md, basename2(target));
6973
+ const html = renderHtml(md, basename3(target));
6507
6974
  return new Response(html, {
6508
6975
  headers: {
6509
6976
  "content-type": "text/html; charset=utf-8",
@@ -12542,218 +13009,98 @@ var bundled = [
12542
13009
  { id: "rosepine", json: rosepine_default },
12543
13010
  { id: "solarized", json: solarized_default },
12544
13011
  { id: "synthwave84", json: synthwave84_default },
12545
- { id: "tokyonight", json: tokyonight_default },
12546
- { id: "vercel", json: vercel_default },
12547
- { id: "vesper", json: vesper_default },
12548
- { id: "zenburn", json: zenburn_default }
12549
- ];
12550
- var toDefinition = (id, raw) => {
12551
- if (!isThemeJson(raw))
12552
- return null;
12553
- const json = raw;
12554
- return { id, name: json.name ?? id, source: json };
12555
- };
12556
- var loadBundledThemes = () => {
12557
- const map = new Map;
12558
- for (const { id, json } of bundled) {
12559
- const def = toDefinition(id, json);
12560
- if (def)
12561
- map.set(id, def);
12562
- }
12563
- return map;
12564
- };
12565
-
12566
- // src/theme/registry.ts
12567
- var map = loadBundledThemes();
12568
- var themeDefinitions = [...map.values()];
12569
- var getThemeDefinition = (id) => map.get(id);
12570
-
12571
- // ../../packages/options/src/decode.ts
12572
- var isPresent = (value) => value !== undefined && value !== null;
12573
- var stringifyReceived = (value) => {
12574
- if (typeof value === "string")
12575
- return JSON.stringify(value);
12576
- if (typeof value === "number" || typeof value === "boolean")
12577
- return JSON.stringify(value);
12578
- if (value === null)
12579
- return "null";
12580
- return typeof value;
12581
- };
12582
- var formatResolveError = (error, filePath) => {
12583
- if (error.layer === "file") {
12584
- const where = filePath === undefined ? "" : ` in ${filePath}`;
12585
- return `invalid value for ${error.key}${where}: ${error.message}`;
12586
- }
12587
- return `${error.key}: ${error.message}, got ${stringifyReceived(error.received)}`;
12588
- };
12589
- var fail = (key, layer, message, received) => ({
12590
- key,
12591
- layer,
12592
- message,
12593
- received
12594
- });
12595
- var booleanMessage = "expected true or false";
12596
- var numberMessage = (spec) => {
12597
- if (spec.integer === true && spec.min === 1 && spec.max === undefined) {
12598
- return "expected a positive integer";
12599
- }
12600
- if (spec.integer === true)
12601
- return "expected an integer";
12602
- return "expected a number";
12603
- };
12604
- var inRange = (value, spec) => {
12605
- if (spec.min !== undefined && value < spec.min)
12606
- return false;
12607
- if (spec.max !== undefined && value > spec.max)
12608
- return false;
12609
- if (spec.integer === true && !Number.isSafeInteger(value))
12610
- return false;
12611
- if (!Number.isFinite(value))
12612
- return false;
12613
- return true;
12614
- };
12615
- var decodeBoolean = (key, layer, value) => {
12616
- if (layer === "env") {
12617
- if (value === "true")
12618
- return { ok: true, value: true };
12619
- if (value === "false")
12620
- return { ok: true, value: false };
12621
- return { ok: false, error: fail(key, layer, booleanMessage, value) };
12622
- }
12623
- if (typeof value === "boolean")
12624
- return { ok: true, value };
12625
- return { ok: false, error: fail(key, layer, booleanMessage, value) };
12626
- };
12627
- var decodeNumber = (key, layer, value, spec) => {
12628
- const message = numberMessage(spec);
12629
- if (layer === "env") {
12630
- if (typeof value !== "string" || !/^-?\d+(\.\d+)?$/.test(value)) {
12631
- return { ok: false, error: fail(key, layer, message, value) };
12632
- }
12633
- if (spec.integer === true && !/^-?\d+$/.test(value)) {
12634
- return { ok: false, error: fail(key, layer, message, value) };
12635
- }
12636
- const parsed = spec.integer === true ? Number.parseInt(value, 10) : Number.parseFloat(value);
12637
- if (!inRange(parsed, spec))
12638
- return { ok: false, error: fail(key, layer, message, value) };
12639
- return { ok: true, value: parsed };
12640
- }
12641
- if (typeof value !== "number" || !inRange(value, spec)) {
12642
- return { ok: false, error: fail(key, layer, message, value) };
12643
- }
12644
- return { ok: true, value };
13012
+ { id: "tokyonight", json: tokyonight_default },
13013
+ { id: "vercel", json: vercel_default },
13014
+ { id: "vesper", json: vesper_default },
13015
+ { id: "zenburn", json: zenburn_default }
13016
+ ];
13017
+ var toDefinition = (id, raw) => {
13018
+ if (!isThemeJson(raw))
13019
+ return null;
13020
+ const json = raw;
13021
+ return { id, name: json.name ?? id, source: json };
12645
13022
  };
12646
- var decodeString = (key, layer, value, spec) => {
12647
- if (typeof value !== "string") {
12648
- const message = spec.choices === undefined ? "expected a string" : `expected one of ${spec.choices.join(", ")}`;
12649
- return { ok: false, error: fail(key, layer, message, value) };
12650
- }
12651
- if (spec.choices !== undefined && !spec.choices.includes(value)) {
12652
- return {
12653
- ok: false,
12654
- error: fail(key, layer, `expected one of ${spec.choices.join(", ")}`, value)
12655
- };
13023
+ var loadBundledThemes = () => {
13024
+ const map = new Map;
13025
+ for (const { id, json } of bundled) {
13026
+ const def = toDefinition(id, json);
13027
+ if (def)
13028
+ map.set(id, def);
12656
13029
  }
12657
- return { ok: true, value };
12658
- };
12659
- var decodeValue = (key, spec, layer, value) => {
12660
- if (spec.type === "boolean")
12661
- return decodeBoolean(key, layer, value);
12662
- if (spec.type === "number")
12663
- return decodeNumber(key, layer, value, spec);
12664
- return decodeString(key, layer, value, spec);
13030
+ return map;
12665
13031
  };
12666
13032
 
12667
- // ../../packages/options/src/resolve.ts
12668
- var LAYERS = ["cli", "env", "file"];
12669
- var resolveOptions = (specs, layers = {}) => {
12670
- const values = {};
12671
- for (const key of Object.keys(specs)) {
12672
- const spec = specs[key];
12673
- let found = false;
12674
- for (const layer of LAYERS) {
12675
- const source = layers[layer];
12676
- if (source === undefined)
12677
- continue;
12678
- const raw = source[key];
12679
- if (!isPresent(raw))
12680
- continue;
12681
- const decoded = decodeValue(key, spec, layer, raw);
12682
- if (!decoded.ok)
12683
- return decoded;
12684
- values[key] = decoded.value;
12685
- found = true;
12686
- break;
12687
- }
12688
- if (!found)
12689
- values[key] = spec.default;
13033
+ // src/theme/registry.ts
13034
+ var map = loadBundledThemes();
13035
+ var themeDefinitions = [...map.values()];
13036
+ var getThemeDefinition = (id) => map.get(id);
13037
+
13038
+ // src/config/footerControls.ts
13039
+ var footerControlsFromSession = (options, session, handlers = {}) => {
13040
+ const controls = [];
13041
+ for (const key of footerKeys(options.specs)) {
13042
+ const spec = options.specs[key];
13043
+ const footer = spec.footer;
13044
+ if (footer === undefined)
13045
+ continue;
13046
+ const value = session.get(key);
13047
+ const handler = handlers[key];
13048
+ controls.push({
13049
+ id: key,
13050
+ icon: footerControlGlyph(spec, value),
13051
+ variant: "info",
13052
+ active: handler?.active ?? footerControlActive(spec, value),
13053
+ onMouseUp: () => {
13054
+ if (handler?.onActivate !== undefined) {
13055
+ handler.onActivate();
13056
+ return;
13057
+ }
13058
+ const current = session.get(key);
13059
+ session.set(key, nextFooterValue(spec, current));
13060
+ }
13061
+ });
12690
13062
  }
12691
- return { ok: true, value: values };
12692
- };
12693
- var catalogDefaults = (specs) => {
12694
- const values = {};
12695
- for (const key of Object.keys(specs))
12696
- values[key] = specs[key].default;
12697
- return values;
13063
+ return controls;
12698
13064
  };
12699
13065
 
12700
- // ../../packages/options/src/session.ts
12701
- var persistPolicy = (persist) => persist ?? "session";
12702
- var createSession = (specs, values, options = {}) => {
12703
- let snapshot = values;
12704
- const listeners = new Set;
12705
- const get = (key) => snapshot[key];
12706
- const set = async (key, value) => {
12707
- const spec = specs[key];
12708
- if (spec === undefined) {
12709
- throw new Error(`unknown option ${JSON.stringify(key)}`);
12710
- }
12711
- const decoded = decodeValue(key, spec, "cli", value);
12712
- if (!decoded.ok) {
12713
- throw new Error(`${decoded.error.key}: ${decoded.error.message}`);
12714
- }
12715
- if (Object.is(snapshot[key], decoded.value))
12716
- return;
12717
- snapshot = { ...snapshot, [key]: decoded.value };
12718
- for (const listener of listeners)
12719
- listener(snapshot);
12720
- if (persistPolicy(spec.persist) === "file" && options.persist !== undefined) {
12721
- await options.persist({
12722
- key,
12723
- value: decoded.value,
12724
- values: snapshot
12725
- });
12726
- }
12727
- };
12728
- return {
12729
- get,
12730
- set,
12731
- subscribe: (listener) => {
12732
- listeners.add(listener);
12733
- return () => {
12734
- listeners.delete(listener);
12735
- };
12736
- },
12737
- get values() {
12738
- return snapshot;
13066
+ // src/config/footerLabels.ts
13067
+ var uniqueFooterLabels = (ids) => {
13068
+ const used = new Set;
13069
+ const out = {};
13070
+ for (const id of ids) {
13071
+ const compact = id.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase();
13072
+ const source = compact.length > 0 ? compact : "x";
13073
+ let label = source.slice(0, 2).padEnd(2, "x");
13074
+ let offset = 2;
13075
+ while (used.has(label)) {
13076
+ const next = source[offset];
13077
+ if (next !== undefined) {
13078
+ label = `${source[0]}${next}`;
13079
+ offset += 1;
13080
+ } else {
13081
+ label = `${source[0]}${used.size % 10}`;
13082
+ offset += 1;
13083
+ if (offset > source.length + 20) {
13084
+ throw new Error(`unable to allocate unique footer label for ${JSON.stringify(id)}`);
13085
+ }
13086
+ }
12739
13087
  }
12740
- };
13088
+ used.add(label);
13089
+ out[id] = label;
13090
+ }
13091
+ return out;
12741
13092
  };
12742
13093
 
12743
- // ../../packages/options/src/define.ts
12744
- var defineOptions = (specs) => ({
12745
- specs,
12746
- defaults: catalogDefaults(specs),
12747
- resolve: (layers) => resolveOptions(specs, layers),
12748
- createSession: (values, options) => createSession(specs, values, options)
12749
- });
12750
13094
  // src/config/options.ts
12751
13095
  var FILE_NAVIGATOR_ORDERS = ["tree", "recently-modified"];
13096
+ var themeIds = themeDefinitions.map((theme) => theme.id);
13097
+ var themeFooterLabels = uniqueFooterLabels(themeIds);
12752
13098
  var houseOptions = defineOptions({
12753
13099
  wrap: {
12754
13100
  type: "boolean",
12755
13101
  default: false,
12756
- persist: "session"
13102
+ persist: "session",
13103
+ footer: { icon: "W" }
12757
13104
  },
12758
13105
  width: {
12759
13106
  type: "number",
@@ -12765,7 +13112,8 @@ var houseOptions = defineOptions({
12765
13112
  type: "string",
12766
13113
  default: "opencode",
12767
13114
  persist: "file",
12768
- choices: themeDefinitions.map((theme) => theme.id)
13115
+ choices: themeIds,
13116
+ footer: { icon: "T", labels: themeFooterLabels }
12769
13117
  },
12770
13118
  tone: {
12771
13119
  type: "string",
@@ -12786,17 +13134,25 @@ var houseOptions = defineOptions({
12786
13134
  order: {
12787
13135
  type: "string",
12788
13136
  default: "recently-modified",
12789
- choices: FILE_NAVIGATOR_ORDERS
13137
+ choices: FILE_NAVIGATOR_ORDERS,
13138
+ persist: "session",
13139
+ footer: {
13140
+ icon: "O",
13141
+ labels: {
13142
+ tree: "tr",
13143
+ "recently-modified": "re"
13144
+ }
13145
+ }
12790
13146
  }
12791
13147
  });
12792
13148
 
12793
13149
  // src/config/save.ts
12794
- import { dirname } from "path";
12795
- import { lstat, mkdir, readFile as readFile4, realpath as realpath2, rename, unlink, writeFile } from "fs/promises";
13150
+ import { dirname as dirname3 } from "path";
13151
+ import { lstat, mkdir, readFile as readFile4, realpath as realpath2, rename as rename2, unlink, writeFile } from "fs/promises";
12796
13152
 
12797
13153
  // src/config/load.ts
12798
13154
  import { homedir } from "os";
12799
- import { join as join2 } from "path";
13155
+ import { join as join3 } from "path";
12800
13156
  import { Config, ConfigProvider, Effect as Effect2, Schema } from "effect";
12801
13157
 
12802
13158
  // src/discovery/show.ts
@@ -12933,7 +13289,7 @@ var cliProvider = (overrides) => {
12933
13289
  entries.push(["show", overrides.show.join(",")]);
12934
13290
  return ConfigProvider.fromUnknown(Object.fromEntries(entries));
12935
13291
  };
12936
- var defaultConfigPath = () => join2(process.env["XDG_CONFIG_HOME"] ?? join2(homedir(), ".config"), "house", "config.toml");
13292
+ var defaultConfigPath = () => join3(process.env["XDG_CONFIG_HOME"] ?? join3(homedir(), ".config"), "house", "config.toml");
12937
13293
  var formatConfigError = (err) => {
12938
13294
  if (err instanceof Config.ConfigError) {
12939
13295
  const cause = err.cause;
@@ -13071,8 +13427,8 @@ var updateThemePreferenceToml = (raw, record) => {
13071
13427
  };
13072
13428
  var resolveWritableConfigPath = async (path) => {
13073
13429
  try {
13074
- const stat2 = await lstat(path);
13075
- if (stat2.isSymbolicLink())
13430
+ const stat3 = await lstat(path);
13431
+ if (stat3.isSymbolicLink())
13076
13432
  return await realpath2(path);
13077
13433
  } catch (err) {
13078
13434
  if (err.code !== "ENOENT")
@@ -13085,7 +13441,7 @@ var writeThemePreference = async (record, path) => {
13085
13441
  path = targetPath;
13086
13442
  const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
13087
13443
  try {
13088
- await mkdir(dirname(path), { recursive: true });
13444
+ await mkdir(dirname3(path), { recursive: true });
13089
13445
  let next = `theme = ${encodeTomlString(record.theme)}
13090
13446
  tone = ${encodeTomlString(record.tone)}
13091
13447
  `;
@@ -13097,7 +13453,7 @@ tone = ${encodeTomlString(record.tone)}
13097
13453
  throw err;
13098
13454
  }
13099
13455
  await writeFile(tmp, next, "utf8");
13100
- await rename(tmp, path);
13456
+ await rename2(tmp, path);
13101
13457
  } catch (err) {
13102
13458
  try {
13103
13459
  await unlink(tmp);
@@ -13123,6 +13479,7 @@ var persistHouseOption = async (event, path) => {
13123
13479
 
13124
13480
  // src/Browser.tsx
13125
13481
  import { jsxDEV as jsxDEV13 } from "@opentui/react/jsx-dev-runtime";
13482
+ var asFileNavigatorOrder = (value) => typeof value === "string" && FILE_NAVIGATOR_ORDERS.includes(value) ? value : "recently-modified";
13126
13483
  var defaultReadFile = (path) => Effect3.runPromise(readFileText(path));
13127
13484
  var defaultDiscoveryPolicy = {
13128
13485
  revision: "house-markdown",
@@ -13134,25 +13491,16 @@ var FILTER_DEBOUNCE_MS = 50;
13134
13491
  var RENDERED_PATH_DEBOUNCE_MS = 80;
13135
13492
  var NEW_FILE_MEMBERSHIP_TIMEOUT_MS = 2000;
13136
13493
  var NEW_FILE_MEMBERSHIP_POLL_MS = 32;
13137
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
13494
+ var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
13138
13495
  var isSingleCodePoint = (value) => Array.from(value).length === 1;
13139
13496
  var dropLastCodePoint = (value) => Array.from(value).slice(0, -1).join("");
13140
- var promptLiveStatus = (raw, query, destExists) => {
13141
- const resolved = resolveNewFileName(raw);
13142
- if (!resolved.ok) {
13143
- if (resolved.error === "name required")
13144
- return null;
13145
- return { kind: "error", lines: [resolved.error] };
13146
- }
13147
- const lines = [...resolved.warnings];
13148
- if (query.length > 0 && !queryWouldShowRelativePath(query, resolved.basename)) {
13149
- lines.push(`filter will change to ${resolved.basename}`);
13497
+ var printableFromKey = (key) => {
13498
+ if (key.name === "space")
13499
+ return " ";
13500
+ if (typeof key.name === "string" && isSingleCodePoint(key.name)) {
13501
+ return key.shift ? key.name.toUpperCase() : key.name;
13150
13502
  }
13151
- if (destExists)
13152
- lines.push("already exists");
13153
- if (lines.length === 0)
13154
- return null;
13155
- return { kind: "warning", lines };
13503
+ return null;
13156
13504
  };
13157
13505
  var isPartialDiscoveryWarning2 = (status) => status?.trimStart().startsWith("scan incomplete:") ?? false;
13158
13506
  var noFloatingOverlay = { kind: "none" };
@@ -13197,7 +13545,9 @@ var Browser = ({
13197
13545
  order = "recently-modified",
13198
13546
  launchEditor = openInEditor,
13199
13547
  createEmptyFile = createEmptyFileExclusive,
13200
- newFileMembershipTimeoutMs = NEW_FILE_MEMBERSHIP_TIMEOUT_MS
13548
+ renameFile = renameMarkdownFile,
13549
+ newFileMembershipTimeoutMs = NEW_FILE_MEMBERSHIP_TIMEOUT_MS,
13550
+ initialPreviewServer = null
13201
13551
  }) => {
13202
13552
  const renderer = useRenderer();
13203
13553
  const { width, height } = useTerminalDimensions2();
@@ -13211,15 +13561,17 @@ var Browser = ({
13211
13561
  wrap: initialWrap,
13212
13562
  width: wrapWidth,
13213
13563
  theme: theme.id,
13214
- tone: theme.tone
13564
+ tone: theme.tone,
13565
+ order: asFileNavigatorOrder(order)
13215
13566
  }, { persist: persistHouseOption });
13216
13567
  }
13217
13568
  const wrapEnabled = useSyncExternalStore(optionsSession.current.subscribe, () => optionsSession.current.get("wrap"));
13569
+ const browseOrder = useSyncExternalStore(optionsSession.current.subscribe, () => asFileNavigatorOrder(optionsSession.current.get("order")));
13218
13570
  const toggleWrap = () => {
13219
13571
  const session = optionsSession.current;
13220
13572
  if (session === null)
13221
13573
  return;
13222
- session.set("wrap", !session.get("wrap"));
13574
+ session.set("wrap", nextFooterValue(houseOptions.specs.wrap, session.get("wrap")));
13223
13575
  };
13224
13576
  const [loaded, setLoaded] = useState4(null);
13225
13577
  const [error, setError] = useState4(null);
@@ -13335,22 +13687,30 @@ var Browser = ({
13335
13687
  const paletteIndexRef = useRef3(0);
13336
13688
  const [promptInput, setPromptInput] = useState4("");
13337
13689
  const [promptStatus, setPromptStatus] = useState4(null);
13690
+ const [promptPurpose, setPromptPurpose] = useState4("new-file");
13691
+ const [promptContext, setPromptContext] = useState4(undefined);
13338
13692
  const promptInputRef = useRef3("");
13693
+ const promptPurposeRef = useRef3("new-file");
13339
13694
  const promptFocusBeforeRef = useRef3("sidebar");
13340
13695
  const promptSubmittingRef = useRef3(false);
13341
- const newFileTaskGenRef = useRef3(0);
13696
+ const [promptSubmitting, setPromptSubmitting] = useState4(false);
13697
+ const actionTargetRef = useRef3(null);
13698
+ const promptTaskGenRef = useRef3(0);
13342
13699
  const mountedRef = useRef3(true);
13343
13700
  const [readerEmptyStateTipRotation, setReaderEmptyStateTipRotation] = useState4(() => nextReaderEmptyStateTipRotation);
13344
13701
  const readerEmptyStateVisibleRef = useRef3(false);
13345
13702
  const filterOpenRef = useRef3(startInFilter);
13346
13703
  const filterInputRef = useRef3(initialQuery);
13704
+ const filterInputReadyRef = useRef3(false);
13705
+ const paletteInputReadyRef = useRef3(false);
13706
+ const promptInputReadyRef = useRef3(false);
13347
13707
  const focusRef = useRef3(focus);
13348
13708
  const restoreFilterOnSidebarFocusRef = useRef3(startInFilter);
13349
13709
  const [footerNotice, setFooterNoticeState] = useState4(null);
13350
13710
  const pushFooterNotice = (text, ttlMs = 2000) => setFooterNoticeState({ text, ttlMs });
13351
- const serverRef = useRef3(null);
13711
+ const serverRef = useRef3(initialPreviewServer);
13352
13712
  const pendingSelectionPathRef = useRef3(null);
13353
- useLayoutEffect2(() => {
13713
+ useLayoutEffect3(() => {
13354
13714
  if (readerRootRef.current === root)
13355
13715
  return;
13356
13716
  readerRootRef.current = root;
@@ -13363,14 +13723,14 @@ var Browser = ({
13363
13723
  setLoaded(null);
13364
13724
  setError(null);
13365
13725
  }, [root]);
13366
- useLayoutEffect2(() => () => {
13726
+ useLayoutEffect3(() => () => {
13367
13727
  advanceReaderEpoch();
13368
13728
  }, []);
13369
13729
  useEffect4(() => {
13370
13730
  mountedRef.current = true;
13371
13731
  return () => {
13372
13732
  mountedRef.current = false;
13373
- newFileTaskGenRef.current += 1;
13733
+ promptTaskGenRef.current += 1;
13374
13734
  newFileEditPathRef.current = null;
13375
13735
  serverRef.current?.stop();
13376
13736
  serverRef.current = null;
@@ -13431,6 +13791,14 @@ var Browser = ({
13431
13791
  rememberAppearance(session.set("theme", next.id));
13432
13792
  pushFooterNotice(`theme: ${next.name}`);
13433
13793
  };
13794
+ const cycleOrder = () => {
13795
+ const session = optionsSession.current;
13796
+ if (session === null)
13797
+ return;
13798
+ const next = nextFooterValue(houseOptions.specs.order, session.get("order"));
13799
+ session.set("order", next);
13800
+ pushFooterNotice(`order: ${next}`);
13801
+ };
13434
13802
  const toggleTone = () => {
13435
13803
  const session = optionsSession.current;
13436
13804
  if (session === null)
@@ -13503,18 +13871,32 @@ var Browser = ({
13503
13871
  const applyPromptInput = (next) => {
13504
13872
  promptInputRef.current = next;
13505
13873
  setPromptInput(next);
13506
- const resolved = resolveNewFileName(next);
13507
- const destExists = resolved.ok && existsSync(join3(root, resolved.basename));
13508
- setPromptStatus(promptLiveStatus(next, filterInputRef.current, destExists));
13874
+ const mode = promptPurposeRef.current;
13875
+ const resolved = resolveMarkdownBasename(next, mode);
13876
+ const target = actionTargetRef.current;
13877
+ let destExists = false;
13878
+ let matchRelative = resolved.ok ? resolved.basename : "";
13879
+ if (resolved.ok && mode === "new-file") {
13880
+ destExists = siblingExistsExact(root, resolved.basename);
13881
+ } else if (resolved.ok && mode === "rename" && target !== null) {
13882
+ matchRelative = destinationRelativePath(target.parentRelative, resolved.basename);
13883
+ destExists = siblingExistsExact(target.parentDir, resolved.basename, target.basename);
13884
+ }
13885
+ setPromptStatus(promptLiveStatus(next, mode, filterInputRef.current, matchRelative, destExists));
13509
13886
  };
13510
- const closeNewFilePrompt = (restoreFocus) => {
13887
+ const closePrompt = (restoreFocus) => {
13511
13888
  promptSubmittingRef.current = false;
13889
+ setPromptSubmitting(false);
13512
13890
  promptInputRef.current = "";
13513
13891
  setPromptInput("");
13514
13892
  setPromptStatus(null);
13893
+ setPromptContext(undefined);
13894
+ actionTargetRef.current = null;
13895
+ promptPurposeRef.current = "new-file";
13896
+ setPromptPurpose("new-file");
13515
13897
  closeFloatingOverlay();
13516
13898
  if (restoreFocus) {
13517
- newFileTaskGenRef.current += 1;
13899
+ promptTaskGenRef.current += 1;
13518
13900
  const prev = promptFocusBeforeRef.current;
13519
13901
  focusRef.current = prev;
13520
13902
  setFocus(prev);
@@ -13581,6 +13963,23 @@ var Browser = ({
13581
13963
  }
13582
13964
  })();
13583
13965
  };
13966
+ const beginPrompt = (purpose, initialValue, context) => {
13967
+ promptTaskGenRef.current += 1;
13968
+ promptSubmittingRef.current = false;
13969
+ setPromptSubmitting(false);
13970
+ promptPurposeRef.current = purpose;
13971
+ setPromptPurpose(purpose);
13972
+ setPromptContext(context);
13973
+ promptInputRef.current = initialValue;
13974
+ setPromptInput(initialValue);
13975
+ promptFocusBeforeRef.current = focusRef.current;
13976
+ if (filterOpenRef.current) {
13977
+ filterOpenRef.current = false;
13978
+ setFilterOpen(false);
13979
+ }
13980
+ dispatchFloatingOverlay({ type: "open-prompt" });
13981
+ applyPromptInput(initialValue);
13982
+ };
13584
13983
  const openNewFilePrompt = () => {
13585
13984
  const editor = resolveEditor(process.env);
13586
13985
  if (!editor) {
@@ -13591,41 +13990,68 @@ var Browser = ({
13591
13990
  pushFooterNotice("editor unavailable in this environment");
13592
13991
  return;
13593
13992
  }
13594
- newFileTaskGenRef.current += 1;
13595
- promptSubmittingRef.current = false;
13596
- promptInputRef.current = "";
13597
- setPromptInput("");
13598
- setPromptStatus(null);
13599
- promptFocusBeforeRef.current = focusRef.current;
13600
- if (filterOpenRef.current) {
13601
- filterOpenRef.current = false;
13602
- setFilterOpen(false);
13993
+ actionTargetRef.current = null;
13994
+ beginPrompt("new-file", "", undefined);
13995
+ };
13996
+ const openRenamePrompt = () => {
13997
+ const file = navigator2.getSnapshot().selectedFile;
13998
+ if (!file)
13999
+ return;
14000
+ actionTargetRef.current = captureActionTarget(file);
14001
+ beginPrompt("rename", file.basename, file.relativePath);
14002
+ };
14003
+ const waitForMembership = async (opts) => {
14004
+ const stillWaiting = () => opts.task === promptTaskGenRef.current && mountedRef.current && opts.isArmed();
14005
+ await navigator2.refresh();
14006
+ const deadline = Date.now() + newFileMembershipTimeoutMs;
14007
+ while (Date.now() < deadline) {
14008
+ if (!stillWaiting())
14009
+ return;
14010
+ const snap = navigator2.getSnapshot();
14011
+ if (snap.files.some((file) => file.absolutePath === opts.dest)) {
14012
+ const selectedSnapshot = navigator2.selectPath(opts.dest);
14013
+ if (!stillWaiting())
14014
+ return;
14015
+ if (selectedSnapshot.selectedFile?.absolutePath === opts.dest) {
14016
+ pendingSelectionPathRef.current = null;
14017
+ opts.disarm();
14018
+ opts.onReady?.();
14019
+ return;
14020
+ }
14021
+ }
14022
+ await sleep(NEW_FILE_MEMBERSHIP_POLL_MS);
13603
14023
  }
13604
- dispatchFloatingOverlay({ type: "open-prompt" });
14024
+ if (!stillWaiting())
14025
+ return;
14026
+ pendingSelectionPathRef.current = null;
14027
+ opts.disarm();
14028
+ pushFooterNotice(opts.timeoutNotice);
13605
14029
  };
13606
14030
  const submitNewFile = () => {
13607
14031
  if (promptSubmittingRef.current)
13608
14032
  return;
13609
14033
  const raw = promptInputRef.current;
13610
- const resolved = resolveNewFileName(raw);
14034
+ const resolved = resolveMarkdownBasename(raw, "new-file");
13611
14035
  if (!resolved.ok) {
13612
14036
  setPromptStatus({ kind: "error", lines: [resolved.error] });
13613
14037
  return;
13614
14038
  }
13615
14039
  promptSubmittingRef.current = true;
13616
- const dest = join3(root, resolved.basename);
13617
- const task = newFileTaskGenRef.current;
14040
+ setPromptSubmitting(true);
14041
+ const dest = join4(root, resolved.basename);
14042
+ const task = promptTaskGenRef.current;
13618
14043
  (async () => {
13619
14044
  const created = await createEmptyFile(dest);
13620
14045
  if (!mountedRef.current)
13621
14046
  return;
13622
- if (task !== newFileTaskGenRef.current) {
14047
+ if (task !== promptTaskGenRef.current) {
13623
14048
  if (created.ok)
13624
14049
  pushFooterNotice(`created ${resolved.basename}`);
13625
14050
  return;
13626
14051
  }
13627
14052
  if (!created.ok) {
13628
14053
  promptSubmittingRef.current = false;
14054
+ setPromptSubmitting(false);
13629
14055
  setPromptStatus({
13630
14056
  kind: "error",
13631
14057
  lines: [
@@ -13634,7 +14060,7 @@ var Browser = ({
13634
14060
  });
13635
14061
  return;
13636
14062
  }
13637
- closeNewFilePrompt(false);
14063
+ closePrompt(false);
13638
14064
  const query = filterInputRef.current;
13639
14065
  if (query.length > 0 && !queryWouldShowRelativePath(query, resolved.basename)) {
13640
14066
  filterInputRef.current = resolved.basename;
@@ -13643,33 +14069,93 @@ var Browser = ({
13643
14069
  }
13644
14070
  pendingSelectionPathRef.current = dest;
13645
14071
  newFileEditPathRef.current = dest;
13646
- const stillWaiting = () => task === newFileTaskGenRef.current && mountedRef.current && newFileEditPathRef.current === dest;
13647
- await navigator2.refresh();
13648
- const deadline = Date.now() + newFileMembershipTimeoutMs;
13649
- while (Date.now() < deadline) {
13650
- if (!stillWaiting())
13651
- return;
13652
- const snap = navigator2.getSnapshot();
13653
- if (snap.files.some((file) => file.absolutePath === dest)) {
13654
- const selectedSnapshot = navigator2.selectPath(dest);
13655
- if (!stillWaiting())
13656
- return;
13657
- if (selectedSnapshot.selectedFile?.absolutePath === dest) {
13658
- pendingSelectionPathRef.current = null;
14072
+ await waitForMembership({
14073
+ dest,
14074
+ task,
14075
+ isArmed: () => newFileEditPathRef.current === dest,
14076
+ disarm: () => {
14077
+ if (newFileEditPathRef.current === dest)
13659
14078
  newFileEditPathRef.current = null;
13660
- editCurrent();
13661
- return;
13662
- }
14079
+ },
14080
+ onReady: () => editCurrent(),
14081
+ timeoutNotice: `created ${resolved.basename}, but it isn't in the file list`
14082
+ });
14083
+ })();
14084
+ };
14085
+ const submitRename = () => {
14086
+ if (promptSubmittingRef.current)
14087
+ return;
14088
+ const target = actionTargetRef.current;
14089
+ if (!target)
14090
+ return;
14091
+ const raw = promptInputRef.current;
14092
+ const resolved = resolveMarkdownBasename(raw, "rename");
14093
+ if (!resolved.ok) {
14094
+ setPromptStatus({ kind: "error", lines: [resolved.error] });
14095
+ return;
14096
+ }
14097
+ if (resolved.basename === target.basename) {
14098
+ closePrompt(true);
14099
+ return;
14100
+ }
14101
+ promptSubmittingRef.current = true;
14102
+ setPromptSubmitting(true);
14103
+ const dest = join4(target.parentDir, resolved.basename);
14104
+ const destRelative = destinationRelativePath(target.parentRelative, resolved.basename);
14105
+ const task = promptTaskGenRef.current;
14106
+ const sourcePath = target.absolutePath;
14107
+ (async () => {
14108
+ const renamed = await renameFile({
14109
+ discoveryRoot: root,
14110
+ sourcePath,
14111
+ parentDir: target.parentDir,
14112
+ newBasename: resolved.basename
14113
+ });
14114
+ if (!mountedRef.current)
14115
+ return;
14116
+ if (task !== promptTaskGenRef.current) {
14117
+ if (renamed.ok && !renamed.noop) {
14118
+ pushFooterNotice(`renamed to ${resolved.basename}`);
13663
14119
  }
13664
- await sleep(NEW_FILE_MEMBERSHIP_POLL_MS);
14120
+ return;
13665
14121
  }
13666
- if (!stillWaiting())
14122
+ if (!renamed.ok) {
14123
+ promptSubmittingRef.current = false;
14124
+ setPromptSubmitting(false);
14125
+ setPromptStatus({
14126
+ kind: "error",
14127
+ lines: [renamed.reason === "already-exists" ? "already exists" : renamed.message]
14128
+ });
13667
14129
  return;
13668
- pendingSelectionPathRef.current = null;
13669
- newFileEditPathRef.current = null;
13670
- pushFooterNotice(`created ${resolved.basename}, but it isn't in the file list`);
14130
+ }
14131
+ closePrompt(false);
14132
+ const query = filterInputRef.current;
14133
+ if (query.length > 0 && !queryWouldShowRelativePath(query, destRelative)) {
14134
+ filterInputRef.current = resolved.basename;
14135
+ setFilterInput(resolved.basename);
14136
+ navigator2.flushSearch(resolved.basename);
14137
+ }
14138
+ retargetPreviewIfNeeded(serverRef.current, sourcePath, dest);
14139
+ pendingSelectionPathRef.current = dest;
14140
+ await waitForMembership({
14141
+ dest,
14142
+ task,
14143
+ isArmed: () => pendingSelectionPathRef.current === dest,
14144
+ disarm: () => {
14145
+ if (pendingSelectionPathRef.current === dest)
14146
+ pendingSelectionPathRef.current = null;
14147
+ },
14148
+ onReady: null,
14149
+ timeoutNotice: `renamed to ${resolved.basename}, but it isn't in the file list`
14150
+ });
13671
14151
  })();
13672
14152
  };
14153
+ const submitPrompt = () => {
14154
+ if (promptPurposeRef.current === "rename")
14155
+ submitRename();
14156
+ else
14157
+ submitNewFile();
14158
+ };
13673
14159
  const ctx = {
13674
14160
  files: displayedFiles,
13675
14161
  hasSelected: selected != null,
@@ -13784,6 +14270,7 @@ var Browser = ({
13784
14270
  },
13785
14271
  editCurrent,
13786
14272
  openNewFilePrompt,
14273
+ openRenamePrompt,
13787
14274
  copyCurrentContents: () => {
13788
14275
  const file = navigator2.getSnapshot().selectedFile;
13789
14276
  if (!file)
@@ -13822,77 +14309,88 @@ var Browser = ({
13822
14309
  const allCommands = buildCommands(ctx);
13823
14310
  const filtered = orderCommandsForPalette(filterCommands(allCommands, paletteQueryRef.current));
13824
14311
  if (key.name === "escape") {
14312
+ key.preventDefault();
13825
14313
  closePalette();
13826
14314
  return;
13827
14315
  }
13828
14316
  if (key.name === "return") {
14317
+ key.preventDefault();
13829
14318
  const picked = filtered[clampSelectedIndex(paletteIndexRef.current, filtered)];
13830
14319
  closePalette();
13831
14320
  picked?.run();
13832
14321
  return;
13833
14322
  }
13834
14323
  if (key.name === "up") {
14324
+ key.preventDefault();
13835
14325
  setPaletteIndexSync(Math.max(0, paletteIndexRef.current - 1));
13836
14326
  return;
13837
14327
  }
13838
14328
  if (key.name === "down") {
14329
+ key.preventDefault();
13839
14330
  setPaletteIndexSync(Math.min(Math.max(0, filtered.length - 1), paletteIndexRef.current + 1));
13840
14331
  return;
13841
14332
  }
13842
- if (key.name === "backspace" || key.name === "delete") {
13843
- if (paletteQueryRef.current.length === 0)
13844
- return;
13845
- paletteQueryRef.current = paletteQueryRef.current.slice(0, -1);
13846
- setPaletteQuery(paletteQueryRef.current);
13847
- setPaletteIndexSync(0);
13848
- return;
13849
- }
13850
14333
  if (key.ctrl && !key.meta && key.name === "p") {
14334
+ key.preventDefault();
13851
14335
  closePalette();
13852
14336
  return;
13853
14337
  }
13854
- if (key.ctrl || key.meta)
13855
- return;
13856
- let char = null;
13857
- if (key.name === "space")
13858
- char = " ";
13859
- else if (typeof key.name === "string" && key.name.length === 1) {
13860
- char = key.shift ? key.name.toUpperCase() : key.name;
13861
- }
13862
- if (char !== null) {
13863
- paletteQueryRef.current = paletteQueryRef.current + char;
13864
- setPaletteQuery(paletteQueryRef.current);
13865
- setPaletteIndexSync(0);
14338
+ if (!paletteInputReadyRef.current) {
14339
+ if (key.name === "backspace" || key.name === "delete") {
14340
+ if (paletteQueryRef.current.length === 0)
14341
+ return;
14342
+ paletteQueryRef.current = paletteQueryRef.current.slice(0, -1);
14343
+ setPaletteQuery(paletteQueryRef.current);
14344
+ setPaletteIndexSync(0);
14345
+ return;
14346
+ }
14347
+ if (key.ctrl || key.meta)
14348
+ return;
14349
+ const char = printableFromKey(key);
14350
+ if (char !== null) {
14351
+ paletteQueryRef.current = paletteQueryRef.current + char;
14352
+ setPaletteQuery(paletteQueryRef.current);
14353
+ setPaletteIndexSync(0);
14354
+ }
13866
14355
  }
13867
14356
  return;
13868
14357
  }
13869
14358
  if (floatingOverlayRef.current.kind === "prompt") {
13870
14359
  if (key.name === "escape") {
13871
- closeNewFilePrompt(true);
14360
+ key.preventDefault();
14361
+ closePrompt(true);
13872
14362
  return;
13873
14363
  }
13874
- if (promptSubmittingRef.current)
14364
+ if (promptSubmittingRef.current) {
14365
+ key.preventDefault();
13875
14366
  return;
14367
+ }
13876
14368
  if (key.name === "return") {
13877
- submitNewFile();
14369
+ key.preventDefault();
14370
+ submitPrompt();
13878
14371
  return;
13879
14372
  }
13880
- if (key.name === "backspace" || key.name === "delete") {
13881
- if (promptInputRef.current.length === 0)
13882
- return;
13883
- applyPromptInput(dropLastCodePoint(promptInputRef.current));
14373
+ if (key.name === "up" || key.name === "down") {
14374
+ key.preventDefault();
13884
14375
  return;
13885
14376
  }
13886
- if (key.ctrl || key.meta)
14377
+ if (key.ctrl && !key.meta && key.name === "p") {
14378
+ key.preventDefault();
13887
14379
  return;
13888
- let char = null;
13889
- if (key.name === "space")
13890
- char = " ";
13891
- else if (typeof key.name === "string" && isSingleCodePoint(key.name)) {
13892
- char = key.shift ? key.name.toUpperCase() : key.name;
13893
- }
13894
- if (char !== null)
13895
- applyPromptInput(promptInputRef.current + char);
14380
+ }
14381
+ if (!promptInputReadyRef.current) {
14382
+ if (key.name === "backspace" || key.name === "delete") {
14383
+ if (promptInputRef.current.length === 0)
14384
+ return;
14385
+ applyPromptInput(dropLastCodePoint(promptInputRef.current));
14386
+ return;
14387
+ }
14388
+ if (key.ctrl || key.meta)
14389
+ return;
14390
+ const char = printableFromKey(key);
14391
+ if (char !== null)
14392
+ applyPromptInput(promptInputRef.current + char);
14393
+ }
13896
14394
  return;
13897
14395
  }
13898
14396
  if (filterOpenRef.current && focusRef.current === "sidebar") {
@@ -13913,14 +14411,17 @@ var Browser = ({
13913
14411
  }
13914
14412
  };
13915
14413
  if (key.name === "escape") {
14414
+ key.preventDefault();
13916
14415
  closeFilter(false);
13917
14416
  return;
13918
14417
  }
13919
14418
  if (key.name === "return") {
14419
+ key.preventDefault();
13920
14420
  closeFilter(true);
13921
14421
  return;
13922
14422
  }
13923
14423
  if (key.name === "tab" || key.ctrl && key.name === "i" && !key.shift && !key.meta) {
14424
+ key.preventDefault();
13924
14425
  focusRef.current = "reader";
13925
14426
  restoreFilterOnSidebarFocusRef.current = true;
13926
14427
  filterOpenRef.current = false;
@@ -13929,46 +14430,57 @@ var Browser = ({
13929
14430
  return;
13930
14431
  }
13931
14432
  if (key.ctrl && !key.meta && key.name === "p") {
14433
+ key.preventDefault();
13932
14434
  ctx.openPalette();
13933
14435
  return;
13934
14436
  }
13935
14437
  if (key.ctrl && key.name === "\\") {
14438
+ key.preventDefault();
13936
14439
  filterInputRef.current = "";
13937
14440
  setFilterInput("");
13938
14441
  navigator2.flushSearch("");
13939
14442
  navigator2.selectFirst();
13940
14443
  return;
13941
14444
  }
13942
- if (key.name === "backspace" || key.name === "delete") {
13943
- if (filterInputRef.current.length === 0) {
13944
- closeFilter(false);
13945
- return;
13946
- }
13947
- filterInputRef.current = filterInputRef.current.slice(0, -1);
13948
- setFilterInput(filterInputRef.current);
13949
- navigator2.selectIndex(0);
14445
+ if ((key.name === "backspace" || key.name === "delete") && filterInputRef.current.length === 0) {
14446
+ key.preventDefault();
14447
+ closeFilter(false);
13950
14448
  return;
13951
14449
  }
13952
14450
  if (key.name === "up") {
14451
+ key.preventDefault();
13953
14452
  navigator2.moveBy(-1);
13954
14453
  return;
13955
14454
  }
13956
14455
  if (key.name === "down") {
14456
+ key.preventDefault();
13957
14457
  navigator2.moveBy(1);
13958
14458
  return;
13959
14459
  }
13960
- if (key.ctrl || key.meta)
14460
+ if (key.ctrl && (key.name === "u" || key.name === "d")) {
14461
+ key.preventDefault();
13961
14462
  return;
13962
- let char = null;
13963
- if (key.name === "space")
13964
- char = " ";
13965
- else if (typeof key.name === "string" && key.name.length === 1) {
13966
- char = key.shift ? key.name.toUpperCase() : key.name;
13967
14463
  }
13968
- if (char !== null) {
13969
- filterInputRef.current = filterInputRef.current + char;
13970
- setFilterInput(filterInputRef.current);
13971
- navigator2.selectIndex(0);
14464
+ if (!filterInputReadyRef.current) {
14465
+ if (key.name === "backspace" || key.name === "delete") {
14466
+ if (filterInputRef.current.length === 0) {
14467
+ key.preventDefault();
14468
+ closeFilter(false);
14469
+ return;
14470
+ }
14471
+ filterInputRef.current = filterInputRef.current.slice(0, -1);
14472
+ setFilterInput(filterInputRef.current);
14473
+ navigator2.selectIndex(0);
14474
+ return;
14475
+ }
14476
+ if (key.ctrl || key.meta)
14477
+ return;
14478
+ const char = printableFromKey(key);
14479
+ if (char !== null) {
14480
+ filterInputRef.current = filterInputRef.current + char;
14481
+ setFilterInput(filterInputRef.current);
14482
+ navigator2.selectIndex(0);
14483
+ }
13972
14484
  }
13973
14485
  return;
13974
14486
  }
@@ -14007,15 +14519,11 @@ var Browser = ({
14007
14519
  width,
14008
14520
  notice: footerNotice?.text ?? null,
14009
14521
  discoveryStatus: effectiveDiscoveryStatus,
14010
- indicators: [
14011
- {
14012
- id: "wrap",
14013
- icon: "W",
14014
- variant: "info",
14015
- active: wrapEnabled,
14016
- onMouseUp: toggleWrap
14017
- }
14018
- ],
14522
+ indicators: optionsSession.current === null ? [] : footerControlsFromSession(houseOptions, optionsSession.current, {
14523
+ wrap: { onActivate: toggleWrap },
14524
+ theme: { onActivate: () => cycleTheme(1) },
14525
+ order: { onActivate: cycleOrder }
14526
+ }),
14019
14527
  ...discoverySpinnerIntervalMs === undefined ? {} : { discoverySpinnerIntervalMs },
14020
14528
  ...discoverySpinnerInitialFrameIndex === undefined ? {} : { discoverySpinnerInitialFrameIndex },
14021
14529
  ...discoverySpinnerRegisterTick === undefined ? {} : { discoverySpinnerRegisterTick },
@@ -14050,12 +14558,20 @@ var Browser = ({
14050
14558
  root,
14051
14559
  policy,
14052
14560
  watch: watch2,
14053
- order,
14561
+ order: browseOrder,
14054
14562
  debounceMs: filterDebounceMs,
14055
14563
  navigatorRef,
14056
14564
  snapshot: liveSnapshot,
14057
14565
  filterInput,
14058
14566
  filterOpen,
14567
+ onFilterInput: (next) => {
14568
+ filterInputRef.current = next;
14569
+ setFilterInput(next);
14570
+ navigator2.selectIndex(0);
14571
+ },
14572
+ onFilterEditingReady: (ready) => {
14573
+ filterInputReadyRef.current = ready;
14574
+ },
14059
14575
  discoveryActive,
14060
14576
  rootLabel,
14061
14577
  viewportHeight: height,
@@ -14191,16 +14707,31 @@ var Browser = ({
14191
14707
  query: paletteQuery,
14192
14708
  selectedIndex: paletteIndex,
14193
14709
  viewportWidth: width,
14194
- viewportHeight: height
14710
+ viewportHeight: height,
14711
+ onQueryChange: (next) => {
14712
+ paletteQueryRef.current = next;
14713
+ setPaletteQuery(next);
14714
+ paletteIndexRef.current = 0;
14715
+ setPaletteIndex(0);
14716
+ },
14717
+ onInputReady: (ready) => {
14718
+ paletteInputReadyRef.current = ready;
14719
+ }
14195
14720
  }, undefined, false, undefined, this),
14196
14721
  promptOpen && /* @__PURE__ */ jsxDEV13(PromptModal, {
14197
- title: "New file",
14722
+ title: promptPurpose === "rename" ? "Rename" : "New file",
14198
14723
  query: promptInput,
14199
14724
  placeholder: "File name",
14200
- hints: "enter create esc cancel",
14725
+ hints: promptPurpose === "rename" ? "enter rename esc cancel" : "enter create esc cancel",
14201
14726
  status: promptStatus,
14727
+ ...promptContext !== undefined ? { context: promptContext } : {},
14202
14728
  viewportWidth: width,
14203
- viewportHeight: height
14729
+ viewportHeight: height,
14730
+ onQueryChange: applyPromptInput,
14731
+ onInputReady: (ready) => {
14732
+ promptInputReadyRef.current = ready;
14733
+ },
14734
+ inputEnabled: !promptSubmitting
14204
14735
  }, undefined, false, undefined, this),
14205
14736
  activeStatusPopover && /* @__PURE__ */ jsxDEV13(StatusPopoverPanel, {
14206
14737
  content: activeStatusPopover.content,
@@ -14345,15 +14876,15 @@ var parseAndHandleFastExit = (argv) => {
14345
14876
  };
14346
14877
 
14347
14878
  // src/discovery/rootLabel.ts
14348
- import { isAbsolute, relative as relative3 } from "path";
14879
+ import { isAbsolute as isAbsolute2, relative as relative4 } from "path";
14349
14880
  var formatDiscoveryRootLabel = ({
14350
14881
  discoveryRoot,
14351
14882
  home
14352
14883
  }) => {
14353
14884
  if (discoveryRoot === home)
14354
14885
  return "~";
14355
- const homeRelative = relative3(home, discoveryRoot);
14356
- if (homeRelative.length > 0 && !homeRelative.startsWith("..") && !isAbsolute(homeRelative)) {
14886
+ const homeRelative = relative4(home, discoveryRoot);
14887
+ if (homeRelative.length > 0 && !homeRelative.startsWith("..") && !isAbsolute2(homeRelative)) {
14357
14888
  return `~/${homeRelative}`;
14358
14889
  }
14359
14890
  return discoveryRoot;
@@ -14401,16 +14932,16 @@ var isNewer = (candidate, current) => {
14401
14932
  };
14402
14933
 
14403
14934
  // src/update/cache.ts
14404
- import { dirname as dirname2, join as join4 } from "path";
14935
+ import { dirname as dirname4, join as join5 } from "path";
14405
14936
  import { homedir as homedir2 } from "os";
14406
- import { mkdir as mkdir2, readFile as readFile5, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
14937
+ import { mkdir as mkdir2, readFile as readFile5, rename as rename3, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
14407
14938
  var cacheDir = () => {
14408
14939
  const xdg = process.env.XDG_CACHE_HOME;
14409
14940
  if (xdg && xdg.length > 0)
14410
- return join4(xdg, "house");
14411
- return join4(homedir2(), ".cache", "house");
14941
+ return join5(xdg, "house");
14942
+ return join5(homedir2(), ".cache", "house");
14412
14943
  };
14413
- var cachePath = () => join4(cacheDir(), "update-check.json");
14944
+ var cachePath = () => join5(cacheDir(), "update-check.json");
14414
14945
  var readCache = async (path = cachePath()) => {
14415
14946
  try {
14416
14947
  const raw = await readFile5(path, "utf8");
@@ -14433,9 +14964,9 @@ var readCache = async (path = cachePath()) => {
14433
14964
  var writeCache = async (record, path = cachePath()) => {
14434
14965
  const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
14435
14966
  try {
14436
- await mkdir2(dirname2(path), { recursive: true });
14967
+ await mkdir2(dirname4(path), { recursive: true });
14437
14968
  await writeFile2(tmp, JSON.stringify(record), "utf8");
14438
- await rename2(tmp, path);
14969
+ await rename3(tmp, path);
14439
14970
  } catch {
14440
14971
  try {
14441
14972
  await unlink2(tmp);
@@ -14575,18 +15106,18 @@ var useUpdateNotice = () => {
14575
15106
  import { jsxDEV as jsxDEV14 } from "@opentui/react/jsx-dev-runtime";
14576
15107
  var pathIsDirectory = async (path) => {
14577
15108
  try {
14578
- return (await stat2(path)).isDirectory();
15109
+ return (await stat3(path)).isDirectory();
14579
15110
  } catch {
14580
15111
  return false;
14581
15112
  }
14582
15113
  };
14583
15114
  var findGitRoot = async (cwd) => {
14584
- const start = resolve3(cwd);
15115
+ const start = resolve4(cwd);
14585
15116
  let current2 = start;
14586
15117
  for (;; ) {
14587
- if (await pathIsDirectory(resolve3(current2, ".git")))
15118
+ if (await pathIsDirectory(resolve4(current2, ".git")))
14588
15119
  return current2;
14589
- const parent = dirname3(current2);
15120
+ const parent = dirname5(current2);
14590
15121
  if (parent === current2)
14591
15122
  return start;
14592
15123
  current2 = parent;
@@ -14598,7 +15129,7 @@ var resolveDiscoveryRoot = async ({
14598
15129
  cwd
14599
15130
  }) => {
14600
15131
  if (cliRoot !== null)
14601
- return resolve3(cwd, cliRoot);
15132
+ return resolve4(cwd, cliRoot);
14602
15133
  if (defaultRoot === "git")
14603
15134
  return findGitRoot(cwd);
14604
15135
  return cwd;
@@ -14606,7 +15137,7 @@ var resolveDiscoveryRoot = async ({
14606
15137
  var validateDiscoveryRoot = async (root) => {
14607
15138
  let stats;
14608
15139
  try {
14609
- stats = await stat2(root);
15140
+ stats = await stat3(root);
14610
15141
  } catch (err) {
14611
15142
  throw new Error(`cannot access discovery root ${root}: ${String(err)}`);
14612
15143
  }
@@ -14640,10 +15171,10 @@ var buildDiscoveryPolicy = (show, extensions) => {
14640
15171
  followSymlinks: false,
14641
15172
  ignoreFiles: show.includes("gitignored") ? [] : [".gitignore"],
14642
15173
  includeFile: (path) => {
14643
- const name = basename3(path);
15174
+ const name = basename4(path);
14644
15175
  return allowed.has(name.slice(name.lastIndexOf(".")).toLowerCase()) && visiblePath(path) && !path.split(/[\\/]/).some((part) => hardSkips.has(part));
14645
15176
  },
14646
- includeDirectory: (path) => !hardSkips.has(basename3(path)) && visiblePath(path)
15177
+ includeDirectory: (path) => !hardSkips.has(basename4(path)) && visiblePath(path)
14647
15178
  };
14648
15179
  };
14649
15180
  var DiscoverShell = ({
@@ -14683,11 +15214,11 @@ var resolveInitialQuery = ({
14683
15214
  }) => {
14684
15215
  if (pathArg === null)
14685
15216
  return "";
14686
- const resolvedPath = resolve3(cwd, pathArg);
14687
- const rel = relative4(discoveryRoot, resolvedPath);
15217
+ const resolvedPath = resolve4(cwd, pathArg);
15218
+ const rel = relative5(discoveryRoot, resolvedPath);
14688
15219
  if (rel.length === 0)
14689
15220
  return "";
14690
- if (!rel.startsWith("..") && !isAbsolute2(rel))
15221
+ if (!rel.startsWith("..") && !isAbsolute3(rel))
14691
15222
  return rel;
14692
15223
  if (pathArg.startsWith("./"))
14693
15224
  return pathArg.slice(2);
@@ -14755,7 +15286,7 @@ async function main(argv = Bun.argv.slice(2)) {
14755
15286
  }
14756
15287
  let stats;
14757
15288
  try {
14758
- stats = await stat2(target);
15289
+ stats = await stat3(target);
14759
15290
  } catch (err) {
14760
15291
  console.error(`house: cannot access ${target}: ${String(err)}`);
14761
15292
  process.exit(1);