---
title: Writer Studio (standalone writing editor) (part 4)
---

# Writer Studio (standalone writing editor) (part 4)

## What it is

This is part 4 of the [Writer Studio (standalone writing editor)](ai-writer.md) page. It covers everything a document can leave through — publishing to Shares and sending by email, SMS or Slack, PDF and Word export, Google Docs round-trips, local file import, and the Session, Mission Control, KMS vault, Team Chat and PM-tool bridges.

## Where to find it

Writer Studio opens from the **Writer** virtual project in the sidebar, and the [parent page](ai-writer.md) covers gating, surfaces and first-run setup. Inside the writer view these actions live in the document header — the **Publish**, **Export to Docs**, **Send to MC** and **Send to...** buttons — and in the **File** and **Share** menus; local file import is reached from the Welcome screen, the empty-state button and the sidebar **+** menu.

## How it behaves

### Publish to Shares

A **Publish** button in the document header publishes the current document's markdown
to Omniscio Shares, generating a shareable URL anyone can open — no Omniscio account
needed to view it. The document sidebar shows a small share icon on published
documents.

- **Publish** — calls `writer:publish`, which pushes the markdown content to Shares
  and stores the resulting share URL and token on the document row. The link is copied
  to the clipboard automatically (best-effort — a clipboard failure does not affect the
  publish itself).
- **Republish** — appears when a document is already published. Updates the existing
  share **in place** (same URL, new content) by passing the stored `republishToken` to
  `publishArtifact`. The old share is revoked only if the token changed.
- **Unpublish** — revokes the published link so it stops working. This is a destructive
  action (anyone with the old link loses access), so it opens a confirmation dialog
  first.
- **Published indicator** — when a document has a live share link, the header shows an
  accent-colored "Published" button that copies the link on click, plus Republish and
  Unpublish options.

Deleting a document automatically revokes its share link if one exists.
Empty documents cannot be published.

#### Send via channels

Published documents can be sent directly to recipients via email, SMS, or Slack from the
Share menu. When a document is published, the menu shows "Send via Email", "Send via SMS",
and "Send via Slack" items (hidden when not published).

Clicking any channel opens `WriterSendViaDialog` — a modal with a recipient field
(channel-appropriate placeholder from the `CHANNELS` catalog in `shares-detail-helpers.ts`)
and an optional message textarea. The dialog resolves the document's `shareToken` to a
`shareId` via the share store, then calls `IPC.SHARE_SEND` with `{ shareId, channelKind,
recipient, message }`. Success shows a toast; errors are humanized via `humanizeError`.

This reuses the existing share-send infrastructure (`src/main/ipc/share/send.ts`) — no new
IPC channels or backend changes. The `SHARE_SEND` handler validates recipients per channel
(email format, phone E.164, Slack channel name) and applies a 10/minute rate limit.

### File export (PDF / Word)

Writer documents can be exported as PDF or Word (DOCX) files from both the File menu and
the Share menu. Both entry points call the shared `useExportMarkdownToFile` hook
(`src/renderer/src/hooks/useExportMarkdownToFile.ts`) with the document's markdown content,
format (`'pdf'` or `'docx'`), title, and surface `'standalone'`.

- **PDF export** is always available.
- **Word export** is gated by the `wordExportEnabled` setting (matching the ExportMenu
  pattern used elsewhere in the app).
- Both items show a disabled state while an export is in progress.
- The hook handles empty-document warnings, file save dialogs, and error toasts internally.

### Import & export with Google Docs

Writer Studio can pull a Google Doc in and push a document out, reusing Omniscio's existing Google
plumbing — no Writer Studio-specific Google services.

- **Import from Drive** — the sidebar `+` menu "Import from Google Docs" item (and empty-state
  "Import" button) opens `WriterDriveImportDialog`, a `DialogShell` picker that supports two
  import methods: pasting a direct Google Doc/Sheet/Slides URL, or searching by keyword. The URL
  input sits above a divider; the keyword search lists the user's Google **Docs, Sheets, and
  Slides** (three parallel `drive:list-files` IPC calls, one per MIME type, merged and sorted by
  `modifiedTime`). Docs import via `GOOGLE_DOC_FETCH_MARKDOWN` (markdown export). Sheets import
  via `writer:import-google-sheet` (XLSX export → `extractOfficeText` → markdown table). Slides
  import via `writer:import-google-slides` (PPTX export → `extractOfficeText` → text outline).
  All three create a **new** Writer Studio document via `writer:create` + `writer:save-content`.
  The original file is untouched; import never overwrites the open document. Picker state lives
  in the dedicated `writer-drive-import-store.ts` slice (separate from the standalone Drive
  browser's `drive-store.ts`). File type labels ("Sheet", "Slides") appear next to each file in
  the picker via `google-apps-mime-labels.ts`. Not-connected / expired Google sessions show a
  "Connect Google" prompt routing to Settings → Google Workspace.
- **Export to Docs** — the header "Export to Docs" button publishes the open document's markdown to
  a **fresh** Google Doc via the shared `usePublishMarkdownToGoogleDoc` hook (surface `'writer'`).
  Re-exporting creates another new Doc — no linked/two-way sync. Export is gated by the existing
  `googleDocsExportEnabled` setting; when off, the hook shows a "turn it on in Settings" prompt.

Both entry points live inside the gated `ai-writer` surface. For offline E2E, `AMC_GOOGLE_FAKE=1`
returns deterministic Drive/Doc data without real OAuth.

### File import (local files)

Writer Studio can import local document files, converting them to markdown and creating a new
Writer document. Supported formats:

| Format   | Extension(s)       | Conversion method                                          |
| -------- | ------------------ | ---------------------------------------------------------- |
| DOCX     | `.docx`            | mammoth → turndown (HTML → markdown)                       |
| Markdown | `.md`, `.markdown` | Direct read (with UTF-8 BOM stripping)                     |
| Text     | `.txt`             | Direct read (with UTF-8 BOM stripping)                     |
| RTF      | `.rtf`             | Pure-JS control-word parser (extracts text + `\u` Unicode) |
| ODT      | `.odt`             | adm-zip → `content.xml` XML tag walking                    |
| EPUB     | `.epub`            | adm-zip → container.xml → OPF → spine → turndown           |

The import service (`src/main/services/writer/writer-file-import-service.ts`) is
self-contained — it does NOT extend the generic conversion engine. No new npm dependencies;
mammoth, turndown, and adm-zip were already in the tree.

- **Entry points** — the Welcome screen (2×2 grid "Upload a file" card), the empty-state
  "Upload a file" button, and the sidebar `+` menu "Upload" item all open a native file
  picker (`DIALOG_OPEN_FILE`) filtered to the supported extensions, then call
  `writer:import-file` → `handleImported({ title, markdown })` (the same callback Google Docs
  import uses, which creates a new document + navigates to it).
- **Safety** — ZIP-based formats (DOCX, ODT, EPUB) run through `assertArchiveNotBomb()`
  (total uncompressed XML capped at `MAX_DOCX_XML_BYTES`). File size capped at 200 MB
  (`MAX_FILE_BYTES`); converted markdown capped at 10 MB (`MAX_MARKDOWN_BYTES`). Image
  references are stripped (Writer is text-only). DRM-protected EPUBs are detected and
  rejected with a humanized error.
- **Desktop-only** — `writer:import-file` reads a local file path, so it is in
  `BLOCKED_CHANNELS` for web-access WS and exempt from CLI parity (`feature-scoped-surface`).
- **Tests** — `tests/integration/writer-file-import-service.test.ts` (63 tests:
  all formats + error cases + image stripping + DRM detection + BOM + size limits).

### Session bridge (Phase B2)

Writer Studio connects bi-directionally with Claude Code sessions:

- **Session → Writer Studio**: right-click a session in the dashboard → "Send to Writer
  Studio". This assembles the session's visible-only transcript as markdown, creates a new
  Writer document with that content, and navigates to it in Writer Studio. The action appears
  only when `ai-writer` is enabled and exactly one session is selected.
- **Writer Studio → Session**: click "Send to Session" in the Writer toolbar. This opens a
  new Claude Code session with the document's content pre-filled in the composer (not
  auto-sent), so you can continue working on it with Claude.
- **CLI**: `POST /writer/create-from-session` accepts `{ sessionId, title? }` and returns
  the new document metadata. Returns 404 for unknown sessions, 403 when the feature is
  disabled. Agents can use this to programmatically create Writer documents from session
  transcripts.

Both UI actions are gated by the `ai-writer` unreleased feature. The IPC channel
(`writer:create-from-session`) is blocked for mobile/web access (desktop-only — it reads
the local session database).

### Mission Control bridge

Writer documents can be linked to Mission Control (PM) tasks. The bridge requires BOTH `ai-writer`
AND `settings.missionControlEnabled` to be active (dual gate).

- **"Send to MC" button** in the Writer header opens a dialog (`SendToMCDialog`) where the user
  creates a new MC task (with board picker) or links to an existing one (via `PmItemPicker`).
- **Linked task chip** replaces the button once linked, showing the task name. Clicking navigates
  to Mission Control; the unlink button removes the association after a confirm dialog.
- **Dual-write pattern** — linking stores both a forward FK (`writer_documents.mc_item_id`) and a
  reverse row in `pm_item_links` (entity_type `writer_document`), transactionally.
- **Content sync** — when a linked Writer document is saved (non-conflict), `syncWriterContentToMcItem`
  in `writer-mc-bridge.ts` copies the document content to the MC item's `description` column
  (truncated at 5,000 graphemes with a `…(truncated)` suffix). The sync is fire-and-forget
  (try/catch, non-blocking) so a sync failure never blocks the document save. Initial sync also
  runs at link-creation time via `createMcItemAndLink`.
- **MC LinksTab** shows linked Writer documents with click-through navigation back to the Writer
  surface (via `navigateToEntity` on the session bridge).
- **CLI routes** — `GET/POST/DELETE /writer/documents/:id/mc-link` for headless linking;
  `POST /writer/documents/:id/mc-create-and-link` to atomically create a new MC item and link it.
- **Hook**: `useWriterMcLink(docId)` manages link state with auto-refresh.

### Vault (KMS) bridge

Writer Studio documents can be exported to and imported from the KMS knowledge base
(vault), creating a bi-directional markdown bridge between the two surfaces.

- **Export to Vault** — the Writer header's "Export to Vault" button saves the current
  document's markdown content as a KMS note. On first export, a new note is created in
  the user's default vault (path derived from the document title via `sanitizeTitleForPath`
  - `pickAvailableNoteRelativePath`), and the Writer document stores the link
    (`linked_kms_note_id` + `linked_kms_vault_id`). Subsequent exports update the existing
    linked note's body rather than creating a duplicate. If the link is lost (e.g. after a
    DB restore), a path-based reconciliation re-links an existing note at the expected path
    before creating a new one. The handler dual-gates on both `ai-writer` and KMS being
    enabled.
- **Open in Writer Studio** — the KMS file tree's context menu offers "Open in Writer
  Studio" on any note. If a Writer document already links to that KMS note, it is opened
  directly (no duplicate). Otherwise a new Writer document is created with the note's body
  as content, and the link is stored. Navigation dispatches `open-ai-writer` to switch to
  the Writer surface.
- **Link tracking** — the `linked_kms_note_id` column has a unique partial index (WHERE
  NOT NULL) preventing two Writer documents from linking to the same KMS note. Soft-deleted
  documents do not block the index. Both directions emit `KMS_NOTE_CHANGED` via `emitPush`
  so the KMS file tree refreshes without polling.

- **CLI**: `POST /writer/documents/:id/export-to-vault` mirrors the IPC handler but
  additionally tags the note with `writer-studio` and `exported` (merged with any
  content-derived tags). Idempotent via `X-Client-Request-Id`. Returns 403 when KMS
  is disabled, 409 when no vault is configured.

Both entry points are gated behind `ai-writer` AND KMS being enabled. The vault bridge
invariants are I34-I38 in `writer-contract.md`.

### Send to Team Chat

Writer documents can be shared to Team Chat channels from the Share menu. The dialog
(`SendToTeamChatDialog`) posts the document content as a message with an
`omniscio://writer/<docId>` deep-link appended. Messages longer than 8,000 characters are
truncated with a "(truncated; full document in Writer)" suffix.

- **Threading** — the first send stores the `clientMsgId` and `channelId` on the document
  (`writer_documents.team_chat_msg_id` / `team_chat_channel_id`). Subsequent sends to the same
  channel post as a **threaded reply** via `sendReply` (using the stored msgId as `parentId`).
  The button label changes to "Update in Thread" when a reference exists.
- **Deep-links** — the `writer` host is registered in `deep-link.ts`. Clicking an
  `omniscio://writer/<docId>` link dispatches `WRITER_OPEN_DOCUMENT` to navigate to the document
  in Writer Studio.
- **Deleted-channel recovery** — if the reply fails (e.g. the channel was deleted), the reference
  is cleared via `writer:clear-team-chat-link` and the message is re-sent as a fresh message with
  a "thread unavailable" warning toast.
- **IPC channels** — `writer:set-team-chat-link` and `writer:clear-team-chat-link` store/clear
  the reference. Both are desktop-only.

#### Bidirectional sync (F001)

Linked Writer documents support bidirectional sync with their vault notes:

- **Sync on Save** — a global toggle (`writerKmsSyncOnSave` in `kmsSettingsSchema`,
  default off) that auto-pushes the document's content to the linked vault note on every
  save. The sync is fire-and-forget (does not block the save response) and is gated on
  KMS being enabled, the setting being on, and the document having a link. If the vault
  note was modified externally since the last sync (determined by comparing
  `note.updatedAt > meta.kmsSyncedAt`), the push is skipped and a warning is logged to
  avoid overwriting external changes. The toggle appears as a "Sync on Save" menu item
  with a checkbox indicator in the Share menu, visible only when the document is linked.
- **Pull Latest from Vault** — a menu item in the Share menu that replaces the Writer
  document's content with the latest vault note body. Before overwriting, a pre-import
  version snapshot is created (`appendWriterVersion` with reason `'pre-import'`) so the
  user can restore via the version history panel. The pull updates `kms_synced_at` on
  success and refreshes the sync status indicator.
- **Sync status indicator** — a colored dot on the "Save to Vault" / "Update in Vault"
  menu item showing the current sync state: green (in-sync), amber (writer-ahead or
  vault-ahead), red (conflict — both sides changed since last sync). The status is
  computed by comparing `meta.updatedAt`, `note.updatedAt`, and `meta.kmsSyncedAt`
  via the `WRITER_KMS_SYNC_STATUS` IPC handler.
- **Conflict handling** — when both the Writer document and vault note have changed
  since the last sync (`meta.updatedAt > kmsSyncedAt` AND `note.updatedAt > kmsSyncedAt`),
  the status reports `'conflict'` (red dot) and sync-on-save is skipped. The user
  resolves conflicts manually via Pull Latest (which snapshots first) or Export
  (which overwrites the vault note).
- **`kms_synced_at`** — a new `TEXT` column on `writer_documents` (migration
  `20260902214334`) that records the ISO timestamp of the last successful sync in
  either direction (export or pull). Both the export handler and pull handler update
  this column on success. It is exposed in `WriterDocumentMeta` and the plugin bridge
  schema.

### Send to PM tool (Phase C3)

A "Send to..." dropdown in the document header creates a task/card/issue in a connected PM tool
(ClickUp, Trello, Jira, or Arij) with the document content as the description. Each PM tool
appears only when its integration is visible (unreleased-feature-gated) and connected (credentials
exist). The dialog shows cascading selects for the provider's hierarchy (e.g. ClickUp:
Workspace → Space → Folder → List). The same flow is also accessible from the **Share menu**
("Send to PM Tool" item, gated on `pmEnabled`).

After creation, the external URL and provider are stored on the `writer_documents` row
(`external_url`, `external_provider`), and a linked indicator appears in the document header.
Sending again overwrites the previous link. Trello descriptions are truncated at 16,000 chars.

Both new IPC channels (`writer:pm-status`, `writer:send-to-pm`) are desktop-only
(BLOCKED_CHANNELS in the WS bridge) and CLI-parity-exempt (`feature-scoped-surface`).
Credentials stay in main — the renderer never sees API keys.

## Related

The editor and document list the document is written in are in [part 2](ai-writer-part-2.md), the AI capabilities that edit it in [part 3](ai-writer-part-3.md), and the IPC channels behind these bridges in [part 5](ai-writer-part-5.md); the [Writer Studio (standalone writing editor)](ai-writer.md) page is the parent.
