feat: prompt library + {{placeholder}} templating - #135
Conversation
Reusable prompt templates inserted in the composer by typing `/<name>` — the roadmap v0.5 slash-commands seed in its genuinely-useful form (validated by Open WebUI, which has the same `/` prompt menu). - `prompts` table (owner-scoped, FORCE RLS, UNIQUE(user_id, name), migration 0015) + `/api/v1/me/prompts` CRUD, mirroring memories. `name` is a slug (^[A-Za-z0-9_-]+$ DB CHECK) so `/<name>` is unambiguous; a duplicate name is a real 409 (DB unique + a 23505-walking catch — stricter than the memories pre-check). - Settings section to manage prompts; a `/` autocomplete menu in the chat composer to insert them. Two-reviewer round hardened the composer: - trigger is `^/(\S+)$` — bare `/` never opens the menu, so a literal `/` message still sends (the adversarial literal-send trap); - prefix matching is case-insensitive; - only BARE Enter selects (Shift+Enter falls through to a newline); - the list comes from one shared usePromptsQuery() (fetched once, filtered per keystroke, invalidated by a settings edit). The core-composer change is minimal + backward-compatible: PromptInputTextarea now calls a passed onKeyDown first and bails on preventDefault — before, a passed handler silently clobbered Enter-to-send. Carved out of experiment/overnight-loop-3 as an independent PR. Also adds the missing `prompts-rls.integration` invocation to `scripts/rls-test.sh` (the harness previously only ran `chats-rls.integration`, so this suite was never exercised), and adds `sonner` as a direct `apps/web` dependency for the settings section's `toast()` calls (the shared `@workspace/ui` sonner wrapper only re-exports `<Toaster/>`, matching upstream shadcn/ui convention). Verified: 5 prompts RLS integration cases (owner CRUD, cross-tenant denied, per-user unique name / same-name-across-users allowed, slug + content CHECKs) + pure-trigger and web service cases, api + web typecheck/lint/unit tests clean, migration 0015 + drizzle-kit check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Saved prompts can now carry `{{placeholder}}` variables, completing the
prompt library into reusable, parameterized prompts. Inserting a prompt
with placeholders (via `/name`) opens a small fill-in dialog — one field
per unique placeholder — and substitutes the values into the composer; a
plain prompt inserts directly as before. Client-only, no schema change
(placeholders are plain text in the body).
Chose a fill DIALOG over inline cursor-jump templating deliberately: it
sidesteps textarea DOM-ref/selection machinery, is complete for any number
of variables, and is fully unit-testable via pure
extractPlaceholders/fillPlaceholders.
Two-reviewer round hardened it:
- substitution is a SINGLE String.replace(regex, callback) pass over the
original body — a value that itself contains `{{x}}` is never
re-expanded;
- the `/` menu is dismissed via setDismissedFor(input) when the dialog
opens — no menu-behind-dialog glitch, no cancel-reopen loop;
- ReDoS-safe regex (`{{([^{}]*?)}}`, no overlapping quantifiers);
- the adversarial reviewer's catch: dialog field ids are INDEX-based,
because a placeholder name can contain spaces (`{{target language}}`) —
invalid in a DOM id, would break label[for].
Verified: 8 pure templating cases (unique/ordered extraction, dedupe,
empty `{{}}` ignored, substitution, duplicate-fill, unfilled->empty,
no-double-expansion), web build/lint/tsc clean; no api/schema touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apps/api/openapi.json is generated (pnpm --filter api build); regenerate rather than hand-merge per AGENTS.md convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 57 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (32)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements a prompt library and templating feature, allowing users to save reusable prompt templates with {{placeholder}} variables and insert them in the chat composer using a / autocomplete menu. It introduces a database migration for the prompts table with Row Level Security (RLS), backend CRUD endpoints, and corresponding frontend settings and composer UI components. The review feedback highlights a dependency configuration issue with sonner, an architectural concern regarding throwing HTTP exceptions inside database transaction callbacks, a potential bug in the prompt update error message when dto.name is undefined, and a UX improvement opportunity for navigating the placeholder fill-in dialog using the Enter key.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
14 issues found across 29 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/package.json">
<violation number="1" location="apps/web/package.json:36">
P2: sonner is used by two workspaces but isn't in the workspace catalog. Both `apps/web` and `packages/ui` declare `"sonner": "^2.0.5"` — they match today, but without a catalog entry they can drift apart on future manual updates. Per the pnpm-workspace.yaml convention ("One shared version per dependency used by 2+ workspaces"), add `sonner` to the catalog in `pnpm-workspace.yaml` and reference it as `"catalog:"` in both `package.json` files.</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…library # Conflicts: # CHANGELOG.md
Review-response fixes for PR #135 (5 unresolved threads, verified real): - DTO: trim `content` before length validation (CreatePromptDto/ UpdatePromptDto). A whitespace-only body previously passed `@MinLength(1)` on the RAW string, then the controller's `.trim()` produced an empty string that failed the DB CHECK as an unhandled 500 instead of a clean 400. Proven by a new prompts.dto.spec.ts. - Schema/migration: `prompts_user_name_idx` is now a case-insensitive functional unique index (`lower(name)`), matching the composer menu's already-case-insensitive prefix matching (matching.ts) — "Standup" and "standup" could otherwise coexist as two distinct, ambiguous `/name` triggers for the same user. Migration 0015 regenerated in place (not yet merged, so no new migration number needed) with FORCE ROW LEVEL SECURITY re-appended per the documented pattern. Proven by a new RLS integration case. - Repository: `PromptsRepository.lockUserForCreate()` takes a transaction-scoped `pg_advisory_xact_lock` (namespaced, 2-int-arg form) before the per-user cap check in the controller's create path, closing the TOCTOU race where concurrent creates from the same user could each read the same pre-insert count and overshoot PROMPT_MAX_PER_USER. Different users never block each other. Proven by a new concurrent-create RLS integration case (3 concurrent attempts at the cap boundary → exactly 1 succeeds). - Module boundary: extracted `apps/api/src/prompts/` as its own NestJS module (`PromptsModule`, registered in `app.module.ts`), instead of bundling `MePromptsController` into `ChatsModule`. Prompts is a standalone resource (own table/RLS/DTOs/repository/tests) — matches the "one module per feature" convention already followed by chats/runs/auth/users. `AGENTS.md`'s structure list updated. Verified: full RLS harness green (chats-rls 13/13, prompts-rls 7/7 incl. the two new cases, queue 8/8, auth e2e 36/39 incl. 3 skipped), api typecheck/lint/unit tests clean (141 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review-response fixes for PR #135 (3 unresolved threads, verified real): - The `/` menu's Enter-to-select and the fill dialog's Enter-to-submit both intercepted every Enter keydown, including the one that CONFIRMS an IME composition (e.g. romaji "su" typed toward a kana conversion can transiently match a saved "/summarize" prompt, or a placeholder fill value can be any text). Guarded both with `e.nativeEvent.isComposing`, so composing text no longer risks a hijacked keystroke. - Fill-dialog UX: Enter on an intermediate placeholder field now advances focus to the next field instead of doing nothing; only the last field's Enter submits (as before). Verified: web typecheck/lint/vitest clean (40/40 tests, no regressions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review-response fixes for PR #135 (3 unresolved threads, verified real): - Client-side `nameOk` didn't cap length, so a name past the API's 64-char max (PROMPT_NAME_MAX) passed local validation and only failed at the server, with no explanation. Now checked client-side too. - `contentOk` measured the untrimmed string against PROMPT_CONTENT_MAX while the submitted value is trimmed — valid trimmed content near the max could be rejected by the client before ever reaching the server. Now measures the trimmed length, matching what's actually sent. - The save-error toast always said "the name may already be taken" regardless of cause, misattributing network/server errors as name conflicts. Now inspects the ky HTTPError status (the existing login-form.tsx convention) and only blames the name on an actual 409. - Delete had no error feedback at all — a failed delete silently left the prompt in the list with no indication anything went wrong. Added an onError toast. Verified: web typecheck/lint/vitest clean (40/40 tests, no regressions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review-response fix for PR #135 (1 unresolved thread, verified real): the non-goals section still listed `{{placeholder}}` templating as an unshipped "named follow-up", but this PR ships it (in the same PR, via the sibling templating design doc). Removed the stale bullet and pointed at 2026-07-03-prompt-templating-design.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure route-registration-order artifact from moving MePromptsController out of ChatsModule into its own PromptsModule — verified structurally identical (same paths, same schemas, same content per path) to the pre-extraction spec; only the JSON key order shifted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 17 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
The review round added two more integration cases (case-insensitive name conflict, concurrent-create cap race under the advisory lock) after this line was originally written. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pg_advisory_xact_lock(int, int)'s second key was plain hashtext() — a 32-bit hash of userId — so two unrelated users' create() calls could occasionally collide and serialize against each other under load (not a correctness/isolation bug, just spurious contention). Switch to the single-bigint-arg overload keyed by hashtextextended(userId, classid), which gives a 64-bit hash and folds in the same namespace via the seed argument. No other code in this repo takes an advisory lock, so there is nothing else this key could collide with. Verified against real Postgres 17 via scripts/rls-test.sh — all 7 prompts RLS integration cases pass, including the concurrent-create cap race. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sonner is now consumed directly by 2 workspaces (apps/web and
packages/ui), so the repo's own catalog convention ("one shared
version per dependency used by 2+ workspaces") applies. Add it to
pnpm-workspace.yaml's catalog at the version both workspaces already
pinned (^2.0.5) and switch both package.json entries to "catalog:".
Resolved version is unchanged (2.0.5); require.resolve confirms both
workspaces still resolve to the identical physical module instance
(single toast store, no dual-instance risk). Closes the two review
threads on PR #135 that flagged the direct pin as convention drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ripple merge from #141 (chat management actions — rename, delete, pin, fork, export). Conflicts resolved: - Migration renumber: master now owns 0015_curvy_molly_hayes + 0016_big_supernaut, so the prompts migration is regenerated fresh as 0017_plain_tempest via `db:generate` against the merged schema (not hand-renamed — a git merge of a hand-renamed migration would silently break the snapshot prevId chain). FORCE ROW LEVEL SECURITY is re-appended by hand (Drizzle only emits ENABLE) and the lower(name) case-insensitive unique index is intact. `drizzle-kit check` passes. - CHANGELOG.md: union, both dated sections kept, newest-first. - rls-test.sh: union of both branches' integration-suite lines (prompts + #141's chat-delete/chat-pinning/fork-chat). - chat-page.tsx: re-seated the `/` prompt-menu wiring — a 4-line additive diff against master, no interaction with #141's sidebar changes. - settings/page.tsx: additive (PromptsSection import + mount). - openapi.json: regenerated via `pnpm --filter api build` rather than hand-merged. - packages/ui/src/components/sonner.tsx: master's wrapper now re-exports `toast` directly; apps/web's own `sonner` catalog entry stands, require.resolve confirms one physical module instance post-merge. Verified post-merge: api + web typecheck clean, api + web lint clean, focused jest (prompts, chats) + vitest (74/74) green, and the full RLS harness (migration changed) — RLS+prompts (7), chats-rls (13), chat-delete (3), chat-pinning (3), fork-chat (6), queue (8), and the e2e suite (36) all pass against a fresh non-superuser-owned Postgres. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ripple merge from #130 (public read-only chat sharing, activating chats.visibility). Conflicts resolved: - Migration renumber: master's #130 took 0017_clammy_clea (the chat-sharing RLS policies), so the prompts migration is regenerated fresh as 0018_mysterious_harrier via `db:generate` against the merged schema (delete + regen, not hand-renamed — same reasoning as the previous 0015→0017 renumber: a git merge of a hand-renamed migration silently breaks the snapshot prevId chain). FORCE ROW LEVEL SECURITY re-appended by hand; the lower(name) case-insensitive unique index and both CHECK constraints are intact. `drizzle-kit check` passes. - CHANGELOG.md: union, both dated sections kept, newest-first (updated the prompt-library entry's migration-number callout to 0018 in the same pass). - rls-test.sh: union of the prompts line + master's new chat-sharing-integration line. - chat-page.tsx / settings/page.tsx: re-checked against master — #130 touched chat-list-sidebar and the new /shared route, zero overlap with the composer/settings hunks; my diff against master is still the same minimal 4-line composer wiring + 1-line settings mount as before. - openapi.json: regenerated via `pnpm --filter api build`. Verified post-merge: api + web typecheck clean, api + web lint clean, focused jest (prompts, chats) + vitest (87/87) green, and the full RLS harness (migration changed) — RLS (13), prompts-rls (7), chat-sharing (15, new from #130), chat-delete (3), chat-pinning (3), fork-chat (6), queue (8), and the e2e suite (46) all pass against a fresh non-superuser-owned Postgres. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ripple merge from #143 (chat search + command palette content search), #128 (stop cancels the durable run), #136 (compaction surfacing UI), and #138 (per-turn usage & cost transparency). Conflicts resolved: - CHANGELOG.md: union, all dated entries kept, newest-first. - rls-test.sh: union of the prompts line + master's new compaction-surfacing and chat-search integration lines. - chat-page.tsx: import-block conflict only — my `/` prompt-menu import/wiring and master's compaction-boundary imports/rendering are in non-overlapping regions of the file (composer footer vs. the message-list boundary marker), so the resolution is a plain union of both import blocks. Verified by grep: promptMenu wiring stays at lines ~185/476/482, CompactionBoundary rendering at ~269/339/450 — no interleaving. - No migration conflict: master tops out at 0017, my 0018 stands untouched. Ran `drizzle-kit check` anyway per instruction — clean. - openapi.json: regenerated via `pnpm --filter api build`, matched the auto-merged version exactly (no diff). Verified post-merge: api + web typecheck clean, api + web lint clean, focused jest (prompts: 5 passed, chats: 90 passed) + vitest (155/155, including the new compaction-checkpoint and command-palette render suites) all green. RLS harness skipped this round — the migration journal did not conflict, so no schema/RLS surface changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ripple merge reconciling prompt-library + templating (#135) against #129 (org units/memberships admin surface), #132 (run-completion notifications), #156 (reasoning persistence), and #161 (available-models API + composer restructuring). - CHANGELOG.md: unioned both sides' 2026-07-05 entries. - apps/api/AGENTS.md: took master's migration-gotchas prose, added the 0021 prompt-library renumbering note. - apps/api/scripts/rls-test.sh: took master's broad `.integration` glob (already covers prompts-rls.integration.spec.ts). - apps/api/src/db/schema/index.ts: export both `./identity` and `./prompts`. - Migrations: renumbered the prompts migration 0018 -> 0021 (master's head is now 0020) via delete + `drizzle-kit generate` against the merged schema, re-appended `FORCE ROW LEVEL SECURITY` for `prompts`; `drizzle-kit check` passes. - apps/web/.../chat-page.tsx: kept master's composer restructuring (#161 model-selector + Send pill, required modelId) and re-attached the prompt-command-menu (`{promptMenu}`) above the composer. - apps/api/openapi.json: took master's version as a placeholder, to be regenerated by `pnpm --filter api build`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerate openapi.json via pnpm --filter api build now that the merge compiles, picking up the prompts routes alongside master's models/ identity surface (per ripple protocol: never hand-merge openapi.json). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Ripple-merged Conflicts & resolutions:
Verification (all green):
All 19 existing review threads were already resolved — nothing further to action there. Pushed as |
Mesa DescriptionTL;DRIntroduces a tenant-isolated prompt library with slash-command ( What changed?Backend & Database (API)
Frontend (Web)
Documentation & Specifications
Description generated by Mesa. Update settings |
Ripple #165 (instance config-as-code) onto the prompt-library stack. Only conflict: apps/api/src/app.module.ts (both branches added an import line at the same position) — kept both PromptsModule and InstanceConfigModule imports; the module's `imports: []` array had already been auto-merged with both entries present. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Rippled Conflicts: one — Everything else ( No new migrations on master since Verification (all green):
Pushed as a plain fast-forward ( Merge commit: c840d93 |
What
Prompt library + templating, carved out of
experiment/overnight-loop-3as an independent PR againstmaster.promptstable (owner-scoped,FORCERLS,UNIQUE(user_id, name), migration0015) +/api/v1/me/promptsCRUD (GET/POST/PATCH/DELETE), mirroring the existing memories pattern.nameis a slug (^[A-Za-z0-9_-]+$, DBCHECK) so/<name>is unambiguous in the composer; a duplicate name per user is a real409(DB unique constraint + a23505-walking catch)./-triggered autocomplete menu in the chat composer to insert them. The composer trigger is^/(\S+)$— a bare/never opens the menu, so a literal/message still sends. Prefix matching is case-insensitive; only bare Enter selects (Shift+Enter still makes a newline). The core composer change is minimal and backward-compatible:PromptInputTextareanow calls a passedonKeyDownfirst and bails if it callspreventDefault.{{placeholder}}variables. Inserting a prompt with placeholders opens a small fill-in dialog (one field per unique placeholder) and substitutes the values into the composer; a plain prompt still inserts directly. Client-only, no schema change. Substitution is a singleString.replace(regex, callback)pass over the original body so a value containing{{x}}is never re-expanded; the regex is ReDoS-safe ({{([^{}]*?)}}); dialog field ids are index-based (a placeholder name can contain spaces, which is invalid in a DOM id).No GitHub issue is associated with this feature in the source partition plan.
Security & tenancy
promptsis a new per-user-scoped table, isolated the same way asmemories/chats: RLS isENABLEd andFORCEd (migration0015hand-appendsFORCE ROW LEVEL SECURITY— Drizzle only emitsENABLE; documented inapps/api/AGENTS.md's migration-gotcha list), and every access goes throughTenantDbService.runAs(userId, …).Negative test brought from the source branch:
apps/api/src/chats/prompts-rls.integration.spec.tsproves a cross-tenant read/update/delete is denied (RLS scopes strictly to the owner), thatUNIQUE(user_id, name)rejects a duplicate name for the same user but allows the same name across different users, and that the name-slug/content-length DBCHECKs hold. Ran against a real non-superuser-owned Postgres viascripts/rls-test.sh— see Verification.Review findings & refactors
scripts/rls-test.shonly invokedchats-rls.integration, so the newprompts-rls.integrationsuite would never have run in CI/local RLS proof despite existing. Added aprompts-rls.integrationinvocation alongside the existing one.PromptsSectionimportedtoastfrom@workspace/ui/components/sonner, but that wrapper only re-exports<Toaster/>(matches upstream shadcn/ui convention —toast()is imported from thesonnerpackage directly in consuming code).apps/webalso didn't havesonneras a direct dependency (onlypackages/uidid) — importing it directly would have been a phantom dependency. Fixed by addingsonner(pinned to the same versionpackages/uiuses — a duplicated pin rather than a newcatalog:entry; the minimal fix, flagging it as a judgment call rather than silently introducing repo-convention drift) as a directapps/webdependency and importingtoastfromsonnerdirectly, per the vendored-primitives convention inpackages/ui'sAGENTS.md("prefer composing in app code over editing [shadcn primitives]"). Confirmed this doesn't silently break toasts:toast()(now resolved fromapps/web) and the mounted<Toaster/>(frompackages/ui) resolve to the exact same physical pnpm-store module instance (require.resolve('sonner')from both workspace roots → identical realpath), so they share the same module-level toast store — no dual-instance silent failure.master's current schema (drizzle-kit generate→0015, sincemasteronly has migrations through0014);drizzle-kit checkpasses on the full chain.apps/api/openapi.jsonregenerated viapnpm --filter api buildrather than hand-merged, per convention.Reference comparison
Open WebUI ships the closest comparable feature (
/slash-command prompt library,backend/open_webui/models/prompts.py+src/lib/components/chat/MessageInput/Commands/Prompts.svelte). Two deliberate deviations, both justified by llame's current single-tenant-per-user posture (no shared workspace prompts yet):commandcolumn is globally unique with a separate access-grants/ACL system layered on top (prompts can be shared workspace-wide or to specific users/groups). llame'spromptsare strictly per-user (UNIQUE(user_id, name), not global) — simpler, and correct for the current scope; sharing prompts at the group/project level is a natural extension once llame's group/project primitives (SPEC §8, v0.5) exist, but isn't needed yet.{{...}}syntax overloads a large set of built-in variables ({{CLIPBOARD}},{{USER_NAME}},{{CURRENT_DATE}},{{TOOL:tool_id}}, etc.) resolved automatically from session/browser context, falling back to a fill-in modal only for variables it doesn't recognize. llame's{{placeholder}}is deliberately just user-defined fill-in-the-blank — no built-in variable resolution. This is the right scope for a first cut: built-in variables like{{CLIPBOARD}}or{{USER_LOCATION}}require browser API access with real privacy tradeoffs (clipboard/geolocation permission prompts) that are out of scope here; the extraction/fill primitives (extractPlaceholders/fillPlaceholders) are already generic enough to grow a built-in-variable resolution pass on top later without a redesign.Verification
pnpm install— clean (addedsonneras a directapps/webdependency).pnpm --filter api typecheck && pnpm --filter web typecheck— clean.pnpm --filter api lint/pnpm --filter web lint(oxlint) — clean.pnpm --filter api exec jest --maxWorkers=1— 20/23 suites, 136 passed / 26 skipped (DB-gated), 0 failed.pnpm --filter web test(vitest) — 10/10 suites, 40/40 passed (includes 6 prompt-matching + 8 templating + 4 prompt-query cases).RLS_TEST_PORT=55461 bash apps/api/scripts/rls-test.sh— full green:chats-rls.integration(13/13),prompts-rls.integration(5/5, cross-tenant denial proven),queue.integration(8/8), auth e2e (36 passed / 3 skipped, live-model-gated).pnpm --filter api build— regeneratedapps/api/openapi.json(238-line additive diff, matches the expected prompts routes).Summary by cubic
Adds a per-user prompt library with
/insertion and{{placeholder}}templating to speed up prompt reuse. Registers a standalonePromptsModule, hardens API writes, makes the composer IME-safe, and now coexists withInstanceConfigModuleinapp.module.ts.New Features
promptstable (owner-scoped, FORCE RLS, case-insensitiveUNIQUE(user_id, lower(name)); migration 0021) with/api/v1/me/promptsCRUD; DTOs trimcontentbefore length checks; per-user cap enforced via a 64‑bit advisory lock; OpenAPI regenerated;PromptsModuleadded and imported alongsideInstanceConfigModule./autocomplete in the composer (^/(\S+)$, case-insensitive prefix; bare/doesn’t open; Enter selects; Shift+Enter newline).PromptInputTextareacalls a passedonKeyDownfirst; Enter handling is IME-safe. Client-side limits match the API (64‑char name, trimmed content length) with accurate error toasts.{{placeholder}}variables with a fill dialog; single-pass substitution; Enter advances fields and only the last submits. Tests cover matching, templating, and RLS.Migration
sonneris in the workspacecatalog:and used by bothapps/webandpackages/ui.Written for commit c840d93. Summary will update on new commits.