---
title: Quick Replies (manage your snippet library)
---

# Quick Replies (manage your snippet library)

## What it is

**Quick Replies** is a dedicated virtual project in Omniscio that holds your library of saved, reusable quick replies — the ones you can drop into any session's composer via the Alt+S picker, wire to an Auto-reply rule, or cite from an Automation's `auto_respond` action. Each entry is one of three types: a **quick reply** (a label + body text + an optional auto-submit flag), a **divider** (a label-only row used to visually group items — purely visual, no group semantics), or a **folder** (a nameable container that can hold quick replies, dividers, and other folders — unlimited nesting depth). Folder names may repeat freely; quick-reply labels are globally unique across every folder.

## Where to find it

Quick Replies appears in the sidebar alongside the other Integrations virtual projects (Automations, Recipes, Skills, etc.). Opening it shows a sub-sidebar with three tab rows and a main panel whose contents swap based on the active tab:

- **Settings** — the per-feature toggles: AI Suggestions on/off, the Suggestion Model dropdown, Quick Reply Button visibility, Quick Reply Message text, Quick Reply Hotkey. This tab is the **single home** for these settings as of 2026-05-20. (Prior to that, an equivalent panel also lived under main Settings → Quick Replies; the panel was removed and right-click "Edit in Settings" on a snippet now routes into this VP tab, switches to Quick replies, and opens the inline editor on the target row.)
- **Quick replies** — your quick-reply + divider + folder library: a sticky toolbar pinned at the top of the panel ("Quick replies" title + count, with **+ Folder**, **+ Divider**, and **+ Add reply** buttons that prepend new items to the current level, plus the library-level **Export**, **Share**, and **Import** actions), a draggable tree with per-quick-reply usage counts, folder rows that show `Name (direct/total)` counts (direct children vs. all descendants) and a chevron to collapse/expand (state persists across restarts via the `collapsedQuickReplyFolders` setting), an inline editor that expands when you click a row, and a single collapsible **+ Add quick reply** button at the bottom that reveals an inline editor when clicked (saving from there appends a new quick reply to the bottom of the list).
- **Sessions** — your AI edit sessions for this project. Click a session row and the SessionPanel takes over the entire right pane; close the session to return to the tab list.

The active tab persists across project switches and app restarts (stored in `localStorage` under the key `amc.quickRepliesTab`). This is the _authoring_ surface; for the composer-side UX (Alt+S picker, Alt+1/2/3 AI chips, Alt+Z Zap button), see [use-quick-responses.md](use-quick-responses.md).

## How it behaves

### How to use it

1. **Open it.** Click **Quick Replies** in the left sidebar's Integrations group — it lives next to **Automations**.
2. **Switch tabs.** Click **Settings**, **Quick replies**, or **Sessions** in the sub-sidebar. The active tab persists across project switches and app restarts.
3. **Settings tab.** Flip **AI Suggestions** to enable/disable AI-generated reply pills (Alt+1/2/3) for sessions, SMS, email, and Telegram. When on, a **Suggestion Model** dropdown appears (Haiku is fastest/cheapest). **Quick Reply Button** controls the lightning-bolt Zap button next to Snippets; with it enabled you also get the **Quick Reply Message** text field and **Quick Reply Hotkey** binding (default Alt+Z). This tab is the **only** home for these toggles — the legacy duplicate panel under main Settings was removed on 2026-05-20.
4. **Quick replies tab — create a quick reply.** Two entry points. **From the top toolbar:** click **+ Add reply** to reveal an inline editor between the toolbar and the list — saving from here PREPENDS the new quick reply to the top of the list, with Cancel to dismiss. **From the bottom of the list:** click the dashed-border **+ Add quick reply** button to reveal the same inline editor — saving from here APPENDS to the bottom. The editor leads with just the basics — the **Label** (shown in the picker and sidebar row — must be globally unique), the **Body** textarea (the text that gets sent), and the **Auto-send when selected** toggle (with it on, picking the quick reply sends immediately; with it off, the text lands in the textarea for you to edit before hitting send). Everything fancier is tucked under a collapsible **Advanced options** disclosure, collapsed by default when adding a new reply so a basic user isn't overwhelmed — and auto-expanded when you edit a reply that already uses any advanced option, so your existing config is never hidden. Advanced options groups into **References** (the **Prepend other quick replies** chip row, the **Append other quick replies** chip row, and a live **What gets sent** preview that only appears when at least one prepend/append is configured) and **Behavior** (a **Number key** pin, an **engine & model** pin, and the "per-snippet effects" toggles described in the next subsection). The Save / Cancel / Delete footer is pinned and always visible. Ctrl/Cmd+Enter saves from anywhere; plain Enter saves from any single-line field.
5. **Quick replies tab — edit, reorder, delete, add dividers + folders.** Click any quick-reply row to expand it inline into the same editor pre-filled; change fields and Save, or hit the trash icon to remove — the trash icon opens a confirm dialog (`Remove "X"? You can undo with Ctrl+Z.`) so a destructive delete is never a single mis-click, and after confirming you can press **Ctrl+Z** (or the toast's Undo) to restore the quick reply **exactly** — same list position, "used X times" count, folder, and tags (it is re-inserted verbatim, not re-created as a fresh copy). Click **Cancel** to collapse without saving. Drag rows by their grip handle to reorder — every row type (quick reply, divider, **and folder**) is both draggable and a drop target. While dragging, hover near the top or bottom edge of a target row to insert _above_ or _below_ it; the accent hairline **indents to the depth where the item will actually land**, so a drop _inside_ a folder shows an indented line while a drop _outside_ at the parent level sits flush at the left margin — the fix for the old ambiguity at a folder's bottom edge where in-folder and out-of-folder drops painted an identical full-width line. On a folder row, hover the middle band to drop _inside_ that folder (the whole row gets an accent-tinted fill). Dropping below an _expanded_ folder inserts as that folder's first child (the gap visually belongs inside); dropping below a _collapsed_ folder behaves like any other sibling-after insertion. A drop zone at the **very bottom of the list** — shown only while you're dragging — always drops at the top level (at the end), so you can pull an item out from under a folder that's the last row. Cycles are prevented automatically: dropping a folder onto itself or any of its descendants surfaces a toast and skips the move. The list keeps scrolling while you drag — spin the mouse wheel, or drag near the top/bottom edge to auto-scroll — so reordering across a long list never means dropping the row just to scroll. With a mouse or trackpad, just press a row and drag. **On touch, press and HOLD a row for about half a second to pick it up, then drag** — the same press-and-hold gesture that reorders projects, tasks, bookmarks and docs; a plain swipe (a finger that moves before the hold lands) still scrolls the list as normal. The up/down arrows on each row stay a one-tap alternative on any device. Use the right-click context menu's **Move to folder…** option for a click-driven alternative to drag-into. Insert a divider via **+ Divider**, or a folder via **+ Folder** — both prepend to the top of the current level. **Delete folder** opens a two-button dialog: **Delete folder AND contents** (cascades — removes every quick reply, divider, and subfolder inside) or **Move contents up one level** (promotes children to the folder's parent; auto-rename collisions like `"PR description (2)"`).
6. **Sessions tab — open or spawn an AI edit session.** Click any existing session row to take over the right pane with the full SessionPanel; close the session (X button) to return to the list. To spawn a new session, use the **+** button in the sidebar's session rail (same as any other project). Omniscio starts a Claude session pre-seeded with: (a) this page, (b) your current snippet inventory as JSON, (c) a REST cheatsheet for `/quick-replies/list`, `/quick-replies/create`, `/quick-replies/:id`, and `/quick-replies/reorder`, and (d) a short-lived in-app token that the Claude-Code process uses to write straight into the library — writes authorized with that token skip the CLI approval inbox and land as approved immediately. Ask it to "add a snippet that thanks the sender and confirms I'll reply tomorrow" and watch the Quick replies tab update.
7. **Use what you built.** Inside any session composer, press **Alt+S** to open the quick-reply picker. The picker opens at the root level; click a folder row to drill in with a horizontal slide animation. A top bar shows a back arrow and a clickable breadcrumb (`Root › Email › Replies`) — click any segment to jump back to that level. Typing in the search box auto-flattens results across every folder, prefixing each row with its folder breadcrumb so you can see where the match lives. Arrow keys move between visible rows and Enter inserts the highlighted item. AI suggestion chips (Alt+1/2/3) and the Zap button (Alt+Z) stay flat — they never drill into a folder. See [use-quick-responses.md](use-quick-responses.md) for the full composer-side UX including the AI-suggestion chips and the Zap button. Attachments (screenshots, PDFs, text docs) attached to the composer ride along on auto-submit quick replies and Zap sends — see [use-quick-responses.md](use-quick-responses.md) "Attachments ride along" for the contract and [chat-attachments.md](chat-attachments.md) for the underlying delivery pipeline.

#### Per-snippet effects (Advanced options → Behavior)

The **Behavior** group inside **Advanced options** holds three opt-in effect toggles plus an auto-apply tags row (the **Auto-send** toggle moved out to the always-visible basics). Each one persists on the snippet itself, so updating the override text is a single edit and any subsequent snippet send picks it up:

- **Set session title from this quick reply** — when this snippet is the first user message in a fresh session, Omniscio sets the session's title to the configured text (or the snippet's label if the field is left blank). This applies wherever you *fire the saved reply* as the first message — a session's Alt+S picker, Quick Launch, or the mobile new-session composer; it does **not** apply when you type the reply's text by hand (that's just a typed prompt with no reply identity). The AI title-generator may still rename later — this is a starting title, not a lock. Two nested checkboxes appear when this toggle is on: **Append usage count** adds the running number of times this quick reply has fired — including the current send — as ` #150`, and **Append timestamp** adds the local date and time as ` · 2026-05-30 14:32`. With both on, the title reads `Full Local Git #150 · 2026-05-30 14:32` (count first, then timestamp). These suffixes apply **only** to titles set by this quick reply; AI-generated titles and manual renames are never touched.
- **Override Plain Speak "Latest" with this quick reply** — when you send this snippet, Omniscio waits for the agent's reply to that turn and stamps the configured override text (or the snippet's label if blank) onto the **final non-aside agent message** of that turn. From then on, that one historical message renders its `## Latest` line as the override text instead of the model's recap; the other four sections of the rewrite (TLDR / Recommended Action / Response / Questions) are unchanged. The stamp is **sticky and per-message** — it stays on that message permanently, even after you scroll back days later. Subsequent agent messages (the next turn, or any later turn) render the normal Plain Speak rewrite with no override. If Plain Speak didn't run for that turn (paused, capped, master-off, per-session off, short message), no override is shown — you see the original agent message.
- **Allow Inbox Pilot to auto-send this reply** — gates whether the AI classifier can pick this snippet as an auto-respond reply. When on, you can also fill an optional **Hint for the classifier** (e.g. "acknowledge thanks", "ask for more details") so the classifier knows when this snippet is the right pick.
- **Auto-apply tags** — pick up to 5 library tags inside the snippet editor's "Auto-apply tags" section. Each time the snippet fires (Alt+S picker, Alt+Z Zap button, or an Auto-reply / Automation `auto_respond` action), Omniscio attaches those tags to the receiving session. The picker shows every tag in your library — global and project-scoped alike — because tag application happens _unscoped_ at send time, so a tag scoped to one project still binds to a session in another project. Tags appear as chips on the snippet's row in the list (up to 2 visible, with "+N" overflow). Sessions cap at 10 tags total — once a session is at that cap, additional tags silently fail to attach (the message still delivers normally). Soft-deleted tags also silently no-op so a stale snippet→tag binding doesn't break sends; the binding gets cleared the next time the snippet is saved.

#### Reusing other quick replies via prepend/append (Advanced options → References)

The **References** group inside **Advanced options** includes two chip rows — **Prepend other quick replies** and **Append other quick replies** — for citing existing quick replies as shared headers/footers. The picker for each row excludes (a) the snippet currently being edited (no self-reference), (b) any quick reply already selected on the same row, and (c) dividers and folders (they have no sendable body). Each row caps at **5 entries**; the **+ Add** button visibly disables once you hit the cap. Removing a chip is a single-click **×** on the chip. The two rows are independent, so you can prepend up to 5 and append up to 5 on the same snippet.

When the snippet fires, Omniscio sends the final text by concatenating, in order: each resolved prepend body, the snippet's own body, then each resolved append body — joined by single newlines. So **`prepend = [greeting]`**, **`body = "Thanks for the heads-up."`**, **`append = [signoff]`** produces:

```
Hi there!
Thanks for the heads-up.
— Bot
```

A live **What gets sent** preview renders just below the chip rows whenever at least one prepend or append is configured, so you can see the exact expanded text before saving. The preview only appears when there's something to preview — for a bare body with no refs, the body IS the final text and a duplicate preview would just be noise.

Expansion is **fail-open and shallow on purpose**:

- A reference whose target snippet was deleted is **silently skipped** at send time — no error, no blank line in the output. Deleting a referenced snippet never breaks the editor of others that referenced it. This matches the same reader-time skip pattern used by `snippet_tags`.
- A reference whose target body is empty is also skipped — no stray blank line between prepend and body.
- **One level only.** If snippet A prepends B and B prepends C, sending A emits `B-body\nA-body` — _not_ `C-body\nB-body\nA-body`. Recursion is out of scope on purpose; one level is the simple "no runaway expansion at fire time" rule.
- **No self-reference.** Even if a stale id somehow points the snippet at itself, the helper skips it instead of infinite-looping or emitting the body twice. The picker also blocks self-selection at the UI layer.

This makes expansion safe to use from every fire path — the manual Alt+S picker, the Alt+Z Zap button, Auto-reply rules, Automation `auto_respond` actions, and the Inbox Pilot classifier all route through the same `expandQuickReplyText` helper. When the Inbox Pilot fires a snippet with refs, the audit row records the **fully expanded text** in `frozen_snippet_text` so replay is byte-equivalent to what the CLI received.

#### Export, share, and import the library

The **Quick replies** tab's toolbar carries three library-level actions — **Export**, **Share**, and **Import** — for moving a whole library between machines or between people. They deliberately have NO keyboard shortcuts, so a stray chord can never write a file or publish the library.

- **Export** writes the entire library to ONE small JSON file you pick in a native save dialog (default name `omniscio-quick-replies-<date>.json`). Every quick reply, divider and folder goes in, and the folder structure is preserved via each row's `parentId`. Per-machine usage statistics — how often a reply was used and when it was last used — are deliberately absent from the file. A toast confirms how many replies were written; dismissing the dialog writes nothing.
- **Share** publishes the same library as a readable page at a public Omniscio Shares link and copies that link to your clipboard. It warns FIRST that the link is public — anyone with it can open the page — and publishes only once you confirm; the link is also listed in Shares, where it can be revoked later. The page is built from the same fresh read as the exported file, so the link and the file can never describe different libraries. Re-sharing an unchanged library returns the same link.
- **Import** takes either an exported file (**Choose a file…**) or the block pasted from a shared page (**Or paste a shared library**). It writes NOTHING until you confirm: it first previews exactly what it will do — how many replies, folders and dividers the source holds, how many will be added, how many are already here and will be skipped, any that will arrive under a numbered name, any tags it will create, and any pinned shortcut digits it will drop — and only **Add to my quick replies** applies it.

The merge is **add-only and repeatable**:

- It only ever ADDS. Nothing already on the machine is deleted, replaced or edited.
- A reply whose label AND text already match an existing reply of the same type is skipped, so importing the same file twice adds nothing.
- A name clash keeps both: a snippet whose label is already taken by DIFFERENT text arrives renamed `(2)`, `(3)`, … after the existing one.
- A pinned shortcut digit is never stolen — an arriving digit (1–9) survives only where that digit is free on this machine and no other row in the same file claimed it.
- The whole import is ONE database transaction, so a failure cannot leave half a library behind.

Both halves are also reachable from the command line: `GET /quick-replies/export` returns the same bundle as JSON, and `POST /quick-replies/import` performs the same merge. See [cli-quick-replies.md](cli-quick-replies.md).

### Localization (built-in defaults)

The **built-in** quick replies seeded on a fresh install (Red Team, Run the app, **Plain English Rephrase**, Interview Me, …) are language-aware. Each seeded default carries a hidden `default_key` column tying it to an i18n catalog entry at `quickReplies.defaults.<key>.{label,text}`; while that key is set, the picker, the settings list, the editor, AND the send path (composer + Inbox Pilot) render the reply's **label and text from the catalog in the user's `uiLanguage`** instead of the stored English. So a user running AMC in Spanish sees Spanish buttons and — because the whole prompt is sent in Spanish — the agent answers in Spanish. English users see no change, and nothing is visible until a non-English language is enabled (the pilot languages are gated behind unreleased-feature flags today).

**Editing adopts it.** The moment you change a built-in's label or text and Save, it becomes a personal copy: the `default_key` is cleared (the editor sends `defaultKey: null` — the IPC update field is **clear-only**, so a client can never SET a key), the row keeps exactly what you typed, and it never re-translates. An unchanged save, a settings-only edit (autoSubmit, tags, a pin), or merely opening the editor leaves it a localized default. A translated label longer than the 50-char cap falls back to the English canonical when unchanged (the key is kept, so the catalog still localizes it).

**Identity + backfill.** Fresh installs get `default_key` stamped by the seed; existing installs are backfilled by a one-time migration that keys ONLY pristine rows — a row is keyed just when its label AND its exact text both match the frozen canonical, so any reply you had already edited stays a personal copy. That canonical list is **append-only and holds every superseded revision**: rewording a default's text without appending its new `sha256` in the same commit silently un-keys pristine rows, dropping them to personal copies that lose localization and every future catalog update. `plainEnglishRephrase` has been reworded twice, so it carries three hashes. The 17 pilot locales are filled by `npm run i18n:translate-new` and enforced at `/ready-to-merge`.

**The two `plainEnglishRephrase` rewords.** _2026-07-17_ — the English source went from _"plain, simple English"_ to _"plain, simple language"_ so its translations target the user's own language rather than literally asking for English (the button is still named "Plain English Rephrase" in English; some locales like Cebuano keep that proper name while fully translating the prompt body). _2026-08-25_ — the spacing bullet stopped naming `<br>`. It used to end _"…never use HTML tags like `<br>` (they show up as literal text here…)"_, and that ban was itself the cause: it put a vivid `<br>` into context on every send while the same sentence asked for vertical white space, which Markdown has no device for — so the model reached for the spacer it had just been shown and the reply rendered a literal `<br>`. It now reads _"For spacing, use real blank lines between sections, with a heading to open each one — pure Markdown throughout."_ **The general rule for any prompt in this app: a prohibition that spells out the forbidden token summons it — state the rule positively, and always leave the model something to do instead of the thing you banned.**

The whole path routes through the shared resolver [src/shared/quick-reply-i18n.ts](/src/shared/quick-reply-i18n.ts) (renderer via the [useQuickReplyResolver](/src/renderer/src/features/quick-replies/useQuickReplyResolver.ts) hook, main via `mainT`); `expandQuickReplyText` takes an injected `resolveText` so the composer and the Inbox Pilot both send the localized wording. Locked by [default-quick-reply-i18n-contract.md](/.claude/memory/contracts/default-quick-reply-i18n-contract.md). Two known limitations: the picker's text SEARCH matches the raw (English) label/text, so searching a built-in by a translated term won't match (the buttons still render localized); and the CLI `PATCH /quick-replies/:id` route (label/text/autoSubmit) does **not** clear the key, so editing a built-in via the CLI leaves it catalog-served — to personalize a built-in from an agent, delete it and create a fresh reply.

## For agents

### How it works

Quick Replies is a virtual project — there is no on-disk folder. It's registered via `QUICK_REPLIES_PROJECT_ID = '__quick_replies__'` in [src/shared/types.ts](/src/shared/types.ts), and the Dashboard routes that sentinel through [src/renderer/src/features/dashboard/Dashboard.tsx](/src/renderer/src/features/dashboard/Dashboard.tsx) into a split of [QuickRepliesSidebarSections.tsx](/src/renderer/src/features/quick-replies/components/QuickRepliesSidebarSections.tsx) (sub-sidebar tab buttons) plus [QuickRepliesProjectView.tsx](/src/renderer/src/features/quick-replies/QuickRepliesProjectView.tsx) (main panel router). Tab state lives in [virtual-project-selection-store.ts](/src/renderer/src/stores/virtual-project-selection-store.ts) as `quickRepliesTab: 'settings' | 'quickReplies' | 'sessions'`, hydrated from and persisted to `localStorage` under `amc.quickRepliesTab`. The main panel mounts both tab panes simultaneously and toggles them with the `hidden` attribute, so inline editor state (e.g. an open `editingId` in `QuickReplyListSection`) is preserved across tab switches. The Settings tab composes [QuickReplyTogglesSection.tsx](/src/renderer/src/features/settings/QuickReplyTogglesSection.tsx), which still physically lives under [src/renderer/src/features/settings/](/src/renderer/src/features/settings/) for historical reasons (the VP is its **only** consumer now); the Quick replies tab composes [QuickReplyListSection.tsx](/src/renderer/src/features/quick-replies/components/QuickReplyListSection.tsx). The matching main Settings panel (`SnippetSettings.tsx`) was removed on 2026-05-20 along with the `'snippets'` `SettingsSection` value, sidebar entry, content-router branch, and all nine `section: 'snippets'` entries in the settings search index. The `idPrefix="qr-"` prop the toggles component still accepts is now a no-op vestige and would only matter again if a second concurrent mount were ever re-introduced. Two one-shot slices on [virtual-project-selection-store.ts](/src/renderer/src/stores/virtual-project-selection-store.ts) — `pendingEditQuickReplyId` and `pendingSettingsScrollId` — power the right-click "Edit" handoff: `QuickRepliesProjectView` polls each slice for up to 2 s after activation, opens the inline editor (or scrolls + flashes the corresponding Settings row), and clears the slice; the 2 s timeout guards against a missed AnimatePresence enter wedging the slice indefinitely. Sessions are not a tab: selecting a session row (`selection: { kind: 'session', id }` on the same store) swaps `QuickRepliesProjectView` to a full SessionPanel takeover.

Persistence is the `response_snippets` SQLite table: `type` column is `'snippet' | 'divider' | 'folder'`, `parent_id` is a nullable self-referential foreign key (migration v169) that points to the containing folder row (`NULL` = root level), `display_order` controls list position WITHIN a parent, and `auto_submit` is a boolean on snippet rows. Drag-driven reorder is **pointer-based** — the row roots use `onPointerDown`/`Move`/`Leave`/`Up`, NOT native HTML5 `draggable` — specifically so the scroll wheel keeps working mid-reorder: native drag's Windows `DoDragDrop()` modal loop suppresses `wheel` (and `pointer*`) events for the whole drag, whereas a pointer drag never enters that loop, so the list stays a normal `overflow-y-auto` scroll container. For a mouse/pen it activates only past an 8px travel threshold (a plain click still edits/deletes, and a press that begins on a row button is that button's click). For **touch** it activates on a 400ms press-and-hold (`TOUCH_HOLD_MS`) instead — the same gesture `useDragReorder({ activateOn: 'longPress' })` gives every other reorderable list in the app — so a finger that travels before the hold fires is treated as a scroll and swipe-to-scroll is untouched; the row releases touch's implicit pointer capture so the drop resolves against the row under the finger, and a non-passive `touchmove` handler suppresses native scroll only WHILE the drag is live. The up/down `ReorderButtons` remain the one-tap alternative. Every row (quick reply, divider, folder) is a pointer-drag source and a drop target with `above`/`below` zones, and folder rows add a middle `into` zone (25–75% of the row's height) for nesting; `useDragAutoScroll` (edge auto-scroll) tracks the pointer from both `pointermove` (this list) and `dragover` (the composer picker, still native drag). Locked by [quick-reply-reorder-pointer-drag-contract.md](/.claude/memory/contracts/quick-reply-reorder-pointer-drag-contract.md). The renderer's `computeDropTarget()` translates the zone into a `{ parentId, beforeId }` pair which the IPC handler resolves into the right `display_order` slot via `QUICK_REPLY_MOVE` (id + new parentId + optional beforeId, transactional). A sibling `computeDropIndicatorDepth()` — kept in lock-step with `computeDropTarget()` by an anti-drift test — drives a `--drag-indent` CSS custom property so the drop hairline is inset to the depth the item will land at (inside-folder vs. parent-level), and an end-of-list drop zone rendered only mid-drag posts the same `QUICK_REPLY_MOVE` with `parentId: null, beforeId: null` for a top-level append; the indicator CSS keeps a `0px` fallback so the app's ~20 other drag surfaces stay unaffected. The legacy `QUICK_REPLY_REORDER` channel (ordered-id list, top-level only) is retained for the "prepend new item" entry points (+ Folder / + Divider / + Add reply toolbar buttons). Cycle prevention runs client-side in `isDescendantOrSelf()` against the live tree before the IPC call ever fires. Folder collapse state lives in `AppSettings.collapsedQuickReplyFolders` (a `Record<string, true>` keyed by folder id) so a UI-only toggle never bumps a DB row. There is no soft-delete; deleting a row removes it outright (Quick Replies is low-stakes user-editable content, unlike projects/sessions where soft-delete is load-bearing). Delete is still **undoable without** a soft-delete column: the renderer captures the full row before deleting, and Ctrl+Z (or the toast's Undo) re-inserts that exact snapshot via the `QUICK_REPLY_RESTORE` channel → `restoreQuickReply`, preserving `id`, `display_order`, `created_at`, `use_count`/`last_used_at`, parent, tags, pin, and flags — a true restore, NOT a `QUICK_REPLY_CREATE` (which would mint a new id, append at the end, and zero the usage count). Three guards keep a stale snapshot safe: a since-deleted parent folder restores to the top level, a since-deleted tag is filtered out (the `quick_reply_tags.tag_id → tags(id)` FK would otherwise abort the insert), and an id that's been taken back throws instead of clobbering the live row. The row's trash icon routes through a `ConfirmDialog` (not a one-click delete) so the undo affordance is always shown. Locked by [quick-reply-delete-undo-contract.md](/.claude/memory/contracts/quick-reply-delete-undo-contract.md). The TWO-button **Delete folder** dialog calls `QUICK_REPLY_FOLDER_DELETE` with `{ folderId, mode: 'cascade' | 'promote' }`; the cascade path uses SQLite's `ON DELETE CASCADE` on `parent_id`, the promote path rewrites each child's `parent_id` to the folder's own parent (auto-renaming name collisions with `(2)`, `(3)`, … in a transaction) before deleting the folder row. Spawning an AI edit session goes through [src/main/services/session/session-create.ts](/src/main/services/session/session-create.ts): when `project.folderPath === QUICK_REPLIES_PROJECT_ID`, the launch path calls [src/main/services/virtual-project-seed.ts](/src/main/services/virtual-project-seed.ts) to assemble a seed bundle (library doc + inventory JSON + endpoint cheatsheet + auth info), mints an in-app session token with [src/main/services/in-app-session-token.ts](/src/main/services/in-app-session-token.ts), formats the combined text as the first CLI prompt, and persists it as a `source: 'system'` row in `conversation_messages` so the chat UI renders it as injected context rather than operator input. Writes the AI makes over the `/quick-replies/*` endpoints using the in-app token bypass the CLI approval inbox — the trust boundary is "this session was launched in-app by the user", which [src/main/services/cli/cli-server.ts](/src/main/services/cli/cli-server.ts) `getAuthContext()` recognises as the `in-app-session` class.

Quick-reply→tag bindings live in the `quick_reply_tags` join table (`quick_reply_id, tag_id` composite PK with `ON DELETE CASCADE` on both columns), mirroring `session_tags`. Reads on `listSnippets` / `getSnippet` aggregate the bindings via a `json_group_array` correlated subquery so each `ResponseSnippet` carries `tagIds: string[]` in one round-trip. Writes go through `setSnippetTags(snippetId, tagIds)` — replace-semantics, atomic (DELETE then INSERT inside one transaction). Auto-apply runs inside [src/main/services/quick/quick-reply-effects.ts](/src/main/services/quick/quick-reply-effects.ts) `applyQuickReplyEffects`, the same hook that handles auto-title and Latest-override; it iterates `snippet.tagIds` and calls `applyTagToSessionUnscoped(sessionId, tagId)` for each, which bypasses the normal scope check at apply time. Cap behavior: per-quick-reply cap of 5 is enforced at the IPC schema (the `tagIds` array carries `.max(5)`), per-session cap of 10 is enforced inside `applyTagToSessionUnscoped` and surfaces as a `TAG_CAP_REACHED` ApplyResult that the runner ignores — the quick-reply text still sends.

Prepend/append refs live as two TEXT columns on `response_snippets` — `prepend_snippet_ids` and `append_snippet_ids`, both `NOT NULL DEFAULT '[]'` (migration v197). Each holds a JSON array of snippet ids; the row reader at [src/main/db/queries-quick-replies.ts](/src/main/db/queries-quick-replies.ts) parses each column into `prependSnippetIds: string[]` / `appendSnippetIds: string[]` on every `ResponseSnippet`. Caps are enforced at the IPC schema layer in [src/shared/ipc-schemas.ts](/src/shared/ipc-schemas.ts) — both arrays carry `.max(5)` on the create + update + Quick-Replies CLI-control endpoints, so a malformed/oversized write rejects at the wrapper before it can hit the database. Expansion is centralised in the shared helper [src/shared/quick-reply-expansion.ts](/src/shared/quick-reply-expansion.ts): `expandQuickReplyText(snippet, lookup)` walks `prependSnippetIds` (in order) → snippet body → `appendSnippetIds` (in order), joins with `\n`, and silently skips any id whose `lookup(id)` returns `null`, any resolved snippet whose body is empty, and `snippet.id` itself (the no-self-reference floor). `lookupFromList(snippets)` builds an id→snippet map for callers who already have the full snippet list in hand. The same helper is consumed by BOTH the renderer-side **What gets sent** preview inside [QuickReplyEditor.tsx](/src/renderer/src/features/quick-replies/components/QuickReplyEditor.tsx) (driven by `useQuickRepliesStore` so the preview tracks live edits) AND the main-process Inbox Pilot dispatcher at [src/main/services/ai-manager/action-dispatcher.ts](/src/main/services/ai-manager/action-dispatcher.ts), so the preview text is guaranteed byte-equivalent to what the CLI receives. The dispatcher records the FULLY EXPANDED string in `frozen_snippet_text` on the audit row (`ai_action_audit` — see [docs/llm-library/inbox-pilot.md](inbox-pilot.md)) — replay is byte-equivalent; `snippet_id` on the same row remains the AUTHOR-chosen snippet, not any of the refs, because refs are content and not identity. The chip-row UI is the shared [QuickReplyRefChipRow.tsx](/src/renderer/src/features/quick-replies/components/QuickReplyRefChipRow.tsx) component (one render for the prepend row, one for the append row inside `QuickReplyEditor`) — it filters its picker down to `type === 'snippet'` rows that aren't the snippet currently being edited and aren't already on the same row, and it disables its **+ Add** button at the IPC cap so the UI never invites a write the schema would reject.

Export, Share and Import are all built on ONE read. `buildQuickReplyExport()` in [src/main/services/quick/quick-reply-transfer.ts](/src/main/services/quick/quick-reply-transfer.ts) turns `listQuickReplies()` into a flat `QuickReplyExportBundle` — `{ kind: 'omniscio.quick-replies', version, exportedAt, appVersion, items }`, shape in [src/shared/types/quick-reply-transfer.ts](/src/shared/types/quick-reply-transfer.ts) — where each item names its own `parentId`, its tag NAMES (not ids, which are machine-local), and its prepend/append refs in the FILE's id space, with `useCount`/`lastUsedAt` deliberately absent. The in-app **Export** writes that bundle to a file (`omniscio-quick-replies-<date>.json`) through the `QUICK_REPLY_EXPORT` IPC; the read-only `QUICK_REPLY_EXPORT_BUNDLE` channel hands the identical bundle to **Share**, which publishes it as Markdown via the Share Artifacts pipeline (`SHARE_PUBLISH_ARTIFACT`, rendered by `buildQuickReplyShareMarkdown` in [src/shared/quick-reply-share-page.ts](/src/shared/quick-reply-share-page.ts)) — so the file and the page always describe one library. **Import** previews and applies through `previewQuickReplyImport` / `importQuickReplies` on that same module, both running the ONE private `planImport`, so what the preview shows and what lands cannot disagree. On apply, any tag the install lacks is created first (`ensureLibraryTagByName`), then every planned row is inserted in a single named write-lane transaction (`dbNamedTxFailOpen('importQuickReplies', …)` in [src/main/db/worker/named-tx-quick-replies.ts](/src/main/db/worker/named-tx-quick-replies.ts)) — a failure leaves the library exactly as it was. The merge is the import's contract: add-only; a row of the same type with the same label AND text is skipped (`rowIdentity`); a snippet label clash becomes `"<label> (2)"`; a `numberKey` survives only when that digit is free here and unclaimed by the file; and `parentId` / refs are remapped into fresh local ids (a parent the file does not contain lands the row at the top level with a warning, never dropped). An imported row never carries `defaultKey`, so its own text is what renders. Bounds are enforced before any parse — `QUICK_REPLY_IMPORT_MAX_ITEMS` (2000) and `QUICK_REPLY_IMPORT_MAX_BYTES` (5 MB) — and a bad payload leaves as a `QuickReplyImportPayloadError` with a plain-English reason (`400` on the CLI twin). The two CLI twins are `GET /quick-replies/export` (the identical bundle) and `POST /quick-replies/import` (the same merge; body `{ bundle }` or `{ text }`; no preview round-trip — the response carries `added` / `skipped` / `renamed` / `tagsCreated` / `keysDropped`). Locked by [quick-reply-transfer-contract.md](/.claude/memory/contracts/quick-reply-transfer-contract.md).

## Related

For the composer side of the same feature — the Alt+S snippet picker, the Alt+1/2/3 AI suggestion chips and the Alt+Z Zap button — see [use-quick-responses.md](use-quick-responses.md). Saved replies also fire from outside the composer, so [automations-and-auto-replies.md](automations-and-auto-replies.md) covers the Auto-replies rules and Automation `auto_respond` actions that can fire a snippet by label, and [cli-control.md](cli-control.md) documents the `/snippet/*` HTTP endpoints for scripting the snippet library from AutoHotKey, curl, or an external AI.
