Let the console search, browse, forget and mount
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.
This commit is contained in:
1 parent
a5008e1476
commit
9f91ff7ed7
27 files changed
+2146
-616
No files matched your search
@@ -0,0 +1,133 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user