Files
ely-eye/frontend/src/components/CartridgePanel.tsx
T
ZacharyZhang-NY 9f91ff7ed7 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.
2026-10-08 05:13:38 -04:00

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>
);
}