diff --git a/app/globals.css b/app/globals.css index efb8a29..c1c9c7c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -197,6 +197,16 @@ body { color: var(--lh-ink-italic); } +/* Markdown lists indent from the prose edge (owner ask, 2026-09-24): + streamdown ships list-inside with no container padding (top-level + markers sit ON the text edge — nested lists get their pl-6 only via + [li_&]). 24px — the same measure as that nested-case pl-6 — pulls + the whole list right as a block. Unlayered CSS beats the utility + layer; :where keeps specificity zero. */ +.lh-prose :where(ul, ol) { + padding-left: 1.5rem; +} + /* Markdown headings render at the font's BLACK weight (owner ask, 2026-09-19) — 900, not streamdown's font-semibold 600. The variable Source Sans 3 axis spans 200–900 (next/font loads the full range), so diff --git a/components/extraction-coverage.tsx b/components/extraction-coverage.tsx index 9758c2b..bf572d9 100644 --- a/components/extraction-coverage.tsx +++ b/components/extraction-coverage.tsx @@ -97,7 +97,7 @@ async function requestExtract(chatId: string): Promise { // ─── The sidebar context menu's coverage block ────────────────────────────── /** - * The info block at the top of the session context menu (owner-locked + * The info block at the bottom of the session context menu (owner-locked * redesign): what the extractor hasn't covered yet, plus the manual Extract * affordance — visually separated from Rename/Delete below (those stay the * two session-management actions). Fetched ON MENU OPEN (the block mounts @@ -191,7 +191,7 @@ export function CoverageMenuBody({ ) : ( - {formatTokens(coverage.uncoveredTokens)} uncovered + {formatTokens(coverage.uncoveredTokens)} tok uncovered {coverage.uncoveredCount} message{coverage.uncoveredCount === 1 ? "" : "s"} unextracted @@ -305,7 +305,7 @@ export function CoverageRing({ chatId, refreshKey = 0 }: { chatId: string; refre {open && (
diff --git a/components/question-dock.tsx b/components/question-dock.tsx index a0adbb0..feb1bf4 100644 --- a/components/question-dock.tsx +++ b/components/question-dock.tsx @@ -323,8 +323,19 @@ function QuestionDockForm({ else if (kind === "quiz") setSel((prev) => ({ ...prev, selected: new Set(), otherSelected: false, dontKnow: !prev.dontKnow })); else { - selectOther(); - otherInputRef.current?.focus(); + // Space on the ask's Other row (owner refinement round, + // 2026-09-24): SELECTING it focuses the input for typing, but + // the Space that DESELECTS an already-selected Other must never + // drop the user back into the typing field — the core's + // toggleOther has no single-select off-path (it displaces), so + // the off-path lives here: clear the pick, keep the typed text, + // focus stays where it is. Re-selecting is the way back into + // the field. + if (sel.otherSelected) setSel((prev) => ({ ...prev, otherSelected: false, dontKnow: false })); + else { + otherInputRef.current?.focus(); + selectOther(); + } } }; @@ -439,7 +450,7 @@ function QuestionDockForm({ )} > {isSel && ( - + {qd.mode === "multi-select" ? "✓" : ""} )} @@ -509,7 +520,7 @@ function QuestionDockForm({ )} > {sel.otherSelected && ( - + {qd.mode === "multi-select" ? "✓" : ""} )} @@ -523,6 +534,20 @@ function QuestionDockForm({ // single-select, alongside the siblings in multi. setSel((prev) => (prev.otherSelected ? prev : toggleOther(selectMode, prev))); }} + onKeyDown={(e) => { + // Esc escapes the typing (owner refinement round, + // 2026-09-24): focus leaves the input, the row STAYS + // selected — deselection is the next Space's job, acting + // on the row — and the arrow walk resumes from this + // row (kbdIdx pins it). stopPropagation keeps the + // window chain from reading this Esc as abort-turn. + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + e.currentTarget.blur(); + setKbdIdx(optionCount); + } + }} placeholder="Type your own answer…" className="lh-input w-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" onClick={(e) => e.stopPropagation()} diff --git a/components/sidebar.tsx b/components/sidebar.tsx index 0f266d9..0809bc0 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -377,11 +377,11 @@ function SessionsSidebar() { const listRef = useRef(null); /* ── Wave 6 chunk A: context menu + inline rename + deletion ── - Right-click a session row → MenuShell: the extraction coverage - block (owner-locked redesign, 2026-09-24 — numbers + the manual - Extract affordance, separated by a hairline) above the two - session-management actions (owner ruling): Rename (inline, below) - and Delete (the two-option dialog). The anchor is the row that was + Right-click a session row → MenuShell: the two management actions + first (owner ruling), then — below a hairline — the extraction + coverage block (owner-locked redesign 2026-09-24, order refined + 2026-09-24: Rename/Delete on top, the numbers + manual Extract + affordance at the bottom). The anchor is the row that was right-clicked — assigned into a ref before the state flip so the shell measures it on its first (hidden) pass. */ const [ctxChatId, setCtxChatId] = useState(null); @@ -662,14 +662,12 @@ function SessionsSidebar() { preferredSide="down" menuClass="flex w-52 flex-col gap-0.5 rounded-xl border bg-card p-2 text-xs shadow-xl" > - {/* The extraction block (owner-locked redesign, 2026-09-24): - what the extractor hasn't covered yet + the manual Extract - affordance — visually separated from the management - actions below by a hairline. Fetched on menu open (the - block mounts WITH the menu); 409 = benign (already - running). Renders nothing on a failed read. */} - -