Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Writer Studio (standalone writing editor) (part 3)

Part 3 of the Writer Studio page: the four AI capabilities the editor ships — selection edits and their review flow, options-mode variants, inline ghost-text autocomplete, and the chat assistant with its suggested edits, persisted history and voice context.

What it is

This is part 3 of the Writer Studio (standalone writing editor) page. It covers the four AI capabilities the editor ships — selection edits and their review flow, options-mode variants, inline ghost-text autocomplete, and the chat assistant with its suggested edits, persisted history and voice context.

Where to find it

Writer Studio opens from the Writer virtual project in the sidebar, and the parent page covers gating, surfaces and first-run setup. The selection bubble menu and the header toggles for Options mode, Autocomplete and the Assistant are all in the writer view itself; the credential cascade and the daily cost cap that sit behind every one of them are described below.

How it behaves

Selection edits (AI — Plan 2)

How it works

Select any text in the editor and a bubble menu appears with these actions:

Action What it does
Improve Rewrites the selection for clarity and flow
Fix grammar Corrects grammatical and spelling errors
Shorten Condenses the selection without losing key meaning
Expand Elaborates the selection with more detail
More formal (plugin) A more formal, professional tone — a canned custom instruction
Punchier (plugin) Punchier and more concise — a canned custom instruction
Translate (plugin) Translates the selection — a canned custom instruction
Free-text You type a custom instruction; the model follows it

The plugin's presets live in lib/selection-presets.ts; the tone presets "More formal", "Punchier" and "Translate" reuse the custom action with a canned instruction, so they need no new bridge method. Every preset lands in the review flow below.

Choosing an action calls writer:ai-edit on the main process, which calls the Anthropic Messages API or OpenRouter (the model is the user-selected Writer Studio model — default Luna) and returns the rewritten text. The document is not touched at this point.

Review flow

The client word-diffs the original selection against the rewritten version (using the diff package) and renders a ReviewPanel showing additions and deletions inline. The panel is positioned at the viewport coordinates of the selection start (editor.view.coordsAtPos(review.from)) and clamped to the window via containToViewport so it never renders off-screen. Press Esc to reject without reaching for the mouse.

  • Accept — replaces the captured selection range in the editor with the rewritten text (insertContentAt); the editor briefly flashes an accent outline (600 ms, suppressed under prefers-reduced-motion) to confirm the change landed.
  • Reject — discards the rewritten text; the editor is unchanged.

Nothing is written to the document until you click Accept.

Credentials — three-step cascade

writer:ai-edit resolves credentials through a three-step cascade (runWriterCompletion in writer-ai-service.ts):

  1. OpenRouter model (default path) — when the resolved model id contains / (e.g. Luna, the default for edits/options/autocomplete), the request routes through llmProviderService.chat() using the bundled company-paid OpenRouter key. No user credentials are needed — this is the zero-setup path that works out of the box. Cost-tracked and daily-capped against the writer budget.
  2. API-key account — for bare Claude model ids (Haiku/Sonnet/Opus), calls the Anthropic Messages API directly; fast, cost-tracked, and daily-capped.
  3. Subscription login (fallback) — if no API key is configured but you are signed in to a Claude account, Omniscio runs the request through claude -p (the same one-shot bridge the Council uses) on your subscription. No API key is needed — it rides your Claude plan. This is the sanctioned path (it is Claude Code); the Messages API still rejects OAuth tokens directly, so the subscription path spawns the CLI instead of calling the API.

Only when you have neither a key nor a signed-in Claude account AND the model is not an OpenRouter model does the handler return a humanized "add an API key" error — it never crashes.

Daily cost cap

Every successful edit is logged with trackApiCost(accountId, 'writer', …). Before each call, checkWriterCap checks the accumulated cost against the writerCostCapUsd setting (default $2 / day, range $0.50–$50). Edits are refused once the cap is reached. The cap resets at midnight local time. The cap applies to the OpenRouter and API-key paths. On the subscription-login path, edits ride your Claude plan's own limits instead.

The model is resolveWriterModel('edit') — the user-selected writerModel (default Automatic = Luna); no streaming. Typical edits cost fractions of a cent; the cap is a safety guard, not a practical limit.

Feature gating

Selection edits are part of the ai-writer gated surface — they are only available when the Writer Studio feature itself is visible (see "How it is gated" above).

Options mode (AI — Plan 3)

How it works

Enable Options mode via the toolbar toggle. With Options mode ON, selecting text triggers the SelectionMenu (a Tiptap BubbleMenu) to show N alternative versions of the selected segment inline. The scope (sentence vs paragraph) is derived automatically by deriveOptionsScope from the selection — it is never a user-facing tab. Press Esc to dismiss.

Alternatives are generated on demand when a non-empty selection is made (NOT on a bare caret click, NOT pre-generated in a background queue). The client caches results by scope:contentHash(text), so re-selecting the same text instantly re-shows the same options at no cost. The Refresh button bypasses the cache and regenerates.

Picking an alternative replaces exactly the selected segment's range in the editor via insertContentAt — nothing outside the active scope is touched.

Scope detection

Scope is derived from the selection by deriveOptionsScope: if the selection fits within a single sentence (detected via Intl.Segmenter, granularity 'sentence'), the scope is 'sentence'; otherwise it is 'paragraph' (the full ProseMirror text block).

IPC channel

writer:ai-options (WRITER_AI_OPTIONS) receives the selected text and scope, calls the AI provider (OpenRouter for Luna, Anthropic for Claude models), and parses the response by splitting on --- separators to produce N distinct alternatives. The response is plain text (no streaming).

Credential and cost guardrails

writer:ai-options shares all the same guardrails as selection edits:

  • Resolves credentials via the same three-step cascade as edits (runWriterCompletion): OpenRouter bundled key (default Luna path), else API-key account, else signed-in Claude account (claude -p). No credential on any path → humanized error, no crash.
  • Checks and logs cost against the same 'writer' daily cap (checkWriterCap / writerCostCapUsd, default $2 / day) on all paths. The cache means cached hits never incur a new call.
  • Model: resolveWriterModel('options') — user-selected writerModel (default Luna). No streaming.

Offline testing

The E2E spec uses AMC_WRITER_AI_FAKE=1, which makes the service return deterministic fake alternatives without calling the real API.

Inline autocomplete (AI — Plan 4)

How it works

Enable Autocomplete via the header toggle (or Settings → Writer Studio → Autocomplete; writerAutocompleteEnabled, default OFF). When enabled, after a ~400 ms typing pause with the cursor at the end of a paragraph that is at least 15 characters long, Omniscio calls writer:ai-autocomplete and renders the returned continuation as grey ghost text inline in the editor (a ProseMirror widget decoration styled .writer-ghost-text / data-writer-ghost).

  • Tab — accepts the suggestion, inserting it at the cursor and clearing the ghost text.
  • Any document change, selection move, or Escape — immediately clears the ghost text without inserting anything.

The suggestion is intentionally short (model: resolveWriterModel('autocomplete') — user-selected writerAutocompleteModel, default Luna; max_tokens 40) — it is a sentence-completion nudge, not a paragraph generator.

Guardrails

  • Off by default — the toggle must be explicitly enabled; no autocomplete fires on a fresh install.
  • Trigger conditions — selection must be collapsed (no range selection) and positioned at the very end of the paragraph; the paragraph must be ≥15 characters. Neither the debounce (~400 ms) nor the min-length check can be bypassed.
  • Stale-drop — a monotonically increasing sequence counter is incremented on every request; responses that arrive for an earlier counter value are silently discarded, preventing out-of-order flicker.
  • No background pre-generation — no speculative calls are made while the user is not typing.

Credential and cost guardrails

writer:ai-autocomplete does NOT use the shared runWriterCompletion helper and does NOT fall back to a subscription login (a per-keystroke claude -p spawn would feel laggy for inline ghost text). It has two credential paths:

  • OpenRouter model (default) — when the resolved model contains / (Luna, the default), autocomplete routes through llmProviderService.chat() using the bundled OpenRouter key. No user credentials are needed — any user with autocomplete enabled gets ghost text out of the box.
  • Bare Claude model — when the user explicitly selects Haiku/Sonnet/Opus, resolves an API-key account via resolveApiKeyForAiFeatures(). No key → empty suggestion (no toast).
  • Checks cost against the same 'writer' daily cap (checkWriterCap / writerCostCapUsd, default $2 / day) on BOTH paths. Cap hit → empty suggestion (no toast).
  • Model: resolveWriterModel('autocomplete') (default Luna), max_tokens 40. No streaming.

Silent failure — no toast storm

Unlike writer:ai-edit and writer:ai-options, the autocomplete handler never surfaces a toast or error message on failure. Any soft error (no key, cap hit, rate limit, network error) returns an empty string so the ghost text simply does not appear. This prevents a toast storm on every keystroke when, for example, no API key is configured.

Offline testing

The E2E spec (tests/e2e/ui/writer-autocomplete.spec.ts) uses AMC_WRITER_AI_FAKE=1, which makes the service return a deterministic fake continuation without calling the real API.

Chat assistant (AI — Plan 5)

The panel

A ChatPanel slides in on the right side of the editor when you click the Assistant toggle in the header. The panel is resizable — drag its left edge to set the width (300–720px), double-click the edge to reset to the 400px default, and the chosen width is remembered across sessions (the writerAssistantWidth setting, driven by the shared useResizeSidebar hook). Toggling hides/shows the panel without losing the conversation. The chat input receives autoFocus on mount so you can start typing immediately after opening.

If an AI request fails, an inline error bubble appears in the chat history with a "Try again" button that replays the last user message (retryChat in the store). The store's aiStatus field also drives the ambient AI banner (see above).

Protocol

Each user turn is sent to writer:ai-chat (WRITER_AI_CHAT). The handler calls the Anthropic Messages API (the user-selected Writer Studio model (writerModel); default Automatic = Sonnet for chat — low-frequency and capped at 2048 tokens, so the stronger model is used for reliable, applicable edits) and returns a structured response. The model is instructed to wrap its reply in two XML sections:

<conversation>
  …free-form reply to the user…
</conversation>
<suggested_edits>
  <edit>
    <search>exact text to find in the document</search>
    <replace>replacement text</replace>
  </edit>
  …additional edits…
</suggested_edits>

The parseChatResponse helper parses this into { conversation: string, edits: SuggestedEdit[] }. If the model omits <suggested_edits>, edits is an empty array. Malformed XML or a dropped <conversation> wrapper is treated as a pure conversation reply. The conversation text is rendered as markdown in the assistant bubble (via the shared sanitized ProseBlock renderer — headings, lists, bold, links, code, tables); user messages stay plain text.

Suggested edits through the review flow

Each SuggestedEdit in the response shows an "Apply suggested edit" button that previews what the change becomes (→ <replace>, or → (delete)). Clicking it calls findTextRangeInDoc, which searches the ProseMirror document for the <search> text within a single text block. It first tries an exact substring match; if that fails it falls back to a tolerant match (runs of whitespace, letter case, and straight-vs-smart quotes/dashes are all treated as equivalent) so a near-miss quote from the model still applies, landing on the real document characters. When found, the match range is passed to the EXISTING Plan 2 review flow: the store transitions to review state and the same ReviewPanel renders, showing the diff between the original and replacement text. The user clicks:

  • Accept — insertContentAt replaces the found range with the <replace> text.
  • Reject — the store returns to idle; the document is unchanged.

There is no second review UI for chat-suggested edits — they go through the identical ReviewPanel path as bubble-menu selection edits.

If the search text is not found in the document even after the tolerant match (e.g. the user rewrote that passage since the response arrived), applySuggestedEdit returns false and a friendly toast is shown instead of opening the review panel — never a silent no-op.

Persisted history (per document)

Chat messages are persisted per document in the writer_chat_messages table (a child of writer_documents). Each turn — your message plus the assistant's reply — is saved by the writer:ai-chat handler on success (a failed turn saves nothing, so retrying it never creates a duplicate). Switching to another document loads that document's saved conversation; reopening a document you chatted in before restores the whole conversation, including the assistant's "Apply" buttons (the suggested edits are stored alongside each reply). A new, never-chatted document opens with an empty panel.

Each document keeps up to ~200 messages; older messages beyond that are trimmed automatically. Documents are global per install — chat history has no account_id and is the same regardless of which Anthropic account is active. Deleting a document hides its chat history along with it (and restoring the document brings the conversation back).

Voice context integration

The chat assistant automatically receives two pieces of context from other Omniscio integrations, when available:

  • Voiceprint style guide — if the user has completed a Voiceprint Studio calibration, the resulting markdown style guide (derived from their actual sent emails) is injected into the system prompt as a "Your writing voice" section. The assistant uses it to match the user's natural tone in any suggested edits or drafted text. Resolved via resolveEmailVoiceGuide(db), capped at 4,000 characters.
  • Sender engagement signals — if the document's Audience field contains an email address or name that matches a known sender in SuperMail, the assistant receives a brief "Relationship context" summary (reply frequency, engagement level, last reply date). This helps it calibrate formality and familiarity. Resolved via readSenderScores().

Both are resolved by a shared resolveWriterVoiceContext(db, audience) function (src/main/services/writer/voice-context.ts) called in both the bridge and native IPC transports before chatWriterText(). Both are optional: missing voice guide or unknown sender silently produces no injection, and all existing callers are unaffected.

Credential and cost guardrails

writer:ai-chat shares all the same guardrails as the other AI capabilities:

  • Resolves credentials via the same three-step cascade as edits (runWriterCompletion): (1) OpenRouter bundled key for models whose id contains /, (2) API-key account for bare Claude model ids, (3) subscription login via claude -p. No key AND no login AND no OpenRouter model → humanized error in the chat panel, no crash.
  • On the API-key path, checks and logs cost against the same 'writer' daily cap (checkWriterCap / writerCostCapUsd, default $2 / day); the subscription path is uncapped.
  • Model: resolveWriterModel('chat') — user-selected writerModel (default Sonnet). No streaming.

Offline testing

The E2E spec (tests/e2e/ui/writer-chat.spec.ts) uses AMC_WRITER_AI_FAKE=1, which makes the service return a deterministic fake response (a markdown conversation plus a <suggested_edits> block) without calling the real API. Panel resize persistence has its own spec, tests/e2e/ui/writer-assistant-resize.spec.ts.

Related

The editor these capabilities act on is in part 2, the exports and integrations around them in part 4, and the IPC channels they call in part 5; the Writer Studio (standalone writing editor) page is the parent.

Last verified 2026-09-23