tandem-editor 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,6 @@ Have you ever been working on a piece of writing with an LLM and caught yourself
6
6
 
7
7
  And because Tandem hooks into Claude as an MCP server, you're not stuck in some stripped-down document-editing silo. It's the full Claude — with all its knowledge, your conversation context, and every tool it has access to — just now it can also see and edit your document.
8
8
 
9
- ![Tandem editor showing a document with annotations, side panel, and Claude's presence](docs/screenshots/01-editor-overview.png)
10
-
11
9
  ## Why Tandem?
12
10
 
13
11
  - **No more copy-paste ping-pong.** Select text in the editor, and Claude reads your selection directly. Ask "what do you think of this?" or "make this more concise" — Claude knows exactly which text you mean.
@@ -29,7 +27,7 @@ Requires **Node.js 22+** ([download](https://nodejs.org)) and **Claude Code** (`
29
27
  ```bash
30
28
  npm install -g tandem-editor
31
29
  tandem setup # registers MCP tools + installs Claude Code skill
32
- tandem # starts server + opens browser
30
+ tandem # starts server + opens editor
33
31
  ```
34
32
 
35
33
  `tandem setup` auto-detects Claude Code and Claude Desktop, writes MCP configuration, and installs a skill (`~/.claude/skills/tandem/SKILL.md`) that teaches Claude how to use Tandem's tools effectively. Re-run after upgrading (`npm update -g tandem-editor && tandem setup`).
@@ -73,7 +71,7 @@ claude --dangerously-load-development-channels server:tandem-channel
73
71
 
74
72
  This is the magic-sauce mode — and it's the one I'd recommend you run with. The channel shim pushes events (selections, annotations, chat) to Claude over SSE the moment they happen, so Tandem genuinely feels like there's another person on the other end of the document: someone watching what you highlight, reacting to edits you accept, and chiming in on a paragraph the instant you select it, the way a collaborator on a Google Doc would. The `--dangerously-load-development-channels` flag is an experimental Claude Code feature, which is why it isn't on by default — but turning it on is what makes the whole experience click.
75
73
 
76
- **Recommended layout:** snap the Claude Code terminal to one side of your screen and the Tandem browser window to the other. You'll be flipping attention between them constantly, and having both visible is what makes the side-by-side-collaborator feeling land.
74
+ **Recommended layout:** snap the Claude Code terminal to one side of your screen and the Tandem editor window to the other. You'll be flipping attention between them constantly, and having both visible is what makes the side-by-side-collaborator feeling land.
77
75
 
78
76
  Then try:
79
77
 
@@ -81,11 +79,11 @@ Then try:
81
79
  "Open sample/welcome.md and review it with me"
82
80
  ```
83
81
 
84
- Claude calls `tandem_open`, the document appears in the browser, and you're ready to collaborate.
82
+ Claude calls `tandem_open`, the document appears in the editor, and you're ready to collaborate.
85
83
 
86
84
  #### The core loop — no copy/paste
87
85
 
88
- 1. Highlight a paragraph in the browser editor.
86
+ 1. Highlight a paragraph in the editor.
89
87
  2. With channels on, Claude often reacts before you even say anything. Otherwise, just type what you want in the terminal: *"what do you think of this paragraph?"* or *"rewrite this to be more concise"*.
90
88
  3. Claude reads your selection directly from the shared Tandem state (via `activity.selectedText` on `tandem_checkInbox`). You never paste the passage into the terminal.
91
89
  4. Claude replies in the Tandem chat sidebar (`tandem_reply`) or drops annotations on the document (`tandem_annotate` / `tandem_suggestEdit`), which you can accept, dismiss, or edit in the side panel.
@@ -119,7 +117,7 @@ Or check the raw health endpoint:
119
117
 
120
118
  ```bash
121
119
  curl http://localhost:3479/health
122
- # → {"status":"ok","version":"0.4.0","transport":"http","hasSession":false}
120
+ # → {"status":"ok","version":"0.8.0","transport":"http","hasSession":false}
123
121
  ```
124
122
 
125
123
  `hasSession` becomes `true` once Claude Code connects.
@@ -131,7 +129,7 @@ curl http://localhost:3479/health
131
129
  git clone https://github.com/bloknayrb/tandem.git
132
130
  cd tandem
133
131
  npm install
134
- npm run dev:standalone # starts server (:3478/:3479) + browser client (:5173)
132
+ npm run dev:standalone # starts server (:3478/:3479) + editor client (:5173)
135
133
  ```
136
134
 
137
135
  Open http://localhost:5173 — you'll see `sample/welcome.md` loaded automatically on first run. The `.mcp.json` in the repo configures Claude Code automatically when run from this directory.
@@ -142,7 +140,7 @@ Open http://localhost:5173 — you'll see `sample/welcome.md` loaded automatical
142
140
 
143
141
  You point at text, Claude sees it. Here's how that plays out day-to-day:
144
142
 
145
- - **Open a document.** Ask Claude (`"let's work on notes.md in tandem"`), drag a file onto the browser, or click the **+** in the tab bar. `.md`, `.txt`, `.html`, and `.docx` (review-only) are supported.
143
+ - **Open a document.** Ask Claude (`"let's work on notes.md in tandem"`), drag a file onto the editor, or click the **+** in the tab bar. `.md`, `.txt`, `.html`, and `.docx` (review-only) are supported.
146
144
  - **Point at what you mean.** Select text in the editor and ask Claude about "this paragraph" in the terminal — or just wait for Claude to react if you have channels on. Claude reads your selection directly, no copy-paste needed. Hold the selection for about a second so it registers (dwell-time gating filters out incidental clicks).
147
145
  - **Iterate on Claude's response.** Claude's suggestions appear as annotations in the side panel — accept, dismiss, edit, or ask follow-up questions. Each round refines the text without you ever leaving the document. Press **Ctrl+Shift+R** for keyboard review mode: **Tab** to navigate, **Y** accept, **N** dismiss, **E** edit, **Z** undo within a 10-second window.
148
146
  - **Heads-down vs collaborative.** Toggle **Solo** mode when you want to write without interruptions — Tandem queues non-urgent annotations until you flip back to **Tandem** mode. Both `tandem_status` and `tandem_checkInbox` return the current mode so Claude adapts its behavior automatically.
@@ -154,20 +152,14 @@ Everything in Tandem is built around one idea: you work in the document, Claude
154
152
 
155
153
  ### Chat
156
154
 
157
- ![Chat sidebar showing messages, typing indicator, and panel toggle](docs/screenshots/02-chat-sidebar.png)
158
-
159
155
  Send messages to Claude alongside your document. Select text before sending to attach it as context — Claude sees exactly what you mean. Clicking an anchored selection later scrolls back to that passage.
160
156
 
161
157
  ### Annotations
162
158
 
163
- ![Side panel showing annotation cards with filtering, bulk actions, and text previews](docs/screenshots/03-side-panel.png)
164
-
165
159
  This is how Claude's feedback shows up in the document. Claude adds highlights, comments, suggestions, and flags directly on the text. Suggestion cards show a visual diff — original text in red strikethrough, replacement in green. The side panel lists all annotations with filtering by type, author, and status. Accept, dismiss, or edit each one individually — or use bulk actions to process them in batches.
166
160
 
167
161
  ### Review Mode
168
162
 
169
- ![Review mode with dimmed editor and active annotation highlighted](docs/screenshots/05-review-mode.png)
170
-
171
163
  Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, accept with **Y**, dismiss with **N**, examine with **E**. A 10-second undo window with a visual countdown lets you reverse accidental accepts. Shortcut hints appear below the Review button.
172
164
 
173
165
  ### More
@@ -183,42 +175,52 @@ Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, acc
183
175
  - **Configurable display name** — set your name so Claude knows who's reviewing
184
176
  - **Atomic file saves** — write to temp, then rename, preventing partial writes
185
177
  - **E2E tested** — Playwright tests cover the annotation lifecycle end-to-end
178
+ - **Authorship text coloring** — blue for your edits, orange for Claude's, toggled per-document
179
+ - **Threaded annotation replies** — back-and-forth conversation on any annotation
180
+ - **Auto-save** — documents save on change; Ctrl+S for manual trigger
181
+ - **Settings popover** — Light/Dark/System theme, text size (S/M/L), reduce motion, display name (Ctrl+,)
182
+ - **Auth tokens for LAN exposure** — bind to `0.0.0.0` with auto-generated tokens; `tandem rotate-token` for rotation
183
+ - **Durable annotation persistence** — annotations survive server restarts independently of session files
184
+ - **Claude Code plugin** — `tandem mcp-stdio` + `tandem channel` bridge Tandem into Cowork and Claude Desktop
186
185
 
187
186
  ## Where Tandem is headed
188
187
 
189
- Tandem v0.4.0 ships a native desktop app (macOS, Linux, Windows) alongside the existing npm CLI. A few directions on the radar for later releases:
188
+ Since the v0.4.0 desktop app launch, Tandem has added auth tokens for LAN exposure (v0.7.0), a Claude Code plugin bridge for Cowork and Claude Desktop (v0.6.0+), durable annotation persistence, settings with Light/Dark/System theming, authorship text coloring, and coordinate system correctness fixes with semantic token enforcement (v0.8.0). A few directions on the radar for later releases:
190
189
 
191
- - **High-fidelity .docx round-trip** — current `.docx` support is review-only; LibreOffice-headless-based production export is planned so you can stay in Tandem through the final draft.
192
- - **Claude Desktop parity** — the MCP server already works with Claude Desktop; polish and documentation for a first-class experience there is in the works.
190
+ - **High-fidelity .docx round-trip** — current `.docx` support is review-only; production export is planned so you can stay in Tandem through the final draft.
193
191
  - **Exportable annotated documents** — PDF (and eventually `.docx`) with annotations baked in, so you can share reviewed drafts outside Tandem.
194
192
  - **Code editing mode** — CodeMirror 6 surface for reviewing code the same way you review prose.
195
- - **Standalone mode** — direct Anthropic API connection so Tandem can run without Claude Code in the loop, for users who want a pure browser-based experience.
193
+ - **Standalone mode** — direct Anthropic API connection so Tandem can run without Claude Code in the loop, for users who want a pure standalone experience.
196
194
 
197
195
  See the full [Roadmap](docs/roadmap.md) and [Known Limitations](docs/roadmap.md#known-limitations-v1) for the complete picture, including items that are explicitly out of scope for v1.
198
196
 
199
197
  ## Documentation
200
198
 
201
- - **[User Guide](docs/user-guide.md)** — How to use Tandem: browser UI, annotations, chat, review mode, keyboard shortcuts
202
- - [MCP Tool Reference](docs/mcp-tools.md) — 31 MCP tools + channel API endpoints
199
+ - **[User Guide](docs/user-guide.md)** — How to use Tandem: editor UI, annotations, chat, review mode, keyboard shortcuts
200
+ - [MCP Tool Reference](docs/mcp-tools.md) — 28 MCP tools + channel API endpoints
203
201
  - [Architecture](docs/architecture.md) — System design, data flows, coordinate systems, channel push
204
202
  - [Workflows](docs/workflows.md) — Claude Code usage patterns: text iteration, cross-referencing, multi-model
205
203
  - [Roadmap](docs/roadmap.md) — Phase 2+ roadmap, known issues, future extensions
206
- - [Design Decisions](docs/decisions.md) — ADR-001 through ADR-022
207
- - [Lessons Learned](docs/lessons-learned.md) — 37 implementation lessons
204
+ - [Design Decisions](docs/decisions.md) — ADR-001 through ADR-024
205
+ - [Lessons Learned](docs/lessons-learned.md) — 48 implementation lessons
208
206
 
209
207
  ## CLI Commands
210
208
 
211
209
  | Command | What it does |
212
210
  |---------|-------------|
213
- | `tandem` | Start server and open browser (global install) |
211
+ | `tandem` | Start server and open editor (global install) |
214
212
  | `tandem setup` | Register MCP tools with Claude Code / Claude Desktop |
215
213
  | `tandem setup --force` | Register to default paths regardless of auto-detection |
216
214
  | `tandem --version` | Show installed version |
217
215
  | `tandem --help` | Show usage |
216
+ | `tandem setup --with-channel-shim` | Also register the stdio channel shim |
217
+ | `tandem rotate-token` | Rotate auth token (60-second grace window) |
218
+ | `tandem mcp-stdio` | Run as stdio MCP server (proxy to local HTTP, for plugin bridge) |
219
+ | `tandem channel` | Run the channel shim (stdio MCP for plugin's tandem-channel entry) |
218
220
 
219
221
  ## MCP Configuration
220
222
 
221
- Tandem registers two MCP connections: **HTTP** for document tools (30 tools including annotation editing — always on), and a **channel shim** for real-time push notifications. The channel shim is what enables the live-collaborator experience described in [Connect Claude Code](#connect-claude-code) and is recommended; it activates when you start Claude Code with `--dangerously-load-development-channels server:tandem-channel`. If you'd rather not pass that experimental flag, the entry sits idle and everything still works through polling on the HTTP connection — you just lose spontaneous reactions.
223
+ Tandem registers two MCP connections: **HTTP** for document tools (28 tools including annotation editing — always on), and a **channel shim** for real-time push notifications. The channel shim is what enables the live-collaborator experience described in [Connect Claude Code](#connect-claude-code) and is recommended; it activates when you start Claude Code with `--dangerously-load-development-channels server:tandem-channel`. If you'd rather not pass that experimental flag, the entry sits idle and everything still works through polling on the HTTP connection — you just lose spontaneous reactions.
222
224
 
223
225
  **Global install** (`tandem setup`): Automatically writes both entries to `~/.claude/mcp_settings.json` (Claude Code) and/or `claude_desktop_config.json` (Claude Desktop) with absolute paths. No manual configuration needed.
224
226
 
@@ -254,6 +256,13 @@ All optional — defaults work out of the box.
254
256
  | `TANDEM_TRANSPORT` | `http` | Transport mode (`http` or `stdio`) |
255
257
  | `TANDEM_NO_SAMPLE` | unset | Set to `1` to skip auto-opening `sample/welcome.md` |
256
258
  | `TANDEM_CLAUDE_CMD` | `claude` | Claude Code executable name (for `tandem setup` auto-detection) |
259
+ | `TANDEM_BIND_HOST` | `127.0.0.1` | Bind address for MCP HTTP (`0.0.0.0` for LAN) |
260
+ | `TANDEM_AUTH_TOKEN` | auto-generated | Override auth token (set by Tauri; manual use rare) |
261
+ | `TANDEM_ALLOW_UNAUTHENTICATED_LAN` | unset | Set to `1` to skip token requirement on LAN bind |
262
+ | `TANDEM_LAN_IP` | auto-detected | Explicit LAN IP for multi-homed machines |
263
+ | `TANDEM_REQUEST_TIMEOUT_MS` | `30000` | Per-request timeout in stdio bridge (ms) |
264
+ | `TANDEM_APP_DATA_DIR` | platform default | Override app-data root (sessions, auth-token, annotations) |
265
+ | `TANDEM_ANNOTATION_STORE` | unset | Set to `off` to disable durable annotation persistence |
257
266
 
258
267
  See `.env.example` for a copy-paste template.
259
268
 
@@ -270,10 +279,10 @@ Tandem kills stale processes on :3478/:3479 at startup. If another app uses thos
270
279
  **Channel shim fails to start**
271
280
  The `tandem-channel` entry spawns a subprocess. For global installs, `tandem setup` writes absolute paths to the bundled `dist/channel/index.js` — re-run `tandem setup` after upgrading. For dev setup, if you see `MODULE_NOT_FOUND` with a production config (`node dist/channel/index.js`), run `npm run build`. The default dev config uses `npx tsx` and doesn't require a build step.
272
281
 
273
- **Browser shows "Cannot reach the Tandem server"**
274
- The browser connects to the server via WebSocket. For global installs, run `tandem` to start the server. For dev setup, use `npm run dev:standalone` (or `npm run dev:server`). The message appears after 3 seconds of failed connection.
282
+ **Editor shows "Cannot reach the Tandem server"**
283
+ The editor connects to the server via WebSocket. For global installs, run `tandem` to start the server. For dev setup, use `npm run dev:standalone` (or `npm run dev:server`). The message appears after 3 seconds of failed connection.
275
284
 
276
- **Empty browser with no document**
285
+ **Empty editor with no document**
277
286
  On first run, `sample/welcome.md` auto-opens. If you've cleared sessions or deleted the sample file, click the **+** button in the tab bar or drop a file onto the editor.
278
287
 
279
288
  ## Development
@@ -17945,7 +17945,7 @@ async function authFetch(url, init) {
17945
17945
  return fetch(url, init);
17946
17946
  }
17947
17947
 
17948
- // src/server/events/types.ts
17948
+ // src/shared/events/types.ts
17949
17949
  var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
17950
17950
  "annotation:created",
17951
17951
  "annotation:accepted",
@@ -18005,6 +18005,7 @@ function formatEventContent(event) {
18005
18005
  }
18006
18006
  default: {
18007
18007
  const _exhaustive = event;
18008
+ void _exhaustive;
18008
18009
  return `Unknown event${doc}`;
18009
18010
  }
18010
18011
  }
@@ -18034,6 +18035,7 @@ function formatEventMeta(event) {
18034
18035
  break;
18035
18036
  default: {
18036
18037
  const _exhaustive = event;
18038
+ void _exhaustive;
18037
18039
  break;
18038
18040
  }
18039
18041
  }
@@ -18103,7 +18105,11 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18103
18105
  status: "idle",
18104
18106
  active: false
18105
18107
  })
18106
- }).catch(() => {
18108
+ }).catch((err) => {
18109
+ console.error(
18110
+ "[Channel] clearAwareness failed (non-fatal):",
18111
+ err instanceof Error ? err.message : err
18112
+ );
18107
18113
  });
18108
18114
  }
18109
18115
  function flushAwareness() {
@@ -18149,11 +18155,21 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18149
18155
  try {
18150
18156
  event = parseTandemEvent(JSON.parse(data));
18151
18157
  } catch {
18152
- console.error("[Channel] Malformed SSE event data (skipping):", data.slice(0, 200));
18158
+ console.error(
18159
+ "[Channel] Malformed SSE event data (skipping), eventId=%s:",
18160
+ eventId,
18161
+ data.slice(0, 200)
18162
+ );
18163
+ if (eventId) onEventId(eventId);
18153
18164
  continue;
18154
18165
  }
18155
18166
  if (!event) {
18156
- console.error("[Channel] Received invalid SSE event, skipping");
18167
+ console.error(
18168
+ "[Channel] Invalid SSE event structure (skipping), eventId=%s:",
18169
+ eventId,
18170
+ data.slice(0, 200)
18171
+ );
18172
+ if (eventId) onEventId(eventId);
18157
18173
  continue;
18158
18174
  }
18159
18175
  if (event.type !== "chat:message") {
@@ -18164,7 +18180,6 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18164
18180
  continue;
18165
18181
  }
18166
18182
  }
18167
- if (eventId) onEventId(eventId);
18168
18183
  try {
18169
18184
  await mcp.notification({
18170
18185
  method: "notifications/claude/channel",
@@ -18177,6 +18192,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18177
18192
  console.error("[Channel] MCP notification failed (transport broken?):", err);
18178
18193
  throw err;
18179
18194
  }
18195
+ if (eventId) onEventId(eventId);
18180
18196
  scheduleAwareness(event);
18181
18197
  }
18182
18198
  }