Writer Studio (standalone writing editor) (part 2)
Part 2 of the Writer Studio page: the formatting toolbar, find and replace, the slash menu, document templates, tags, cloning, bulk operations, rename, font and spacing controls, Save as, the editor theme and colour palettes, and the two writer data-model tables.
What it is
This is part 2 of the Writer Studio (standalone writing editor) page. It covers the editor surface and the document list around it — the formatting toolbar, the plugin’s find bar, slash menu and template picker, the row-level document operations, the font, spacing, theme and palette controls, Save as, and the two data-model tables behind all of it.
Where to find it
Writer Studio opens from the Writer virtual project in the sidebar, and the parent page covers how the feature is gated, which of its two surfaces you are looking at, and the first-run wizard. Within the writer view the formatting toolbar sits between the document title and the editor body, the document list occupies the left sidebar, and the theme, palette, font and spacing controls live under the Appearance gear icon in the document header.
How it behaves
Formatting toolbar
A Word-style formatting toolbar sits between the document title and the editor
body (FormatToolbar.tsx). Left to right: Undo · Redo, a paragraph-style
dropdown (Normal text / Heading 1–3), Bold · Italic · Strikethrough · Code,
Bullet list · Numbered list · Quote, and Link · Divider · Clear formatting.
- Undo / Redo are the leftmost two buttons; they reflect the Tiptap editor
history state (
can().undo()/can().redo()) and disable when no action is available. They are keyboard-equivalent to Ctrl+Z / Ctrl+Y. - Buttons reflect the cursor's context — a toggle shows
aria-pressed/ the accent style when the selection is inside that mark or block; the dropdown shows the current block's style. - Link opens a small inline box to type a URL (auto-prefixed with
https://when no scheme is given; onlyhttp/https/mailtoare accepted), with a Remove option on an existing link. - Clear formatting runs
unsetAllMarks()+clearNodes()(matches Word's "Clear All Formatting").
The toolbar is markdown-clean: every action maps to a command StarterKit already serializes to markdown, so the stored format stays markdown (no HTML). There are deliberately no Underline, text-color, or alignment buttons — markdown has no standard representation for them, and adding them would require embedding HTML in the stored content. Clicking a button, pressing the keyboard shortcut (Ctrl+B…), and typing markdown all drive the same Tiptap commands.
Find & replace (plugin)
Cmd/Ctrl+F opens an in-document find bar (FindBar) over the editor — query + replace inputs,
case-sensitive and whole-word toggles, an "N of M" count, and next / previous / Replace / Replace-all.
The key is intercepted (preventDefault) so the webview's own native find doesn't hijack it; Esc closes
and clears the search.
Matching runs in a custom ProseMirror extension (editor/extensions/FindReplace.ts, the
InlineAutocomplete pattern — permitted by I20's Wave-2b clarification): all matches are highlighted via
inline decorations (.writer-find-match; the current one also .writer-find-current), and it edits
PLAIN TEXT only so the document stays markdown-clean (I3). Match ranges come from the pure findAllMatches
(lib/find-matches.ts — exact substring, case / whole-word, non-overlapping) over collectTextBlocks
(the SAME text→position map the chat "apply edit" flow uses). Replace-all runs last match → first so
an earlier match's position never shifts under a later edit.
Slash "/" menu (plugin)
Type / at a line start (or after whitespace) to open a floating command menu at the caret,
filtered by what you keep typing; up/down move, Enter runs, Esc dismisses. Commands: Heading 1-3,
Bulleted / Numbered list, Quote, Divider (pure StarterKit chains -> clean markdown, I3/I20), From
template (opens the TemplatePicker), and the AI commands Continue writing (via the existing
aiAutocomplete, stale-guarded), Summarize, Explain, and Improve writing (the last three
open the Assistant + send via sendChat). The AI commands reuse the existing cost-cap + humanized-error
flow; no new bridge.
Detection is a pure matchSlashQuery (lib/slash-trigger.ts) over the text before the caret, wrapped
in the SlashMenu extension (editor/extensions/SlashMenu.ts) that computes { active, query, from }
PURELY in the plugin's apply (no dispatch-from-view) and SKIPS code blocks. The command list is
lib/slash-commands.ts; the floating UI + keyboard live in components/SlashMenu.tsx -- the keyboard
is a capture-phase listener mounted ONLY while the menu is open. On pick it deleteRanges exactly the
/query before running the command.
Document templates (plugin)
Creating a new document in the plugin (the sidebar + or the empty-state create button) opens a
template picker (TemplatePicker, reusing the guest Modal) with a Blank option first,
then any user-defined templates (accent-colored border), then the built-in starter templates
(lib/writer-templates.ts) — the same set Writer Studio ships, kept identical by a drift check.
Picking one seeds structured markdown (and, where it helps, a writing-guidance seed) via the existing
createDocument → setContent → updateGuidance path (the same flow as a Drive import). The picker
ONLY ever seeds a fresh document — there is no "apply to the open document" path, so it can never
overwrite an existing draft, and Blank preserves the one-click create.
User-defined templates
Users can save any open document as a reusable template via the BookmarkPlus button in the
document header. The template captures the document's title, markdown content, and writing guidance
(goal, audience, tone). Templates are stored in the writer_templates table (see "Data model"
below) with soft-delete and are global per install (no account_id).
User templates appear ABOVE the built-in templates in the picker with an accent-colored border.
Each user template has a hover-visible delete button (Trash2 icon) that soft-deletes it. The picker
fetches user templates via writerApi.templateList() on every open.
Both the IPC handlers and the plugin bridge support full CRUD:
| Surface | List | Get | Create | Delete |
|---|---|---|---|---|
| IPC (native) | WRITER_TEMPLATE_LIST |
WRITER_TEMPLATE_GET |
WRITER_TEMPLATE_CREATE |
WRITER_TEMPLATE_DELETE |
| Plugin bridge | templateList |
templateGet |
templateCreate |
templateDelete |
Template operations do NOT emit writer:documents-changed pushes (templates are a separate entity
from documents).
Document clone
The document row context menu includes a Duplicate action that atomically copies a
document. cloneWriterDocument reads the source row, creates a new document with a
"Copy of [title]" prefix, copies all metadata fields (goal, audience, tone, creativity,
folder), and duplicates the document's tags. The clone is a fully independent document
with its own id and timestamps. Cloning a nonexistent or deleted document returns
undefined (the handler shows a humanized error). The WRITER_DOCUMENTS_CHANGED push
fires after clone so the sidebar refreshes.
Document tags
Documents can be tagged with freeform text labels for organization and filtering.
Tags are stored in a junction table writer_document_tags (created by migration
20260903023357):
| Column | Type | Notes |
|---|---|---|
document_id |
TEXT FK | references writer_documents.id |
tag |
TEXT | the tag label (case-sensitive) |
created_at |
TEXT | ISO timestamp |
Primary key is (document_id, tag). Adding a duplicate tag is a no-op (INSERT OR IGNORE). Tags are loaded alongside documents via loadAllDocumentTags (a single
query that batch-loads all tags grouped by document id, avoiding N+1).
In the plugin DocumentList, each row shows up to 3 tag chips with an overflow count. A tag filter bar appears above the document list when any tags exist; clicking a tag filters the list to documents carrying that tag. The row context menu includes a "Tags" item that opens a tag management popover with an input to add new tags and buttons to remove existing ones.
Templates, tags, copies and export in Writer Studio (native)
Writer Studio has the same five document tools as the plugin, over the same tables and channels.
Contract: writer-library-contract.
- Start from a template. New from template in the create menu, or the From template
item in the
/menu, opens a picker: a Blank card, then your saved templates, then the built-in ones, with one search box over all three. Picking one creates the document, saves the template's text, then its writing guidance, and only then opens it. If the text cannot be saved the new document is removed; if only the guidance fails, the document is kept and you are told. - Save as template. An item in the document's File menu. It stores the title, text and guidance as a snapshot; later edits do not reach it. An empty document is refused with a plain sentence. The template's name is the title cut to 200 characters. A saved template's delete button is always visible, not hover-only, and asks for confirmation.
- Tags. Each list row shows up to three tag chips, then +N. Manage tags in the row menu opens a small dialog: the tags as removable chips, a box where Enter adds one, and suggestions from every tag in use. Adding or removing a tag does not change the document's last-edited time, so it never reorders the list or makes the next autosave conflict.
- Filter by tag. A row of tag pills appears above the list once any document is tagged. Picking one narrows the whole list, folders included, and works together with a search. Picking it again clears it, and it clears itself if no document carries that tag any more.
- Duplicate. Makes a "Copy of" document with the text, guidance and tags, and opens it. The copy carries none of the original's share page, board card, knowledge-base note, project item or chat links.
- Export from the list. Export as Markdown and Export as HTML in the row menu use the same export as the File menu. A failure is reported in words; cancelling the save dialog is silent.
- Translate. A preset in the selection popup, beside the other AI edit presets, reviewed before it applies like any AI edit.
The four row actions work on one document at a time; with several selected they show as disabled,
with a tooltip saying why. None of these actions has a keyboard shortcut: the / menu is already
the keyboard path to templates, and the rest are one click away in a menu.
- In the web client (a phone or browser linked to the app) the link refuses templates, tag
editing, copies and export, so those menu items and the
/template command are hidden there rather than shown only to fail. Tags still show on each row, and the tag filter still works. - From the command line, the same actions are routes on the local control server:
POST /writer/documents/:id/clone,POST /writer/documents/:id/tags(body{ tag }),DELETE /writer/documents/:id/tags/:tag,GETandPOST /writer/templates, andDELETE /writer/templates/:id. They use the same saving code as the app, so a document copied or tagged from the command line shows up in the open Writer. Copy and template create accept anX-Client-Request-Idheader, so a retried request never makes a second copy.
Multi-select and bulk operations
The plugin DocumentList supports multi-select: click the checkbox on any row to toggle selection (checkboxes appear on hover, or always when items are selected). A bulk toolbar appears above the list showing the selection count, with actions for Select all, Delete selected, and Clear selection. Bulk delete calls the per-document delete for each selected id and clears the selection.
Inline document rename
Double-clicking a document title in the sidebar, or choosing "Rename" from the row
context menu, turns the title into an inline text input. Commit with Enter or blur,
cancel with Escape. The cancelDocRenameRef pattern prevents a blur-after-Escape
from committing the cancelled value (same pattern as folder rename).
Font size and line spacing
The Appearance menu (gear icon in the document header) includes two new submenus:
- Font size — 7 options from 12px to 24px (default 16px). Stored as
writerFontSizeinappearance-settings.ts. - Line spacing — 3 options: Compact (1.4), Normal (1.7, default), Relaxed (2.0).
Stored as
writerLineSpacing(WriterLineSpacingenum).
Both are applied to the editor via CSS custom properties (--writer-font-size,
--writer-line-height) set on the prose container's inline style. The plugin reads
them on mount via the writer.getAppearanceSettings bridge method. The CSS variables
have fallback defaults in index.css so the editor renders correctly before settings
load.
Save as (local file export)
The File menu includes a Save as submenu with two options:
- Markdown (.md) — writes the raw markdown content to the chosen path.
- HTML (.html) — converts the markdown to HTML via
markdownToExportHtml(the same converter used for Google Docs export) and writes it.
Both open a system save dialog (dialog.showSaveDialog) with a default filename
derived from the document title (sanitized via sanitizeTitleForPath). Empty
documents cannot be exported (the handler returns a humanized error). The same export
options are also available in the document row context menu in the sidebar.
Editor theme and color palette
The Writer editor can use a different light/dark theme from the rest of the app,
and an optional color palette that restyles the writing surface. Both live under
the Appearance menu (gear icon) in the document header (WriterAppearanceMenu):
Theme (Sun/Moon/SunMoon icon) — a menu item that cycles between
match-app(default),light, anddark. When set to an explicit theme that differs from the app's current mode, the Writer root div gets a.writer-lightor.writer-darkCSS class that overrides the--color-surface-*-rgbcustom properties. All existingsurface-*Tailwind utilities within the scope resolve to the overridden values automatically, so zero child components needed refactoring.Color palette (Palette icon) — a submenu offering 8 curated palettes (Parchment, Midnight Ink, Candlelight, Noir, Velvet, Frost, Sage, Rose) plus None. Each palette adds a
.writer-palette-{name}class to the WRITER_PAGE element with custom gradients, borders, and shadows. Parchment and Candlelight include SVGfeTurbulencenoise texture and are light-only (skipped in dark mode). All other palettes have both light and dark variants.Toolbar layout (LayoutGrid icon) — a submenu offering 4 toolbar button styles: Icons + labels (icon + full text), Icons only (icon with rich tooltip, default), Compact (icon + shortened label), and All icons (everything icon-only, including File/Share/Send-to menus). The default is Icons only because it matches other Omniscio toolbars and Writer Studio is a breadcrumbed tool for experienced users.
Settings: writerTheme ('match-app' | 'light' | 'dark', default 'match-app'),
writerPalette (enum, default 'none'), and writerToolbarLayout
('icons-text' | 'icons-only' | 'compact' | 'full-icon', default 'icons-only') in
appearance-settings.ts. The Appearance gear icon shows accent highlighting when
palette is set away from its default. The theme logic is in WriterView.tsx
(writerThemeClass memo, paletteClass memo); the CSS classes are in globals.css.
Each palette tints the page with a single hairline inset shadow in the palette's own
hue. The class name is built in WriterView.tsx (the paletteClass memo) as
writer-palette-{name}, and the palette rules themselves are written out as literals in
src/renderer/src/styles/globals.css: Tailwind purges a rule whose class name it never
sees spelled out, which is how the light palettes once rendered nothing. The two
light-only palettes are listed once, as LIGHT_ONLY_PALETTES in
src/shared/types/settings/appearance-settings.ts, and the renderer skips their class in
dark mode. That file is also where the WriterPalette, WriterLineSpacing and
WriterToolbarLayout types and their WRITER_PALETTE_VALUES /
WRITER_LINE_SPACING_VALUES / WRITER_TOOLBAR_LAYOUT_VALUES lists live.
The pop-out Writer window (WriterWindowApp) renders the same WriterView, so both
settings work identically in the standalone window.
The writing surface
The writing surface is a portrait sheet — WRITER_PAGE in
src/renderer/src/features/writer/styles.ts caps the line length at 70 characters and
gives the panel its rounded border and generous padding, so it reads like a page rather
than a strip. Under it, the editor status strip in WriterEditorHost.tsx shows the word
count and an estimated reading time.
There are no page controls: neither an "Add page" page break nor a two-column "book
view" exists in the app today, in this editor or in the Writer plugin. The editor's
extensions are FindReplace, InlineAutocomplete and SlashMenu only.
For agents
Data model — writer_documents
Created by migration
src/main/db/migrations/20260603162951-create-writer-documents-table.ts.
| Column | Type | Notes |
|---|---|---|
id |
TEXT PK | randomUUID() |
title |
TEXT | display name (≤ 255 chars, Zod-enforced) |
content |
TEXT | canonical markdown body (stored as markdown, never HTML) |
goal |
TEXT | what the document is for (writing guidance for AI) |
audience |
TEXT | who the document is for (writing guidance for AI) |
tone |
TEXT | desired tone, e.g. warm, formal, punchy (guidance for AI) |
creativity |
INTEGER | 0–100 creativity dial value |
created_at / updated_at |
TEXT | ISO timestamps, bound in JS |
is_deleted |
INTEGER | 0/1 soft-delete — hard-deleted by the retention sweep |
folder_id |
TEXT | FK to writer_folders; NULL = Unfiled |
pinned |
INTEGER | 0/1 pin within folder |
share_token |
TEXT | Shares republish token (set on publish) |
share_url |
TEXT | published Shares URL |
mc_item_id |
TEXT | linked Mission Control item id |
linked_kms_note_id |
TEXT | linked KMS note id (vault export bridge) |
linked_kms_vault_id |
TEXT | linked KMS vault id |
external_url |
TEXT | URL of the linked external document (e.g. Google Doc) |
external_provider |
TEXT | provider name for the external link (e.g. google-docs) |
external_id |
TEXT | provider-specific document id |
source_session_id |
TEXT | id of the Claude session this document was created from |
Documents are global per install — the table has no account_id column. Every
document is visible regardless of which Anthropic account is currently active.
Every READ query filters AND is_deleted = 0. Delete flips is_deleted = 1; a
toast action can restore it (is_deleted = 0) within the standard Undo window.
A deleted document is destroyed for good once the retention window passes.
The daily retention tick calls purgeDeletedWriterDocuments(db, retentionDays)
(src/main/db/queries-writer.ts), which runs
DELETE FROM writer_documents WHERE is_deleted = 1 AND updated_at < ?. Every
version of the document goes with it
(purgeDeletedWriterDocumentVersions, the same rule on created_at), so a
deleted draft and its history become unrecoverable at the cutoff —
dataRetentionDays, 30 days by default. The Undo window is the only recovery
path; after it closes, do not treat a deleted document as still being in the
database.
Data model — writer_templates
Created by migration
src/main/db/migrations/20260903023722-create-writer-templates-table.ts.
| Column | Type | Notes |
|---|---|---|
id |
TEXT PK | randomUUID() |
label |
TEXT | display name (Zod: trimmed, 1–200 chars) |
description |
TEXT | optional description (Zod: max 500 chars, default '') |
title |
TEXT | seeded document title (Zod: trimmed, 1–500 chars) |
markdown |
TEXT | seeded document content (Zod: 1–500k chars) |
goal |
TEXT | writing guidance goal (nullable) |
audience |
TEXT | writing guidance audience (nullable) |
tone |
TEXT | writing guidance tone (nullable) |
is_deleted |
INTEGER | 0/1 soft-delete |
created_at / updated_at |
TEXT | ISO timestamps, bound in JS |
Templates are global per install — no account_id column. Same soft-delete
pattern as documents. Query layer: src/main/db/queries-writer-templates.ts.
Content format
Content is always stored as markdown. The Tiptap editor uses the
@tiptap/markdown extension: getMarkdown() serialises on save;
setContent(md, { contentType: 'markdown' }) hydrates on open. Never store or
persist HTML.
Related
The surfaces that frame these controls — the document-list view modes, search, folders, version history and autosave — are on the Writer Studio (standalone writing editor) page. The AI capabilities that read this document and its guidance are in part 3, sharing and integrations in part 4, and the IPC reference in part 5.
Last verified 2026-10-06