Skip to content

feat(prompts): portal library reorg - collections, facets, versions, two-bucket model (#1010)#1024

Merged
cjimti merged 1 commit into
mainfrom
feat/1010-prompt-library-reorg
Jul 22, 2026
Merged

feat(prompts): portal library reorg - collections, facets, versions, two-bucket model (#1010)#1024
cjimti merged 1 commit into
mainfrom
feat/1010-prompt-library-reorg

Conversation

@cjimti

@cjimti cjimti commented Jul 22, 2026

Copy link
Copy Markdown
Member

Closes #1010

Summary

The portal prompt library is reorganized around the user's actual mental model: two buckets instead of a flat three-tab table, collections instead of free-text category as the primary organizing structure, audit-derived usage on every row so dead prompts are identifiable at a glance, and a version history surface that shows exactly what was approved, by whom, and how any version differs from what is served today. The scope taxonomy (global, persona, personal) no longer appears anywhere in the user-facing library; it surfaces only inside the promote and admin flows where it belongs.

Two-bucket model

My Prompts merges the caller's personal prompts with prompts shared to them person-to-person, each shared row attributed with "Shared by ". Library shows the approved shared prompts visible to the caller, grouped by collection with uncollected prompts under a trailing General group. Semantic search spans both buckets: server-ranked results across personal and Library, plus name and description matches from the shared-with-me list.

Library grouped by collection

Collections

Migration 000086 adds prompt_collections (case-insensitively unique names, bounded name and description) and prompts.collection_id with ON DELETE SET NULL, so a prompt belongs to at most one collection and deleting a collection releases its members to the General group rather than deleting them. Placement is organizational metadata: assignment never produces a version snapshot, never triggers the review gate, and a review-gated content edit cannot smuggle a collection change (identityFieldsChanged includes CollectionID).

REST lives in pkg/prompt/versionhttp (the established prompt REST surface; pkg/portal and pkg/admin sit at the package-size cap) and is mounted on both surfaces: GET/POST /prompt-collections, PUT/DELETE /prompt-collections/{id}, and PUT /prompts/{id}/collection. Any authenticated user can create a collection; renaming and deleting are creator-or-admin; assignment follows the prompt's own mutation rule (owner for personal prompts, admin for shared, system rows read-only). Bodies are capped at 1 MiB and malformed UUIDs resolve to 404 rather than 500 (SQLSTATE 22P02 maps to not-found in the store, including the pre-existing GetByID path).

The collection capability reaches the handlers through a CollectionProvider accessor on the promptlayer notifying wrapper, so the store decorator exposes it from every capability-combination shape without doubling the wrapper matrix, and collection writes correctly skip the prompts/list_changed hook (they never change the MCP prompt list).

Version history, diffs, and provenance

The prompt page renders the version list with per-version author, timestamp, status, and the approval stamp bound to that specific version, plus a line diff between any version and the current content (a small LCS implementation in ui/src/lib/textDiff.ts; no new dependency). A pending draft on an approved shared prompt is flagged with a banner: readers keep being served the approved version until an admin approves the draft.

GET /api/v1/portal/prompts/{id}/versions access now mirrors prompt visibility instead of owner-or-admin, since history is the library's verification surface. Non-privileged viewers of a shared prompt receive the served history only: applied snapshots in full, the pending draft as a content-redacted author/date stub (its existence is the at-a-glance re-review signal), and rejected or superseded drafts omitted entirely. Owners keep full access to their personal history and admins to everything.

Version history with pending draft and provenance

Usage and dead-prompt identification

Every row shows run count and last-run age from the #1009 prompt_serve audit rollup, joined client-side from GET /portal/prompts/usage. Sorting covers name, runs, and last run (usage sorts default to most-active-first), a usage facet isolates recently-used or never/long-unused prompts, and prompts never run or unrun for over 60 days carry an inactive badge. The portal usage endpoint now also includes prompts shared person-to-person with the caller (wired from the portal share store), so shared rows show real usage instead of a false never-run state. While the rollup is loading or unavailable the columns show a dash rather than claiming inactivity.

Facets and invocation help

Browse mode narrows by collection, tag, status (My Prompts), owner (Library), and usage, with facet state reset on tab switch so a filter set on one bucket never silently narrows the other. The prompt page adds a copyable natural-language invocation ("Run the prompt with date ") built from the stable name and required arguments, resolved agent-side by manage_prompt use.

UI structure

MyPromptsPage is decomposed into a list model (promptList.ts: rows, facets, decorate-sort), PromptListTable, PromptFacetsBar, and PromptCreateForm; the viewer gains VersionHistory, InvocationHelp, and CollectionPicker. The viewer's badge vocabulary is now Personal / Library / System. The portal API client accepts the {"error": ...} body shape alongside RFC 9457 detail, matching the surfaces it actually calls.

Testing

  • Go unit tests across pkg/prompt, pkg/prompt/postgres (sqlmock), pkg/prompt/versionhttp (full permission matrices, redaction, shared-usage inclusion, validation bounds), and the promptlayer capability accessor; every new function at or above the 80% gate.
  • Real-Postgres integration test (TestCollections_RealDB_RoundTrip) covering the migration schema: case-insensitive uniqueness, FK sentinel, ON DELETE SET NULL release, NULL-clearing assignment; make test-realdb green.
  • 15 new Playwright interactive tests (ui/e2e/interactive/prompts.spec.ts) covering both buckets, attribution, collection grouping, every facet including the filtered empty state, run-count sort order, search across buckets, the collections manager create/duplicate/rename/delete flow, version history, diff open/close, viewer assignment, and back navigation, plus an assertion that the words Scope and Persona never render in the library.
  • MSW handlers mirror the real server's status codes and shapes (create 201, duplicate 409, rename 400/409, live prompt counts), and version fixtures keep the served-content invariant so diffs against "current" behave as on the real server.
  • Screenshot suite regenerated in both themes with four new captures (library grouping, collections manager, version history, version diff), embedded in docs/server/portal-user.md; docs/llms.txt and docs/llms-full.txt updated to the implemented contracts.
  • make verify green end to end.

Depends on #1008 (stable invocation identity) and #1009 (versions, provenance, usage stats), both previously merged. The prompt browser MCP App (#1011) consumes the same collections, facets, and search this establishes.

…two-bucket model (#1010)

The prompt library presents two buckets: My Prompts (personal plus shared-with-me, attributed) and Library (approved shared prompts grouped by collection); scope taxonomy appears only in promote/admin flows. Collections are org-visible named groups (prompt_collections + prompts.collection_id, at most one per prompt, ON DELETE SET NULL) with creator-or-admin management and owner/admin assignment via new versionhttp REST routes on both surfaces. Facets (collection, tag, status, owner, usage), usage columns and sorting from the audit rollup (now including person-shared prompts), and an inactive badge make dead prompts identifiable. The prompt page adds version history with per-version approval provenance and line diffs (non-privileged viewers get served history only; pending drafts are content-redacted stubs), point-of-use invocation help, and a collection picker. Malformed UUID lookups map to 404, collection bodies are size-bounded, and the portal client accepts both RFC 9457 and {error} shapes.
@cjimti
cjimti merged commit b6b75a3 into main Jul 22, 2026
8 checks passed
@cjimti
cjimti deleted the feat/1010-prompt-library-reorg branch July 22, 2026 18:02
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.63880% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.31%. Comparing base (2142297) to head (c674812).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pkg/prompt/postgres/collection.go 88.09% 5 Missing and 5 partials ⚠️
pkg/prompt/versionhttp/usage.go 82.92% 5 Missing and 2 partials ⚠️
pkg/prompt/collection.go 75.00% 4 Missing ⚠️
pkg/prompt/versionhttp/collections.go 97.03% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1024    +/-   ##
========================================
  Coverage   89.30%   89.31%            
========================================
  Files         465      468     +3     
  Lines       51375    51652   +277     
========================================
+ Hits        45883    46132   +249     
- Misses       3650     3668    +18     
- Partials     1842     1852    +10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prompts: portal library reorganization - collections, facets, versions, two-bucket model

1 participant