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 +25 -0
- package/README.md +41 -23
- package/dist/channel/index.js +18 -50
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +6 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/index-BS8jwldm.js +345 -0
- package/dist/client/assets/webview-0tvvWtyc.js +1 -0
- package/dist/client/index.html +1 -1
- package/dist/server/index.js +898 -588
- package/dist/server/index.js.map +1 -1
- package/package.json +10 -7
- package/sample/welcome.md +3 -3
- package/dist/client/assets/index-D6wQrQ7U.js +0 -308
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
|
|
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
|

|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
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.
|
|
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
|
-
|
|
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 (`"
|
|
106
|
-
- **
|
|
107
|
-
- **
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-

|
|
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
|

|
|
122
131
|
|
|
123
|
-
Send
|
|
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
|
+

|
|
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
|
|
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) —
|
|
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:
|
|
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-
|
|
166
|
-
- [Lessons Learned](docs/lessons-learned.md) —
|
|
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)
|
package/dist/channel/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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.",
|