minimal home: the buttons carve out of the drag band; own-line summary (release polish 2.3)
Owner report (Electron): the Chains and Import-notes buttons were not
clickable — clicking dragged the window. Root cause: the chrome column
lacked lh-no-drag. The drag mask resolves by DOM order, and the graph
chrome's column carries the carve exactly for this reason (the
2026-09-17 mask law). The minimal home's column now carries the same
class on the same shape — byte-for-byte the graph chrome's law — and
the pointer-events juggling is gone (it was browser-thinking; Electron
needs the region carve, not the pointer filter).
Summary refinement (owner, same round): every count on its OWN line —
no dot separators; the two blocks (propositions/chains, teaching/
practice sessions) part with one empty line (a dedicated h-4 spacer);
imported notes render as a COUNT ("4 imported notes"), never the file
list. KnowledgeSummary's contract changes sources: string[] →
importedNotes: number.
1419 tests (own-line pins: no " · " anywhere, h-4 parting, count-not-
paths, singulars); tsc, lint, build green.
This commit is contained in:
+39
-31
@@ -33,39 +33,47 @@ export function KnowledgeSummary({
|
|||||||
chains,
|
chains,
|
||||||
teachSessions,
|
teachSessions,
|
||||||
practiceSessions,
|
practiceSessions,
|
||||||
sources,
|
importedNotes,
|
||||||
}: {
|
}: {
|
||||||
propositions: number;
|
propositions: number;
|
||||||
chains: number;
|
chains: number;
|
||||||
teachSessions: number;
|
teachSessions: number;
|
||||||
practiceSessions: number;
|
practiceSessions: number;
|
||||||
sources: string[];
|
importedNotes: number;
|
||||||
}) {
|
}) {
|
||||||
const empty = propositions === 0 && chains === 0 && teachSessions === 0 && practiceSessions === 0 && sources.length === 0;
|
const empty =
|
||||||
|
propositions === 0 && chains === 0 && teachSessions === 0 && practiceSessions === 0 && importedNotes === 0;
|
||||||
return (
|
return (
|
||||||
<div data-testid="knowledge-summary" className="flex max-w-[36rem] flex-col items-center gap-3 text-center">
|
<div data-testid="knowledge-summary" className="flex flex-col items-center text-center">
|
||||||
|
{/* Own-line law (owner, 2026-10-06): every count sits on its own
|
||||||
|
row — no dot separators; the two blocks (graph, sessions)
|
||||||
|
part with one empty line; the notes COUNT (never the file
|
||||||
|
list) rides after the same parting. */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="font-ui text-sm text-muted-foreground">
|
||||||
|
{propositions} {propositions === 1 ? "proposition" : "propositions"}
|
||||||
|
</p>
|
||||||
|
<p className="font-ui text-sm text-muted-foreground">
|
||||||
|
{chains} {chains === 1 ? "chain" : "chains"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-4" aria-hidden="true" />
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="font-ui text-sm text-muted-foreground">
|
||||||
|
{teachSessions} {teachSessions === 1 ? "teaching session" : "teaching sessions"}
|
||||||
|
</p>
|
||||||
|
<p className="font-ui text-sm text-muted-foreground">
|
||||||
|
{practiceSessions} {practiceSessions === 1 ? "practice session" : "practice sessions"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="h-4" aria-hidden="true" />
|
||||||
<p className="font-ui text-sm text-muted-foreground">
|
<p className="font-ui text-sm text-muted-foreground">
|
||||||
{propositions} {propositions === 1 ? "proposition" : "propositions"} · {chains}{" "}
|
{importedNotes} {importedNotes === 1 ? "imported note" : "imported notes"}
|
||||||
{chains === 1 ? "chain" : "chains"}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="font-ui text-sm text-muted-foreground">
|
{empty && (
|
||||||
{teachSessions} {teachSessions === 1 ? "teaching session" : "teaching sessions"} · {practiceSessions}{" "}
|
|
||||||
{practiceSessions === 1 ? "practice session" : "practice sessions"}
|
|
||||||
</p>
|
|
||||||
{empty ? (
|
|
||||||
<p className="pt-2 font-ui text-xs text-muted-foreground/70">
|
<p className="pt-2 font-ui text-xs text-muted-foreground/70">
|
||||||
Nothing here yet — import notes to begin building the graph.
|
Nothing here yet — import notes to begin building the graph.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
|
||||||
sources.length > 0 && (
|
|
||||||
<div className="flex w-full flex-col items-center gap-0.5 pt-3">
|
|
||||||
{sources.map((path) => (
|
|
||||||
<p key={path} className="w-full truncate font-ui text-xs text-muted-foreground/70" title={path}>
|
|
||||||
{path}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -159,17 +167,21 @@ export function MinimalHome({
|
|||||||
chains={snapshot?.chains.length ?? 0}
|
chains={snapshot?.chains.length ?? 0}
|
||||||
teachSessions={teachSessions}
|
teachSessions={teachSessions}
|
||||||
practiceSessions={practiceSessions}
|
practiceSessions={practiceSessions}
|
||||||
sources={Object.values(snapshot?.fileSources ?? {})}
|
importedNotes={Object.keys(snapshot?.fileSources ?? {}).length}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The chrome survivors in the graph view's own places (owner,
|
{/* The chrome survivors in the graph view's own places (owner,
|
||||||
2026-10-06): the top-right column — actions row (Chains +
|
2026-10-06): the top-right column — actions row (Chains +
|
||||||
Import, the same 36px band pills), the cost pill BELOW it.
|
Import, the same 36px band pills), the cost pill BELOW it.
|
||||||
Node search and the render-settings gear are canvas things
|
The column carries lh-no-drag EXACTLY like the graph
|
||||||
and stay gone; Import ALWAYS renders (the entry-point law). */}
|
chrome's (the DOM-order mask law): without it the drag band
|
||||||
<div className="pointer-events-none absolute right-2 top-2 z-10 flex max-w-[calc(100%-5.5rem)] flex-col items-end gap-2">
|
swallows the clicks in Electron — the buttons dragged the
|
||||||
<div className="pointer-events-auto flex min-w-0 items-start gap-2">
|
window instead. Node search and the render-settings gear are
|
||||||
|
canvas things and stay gone; Import ALWAYS renders (the
|
||||||
|
entry law). */}
|
||||||
|
<div className="lh-no-drag absolute right-2 top-2 z-10 flex max-w-[calc(100%-5.5rem)] flex-col items-end gap-2">
|
||||||
|
<div className="flex min-w-0 items-start gap-2">
|
||||||
<div className="graph-chrome-fade-in shrink-0 rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
|
<div className="graph-chrome-fade-in shrink-0 rounded-xl border bg-card/85 px-1 py-[3px] shadow-lg backdrop-blur">
|
||||||
<Hint label="Every reasoning chain" side="bottom">
|
<Hint label="Every reasoning chain" side="bottom">
|
||||||
<button
|
<button
|
||||||
@@ -198,11 +210,7 @@ export function MinimalHome({
|
|||||||
</Hint>
|
</Hint>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{costs && costs.runs.length > 0 && (
|
{costs && costs.runs.length > 0 && <GraphCostControl costs={costs} />}
|
||||||
<div className="pointer-events-auto">
|
|
||||||
<GraphCostControl costs={costs} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The chain table — the STANDARD overlay, unchanged, behind its
|
{/* The chain table — the STANDARD overlay, unchanged, behind its
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/** The minimal home's KnowledgeSummary — the quiet centered summary of
|
/** The minimal home's KnowledgeSummary — the quiet centered summary of
|
||||||
* the knowledge base (owner, 2026-10-06: "very quiet, very minimal").
|
* the knowledge base (owner, 2026-10-06: "very quiet, very minimal").
|
||||||
* Pure body: props in, markup out. */
|
* Pure body: props in, markup out. Own-line law (same day refinement):
|
||||||
|
* every count on its own row, NO dot separators, blocks parted by an
|
||||||
|
* empty line, imported notes as a COUNT — never the file list. */
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -14,40 +16,60 @@ function render(props: Partial<Parameters<typeof KnowledgeSummary>[0]> = {}) {
|
|||||||
chains: 5,
|
chains: 5,
|
||||||
teachSessions: 3,
|
teachSessions: 3,
|
||||||
practiceSessions: 2,
|
practiceSessions: 2,
|
||||||
sources: [],
|
importedNotes: 4,
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("KnowledgeSummary — the quiet home summary (2026-10-06)", () => {
|
describe("KnowledgeSummary — the quiet home summary (2026-10-06)", () => {
|
||||||
it("renders the knowledge base in numbers: propositions · chains, teaching · practice sessions", () => {
|
it("every count on its OWN line — no dot separators (owner refinement, same day)", () => {
|
||||||
const html = render();
|
const html = render();
|
||||||
assert.ok(html.includes("12 propositions"), "propositions count");
|
for (const line of ["12 propositions", "5 chains", "3 teaching sessions", "2 practice sessions", "4 imported notes"]) {
|
||||||
assert.ok(html.includes("5 chains"), "chains count");
|
const i = html.indexOf(line);
|
||||||
assert.ok(html.includes("3 teaching sessions"), "teaching count (plural)");
|
assert.ok(i !== -1, `${line} renders`);
|
||||||
assert.ok(html.includes("2 practice sessions"), "practice count (plural)");
|
// own-line: the count is its own text node — the char after the
|
||||||
assert.ok(!html.includes("import notes"), "no empty-state hint when there is content");
|
// label is a tag boundary (</p>), never " · "
|
||||||
|
const after = html.slice(i + line.length, i + line.length + 2);
|
||||||
|
assert.ok(after.startsWith("<"), `${line} ends its own element, not a dot-joined run`);
|
||||||
|
}
|
||||||
|
assert.ok(!html.includes(" · "), "no dot separators anywhere");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the two blocks part with an empty line — graph counts, then session counts", () => {
|
||||||
|
const html = render();
|
||||||
|
const graphBlock = html.indexOf("12 propositions");
|
||||||
|
const sessionsBlock = html.indexOf("3 teaching sessions");
|
||||||
|
assert.ok(graphBlock !== -1 && sessionsBlock !== -1);
|
||||||
|
assert.ok(sessionsBlock > graphBlock, "sessions come after the graph block");
|
||||||
|
// the parting is a dedicated spacer element (h-4), not a paragraph
|
||||||
|
assert.ok(html.includes('class="h-4"'), "one empty line between blocks");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("singularizes: one of each reads as single, not '1 sessions'", () => {
|
it("singularizes: one of each reads as single, not '1 sessions'", () => {
|
||||||
const html = render({ propositions: 1, chains: 1, teachSessions: 1, practiceSessions: 1 });
|
const html = render({ propositions: 1, chains: 1, teachSessions: 1, practiceSessions: 1, importedNotes: 1 });
|
||||||
assert.ok(html.includes("1 proposition ·"));
|
assert.ok(html.includes("1 proposition"));
|
||||||
assert.ok(html.includes("1 chain"));
|
assert.ok(html.includes("1 chain"));
|
||||||
assert.ok(html.includes("1 teaching session"));
|
assert.ok(html.includes("1 teaching session"));
|
||||||
assert.ok(html.includes("1 practice session"));
|
assert.ok(html.includes("1 practice session"));
|
||||||
|
assert.ok(html.includes("1 imported note"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lists the imported sources — quiet truncated rows (owner: 'a list of sources that have been imported')", () => {
|
it("imported notes are a COUNT — never the file list (owner: 'the number of imported notes')", () => {
|
||||||
const html = render({ sources: ["/notes/databases.md", "/notes/laravel.md"] });
|
const html = render({ importedNotes: 7 });
|
||||||
assert.ok(html.includes("/notes/databases.md"));
|
assert.ok(html.includes("7 imported notes"));
|
||||||
assert.ok(html.includes("/notes/laravel.md"));
|
assert.ok(!html.includes(".md"), "no file paths render");
|
||||||
assert.ok(html.includes('class="w-full truncate'), "each row truncates — long paths never blow the column");
|
assert.ok(!html.includes("truncate"), "no truncating source rows");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("the empty knowledge base says so, quietly — import is the entry point", () => {
|
it("the empty knowledge base says so, quietly — import is the entry point", () => {
|
||||||
const html = render({ propositions: 0, chains: 0, teachSessions: 0, practiceSessions: 0 });
|
const html = render({ propositions: 0, chains: 0, teachSessions: 0, practiceSessions: 0, importedNotes: 0 });
|
||||||
assert.ok(html.includes("Nothing here yet"));
|
assert.ok(html.includes("Nothing here yet"));
|
||||||
assert.ok(html.includes("import notes"), "the hint names the entry point");
|
assert.ok(html.includes("import notes"), "the hint names the entry point");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("non-empty shows no empty-state hint", () => {
|
||||||
|
const html = render();
|
||||||
|
assert.ok(!html.includes("Nothing here yet"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user