The store had a write path, a mount path and a search path, and the console
still showed a dashboard of counters. Nothing in the UI could ask what someone
knows, page through atoms, look at a wordless picture, forget one memory, or
mount a cartridge — so the mechanisms built in P0 were invisible.
Backend:
- senses.py: one function decides what this machine can perceive. /api/status
and doctor each computed it before, which is two truths. Picture atoms are now
counted by image_ref rather than by modality, so a screenshot crop from a PDF
counts and a picture whose bytes were deleted stops claiming it can be encoded.
- browse.py: GET /api/atoms pages newest-first through v_atoms_current with
modality/holder/source filters; GET /api/atoms/{id}/image serves the stored
bytes so a picture with no OCR text can be looked at.
- Deleted 8 HTTP routes nobody calls (4 benchmark, research modules, dense index,
visual-contradiction-proof, memory-map). Their functions stay: the CLI and the
proof checks use them. The surface is 30 routes, pinned as a set by a test so a
new unused route fails the suite.
Frontend: App.tsx (517 lines) became a 131-line shell plus seven panels that own
their own fetches, and lib/format.ts. types.ts now carries the memory dimensions
the backend has been sending since schema v2.
Two invented fields were rendering lies: AdapterPanel showed gpu_available and
probe_status, which AdapterLoadReport does not have — every machine looked GPU-less
including this one. The panel now shows only what the server says.
Proof: scripts/ui_smoke.sh starts the API on a fresh temp home, serves the built
UI from the same origin, and drives five interactions in a real browser — search
as somebody in a scenario, page and view a wordless picture, forget, mount, read
adapter states with reasons. It prints the mount refusal verbatim rather than
pretending the mount succeeded.
Gates: 263 pytest passed; ruff and mypy clean on the 8 touched backend files
(repo-wide mypy unchanged at 24 errors / 6 files); pyright 0 errors there;
go build/vet/test/gofmt clean; npm build + eslint clean; ui_smoke and
mcp_write_smoke both PASS.
134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { Archive, Play, Square, SwitchCamera } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import { getMounts, listCartridges, mountCartridge, switchCartridge, unmountCartridge } from '../api';
|
|
import type { CartridgeManifest, MountState } from '../types';
|
|
import { errorMessage, formatNumber, shortHash } from '../lib/format';
|
|
import { Panel, Quiet } from './ui';
|
|
|
|
export function CartridgePanel({ onChanged }: { onChanged: () => void }) {
|
|
const [cartridges, setCartridges] = useState<CartridgeManifest[]>([]);
|
|
const [mounts, setMounts] = useState<MountState | null>(null);
|
|
const [busy, setBusy] = useState<string | null>(null);
|
|
const [notice, setNotice] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function load() {
|
|
try {
|
|
const [nextCartridges, nextMounts] = await Promise.all([listCartridges(), getMounts()]);
|
|
setCartridges(nextCartridges);
|
|
setMounts(nextMounts);
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
async function loadInitial() {
|
|
try {
|
|
const [nextCartridges, nextMounts] = await Promise.all([listCartridges(), getMounts()]);
|
|
if (!cancelled) {
|
|
setCartridges(nextCartridges);
|
|
setMounts(nextMounts);
|
|
}
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
setError(errorMessage(err));
|
|
}
|
|
}
|
|
}
|
|
void loadInitial();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
async function act(cartridgeId: string, verb: 'mount' | 'switch' | 'unmount') {
|
|
setBusy(`${verb}-${cartridgeId}`);
|
|
setError(null);
|
|
try {
|
|
if (verb === 'mount') {
|
|
await mountCartridge(cartridgeId);
|
|
} else if (verb === 'switch') {
|
|
await switchCartridge(cartridgeId);
|
|
} else {
|
|
await unmountCartridge(cartridgeId);
|
|
}
|
|
setNotice(`${verb} ${cartridgeId}`);
|
|
await load();
|
|
onChanged();
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Panel title="Cartridges" icon={<Archive size={17} />}>
|
|
<div className="metric-strip">
|
|
<span>{mounts?.mounted.length ?? 0} mounted</span>
|
|
<span>{mounts?.scoped ? 'search is scoped' : 'search sees the whole store'}</span>
|
|
</div>
|
|
<div className="adapter-list">
|
|
{cartridges.map((cartridge) => {
|
|
const mounted = mounts?.mounted.includes(cartridge.cartridge_id) ?? false;
|
|
return (
|
|
<article className="adapter-row" key={cartridge.cartridge_id}>
|
|
<header>
|
|
<span>{cartridge.name}</span>
|
|
{mounted && <span className="proof-pill passed">mounted</span>}
|
|
</header>
|
|
<strong>{cartridge.cartridge_id}</strong>
|
|
<div className="adapter-metrics">
|
|
<small>{formatNumber(cartridge.atom_count)} atoms</small>
|
|
<small>{formatNumber(cartridge.source_count)} sources</small>
|
|
<small>{formatNumber(cartridge.token_equivalent)} token eq</small>
|
|
<small>{cartridge.model_base}</small>
|
|
<small title={cartridge.dna ?? undefined}>
|
|
{cartridge.dna ? `dna ${shortHash(cartridge.dna)}` : 'dna pending'}
|
|
</small>
|
|
<small>
|
|
{cartridge.kv_fingerprint
|
|
? 'kv fingerprint pinned'
|
|
: 'no kv fingerprint: mounting must re-check this machine'}
|
|
</small>
|
|
</div>
|
|
<div className="button-row">
|
|
<button
|
|
className="action-button secondary"
|
|
onClick={() => act(cartridge.cartridge_id, 'mount')}
|
|
disabled={busy !== null || mounted}
|
|
>
|
|
<Play size={15} />
|
|
Mount
|
|
</button>
|
|
<button
|
|
className="action-button secondary"
|
|
onClick={() => act(cartridge.cartridge_id, 'switch')}
|
|
disabled={busy !== null}
|
|
>
|
|
<SwitchCamera size={15} />
|
|
Switch
|
|
</button>
|
|
<button
|
|
className="action-button secondary"
|
|
onClick={() => act(cartridge.cartridge_id, 'unmount')}
|
|
disabled={busy !== null || !mounted}
|
|
>
|
|
<Square size={15} />
|
|
Unmount
|
|
</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
{cartridges.length === 0 && <Quiet>No cartridge has been finalized yet.</Quiet>}
|
|
</div>
|
|
{notice && <div className="metric-strip"><span>{notice}</span></div>}
|
|
{error && <div className="error-line">{error}</div>}
|
|
</Panel>
|
|
);
|
|
}
|