Files
shared-memory/apps/web/app/(authed)/snippets/new/page.tsx
T
shadowdaoandClaude Opus 4.7 fcc8708dd5 feat(snippets): named reusable artifacts with scope mirroring memories
Snippets are exact-name-keyed templates (PR formats, checklists, style
rules) — prescriptive artifacts the user wants applied consistently.
Distinct from memories, which are searched descriptive prose.

- Migration 0002 adds scope/project_id/deleted_at to snippets, partial
  unique indexes per scope, and a scope/project CHECK constraint
  mirroring memories_scope_project_chk.
- New @shared-memory/schemas: SnippetName/Put/Get/List/Delete inputs
  with shared scope-project refinement.
- lib/snippets.ts: get/put/list/softDelete helpers used by both the
  Web Server Actions and the MCP tool handlers.
- Four new MCP tools (snippet.put/get/list/delete) with directive
  descriptions contrasting against memory.* (exact-name lookup vs
  search; templates vs facts).
- /snippets pages: list, new, [name] with edit + delete and a
  scope-picker when a name lives in multiple scopes.
- Nav: Snippets link between Memories and Projects.

Design note: when a name exists in both user and project scope,
snippet.get without an explicit scope prefers project (if project key
given) then falls back to user. The detail page shows a picker when
the name is ambiguous and no scope query param was supplied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 06:48:00 -07:00

138 lines
4.4 KiB
TypeScript

import Link from "next/link";
import { desc, eq } from "drizzle-orm";
import { auth } from "@/auth";
import { db } from "@/lib/db/client";
import { projects } from "@/lib/db/schema";
import { createSnippetAction } from "@/lib/snippet-actions";
import { Container, PageHeader } from "@/app/_components/ui/container";
import { Card, CardBody } from "@/app/_components/ui/card";
import { Input, Textarea, Label } from "@/app/_components/ui/input";
import { Button } from "@/app/_components/ui/button";
export const dynamic = "force-dynamic";
export default async function NewSnippetPage({
searchParams,
}: {
searchParams: Promise<{ scope?: string; project?: string; name?: string }>;
}) {
const session = await auth();
const userId = session!.user.id;
const params = await searchParams;
const initialScope = params.scope === "project" ? "project" : "user";
const initialProject = params.project ?? "";
const initialName = params.name ?? "";
const projectList = await db
.select({ key: projects.key, displayName: projects.displayName })
.from(projects)
.where(eq(projects.userId, userId))
.orderBy(desc(projects.updatedAt))
.limit(50);
return (
<Container className="pt-6 max-w-2xl">
<PageHeader
title="New snippet"
description="Name it something stable — that name is the lookup key from now on."
/>
<Card>
<CardBody>
<form action={createSnippetAction} className="space-y-4">
<div>
<Label htmlFor="name" hint="alphanumerics + ._-/">
Name
</Label>
<Input
id="name"
name="name"
required
defaultValue={initialName}
placeholder="pr-description-format"
className="mt-1 font-mono"
/>
</div>
<div>
<Label htmlFor="scope">Scope</Label>
<select
id="scope"
name="scope"
defaultValue={initialScope}
className="mt-1 h-9 px-2 rounded-md bg-surface-1 border border-border text-fg text-sm w-full"
>
<option value="user">User applies everywhere (default)</option>
<option value="project">Project tied to a specific repo</option>
</select>
</div>
<div>
<Label htmlFor="project" hint="Required for project scope">
Project key
</Label>
<Input
id="project"
name="project"
defaultValue={initialProject}
placeholder="repo name, slug, or any stable string"
list="project-list"
className="mt-1"
/>
{projectList.length > 0 ? (
<datalist id="project-list">
{projectList.map((p) => (
<option key={p.key} value={p.key}>
{p.displayName ?? p.key}
</option>
))}
</datalist>
) : null}
</div>
<div>
<Label htmlFor="description" hint="Optional">
Description
</Label>
<Input
id="description"
name="description"
placeholder="When should this template be used?"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="body">Body</Label>
<Textarea
id="body"
name="body"
required
rows={14}
placeholder="The full template, format, or checklist…"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="tags" hint="comma- or space-separated">
Tags
</Label>
<Input id="tags" name="tags" placeholder="format, review, …" className="mt-1" />
</div>
<div className="flex justify-end gap-2 pt-2">
<Link href="/snippets" className="no-underline">
<Button type="button" variant="secondary">
Cancel
</Button>
</Link>
<Button type="submit">Save snippet</Button>
</div>
</form>
</CardBody>
</Card>
</Container>
);
}