Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/tui/reader/reader-reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,55 @@ describe("readerReducer", () => {
if (next.kind !== "awaiting") throw new Error("expected awaiting");
expect(next.phase).toBe("chapter");
});

it("preserves chapter phase when the trailing space is deleted (query equals book name exactly)", () => {
// Bug: backspacing the trailing space after picking "John" used to drop
// the user out of chapter phase, hiding the chapter grid. Deleting the
// space alone is not a signal that the user wants to re-search books.
const state = makeAwaiting({
phase: "chapter",
bookChosen: { alias: "john", canonical: "JHN", displayName: "John" },
chapters: Array.from({ length: 21 }, (_, i) => i + 1),
selectedIndex: 0,
query: "John ",
});
const next = dispatch(state, { type: "QueryTyped", query: "John" });
if (next.kind !== "awaiting") throw new Error("expected awaiting");
expect(next.phase).toBe("chapter");
expect(next.bookChosen).not.toBeNull();
expect(next.chapters.length).toBe(21);
});

it("highlights the chapter when digits follow the book name without a space", () => {
// Bug follow-up: after deleting the trailing space, typing a digit
// produces "John1" (no space). The chapter grid should still highlight
// chapter 1, matching the "John 1" behavior.
const state = makeAwaiting({
phase: "chapter",
bookChosen: { alias: "john", canonical: "JHN", displayName: "John" },
chapters: Array.from({ length: 21 }, (_, i) => i + 1),
selectedIndex: 0,
query: "John",
});
const next = dispatch(state, { type: "QueryTyped", query: "John1" });
if (next.kind !== "awaiting") throw new Error("expected awaiting");
expect(next.phase).toBe("chapter");
expect(next.selectedIndex).toBe(0);
});

it("highlights chapter 10 for 'John10' (multi-digit, no space)", () => {
const state = makeAwaiting({
phase: "chapter",
bookChosen: { alias: "john", canonical: "JHN", displayName: "John" },
chapters: Array.from({ length: 21 }, (_, i) => i + 1),
selectedIndex: 0,
query: "John",
});
const next = dispatch(state, { type: "QueryTyped", query: "John10" });
if (next.kind !== "awaiting") throw new Error("expected awaiting");
expect(next.phase).toBe("chapter");
expect(next.selectedIndex).toBe(9);
});
});

describe("PassageFetched (intent branches)", () => {
Expand Down
40 changes: 22 additions & 18 deletions src/tui/reader/reader-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,29 @@ export type ReaderAction =
const handlers = {
QueryTyped: (s: ReaderState, a: Extract<ReaderAction, { type: "QueryTyped" }>): ReaderState => {
if (s.kind !== "awaiting") return s;
// Stay in chapter phase if the new query still has the chosen book's display
// name as a prefix. Without this, OpenTUI's controlled <input> kicks us out
// of chapter mode the instant SuggestionAccepted programmatically rewrites
// the query, because the value-prop change synthesizes an onInput event.
if (
s.phase === "chapter" &&
s.bookChosen !== null &&
a.query.toLowerCase().startsWith(`${s.bookChosen.displayName.toLowerCase()} `)
) {
// Decode trailing digit suffix → grid selection. Multi-digit naturally works:
// "John 1" highlights chapter 1, "John 10" highlights chapter 10.
const suffix = a.query.slice(s.bookChosen.displayName.length + 1);
const digitMatch = /^(\d+)/.exec(suffix);
let selectedIndex = s.selectedIndex;
if (digitMatch) {
const n = parseInt(digitMatch[1], 10);
if (n >= 1 && n <= s.chapters.length) selectedIndex = n - 1;
// Stay in chapter phase when the new query is the chosen book's display
// name, optionally followed by a space and/or digits (e.g. "John", "John ",
// "John 1", "John1"). Deleting just the trailing space must NOT kick the
// user back to book phase — that hides the chapter grid mid-edit and is
// what produced the "sometimes the chapter menu doesn't show up" bug.
if (s.phase === "chapter" && s.bookChosen !== null) {
const dn = s.bookChosen.displayName.toLowerCase();
const q = a.query.toLowerCase();
if (q.startsWith(dn)) {
const rest = q.slice(dn.length);
if (rest === "" || rest.startsWith(" ") || /^\d/.test(rest)) {
// Decode digit suffix → grid selection. Optional space between name
// and digits; multi-digit naturally works ("John 10" / "John10").
const afterBook = a.query.slice(s.bookChosen.displayName.length);
const digitMatch = /^\s*(\d+)/.exec(afterBook);
let selectedIndex = s.selectedIndex;
if (digitMatch) {
const n = parseInt(digitMatch[1], 10);
if (n >= 1 && n <= s.chapters.length) selectedIndex = n - 1;
}
return { ...s, query: a.query, parseError: null, selectedIndex };
}
}
return { ...s, query: a.query, parseError: null, selectedIndex };
}
// Book phase: re-suggest and auto-highlight the top match so Tab/Enter
// accept the obvious choice without arrow-down first (fzf-style).
Expand Down
Loading