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
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:
@@ -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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user