feat(scribe): snippet merge UI — multi-select merge, repeatable locations
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Build & push image (push) Successful in 1m2s

Step 3 of the snippet-merge milestone (#231): the human surfaces for
merge + multi-location, at v1 quality.

Frontend:
- SnippetListView: a Select mode (checkbox on each card) → a sticky action
  bar → a merge modal that lets you pick which selected snippet is the
  canonical (the others fold in and go to trash). Accent border on selected
  cards, Moss action buttons (Hybrid rule).
- SnippetEditorView: the single Location fieldset becomes a repeatable
  locations list (add/remove rows), so editing a merged snippet no longer
  collapses its call sites — no data loss. Sends `locations`.
- SnippetDetailView: renders every location (Location vs Locations label).
- api/snippets.ts: SnippetLocation type, `locations` on fields/input,
  mergeSnippets().

Backend (editor enablement):
- create_snippet service + POST route accept an optional `locations` list;
  PATCH route forwards `locations` — so the editor's location list works
  uniformly on create and edit. Single repo/path/symbol remain the
  one-location shorthand (MCP create contract unchanged).

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 17:22:01 -04:00
parent 85625de394
commit 7a81b7333e
6 changed files with 414 additions and 48 deletions
+21 -4
View File
@@ -1,7 +1,16 @@
import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";
/** One canonical location of a reusable thing. A snippet that unifies several
* one-offs carries several — one per call site. */
export interface SnippetLocation {
repo: string;
path: string;
symbol: string;
}
/** Structured fields parsed out of a snippet note (mirrors the backend
* `parse_snippet_fields` — see services/snippets.py). */
* `parse_snippet_fields` — see services/snippets.py). `repo`/`path`/`symbol`
* mirror the first location for back-compat; `locations` is the full list. */
export interface SnippetFields {
name: string;
when_to_use: string;
@@ -10,6 +19,7 @@ export interface SnippetFields {
repo: string;
path: string;
symbol: string;
locations: SnippetLocation[];
code: string;
}
@@ -50,9 +60,7 @@ export interface SnippetInput {
language?: string;
signature?: string;
when_to_use?: string;
repo?: string;
path?: string;
symbol?: string;
locations?: SnippetLocation[];
tags?: string[];
project_id?: number | null;
}
@@ -85,3 +93,12 @@ export async function updateSnippet(
export async function deleteSnippet(id: number): Promise<void> {
return apiDelete(`/api/snippets/${id}`);
}
/** Unify `sourceIds` into the canonical snippet `targetId`. Returns the merged
* survivor plus `merged_ids` — the sources actually folded in and trashed. */
export async function mergeSnippets(
targetId: number,
sourceIds: number[],
): Promise<Snippet & { merged_ids: number[] }> {
return apiPost(`/api/snippets/${targetId}/merge`, { source_ids: sourceIds });
}