Refinement round: ring popover down, menu order, tok unit, list indent, lowercase levels

Five small fixes (owner refinement round, 2026-09-24):

1. RING POPOVER OPENS DOWN: the ring lives in the chat top bar —
   the window's top edge is right above it, so the upward popover was
   clipped. top-full mt-1.5 now: it drops below the ring.

2. SESSION MENU ORDER FLIPPED: Rename/Delete first, hairline, then
   the extraction coverage block + Extract at the bottom — the info
   reads after the actions (was the reverse, the block on top).

3. UNIT TYPED OUT: the sidebar block reads "2k tok uncovered" now —
   the tokens unit is explicit (owner: "tok").

4. MARKDOWN LIST INDENT: streamdown ships list-inside with no
   container padding — top-level markers sat ON the text edge (only
   nested lists got their pl-6 via [li_&]). .lh-prose :where(ul, ol)
   { padding-left: 1.5rem } indents both list kinds as blocks, 24px —
   the same measure as the nested-case pl-6. Unlayered CSS beats the
   utility layer; :where keeps specificity zero.

5. THINKING LEVELS LOWERCASE EVERYWHERE: the composer pill and setup
   dialog rendered THINKING_LABEL (Off/Minimal/…/Max) while every
   menu body renders the raw lowercase level — the map is now
   lowercase, the level string IS the display form, everywhere.

Live-verified on :3001: list content inset 24px vs paragraphs
(padding-left computed 24px, nested fine); the composer pill reads
"low" with options low/high/max; menu order Rename → Delete… →
hairline → block. 901/901.
This commit is contained in:
amosblomqvist
2026-09-24 15:51:40 +03:00
parent bc1f668e2a
commit 9f7dd1a7e9
5 changed files with 39 additions and 25 deletions
+10
View File
@@ -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
+3 -3
View File
@@ -97,7 +97,7 @@ async function requestExtract(chatId: string): Promise<boolean> {
// ─── 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({
) : (
<span className="flex flex-col">
<span className="tabular-nums text-foreground">
{formatTokens(coverage.uncoveredTokens)} uncovered
{formatTokens(coverage.uncoveredTokens)} tok uncovered
</span>
<span className="tabular-nums text-muted-foreground">
{coverage.uncoveredCount} message{coverage.uncoveredCount === 1 ? "" : "s"} unextracted
@@ -305,7 +305,7 @@ export function CoverageRing({ chatId, refreshKey = 0 }: { chatId: string; refre
{open && (
<div
data-testid="coverage-ring-popover"
className="absolute bottom-full right-0 z-40 mb-1.5 w-64 rounded-xl border bg-card p-3 text-xs shadow-xl"
className="absolute top-full right-0 z-40 mt-1.5 w-64 rounded-xl border bg-card p-3 text-xs shadow-xl"
>
<CoverageRingDetail coverage={coverage} chunkTokens={graph.chunkTokens} />
</div>
+13 -13
View File
@@ -377,11 +377,11 @@ function SessionsSidebar() {
const listRef = useRef<HTMLDivElement>(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<string | null>(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. */}
<CoverageMenuBlock chatId={ctxChatId} />
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
{/* The two management actions first, then the extraction block
(owner refinement round, 2026-09-24): Rename/Delete stay at the
top, the coverage numbers + Extract affordance sit below the
hairline — the block is info + a stateful affordance, the
actions above it. Fetched on menu open (the block mounts WITH
the menu); 409 = benign. Renders nothing on a failed read. */}
<button
type="button"
data-testid="ctx-rename"
@@ -693,6 +691,8 @@ function SessionsSidebar() {
>
Delete…
</button>
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
<CoverageMenuBlock chatId={ctxChatId} />
</MenuShell>
)}
+12 -8
View File
@@ -15,15 +15,19 @@
import { DEFAULT_THINKING_ORDER } from "@/src/core";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
/** Display labels for the levels (shared by every trigger that shows one). */
/** Display labels for the levels (shared by every trigger that shows
* one). Lowercase, always (owner refinement round, 2026-09-24): the
* level strings ARE the display form — the settings menus render them
* raw, so the composer pill and setup dialog must match, never Title
* Case. */
export const THINKING_LABEL: Record<string, string> = {
off: "Off",
minimal: "Minimal",
low: "Low",
medium: "Medium",
high: "High",
xhigh: "Xhigh",
max: "Max",
off: "off",
minimal: "minimal",
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
max: "max",
};
/** The level a fresh pick of `supported` populates with. Empty support
+1 -1
View File
@@ -54,7 +54,7 @@ describe("CoverageMenuBody — the context-menu block", () => {
<CoverageMenuBody coverage={coverage()} extracting={false} onExtract={() => {}} />,
);
assert.ok(html.includes('data-testid="ctx-coverage"'), "the block carries its testid");
assert.match(html, /2k uncovered/);
assert.match(html, /2k tok uncovered/, "the unit is typed out — tok (owner refinement round)");
assert.match(html, /5 messages unextracted/);
assert.ok(html.includes('data-testid="ctx-extract"'), "the Extract affordance");
assert.ok(!html.includes('disabled=""'), "an uncovered session can extract (the disabled: utilities alone don't count)");