---
title: Writer Studio (standalone writing editor)
---

# Writer Studio (standalone writing editor)

## What it is

**Writer Studio** is a standalone writing surface inside Omniscio,
surfaced as a virtual project in the sidebar. It gives you a full-screen
Tiptap-powered markdown editor with a saved-documents list, a **Writing Guidance**
panel (instructions that will guide AI editing), and a **Creativity Dial**
(controls how conservative or inventive AI suggestions are).

The foundation ships the core document management loop: create, open, rename,
delete (soft-delete with Undo), and auto-save. All four AI capabilities are now
shipped: **selection edits**, **options variants**, **inline autocomplete**, and
**chat assistant**. The Writer Studio feature is complete.

**AI works out of the box — no API key required.** The default model for edits, options,
and autocomplete is Luna (GPT-5.6 via OpenRouter), which uses a bundled company-paid key.
Chat defaults to Sonnet and resolves credentials API-key-first, then falls back to your
**signed-in Claude account** (run through `claude -p` on your subscription — the same
sanctioned path the Council uses). See "Credentials — three-step cascade" below.

## Where to find it

### How it is gated

Writer Studio is an in-development feature registered as `'ai-writer'` in
`src/shared/unreleased-features.ts` (`settingKey: 'aiWriterEnabled'`,
`envVar: AMC_SHOW_AI_WRITER`, `status: 'in-development'`).

It is visible when **any one** of the following is true:

- The feature is marked `'shipped'` in the registry (releases to everyone).
- Omniscio is launched with `AMC_SHOW_AI_WRITER=1`.
- The user flips the **Settings → Lab → Writer Studio** toggle on.

No code reads `settings.aiWriterEnabled` directly to make a visibility decision —
all gates go through `isUnreleasedFeatureVisible` (main) /
`isUnreleasedFeatureVisibleInRenderer` (renderer). Doing otherwise bypasses the env
reveal path and will fail the gating lint test.

### Choosing the model

Two dropdowns pick the AI model. Since 2026-07-24 they live in **Settings → Plugins → AI
Writer**, declared in the plugin's own `manifest.json`:

- **AI model** (`writerModel`) — chat, selection edits and options variants together.
  Default **Automatic** (chat uses Sonnet; edits and options use Luna).
- **Autocomplete model** (`writerAutocompleteModel`) — inline autocomplete only. Default
  **Automatic** (Luna). Kept separate so a pricier main-model choice cannot silently make
  the always-firing autocomplete expensive.

They moved out of the Lab settings list to follow the convention every other plugin with
settings already uses: a plugin **declares** its settings in its manifest and AMC renders
them in that plugin's own card, storing values under `pluginSettings.writer`. RepoGuard,
prdstack, virtual-pets and daily-spend-report all do the same, and none reaches into the
app's general settings pages.

`resolveWriterModel` reads `pluginSettings.writer` and falls back to the legacy top-level
`AppSettings.writerModel` / `writerAutocompleteModel`, so a choice made before the
migration still applies; that fallback goes when the native surface retires. An empty
value is the manifest default meaning "Automatic" and is treated as ABSENT rather than as
an override, so the default cannot wipe an existing choice.

Available models: Luna / Haiku / Sonnet / Opus (the `WRITER_AI_MODELS` catalog in
`src/shared/types/writer.ts`).

### Setup wizard (first-run onboarding)

A **7-step setup wizard** opens automatically on the first Writer Studio visit —
once the `writer-setup-wizard` feature is turned on and `writerSetupComplete` is
false. It follows the same shared `Wizard`
component pattern as KMS Quick Reference — portal overlay, progress dots, keyboard
nav, focus trapping, cinematic welcome.

**Steps:** Welcome (cinematic fullBleed) → Writing Defaults (creativity dial + tone)
→ AI Editing → Rewrite & Autocomplete → AI Assistant → Integrations → Get Started.

- **Writing Defaults** (step 2) persists `writerDefaultCreativity` and
  `writerDefaultTone` to settings. New documents inherit these defaults via
  `createDocument` in `writer-store.ts`.
- **AI Editing / AI Assistant** (steps 3, 5) have a "Jump in and try it" button
  that creates a sample document and exits the wizard early.
- **Rewrite & Autocomplete** (step 4) has an interactive toggle for
  `writerAutocompleteEnabled` (ghost-text autocomplete).
- **Get Started** (step 7) action cards dispatch via `onComplete({ action })`;
  `WriterWizard.handleFinish` reads the step result and creates a blank doc,
  a sample doc, or triggers file/Drive import via callbacks from `WriterView`.
- **Early dismissal** (X / Escape / backdrop) sets `writerSetupComplete = true`
  AND `writerOnboardingCompleted = true`, preventing both the wizard and the legacy
  welcome screen from re-appearing.
- **Welcome coordination** — `shouldShowWriterWelcome()` returns false when
  `writerSetupComplete === false` (wizard takes priority); after the wizard
  completes, `writerOnboardingCompleted = true` suppresses the welcome too.
- **Replay** — Settings → Setup Wizards → "Writer Studio Setup" card navigates to
  the Writer view and reopens the wizard via `useWriterWizardStore`.
- **Feature-gated** via `writer-setup-wizard` in `UNRELEASED_FEATURES` — off by
  default (in-development), with `parent: 'ai-writer'` so it is hidden wholesale
  while Writer Studio is. Its `settingKey` is the dedicated `writerSetupWizardEnabled`,
  NOT `writerSetupComplete`: the completion marker means the opposite of an enable
  flag, and pointing the gate at it inverted the feature's own Settings toggle.

Source: `src/renderer/src/features/writer/wizard/`.

## How it behaves

### Two surfaces today: the native Writer and the AI Writer plugin

There are currently **two** Writer UIs in the repo, and they coexist on purpose.

- **The native surface** — `src/renderer/src/features/writer/`, opened from the
  "Writer" virtual project in the sidebar, talking to the `writer:*` IPC channels.
  This is the one users get, and everything else on this page describes it. The
  native editor now has full extension parity with the plugin: InlineAutocomplete
  (with Ctrl+Right word-by-word accept), FindReplace (Ctrl+F), and SlashMenu (/).
  Both the "More formal" and "Punchier" tone presets are in the native SelectionMenu.
- **The AI Writer plugin** — `src/plugins/writer/`, a first-party plugin whose UI is a
  standalone React app rendered inside an isolated `<webview>`. It is an in-progress
  port of the same feature, not a replacement, and not the shipped surface.

**Which one to edit:** unless the task explicitly says "the plugin", edit the native
surface.

What the plugin can do so far: a searchable document list, a document title header with
a save indicator, a Tiptap writing surface with the same 500 ms autosave, an empty
state, and a delete that asks for confirmation first and then offers a working undo.
It also has a working Assistant: open it from the button in the document header, ask a
question or request a change, and the reply comes back rendered as proper formatted text
rather than raw markdown. A conversation belongs to one document, so switching documents
shows that document's own history, and reopening a document brings its past chat back.
If a message fails, the panel says why in plain words and offers a Try again that resends
without duplicating what you typed.

The plugin can also edit a selection. Highlight some text and a small menu appears with
Improve, Fix grammar, Shorten and Expand, a box to type your own instruction, and three
alternative phrasings it can generate for you. Whatever you pick, nothing is rewritten
until you see it: every change opens a review showing exactly what would be added and
removed, and you Accept or Reject it (Escape rejects). Accepting counts as one edit, so a
single undo takes it back. The Assistant's "Apply suggested edit" now works the same way,
finding where the suggestion belongs in the document and opening the same review. When
the assistant replies with general content rather than a targeted edit, an "Insert into
document" button appears so you can add that text with one click. There is also an
**Auto-insert AI text** toggle in the plugin's settings: when on, the assistant's
suggested changes are applied to the document automatically without the review step.

Since then the plugin has reached broad parity. It also has inline autocomplete and the
writing-guidance panel with the creativity dial; folders with drag-and-drop filing and
pinning; version history with restore; a **formatting toolbar** (bold, italic,
strikethrough, inline code, bullet and numbered lists, quote, a paragraph-style dropdown,
links, divider and clear formatting); **import from Google Drive**, which lists your
Google Docs and turns the one you pick into a new Writer document; and **publish to Google
Docs**, which creates a real document in your Drive and links to it.

Publishing is a paid call, so it goes through one shared core (`publishMarkdownCore`) that
applies the export feature gate, a rate limit placed before the paid call, and a
content-hash idempotency guard — publishing the same text twice returns the first document
instead of minting a second. The renderer IPC handler calls that same core, so the two
transports cannot drift.

The plugin also supports **multi-select with bulk operations** (checkbox selection, select-all,
bulk delete), **inline document rename** (double-click a title or use the context menu),
**document cloning** ("Duplicate" in the context menu — atomically copies content, metadata,
and tags with a "Copy of" title prefix), **tagging** (add/remove tags via the context menu;
tag chips on document rows; a tag filter bar above the list), **font size and line spacing
controls** (in the Appearance menu, 12–24px font sizes and compact/normal/relaxed spacing
applied via CSS custom properties), and **Save as** local file export (Markdown or HTML via
a system save dialog, available from both the File menu and the document row context menu).

Still native-only: the pop-out Writer window and the share / unpublish artifact flow.

Removed rather than ported (2026-07-24): the native "Send to Writer Studio" actions in the
session context menu and under a Council verdict. A handoff that only works if you happen
to have the plugin installed is a worse experience than not offering it, so both were
deleted rather than duplicated.

How the plugin reaches data: a plugin webview cannot use IPC. Every call goes through
`window.AgentMC.writer.*`, an identity-gated plugin-bridge namespace (allowed because
the calling plugin's id is `writer`, NOT because the `ai-writer` flag is on) that reads
and writes `writer_documents` rows through the query functions in
`src/main/db/queries-writer.ts`, and `writer_templates` rows through
`src/main/db/queries-writer-templates.ts`.

How the plugin reaches the AI: through the same bridge, via `writer.aiEdit`,
`aiOptions`, `aiAutocomplete`, `aiChat` and `chatHistoryGet`. These call the SAME
`writer-ai-service` the native Writer uses, completely unchanged — which is the point.
That service tries an API-key account first and falls back to the user's Claude
subscription, so **someone signed in without an API key keeps working AI**, and the
models stay the cheaper Luna ones (GPT-5.6 via OpenRouter). Routing the plugin through the generic `ai` bridge
instead would have meant API-key-only, Sonnet-priced AI, and no AI at all for
subscription-only users.

Two behaviours differ from the rest of the bridge, deliberately. A failed edit, options
or chat call arrives in the plugin as a thrown Error carrying the same plain sentence the
native Writer shows — never a raw error code. Inline autocomplete never fails loudly at
all: any problem simply produces an empty suggestion, because ghost text appearing after
every typing pause must not raise an error message. Spend is unchanged: the same
cost tracking and the same daily Writer budget apply, because it is the same engine.

Theme: the plugin is a separate document and cannot read Omniscio's stylesheet, so the
host injects resolved theme values onto the guest page as CSS custom properties and the
plugin maps them into its own Tailwind theme. It follows light/dark, the accent colour,
the font stack and the app text size with no per-mode CSS of its own.

Build and tooling: the guest app lives in `src/plugins/writer/web/` with its own
`package.json` and TypeScript project, and compiles to the **committed**
`src/plugins/writer/ui/` bundle — rebuild it with
`pnpm --prefix src/plugins/writer/web build` and commit the output in the same change,
or the webview keeps serving stale code. Root `npm run typecheck` and `npm run lint`
deliberately exclude `web/` and `ui/`; run `npm run typecheck:writer` from the project root
to typecheck the plugin separately.

The plugin loads as an ordinary built-in for now (it is not in the plugin loader's
`BUILTIN_EXCLUDE` set). Making it marketplace-only, and deciding what happens to the
native surface, belongs to a later phase.

The plugin surface's own test-locked invariants are I27-I32 in
`.claude/memory/contracts/writer-contract.md`.

### Save state indicator

A **Saving… / Saved** pill appears in the document header whenever a write is in
flight or has just completed. It uses the `savingState` field in the writer Zustand
store (`'idle' | 'saving' | 'saved'`). `'saved'` requires the `writer:save-content` call to
resolve AND to come back carrying the saved row — a call can resolve having written
nothing, because the save is refused when another window saved first (a conflict) or when
the document has since been deleted. Both of those return the pill to `'idle'` and say what
happened rather than flashing a "Saved" the text never earned. `'saved'` reverts to `'idle'`
after 2 s, and the pill is hidden in `'idle'` so it does not distract while you type.

### Share / send action indicator

The same header slot carries a **spinner + label** while a longer share or send action is
running — "Sending…" for _Start a Session_, "Exporting…" for _Save to Vault_, "Publishing…"
for _Publish_, _Republish_ and _Unpublish_. It exists because every one of those actions is
started from an item in the **Share menu**, and that menu closes in the same click: a busy
state drawn inside it is never seen, so the window between the click and the confirmation
toast showed nothing at all. The indicator is derived once in `WriterView` and rendered by
the document header, which stays mounted for the whole operation.

### Loading states

Three load paths show a **shape-matching skeleton** rather than a centered spinner, so the
layout does not jump when the real content arrives:

- the document list, while it is being fetched;
- the editor column, while the active document's content is still unavailable (a first open,
  or any open that was not warmed by a hover);
- the editor chunk itself, while the lazily-loaded Tiptap bundle resolves.

The _Send to PM_ dialog likewise draws a skeleton of its dropdown fields, and each level of
the ClickUp workspace → space → folder → list cascade shows "Loading…" in its own field
rather than "No spaces" / "No folders" / "No lists" — an empty-looking dropdown that is
really still loading reads as missing data.

### Assistant history caching

Each document has its own Assistant conversation, and the history is fetched by a separate
call from the document body. Hovering a document in the list warms **both** — the body and
the conversation — so opening it paints immediately instead of showing an empty panel until
the round-trip lands. The warmed conversation is kept in a small bounded cache (the 10 most
recent documents), revalidated from the server on every open, and dropped as soon as you
send a message, so it can never show a conversation missing its newest turn.

### AI status banner

When an AI operation fails with a credential or cost-cap error, a persistent banner
appears below the document header (above the toolbar), naming what to fix. It persists
until dismissed (×) or the page reloads. In the plugin (Writer Studio) it is a calm
neutral strip with an accent icon — the guest webview theme exposes no amber "needs-you"
token, and this is setup guidance, not an error (native renders its own amber variant).

- **"AI features need an Anthropic API key"** (`aiStatus: 'no-key'`) — no usable credential
  (neither an API-key account NOR a signed-in Claude account). The banner names the path in
  TEXT — "Add one in AMC Settings → Accounts" — NOT a button, because a sandboxed guest
  webview cannot navigate the host to open Settings. Rarely seen for edits/options/chat,
  since any signed-in Claude account satisfies them via the subscription fallback.
- **"Today's AI budget is used up"** (`aiStatus: 'cost-cap'`) — the per-day writer cost cap
  was hit; it resets tomorrow.

The status is derived reactively from the AI error sentence via `toAiStatus()`
(`'API key'` → `'no-key'`, `'budget'` → `'cost-cap'`; `lib/ai-status.ts`). Whether to SHOW
the banner is the pure `resolveAiBannerStatus(aiStatus, dismissedAiStatus)`: dismissing
records the CURRENT status, so a later, DIFFERENT problem still re-surfaces it. It is
**reactive by design** — `aiStatus` is null until the first AI attempt fails, so a browsing
no-key user sees the banner only after they try AI (a proactive check is a later wave).
Rendered by `AiStatusBanner` (`App.tsx`); before Wave 1a the plugin computed `aiStatus` on
every AI outcome but rendered nothing, so this common first-run wall showed only a transient
toast.

### Word count status strip

A **word count + reading time** strip sits below the editor, shown only while a document is
open (hidden on the empty-state landing screen). It shows:

- Word count: `N word` / `N words`
- Reading time: `~N min read` at 238 wpm. Native shows `~1 min` down to an empty doc; the
  plugin omits the reading time over an empty document (0 words).

Words are counted by `Intl.Segmenter` at word granularity, NOT by splitting on whitespace.
A whitespace split cannot count a language that does not space its words, so a whole Chinese
or Japanese document counted as **one** word; it also counted markdown punctuation, making
`- a` and `# Title` two words each. One extra rule is load-bearing: the segmenter splits
`e-mail` into `e` / `-` / `mail`, so a lone `-` between two word-like segments continues the
same compound word — without it `state-of-the-art` would count 4.

Native computes it in `WriterEditorHost.tsx` (off a `useDeferredValue` copy of the content)
via `countWords` in `src/renderer/src/features/writer/lib/word-count.ts` — the CANONICAL
implementation. The **plugin (Writer Studio)** has the same strip (`WordCount.tsx` over
`lib/word-count.ts`), and because the island cannot import across the `src/` boundary it
carries a deliberate PORT of that function.

The two are held in agreement by `tests/unit/plugins/writer/writer-word-count-parity.test.ts`,
which runs BOTH implementations over one corpus, so editing either file alone fails the gate.
Parity used to rest on a comment asking the next person to change both files — and it did not
hold. Note the reading time still differs by design: native shows `~1 min` down to an empty
doc, the plugin omits it at 0 words.

### Document list view modes

The document sidebar supports three view modes, toggled via icon buttons in the
section header: **List** (default — one row per document with title and relative
timestamp), **Grid** (card layout with title, timestamp, and pin/share badges),
and **Compact** (dense single-line rows with title only). All three modes share
the same interaction behavior: click, drag-and-drop, multi-select, context menu,
folder grouping, and keyboard navigation. The choice persists in the
`writerDocViewMode` setting (`'list' | 'grid' | 'compact'`). The toggle uses the
WAI-ARIA tablist pattern (`useTabKeyboard`) for keyboard accessibility.

### Document list timestamps

Each document row in the sidebar shows a relative timestamp (`formatRelativeTime(doc.updatedAt)`)
below the title — e.g. "just now", "3 min ago", "yesterday". The delete button is
hidden until the row is hovered or focused (opacity-0 → opacity-100 via a CSS `group`
class), keeping the list uncluttered.

### Search documents

A **search box** at the top of the document sidebar filters the document list as
you type. It matches against the document title AND the full text inside the
document, so you can find a document by a phrase it contains, not just by name.
The search is debounced (~200 ms) so it does not fire on every keystroke. Clearing
the box (or pressing the ×) restores the full document list immediately, with no
IPC call. Empty or whitespace input is treated as no search.

### Folders & pin

Documents can be organized into **folders**, shown as collapsible groups in the
document sidebar (each document also sits under an always-present **Unfiled** group
for anything not filed away). A document lives in exactly one folder at a time, or
stays Unfiled — there's no filing the same document into two folders. Use the row's
**⋮** (actions) menu to move a document into a folder, and the **"New folder"**
button next to the **`+`** menu to create one. The **`+`** menu itself is a dropdown
with four items: "New document" (blank), "Upload" (local file import), "Import from
Google Docs", and "New from sample" (tutorial content). You can also **drag a document row
onto a folder** to file it there — this works even if the folder is collapsed — or
drag it onto the **Unfiled** group to take it back out of its folder; the **⋮**
"Move to folder" menu still works too, and is the option on phones since dragging is
desktop-only. The **⋮** menu also has **Edit title**, which turns the document's
title into an inline text input right in the sidebar row — commit with Enter or blur,
cancel with Escape (same pattern as folder rename). Deleting a folder never deletes the
documents inside it — they're moved back to Unfiled instead, and the delete itself
can be undone like any other delete. Within a folder (or Unfiled), you can **pin**
a document — via the star/pin control on its row — to keep it sorted at the top of
the group regardless of when it was last edited. While you're searching (see
"Search documents" above), the sidebar temporarily shows a flat list of matches
instead of the folder groups; it returns to folders once you clear the search.

### Version history

Writer Studio automatically saves version snapshots as you work — no manual "save
version" step needed. A snapshot is taken when you pause typing (at most about
once every few minutes, so it won't spam a snapshot per keystroke), whenever you
switch away from or close a document, and right before any AI change is applied
to your text (a selection edit accepted in the ReviewPanel, an options-mode
replacement, or an applied chat suggested edit).

A **History** button in the document header opens a panel listing past versions
newest-first, each labeled by why it was taken ("Auto-saved", "Before AI edit",
"Before restore") with a relative time (e.g. "5 min ago"). Selecting a version
previews it; a **"Show changes"** toggle highlights what's different from your
current document. Clicking **"Restore this version"** swaps it into the editor
and shows an Undo — your current text is saved to history first, so restoring
is always reversible.

Each document keeps its most recent **~50** versions; older ones are trimmed
automatically.

### Editor prose measure

The editor body is constrained to **70 characters** wide (`max-w-[70ch]`, centred)
to match comfortable reading and writing line lengths.

### Writing Guidance panel

The **GuidancePanel** occupies the bottom of the left sidebar, below the document
list. It contains four controls that the AI reads on every edit:

- **Goal** — a text field where you describe what the document is for (placeholder:
  "What is this document for?").
- **Audience** — a text field describing who the document is for (placeholder:
  "Who is it for?").
- **Tone** — a text field for the desired writing tone (placeholder: "e.g. warm,
  formal, punchy").
- **Creativity Dial** — a four-option segmented control with plain-language labels:

  | Level | Label    | Meaning                               |
  | ----- | -------- | ------------------------------------- |
  | 1     | Careful  | Minimal changes, preserves your voice |
  | 2     | Balanced | Tightens structure and phrasing       |
  | 3     | Creative | Reworks flow and adds depth           |
  | 4     | Bold     | Reinvents to hit the goal harder      |

  A footer confirms "Applies to all AI edits in this document."

All four values are stored in the `writer_documents` table and sent in every `writer:ai-edit`,
`writer:ai-options`, and `writer:ai-chat` call.

### Autosave

The editor auto-saves **500 ms** after you stop typing. The timer lives in a
module-private map (`pendingWrites`) outside Zustand state. Switching to another
document flushes the pending write for the current document first
(`flushPendingWrite`), so no content is lost on quick switches.

**Cross-window safety.** The same document can be open in two places at once
(the popped-out Writer window, or a paired phone). Each save carries the version the
window last saw, so if the _other_ window saved in between, this window's save is
**refused instead of silently overwriting** it — your in-progress text is kept, a small
"This document changed in another window — your version will save on your next edit."
toast appears once, and your very next edit saves cleanly on top of the newer version.

## Related

Writer Studio opens from the **Writer** virtual project in the sidebar — [Projects sidebar](projects-sidebar.md) explains how virtual projects sit there, and [App tour](app-tour.md) walks the surfaces around it. Its model dropdowns and first-run wizard live in Settings, indexed by [Settings reference](settings-reference.md). This page is split across [part 2](ai-writer-part-2.md) (the editor, document list and data model), [part 3](ai-writer-part-3.md) (the four AI capabilities), [part 4](ai-writer-part-4.md) (sharing, exports and integrations) and [part 5](ai-writer-part-5.md) (IPC channels and key files).
