VisualLLM is a visual fallback router: arrange models into lanes, each lane is a local OpenAI-compatible endpoint, the rightmost model answers first and everything to its left is a fallback.
This file replaces the old milestone roadmap. It is a working plan: items are checked off as they land. Each workstream lists its goal ("done when") up front, then the concrete tasks.
- Routing should be visible. A user should be able to understand why a model was chosen and what happens next when it fails.
- The simple path stays simple. Adding a provider, creating a lane, and connecting a client should not require knowledge of YAML, Rust, or HTTP internals.
- Fallback must be honest. A response is not successful merely because an upstream returned HTTP 200. Evidence, passed-over members, and the serving model should remain inspectable.
- Local means local. The webview should not receive provider secrets, and the engine should bind to loopback unless the user explicitly chooses otherwise.
- Compatibility matters. Existing lanes and OpenAI-compatible clients should keep working as the application evolves.
The active v0.6.0 installation and distribution workstream is tracked in
docs/INSTALLATION_PLAN-v0.6.0.md. That
document contains the ordered checklist, acceptance evidence, certificate
constraints, and handoff for the next session.
Done when: normal, by-design behavior (capability skips) never produces a toast or bell badge, and every alert that fires is something worth acting on.
- Don't record
skipped_by_catalogas an incident. Capability skips now log and appear in the trail header only; legacy records render silently (never toast/badge). The missingstalleddiagnosis was added so dead connections explain themselves. - Keep skips visible on the lane, not in the notification center: the per-lane activity line carries the live "passed over" story.
- Verify no other incident kind fires during normal operation. Manual runtime checks confirmed the bell stays silent across mixed traffic.
Done when: testing a lane and looking at a lane both tell you which model answered and what was passed over, using data the engine already returns.
- Lane test shows served-by + trail. The toast now reads
answered by <model>with the trail as detail on success. - Lane test exercises the commit gate. The probe budget is 64, above
the
budget < 16bypass, so Test measures the gate's verdict. - Per-lane trail view. The lane activity line opens the notification center scoped to that lane, with a "show all" clear chip.
Done when: while a client request is in flight, the lane shows which member is being tried and, on completion, which member answered and how many were passed over — without waiting for a failure.
- Engine appends one JSON line per request phase to
activity.jsonl: timestamp, lane, member, phase (trying/answered/failed/exhausted), detail — exposed overGET /activityand theactivity_readcommand. - Renderer tails it incrementally (high-water mark) and shows a live pill
on the lane: pulsing dot while trying,
answered by <model> · N passed over, red on failure. States decay on a one-second clock. - The activity file is scrubbed to one line, capped, and trimmed by size.
Done when: no single click or drag can permanently destroy a lane or a tuned member.
- Undo for destructive actions. Lane delete, chip removal, and
drag-out-of-lane all toast
X removed — undo?with a ~5s window restoring the previousstate.lanes(already snapshotted on everylanes_write). - Right-click context menu on track chips: settings / disable / remove. Surfaces the gear and the drag-out-to-remove gesture, both previously invisible.
- Per-member disable (skip at request time, keep position and dials)
for tuning lanes without delete-and-redrag. Engine-side: a
disabledflag onMemberthatchat()skips like a capability miss.
Done when: a transient provider outage never silently degrades every lane's capability checks, and the user is told when it happens.
- Catalog cache is never shrunk by a partial fetch.
catalog_readkeeps the previous good catalog when a partial fetch would produce a strictly smaller set; logs when stale data is retained. - Catalog errors surface as a notification (" catalog failed — using last good cache"), not just a red count in the provider list. A stale cache with no active provider errors also toasts once, so a user who clears the alert still knows the engine is running from retained data.
- Engine logs one line when serving from a stale cache.
Done when: clicks never fall through to windows below, and the window stacks normally on X11, Wayland, and when launched as a child of VS Code Insiders — verified by manual test on each.
- Input shape is re-applied on resize. The
input_shape_combine_regioncall inmain.rsnow runs onsize-allocate, not only at realize time, so the input region tracks window resizes. - Stable VS Code is detected.
vscode_chat_models_pathnow checksCode/User/chatLanguageModels.jsonas well asCode - Insiders. - Launch paths documented.
tools/launch-system.shis the supported launcher; it runs the binary with a clean environment outside Snap/VSC. - Verify transparent window z-order on your hardware. Confirmed on the user's session: clicks land on VisualLLM when stacked over VS Code.
Done when: a user can download one file and run VisualLLM on a clean supported Linux install without installing WebKitGTK separately.
- Confirm the built binary embeds the renderer (it does —
frontendDist) and needs no repo files at runtime. - Verify the AppImage bundles WebKitGTK (confirmed by extracting the
AppImage:
libwebkit2gtk-4.1.so.0,libjavascriptcoregtk-4.1.so.0, andlibgtk-3.so.0are present). Runtime verification on a clean VM still needed before tagging. - Decide: is the AppImage the canonical "single binary"? Yes for
portable use. The AppImage is the recommended download for most Linux
users; the
.debremains available for apt-based installs. Documented in README release checklist. - Add a release checklist entry: run the AppImage on a clean VM before tagging.
Done when: the VS Code button works for both VS Code and VS Code Insiders users, or says clearly why it can't.
-
vscode_chat_models_path(main.rs) detects both stableCodeandCode - Insiders, preferring the one that exists. - On success the toast tells the user to reload the editor window to see the model.
Done when: the interactions below are discoverable without reading source comments.
- Track scroll affordance: thin custom scrollbar + left fade on
.trackwhen chips overflow. - Member popover placeholders show the effective value when known
(
client: 0.7/provider default) instead of bare—. - Keyboard shortcuts:
Ctrl+Nnew lane,Ctrl+Bbrowse,Ctrl+,settings,?shortcut help overlay. - First-lane completion state: "Your endpoint is live at
http://127.0.0.1:PORT/lane/<slug>/v1" with the VS Code setup inline. - Dead members: lane footer shows "N members will be skipped at request time" instead of relying on chip hover.
- Notification center: filter by lane; per-(lane, kind) mute instead of global kind mute.
- Sidebar (300) and browse (150) caps gain a "show all" / render-on-scroll instead of only "narrow the search".
Done when: configuration moves between machines without moving secrets, the packaged app is verified on a clean install, and the project is ready for public users.
- Export/import (backup/restore) of lanes, pool, providers — excluding API keys (they stay in the keychain; re-entered on the new machine).
- Manual startup verification: clean start, duplicate launch, restart,
shutdown, documented in README release checklist. Release binary verified:
second instance exits quickly (single-instance plugin works), but one run
printed
free(): corrupted unsorted chunkson kill — needs investigation before tagging. - Publish tested
.deb+ AppImage; verify desktop-menu launch on a clean install. - Screenshots or a short add-provider → drag-models → connect-client demo.
- Define the first public release version and support policy; separate beginner documentation from implementation history. See README "Version policy" and this roadmap's release criteria.
- Add issue labels and a small triage process; invite users to test
provider setup, lane creation, and fallback behavior. See
CONTRIBUTING.md.
Done when: the confirmed review findings are fixed, the renderer's core logic is under test, and version drift fails CI instead of shipping.
- Loopback-only engine, no token. The gateway previously required
Authorization: Bearer <token>on lane routes (random 32-byte secret, persisted next toport.json, chmod 600) with a reveal/copy/regenerate settings row andhttpHeadersin editor integrations. It still did not reliably let IDE clients call lanes, so the token requirement was removed end-to-end (v0.5.2): the engine stays bound to loopback, and lane routes, editor integrations, and copied examples work without a credential. - Keyring failure degrades, never blocks. A provider saves even when the OS keychain is unavailable — keys are held in memory for the session and the UI says so instead of hard-failing "add provider".
- Dead assets removed. The unused
renderer/style.css(old neumorphic skin) and therenderer-backup-original/directory no longer ship; onlyegl.cssis loaded and inlined. - Renderer logic is unit-tested.
npm testrunstools/renderer.test.js(node:test) over the pure scoring/sort/price/drag functions — percentile scoring with ties and nulls, data-driven columns, browse filters, price/free handling, display reversal — on top of the existing smoke test. - Version is a single source of truth.
tools/check-version.jsfails on any mismatch between Cargo.toml, tauri.conf.json, and package.json; wired into CI and the release tag pre-flight. - macOS auto-update not pretended. Documented in
docs/archive/RELEASE_ROADMAP-v0.x.md§7: macOS distribution deferred (needs Apple Developer ID + notarization); Linux + Windows remain the supported update matrix. - Frontend hygiene. The dead theme toggle is gone; the active skin is dark-first by construction, no half-baked light path.
Done when: a lane that keeps failing the same transient way parks itself, answers 503 until a human unparks it, and the parking is visible and reversible on the canvas.
- Per-lane failure budget.
lanes::Lane.budget(failures/window_secs, default 5/600) plusbudget_hitstimestamps;over_budget()is a pure sliding-window decision, unit-tested. - Engine parks the lane. Every budgetable failure (transient,
provider-side kinds only —
provider_trouble,unreachable,stalled,rate_limited) is recorded and counted; crossing the budget setsparked+parked_afterand writes anauto_parkedincident with the receipt. Behavioural failures never park a lane. - Parked lanes refuse work. A parked lane answers
503witherror.type = lane_parkedbefore any member is contacted; the engine's budget bookkeeping survives restarts. - Unpark resets the budget.
lane_unparkcommand →lanes::unparkclears the flag and the history. Header shows an amber "Parked — resume" button; the notification center'sauto_parkedcard carries an unpark action. - Visible and reversible.
DIAGNOSIS.auto_parkedexplains the parking with the engine's receipt; the incident notifies like any other kind.
Done when: a failed request leaves a structured snapshot behind, and the notification center can retry it — confirmation-gated, without the captured request ever crossing to the webview.
- Incidents carry a replayable snapshot.
incidents.rsrecords an optionalreplay(method/path/body) alongside every failure, plus a stableidassigned on record. The body is capped at 32 KiB; an oversized request is still an incident, just not a replayable one. - The webview never sees the request.
incidents_readreturns anIncidentView— every field the renderer reads plus areplayableflag; the captured body stays on the engine's disk. This view also maps the disk'slaneto the canvas'shall, fixing per-hall filtering that had silently read an undefined field. - Replay runs server-side through the lane.
lane_replay(main.rs) looks the incident up by id, re-POSTs the captured body through the lane's own endpoint, and reports status / served-by / trail. A replayed failure is recorded as a fresh incident like any other. - Confirmation-gated in the UI. The Replay action is two-step: first click arms ("it can spend money"), second fires; the armed id survives re-renders and the button disables while in flight.
Done when: one click duplicates a lane — the same members (order, dials and park state), criteria, toggles and budget — under a fresh slug and name, beside the original, without pulling in editor integration or live park state.
- Duplicate button in the lane footer. A labeled pill beside Integrate
(
Duplicate), one click per lane, sharing the footer's management actions rather than crowding the header's icon row. - A clone is a definition, not a state.
cloneLaneShapecarries over the whole lane definition (members and their params/criteria as deep copies,suppress_reasoning,unstick, budget config) and deliberately omitsintegrated_editors,parkedandbudget_hits— integrating a lane into an editor stays an explicit act, and the clone is a place to try a fix. - Fresh slug, beside the original. Unique-suffix generation
(
hallway-copy,hallway-copy-2, …) matchesnewLane; the clone is spliced right after its source with the same undo/toast as the other destructive edits. - The carry-over contract is tested. Four
cloneLaneShapecases pin order/dials preservation, deep-copy isolation, slug uniqueness, and the no-integration/no-park guarantees.
Done when: each lane shows how much it has actually moved — requests and failures inside the trailing 24h and 7d — as a read-only line the engine owns, so no UI edit can ever reset it.
- The engine keeps the ledger.
Lanecarries two timestamp lists (usage_requests,usage_failures) pruned to 7 days on every write. One line per REQUEST, not per member attempt:chatcounts the request once the lane exists (the 404 answers before the ledger is touched) and marks the failure when the response is not success. A stream that dies after its 200 is committed is the one thing the meter cannot see, and does not need. - UI edits can never reset the counters.
lanes_writefolds the prior file's engine-owned fields onto whatever the renderer saves (merge_engine_owned), which also fixed a latent bug where a renderer save droppedbudget_hits. A clone starts its own empty ledger. - A quiet lane stays quiet. The footer line appears only when the
lane has moved in the last week —
24h 42 req · 3 fail · 7d 310 req— never as a row of zeros on an idle hall. - The windows are pinned by tests. Boundary rollover in Rust
(
prune_usage), window nesting in the renderer (usageCounts), the merge carry-over, and a server integration test for the request/failure/404 counting rules.
Done when: the status bar reads the engine's own ledger (not the dead Python-gateway scaffold), every copied example and doc snippet works against the lane endpoints, the auto-park budget is tunable in the UI, and mutes are per-(lane, kind) as advertised.
- Status bar reads the engine.
/healthnow reportsmodels_totalplus trailing-24hrequests_24h/failures_24haggregated from the engine's usage ledger (usage_24h, unit-tested);State::parseand the deadModel/Lane/Trafficgateway-scaffold structs inmain.rsare gone. The bar shows "N endpoints · M models", the fastest measured catalog speed, and real 24h traffic. - Copied setup examples are ready to run.
laneCurlExampleemits the exact command a client sends; README curl notes match. (Removed with the token in v0.5.2 — the example no longer carries an auth header.) - Auto-park budget is tunable. The gauge in a hall's header opens the lane budget popover (failures / window minutes), saved on every change, amber when tuned away from the 5-in-10-minutes standard.
- Mute is per-(lane, kind).
notifMuted(kind, hall)keys mutes ashall::kind, honouring legacy bare-kind entries so nothing saved breaks — this makes workstream 9's claim true rather than aspirational. - Hidden-window + burst hygiene. Polling and the short clocks pause while the window is hidden (with a visibilitychange catch-up), and the toast stack is capped at three so a failure burst cannot bury the screen.
A first public release should meet all of these conditions:
- The app installs and launches from a packaged artifact on a clean supported Linux system; a second launch behaves cleanly.
- A user can create a lane and copy a working endpoint without guessing.
- Provider keys never appear in the renderer or logs.
- Routing and persistence tests pass without live provider access.
- The README explains the product, installation, first lane, client connection, limitations, and security model.
- A failed upstream response produces an understandable diagnosis, and a normal capability skip produces none.
- The window stacks correctly on X11, Wayland, and when launched from VS Code.
- Running or downloading model weights locally.
- Becoming a general-purpose model serving platform.
- Exposing the gateway to a LAN or the public internet by default.
- Automatically spending money or selecting paid models without an explicit user decision.
- Replacing every provider-specific feature with a universal abstraction.