tandem-editor 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to Tandem will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2026-04-13
9
+
10
+ ### Added
11
+
12
+ - **Authorship tracking** — Y.Map overlay marks text as user-written or Claude-written, with text-color styling (blue for user, orange for Claude) (#190)
13
+ - **Threaded annotation replies** — reply to annotations with back-and-forth conversation threads (#187)
14
+ - **Claude cursor decoration** — character-level cursor shows where Claude is editing in real time (#209)
15
+ - **Auto-save** — documents save automatically on change; Ctrl+S triggers immediate manual save (#272)
16
+ - **Text zoom** — keyboard shortcuts (Ctrl+=/Ctrl+-) for adjusting text size in the Tauri desktop app (#273)
17
+ - **Three-panel default layout** — editor, side panel, and chat visible by default (#264)
18
+ - **Selection event suppression** — selection events only fire after a chat message is sent, reducing noise (#270)
19
+ - V1.0 release plan added to roadmap (#279)
20
+
21
+ ### Fixed
22
+
23
+ - Session persistence, tab bar horizontal scrollbar, and tab cycling keyboard shortcuts (#278)
24
+ - Authorship styling uses text color instead of background highlight; reopen sync corrected
25
+ - Annotation replies renamed from Acknowledge/Dismiss to Accept/Reject for clarity
26
+
27
+ ### Changed
28
+
29
+ - Pinned Hocuspocus and Y.js dependency versions to prevent upstream breakage (#271)
30
+ - EOL normalizer added to lint-staged for .yml and .md files (#263)
31
+ - Lessons learned applied to codebase and tooling (#280)
32
+
8
33
  ## [0.4.0] - 2026-04-12
9
34
 
10
35
  ### Added
package/README.md CHANGED
@@ -2,18 +2,29 @@
2
2
  <img src="docs/assets/banner.png" alt="Tandem — Collaborative AI-Human Document Editor" width="800">
3
3
  </p>
4
4
 
5
- Have you ever been working on a document (or any multi-paragraph piece of text) with Claude and wondered aloud, "Why isn't there an easier way to do this?" Well look no further, because now there is! My goal with this was to try to create an experience kind of similar to editing a Google doc with another person except that that other person is Claude Code.
5
+ Have you ever been working on a piece of writing with an LLM and caught yourself copy-pasting the same paragraph into the chat for the fifth time just so the model knows what you're talking about? That's the friction Tandem eliminates. Open a file directly, or just tell Claude "let's work on my draft in tandem" the document appears in the editor, and from that point on you highlight text and Claude sees it directly. No pasting, no "here's the paragraph I mean," no losing your place.
6
+
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.
6
8
 
7
9
  ![Tandem editor showing a document with annotations, side panel, and Claude's presence](docs/screenshots/01-editor-overview.png)
8
10
 
11
+ ## Why Tandem?
12
+
13
+ - **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.
14
+ - **Your full LLM, not a toy editor.** Tandem connects via MCP, so Claude keeps all its knowledge, all its tools, and your full conversation context. Need it to cross-reference your document against a codebase, a URL, or another file? It can — it's still Claude.
15
+ - **Iterate in place.** Claude can suggest rewrites, leave comments, flag issues, and edit text — all appearing as annotations you accept, dismiss, or tweak right in the document.
16
+
9
17
  ## Quick Start
10
18
 
11
- ### Prerequisites
19
+ ### Option A: Desktop App
20
+
21
+ Download the installer for your platform from the [latest release](https://github.com/bloknayrb/tandem/releases/latest).
12
22
 
13
- - **Node.js 22+** ([download](https://nodejs.org))
14
- - **Claude Code** (`irm https://claude.ai/install.ps1 | iex`)
23
+ The desktop app bundles everything — no Node.js required. It auto-configures Claude Code on launch, manages the server as a background process, and updates itself automatically. Just install and open.
15
24
 
16
- ### Install and Run
25
+ ### Option B: npm Global Install
26
+
27
+ Requires **Node.js 22+** ([download](https://nodejs.org)) and **Claude Code** (`npm install -g @anthropic-ai/claude-code`).
17
28
 
18
29
  ```bash
19
30
  npm install -g tandem-editor
@@ -27,6 +38,8 @@ tandem # starts server + opens browser
27
38
 
28
39
  For the full Tandem experience, start Claude Code with the **channel push** flag:
29
40
 
41
+ > **Desktop app users:** Claude Code is configured automatically on every launch — skip `tandem setup` and just start Claude Code. The `tandem_*` tools will be available immediately.
42
+
30
43
  ```bash
31
44
  claude --dangerously-load-development-channels server:tandem-channel
32
45
  ```
@@ -79,7 +92,7 @@ Or check the raw health endpoint:
79
92
 
80
93
  ```bash
81
94
  curl http://localhost:3479/health
82
- # → {"status":"ok","version":"0.3.0","transport":"http","hasSession":false}
95
+ # → {"status":"ok","version":"0.4.0","transport":"http","hasSession":false}
83
96
  ```
84
97
 
85
98
  `hasSession` becomes `true` once Claude Code connects.
@@ -100,27 +113,29 @@ Open http://localhost:5173 — you'll see `sample/welcome.md` loaded automatical
100
113
 
101
114
  ## Using Tandem
102
115
 
103
- A one-minute mental model of the daily loop:
116
+ You point at text, Claude sees it. Here's how that plays out day-to-day:
104
117
 
105
- - **Open a document.** Ask Claude (`"open notes.md"`), drag a file onto the browser, or click the **+** in the tab bar. `.md`, `.txt`, `.html`, and `.docx` (review-only) are supported.
106
- - **Talk about specific text.** Select it in the browser editor, then ask Claude about "this paragraph" in the terminal. Claude reads your selection from `tandem_checkInbox` — no copy/paste. Hold the selection still for about a second so it registers (dwell-time gating filters out incidental clicks).
107
- - **Review what Claude suggests.** Annotations appear in the side panel. Press **Ctrl+Shift+R** to enter keyboard review mode: **Tab** to navigate, **Y** accept, **N** dismiss, **E** edit, **Z** undo within a 10-second window.
118
+ - **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.
119
+ - **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).
120
+ - **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.
108
121
  - **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.
109
122
  - **Save.** Ask Claude ("save the file"), press the save button, or let session auto-persistence take over — your documents and annotations survive server restarts either way.
110
123
 
111
124
  ## Features
112
125
 
113
- ### Annotations
114
-
115
- ![Side panel showing annotation cards with filtering, bulk actions, and text previews](docs/screenshots/03-side-panel.png)
116
-
117
- Claude adds highlights, comments, suggestions, and flags directly in the document. 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.
126
+ Everything in Tandem is built around one idea: you work in the document, Claude works alongside you, and neither of you has to leave your surface to stay in sync.
118
127
 
119
128
  ### Chat
120
129
 
121
130
  ![Chat sidebar showing messages, typing indicator, and panel toggle](docs/screenshots/02-chat-sidebar.png)
122
131
 
123
- Send freeform messages to Claude alongside annotation review. Select text before sending to attach it as a clickable anchor clicking it later scrolls back to that passage.
132
+ 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.
133
+
134
+ ### Annotations
135
+
136
+ ![Side panel showing annotation cards with filtering, bulk actions, and text previews](docs/screenshots/03-side-panel.png)
137
+
138
+ 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.
124
139
 
125
140
  ### Review Mode
126
141
 
@@ -130,11 +145,11 @@ Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, acc
130
145
 
131
146
  ### More
132
147
 
148
+ - **Full LLM via MCP** — Claude connects through MCP tools, so it retains all its knowledge, conversation context, and tool access while working on your document
133
149
  - **Multi-document tabs** — open `.md`, `.txt`, `.html`, `.docx` files side by side; drag to reorder
134
150
  - **.docx review-only mode** — open Word documents for annotation; imported Word comments appear alongside Claude's
135
151
  - **Session persistence** — documents and annotations survive server restarts
136
152
  - **Solo / Tandem mode** — flip to Solo when you want to write heads-down; Tandem queues non-urgent annotations until you're ready
137
- - **Selection-aware chat** — highlight text in the browser, ask Claude about "this" in the terminal; Claude reads your selection directly, no copy/paste
138
153
  - **Real-time channel push** *(recommended)* — with the `--dangerously-load-development-channels` Claude Code flag, selections, annotations, and chat push to Claude instantly, making Tandem feel like a live collaborator watching over your shoulder
139
154
  - **Keyboard shortcuts** — press `?` for the full reference
140
155
  - **Unsaved-changes indicator** — dot on tab title when a document has pending edits
@@ -144,9 +159,8 @@ Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, acc
144
159
 
145
160
  ## Where Tandem is headed
146
161
 
147
- Tandem v1 covers the core loop well single user editing prose with Claude, with `.md`/`.txt`/`.html` round-trip and `.docx` review. A few directions on the radar for later releases:
162
+ 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:
148
163
 
149
- - **Progressive Web App** — install Tandem from the browser for a real app window, taskbar icon, and offline-capable shell.
150
164
  - **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.
151
165
  - **Claude Desktop parity** — the MCP server already works with Claude Desktop; polish and documentation for a first-class experience there is in the works.
152
166
  - **Exportable annotated documents** — PDF (and eventually `.docx`) with annotations baked in, so you can share reviewed drafts outside Tandem.
@@ -158,12 +172,12 @@ See the full [Roadmap](docs/roadmap.md) and [Known Limitations](docs/roadmap.md#
158
172
  ## Documentation
159
173
 
160
174
  - **[User Guide](docs/user-guide.md)** — How to use Tandem: browser UI, annotations, chat, review mode, keyboard shortcuts
161
- - [MCP Tool Reference](docs/mcp-tools.md) — 30 MCP tools + channel API endpoints
175
+ - [MCP Tool Reference](docs/mcp-tools.md) — 31 MCP tools + channel API endpoints
162
176
  - [Architecture](docs/architecture.md) — System design, data flows, coordinate systems, channel push
163
- - [Workflows](docs/workflows.md) — Claude Code usage patterns: document review, cross-referencing, multi-model
177
+ - [Workflows](docs/workflows.md) — Claude Code usage patterns: text iteration, cross-referencing, multi-model
164
178
  - [Roadmap](docs/roadmap.md) — Phase 2+ roadmap, known issues, future extensions
165
- - [Design Decisions](docs/decisions.md) — ADR-001 through ADR-021
166
- - [Lessons Learned](docs/lessons-learned.md) — 31 implementation lessons
179
+ - [Design Decisions](docs/decisions.md) — ADR-001 through ADR-022
180
+ - [Lessons Learned](docs/lessons-learned.md) — 37 implementation lessons
167
181
 
168
182
  ## CLI Commands
169
183
 
@@ -246,5 +260,9 @@ On first run, `sample/welcome.md` auto-opens. If you've cleared sessions or dele
246
260
  | `npm test` | Run vitest (unit tests) |
247
261
  | `npm run test:e2e` | Run Playwright E2E tests |
248
262
  | `npm run test:e2e:ui` | Playwright UI mode |
263
+ | `cargo tauri dev` | Tauri desktop app (dev mode with hot-reload) |
264
+ | `cargo tauri build` | Tauri production build (installer output) |
265
+
266
+ **Tauri development** requires the [Rust toolchain](https://www.rust-lang.org/tools/install) and [Tauri CLI](https://v2.tauri.app/start/prerequisites/). Web-only development (`npm run dev:standalone`) does not require Rust.
249
267
 
250
268
  **Tech Stack:** React 19, Tiptap, Vite, TypeScript | Node.js, Hocuspocus (Yjs WebSocket), MCP SDK, Express | Yjs (CRDT), y-prosemirror | mammoth.js (.docx), unified/remark (.md)
@@ -17920,8 +17920,8 @@ var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
17920
17920
  "annotation:created",
17921
17921
  "annotation:accepted",
17922
17922
  "annotation:dismissed",
17923
+ "annotation:reply",
17923
17924
  "chat:message",
17924
- "selection:changed",
17925
17925
  "document:opened",
17926
17926
  "document:closed",
17927
17927
  "document:switched"
@@ -17949,15 +17949,17 @@ function formatEventContent(event) {
17949
17949
  const { annotationId, textSnippet } = event.payload;
17950
17950
  return `User dismissed annotation ${annotationId}${textSnippet ? ` ("${textSnippet}")` : ""}${doc}`;
17951
17951
  }
17952
+ case "annotation:reply": {
17953
+ const { annotationId, replyAuthor, replyText, textSnippet } = event.payload;
17954
+ const who = replyAuthor === "claude" ? "Claude" : "User";
17955
+ const snippet = textSnippet ? ` (on "${textSnippet}")` : "";
17956
+ return `${who} replied to annotation ${annotationId}${snippet}: ${replyText}${doc}`;
17957
+ }
17952
17958
  case "chat:message": {
17953
- const { text, replyTo } = event.payload;
17959
+ const { text, replyTo, selection } = event.payload;
17954
17960
  const reply = replyTo ? ` (replying to ${replyTo})` : "";
17955
- return `User says${reply}: ${text}${doc}`;
17956
- }
17957
- case "selection:changed": {
17958
- const { from, to, selectedText } = event.payload;
17959
- if (!selectedText) return `User cleared selection${doc}`;
17960
- return `User is pointing at text (${from}-${to}): "${selectedText}"${doc} \u2014 respond via tandem_reply`;
17961
+ const sel = selection && selection.selectedText ? ` [selection: "${selection.selectedText}"${"from" in selection ? ` (${selection.from}-${selection.to})` : ""}]` : "";
17962
+ return `User says${reply}: ${text}${sel}${doc}`;
17961
17963
  }
17962
17964
  case "document:opened": {
17963
17965
  const { fileName, format } = event.payload;
@@ -17988,11 +17990,13 @@ function formatEventMeta(event) {
17988
17990
  case "annotation:dismissed":
17989
17991
  meta.annotation_id = event.payload.annotationId;
17990
17992
  break;
17993
+ case "annotation:reply":
17994
+ meta.annotation_id = event.payload.annotationId;
17995
+ meta.reply_id = event.payload.replyId;
17996
+ break;
17991
17997
  case "chat:message":
17992
17998
  meta.message_id = event.payload.messageId;
17993
- break;
17994
- case "selection:changed":
17995
- meta.respond_via = "tandem_reply";
17999
+ if (event.payload.selection?.selectedText) meta.has_selection = "true";
17996
18000
  break;
17997
18001
  case "document:opened":
17998
18002
  case "document:closed":
@@ -18008,7 +18012,6 @@ function formatEventMeta(event) {
18008
18012
 
18009
18013
  // src/channel/event-bridge.ts
18010
18014
  var AWARENESS_DEBOUNCE_MS = 500;
18011
- var SELECTION_DEBOUNCE_MS = 300;
18012
18015
  var MODE_CACHE_TTL_MS = 2e3;
18013
18016
  async function startEventBridge(mcp2, tandemUrl) {
18014
18017
  let retries = 0;
@@ -18096,35 +18099,7 @@ async function connectAndStream(mcp2, tandemUrl, lastEventId, onEventId) {
18096
18099
  if (awarenessTimer) clearTimeout(awarenessTimer);
18097
18100
  awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
18098
18101
  }
18099
- let selectionTimer = null;
18100
- let pendingSelection = null;
18101
- let transportBroken = false;
18102
- async function flushSelection() {
18103
- if (!pendingSelection) return;
18104
- const { event, eventId } = pendingSelection;
18105
- pendingSelection = null;
18106
- if (eventId) onEventId(eventId);
18107
- try {
18108
- await mcp2.notification({
18109
- method: "notifications/claude/channel",
18110
- params: {
18111
- content: formatEventContent(event),
18112
- meta: formatEventMeta(event)
18113
- }
18114
- });
18115
- } catch (err) {
18116
- console.error("[Channel] MCP notification failed (transport broken?):", err);
18117
- transportBroken = true;
18118
- return;
18119
- }
18120
- scheduleAwareness(event);
18121
- }
18122
- function isSelectionCleared(event) {
18123
- const p = event.payload;
18124
- return !p || p.from === p.to && !p.selectedText;
18125
- }
18126
18102
  while (true) {
18127
- if (transportBroken) throw new Error("MCP transport broken (detected in debounced flush)");
18128
18103
  const { done, value } = await reader.read();
18129
18104
  if (done) throw new Error("SSE stream ended");
18130
18105
  buffer += decoder.decode(value, { stream: true });
@@ -18159,14 +18134,6 @@ async function connectAndStream(mcp2, tandemUrl, lastEventId, onEventId) {
18159
18134
  continue;
18160
18135
  }
18161
18136
  }
18162
- if (event.type === "selection:changed") {
18163
- if (eventId) onEventId(eventId);
18164
- if (isSelectionCleared(event)) continue;
18165
- pendingSelection = { event, eventId };
18166
- if (selectionTimer) clearTimeout(selectionTimer);
18167
- selectionTimer = setTimeout(flushSelection, SELECTION_DEBOUNCE_MS);
18168
- continue;
18169
- }
18170
18137
  if (eventId) onEventId(eventId);
18171
18138
  try {
18172
18139
  await mcp2.notification({
@@ -18254,8 +18221,9 @@ var mcp = new Server(
18254
18221
  instructions: [
18255
18222
  'Events from Tandem arrive as <channel source="tandem-channel" event_type="..." document_id="...">.',
18256
18223
  "These are real-time push notifications of user actions in the collaborative document editor.",
18257
- "Event types: annotation:created, annotation:accepted, annotation:dismissed,",
18258
- "chat:message, selection:changed, document:opened, document:closed, document:switched.",
18224
+ "Event types: annotation:created, annotation:accepted, annotation:dismissed, annotation:reply,",
18225
+ "chat:message, document:opened, document:closed, document:switched.",
18226
+ "Chat messages may include a 'selection' field with buffered selection context.",
18259
18227
  "Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight, etc.) to act on them.",
18260
18228
  "Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.",
18261
18229
  "Do not reply to non-chat events \u2014 just act on them using tools.",