Skip to content

feat: prompt library + {{placeholder}} templating - #135

Open
leon0399 wants to merge 18 commits into
masterfrom
stack/split-prompt-library
Open

feat: prompt library + {{placeholder}} templating#135
leon0399 wants to merge 18 commits into
masterfrom
stack/split-prompt-library

Conversation

@leon0399

@leon0399 leon0399 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

Prompt library + templating, carved out of experiment/overnight-loop-3 as an independent PR against master.

  • A prompts table (owner-scoped, FORCE RLS, UNIQUE(user_id, name), migration 0015) + /api/v1/me/prompts CRUD (GET/POST/PATCH/DELETE), mirroring the existing memories pattern. name is a slug (^[A-Za-z0-9_-]+$, DB CHECK) so /<name> is unambiguous in the composer; a duplicate name per user is a real 409 (DB unique constraint + a 23505-walking catch).
  • A Settings section to manage saved prompts, and a /-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: PromptInputTextarea now calls a passed onKeyDown first and bails if it calls preventDefault.
  • Prompt templating: saved prompts can carry {{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 single String.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

prompts is a new per-user-scoped table, isolated the same way as memories/chats: RLS is ENABLEd and FORCEd (migration 0015 hand-appends FORCE ROW LEVEL SECURITY — Drizzle only emits ENABLE; documented in apps/api/AGENTS.md's migration-gotcha list), and every access goes through TenantDbService.runAs(userId, …).

Negative test brought from the source branch: apps/api/src/chats/prompts-rls.integration.spec.ts proves a cross-tenant read/update/delete is denied (RLS scopes strictly to the owner), that UNIQUE(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 DB CHECKs hold. Ran against a real non-superuser-owned Postgres via scripts/rls-test.sh — see Verification.

Review findings & refactors

  • Harness gap (self-caught during carving): scripts/rls-test.sh only invoked chats-rls.integration, so the new prompts-rls.integration suite would never have run in CI/local RLS proof despite existing. Added a prompts-rls.integration invocation alongside the existing one.
  • Real type error (self-caught via typecheck): PromptsSection imported toast from @workspace/ui/components/sonner, but that wrapper only re-exports <Toaster/> (matches upstream shadcn/ui convention — toast() is imported from the sonner package directly in consuming code). apps/web also didn't have sonner as a direct dependency (only packages/ui did) — importing it directly would have been a phantom dependency. Fixed by adding sonner (pinned to the same version packages/ui uses — a duplicated pin rather than a new catalog: entry; the minimal fix, flagging it as a judgment call rather than silently introducing repo-convention drift) as a direct apps/web dependency and importing toast from sonner directly, per the vendored-primitives convention in packages/ui's AGENTS.md ("prefer composing in app code over editing [shadcn primitives]"). Confirmed this doesn't silently break toasts: toast() (now resolved from apps/web) and the mounted <Toaster/> (from packages/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.
  • Migration regenerated fresh against master's current schema (drizzle-kit generate0015, since master only has migrations through 0014); drizzle-kit check passes on the full chain.
  • apps/api/openapi.json regenerated via pnpm --filter api build rather 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):

  • Scope/access model. Open WebUI's command column 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's prompts are 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.
  • Variable syntax. Open WebUI's {{...}} 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 (added sonner as a direct apps/web dependency).
  • 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 — regenerated apps/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 standalone PromptsModule, hardens API writes, makes the composer IME-safe, and now coexists with InstanceConfigModule in app.module.ts.

  • New Features

    • API: prompts table (owner-scoped, FORCE RLS, case-insensitive UNIQUE(user_id, lower(name)); migration 0021) with /api/v1/me/prompts CRUD; DTOs trim content before length checks; per-user cap enforced via a 64‑bit advisory lock; OpenAPI regenerated; PromptsModule added and imported alongside InstanceConfigModule.
    • Web: Settings UI to create/edit/delete prompts; / autocomplete in the composer (^/(\S+)$, case-insensitive prefix; bare / doesn’t open; Enter selects; Shift+Enter newline). PromptInputTextarea calls a passed onKeyDown first; Enter handling is IME-safe. Client-side limits match the API (64‑char name, trimmed content length) with accurate error toasts.
    • Templating: {{placeholder}} variables with a fill dialog; single-pass substitution; Enter advances fields and only the last submits. Tests cover matching, templating, and RLS.
  • Migration

    • Run DB migration 0021.
    • Install deps; sonner is in the workspace catalog: and used by both apps/web and packages/ui.

Written for commit c840d93. Summary will update on new commits.

Review in cubic

leon0399 and others added 3 commits July 5, 2026 23:35
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>
@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@leon0399, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bcb6292-9ccc-4ee0-8a81-00291318a9a7

📥 Commits

Reviewing files that changed from the base of the PR and between cd45c7e and c840d93.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • CHANGELOG.md
  • apps/api/AGENTS.md
  • apps/api/openapi.json
  • apps/api/scripts/rls-test.sh
  • apps/api/src/app.module.ts
  • apps/api/src/db/migrations/0021_dark_lady_mastermind.sql
  • apps/api/src/db/migrations/meta/0021_snapshot.json
  • apps/api/src/db/migrations/meta/_journal.json
  • apps/api/src/db/schema/index.ts
  • apps/api/src/db/schema/prompts.ts
  • apps/api/src/prompts/dto/prompts.dto.spec.ts
  • apps/api/src/prompts/dto/prompts.dto.ts
  • apps/api/src/prompts/me-prompts.controller.ts
  • apps/api/src/prompts/prompts-repository.ts
  • apps/api/src/prompts/prompts-rls.integration.spec.ts
  • apps/api/src/prompts/prompts.module.ts
  • apps/web/app/(chat)/components/chat-page.tsx
  • apps/web/app/(chat)/components/prompt-command-menu.tsx
  • apps/web/app/(chat)/settings/components/prompts-section.tsx
  • apps/web/app/(chat)/settings/page.tsx
  • apps/web/components/components/ai/prompt-input.tsx
  • apps/web/lib/services/prompts/matching.test.ts
  • apps/web/lib/services/prompts/matching.ts
  • apps/web/lib/services/prompts/queries.test.ts
  • apps/web/lib/services/prompts/queries.ts
  • apps/web/lib/services/prompts/templating.test.ts
  • apps/web/lib/services/prompts/templating.ts
  • apps/web/package.json
  • docs/superpowers/specs/2026-07-03-prompt-library-design.md
  • docs/superpowers/specs/2026-07-03-prompt-templating-design.md
  • packages/ui/package.json
  • pnpm-workspace.yaml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stack/split-prompt-library

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

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread apps/web/package.json Outdated
Comment thread apps/api/src/prompts/me-prompts.controller.ts
Comment thread apps/api/src/prompts/me-prompts.controller.ts
Comment thread apps/web/app/(chat)/components/prompt-command-menu.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread apps/api/src/prompts/dto/prompts.dto.ts
Comment thread apps/api/src/prompts/me-prompts.controller.ts
Comment thread docs/superpowers/specs/2026-07-03-prompt-library-design.md Outdated
Comment thread apps/api/src/chats/chats.module.ts Outdated
Comment thread apps/api/src/prompts/prompts-repository.ts
Comment thread apps/web/app/(chat)/settings/components/prompts-section.tsx Outdated
Comment thread apps/web/app/(chat)/settings/components/prompts-section.tsx Outdated
Comment thread apps/web/app/(chat)/settings/components/prompts-section.tsx Outdated
Comment thread apps/web/app/(chat)/settings/components/prompts-section.tsx
Comment thread apps/web/app/(chat)/components/prompt-command-menu.tsx Outdated
leon0399 and others added 6 commits July 6, 2026 00:36
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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 17 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/api/src/prompts/prompts-repository.ts Outdated
leon0399 and others added 4 commits July 6, 2026 12:56
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>
leon0399 and others added 4 commits July 6, 2026 17:43
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>
@leon0399

Copy link
Copy Markdown
Owner Author

Ripple-merged origin/master (through #129 org-units, #132 run-notifications, #156 reasoning, #161 available-models) to resolve the conflict against master.

Conflicts & resolutions:

  • 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, no separate step needed).
  • apps/api/src/db/schema/index.ts — export both ./identity and ./prompts.
  • apps/api/src/db/migrations/** — renumbered the prompts migration 00180021 (master's head is now 0020): deleted the old migration file + its meta snapshot/journal entry, regenerated via drizzle-kit generate against the merged schema, re-appended FORCE ROW LEVEL SECURITY for prompts. drizzle-kit check passes.
  • apps/web/app/(chat)/components/chat-page.tsx — kept master's composer restructuring (feat: available models API + composer model selector #161 model-selector + Send pill, required modelId) and re-attached the prompt-command-menu ({promptMenu}) above it.
  • apps/api/openapi.json — regenerated via pnpm --filter api build (never hand-merged).

Verification (all green):

  • pnpm --filter api build / pnpm --filter web build — clean
  • pnpm --filter api test — 27/27 suites, 214 tests
  • pnpm --filter web test — 40/40 files, 217 tests
  • pnpm --filter api typecheck / pnpm --filter web typecheck — clean
  • pnpm --filter api lint / pnpm --filter web lint — clean
  • pnpm format:check — clean
  • apps/api/scripts/rls-test.sh — full green run (111 integration + 61/64 e2e, 3 intentionally skipped), including prompts-rls.integration.spec.ts under FORCE RLS with the renumbered migration

All 19 existing review threads were already resolved — nothing further to action there.

Pushed as 9a821b9.

@mesa-dot-dev

mesa-dot-dev Bot commented Jul 10, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

Introduces a tenant-isolated prompt library with slash-command (/) autocomplete in the composer and client-side {{placeholder}} templating with interactive fill-in dialogs.

What changed?

Backend & Database (API)

  • Database Schema & Migration: Added the prompts table with automatic IDs, user scoping, size validations, and a case-insensitive UNIQUE(user_id, lower(name)) constraint (Migration 0021). Row Level Security (RLS) is enabled and forced (FORCE ROW LEVEL SECURITY).
  • NestJS Prompts Module:
    • Registered PromptsModule in app.module.ts.
    • Added MePromptsController at /api/v1/me/prompts supporting GET, POST, PATCH, and DELETE.
    • Added PromptsRepository implementing database actions with transaction-level Postgres advisory locks (lockUserForCreate) to serialize concurrent creations and enforce prompt limits.
    • Defined input-trimmed CreatePromptDto, UpdatePromptDto, and PromptResponse with validation and documentation specs.
  • RLS & Validation Tests: Introduced comprehensive integration tests in prompts-rls.integration.spec.ts confirming cross-tenant isolation, unique constraints, and check constraint validation. Included the suite in the rls-test.sh test run.
  • OpenAPI: Regenerated apps/api/openapi.json to expose the new prompts endpoints.

Frontend (Web)

  • Composer Autocomplete:
    • Added a keyboard-navigable /-triggered prompt autocomplete dropdown inside chat-page.tsx using a custom usePromptMenu hook in prompt-command-menu.tsx.
    • Modified PromptInputTextarea to accept and execute an optional onKeyDown prop first, enabling custom keyboard interception and IME safety.
  • Prompt Templating:
    • Added regex-safe ({{([^{}]*?)}}) placeholder extraction (extractPlaceholders) and single-pass variable injection (fillPlaceholders) in templating.ts to prevent recursive expansion.
    • Created the interactive <FillPromptDialog /> to prompt users to fill in placeholder values before inserting the compiled string.
  • Settings UI: Created <PromptsSection /> to support adding, editing, and deleting saved prompts with name slug and trimmed body validation.
  • State Management & Queries: Integrated TanStack Query hooks (queries.ts) with cache invalidation for handling prompt CRUD.
  • Dependencies: Added sonner package to workspace catalog and apps/web directly to avoid phantom dependency issues, resolving toasts cleanly.

Documentation & Specifications

  • Added design specifications: 2026-07-03-prompt-library-design.md and 2026-07-03-prompt-templating-design.md.
  • Updated CHANGELOG.md with features and apps/api/AGENTS.md with database migration RLS details.

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>
@leon0399

Copy link
Copy Markdown
Owner Author

Rippled origin/master through #165 (instance config-as-code, cd45c7e) — this branch was previously rippled only through #161.

Conflicts: one — apps/api/src/app.module.ts (both sides added an import line at the same position: PromptsModule vs InstanceConfigModule). Kept both imports; the imports: [] array itself had already auto-merged with both modules present, so no other change was needed there.

Everything else (CHANGELOG.md, apps/api/AGENTS.md, pnpm-lock.yaml, and all the #165-touched files — app.setup.ts, main.ts, chat-loop.service.ts, models.service.ts, run-dispatch.service.ts, run-queues.ts, runs-worker.service.ts, generate-openapi.ts — plus the new instance-config/ module) auto-merged cleanly; master's ConfigServiceInstanceConfigService swap in constructors/specs came through as-is.

No new migrations on master since 0020; this branch's prompt-library migration stays 0021 and drizzle-kit check's implicit journal-chain check passed. apps/api/openapi.json regenerated identical (no diff) after the merge, so nothing extra to commit there.

Verification (all green):

  • pnpm --filter api build — exit 0 (openapi.json regenerated, no diff)
  • pnpm --filter api test --maxWorkers=2 — exit 0 (30 suites / 275 tests, 111 skipped RLS/integration)
  • pnpm --filter api typecheck — exit 0
  • pnpm --filter web test — exit 0 (40 suites / 217 tests)
  • pnpm --filter web typecheck — exit 0
  • pnpm --filter api lint (oxlint, dist removed first) — exit 0
  • pnpm --filter web lint — exit 0
  • RLS_TEST_PORT=55475 bash apps/api/scripts/rls-test.sh — exit 0 (14 integration suites / 111 tests, active-runs integration / 3 tests, 7 e2e suites / 61 tests)

Pushed as a plain fast-forward (9a821b9..c840d93), no rebase/force-push.

Merge commit: c840d93

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.

1 participant