feat(snippets): a snippet has notes; when_to_use is the situation it is ranked on (#4378)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 34s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 34s
A snippet had no field for prose, so what a session learned about one went into when_to_use — the trigger joined onto every chunk it is embedded as. A sweep found write-ups of up to 3 KB there, headings and all. - notes: stored after the code under `## Notes`, parsed back from the body, carried by every path that rebuilds it (update, merge, un-merge). A snippet with no notes composes the body it always did. - create/update_snippet (MCP) take notes and return trigger_advice when when_to_use is long, headed or multi-paragraph. Advice, not a refusal. - Tool docs, the reusing-code skill and the editor hint describe the trigger as the situation and point the explanation at notes. - Editor gains a Notes field; the detail view renders it as markdown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,9 @@ export interface SnippetFields {
|
||||
* no `locations`/`tags` predates that attribution and cannot be un-merged. */
|
||||
merged_from: { id: number; locations?: SnippetLocation[]; tags?: string[] }[];
|
||||
code: string;
|
||||
/** Free text that is not the situation — why, history, caveats (#4378).
|
||||
* Kept out of `when_to_use`, which the snippet is ranked on. */
|
||||
notes: string;
|
||||
}
|
||||
|
||||
/** A full snippet record: the note dict plus the parsed `snippet` sub-object,
|
||||
@@ -110,6 +113,7 @@ export interface SnippetInput {
|
||||
language?: string;
|
||||
signature?: string;
|
||||
when_to_use?: string;
|
||||
notes?: string;
|
||||
locations?: SnippetLocation[];
|
||||
tags?: string[];
|
||||
project_id?: number | null;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@/api/snippets";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import ConfirmDialog from "@/components/ConfirmDialog.vue";
|
||||
import { renderMarkdown } from "@/utils/markdown";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -184,6 +185,11 @@ async function confirmDelete() {
|
||||
<pre><code>{{ snippet.snippet.code }}</code></pre>
|
||||
</div>
|
||||
|
||||
<section v-if="snippet.snippet.notes" class="notes">
|
||||
<h2 class="notes-heading">Notes</h2>
|
||||
<div class="prose" v-html="renderMarkdown(snippet.snippet.notes)" />
|
||||
</section>
|
||||
|
||||
<div v-if="snippet.tags.length" class="tag-row">
|
||||
<span v-for="t in snippet.tags" :key="t" class="tag-pill">{{ t }}</span>
|
||||
</div>
|
||||
@@ -343,6 +349,17 @@ async function confirmDelete() {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.notes {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
.notes-heading {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--fs-text-tertiary);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
border: 1px solid var(--fs-border-color);
|
||||
border-radius: var(--fs-radius-lg);
|
||||
|
||||
@@ -29,6 +29,7 @@ interface FormState {
|
||||
language: string;
|
||||
signature: string;
|
||||
when_to_use: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
const blankLocation = (): SnippetLocation => ({ repo: "", path: "", symbol: "" });
|
||||
@@ -39,6 +40,7 @@ const form = ref<FormState>({
|
||||
language: "",
|
||||
signature: "",
|
||||
when_to_use: "",
|
||||
notes: "",
|
||||
});
|
||||
// A snippet that unified several one-offs carries several locations; a fresh one
|
||||
// starts with a single blank row.
|
||||
@@ -126,6 +128,7 @@ async function load() {
|
||||
language: f.language,
|
||||
signature: f.signature,
|
||||
when_to_use: f.when_to_use,
|
||||
notes: f.notes ?? "",
|
||||
};
|
||||
locations.value = f.locations?.length
|
||||
? f.locations.map((l) => ({ ...l }))
|
||||
@@ -169,6 +172,7 @@ async function save() {
|
||||
language: form.value.language.trim(),
|
||||
signature: form.value.signature.trim(),
|
||||
when_to_use: form.value.when_to_use.trim(),
|
||||
notes: form.value.notes.trim(),
|
||||
locations: cleanLocations(),
|
||||
tags: parseTags(),
|
||||
project_id: projectId.value,
|
||||
@@ -243,7 +247,10 @@ function cancel() {
|
||||
placeholder="Debounce a reactive ref that updates too often"
|
||||
@keydown.escape="cancel"
|
||||
/>
|
||||
<p class="hint">Shown in the recall menu — keep it sharp.</p>
|
||||
<p class="hint">
|
||||
The situation it is for, in a sentence or two — the snippet is ranked
|
||||
on this. Why it's shaped this way belongs in Notes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
@@ -303,6 +310,18 @@ function cancel() {
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="sn-notes">Notes</label>
|
||||
<textarea
|
||||
id="sn-notes"
|
||||
v-model="form.notes"
|
||||
class="fs-input input"
|
||||
rows="5"
|
||||
placeholder="Why it's shaped this way, what it replaced, caveats…"
|
||||
></textarea>
|
||||
<p class="hint">Markdown. Shown with the snippet, after the code.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="sn-tags">Tags</label>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user