tandem-editor 0.3.0 → 0.3.2
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 +39 -13
- package/README.md +60 -11
- package/dist/channel/index.js +3 -2
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +11 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/index-DRkek0mA.js +308 -0
- package/dist/client/index.html +1 -1
- package/dist/server/index.js +1597 -1504
- package/dist/server/index.js.map +1 -1
- package/package.json +9 -3
- package/dist/client/assets/index-BXWLR51Y.js +0 -308
package/CHANGELOG.md
CHANGED
|
@@ -5,36 +5,62 @@ 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
|
-
## [
|
|
8
|
+
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## [0.3.2] - 2026-04-12
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- **Dwell-time selection events** — selections fire after 1s hold (#188)
|
|
14
|
-
- **Configurable layout** — tabbed or three-panel, with settings popover (#206)
|
|
15
|
-
- **Click-to-navigate** — click annotated text to jump to annotation card
|
|
16
|
-
- **Tab badges** — notification counts on inactive panel tabs
|
|
17
|
-
- **Skill-directed response routing** — Claude responds in chat panel, not terminal
|
|
18
|
-
- Review banner replaced with per-annotation toasts (#208, landed earlier)
|
|
19
|
-
- `Y_MAP_MODE` constant, Zod validation for mode reads, error logging in channel event bridge
|
|
20
|
-
- 894 tests passing
|
|
12
|
+
### Changed
|
|
21
13
|
|
|
22
|
-
|
|
14
|
+
- **Annotation type unification:** Three semantically identical types (`comment`, `suggestion`, `question`) collapsed into a single `comment` type with optional `suggestedText` and `directedAt` fields. `AnnotationTypeSchema` reduced from 5 values to 3: `highlight`, `comment`, `flag`. (#193, #245, #255)
|
|
15
|
+
- **Toolbar:** Three annotation buttons (Comment, Suggest, Ask Claude) replaced with a single Comment button with "Replace" and "@Claude" toggles (#193)
|
|
16
|
+
- **Side panel filters:** "Suggestions" → "With replacement", "Questions" → "For Claude" (#193)
|
|
17
|
+
- **`sanitizeAnnotation()` moved to `src/shared/sanitize.ts`** — now available to both server and client code. Client-side Y.Map reads are sanitized to handle legacy session data. (#255)
|
|
23
18
|
|
|
24
19
|
### Added
|
|
25
20
|
|
|
26
21
|
- Link (Ctrl+K), Horizontal Rule, and Code Block buttons in the formatting toolbar (#204)
|
|
27
|
-
-
|
|
22
|
+
- Replacement cards show a visual diff — original text in red strikethrough → replacement in green (#195)
|
|
28
23
|
- Undo countdown progress bar — a shrinking indicator shows the 10-second undo window (#196)
|
|
29
24
|
- Review mode shortcut hints (Y / N / ↑↓ / Z) shown below the Review button (#200)
|
|
30
25
|
- Chat anchor previews expand on hover to show full text (#198)
|
|
31
26
|
- `disabledTitle` prop on toolbar buttons — annotation buttons show "Select text first" when no text is selected (#197)
|
|
32
27
|
- Explicit ✕ close button on the highlight color picker (#203)
|
|
28
|
+
- `tandem_comment` now accepts optional `suggestedText` and `directedAt` parameters (#193)
|
|
29
|
+
- `sanitizeAnnotation()` normalizes legacy `suggestion`/`question` entries at read boundaries — permanent migration for historical session data (#193)
|
|
30
|
+
- Exhaustive type switch in `buildDecorations` catches unhandled annotation types at compile time (#255)
|
|
31
|
+
- 8 new tests covering MCP tool params, sanitization edge cases, and legacy migration paths (#255)
|
|
33
32
|
|
|
34
33
|
### Fixed
|
|
35
34
|
|
|
36
35
|
- Toolbar wraps to a second row on narrow windows instead of overflowing; inline inputs shrink responsively (#192)
|
|
37
36
|
- Edit button on annotation cards now shows a visible "✎ Edit" label instead of icon-only (#201)
|
|
37
|
+
- Client-side legacy annotations (from pre-0.3.2 sessions) no longer render as invisible decorations or display raw JSON (#255)
|
|
38
|
+
- `sanitizeAnnotation` no longer drops `textSnapshot: ""` or `editedAt: 0` via falsy-check bug (#255)
|
|
39
|
+
- Event queue observer no longer silently dies if `sanitizeAnnotation` throws (#255)
|
|
40
|
+
- `handleEdit` catch-block no longer corrupts annotation data on JSON parse failure (#255)
|
|
41
|
+
|
|
42
|
+
### Deprecated
|
|
43
|
+
|
|
44
|
+
- `tandem_suggest` MCP tool — use `tandem_comment` with `suggestedText` parameter instead (#193)
|
|
45
|
+
|
|
46
|
+
### Removed
|
|
47
|
+
|
|
48
|
+
- **MCP wire change:** Removed unused `"overlay"` annotation kind from `AnnotationTypeSchema`. External clients sending `type: "overlay"` will now receive a Zod validation error. (#249)
|
|
49
|
+
- **MCP wire change:** `suggestion` and `question` annotation types removed from `AnnotationTypeSchema`. Use `comment` with `suggestedText` or `directedAt` fields. Legacy data is migrated automatically via `sanitizeAnnotation()`. (#193)
|
|
50
|
+
|
|
51
|
+
## [0.3.0] - 2026-04-07
|
|
52
|
+
|
|
53
|
+
### Wave 4: Notification & Interruption Redesign
|
|
54
|
+
|
|
55
|
+
- **Solo/Tandem mode** replaces All/Urgent/Paused interruption controls (#207, #226)
|
|
56
|
+
- **Dwell-time selection events** — selections fire after 1s hold (#188)
|
|
57
|
+
- **Configurable layout** — tabbed or three-panel, with settings popover (#206)
|
|
58
|
+
- **Click-to-navigate** — click annotated text to jump to annotation card
|
|
59
|
+
- **Tab badges** — notification counts on inactive panel tabs
|
|
60
|
+
- **Skill-directed response routing** — Claude responds in chat panel, not terminal
|
|
61
|
+
- Review banner replaced with per-annotation toasts (#208, landed earlier)
|
|
62
|
+
- `Y_MAP_MODE` constant, Zod validation for mode reads, error logging in channel event bridge
|
|
63
|
+
- 894 tests passing
|
|
38
64
|
|
|
39
65
|
## [0.2.12] - 2026-04-06
|
|
40
66
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="docs/assets/banner.png" alt="Tandem — Collaborative AI-Human Document Editor" width="800">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -25,25 +25,49 @@ tandem # starts server + opens browser
|
|
|
25
25
|
|
|
26
26
|
### Connect Claude Code
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
For the full Tandem experience, start Claude Code with the **channel push** flag:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
claude --dangerously-load-development-channels server:tandem-channel
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
**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.
|
|
37
|
+
|
|
34
38
|
Then try:
|
|
35
39
|
|
|
36
40
|
```
|
|
37
|
-
"
|
|
41
|
+
"Open sample/welcome.md and review it with me"
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
Claude calls `tandem_open`, the document appears in the browser, and
|
|
44
|
+
Claude calls `tandem_open`, the document appears in the browser, and you're ready to collaborate.
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
#### The core loop — no copy/paste
|
|
43
47
|
|
|
48
|
+
1. Highlight a paragraph in the browser editor.
|
|
49
|
+
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"*.
|
|
50
|
+
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.
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
#### Prefer not to use the experimental flag?
|
|
54
|
+
|
|
55
|
+
You don't have to turn on channel push — every feature of Tandem works without it. You lose the "Claude reacts on its own" magic, but Claude still sees every selection, annotation, and chat message. Start Claude Code normally:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
claude
|
|
44
59
|
```
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
|
|
61
|
+
Then pick one of two ways to keep the conversation flowing:
|
|
62
|
+
|
|
63
|
+
1. **Just chat in the terminal (simplest).** Every time you send Claude a message, it has a chance to call `tandem_checkInbox` and pick up your latest selection, any annotations you accepted or dismissed, and any chat messages from the Tandem sidebar. Zero setup — this is how it works out of the box. With Tandem and the terminal snapped side by side, the loop feels surprisingly natural; Claude just reacts when you nudge it rather than spontaneously.
|
|
64
|
+
2. **Background polling with `/loop` (hands-off).** Ask Claude to check in on its own using the `/loop` skill:
|
|
65
|
+
```
|
|
66
|
+
/loop 30s check tandem inbox and respond to any new messages
|
|
67
|
+
```
|
|
68
|
+
Claude polls every 30 seconds — responses lag by up to that interval, but you don't have to prompt it yourself.
|
|
69
|
+
|
|
70
|
+
Either way, Claude reads the exact same information (selections, annotations, chat) through the same `tandem_checkInbox` tool. The only thing channels change is *when* Claude finds out something happened — not *whether* it can see it.
|
|
47
71
|
|
|
48
72
|
### Verify
|
|
49
73
|
|
|
@@ -55,7 +79,7 @@ Or check the raw health endpoint:
|
|
|
55
79
|
|
|
56
80
|
```bash
|
|
57
81
|
curl http://localhost:3479/health
|
|
58
|
-
# → {"status":"ok","version":"0.
|
|
82
|
+
# → {"status":"ok","version":"0.3.0","transport":"http","hasSession":false}
|
|
59
83
|
```
|
|
60
84
|
|
|
61
85
|
`hasSession` becomes `true` once Claude Code connects.
|
|
@@ -74,6 +98,16 @@ Open http://localhost:5173 — you'll see `sample/welcome.md` loaded automatical
|
|
|
74
98
|
|
|
75
99
|
</details>
|
|
76
100
|
|
|
101
|
+
## Using Tandem
|
|
102
|
+
|
|
103
|
+
A one-minute mental model of the daily loop:
|
|
104
|
+
|
|
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.
|
|
108
|
+
- **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
|
+
- **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
|
+
|
|
77
111
|
## Features
|
|
78
112
|
|
|
79
113
|
### Annotations
|
|
@@ -96,16 +130,31 @@ Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, acc
|
|
|
96
130
|
|
|
97
131
|
### More
|
|
98
132
|
|
|
99
|
-
- **Multi-document tabs** — open `.md`, `.txt`, `.docx` files side by side; drag to reorder
|
|
133
|
+
- **Multi-document tabs** — open `.md`, `.txt`, `.html`, `.docx` files side by side; drag to reorder
|
|
100
134
|
- **.docx review-only mode** — open Word documents for annotation; imported Word comments appear alongside Claude's
|
|
101
135
|
- **Session persistence** — documents and annotations survive server restarts
|
|
102
|
-
- **
|
|
136
|
+
- **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
|
+
- **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
|
|
103
139
|
- **Keyboard shortcuts** — press `?` for the full reference
|
|
104
140
|
- **Unsaved-changes indicator** — dot on tab title when a document has pending edits
|
|
105
141
|
- **Configurable display name** — set your name so Claude knows who's reviewing
|
|
106
142
|
- **Atomic file saves** — write to temp, then rename, preventing partial writes
|
|
107
143
|
- **E2E tested** — Playwright tests cover the annotation lifecycle end-to-end
|
|
108
144
|
|
|
145
|
+
## Where Tandem is headed
|
|
146
|
+
|
|
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:
|
|
148
|
+
|
|
149
|
+
- **Progressive Web App** — install Tandem from the browser for a real app window, taskbar icon, and offline-capable shell.
|
|
150
|
+
- **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
|
+
- **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
|
+
- **Exportable annotated documents** — PDF (and eventually `.docx`) with annotations baked in, so you can share reviewed drafts outside Tandem.
|
|
153
|
+
- **Code editing mode** — CodeMirror 6 surface for reviewing code the same way you review prose.
|
|
154
|
+
- **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.
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
109
158
|
## Documentation
|
|
110
159
|
|
|
111
160
|
- **[User Guide](docs/user-guide.md)** — How to use Tandem: browser UI, annotations, chat, review mode, keyboard shortcuts
|
|
@@ -128,7 +177,7 @@ Press **Ctrl+Shift+R** to enter keyboard review mode. Navigate with **Tab**, acc
|
|
|
128
177
|
|
|
129
178
|
## MCP Configuration
|
|
130
179
|
|
|
131
|
-
Tandem
|
|
180
|
+
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.
|
|
132
181
|
|
|
133
182
|
**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.
|
|
134
183
|
|
package/dist/channel/index.js
CHANGED
|
@@ -37,9 +37,10 @@ function formatEventContent(event) {
|
|
|
37
37
|
const doc = event.documentId ? ` [doc: ${event.documentId}]` : "";
|
|
38
38
|
switch (event.type) {
|
|
39
39
|
case "annotation:created": {
|
|
40
|
-
const { annotationType, content, textSnippet } = event.payload;
|
|
40
|
+
const { annotationType, content, textSnippet, hasSuggestedText, directedAt } = event.payload;
|
|
41
41
|
const snippet = textSnippet ? ` on "${textSnippet}"` : "";
|
|
42
|
-
|
|
42
|
+
const label = hasSuggestedText ? "replacement" : directedAt === "claude" ? "question for Claude" : annotationType;
|
|
43
|
+
return `User created ${label}${snippet}: ${content || "(no content)"}${doc}`;
|
|
43
44
|
}
|
|
44
45
|
case "annotation:accepted": {
|
|
45
46
|
const { annotationId, textSnippet } = event.payload;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/channel/index.ts","../../src/shared/constants.ts","../../src/server/events/types.ts","../../src/channel/event-bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Tandem Channel Shim — Claude Code spawns this as a subprocess.\n *\n * Bridges Tandem's SSE event stream → Claude Code channel notifications,\n * and exposes a `tandem_reply` tool for Claude to respond to chat messages.\n *\n * Uses the low-level MCP `Server` class (not `McpServer`) as required by\n * the Channels API spec.\n */\n\nimport { createConnection } from \"node:net\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\nimport { DEFAULT_MCP_PORT } from \"../shared/constants.js\";\nimport { startEventBridge } from \"./event-bridge.js\";\n\n// stdout is the MCP wire — redirect console.log to stderr\nconsole.log = console.error;\nconsole.warn = console.error;\nconsole.info = console.error;\n\nconst TANDEM_URL = process.env.TANDEM_URL || \"http://localhost:3479\";\n\n// --- Pre-flight: verify Tandem server is reachable before MCP handshake ---\n\nasync function checkServerReachable(url: string, timeoutMs = 2000): Promise<boolean> {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n console.error(\n `[Channel] Invalid TANDEM_URL: \"${url}\" — expected format: http://localhost:3479`,\n );\n return false;\n }\n const port = parseInt(parsed.port || String(DEFAULT_MCP_PORT), 10);\n return new Promise((resolve) => {\n const socket = createConnection({ port, host: parsed.hostname }, () => {\n socket.destroy();\n resolve(true);\n });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => {\n socket.destroy();\n resolve(false);\n });\n socket.on(\"error\", (err) => {\n console.error(`[Channel] Server probe failed: ${err.message}`);\n socket.destroy();\n resolve(false);\n });\n });\n}\n\n// --- MCP Server setup ---\n\nconst mcp = new Server(\n { name: \"tandem-channel\", version: \"0.1.0\" },\n {\n capabilities: {\n experimental: {\n \"claude/channel\": {},\n \"claude/channel/permission\": {},\n },\n tools: {},\n },\n instructions: [\n 'Events from Tandem arrive as <channel source=\"tandem-channel\" event_type=\"...\" document_id=\"...\">.',\n \"These are real-time push notifications of user actions in the collaborative document editor.\",\n \"Event types: annotation:created, annotation:accepted, annotation:dismissed,\",\n \"chat:message, selection:changed, document:opened, document:closed, document:switched.\",\n \"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight, etc.) to act on them.\",\n \"Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.\",\n \"Do not reply to non-chat events — just act on them using tools.\",\n \"If you haven't received channel notifications recently, call tandem_checkInbox as a fallback.\",\n ].join(\" \"),\n },\n);\n\n// --- Tool: tandem_reply (forwarded to Tandem HTTP server) ---\n\nmcp.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [\n {\n name: \"tandem_reply\",\n description: \"Reply to a chat message in Tandem\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n text: { type: \"string\", description: \"The reply message\" },\n documentId: {\n type: \"string\",\n description: \"Document ID from the channel event (optional)\",\n },\n replyTo: {\n type: \"string\",\n description: \"Message ID being replied to (optional)\",\n },\n },\n required: [\"text\"],\n },\n },\n ],\n}));\n\nmcp.setRequestHandler(CallToolRequestSchema, async (req) => {\n if (req.params.name === \"tandem_reply\") {\n const args = req.params.arguments as Record<string, unknown>;\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-reply`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(args),\n });\n let data: unknown;\n try {\n data = await res.json();\n } catch {\n data = { message: \"Non-JSON response\" };\n }\n if (!res.ok) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Reply failed (${res.status}): ${JSON.stringify(data)}`,\n },\n ],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(data) }] };\n } catch (err) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Failed to send reply: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n };\n }\n }\n throw new Error(`Unknown tool: ${req.params.name}`);\n});\n\n// --- Permission relay: forward Claude Code's tool approval prompts to Tandem browser ---\n\nconst PermissionRequestSchema = z.object({\n method: z.literal(\"notifications/claude/channel/permission_request\"),\n params: z.object({\n request_id: z.string(),\n tool_name: z.string(),\n description: z.string(),\n input_preview: z.string(),\n }),\n});\n\nmcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-permission`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n requestId: params.request_id,\n toolName: params.tool_name,\n description: params.description,\n inputPreview: params.input_preview,\n }),\n });\n if (!res.ok) {\n console.error(\n `[Channel] Permission relay got HTTP ${res.status} — browser may not see prompt`,\n );\n }\n } catch (err) {\n console.error(\"[Channel] Failed to forward permission request:\", err);\n }\n});\n\n// --- Connect and start ---\n\nasync function main() {\n console.error(`[Channel] Tandem channel shim starting (server: ${TANDEM_URL})`);\n\n const reachable = await checkServerReachable(TANDEM_URL);\n if (!reachable) {\n console.error(`[Channel] Cannot reach Tandem server at ${TANDEM_URL}`);\n console.error(\"[Channel] Start it with: npm run dev:standalone\");\n // Continue anyway — the event bridge will retry, and the server may start later\n }\n\n // Connect to Claude Code over stdio\n const transport = new StdioServerTransport();\n await mcp.connect(transport);\n console.error(\"[Channel] Connected to Claude Code via stdio\");\n\n // Start the SSE event bridge (runs until disconnected or max retries)\n startEventBridge(mcp, TANDEM_URL).catch((err) => {\n console.error(\"[Channel] Event bridge failed unexpectedly:\", err);\n process.exit(1);\n });\n}\n\nmain().catch((err) => {\n console.error(\"[Channel] Fatal error:\", err);\n process.exit(1);\n});\n","export const DEFAULT_WS_PORT = 3478;\nexport const DEFAULT_MCP_PORT = 3479;\n\n/** File extensions the server accepts for opening. */\nexport const SUPPORTED_EXTENSIONS = new Set([\".md\", \".txt\", \".html\", \".htm\", \".docx\"]);\nexport const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nexport const MAX_WS_PAYLOAD = 10 * 1024 * 1024; // 10MB\nexport const MAX_WS_CONNECTIONS = 4;\nexport const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes\nexport const SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days\nexport const TYPING_DEBOUNCE = 3000; // 3 seconds\nexport const DISCONNECT_DEBOUNCE_MS = 3000; // 3 seconds before showing \"server not reachable\"\nexport const PROLONGED_DISCONNECT_MS = 30_000; // 30 seconds before showing App-level disconnect banner\nexport const OVERLAY_STALE_DEBOUNCE = 200; // 200ms\n\nexport const HIGHLIGHT_COLORS: Record<string, string> = {\n yellow: \"rgba(255, 235, 59, 0.3)\",\n red: \"rgba(244, 67, 54, 0.3)\",\n green: \"rgba(76, 175, 80, 0.3)\",\n blue: \"rgba(33, 150, 243, 0.3)\",\n purple: \"rgba(156, 39, 176, 0.3)\",\n};\n\nexport const TANDEM_MODE_DEFAULT = \"tandem\" as const;\nexport const TANDEM_MODE_KEY = \"tandem:mode\";\nexport const TANDEM_SETTINGS_KEY = \"tandem:settings\";\nexport const SELECTION_DWELL_DEFAULT_MS = 1000;\nexport const SELECTION_DWELL_MIN_MS = 500;\nexport const SELECTION_DWELL_MAX_MS = 3000;\n\n// Large file thresholds\nexport const CHARS_PER_PAGE = 3_000;\nexport const LARGE_FILE_PAGE_THRESHOLD = 50;\nexport const VERY_LARGE_FILE_PAGE_THRESHOLD = 100;\n\nexport const CLAUDE_PRESENCE_COLOR = \"#6366f1\";\nexport const CLAUDE_FOCUS_OPACITY = 0.1;\n\nexport const CTRL_ROOM = \"__tandem_ctrl__\";\n\n/** Y.Map key constants — centralized to prevent silent bugs from string typos. */\nexport const Y_MAP_ANNOTATIONS = \"annotations\";\nexport const Y_MAP_AWARENESS = \"awareness\";\nexport const Y_MAP_USER_AWARENESS = \"userAwareness\";\nexport const Y_MAP_MODE = \"mode\";\nexport const Y_MAP_CHAT = \"chat\";\nexport const Y_MAP_DOCUMENT_META = \"documentMeta\";\nexport const Y_MAP_SAVED_AT_VERSION = \"savedAtVersion\";\n\nexport const SERVER_INFO_DIR = \".tandem\";\nexport const SERVER_INFO_FILE = \".tandem/.server-info\";\n\nexport const RECENT_FILES_KEY = \"tandem:recentFiles\";\nexport const RECENT_FILES_CAP = 20;\n\nexport const USER_NAME_KEY = \"tandem:userName\";\nexport const USER_NAME_DEFAULT = \"You\";\n\n// Toast notifications\nexport const TOAST_DISMISS_MS = { error: 8000, warning: 6000, info: 4000 } as const;\nexport const MAX_VISIBLE_TOASTS = 5;\nexport const NOTIFICATION_BUFFER_SIZE = 50;\n\n// Onboarding tutorial\nexport const TUTORIAL_COMPLETED_KEY = \"tandem:tutorialCompleted\";\nexport const TUTORIAL_ANNOTATION_PREFIX = \"tutorial-\";\n\n// Editor layout\nexport const EDITOR_WIDTH_MODE_KEY = \"tandem:editorWidthMode\";\n\n// Channel / event queue\nexport const CHANNEL_EVENT_BUFFER_SIZE = 200;\nexport const CHANNEL_EVENT_BUFFER_AGE_MS = 60_000; // 60 seconds\nexport const CHANNEL_SSE_KEEPALIVE_MS = 15_000; // 15 seconds\nexport const CHANNEL_MAX_RETRIES = 5;\nexport const CHANNEL_RETRY_DELAY_MS = 2_000;\n","/**\n * Event types for the Tandem → Claude Code channel.\n *\n * These events flow from browser-originated Y.Map changes through an SSE\n * endpoint to the channel shim, which pushes them into Claude Code as\n * `notifications/claude/channel` messages.\n */\n\n// --- Per-event payload interfaces ---\n\nexport interface AnnotationCreatedPayload {\n annotationId: string;\n annotationType: string;\n content: string;\n textSnippet: string;\n}\n\nexport interface AnnotationAcceptedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface AnnotationDismissedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface ChatMessagePayload {\n messageId: string;\n text: string;\n replyTo: string | null;\n anchor: { from: number; to: number; textSnapshot: string } | null;\n}\n\nexport interface SelectionChangedPayload {\n from: number;\n to: number;\n selectedText: string;\n}\n\nexport interface DocumentOpenedPayload {\n fileName: string;\n format: string;\n}\n\nexport interface DocumentClosedPayload {\n fileName: string;\n}\n\nexport interface DocumentSwitchedPayload {\n fileName: string;\n}\n\n// --- Discriminated union ---\n\ninterface TandemEventBase {\n /** Timestamp-based unique ID for SSE `Last-Event-ID` reconnection. Format: `evt_<timestamp>_<rand>`. Roughly ordered but not strictly monotonic. */\n id: string;\n timestamp: number;\n /** Which document this event relates to (absent for global events). */\n documentId?: string;\n}\n\nexport type TandemEvent =\n | (TandemEventBase & { type: \"annotation:created\"; payload: AnnotationCreatedPayload })\n | (TandemEventBase & { type: \"annotation:accepted\"; payload: AnnotationAcceptedPayload })\n | (TandemEventBase & { type: \"annotation:dismissed\"; payload: AnnotationDismissedPayload })\n | (TandemEventBase & { type: \"chat:message\"; payload: ChatMessagePayload })\n | (TandemEventBase & { type: \"selection:changed\"; payload: SelectionChangedPayload })\n | (TandemEventBase & { type: \"document:opened\"; payload: DocumentOpenedPayload })\n | (TandemEventBase & { type: \"document:closed\"; payload: DocumentClosedPayload })\n | (TandemEventBase & { type: \"document:switched\"; payload: DocumentSwitchedPayload });\n\n/** Union of all event type discriminants. */\nexport type TandemEventType = TandemEvent[\"type\"];\n\n// Re-export from shared utils (single ID generation pattern)\nexport { generateEventId } from \"../../shared/utils.js\";\n\n// --- Parse guard for SSE consumers ---\n\nconst VALID_EVENT_TYPES = new Set<TandemEventType>([\n \"annotation:created\",\n \"annotation:accepted\",\n \"annotation:dismissed\",\n \"chat:message\",\n \"selection:changed\",\n \"document:opened\",\n \"document:closed\",\n \"document:switched\",\n]);\n\n/**\n * Validate a JSON-parsed value as a TandemEvent.\n * Used by the event-bridge to safely consume SSE data.\n */\nexport function parseTandemEvent(raw: unknown): TandemEvent | null {\n if (\n typeof raw !== \"object\" ||\n raw === null ||\n !(\"id\" in raw) ||\n typeof (raw as Record<string, unknown>).id !== \"string\" ||\n !(\"type\" in raw) ||\n !VALID_EVENT_TYPES.has((raw as Record<string, unknown>).type as TandemEventType) ||\n !(\"timestamp\" in raw) ||\n typeof (raw as Record<string, unknown>).timestamp !== \"number\" ||\n !(\"payload\" in raw) ||\n typeof (raw as Record<string, unknown>).payload !== \"object\"\n ) {\n return null;\n }\n return raw as TandemEvent;\n}\n\n/**\n * Convert a TandemEvent into a human-readable string for the channel `content` field.\n * Claude sees this text inside `<channel source=\"tandem-channel\">` tags.\n */\nexport function formatEventContent(event: TandemEvent): string {\n const doc = event.documentId ? ` [doc: ${event.documentId}]` : \"\";\n\n switch (event.type) {\n case \"annotation:created\": {\n const { annotationType, content, textSnippet } = event.payload;\n const snippet = textSnippet ? ` on \"${textSnippet}\"` : \"\";\n return `User created ${annotationType}${snippet}: ${content || \"(no content)\"}${doc}`;\n }\n case \"annotation:accepted\": {\n const { annotationId, textSnippet } = event.payload;\n return `User accepted annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"annotation:dismissed\": {\n const { annotationId, textSnippet } = event.payload;\n return `User dismissed annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"chat:message\": {\n const { text, replyTo } = event.payload;\n const reply = replyTo ? ` (replying to ${replyTo})` : \"\";\n return `User says${reply}: ${text}${doc}`;\n }\n case \"selection:changed\": {\n const { from, to, selectedText } = event.payload;\n if (!selectedText) return `User cleared selection${doc}`;\n return `User is pointing at text (${from}-${to}): \"${selectedText}\"${doc} — respond via tandem_reply`;\n }\n case \"document:opened\": {\n const { fileName, format } = event.payload;\n return `User opened document: ${fileName} (${format})${doc}`;\n }\n case \"document:closed\": {\n const { fileName } = event.payload;\n return `User closed document: ${fileName}${doc}`;\n }\n case \"document:switched\": {\n const { fileName } = event.payload;\n return `User switched to document: ${fileName}${doc}`;\n }\n default: {\n const _exhaustive: never = event;\n return `Unknown event${doc}`;\n }\n }\n}\n\n/**\n * Build the `meta` record for a channel notification.\n * Keys use underscores only (Channels API silently drops hyphenated keys).\n */\nexport function formatEventMeta(event: TandemEvent): Record<string, string> {\n const meta: Record<string, string> = {\n event_type: event.type,\n };\n if (event.documentId) meta.document_id = event.documentId;\n\n switch (event.type) {\n case \"annotation:created\":\n case \"annotation:accepted\":\n case \"annotation:dismissed\":\n meta.annotation_id = event.payload.annotationId;\n break;\n case \"chat:message\":\n meta.message_id = event.payload.messageId;\n break;\n case \"selection:changed\":\n meta.respond_via = \"tandem_reply\";\n break;\n case \"document:opened\":\n case \"document:closed\":\n case \"document:switched\":\n break;\n default: {\n const _exhaustive: never = event;\n break;\n }\n }\n\n return meta;\n}\n","/**\n * SSE event bridge: connects to Tandem server's /api/events endpoint\n * and pushes received events to Claude Code as channel notifications.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { TandemEvent } from \"../server/events/types.js\";\nimport { formatEventContent, formatEventMeta, parseTandemEvent } from \"../server/events/types.js\";\nimport { CHANNEL_MAX_RETRIES, CHANNEL_RETRY_DELAY_MS } from \"../shared/constants.js\";\n\nconst AWARENESS_DEBOUNCE_MS = 500;\nconst SELECTION_DEBOUNCE_MS = 300;\nconst MODE_CACHE_TTL_MS = 2000;\n\nexport async function startEventBridge(mcp: Server, tandemUrl: string): Promise<void> {\n let retries = 0;\n let lastEventId: string | undefined;\n\n while (retries < CHANNEL_MAX_RETRIES) {\n try {\n await connectAndStream(mcp, tandemUrl, lastEventId, (id) => {\n lastEventId = id;\n retries = 0;\n });\n } catch (err) {\n retries++;\n console.error(\n `[Channel] SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,\n err instanceof Error ? err.message : err,\n );\n\n if (retries >= CHANNEL_MAX_RETRIES) {\n console.error(\"[Channel] SSE connection exhausted, reporting error and exiting\");\n try {\n await fetch(`${tandemUrl}/api/channel-error`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n error: \"CHANNEL_CONNECT_FAILED\",\n message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`,\n }),\n });\n } catch (reportErr) {\n console.error(\n \"[Channel] Could not report failure to server:\",\n reportErr instanceof Error ? reportErr.message : reportErr,\n );\n }\n process.exit(1);\n }\n\n await new Promise((r) => setTimeout(r, CHANNEL_RETRY_DELAY_MS));\n }\n }\n}\n\nasync function connectAndStream(\n mcp: Server,\n tandemUrl: string,\n lastEventId: string | undefined,\n onEventId: (id: string) => void,\n): Promise<void> {\n const headers: Record<string, string> = { Accept: \"text/event-stream\" };\n if (lastEventId) headers[\"Last-Event-ID\"] = lastEventId;\n\n const res = await fetch(`${tandemUrl}/api/events`, { headers });\n if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);\n if (!res.body) throw new Error(\"SSE endpoint returned no body\");\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // Debounced awareness: only send the latest status after a quiet period\n let awarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let clearAwarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingAwareness: TandemEvent | null = null;\n const AWARENESS_CLEAR_MS = 3000; // Reset active state after 3s of no new events\n\n function clearAwareness(documentId?: string) {\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: documentId ?? null,\n status: \"idle\",\n active: false,\n }),\n }).catch(() => {});\n }\n\n function flushAwareness() {\n if (!pendingAwareness) return;\n const event = pendingAwareness;\n pendingAwareness = null;\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: event.documentId,\n status: `processing: ${event.type}`,\n active: true,\n }),\n }).catch((err) => {\n console.error(\"[Channel] Awareness update failed:\", err instanceof Error ? err.message : err);\n });\n\n // Auto-clear after timeout so the indicator doesn't stick\n if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);\n clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);\n }\n\n function scheduleAwareness(event: TandemEvent) {\n pendingAwareness = event;\n if (awarenessTimer) clearTimeout(awarenessTimer);\n awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);\n }\n\n // Debounced selection: coalesce rapid selection changes, skip cleared selections\n let selectionTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingSelection: { event: TandemEvent; eventId?: string } | null = null;\n let transportBroken = false;\n\n async function flushSelection() {\n if (!pendingSelection) return;\n const { event, eventId } = pendingSelection;\n pendingSelection = null;\n if (eventId) onEventId(eventId);\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n transportBroken = true;\n return;\n }\n scheduleAwareness(event);\n }\n\n function isSelectionCleared(event: TandemEvent): boolean {\n const p = event.payload as { from?: number; to?: number; selectedText?: string } | undefined;\n return !p || (p.from === p.to && !p.selectedText);\n }\n\n while (true) {\n if (transportBroken) throw new Error(\"MCP transport broken (detected in debounced flush)\");\n const { done, value } = await reader.read();\n if (done) throw new Error(\"SSE stream ended\");\n\n buffer += decoder.decode(value, { stream: true });\n\n let boundary: number;\n while ((boundary = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const frame = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n\n if (frame.startsWith(\":\")) continue;\n\n let eventId: string | undefined;\n let data: string | undefined;\n\n for (const line of frame.split(\"\\n\")) {\n if (line.startsWith(\"id: \")) eventId = line.slice(4);\n else if (line.startsWith(\"data: \")) data = line.slice(6);\n }\n\n if (!data) continue;\n\n let event: TandemEvent | null;\n try {\n event = parseTandemEvent(JSON.parse(data));\n } catch {\n console.error(\"[Channel] Malformed SSE event data (skipping):\", data.slice(0, 200));\n continue;\n }\n if (!event) {\n console.error(\"[Channel] Received invalid SSE event, skipping\");\n continue;\n }\n\n // Solo mode suppression: drop non-chat events when mode is \"solo\"\n if (event.type !== \"chat:message\") {\n const mode = await getCachedMode(tandemUrl);\n if (mode === \"solo\") {\n console.error(`[Channel] Solo mode: suppressed ${event.type} event`);\n if (eventId) onEventId(eventId);\n continue;\n }\n }\n\n // Selection events: drop cleared selections, debounce the rest\n if (event.type === \"selection:changed\") {\n if (eventId) onEventId(eventId);\n if (isSelectionCleared(event)) continue; // silently drop\n pendingSelection = { event, eventId };\n if (selectionTimer) clearTimeout(selectionTimer);\n selectionTimer = setTimeout(flushSelection, SELECTION_DEBOUNCE_MS);\n continue;\n }\n\n if (eventId) onEventId(eventId);\n\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n throw err;\n }\n\n scheduleAwareness(event);\n }\n }\n}\n\n// Cached mode lookup — avoids an HTTP fetch per event\nlet cachedMode: string = \"tandem\";\nlet cachedModeAt = 0;\n\nasync function getCachedMode(tandemUrl: string): Promise<string> {\n const now = Date.now();\n if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;\n try {\n const res = await fetch(`${tandemUrl}/api/mode`);\n if (res.ok) {\n const { mode } = (await res.json()) as { mode: string };\n cachedMode = mode;\n } else {\n console.error(`[Channel] Mode check returned ${res.status}, using cached: \"${cachedMode}\"`);\n }\n cachedModeAt = now;\n } catch (err) {\n console.error(\n \"[Channel] Mode check failed, delivering event (fail-open):\",\n err instanceof Error ? err.message : err,\n );\n cachedModeAt = now;\n }\n return cachedMode;\n}\n"],"mappings":"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","names":["mcp"]}
|
|
1
|
+
{"version":3,"sources":["../../src/channel/index.ts","../../src/shared/constants.ts","../../src/server/events/types.ts","../../src/channel/event-bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Tandem Channel Shim — Claude Code spawns this as a subprocess.\n *\n * Bridges Tandem's SSE event stream → Claude Code channel notifications,\n * and exposes a `tandem_reply` tool for Claude to respond to chat messages.\n *\n * Uses the low-level MCP `Server` class (not `McpServer`) as required by\n * the Channels API spec.\n */\n\nimport { createConnection } from \"node:net\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\nimport { DEFAULT_MCP_PORT } from \"../shared/constants.js\";\nimport { startEventBridge } from \"./event-bridge.js\";\n\n// stdout is the MCP wire — redirect console.log to stderr\nconsole.log = console.error;\nconsole.warn = console.error;\nconsole.info = console.error;\n\nconst TANDEM_URL = process.env.TANDEM_URL || \"http://localhost:3479\";\n\n// --- Pre-flight: verify Tandem server is reachable before MCP handshake ---\n\nasync function checkServerReachable(url: string, timeoutMs = 2000): Promise<boolean> {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n console.error(\n `[Channel] Invalid TANDEM_URL: \"${url}\" — expected format: http://localhost:3479`,\n );\n return false;\n }\n const port = parseInt(parsed.port || String(DEFAULT_MCP_PORT), 10);\n return new Promise((resolve) => {\n const socket = createConnection({ port, host: parsed.hostname }, () => {\n socket.destroy();\n resolve(true);\n });\n socket.setTimeout(timeoutMs);\n socket.on(\"timeout\", () => {\n socket.destroy();\n resolve(false);\n });\n socket.on(\"error\", (err) => {\n console.error(`[Channel] Server probe failed: ${err.message}`);\n socket.destroy();\n resolve(false);\n });\n });\n}\n\n// --- MCP Server setup ---\n\nconst mcp = new Server(\n { name: \"tandem-channel\", version: \"0.1.0\" },\n {\n capabilities: {\n experimental: {\n \"claude/channel\": {},\n \"claude/channel/permission\": {},\n },\n tools: {},\n },\n instructions: [\n 'Events from Tandem arrive as <channel source=\"tandem-channel\" event_type=\"...\" document_id=\"...\">.',\n \"These are real-time push notifications of user actions in the collaborative document editor.\",\n \"Event types: annotation:created, annotation:accepted, annotation:dismissed,\",\n \"chat:message, selection:changed, document:opened, document:closed, document:switched.\",\n \"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight, etc.) to act on them.\",\n \"Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.\",\n \"Do not reply to non-chat events — just act on them using tools.\",\n \"If you haven't received channel notifications recently, call tandem_checkInbox as a fallback.\",\n ].join(\" \"),\n },\n);\n\n// --- Tool: tandem_reply (forwarded to Tandem HTTP server) ---\n\nmcp.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [\n {\n name: \"tandem_reply\",\n description: \"Reply to a chat message in Tandem\",\n inputSchema: {\n type: \"object\" as const,\n properties: {\n text: { type: \"string\", description: \"The reply message\" },\n documentId: {\n type: \"string\",\n description: \"Document ID from the channel event (optional)\",\n },\n replyTo: {\n type: \"string\",\n description: \"Message ID being replied to (optional)\",\n },\n },\n required: [\"text\"],\n },\n },\n ],\n}));\n\nmcp.setRequestHandler(CallToolRequestSchema, async (req) => {\n if (req.params.name === \"tandem_reply\") {\n const args = req.params.arguments as Record<string, unknown>;\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-reply`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(args),\n });\n let data: unknown;\n try {\n data = await res.json();\n } catch {\n data = { message: \"Non-JSON response\" };\n }\n if (!res.ok) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Reply failed (${res.status}): ${JSON.stringify(data)}`,\n },\n ],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(data) }] };\n } catch (err) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Failed to send reply: ${err instanceof Error ? err.message : String(err)}`,\n },\n ],\n isError: true,\n };\n }\n }\n throw new Error(`Unknown tool: ${req.params.name}`);\n});\n\n// --- Permission relay: forward Claude Code's tool approval prompts to Tandem browser ---\n\nconst PermissionRequestSchema = z.object({\n method: z.literal(\"notifications/claude/channel/permission_request\"),\n params: z.object({\n request_id: z.string(),\n tool_name: z.string(),\n description: z.string(),\n input_preview: z.string(),\n }),\n});\n\nmcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {\n try {\n const res = await fetch(`${TANDEM_URL}/api/channel-permission`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n requestId: params.request_id,\n toolName: params.tool_name,\n description: params.description,\n inputPreview: params.input_preview,\n }),\n });\n if (!res.ok) {\n console.error(\n `[Channel] Permission relay got HTTP ${res.status} — browser may not see prompt`,\n );\n }\n } catch (err) {\n console.error(\"[Channel] Failed to forward permission request:\", err);\n }\n});\n\n// --- Connect and start ---\n\nasync function main() {\n console.error(`[Channel] Tandem channel shim starting (server: ${TANDEM_URL})`);\n\n const reachable = await checkServerReachable(TANDEM_URL);\n if (!reachable) {\n console.error(`[Channel] Cannot reach Tandem server at ${TANDEM_URL}`);\n console.error(\"[Channel] Start it with: npm run dev:standalone\");\n // Continue anyway — the event bridge will retry, and the server may start later\n }\n\n // Connect to Claude Code over stdio\n const transport = new StdioServerTransport();\n await mcp.connect(transport);\n console.error(\"[Channel] Connected to Claude Code via stdio\");\n\n // Start the SSE event bridge (runs until disconnected or max retries)\n startEventBridge(mcp, TANDEM_URL).catch((err) => {\n console.error(\"[Channel] Event bridge failed unexpectedly:\", err);\n process.exit(1);\n });\n}\n\nmain().catch((err) => {\n console.error(\"[Channel] Fatal error:\", err);\n process.exit(1);\n});\n","export const DEFAULT_WS_PORT = 3478;\nexport const DEFAULT_MCP_PORT = 3479;\n\n/** File extensions the server accepts for opening. */\nexport const SUPPORTED_EXTENSIONS = new Set([\".md\", \".txt\", \".html\", \".htm\", \".docx\"]);\nexport const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nexport const MAX_WS_PAYLOAD = 10 * 1024 * 1024; // 10MB\nexport const MAX_WS_CONNECTIONS = 4;\nexport const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes\nexport const SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days\nexport const TYPING_DEBOUNCE = 3000; // 3 seconds\nexport const DISCONNECT_DEBOUNCE_MS = 3000; // 3 seconds before showing \"server not reachable\"\nexport const PROLONGED_DISCONNECT_MS = 30_000; // 30 seconds before showing App-level disconnect banner\nexport const OVERLAY_STALE_DEBOUNCE = 200; // 200ms\n\nexport const HIGHLIGHT_COLORS: Record<string, string> = {\n yellow: \"rgba(255, 235, 59, 0.3)\",\n red: \"rgba(244, 67, 54, 0.3)\",\n green: \"rgba(76, 175, 80, 0.3)\",\n blue: \"rgba(33, 150, 243, 0.3)\",\n purple: \"rgba(156, 39, 176, 0.3)\",\n};\n\nexport const TANDEM_MODE_DEFAULT = \"tandem\" as const;\nexport const TANDEM_MODE_KEY = \"tandem:mode\";\nexport const TANDEM_SETTINGS_KEY = \"tandem:settings\";\n// Panel-width localStorage keys.\n//\n// NOTE: these use legacy hyphen naming (vs the neighboring colon convention\n// `tandem:mode`/`tandem:settings`) because they predate the colon scheme and\n// changing the strings would invalidate every existing user's saved widths.\n// Do not \"fix\" the style — the key string is the persistence contract.\n//\n// Right-side panel width is shared between the tabbed layout and the\n// three-panel right panel. The left key only applies in three-panel mode.\nexport const PANEL_WIDTH_KEY = \"tandem-panel-width\";\nexport const LEFT_PANEL_WIDTH_KEY = \"tandem-left-panel-width\";\n\nexport type PanelSide = \"left\" | \"right\";\n\n/**\n * Maps a panel side to its localStorage key. Using a Record instead of two\n * bare constants makes the \"both handles write to the same key\" regression\n * (#228) structurally impossible — you can't accidentally map both sides to\n * the same value at a callsite.\n *\n * Uses `as const satisfies Record<PanelSide, string>` so the value type stays\n * as the literal strings rather than widening to `string` — this preserves\n * the persistence-key identity at every callsite while still enforcing\n * exhaustive coverage of `PanelSide`.\n */\nexport const PANEL_WIDTH_KEYS = {\n left: LEFT_PANEL_WIDTH_KEY,\n right: PANEL_WIDTH_KEY,\n} as const satisfies Record<PanelSide, string>;\nexport const SELECTION_DWELL_DEFAULT_MS = 1000;\nexport const SELECTION_DWELL_MIN_MS = 500;\nexport const SELECTION_DWELL_MAX_MS = 3000;\n\n// Large file thresholds\nexport const CHARS_PER_PAGE = 3_000;\nexport const LARGE_FILE_PAGE_THRESHOLD = 50;\nexport const VERY_LARGE_FILE_PAGE_THRESHOLD = 100;\n\nexport const CLAUDE_PRESENCE_COLOR = \"#6366f1\";\nexport const CLAUDE_FOCUS_OPACITY = 0.1;\n\nexport const CTRL_ROOM = \"__tandem_ctrl__\";\n\n/** Y.Map key constants — centralized to prevent silent bugs from string typos. */\nexport const Y_MAP_ANNOTATIONS = \"annotations\";\nexport const Y_MAP_AWARENESS = \"awareness\";\nexport const Y_MAP_USER_AWARENESS = \"userAwareness\";\nexport const Y_MAP_MODE = \"mode\";\nexport const Y_MAP_DWELL_MS = \"selectionDwellMs\";\nexport const Y_MAP_CHAT = \"chat\";\nexport const Y_MAP_DOCUMENT_META = \"documentMeta\";\nexport const Y_MAP_SAVED_AT_VERSION = \"savedAtVersion\";\n\nexport const SERVER_INFO_DIR = \".tandem\";\nexport const SERVER_INFO_FILE = \".tandem/.server-info\";\n\nexport const RECENT_FILES_KEY = \"tandem:recentFiles\";\nexport const RECENT_FILES_CAP = 20;\n\nexport const USER_NAME_KEY = \"tandem:userName\";\nexport const USER_NAME_DEFAULT = \"You\";\n\n// Toast notifications\nexport const TOAST_DISMISS_MS = { error: 8000, warning: 6000, info: 4000 } as const;\nexport const MAX_VISIBLE_TOASTS = 5;\nexport const NOTIFICATION_BUFFER_SIZE = 50;\n\n// Onboarding tutorial\nexport const TUTORIAL_COMPLETED_KEY = \"tandem:tutorialCompleted\";\nexport const TUTORIAL_ANNOTATION_PREFIX = \"tutorial-\";\n\n// Editor layout\nexport const EDITOR_WIDTH_MODE_KEY = \"tandem:editorWidthMode\";\n\n// Channel / event queue\nexport const CHANNEL_EVENT_BUFFER_SIZE = 200;\nexport const CHANNEL_EVENT_BUFFER_AGE_MS = 60_000; // 60 seconds\nexport const CHANNEL_SSE_KEEPALIVE_MS = 15_000; // 15 seconds\nexport const CHANNEL_MAX_RETRIES = 5;\nexport const CHANNEL_RETRY_DELAY_MS = 2_000;\n","/**\n * Event types for the Tandem → Claude Code channel.\n *\n * These events flow from browser-originated Y.Map changes through an SSE\n * endpoint to the channel shim, which pushes them into Claude Code as\n * `notifications/claude/channel` messages.\n */\n\n// --- Per-event payload interfaces ---\n\nexport interface AnnotationCreatedPayload {\n annotationId: string;\n annotationType: string;\n content: string;\n textSnippet: string;\n hasSuggestedText?: boolean;\n directedAt?: \"claude\";\n}\n\nexport interface AnnotationAcceptedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface AnnotationDismissedPayload {\n annotationId: string;\n textSnippet: string;\n}\n\nexport interface ChatMessagePayload {\n messageId: string;\n text: string;\n replyTo: string | null;\n anchor: { from: number; to: number; textSnapshot: string } | null;\n}\n\nexport interface SelectionChangedPayload {\n from: number;\n to: number;\n selectedText: string;\n}\n\nexport interface DocumentOpenedPayload {\n fileName: string;\n format: string;\n}\n\nexport interface DocumentClosedPayload {\n fileName: string;\n}\n\nexport interface DocumentSwitchedPayload {\n fileName: string;\n}\n\n// --- Discriminated union ---\n\ninterface TandemEventBase {\n /** Timestamp-based unique ID for SSE `Last-Event-ID` reconnection. Format: `evt_<timestamp>_<rand>`. Roughly ordered but not strictly monotonic. */\n id: string;\n timestamp: number;\n /** Which document this event relates to (absent for global events). */\n documentId?: string;\n}\n\nexport type TandemEvent =\n | (TandemEventBase & { type: \"annotation:created\"; payload: AnnotationCreatedPayload })\n | (TandemEventBase & { type: \"annotation:accepted\"; payload: AnnotationAcceptedPayload })\n | (TandemEventBase & { type: \"annotation:dismissed\"; payload: AnnotationDismissedPayload })\n | (TandemEventBase & { type: \"chat:message\"; payload: ChatMessagePayload })\n | (TandemEventBase & { type: \"selection:changed\"; payload: SelectionChangedPayload })\n | (TandemEventBase & { type: \"document:opened\"; payload: DocumentOpenedPayload })\n | (TandemEventBase & { type: \"document:closed\"; payload: DocumentClosedPayload })\n | (TandemEventBase & { type: \"document:switched\"; payload: DocumentSwitchedPayload });\n\n/** Union of all event type discriminants. */\nexport type TandemEventType = TandemEvent[\"type\"];\n\n// Re-export from shared utils (single ID generation pattern)\nexport { generateEventId } from \"../../shared/utils.js\";\n\n// --- Parse guard for SSE consumers ---\n\nconst VALID_EVENT_TYPES = new Set<TandemEventType>([\n \"annotation:created\",\n \"annotation:accepted\",\n \"annotation:dismissed\",\n \"chat:message\",\n \"selection:changed\",\n \"document:opened\",\n \"document:closed\",\n \"document:switched\",\n]);\n\n/**\n * Validate a JSON-parsed value as a TandemEvent.\n * Used by the event-bridge to safely consume SSE data.\n */\nexport function parseTandemEvent(raw: unknown): TandemEvent | null {\n if (\n typeof raw !== \"object\" ||\n raw === null ||\n !(\"id\" in raw) ||\n typeof (raw as Record<string, unknown>).id !== \"string\" ||\n !(\"type\" in raw) ||\n !VALID_EVENT_TYPES.has((raw as Record<string, unknown>).type as TandemEventType) ||\n !(\"timestamp\" in raw) ||\n typeof (raw as Record<string, unknown>).timestamp !== \"number\" ||\n !(\"payload\" in raw) ||\n typeof (raw as Record<string, unknown>).payload !== \"object\"\n ) {\n return null;\n }\n return raw as TandemEvent;\n}\n\n/**\n * Convert a TandemEvent into a human-readable string for the channel `content` field.\n * Claude sees this text inside `<channel source=\"tandem-channel\">` tags.\n */\nexport function formatEventContent(event: TandemEvent): string {\n const doc = event.documentId ? ` [doc: ${event.documentId}]` : \"\";\n\n switch (event.type) {\n case \"annotation:created\": {\n const { annotationType, content, textSnippet, hasSuggestedText, directedAt } = event.payload;\n const snippet = textSnippet ? ` on \"${textSnippet}\"` : \"\";\n const label = hasSuggestedText\n ? \"replacement\"\n : directedAt === \"claude\"\n ? \"question for Claude\"\n : annotationType;\n return `User created ${label}${snippet}: ${content || \"(no content)\"}${doc}`;\n }\n case \"annotation:accepted\": {\n const { annotationId, textSnippet } = event.payload;\n return `User accepted annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"annotation:dismissed\": {\n const { annotationId, textSnippet } = event.payload;\n return `User dismissed annotation ${annotationId}${textSnippet ? ` (\"${textSnippet}\")` : \"\"}${doc}`;\n }\n case \"chat:message\": {\n const { text, replyTo } = event.payload;\n const reply = replyTo ? ` (replying to ${replyTo})` : \"\";\n return `User says${reply}: ${text}${doc}`;\n }\n case \"selection:changed\": {\n const { from, to, selectedText } = event.payload;\n if (!selectedText) return `User cleared selection${doc}`;\n return `User is pointing at text (${from}-${to}): \"${selectedText}\"${doc} — respond via tandem_reply`;\n }\n case \"document:opened\": {\n const { fileName, format } = event.payload;\n return `User opened document: ${fileName} (${format})${doc}`;\n }\n case \"document:closed\": {\n const { fileName } = event.payload;\n return `User closed document: ${fileName}${doc}`;\n }\n case \"document:switched\": {\n const { fileName } = event.payload;\n return `User switched to document: ${fileName}${doc}`;\n }\n default: {\n const _exhaustive: never = event;\n return `Unknown event${doc}`;\n }\n }\n}\n\n/**\n * Build the `meta` record for a channel notification.\n * Keys use underscores only (Channels API silently drops hyphenated keys).\n */\nexport function formatEventMeta(event: TandemEvent): Record<string, string> {\n const meta: Record<string, string> = {\n event_type: event.type,\n };\n if (event.documentId) meta.document_id = event.documentId;\n\n switch (event.type) {\n case \"annotation:created\":\n case \"annotation:accepted\":\n case \"annotation:dismissed\":\n meta.annotation_id = event.payload.annotationId;\n break;\n case \"chat:message\":\n meta.message_id = event.payload.messageId;\n break;\n case \"selection:changed\":\n meta.respond_via = \"tandem_reply\";\n break;\n case \"document:opened\":\n case \"document:closed\":\n case \"document:switched\":\n break;\n default: {\n const _exhaustive: never = event;\n break;\n }\n }\n\n return meta;\n}\n","/**\n * SSE event bridge: connects to Tandem server's /api/events endpoint\n * and pushes received events to Claude Code as channel notifications.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { TandemEvent } from \"../server/events/types.js\";\nimport { formatEventContent, formatEventMeta, parseTandemEvent } from \"../server/events/types.js\";\nimport { CHANNEL_MAX_RETRIES, CHANNEL_RETRY_DELAY_MS } from \"../shared/constants.js\";\n\nconst AWARENESS_DEBOUNCE_MS = 500;\nconst SELECTION_DEBOUNCE_MS = 300;\nconst MODE_CACHE_TTL_MS = 2000;\n\nexport async function startEventBridge(mcp: Server, tandemUrl: string): Promise<void> {\n let retries = 0;\n let lastEventId: string | undefined;\n\n while (retries < CHANNEL_MAX_RETRIES) {\n try {\n await connectAndStream(mcp, tandemUrl, lastEventId, (id) => {\n lastEventId = id;\n retries = 0;\n });\n } catch (err) {\n retries++;\n console.error(\n `[Channel] SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,\n err instanceof Error ? err.message : err,\n );\n\n if (retries >= CHANNEL_MAX_RETRIES) {\n console.error(\"[Channel] SSE connection exhausted, reporting error and exiting\");\n try {\n await fetch(`${tandemUrl}/api/channel-error`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n error: \"CHANNEL_CONNECT_FAILED\",\n message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`,\n }),\n });\n } catch (reportErr) {\n console.error(\n \"[Channel] Could not report failure to server:\",\n reportErr instanceof Error ? reportErr.message : reportErr,\n );\n }\n process.exit(1);\n }\n\n await new Promise((r) => setTimeout(r, CHANNEL_RETRY_DELAY_MS));\n }\n }\n}\n\nasync function connectAndStream(\n mcp: Server,\n tandemUrl: string,\n lastEventId: string | undefined,\n onEventId: (id: string) => void,\n): Promise<void> {\n const headers: Record<string, string> = { Accept: \"text/event-stream\" };\n if (lastEventId) headers[\"Last-Event-ID\"] = lastEventId;\n\n const res = await fetch(`${tandemUrl}/api/events`, { headers });\n if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);\n if (!res.body) throw new Error(\"SSE endpoint returned no body\");\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // Debounced awareness: only send the latest status after a quiet period\n let awarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let clearAwarenessTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingAwareness: TandemEvent | null = null;\n const AWARENESS_CLEAR_MS = 3000; // Reset active state after 3s of no new events\n\n function clearAwareness(documentId?: string) {\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: documentId ?? null,\n status: \"idle\",\n active: false,\n }),\n }).catch(() => {});\n }\n\n function flushAwareness() {\n if (!pendingAwareness) return;\n const event = pendingAwareness;\n pendingAwareness = null;\n fetch(`${tandemUrl}/api/channel-awareness`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n documentId: event.documentId,\n status: `processing: ${event.type}`,\n active: true,\n }),\n }).catch((err) => {\n console.error(\"[Channel] Awareness update failed:\", err instanceof Error ? err.message : err);\n });\n\n // Auto-clear after timeout so the indicator doesn't stick\n if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);\n clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);\n }\n\n function scheduleAwareness(event: TandemEvent) {\n pendingAwareness = event;\n if (awarenessTimer) clearTimeout(awarenessTimer);\n awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);\n }\n\n // Debounced selection: coalesce rapid selection changes, skip cleared selections\n let selectionTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingSelection: { event: TandemEvent; eventId?: string } | null = null;\n let transportBroken = false;\n\n async function flushSelection() {\n if (!pendingSelection) return;\n const { event, eventId } = pendingSelection;\n pendingSelection = null;\n if (eventId) onEventId(eventId);\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n transportBroken = true;\n return;\n }\n scheduleAwareness(event);\n }\n\n function isSelectionCleared(event: TandemEvent): boolean {\n const p = event.payload as { from?: number; to?: number; selectedText?: string } | undefined;\n return !p || (p.from === p.to && !p.selectedText);\n }\n\n while (true) {\n if (transportBroken) throw new Error(\"MCP transport broken (detected in debounced flush)\");\n const { done, value } = await reader.read();\n if (done) throw new Error(\"SSE stream ended\");\n\n buffer += decoder.decode(value, { stream: true });\n\n let boundary: number;\n while ((boundary = buffer.indexOf(\"\\n\\n\")) !== -1) {\n const frame = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n\n if (frame.startsWith(\":\")) continue;\n\n let eventId: string | undefined;\n let data: string | undefined;\n\n for (const line of frame.split(\"\\n\")) {\n if (line.startsWith(\"id: \")) eventId = line.slice(4);\n else if (line.startsWith(\"data: \")) data = line.slice(6);\n }\n\n if (!data) continue;\n\n let event: TandemEvent | null;\n try {\n event = parseTandemEvent(JSON.parse(data));\n } catch {\n console.error(\"[Channel] Malformed SSE event data (skipping):\", data.slice(0, 200));\n continue;\n }\n if (!event) {\n console.error(\"[Channel] Received invalid SSE event, skipping\");\n continue;\n }\n\n // Solo mode suppression: drop non-chat events when mode is \"solo\"\n if (event.type !== \"chat:message\") {\n const mode = await getCachedMode(tandemUrl);\n if (mode === \"solo\") {\n console.error(`[Channel] Solo mode: suppressed ${event.type} event`);\n if (eventId) onEventId(eventId);\n continue;\n }\n }\n\n // Selection events: drop cleared selections, debounce the rest\n if (event.type === \"selection:changed\") {\n if (eventId) onEventId(eventId);\n if (isSelectionCleared(event)) continue; // silently drop\n pendingSelection = { event, eventId };\n if (selectionTimer) clearTimeout(selectionTimer);\n selectionTimer = setTimeout(flushSelection, SELECTION_DEBOUNCE_MS);\n continue;\n }\n\n if (eventId) onEventId(eventId);\n\n try {\n await mcp.notification({\n method: \"notifications/claude/channel\",\n params: {\n content: formatEventContent(event),\n meta: formatEventMeta(event),\n },\n });\n } catch (err) {\n console.error(\"[Channel] MCP notification failed (transport broken?):\", err);\n throw err;\n }\n\n scheduleAwareness(event);\n }\n }\n}\n\n// Cached mode lookup — avoids an HTTP fetch per event\nlet cachedMode: string = \"tandem\";\nlet cachedModeAt = 0;\n\nasync function getCachedMode(tandemUrl: string): Promise<string> {\n const now = Date.now();\n if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;\n try {\n const res = await fetch(`${tandemUrl}/api/mode`);\n if (res.ok) {\n const { mode } = (await res.json()) as { mode: string };\n cachedMode = mode;\n } else {\n console.error(`[Channel] Mode check returned ${res.status}, using cached: \"${cachedMode}\"`);\n }\n cachedModeAt = now;\n } catch (err) {\n console.error(\n \"[Channel] Mode check failed, delivering event (fail-open):\",\n err instanceof Error ? err.message : err,\n );\n cachedModeAt = now;\n }\n return cachedMode;\n}\n"],"mappings":"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","names":["mcp"]}
|
package/dist/cli/index.js
CHANGED
|
@@ -38,7 +38,7 @@ description: >
|
|
|
38
38
|
|
|
39
39
|
# Tandem \u2014 Collaborative Document Editor
|
|
40
40
|
|
|
41
|
-
Tandem lets you annotate and edit documents alongside the user in real time. The user sees your changes in a browser editor; you interact via
|
|
41
|
+
Tandem lets you annotate and edit documents alongside the user in real time. The user sees your changes in a browser editor; you interact via the tandem_* MCP tool suite.
|
|
42
42
|
|
|
43
43
|
## Hard Rules
|
|
44
44
|
|
|
@@ -72,9 +72,7 @@ Choose the right type for each finding:
|
|
|
72
72
|
- **\`tandem_suggest\`** \u2014 Specific text replacement. **Prefer over comment when you can provide replacement text** \u2014 the user gets one-click accept/reject. Cannot create new paragraphs.
|
|
73
73
|
- **\`tandem_flag\`** \u2014 Factual errors, compliance risks, missing required content. Signals a blocking issue the user must address before the document ships.
|
|
74
74
|
|
|
75
|
-
**
|
|
76
|
-
|
|
77
|
-
**User-created types:** \`question\` and \`overlay\` annotations are created by users, not Claude. When you see a \`question\` in \`tandem_checkInbox\` or \`tandem_getAnnotations\`, respond with a \`tandem_comment\` on the same range or \`tandem_reply\` for conversational answers.
|
|
75
|
+
**User-created types:** \`question\` annotation is created by users, not Claude. When you see a \`question\` in \`tandem_checkInbox\` or \`tandem_getAnnotations\`, respond with a \`tandem_comment\` on the same range or \`tandem_reply\` for conversational answers.
|
|
78
76
|
|
|
79
77
|
## Collaboration Mode
|
|
80
78
|
|
|
@@ -83,6 +81,10 @@ Check \`mode\` from \`tandem_status\` or \`tandem_checkInbox\` and adapt:
|
|
|
83
81
|
- **Tandem** (\`"tandem"\`, default) \u2014 Full collaboration. Annotate freely and react to selections and document changes.
|
|
84
82
|
- **Solo** (\`"solo"\`) \u2014 The user wants to write undisturbed. Only respond when the user sends a chat message. Do not proactively annotate or react to document activity.
|
|
85
83
|
|
|
84
|
+
## Reacting to Document Events
|
|
85
|
+
|
|
86
|
+
Selection events can reach you two ways. Over the real-time channel they arrive as notifications with \`meta.respond_via = "tandem_reply"\`. When polling via \`tandem_checkInbox\`, the current selection shows up under \`activity.selectedText\` (no \`meta\` field \u2014 that only exists on channel pushes). Either way, when the user holds a selection, briefly acknowledge what they highlighted via \`tandem_reply\` \u2014 don't annotate unless asked. Use \`tandem_reply\` for any document-context reaction (chat messages, selections, question annotations); reserve terminal output for non-document work the user explicitly requests. In Solo mode, hold reactions until the user sends a chat message.
|
|
87
|
+
|
|
86
88
|
## Collaboration Etiquette
|
|
87
89
|
|
|
88
90
|
- Check \`tandem_getActivity()\` before annotating near the user's cursor. If \`isTyping\` is true, wait for typing to stop before annotating that area.
|
|
@@ -131,12 +133,12 @@ __export(setup_exports, {
|
|
|
131
133
|
installSkill: () => installSkill,
|
|
132
134
|
runSetup: () => runSetup
|
|
133
135
|
});
|
|
134
|
-
import {
|
|
135
|
-
import {
|
|
136
|
+
import { randomUUID } from "crypto";
|
|
137
|
+
import { existsSync, readFileSync } from "fs";
|
|
138
|
+
import { copyFile, mkdir, rename, unlink, writeFile } from "fs/promises";
|
|
136
139
|
import { homedir } from "os";
|
|
137
|
-
import {
|
|
140
|
+
import { dirname, join, resolve } from "path";
|
|
138
141
|
import { fileURLToPath } from "url";
|
|
139
|
-
import { randomUUID } from "crypto";
|
|
140
142
|
function buildMcpEntries(channelPath) {
|
|
141
143
|
return {
|
|
142
144
|
tandem: {
|
|
@@ -336,7 +338,7 @@ var init_start = __esm({
|
|
|
336
338
|
|
|
337
339
|
// src/cli/index.ts
|
|
338
340
|
import updateNotifier from "update-notifier";
|
|
339
|
-
var version = true ? "0.3.
|
|
341
|
+
var version = true ? "0.3.2" : "0.0.0-dev";
|
|
340
342
|
updateNotifier({ pkg: { name: "tandem-editor", version } }).notify();
|
|
341
343
|
var args = process.argv.slice(2);
|
|
342
344
|
if (args.includes("--help") || args.includes("-h")) {
|