feat(dashboard/prompts): central prompt repository page with versions and agent binding (#6160)#6167
Conversation
houko
left a comment
There was a problem hiding this comment.
Automated review pass. Integration test present and clean, data-layer hooks correct, auth allowlist untouched (endpoint is authenticated — correct), route registration via existing prompts::router() — correct, no AI attribution, no dangerous patterns.
Three inline notes:
keys.ts—promptsKeys.list()andpromptsKeys.lists()return identical arrays, collapsing the required two-level hierarchy. Suggest adding a distinguishing segment tolist()now before a filtered variant is added.mutations/prompts.ts—useBindPromptVersionToAgentskipspromptsKeys.details()invalidation. No current subscriber, but the factory exposesdetail(agentId)so this is a latent staleness bug.uk.json— Ukrainian plural forms_fewand_manyare missing; i18next needs them for counts 2–4 and 5–20 respectively.
Generated by Claude Code
| lists: () => [...promptsKeys.all, "list"] as const, | ||
| // The overview is a single fleet-wide list; no per-filter variants yet, | ||
| // but keep the hierarchical shape so a future `list(filters)` slots in. | ||
| list: () => [...promptsKeys.lists()] as const, |
There was a problem hiding this comment.
list() and lists() return the identical array ["prompts", "list"], which collapses the two levels of the hierarchy that CLAUDE.md requires.
The invariant the factory shape is meant to provide: invalidateQueries({ queryKey: promptsKeys.lists() }) should match list(filterA), list(filterB), etc., while invalidateQueries({ queryKey: promptsKeys.list() }) should match only the unfiltered query.
Right now the two are indistinguishable, so today's callers doing invalidateQueries({ queryKey: promptsKeys.list() }) are effectively doing a broad list-invalidation (same as lists()), which is harmless only because there's one variant.
The comment in the code acknowledges this ("no per-filter variants yet"), but the conventional fix is to give list() a distinguishing segment now so a future list(filters) slots in cleanly without a breaking rename:
// Option A — tuck a sentinel segment into the no-filter variant
list: () => [...promptsKeys.lists(), {}] as const,
// Option B — explicit segment name
list: () => [...promptsKeys.lists(), "overview"] as const,Either way, update the keys test at line 694 to assert promptsKeys.list() ≠ promptsKeys.lists() (the anchoring and details ≠ lists assertions already exist and pass; the missing guard is list ≠ lists).
Generated by Claude Code
| }); | ||
| qc.invalidateQueries({ queryKey: agentKeys.detail(variables.agentId) }); | ||
| qc.invalidateQueries({ queryKey: agentKeys.lists() }); | ||
| qc.invalidateQueries({ queryKey: promptsKeys.list() }); |
There was a problem hiding this comment.
useBindPromptVersionToAgent's onSuccess invalidates promptsKeys.list() but not promptsKeys.details().
promptsKeys.detail(agentId) exists in the factory and the factory comment says it's for per-agent detail queries.
Nothing subscribes to it yet, but if any future component does (e.g., a dedicated single-agent prompt-repo detail view), a bind will silently leave it stale.
The safe, forward-compatible pattern per CLAUDE.md is to also invalidate the whole details() subtree in the mutation's onSuccess, colocated here:
qc.invalidateQueries({ queryKey: promptsKeys.details() });The same applies to useCreatePromptVersionForRepo and useDeletePromptVersionForRepo if a detail view is ever added, though for those the impact is narrower (no live-prompt change).
Generated by Claude Code
| "active_badge": "Активна v{{version}}", | ||
| "no_active_badge": "Немає активної версії", | ||
| "empty_live_prompt": "(системний промпт не задано)", | ||
| "version_count_one": "{{count}} версія", |
There was a problem hiding this comment.
Ukrainian pluralization uses four forms (_one, _few, _many, _other), not two.
The missing forms cause i18next to fall back to _other for values like 2, 3 ("версій" instead of "версії") and 5–20 ("версій" is actually correct there, but falling back rather than matching is fragile).
i18next plural suffixes for uk:
| Count | Suffix | Example |
|---|---|---|
| 1 | _one |
1 версія ✓ already present |
| 2, 3, 4 | _few |
2 версії — missing |
| 5–20, 11–19 | _many |
5 версій — missing (rule matters when _many and _other diverge) |
| other fractions | _other |
✓ already present |
Suggested additions after line 3839:
"version_count_few": "{{count}} версії",
"version_count_many": "{{count}} версій",en.json and zh.json are unaffected — English uses only _one/_other and Chinese is count-insensitive.
Generated by Claude Code
4c879c1 to
b13f47e
Compare
Pull request was converted to draft
… and agent binding (#6160) Add a dedicated Prompts page that manages agent system prompts in one place: list every agent's prompt repository, view version history, create/delete versions, and bind a version to make it the agent's live system prompt. Backend reuses the existing per-agent prompt-version store (routes/prompts.rs). The only new endpoint is GET /api/prompts/overview, a read-only cross-agent aggregation over agent_registry().list_arcs() + list_prompt_versions — no new storage and no new kernel trait method. Binding writes the version's text onto manifest.model.system_prompt via PATCH /api/agents/{id} (the prompt actually used in LLM calls) and then flips the store's is_active flag, so activation has a real live effect instead of only updating metadata. Closes #6160
b13f47e to
27c29fe
Compare
Deploying librefang with
|
| Latest commit: |
eb526d1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4b06954a.librefang-7oe.pages.dev |
| Branch Preview URL: | https://feat-prompt-repository-page.librefang-7oe.pages.dev |
…k plurals Addresses the three review notes on #6167. keys.ts: tag promptsKeys.list() with an "overview" segment so it is strictly more specific than lists(), restoring the two-level hierarchy the factory shape promises (a broad lists() invalidation still matches the overview, while list() no longer collapses into lists()). keys.test.ts asserts list() != lists() and that lists() prefixes list(). mutations/prompts.ts: invalidate promptsKeys.details() in the create/delete/bind onSuccess blocks so a future single-agent prompt-repo detail view stays fresh, colocated with the mutation per the data-layer rule. uk.json: add the Ukrainian _few/_many CLDR plural forms for version_count, and for pinned_peers_count and requires_env_count which carried the identical missing-form defect. locale-parity.test.ts: make the parity check plural-aware so it can accept and enforce per-language plural categories. Non-plural keys keep exact cross-locale equality (the original anti-fallback guard); plural families are validated against Intl.PluralRules per locale, so each locale must supply every CLDR category its grammar selects (en one/other, uk one/few/many/other, zh other), while benign extra suffixes a language never selects are tolerated.
|
All three addressed in 2e73c89.
That last point exposed a conflict: the Verification ( |
What
Adds a dedicated Prompts page to the dashboard so operators can manage agent system prompts centrally — list every agent's prompt repository, view version history, create and delete versions, and bind a version to make it the agent's live system prompt — instead of re-typing system prompts per agent and keeping them in external docs.
Closes #6160.
Backend reused vs added
Reused (no changes): the existing per-agent prompt-version store in
crates/librefang-api/src/routes/prompts.rs— version list/get/create/activate/delete plus experiments.The store, kernel handle (
PromptStore), and SQLite layer (crates/librefang-memory/src/prompt.rs) were already complete.Added — one endpoint:
GET /api/prompts/overviewinroutes/prompts.rs— a read-only cross-agent aggregation that returns one summary row per non-hand agent (agent_id,agent_name,version_count,active_version,active_version_id,live_system_prompt,latest_version_at).It folds
agent_registry().list_arcs()together with the existinglist_prompt_versionsper agent.No new storage, no new kernel trait method, no new dependency.
A store error for a single agent degrades that row to a zero-version summary rather than failing the whole response.
The prompts router carries no
#[utoipa::path]annotations (matching every existing prompts handler), so the OpenAPI surface and the reflection tests are unaffected.Agent binding — the load-bearing decision
The existing
POST /api/prompts/versions/{id}/activateonly flips the store'sis_activeflag.The prompt that actually reaches the LLM is read from
manifest.model.system_prompt(crates/librefang-kernel/src/kernel/agent_execution.rs:683,messaging.rs:525/2433), and the prompt store is a downstream snapshot of it (auto_track_prompt_version), never a source — so activation on its own has no effect on the next LLM call.So Bind (
useBindPromptVersionToAgent) does both, in order:PATCH /api/agents/{id}withsystem_prompt= the version's text, hot-swapping the live prompt on the next message and persisting it to the agent manifest (the existing mechanismAgentManifestFormuses).POST /api/prompts/versions/{id}/activate, flipping the store flag so the repository UI shows it active.This wires binding through the existing manifest mechanism rather than inventing a parallel one, and makes activation have a real live effect.
Frontend
src/pages/PromptsPage.tsx: searchable grid of agent prompt-repo cards (active-version badge, live-prompt preview, version count), with a per-agent panel modal for version history — create/delete/bind versions, three-state UI (loading / empty / error / no-match).dashboard/AGENTS.md:src/lib/queries/prompts.ts(usePromptsOverview),src/lib/mutations/prompts.ts(useCreatePromptVersionForRepo,useDeletePromptVersionForRepo,useBindPromptVersionToAgent), hierarchicalpromptsKeysfactory inkeys.ts, raw calllistPromptsOverviewinapi.tsre-exported vialib/http/client.ts.Per-agent version reads reuse the existing
agentKeys.promptVersionscache; mutations invalidate via factory keys inonSuccess.router.tsx) and the sidebar nav (App.tsx) with alucide-reacticon (ScrollText).nav.promptsplus a new top-levelpromptsblock in all three locale files (en.json,zh.json,uk.json), key sets in sync.Verification
Dashboard (
crates/librefang-api/dashboard):pnpm typecheck— clean.pnpm lint— 0 errors (88 pre-existing warnings, none in new files).pnpm test --run— 81 files, 806 tests pass, including the 9 newPromptsPage.test.tsxcases (loading/empty/error/no-match states, bind/create/delete mutation calls, active-version guards), thepromptsKeysfactory tests inkeys.test.ts, and thelocale-paritytest.pnpm build— succeeds.Rust (Docker dev image, per-worktree target volume):
cargo check -p librefang-api --lib— clean.cargo clippy -p librefang-api --all-targets -- -D warnings— clean.cargo test -p librefang-api --test prompts_routes_integration— 21 pass, including the newprompts_overview_returns_paginated_envelope(asserts the paginated envelope + the seeded agent's row shape: UUID id, surfaced live prompt, zero versions / null active version with no store rows).cargo test -p librefang-api --test dead_route_audit_test --test openapi_path_coverage_test— pass (no route/spec drift).cargo fmt --checkon the two touched Rust files — clean.Maintainer notes
agent_id); there is no standalone prompt entity. Rather than introduce a new global-prompt table, the repository page surfaces every agent's prompt store in one view and binds a version onto the chosen agent. This ships full create / version-history / activate / bind against the existing backend.librefang-memory/librefang-kernel-handle— a different crate than this PR touches. Flagging for a maintainer decision rather than building it unilaterally; the current binding model (manifest text + store snapshot) covers the issue's stated need (manage centrally, bind to an agent, version management).(@user)attribution guard plus the no-AI-attribution rule make a per-PR bullet inappropriate here. Add at release time.