coverage meter one-truth fix + chunk-budget setting UI: fetchGraphRead resolves through the shared core resolver (the stale 4096 fallback filled the ring at half the 8192 truth); Graph section gains the extraction chunk budget row (floor 500, empty = factory) and the master switch composes instead of replacing the section

This commit is contained in:
amosblomqvist
2026-10-02 21:18:51 +03:00
parent 2ef09b0282
commit 76387b043b
4 changed files with 152 additions and 30 deletions
+13 -11
View File
@@ -7,6 +7,7 @@ import { useSettingsSaved } from "@/components/settings-signal";
import { formatTokens } from "@/components/usage-format";
import { Hint } from "@/components/ui/hint";
import { timeAgo } from "@/components/sidebar";
import { resolveSettings } from "@/src/core";
/**
* The extraction-coverage surfaces (extraction-UX redesign, owner-locked
@@ -53,25 +54,26 @@ async function fetchCoverage(chatId: string): Promise<SessionCoverage | null> {
}
}
/** The ring's denominator + visibility: the graph section of the settings
* record (both fields optional — absent means enabled/4096, the factory
* resolution; see GraphSettings). Null on fetch failure → no ring. */
/** The ring's denominator + visibility, resolved through the SAME core
* resolver the server's effective settings use (fix, 2026-10-02): the
* meter denominates at exactly the number the extraction trigger waits
* for — the old hand-rolled `?? 4096` fallback drifted from the factory
* default (8192) and the ring filled at half the truth. Null on fetch
* failure → no ring. */
export interface GraphRead {
enabled: boolean;
chunkTokens: number;
}
async function fetchGraphRead(): Promise<GraphRead | null> {
/** Exported for the render pin (the ringFill precedent): resolves the
* raw settings delta to the factory-true graph section. */
export async function fetchGraphRead(): Promise<GraphRead | null> {
try {
const res = await fetch("/api/settings");
if (!res.ok) return null;
const body = (await res.json()) as { settings?: { graph?: { enabled?: boolean; extractionChunkTokens?: number } } };
const graph = body.settings?.graph;
if (!graph) return null;
return {
enabled: graph.enabled !== false,
chunkTokens: graph.extractionChunkTokens ?? 4096,
};
const body = (await res.json()) as { settings?: Parameters<typeof resolveSettings>[0] };
const graph = resolveSettings(body.settings ?? null).graph;
return { enabled: graph.enabled, chunkTokens: graph.extractionChunkTokens };
} catch {
return null;
}
+61 -13
View File
@@ -31,7 +31,12 @@
*/
import { useEffect, useRef, useState } from "react";
import type { AppearanceSettings, GraphSettings } from "@/src/core";
import {
EXTRACTION_CHUNK_TOKENS_FLOOR,
resolveSettings,
type AppearanceSettings,
type GraphSettings,
} from "@/src/core";
import { MenuShell } from "@/components/menu-shell";
import { Hint } from "@/components/ui/hint";
import { useAppearance } from "./appearance.tsx";
@@ -346,18 +351,61 @@ export function GraphSection({
onSectionChange: (next: GraphSettings) => void;
}) {
// Wave 7 collapse (owner ruling 2026-09-23): the section is the MASTER
// SWITCH ONLY. The embedding/clustering/labeling rungs and the BYOK
// embedding-model picker are gone — clustering is the free birth-session
// partition derived on read, labels ARE session names, and the prober's
// graph tools read the ledgers directly. There is nothing left to
// configure but on/off.
// SWITCH — the embedding/clustering/labeling rungs are gone; clustering
// is the free birth-session partition derived on read, labels ARE session
// names. The chunk budget (2026-10-02) joined it as the section's second
// and only other control: it denominates the coverage ring AND gates the
// auto-extraction trigger (turn settle ≥ budget fires the run).
//
// Resolved through the SAME core resolver the server's effective
// settings use (one source of truth, the 4096-fallback fix 2026-10-02):
// the hint and placeholder report exactly the number the trigger waits
// for — never a second hand-rolled default.
const resolved = resolveSettings(graph === undefined ? null : { version: 1, graph }).graph;
return (
<SettingsRow label="Graph" testid="graph-row-enabled">
<Toggle
checked={graph?.enabled ?? true}
label="graph enabled"
onChange={(on) => onSectionChange({ enabled: on })}
/>
</SettingsRow>
<>
<SettingsRow label="Graph" testid="graph-row-enabled">
<Toggle
checked={graph?.enabled ?? true}
label="graph enabled"
onChange={(on) =>
// Compose, never replace-whole (the section PUT is, but the
// row must not drop its sibling field — fixed with the budget
// row's arrival: the old { enabled: on } wiped a stored budget).
onSectionChange({ ...graph, enabled: on })
}
/>
</SettingsRow>
<SettingsRow
label="Extraction chunk budget"
hint={`${resolved.extractionChunkTokens} tok`}
testid="graph-row-chunk-tokens"
>
<CommitInput
type="number"
value={String(graph?.extractionChunkTokens ?? "")}
placeholder={String(resolved.extractionChunkTokens)}
ariaLabel="extraction chunk budget"
testid="input-chunk-tokens"
onCommit={(v) => {
// Empty = factory default (the fix-wave discipline): absence is
// the unset for numbers — Number("") is 0, so clamping would
// write the floor instead of unsetting.
if (v === "") {
onSectionChange({ ...graph, extractionChunkTokens: undefined });
return;
}
const n = Number(v);
if (!Number.isFinite(n) || n <= 0) return; // bad input: no write
// Mirror the server sanitize exactly: floor only (cost
// protection), no ceiling — a big budget is the user's call.
onSectionChange({
...graph,
extractionChunkTokens: Math.max(EXTRACTION_CHUNK_TOKENS_FLOOR, Math.round(n)),
});
}}
/>
</SettingsRow>
</>
);
}