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:
@@ -23,6 +23,7 @@ changing any caller (MCP tool, REST route, UI).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from scribe.services import knowledge as knowledge_svc
|
||||
@@ -32,6 +33,25 @@ SNIPPET_NOTE_TYPE = "snippet"
|
||||
SNIPPET_TAG = "snippet"
|
||||
|
||||
|
||||
def _embed_snippet(note) -> None:
|
||||
"""Fire-and-forget embedding refresh for a snippet.
|
||||
|
||||
A snippet's whole value is *immediate* recall — it must join the semantic /
|
||||
auto-inject pool the moment it's recorded, not wait for the startup backfill.
|
||||
Unlike a plain note (embedded at the REST-route boundary only, so its MCP
|
||||
create path defers to restart-backfill), a snippet is recorded primarily via
|
||||
MCP, so we embed here in the service — covering BOTH the MCP tool and the
|
||||
REST route by construction. Mirrors the route pattern: fire-and-forget,
|
||||
text = title + body. Import lazily so the pure serialize/parse helpers can be
|
||||
imported without pulling in the embedding model.
|
||||
"""
|
||||
text = f"{note.title}\n{note.body}".strip() if note.body else (note.title or "")
|
||||
if not text:
|
||||
return
|
||||
from scribe.services.embeddings import upsert_note_embedding
|
||||
asyncio.create_task(upsert_note_embedding(note.id, note.user_id, text))
|
||||
|
||||
|
||||
# --- serialize: structured fields -> note (title/body/tags) ------------------
|
||||
|
||||
def compose_title(name: str, when_to_use: str = "") -> str:
|
||||
@@ -164,8 +184,9 @@ async def create_snippet(
|
||||
tags: list[str] | None = None,
|
||||
project_id: int | None = None,
|
||||
):
|
||||
"""Create a snippet note. Returns the created Note."""
|
||||
return await notes_svc.create_note(
|
||||
"""Create a snippet note (embedded on create for immediate recall). Returns
|
||||
the created Note."""
|
||||
note = await notes_svc.create_note(
|
||||
user_id,
|
||||
title=compose_title(name, when_to_use),
|
||||
body=compose_body(
|
||||
@@ -176,6 +197,8 @@ async def create_snippet(
|
||||
tags=compose_tags(language, tags),
|
||||
project_id=project_id,
|
||||
)
|
||||
_embed_snippet(note)
|
||||
return note
|
||||
|
||||
|
||||
async def get_snippet(user_id: int, snippet_id: int):
|
||||
@@ -254,4 +277,8 @@ async def update_snippet(
|
||||
if project_id is not None:
|
||||
fields["project_id"] = project_id
|
||||
|
||||
return await notes_svc.update_note(user_id, snippet_id, **fields)
|
||||
updated = await notes_svc.update_note(user_id, snippet_id, **fields)
|
||||
if updated is not None:
|
||||
# Title/body changed → refresh the embedding so recall reflects the edit.
|
||||
_embed_snippet(updated)
|
||||
return updated
|
||||
|
||||
Reference in New Issue
Block a user