feat(scribe): snippet management UI + REST routes; embed snippets on create
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 23s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 48s
CI & Build / Build & push image (push) Successful in 1m4s

Step 6 of the Drafter recall milestone (#227): a human-facing surface for
the reusable-code snippets that agents record via MCP, plus the REST API
behind it. Backend embeds snippets inline on create/update so they're
recallable immediately, not only after a restart.

Backend:
- routes/snippets.py: GET/POST /api/snippets, GET/PATCH/DELETE
  /api/snippets/<id>. Share-aware per rule #78 (get_note_for_user +
  can_write_note), writes performed as the owner; list owner-scoped —
  mirrors routes/notes.py. Registered in app.py.
- services/snippets.py: embed on create/update via a _embed_snippet
  fire-and-forget helper, covering BOTH the MCP tool and the REST route
  by construction. A snippet's value is immediate recall, so it can't wait
  for the startup-only backfill (see issue: MCP create path doesn't embed
  inline for notes/tasks generally).
- tests/test_routes_snippets.py: structural registration + handler/service
  contract + PATCH-field ↔ update_snippet-kwarg parity (rule #33).

Frontend (Vue 3 + TS):
- api/snippets.ts: typed client, modeled on api/systems.ts.
- views: SnippetListView (search, skeleton/empty/error states),
  SnippetDetailView (read + copy-to-clipboard, ConfirmDialog delete),
  SnippetEditorView (create/edit all fields, Ctrl/Cmd+S, Esc, autofocus,
  validation). v1 quality per rules #24/#27.
- router: /snippets, /snippets/new, /snippets/:id, /snippets/:id/edit.
- NoteType union widened to include 'snippet'; Snippets nav link added to
  AppHeader (desktop pill bar + mobile menu).
- Design system: Moss --color-action-primary for action buttons, accent
  --color-primary reserved for tags/brand (Hybrid rule); focus rings;
  JetBrains Mono for code/name/signature/location.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pa2EsuB54BuWQ8GfJq9c7t
This commit is contained in:
2026-07-25 14:16:02 -04:00
parent 0ea3bff797
commit d257c0fd67
11 changed files with 1355 additions and 4 deletions
+87
View File
@@ -0,0 +1,87 @@
import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";
/** Structured fields parsed out of a snippet note (mirrors the backend
* `parse_snippet_fields` — see services/snippets.py). */
export interface SnippetFields {
name: string;
when_to_use: string;
signature: string;
language: string;
repo: string;
path: string;
symbol: string;
code: string;
}
/** A full snippet record: the note dict plus the parsed `snippet` sub-object,
* as returned by the backend `snippet_to_dict`. */
export interface Snippet {
id: number;
title: string;
body: string;
tags: string[];
note_type: string;
project_id: number | null;
permission?: string;
created_at: string;
updated_at: string;
snippet: SnippetFields;
}
/** Lightweight list item from the knowledge preview feed. Note: the `snippet`
* field here is a truncated *body preview* (the knowledge feed's naming), not
* the parsed fields above. */
export interface SnippetListItem {
id: number;
title: string;
tags: string[];
note_type: string;
snippet: string;
created_at: string;
updated_at: string;
}
/** Create/update payload — discrete fields the backend serializes into the
* note (title/body/tags). Empty strings are applied; omitted keys are left
* unchanged on update. */
export interface SnippetInput {
name: string;
code: string;
language?: string;
signature?: string;
when_to_use?: string;
repo?: string;
path?: string;
symbol?: string;
tags?: string[];
project_id?: number | null;
}
export async function listSnippets(
params: { q?: string; tag?: string } = {},
): Promise<{ snippets: SnippetListItem[]; total: number }> {
const qs = new URLSearchParams();
if (params.q) qs.set("q", params.q);
if (params.tag) qs.set("tag", params.tag);
const query = qs.toString();
return apiGet(`/api/snippets${query ? `?${query}` : ""}`);
}
export async function getSnippet(id: number): Promise<Snippet> {
return apiGet(`/api/snippets/${id}`);
}
export async function createSnippet(data: SnippetInput): Promise<Snippet> {
return apiPost("/api/snippets", data);
}
export async function updateSnippet(
id: number,
data: Partial<SnippetInput>,
): Promise<Snippet> {
return apiPatch(`/api/snippets/${id}`, data);
}
export async function deleteSnippet(id: number): Promise<void> {
return apiDelete(`/api/snippets/${id}`);
}