Merge branch 'feat/ui-polish'
This commit is contained in:
@@ -197,6 +197,16 @@ body {
|
|||||||
color: var(--lh-ink-italic);
|
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,
|
/* Markdown headings render at the font's BLACK weight (owner ask,
|
||||||
2026-09-19) — 900, not streamdown's font-semibold 600. The variable
|
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
|
Source Sans 3 axis spans 200–900 (next/font loads the full range), so
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ async function requestExtract(chatId: string): Promise<boolean> {
|
|||||||
// ─── The sidebar context menu's coverage block ──────────────────────────────
|
// ─── 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
|
* redesign): what the extractor hasn't covered yet, plus the manual Extract
|
||||||
* affordance — visually separated from Rename/Delete below (those stay the
|
* affordance — visually separated from Rename/Delete below (those stay the
|
||||||
* two session-management actions). Fetched ON MENU OPEN (the block mounts
|
* 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="flex flex-col">
|
||||||
<span className="tabular-nums text-foreground">
|
<span className="tabular-nums text-foreground">
|
||||||
{formatTokens(coverage.uncoveredTokens)} uncovered
|
{formatTokens(coverage.uncoveredTokens)} tok uncovered
|
||||||
</span>
|
</span>
|
||||||
<span className="tabular-nums text-muted-foreground">
|
<span className="tabular-nums text-muted-foreground">
|
||||||
{coverage.uncoveredCount} message{coverage.uncoveredCount === 1 ? "" : "s"} unextracted
|
{coverage.uncoveredCount} message{coverage.uncoveredCount === 1 ? "" : "s"} unextracted
|
||||||
@@ -305,7 +305,7 @@ export function CoverageRing({ chatId, refreshKey = 0 }: { chatId: string; refre
|
|||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
data-testid="coverage-ring-popover"
|
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} />
|
<CoverageRingDetail coverage={coverage} chunkTokens={graph.chunkTokens} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -323,8 +323,19 @@ function QuestionDockForm({
|
|||||||
else if (kind === "quiz")
|
else if (kind === "quiz")
|
||||||
setSel((prev) => ({ ...prev, selected: new Set(), otherSelected: false, dontKnow: !prev.dontKnow }));
|
setSel((prev) => ({ ...prev, selected: new Set(), otherSelected: false, dontKnow: !prev.dontKnow }));
|
||||||
else {
|
else {
|
||||||
selectOther();
|
// Space on the ask's Other row (owner refinement round,
|
||||||
otherInputRef.current?.focus();
|
// 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 && (
|
{isSel && (
|
||||||
<span className={cn(qd.mode === "multi-select" ? "text-xs" : "size-1.5 rounded-full bg-current")}>
|
<span className={cn(qd.mode === "multi-select" ? "text-base leading-none" : "size-1.5 rounded-full bg-current")}>
|
||||||
{qd.mode === "multi-select" ? "✓" : ""}
|
{qd.mode === "multi-select" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -509,7 +520,7 @@ function QuestionDockForm({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{sel.otherSelected && (
|
{sel.otherSelected && (
|
||||||
<span className={cn(qd.mode === "multi-select" ? "text-xs" : "size-1.5 rounded-full bg-current")}>
|
<span className={cn(qd.mode === "multi-select" ? "text-base leading-none" : "size-1.5 rounded-full bg-current")}>
|
||||||
{qd.mode === "multi-select" ? "✓" : ""}
|
{qd.mode === "multi-select" ? "✓" : ""}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -523,6 +534,20 @@ function QuestionDockForm({
|
|||||||
// single-select, alongside the siblings in multi.
|
// single-select, alongside the siblings in multi.
|
||||||
setSel((prev) => (prev.otherSelected ? prev : toggleOther(selectMode, prev)));
|
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…"
|
placeholder="Type your own answer…"
|
||||||
className="lh-input w-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
|
className="lh-input w-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|||||||
+13
-13
@@ -377,11 +377,11 @@ function SessionsSidebar() {
|
|||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
/* ── Wave 6 chunk A: context menu + inline rename + deletion ──
|
/* ── Wave 6 chunk A: context menu + inline rename + deletion ──
|
||||||
Right-click a session row → MenuShell: the extraction coverage
|
Right-click a session row → MenuShell: the two management actions
|
||||||
block (owner-locked redesign, 2026-09-24 — numbers + the manual
|
first (owner ruling), then — below a hairline — the extraction
|
||||||
Extract affordance, separated by a hairline) above the two
|
coverage block (owner-locked redesign 2026-09-24, order refined
|
||||||
session-management actions (owner ruling): Rename (inline, below)
|
2026-09-24: Rename/Delete on top, the numbers + manual Extract
|
||||||
and Delete (the two-option dialog). The anchor is the row that was
|
affordance at the bottom). The anchor is the row that was
|
||||||
right-clicked — assigned into a ref before the state flip so the
|
right-clicked — assigned into a ref before the state flip so the
|
||||||
shell measures it on its first (hidden) pass. */
|
shell measures it on its first (hidden) pass. */
|
||||||
const [ctxChatId, setCtxChatId] = useState<string | null>(null);
|
const [ctxChatId, setCtxChatId] = useState<string | null>(null);
|
||||||
@@ -662,14 +662,12 @@ function SessionsSidebar() {
|
|||||||
preferredSide="down"
|
preferredSide="down"
|
||||||
menuClass="flex w-52 flex-col gap-0.5 rounded-xl border bg-card p-2 text-xs shadow-xl"
|
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):
|
{/* The two management actions first, then the extraction block
|
||||||
what the extractor hasn't covered yet + the manual Extract
|
(owner refinement round, 2026-09-24): Rename/Delete stay at the
|
||||||
affordance — visually separated from the management
|
top, the coverage numbers + Extract affordance sit below the
|
||||||
actions below by a hairline. Fetched on menu open (the
|
hairline — the block is info + a stateful affordance, the
|
||||||
block mounts WITH the menu); 409 = benign (already
|
actions above it. Fetched on menu open (the block mounts WITH
|
||||||
running). Renders nothing on a failed read. */}
|
the menu); 409 = benign. Renders nothing on a failed read. */}
|
||||||
<CoverageMenuBlock chatId={ctxChatId} />
|
|
||||||
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="ctx-rename"
|
data-testid="ctx-rename"
|
||||||
@@ -693,6 +691,8 @@ function SessionsSidebar() {
|
|||||||
>
|
>
|
||||||
Delete…
|
Delete…
|
||||||
</button>
|
</button>
|
||||||
|
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
|
||||||
|
<CoverageMenuBlock chatId={ctxChatId} />
|
||||||
</MenuShell>
|
</MenuShell>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,19 @@
|
|||||||
import { DEFAULT_THINKING_ORDER } from "@/src/core";
|
import { DEFAULT_THINKING_ORDER } from "@/src/core";
|
||||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-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> = {
|
export const THINKING_LABEL: Record<string, string> = {
|
||||||
off: "Off",
|
off: "off",
|
||||||
minimal: "Minimal",
|
minimal: "minimal",
|
||||||
low: "Low",
|
low: "low",
|
||||||
medium: "Medium",
|
medium: "medium",
|
||||||
high: "High",
|
high: "high",
|
||||||
xhigh: "Xhigh",
|
xhigh: "xhigh",
|
||||||
max: "Max",
|
max: "max",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The level a fresh pick of `supported` populates with. Empty support
|
/** The level a fresh pick of `supported` populates with. Empty support
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ describe("render smoke tests", () => {
|
|||||||
assert.ok(html.includes('role="group"'), "checkbox group");
|
assert.ok(html.includes('role="group"'), "checkbox group");
|
||||||
assert.ok((html.match(/role="checkbox"/g) ?? []).length >= 3, "options AND the Other row are checkboxes");
|
assert.ok((html.match(/role="checkbox"/g) ?? []).length >= 3, "options AND the Other row are checkboxes");
|
||||||
assert.ok(html.includes('data-testid="other-option"'), "Other is still an option row in multi-select");
|
assert.ok(html.includes('data-testid="other-option"'), "Other is still an option row in multi-select");
|
||||||
|
// The bigger \u2713 glyph (text-base, 2026-09-24 owner round) only
|
||||||
|
// mounts on a CHECKED row — unreachable from the unselected static
|
||||||
|
// render, so it's live-verified (Playwright: 14px computed in the
|
||||||
|
// 16px box) instead of pinned here.
|
||||||
});
|
});
|
||||||
|
|
||||||
it("a malformed question input renders the quiet fallback, never a crash", () => {
|
it("a malformed question input renders the quiet fallback, never a crash", () => {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe("CoverageMenuBody — the context-menu block", () => {
|
|||||||
<CoverageMenuBody coverage={coverage()} extracting={false} onExtract={() => {}} />,
|
<CoverageMenuBody coverage={coverage()} extracting={false} onExtract={() => {}} />,
|
||||||
);
|
);
|
||||||
assert.ok(html.includes('data-testid="ctx-coverage"'), "the block carries its testid");
|
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.match(html, /5 messages unextracted/);
|
||||||
assert.ok(html.includes('data-testid="ctx-extract"'), "the Extract affordance");
|
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)");
|
assert.ok(!html.includes('disabled=""'), "an uncovered session can extract (the disabled: utilities alone don't count)");
|
||||||
|
|||||||
Reference in New Issue
Block a user