Skip to content

feat(dashboard/prompts): central prompt repository page with versions and agent binding (#6160)#6167

Merged
houko merged 3 commits into
mainfrom
feat/prompt-repository-page
Jun 17, 2026
Merged

feat(dashboard/prompts): central prompt repository page with versions and agent binding (#6160)#6167
houko merged 3 commits into
mainfrom
feat/prompt-repository-page

Conversation

@houko

@houko houko commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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/overview in routes/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 existing list_prompt_versions per 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}/activate only flips the store's is_active flag.
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:

  1. PATCH /api/agents/{id} with system_prompt = the version's text, hot-swapping the live prompt on the next message and persisting it to the agent manifest (the existing mechanism AgentManifestForm uses).
  2. 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

  • New page 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).
  • Data layer per dashboard/AGENTS.md: src/lib/queries/prompts.ts (usePromptsOverview), src/lib/mutations/prompts.ts (useCreatePromptVersionForRepo, useDeletePromptVersionForRepo, useBindPromptVersionToAgent), hierarchical promptsKeys factory in keys.ts, raw call listPromptsOverview in api.ts re-exported via lib/http/client.ts.
    Per-agent version reads reuse the existing agentKeys.promptVersions cache; mutations invalidate via factory keys in onSuccess.
  • Registered in the router (router.tsx) and the sidebar nav (App.tsx) with a lucide-react icon (ScrollText).
  • i18n: nav.prompts plus a new top-level prompts block 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 new PromptsPage.test.tsx cases (loading/empty/error/no-match states, bind/create/delete mutation calls, active-version guards), the promptsKeys factory tests in keys.test.ts, and the locale-parity test.
  • 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 new prompts_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 --check on the two touched Rust files — clean.

Maintainer notes

  • Scope decision — page is organized by agent. Prompt versions are agent-scoped in the existing store (every endpoint takes 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.
  • Deferred with evidence — a truly global, agent-independent prompt library (one prompt shared across many agents as a first-class entity) would require a new SQLite table and kernel storage trait in 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).
  • CHANGELOG intentionally not touched — entries are batched per release with contributor attribution, and the pre-commit (@user) attribution guard plus the no-AI-attribution rule make a per-PR bullet inappropriate here. Add at release time.

@github-actions github-actions Bot added the size/XL 1000+ lines changed label Jun 17, 2026

@houko houko left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. keys.tspromptsKeys.list() and promptsKeys.lists() return identical arrays, collapsing the required two-level hierarchy. Suggest adding a distinguishing segment to list() now before a filtered variant is added.
  2. mutations/prompts.tsuseBindPromptVersionToAgent skips promptsKeys.details() invalidation. No current subscriber, but the factory exposes detail(agentId) so this is a latent staleness bug.
  3. uk.json — Ukrainian plural forms _few and _many are 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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}} версія",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@github-actions github-actions Bot added the has-conflicts PR has merge conflicts that need resolution label Jun 17, 2026
@houko
houko force-pushed the feat/prompt-repository-page branch from 4c879c1 to b13f47e Compare June 17, 2026 12:28
@houko
houko enabled auto-merge (squash) June 17, 2026 12:28
@houko
houko marked this pull request as draft June 17, 2026 12:32
auto-merge was automatically disabled June 17, 2026 12:32

Pull request was converted to draft

@houko
houko marked this pull request as ready for review June 17, 2026 12:37
@github-actions github-actions Bot added ready-for-review PR is ready for maintainer review and removed has-conflicts PR has merge conflicts that need resolution labels Jun 17, 2026
@houko
houko enabled auto-merge (squash) June 17, 2026 12:41
@github-actions github-actions Bot added has-conflicts PR has merge conflicts that need resolution and removed ready-for-review PR is ready for maintainer review labels Jun 17, 2026
… 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
@houko
houko force-pushed the feat/prompt-repository-page branch from b13f47e to 27c29fe Compare June 17, 2026 13:55
@github-actions github-actions Bot added ready-for-review PR is ready for maintainer review and removed has-conflicts PR has merge conflicts that need resolution labels Jun 17, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown

…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.
@houko

houko commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

All three addressed in 2e73c89.

  1. keys.tslist() vs lists() collapse. promptsKeys.list() now returns ["prompts", "list", "overview"] (Option B), so it is strictly more specific than lists(): a broad invalidateQueries({ queryKey: lists() }) still matches the overview, while list() no longer collapses into lists(). keys.test.ts updates the shape assertion and adds the missing guard — list() !== lists() plus lists() prefixes list().

  2. mutations/prompts.ts — missing details() invalidation. useBindPromptVersionToAgent now also invalidates promptsKeys.details() in onSuccess, and I applied the same forward-compatible invalidation to useCreatePromptVersionForRepo and useDeletePromptVersionForRepo so any future per-agent repo-detail view stays fresh on create/delete too.

  3. uk.json — Ukrainian plural forms. Added version_count_few / version_count_many. The same missing-form defect existed in two pre-existing plural keys in the same file (pinned_peers_count, requires_env_count), so I completed those as well.

That last point exposed a conflict: the locale-parity test enforces an identical flat key set across locales, which is incompatible with per-language CLDR plural categories (uk needs one/few/many/other, en needs one/other, zh needs other) — adding the uk forms in isolation would have failed it as "extra keys". So locale-parity.test.ts is now plural-aware: non-plural keys keep exact cross-locale equality (the original anti-fallback guard from #3557), while plural families are validated against Intl.PluralRules per locale, so each locale must supply every category its grammar selects and benign extra suffixes a language never selects (e.g. a dead _one in zh) are tolerated.

Verification (crates/librefang-api/dashboard): pnpm test --run 812 pass (82 files), pnpm typecheck clean, pnpm lint 0 errors, pnpm build succeeds. No Rust files touched.

@houko
houko merged commit b746a19 into main Jun 17, 2026
33 checks passed
@houko
houko deleted the feat/prompt-repository-page branch June 17, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review PR is ready for maintainer review size/XL 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

增加Prompt集锦或者仓库页面

1 participant