Menu โ–พ โ–ด

#458 feat(llm-lab): full-screen surface with a variant switcher and task sidebar

closed
nobody
None
2026-07-17
2026-07-17
Anonymous
No

Originally created by: Akarsh-Hegde

Base / scope note: this PR targets feat/llm-provider-enum, which trails feat/llm-provider-connectivity-test (the branch this was cut from) by the whole in-flight LLM-provider + Lab stack. So the raw diff is large (~127 files). The change THIS PR adds is the top two commits (875641ea, 8f8a7853) - 11 files under ui/components/timeline/llmlab/, src/llm_experiment/, src/pm_worklog/generate.rs, and the tray llm_lab command. Re-target the base to feat/llm-provider-connectivity-test for a clean 2-commit review.

What

Turns the dev-only LLM Lab from a 1180px modal into its own full-screen surface. A run now shows one variant's timeline at a time with a variant-switcher tab strip, clicking a task card in a fold variant opens a task sidebar beside the timeline, and the sidebar can draft that task's worklog on demand with the shown variant.

Phase A - full-screen surface, switcher, sidebar

  • LlmLabModal -> LlmLabScreen: full-viewport panel (absolute inset-0, own header, Escape-to-close) replacing the ModalShell wrapper.
  • RunView (new): run header + variant-switcher tab strip + single full-width body. Switching variant clears the task selection - task ids (T1,T2) are per-variant (each model builds its own day), so a stale id must not carry across a switch.
  • VariantBody (extracted from the former ResultsGrid.VariantColumn): one variant, full-width, preserving every ok / failed / rate_limited / pending branch so a non-ok variant shows its status, not a blank pane.
  • LabTaskSidebar (new): Lab-local read-only detail (When + the model's per-task log) from the DayTaskDetail the timeline already emits. Deliberately not DayTaskDetailPanel/useWorklog - those drive production worklog tables for a task id, and a fold's ids are a model's simulated day, not real rows.
  • ResultsGrid.tsx / LlmLabModal.tsx removed; shell import + JSX updated.

Phase B - on-demand "Draft with this model"

The sidebar drafts THIS fold task's worklog with whichever variant the timeline is showing - a metered, ephemeral call, gated behind a free/local caution, that writes nothing.

  • Because a fold task id is a model's simulated day (not a production day_tasks row), the draft is built from the task's inline content, never a production lookup:
  • generate.rs: generate_request_from_task builds the worklog-generate request from an inline title/summary/minutes, reusing the real board candidate set + the identical prompt/schema.
  • runner.rs: variant_backend's core is now resolve_backend(provider, model), shared with the draft path (custom:<id> included).
  • cli.rs: meridian llm-experiment draft-task --day --variant --task-json - one completion, prints {"draft": โ€ฆ}, persists nothing.
  • tray draft_lab_worklog: dev-gated command that shells out to it.
  • LabTaskSidebar: the button + free/local caution + a formatted/raw render of the drafted update (posts-to / summary / sections).

Reconciliation carried in code

A fold DayTask has no drafted-worklog prose - only summary bullets. Real prose comes only from worklog_generate, which reads production day_tasks. So the sidebar shows the summary log for free, and the polished prose is the on-demand, metered draft above - never auto-run.

Gating

Unchanged - dev-only (channel === 'dev' in the shell + commands/llm_lab.rs release refusal). This surface does not exist for users.

Verified

  • Daemon: cargo clippy clean; llm_experiment tests (20) pass.
  • Tray: cargo clippy -- -D warnings clean.
  • UI: npm run build (TypeScript pass) + bun test (227 pass).
  • Draft path exercised end-to-end against the freshly-built binary (temp DB, free local variant): meridian llm-experiment draft-task --variant local โ€ฆ returns a real drafted worklog in the {update:{summary,sections}} shape the sidebar renders - so generate_request_from_task -> resolve_backend -> complete all run.
  • Pre-push suite (fmt + clippy + cargo test + ui build/tests + security audit) green.

Not yet verified

  • The in-app GUI click-through in a live tray (full-screen layout, variant switcher, sidebar, and the draft button invoke) has not been run - it needs a dev tray build. Recommend a smoke test on a real day_fold run before merge.

๐Ÿค– Generated with Claude Code

Discussion

  • Anonymous

    Anonymous - 2026-07-17

    Originally posted by: coderabbitai[bot]

    [!IMPORTANT]

    Review skipped

    Auto reviews are disabled on base/target branches other than the default branch.

    Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.


    โš™๏ธ Run configuration

    Configuration used: Organization UI

    Review profile: ASSERTIVE

    Plan: Pro Plus

    Run ID: 2de79f3e-e33c-4bb3-991d-5df03de6126d

    You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

    Use the checkbox below for a quick retry:
    - [ ] ๐Ÿ” Trigger review

    โœจ Finishing Touches
    ๐Ÿงช Generate unit tests (beta) - [ ] Create PR with unit tests - [ ] Commit unit tests in branch `feat/llm-lab-fullscreen`

    Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

    โค๏ธ Share - [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) - [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) - [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) - [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

    Comment @coderabbitai help to get the list of available commands.

     
  • Anonymous

    Anonymous - 2026-07-17

    Originally posted by: Akarsh-Hegde

    Review scope

    Per the PR description, I reviewed the actual 2-commit diff (875641ea, 8f8a7853) against feat/llm-provider-connectivity-test rather than the raw ~127-file diff against the stale feat/llm-provider-enum base โ€” confirmed this is exactly the 11-file / 563(+)/101(-) change described.

    Overview

    Solid piece of work: turns the LLM Lab modal into a full-screen surface with a variant switcher (RunView) and a read-only task sidebar (LabTaskSidebar) that can fire an on-demand, ephemeral worklog draft against the currently-shown variant. The Rust side cleanly factors resolve_backend out of variant_backend and adds a parallel generate_request_from_task next to generate_request for the inline (non-DB) case. I traced the request/response shapes end-to-end (WorkstreamReport fields, DayTaskDetail โ†’ draft_lab_worklog body โ†’ --task-json โ†’ InlineTask โ†’ generate_request_from_task, and the custom:<id> / provider:model variant-token round-trip between RunView.variantToken() and Variant::parse) โ€” all consistent, no shape mismatches.

    Findings

    1. Module doc drift in tray/src-tauri/src/commands/llm_lab.rs (minor, but worth a line)
    The file header still says "Three commands behind the LLM Lab modal" and lists only run_llm_experiment / get_llm_experiments / get_llm_experiment โ€” draft_lab_worklog isn't mentioned, and "modal" is now stale terminology (it's a full-screen surface post-LlmLabScreen). Given this repo's convention of keeping module //! docs authoritative (see CLAUDE.md's "Document it (required)" step), this should get a fourth bullet.

    2. A load-bearing doc claim is now false, in two places

    • tray/src-tauri/src/commands/llm_lab.rs's header: "The llm-experiment CLI itself stays ungated for field debugging โ€” the UI is the gated surface."
    • src/llm_experiment/cli.rs's header doesn't call this out explicitly, but its dev-gating rationale (ungated in release because it "only ever writes the experiment tables") is implicit throughout.

    draft-task breaks that invariant: it's a CLI subcommand that fires a real, metered LLM completion and is not gated by dev_only() โ€” only the tray's draft_lab_worklog wrapper is. So on a shipped release binary, meridian llm-experiment draft-task ... run directly from a terminal will hit a live provider and incur cost, unlike every other subcommand in this file (which only touch local experiment tables). That may well be an acceptable/intentional tradeoff (it's still explicit, deliberate CLI use โ€” not something a user stumbles into), but the doc comments should say so, since they currently assert the opposite ("only ever writes the experiment tables").

    3. Sticky variant selection on an in-progress run (design nit, not a bug)
    RunView's firstOk is useMemo'd but only consumed via useState(firstOk), so it's genuinely computed once at mount (matches the comment). If you open a still-running run, selectedIdx starts at 0 (no ok yet) and never auto-advances to the first variant that finishes โ€” you have to manually click the switcher tab even after your currently-viewed variant fails and a later one succeeds. Probably fine given the explicit "computed once" comment, but worth a decision check: was auto-follow-first-completion considered and deliberately dropped, or just not thought about?

    4. No automated coverage for the new Rust prompt-assembly / resolution paths
    resolve_backend, generate_request_from_task, and draft_task have no unit tests. I manually verified generate_request_from_task's WorkstreamReport construction is field-for-field identical to load_workstream_report's (so the inline draft path builds the byte-identical prompt shape modulo the DB read), but a unit test asserting build_user_prompt output is equal for an inline task vs. an equivalent DB-loaded one would catch future drift between the two paths cheaply (no network/DB needed for that assertion). The PR description's "Verified" section covers this manually/end-to-end, which is reasonable for a dev-only surface, but the two prompt-builders diverging silently later is exactly the kind of thing a golden-string test would catch.

    What's solid

    • Type/shape correctness: DayTaskDetail.{day,title,minutes,summary} โ†’ draft_lab_worklog body โ†’ InlineTask all line up exactly; DayTaskColumn's onSelect: (detail: DayTaskDetail | null) => void signature matches VariantBody's onSelectTask passthrough (an improvement over the old ResultsGrid, which downgraded to just an id).
    • Variant token round-trip: variantToken() in RunView.tsx and Variant::parse in the daemon agree on custom:<id> / provider:model / bare-provider forms.
    • The let-else refactor in resolve_backend (let Some(provider) = LlmProvider::from_wire(provider) else { ...{provider:?}... }) is correct โ€” the else branch can't see the pattern binding, so the error message correctly prints the original &str argument, not an unbound name. Easy thing to get wrong; it's right here.
    • No shell-injection surface: draft_lab_worklog's JSON-encoded --task-json arg goes through tokio::process::Command::args(...), never a shell, so arbitrary task titles/summaries containing quotes/newlines are safe.
    • Gating: draft_lab_worklog correctly calls dev_only() first, matches the existing Lab commands' pattern, registered in lib.rs's invoke_handler!.
    • Stale-reference check (ResultsGrid/LlmLabModal/VariantColumn) came back clean โ€” the rename left no dangling imports.
    • CSS classes (bg-panel, rise, z-40) match existing ModalShell/ReportModal conventions exactly.

    Suggested action

    None of the findings are blocking โ€” [#1] and [#2] are a couple sentences of doc fix-up, [#3] is worth a one-line confirmation from you on intent, [#4] is a nice-to-have. I'd merge after a quick doc pass on [#1]/#2, especially [#2] since it's a security/cost-surface claim that's now inaccurate.

     

    Related

    Tickets: #1
    Tickets: #2
    Tickets: #3
    Tickets: #4

  • Anonymous

    Anonymous - 2026-07-17

    Originally posted by: Akarsh-Hegde

    Review addressed โ€” 3f4f65cb

    Thanks for the thorough pass. All four findings resolved:

    #2 (metered CLI claim โ€” the load-bearing one): fixed, and closed the hole rather than just documenting it. Rather than leave draft-task ungated-with-a-caveat, I gated it: it now refuses in a release build (cfg!(debug_assertions)), matching the tray's dev_only gate. This is safe for the dev flow - a debug tray resolves meridian_bin() to target/debug/meridian (debug_assertions on), and in release the tray's draft_lab_worklog refuses before shelling out anyway, so the button never regresses. The cost-surface (a shipped binary running meridian llm-experiment draft-task and hitting a live provider) is gone. Both doc headers (cli.rs, tray llm_lab.rs) now state the invariant accurately: the read/run subcommands stay ungated because they only write local tables; draft-task is the metered exception and is dev-gated.

    #1 (module doc drift): fixed. The tray llm_lab.rs header now says four commands (adds draft_lab_worklog), drops the stale "modal" wording for "full-screen surface", and the "# Who calls this" section names the draft invoke.

    #3 (sticky variant selection): implemented auto-follow. Not intentional before - now RunView auto-follows the first variant to finish until the user manually picks a tab, then freezes on that choice. It won't jump back to a lower-index variant that finishes later (guards on the shown variant already being ok). So opening a still-running run lands on whichever model completes first instead of a pending/failed one.

    #4 (no coverage): added two tests in generate.rs.

    • build_user_prompt_pins_the_shape_both_draft_paths_share - a golden on the shared assembler both generate_request and generate_request_from_task feed.
    • inline_draft_request_matches_the_worklog_generate_contract - asserts generate_request_from_task builds the byte-identical system/schema/max_tokens contract as the DB-backed path, and that the inline title/summary/minutes reach the workstream block.

    Verified: daemon pm_worklog::generate tests (19) pass; tray cargo clippy clean; ui npm run build (TypeScript) + bun test (227) pass; pre-push suite green.

    Ready to merge from my side. One reminder carried over from the description: the in-app GUI click-through in a live dev tray still hasn't been run (the draft path is verified end-to-end via the CLI, but not the React wiring) - worth a quick smoke test on a real day_fold run before merge.

     
  • Anonymous

    Anonymous - 2026-07-17

    Ticket changed by: Akarsh-Hegde

    • status: open --> closed
     

Log in to post a comment.