diff --git a/frontend/src/api/snippets.ts b/frontend/src/api/snippets.ts index 26c9bcd..0fae70b 100644 --- a/frontend/src/api/snippets.ts +++ b/frontend/src/api/snippets.ts @@ -20,9 +20,13 @@ export interface SnippetFields { path: string; symbol: string; locations: SnippetLocation[]; - /** Ids of the snippets folded into this one by merge, oldest first. Read-only: - * merge is the only thing that adds to it, and an edit carries it forward. */ - merged_from: number[]; + /** The snippets folded into this one by merge, oldest first. Read-only: merge + * is the only thing that adds to it, and an edit carries it forward. + * + * Each entry records what THAT source contributed — never what the survivor + * already had — which is what lets un-merge subtract exactly. An entry with + * no `locations`/`tags` predates that attribution and cannot be un-merged. */ + merged_from: { id: number; locations?: SnippetLocation[]; tags?: string[] }[]; code: string; } @@ -183,6 +187,16 @@ export async function verifySnippet( return apiPost(`/api/snippets/${id}/verify`, verdict); } +/** Pull one source back out of a merged survivor: restores it and strips exactly + * what it contributed. Also repairs a half-undone merge — a source restored + * from the trash by hand leaves the survivor still claiming its call sites. */ +export async function unmergeSnippet( + survivorId: number, + sourceId: number, +): Promise<{ survivor: Snippet; restored: Snippet | null }> { + return apiPost(`/api/snippets/${survivorId}/unmerge`, { source_id: sourceId }); +} + /** 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( diff --git a/frontend/src/views/SnippetDetailView.vue b/frontend/src/views/SnippetDetailView.vue index 3d72f92..82bea87 100644 --- a/frontend/src/views/SnippetDetailView.vue +++ b/frontend/src/views/SnippetDetailView.vue @@ -1,7 +1,12 @@