tandem-editor 0.9.1 → 0.11.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/.claude-plugin/plugin.json +9 -7
- package/CHANGELOG.md +97 -1
- package/LICENSE +58 -21
- package/README.md +1 -1
- package/dist/channel/index.js +168 -49
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +56 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/CoworkSettings-ChE5WuAe.js +3 -0
- package/dist/client/assets/index-BJKuWd_k.css +1 -0
- package/dist/client/assets/index-vu_QxvyU.js +310 -0
- package/dist/client/assets/webview-Ben21ZLJ.js +1 -0
- package/dist/client/assets/window-BxBvHL5k.js +1 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/inter-tight-latin.woff2 +0 -0
- package/dist/client/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/client/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/client/index.html +201 -49
- package/dist/monitor/index.js +50 -16
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +458 -150
- package/dist/server/index.js.map +1 -1
- package/package.json +21 -18
- package/dist/client/assets/CoworkSettings-BlGNryD3.js +0 -1
- package/dist/client/assets/index-Bnc4LNBi.css +0 -1
- package/dist/client/assets/index-D0gSEOxm.js +0 -228
- package/dist/client/assets/webview-BQBJMQvJ.js +0 -1
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
"experimental": {
|
|
28
|
+
"monitors": [
|
|
29
|
+
{
|
|
30
|
+
"name": "tandem-events",
|
|
31
|
+
"command": "node ${CLAUDE_PLUGIN_ROOT}/dist/monitor/index.js",
|
|
32
|
+
"description": "Tandem real-time document events (annotations, chat, selections)"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
34
36
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,103 @@ 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
|
+
## \[0.11.0] - 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Audience-first selection popup (AR3, PR #590)** — Replaces the three-mode state machine (`idle → comment | note`) with a unified popup that appears on text selection. The user types first and chooses audience at submit time via two buttons: "Comment" (sends to Claude, requires text) and "Note to self" (private, always enabled). Bold/Italic formatting and highlight color swatches remain as one-click actions in the top row. Enter submits as Comment; Shift+Enter inserts a newline; Escape dismisses. `InputGroup.svelte` deleted. New testids: `popup-annotation-input`, `popup-note-submit`, `popup-comment-submit`, `popup-highlight-{yellow|green|blue|pink}`.
|
|
13
|
+
- **Five annotation visual languages (AR2, PR #586)** — Claude-authored comments now render with a solid underline (`--tandem-author-claude`) instead of the same dashed style as user comments. All annotation inline decorations carry a `data-annotation-author` attribute for CSS targeting (e.g. theme overrides, annotation-patterns mode). The five languages are now fully distinct: highlight (colored bg), note (dotted muted underline), user comment (dashed blue), Claude comment (solid orange underline), suggestion (wavy violet).
|
|
14
|
+
- **Annotation schema foundation — audience model (AR1, PR #583)** — adds three optional fields to `AnnotationBase`: `audience` (`"private" | "outbound"`), `promotedFrom` (`"note"`), and `importSource` (`{ author, file }`). `sanitize.ts` derives `audience` on every read for legacy annotations (highlight/note/flag → `"private"`, comment → `"outbound"`, import → `"private"` per design brief). Wire-shape change: all MCP tool responses and channel events now include `audience`. Backward-compatible — existing annotations gain the field on first read; no data loss.
|
|
15
|
+
|
|
16
|
+
- **Command palette + action registry (closes #571)** — Ctrl+Shift+P opens a fuzzy-search command palette. A central action registry (`src/client/actions/registry.ts`) is the new source of truth for commands and their display shortcuts; the Settings → Shortcuts tab now derives its content from the registry rather than a hardcoded array. Ctrl+S and Ctrl+, are migrated from dedicated hook files into the global keydown handler; `useSaveShortcut.svelte.ts` and `useSettingsShortcut.svelte.ts` are deleted. ADR-029 records the design. New testids: `command-palette`, `palette-input`, `palette-item-{id}`, `palette-empty`.
|
|
17
|
+
- **Find / Replace bar (closes #570)** — Ctrl+F opens a find bar anchored to the bottom-right of the editor. Highlights all matches in the document using the existing highlight-yellow token; active match gets a warning-bg border. Enter / Shift+Enter cycle through matches. Replace replaces the active match and advances; All replaces in 100-match chunks to keep Yjs updates bounded. Regex-mode toggle (off by default) with inline error for invalid patterns. All options are session-only (not persisted). New testids: `find-replace-bar`, `find-input`, `replace-input`, `find-next-btn`, `find-prev-btn`, `replace-btn`, `replace-all-btn`, `find-close-btn`, `find-match-count`, `find-regex-toggle`, `find-case-toggle`, `find-word-toggle`.
|
|
18
|
+
- **Outline panel for H1–H3 navigation (closes #569)** — Settings → Appearance now offers a "Left Panel" radio (Side / Outline). When Outline is selected, the side/annotations panel is replaced with a compact heading navigator. Click any heading to jump the cursor. Roving tabindex for keyboard navigation. Disabled with explanatory text when the Tabbed layout (no left panel) is active. New testids: `outline-panel`, `outline-heading-{level}-{index}`, `left-slot-kind-radio-{side|outline}`.
|
|
19
|
+
- **Root-scoped editor font (closes #568)** — `--tandem-editor-font-family` is now applied to `document.documentElement` so the chosen font propagates to all surfaces (editor, tab labels, toolbar) rather than only the editor container. `applyEditorFontToRoot` and `createRootEditorFont` added alongside the existing per-element helpers.
|
|
20
|
+
- **Redesigned format badge on document tabs (closes #568)** — the 1-letter format icon is replaced with a styled pill badge (`MD`, `TXT`, `HTML`, `DOCX`) using format-specific semantic token colours. The dirty-dot slot is now always in layout with `visibility` toggled (prevents tab-width shift between dirty/clean states). New testids: `tab-format-badge-{id}`.
|
|
21
|
+
- **Temporary scratchpad (closes #475)** — `Ctrl+N` or "New Scratchpad" in the command palette / tab bar `+` menu opens an ephemeral in-memory document. Content is discarded when the tab is closed. Scratchpad paths use synthetic `upload://` URIs and are excluded from session restore and channel events. New `tandem_scratchpad` MCP tool lets Claude create scratchpads programmatically. Editor auto-focuses on mount for editable documents so the cursor is ready immediately.
|
|
22
|
+
- **Relative markdown link navigation (closes #479)** — Clicking relative `.md`, `.txt`, and `.html` links in the editor opens them as new Tandem tabs. External and non-supported links open in the default browser.
|
|
23
|
+
- **Documentation button in Settings (closes #457)** — A "View Documentation" button in the Settings popover About section opens `docs/workflows.md` as a read-only tab.
|
|
24
|
+
- **Store read-only warning banner (closes #506)** — When the annotation store is locked (read-only), a dismissible warning banner appears in the side panel. Dismiss state persists across sessions.
|
|
25
|
+
- **Claude Code automation hooks, agents, and skills (PR #591)** — 6 new hooks (stdout guard, svelte-check, token scanner, related-test runner, --no-verify blocker, E2E port-kill blocker), 2 specialized review agents (annotation-model, svelte-migration), 2 skills (`/changelog`, `/e2e-debug`), and `settings.json` wiring. Block hooks fail-closed on parse error; warn hooks include env-var opt-outs.
|
|
26
|
+
- **Theme-color meta tag sync** — `<meta name="theme-color">` updates reactively when the app theme changes, improving desktop PWA and mobile browser chrome appearance.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Authorship toggle moved to toolbar (closes #587)** — The "Show Authorship" toggle moved from the Settings popover Accessibility section to the main toolbar right cluster for faster access. New testid: `toolbar-authorship-toggle`.
|
|
31
|
+
- **Settings dialog responsive breakpoint (closes #515)** — stacked single-column layout at ≤640px; sidebar capped at 45% of dialog height with vertical scroll; four E2E tests cover nav reachability, Tab cycling, focus-after-resize, and content width.
|
|
32
|
+
- **Redesign bundle checked into `docs/redesign-bundle/` (#521)** — captured the current handoff, HTML previews, CSS, and JSX surfaces used for the app-shell visual pass so follow-on UI work is grounded in a repo-local artifact instead of a transient design URL.
|
|
33
|
+
- **Regression coverage added for the remaining app-shell contracts (#521)** — new Playwright and Vitest checks now cover connection banners, reply threads, panel resize, layout switching, onboarding, readonly DOCX review, and apply-changes behavior.
|
|
34
|
+
- **Keyboard navigation E2E tests for floating selection toolbar (closes #516)** — Tab/Shift+Tab focus traversal, Enter activation, and Escape-to-editor focus return are now covered by four Playwright tests documenting APG-compliant behavior for transient contextual toolbars.
|
|
35
|
+
- **Redesign final QA suite (closes #522)** — Playwright tests covering viewport layouts (600/1280/1920px), `prefers-reduced-motion`, forced-colors/high-contrast mode, dark/light color scheme switching, and keyboard Tab-order reachability.
|
|
36
|
+
- **Automated WCAG AA gate** — `tests/e2e/accessibility.spec.ts` uses `@axe-core/playwright` to verify zero contrast violations in both light and dark mode on every CI run; editor content area excluded (user-authored content has arbitrary contrast).
|
|
37
|
+
- **Inline link input replaces browser prompt (closes #548, #589)** — The FormattingToolbar's Link button now opens an inline popover instead of `window.prompt()`. The input pre-populates with the existing href when editing a link; submitting empty unsets the link. New testids: `toolbar-link-input`, `toolbar-link-submit`, `toolbar-link-cancel`.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Updater dialogs are now parented to the main window** — "Update Available", "No Updates Available", and "Update Error" dialogs attach to the Tandem window via `MessageDialogBuilder::parent()`, centering them over the app and inheriting Windows 11 dark-mode chrome from the `tauri-plugin-decorum` shell (closes #561, #553)
|
|
42
|
+
- **Custom window chrome via tauri-plugin-decorum** — native OS title bar replaced with a themed custom title bar that re-themes with the rest of the app; preserves Windows Aero Snap, Snap Layouts, resize border, and macOS traffic-light positioning (#554)
|
|
43
|
+
- **Tauri shell**: reload shortcuts (F5, Ctrl+F5, Shift+F5, Ctrl+R, Ctrl+Shift+R) are now blocked in the desktop app to prevent accidental navigation away from the editor; DevTools, Find, Print, and right-click context menu are preserved (#541)
|
|
44
|
+
- **Semantic token foundation expanded for redesign wave 2 (#521)** — added radius, font-size, shadow, z-index, editor-font-size, and highlight-color token families in `index.html`, plus checker rules that now flag raw `border-radius: <n>px` and inline `box-shadow: ... rgba(...)` in `src/client/`.
|
|
45
|
+
- **Read-only/info surfaces now use the shared info token family (#521)** — `ReviewOnlyBanner`, `ConnectionBanner`, `ToastContainer`, `StatusBar`, and related chrome now consume the shared token scales instead of hardcoded radius/text/shadow values.
|
|
46
|
+
|
|
47
|
+
### Tests
|
|
48
|
+
|
|
49
|
+
- **Plugin state machine unit tests for slash command menu (#517)** — added 7 Vitest tests in `tests/client/slash-command.test.ts` that exercise the ProseMirror plugin via a real Tiptap Editor in happy-dom: active state on `/` insertion, close meta, select meta, non-empty selection guard, query filtering with index clamping, ArrowDown wrap-around, and Enter-to-execute.
|
|
50
|
+
|
|
51
|
+
### Removed
|
|
52
|
+
|
|
53
|
+
- **`ReviewSummary` overlay removed with review mode already gone (#521)** — the dead component and `App.svelte` mount path are deleted rather than carried forward as unreachable redesign debt.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **Annotation console flood eliminated (closes #585)** — Deriving `audience` from annotation type is now silent; the `audience-derived` event type has been removed from the sanitization event system. New annotations also carry an explicit `audience` field at creation time.
|
|
58
|
+
- **Audience conflict guard (closes #584)** — User-authored notes and highlights can no longer be stored with `audience: outbound`. The sanitization layer enforces this invariant and emits an `audience-conflict-resolved` event when a conflict is detected.
|
|
59
|
+
- **Browser path: no light-flash on first paint for dark-mode users** — an inline pre-mount script in `index.html` reads the persisted theme preference (falling back to `matchMedia`) and sets `data-theme` on `<html>` before Svelte mounts, matching the behaviour the Tauri shell already provided via `window.__TANDEM_INITIAL_THEME__` (#551 partial — FOUC mitigated; matchMedia source-of-truth fix deferred to #477)
|
|
60
|
+
- **ErrorBoundary now offers in-place recovery before falling back to a full reload (#507)** — the app-root `<svelte:boundary>` re-renders children via `reset()` on a "Try to recover" click, capped at three attempts before forcing the user to reload. The budget resets after each successful recovery so an unrelated subsequent error gets a fresh three attempts. Failed-state surface uses `--tandem-error-bg`/`-border`/`-fg-strong` tokens (was neutral) and re-announces via `role="alert"` on each fresh failure.
|
|
61
|
+
- **Toolbar**: HighlightColorPicker border now uses `--tandem-border` token, correctly adapting to light/dark theme switching (#536)
|
|
62
|
+
- **Theme system: Tauri shell now reads Windows app-mode preference (`AppsUseLightTheme`) for `theme: "system"` instead of taskbar color mode (closes #535)** — `get_app_theme` Rust command reads `WebviewWindow::theme()`, which maps to `HKCU\...\Personalize\AppsUseLightTheme`. Initial theme is seeded before Svelte mounts; `useTauriTheme.svelte.ts` subscribes to `onThemeChanged` and polls every 3s while focused. `matchMedia` subscription is skipped in Tauri to prevent race conditions.
|
|
63
|
+
- **Tauri shell: live OS app-mode flips now retheme without restart** — `systemTheme()` reads the live `tauriTheme.current` reactive store (updated by the Tauri theme bridge) instead of a startup-only snapshot; `applyTheme()` in `useTheme.svelte.ts` subscribes reactively so `<html data-theme>` updates immediately when the user switches Windows between light and dark app mode (Codex P1 follow-up to #535).
|
|
64
|
+
- **Dark annotation highlight colors** — `--tandem-highlight-yellow/green/blue/pink` now have dark-adapted overrides in `[data-theme="dark"]`; the light `rgba(255, 235, 59, 0.3)`-style values were washed out against dark surfaces.
|
|
65
|
+
- **Forced-colors fallbacks for background-only state surfaces (closes #311)** — StatusBar status dots, toast badge, ModeToggle active button, BulkActions confirm button, AnnotationCard type-badge and Private pill now have `border`/`outline` fallbacks in `@media (forced-colors: active)`.
|
|
66
|
+
|
|
67
|
+
## \[0.10.1] - Unreleased
|
|
68
|
+
|
|
69
|
+
Plugin URL and auth resolution for custom-port and network-remote setups.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- **Monitor and channel honor `CLAUDE_PLUGIN_OPTION_SERVER_URL`** — `resolveTandemUrl()` now checks the `CLAUDE_PLUGIN_OPTION_SERVER_URL` environment variable (exported by Claude Code's plugin host from `plugin.json` `userConfig`) before falling back to `TANDEM_URL` and the localhost default. Both the monitor (`src/monitor/index.ts`) and channel shim (`src/channel/run.ts`) benefit automatically. No change for existing installs that don't use `userConfig`.
|
|
74
|
+
- **Monitor and channel honor `CLAUDE_PLUGIN_OPTION_AUTH_TOKEN`** — new `resolveAuthToken()` function in `src/shared/cli-runtime.ts` mirrors `resolveTandemUrl()`. Precedence: `CLAUDE_PLUGIN_OPTION_AUTH_TOKEN` → `TANDEM_AUTH_TOKEN`. `authFetch` uses it automatically, so all stdio subcommands gain the new lookup without caller changes.
|
|
75
|
+
|
|
76
|
+
## \[0.10.0] - 2026-05-03
|
|
77
|
+
|
|
78
|
+
Complete React → Svelte 5 migration. All 39 client `.tsx` files have been replaced with Svelte 5 rune-based equivalents; `react`, `react-dom`, and `@tiptap/react` are no longer in the bundle. Includes a review-mode correctness fix, accessibility improvements, and follow-on Codex security hardening.
|
|
79
|
+
|
|
80
|
+
### Removed
|
|
81
|
+
|
|
82
|
+
- **`react`, `react-dom`, `@tiptap/react` dropped (#472, #508)** — the React adapter layer is gone. The editor integrates directly with `@tiptap/core` via Svelte 5 components. Bundle size and startup time both decrease.
|
|
83
|
+
- **`tandem_suggest`, `tandem_flag`, `tandem_highlight` hard-removed** — stub tools deprecated in v0.9.0 (ADR-027) are now fully removed. MCP tool count: 28 → 25.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- **React → Svelte 5 migration (#472, #508)** — all client components rewritten with Svelte 5 runes (`$state`, `$derived`, `$effect`). Component APIs, data-testid selectors, and observable behavior are unchanged; only the rendering layer is new.
|
|
88
|
+
- **Note annotation actions** — note cards in the side panel now show **Archive** and **Send to Claude** instead of Remove. "Send to Claude" promotes the note to a comment and fires an `annotation:created` channel event so Claude is notified immediately.
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **Review mode incorrectly treated private notes as review targets (#512, #523)** — Tab/Y/N keyboard navigation, "Accept All" / "Dismiss All" bulk actions, the "Review Complete" overlay trigger, tally counts, and the chat tab badge now all exclude `type: "note"` annotations. Notes remain visible as cards in the side panel. Word-imported comments (`author: "import"`) continue to be review targets.
|
|
93
|
+
- **Note privacy — `tandem_getAnnotations` and channel events never surface notes to Claude** — `type: "note"` entries are filtered from MCP tool responses and SSE channel events (Codex security review).
|
|
94
|
+
- **Y.Map key strings enforced via constants** — raw string literals for Y.Map keys eliminated across the codebase; all access goes through `Y_MAP_ANNOTATIONS`, `Y_MAP_AWARENESS`, etc. from `shared/constants.ts` (Codex security review).
|
|
95
|
+
- **Chat message XSS hardening** — link rendering in the chat panel now enforces a protocol allowlist (`https:`, `http:`, `mailto:`), blocking `javascript:` and other unsafe schemes (Codex security review).
|
|
96
|
+
- **`annotation:edited` channel event deduplication** — rapid successive edits no longer emit duplicate events to the channel (Codex security review).
|
|
97
|
+
- **`svelte-check --fail-on-warnings` now gates the build** — 26 pre-existing Svelte type warnings cleared; CI enforces zero-warning policy going forward.
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
|
|
101
|
+
- **Keyboard-accessible panel resize handles (#511, #524)** — Arrow keys resize by ±16 px, Page Up/Down by ±80 px, Home/End snap to the minimum/maximum width. `aria-valuenow` reflects the live panel width.
|
|
102
|
+
- **ARIA dialog focus management (#511, #524)** — HelpModal and ReviewSummary now trap Tab focus, restore focus on close, and close on Escape. Backdrops carry `role="presentation"`; dialog containers carry `role="dialog" aria-modal="true" tabindex="-1"`.
|
|
103
|
+
- **Form label associations (#511, #524)** — AnnotationEditForm inputs are now properly associated with their `<label>` elements.
|
|
104
|
+
- **AnnotationCard role corrected (#511, #524)** — changed from `role="button"` (nested-button violation) to `role="listitem"`.
|
|
9
105
|
|
|
10
106
|
## \[0.9.1] - 2026-05-01
|
|
11
107
|
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved.
|
|
2
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
3
|
+
|
|
4
|
+
Parameters
|
|
5
|
+
|
|
6
|
+
Licensor: Bryan Kolb
|
|
7
|
+
Licensed Work: Tandem (tandem-editor npm package).
|
|
8
|
+
The Licensed Work is (c) 2026 Bryan Kolb.
|
|
9
|
+
Additional Use Grant: Personal use and individual self-hosting are permitted;
|
|
10
|
+
commercial hosting or resale of the Licensed Work is not.
|
|
11
|
+
Change Date: The earlier of 2029-06-10 or two years after the public
|
|
12
|
+
general availability release of Tandem v1.0.
|
|
13
|
+
Change License: MIT License
|
|
14
|
+
|
|
15
|
+
Notice
|
|
16
|
+
|
|
17
|
+
Business Source License 1.1
|
|
18
|
+
|
|
19
|
+
Terms
|
|
20
|
+
|
|
21
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
22
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
23
|
+
Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
24
|
+
|
|
25
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
26
|
+
available distribution of a specific version of the Licensed Work under this
|
|
27
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
28
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
29
|
+
above terminate.
|
|
30
|
+
|
|
31
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
32
|
+
currently in effect as described in this License, you must purchase a
|
|
33
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
34
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
35
|
+
|
|
36
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
37
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
38
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
39
|
+
for each version of the Licensed Work released by Licensor.
|
|
40
|
+
|
|
41
|
+
You must conspicuously display this License on each original or modified copy
|
|
42
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
43
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
44
|
+
License apply to your use of that work.
|
|
45
|
+
|
|
46
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
47
|
+
terminate your rights under this License for the current and all other
|
|
48
|
+
versions of the Licensed Work.
|
|
49
|
+
|
|
50
|
+
This License does not grant you any right in any trademark or logo of
|
|
51
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
52
|
+
Licensor as expressly required by this License).
|
|
53
|
+
|
|
54
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
55
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
56
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
57
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
58
|
+
TITLE.
|
package/README.md
CHANGED
|
@@ -303,4 +303,4 @@ On first run, `sample/welcome.md` auto-opens. If you've cleared sessions or dele
|
|
|
303
303
|
|
|
304
304
|
**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.
|
|
305
305
|
|
|
306
|
-
**Tech Stack:**
|
|
306
|
+
**Tech Stack:** Svelte 5, 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
|
@@ -3106,6 +3106,9 @@ var require_utils = __commonJS({
|
|
|
3106
3106
|
"use strict";
|
|
3107
3107
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3108
3108
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3109
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3110
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3111
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3109
3112
|
function stringArrayToHexStripped(input) {
|
|
3110
3113
|
let acc = "";
|
|
3111
3114
|
let code = 0;
|
|
@@ -3298,27 +3301,77 @@ var require_utils = __commonJS({
|
|
|
3298
3301
|
}
|
|
3299
3302
|
return output.join("");
|
|
3300
3303
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3304
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3305
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3306
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3307
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3308
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3309
|
+
re.lastIndex = 0;
|
|
3310
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3311
|
+
}
|
|
3312
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3313
|
+
if (input.indexOf("%") === -1) {
|
|
3314
|
+
return input;
|
|
3311
3315
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3316
|
+
let output = "";
|
|
3317
|
+
for (let i = 0; i < input.length; i++) {
|
|
3318
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3319
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3320
|
+
if (isHexPair(hex)) {
|
|
3321
|
+
const normalizedHex = hex.toUpperCase();
|
|
3322
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3323
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3324
|
+
output += decoded;
|
|
3325
|
+
} else {
|
|
3326
|
+
output += "%" + normalizedHex;
|
|
3327
|
+
}
|
|
3328
|
+
i += 2;
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
output += input[i];
|
|
3314
3333
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3334
|
+
return output;
|
|
3335
|
+
}
|
|
3336
|
+
function normalizePathEncoding(input) {
|
|
3337
|
+
let output = "";
|
|
3338
|
+
for (let i = 0; i < input.length; i++) {
|
|
3339
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3340
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3341
|
+
if (isHexPair(hex)) {
|
|
3342
|
+
const normalizedHex = hex.toUpperCase();
|
|
3343
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3344
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3345
|
+
output += decoded;
|
|
3346
|
+
} else {
|
|
3347
|
+
output += "%" + normalizedHex;
|
|
3348
|
+
}
|
|
3349
|
+
i += 2;
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
if (isPathCharacter(input[i])) {
|
|
3354
|
+
output += input[i];
|
|
3355
|
+
} else {
|
|
3356
|
+
output += escape(input[i]);
|
|
3357
|
+
}
|
|
3317
3358
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3359
|
+
return output;
|
|
3360
|
+
}
|
|
3361
|
+
function escapePreservingEscapes(input) {
|
|
3362
|
+
let output = "";
|
|
3363
|
+
for (let i = 0; i < input.length; i++) {
|
|
3364
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3365
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3366
|
+
if (isHexPair(hex)) {
|
|
3367
|
+
output += "%" + hex.toUpperCase();
|
|
3368
|
+
i += 2;
|
|
3369
|
+
continue;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
output += escape(input[i]);
|
|
3320
3373
|
}
|
|
3321
|
-
return
|
|
3374
|
+
return output;
|
|
3322
3375
|
}
|
|
3323
3376
|
function recomposeAuthority(component) {
|
|
3324
3377
|
const uriTokens = [];
|
|
@@ -3333,7 +3386,7 @@ var require_utils = __commonJS({
|
|
|
3333
3386
|
if (ipV6res.isIPV6 === true) {
|
|
3334
3387
|
host = `[${ipV6res.escapedHost}]`;
|
|
3335
3388
|
} else {
|
|
3336
|
-
host =
|
|
3389
|
+
host = reescapeHostDelimiters(host, false);
|
|
3337
3390
|
}
|
|
3338
3391
|
}
|
|
3339
3392
|
uriTokens.push(host);
|
|
@@ -3347,7 +3400,10 @@ var require_utils = __commonJS({
|
|
|
3347
3400
|
module.exports = {
|
|
3348
3401
|
nonSimpleDomain,
|
|
3349
3402
|
recomposeAuthority,
|
|
3350
|
-
|
|
3403
|
+
reescapeHostDelimiters,
|
|
3404
|
+
normalizePercentEncoding,
|
|
3405
|
+
normalizePathEncoding,
|
|
3406
|
+
escapePreservingEscapes,
|
|
3351
3407
|
removeDotSegments,
|
|
3352
3408
|
isIPv4,
|
|
3353
3409
|
isUUID,
|
|
@@ -3571,12 +3627,12 @@ var require_schemes = __commonJS({
|
|
|
3571
3627
|
var require_fast_uri = __commonJS({
|
|
3572
3628
|
"node_modules/fast-uri/index.js"(exports, module) {
|
|
3573
3629
|
"use strict";
|
|
3574
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3630
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3575
3631
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3576
3632
|
function normalize(uri, options) {
|
|
3577
3633
|
if (typeof uri === "string") {
|
|
3578
3634
|
uri = /** @type {T} */
|
|
3579
|
-
|
|
3635
|
+
normalizeString(uri, options);
|
|
3580
3636
|
} else if (typeof uri === "object") {
|
|
3581
3637
|
uri = /** @type {T} */
|
|
3582
3638
|
parse3(serialize(uri, options), options);
|
|
@@ -3643,19 +3699,9 @@ var require_fast_uri = __commonJS({
|
|
|
3643
3699
|
return target;
|
|
3644
3700
|
}
|
|
3645
3701
|
function equal(uriA, uriB, options) {
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
} else if (typeof uriA === "object") {
|
|
3650
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3651
|
-
}
|
|
3652
|
-
if (typeof uriB === "string") {
|
|
3653
|
-
uriB = unescape(uriB);
|
|
3654
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
3655
|
-
} else if (typeof uriB === "object") {
|
|
3656
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3657
|
-
}
|
|
3658
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3702
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3703
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3704
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3659
3705
|
}
|
|
3660
3706
|
function serialize(cmpts, opts) {
|
|
3661
3707
|
const component = {
|
|
@@ -3680,12 +3726,12 @@ var require_fast_uri = __commonJS({
|
|
|
3680
3726
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3681
3727
|
if (component.path !== void 0) {
|
|
3682
3728
|
if (!options.skipEscape) {
|
|
3683
|
-
component.path =
|
|
3729
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3684
3730
|
if (component.scheme !== void 0) {
|
|
3685
3731
|
component.path = component.path.split("%3A").join(":");
|
|
3686
3732
|
}
|
|
3687
3733
|
} else {
|
|
3688
|
-
component.path =
|
|
3734
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3689
3735
|
}
|
|
3690
3736
|
}
|
|
3691
3737
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3720,7 +3766,16 @@ var require_fast_uri = __commonJS({
|
|
|
3720
3766
|
return uriTokens.join("");
|
|
3721
3767
|
}
|
|
3722
3768
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3723
|
-
function
|
|
3769
|
+
function getParseError(parsed, matches) {
|
|
3770
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3771
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3772
|
+
}
|
|
3773
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3774
|
+
return "URI port is malformed.";
|
|
3775
|
+
}
|
|
3776
|
+
return void 0;
|
|
3777
|
+
}
|
|
3778
|
+
function parseWithStatus(uri, opts) {
|
|
3724
3779
|
const options = Object.assign({}, opts);
|
|
3725
3780
|
const parsed = {
|
|
3726
3781
|
scheme: void 0,
|
|
@@ -3731,6 +3786,7 @@ var require_fast_uri = __commonJS({
|
|
|
3731
3786
|
query: void 0,
|
|
3732
3787
|
fragment: void 0
|
|
3733
3788
|
};
|
|
3789
|
+
let malformedAuthorityOrPort = false;
|
|
3734
3790
|
let isIP = false;
|
|
3735
3791
|
if (options.reference === "suffix") {
|
|
3736
3792
|
if (options.scheme) {
|
|
@@ -3751,6 +3807,11 @@ var require_fast_uri = __commonJS({
|
|
|
3751
3807
|
if (isNaN(parsed.port)) {
|
|
3752
3808
|
parsed.port = matches[5];
|
|
3753
3809
|
}
|
|
3810
|
+
const parseError = getParseError(parsed, matches);
|
|
3811
|
+
if (parseError !== void 0) {
|
|
3812
|
+
parsed.error = parsed.error || parseError;
|
|
3813
|
+
malformedAuthorityOrPort = true;
|
|
3814
|
+
}
|
|
3754
3815
|
if (parsed.host) {
|
|
3755
3816
|
const ipv4result = isIPv4(parsed.host);
|
|
3756
3817
|
if (ipv4result === false) {
|
|
@@ -3789,14 +3850,18 @@ var require_fast_uri = __commonJS({
|
|
|
3789
3850
|
parsed.scheme = unescape(parsed.scheme);
|
|
3790
3851
|
}
|
|
3791
3852
|
if (parsed.host !== void 0) {
|
|
3792
|
-
parsed.host = unescape(parsed.host);
|
|
3853
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3793
3854
|
}
|
|
3794
3855
|
}
|
|
3795
3856
|
if (parsed.path) {
|
|
3796
|
-
parsed.path =
|
|
3857
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3797
3858
|
}
|
|
3798
3859
|
if (parsed.fragment) {
|
|
3799
|
-
|
|
3860
|
+
try {
|
|
3861
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3862
|
+
} catch {
|
|
3863
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3864
|
+
}
|
|
3800
3865
|
}
|
|
3801
3866
|
}
|
|
3802
3867
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3805,7 +3870,29 @@ var require_fast_uri = __commonJS({
|
|
|
3805
3870
|
} else {
|
|
3806
3871
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3807
3872
|
}
|
|
3808
|
-
return parsed;
|
|
3873
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3874
|
+
}
|
|
3875
|
+
function parse3(uri, opts) {
|
|
3876
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3877
|
+
}
|
|
3878
|
+
function normalizeString(uri, opts) {
|
|
3879
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3880
|
+
}
|
|
3881
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3882
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3883
|
+
return {
|
|
3884
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3885
|
+
malformedAuthorityOrPort
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
function normalizeComparableURI(uri, opts) {
|
|
3889
|
+
if (typeof uri === "string") {
|
|
3890
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3891
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3892
|
+
}
|
|
3893
|
+
if (typeof uri === "object") {
|
|
3894
|
+
return serialize(uri, opts);
|
|
3895
|
+
}
|
|
3809
3896
|
}
|
|
3810
3897
|
var fastUri = {
|
|
3811
3898
|
SCHEMES,
|
|
@@ -17930,23 +18017,45 @@ function redirectConsoleToStderr() {
|
|
|
17930
18017
|
console.info = console.error;
|
|
17931
18018
|
}
|
|
17932
18019
|
function resolveTandemUrl(override) {
|
|
17933
|
-
|
|
17934
|
-
|
|
18020
|
+
return resolveTandemUrlCandidate(override).replace(/\/+$/, "");
|
|
18021
|
+
}
|
|
18022
|
+
function resolveTandemUrlCandidate(override) {
|
|
18023
|
+
const candidates = [
|
|
18024
|
+
override,
|
|
18025
|
+
process.env.CLAUDE_PLUGIN_OPTION_SERVER_URL,
|
|
18026
|
+
process.env.TANDEM_URL
|
|
18027
|
+
];
|
|
18028
|
+
for (const url of candidates) {
|
|
18029
|
+
if (url !== void 0 && url.trim() !== "") return url.trim();
|
|
18030
|
+
}
|
|
18031
|
+
return `http://localhost:${DEFAULT_MCP_PORT}`;
|
|
18032
|
+
}
|
|
18033
|
+
function resolveAuthTokenCandidate(override) {
|
|
18034
|
+
const candidates = [
|
|
18035
|
+
["explicit override", override],
|
|
18036
|
+
["CLAUDE_PLUGIN_OPTION_AUTH_TOKEN", process.env.CLAUDE_PLUGIN_OPTION_AUTH_TOKEN],
|
|
18037
|
+
["TANDEM_AUTH_TOKEN", process.env.TANDEM_AUTH_TOKEN]
|
|
18038
|
+
];
|
|
18039
|
+
for (const [source, token] of candidates) {
|
|
18040
|
+
if (token !== void 0 && token.trim() !== "") return { token, source };
|
|
18041
|
+
}
|
|
18042
|
+
return { token: void 0, source: void 0 };
|
|
17935
18043
|
}
|
|
17936
18044
|
var VALID_TOKEN_RE = /^[A-Za-z0-9_\-]{32,}$/;
|
|
17937
18045
|
var _warnedInvalidToken = false;
|
|
17938
18046
|
async function authFetch(url, init) {
|
|
17939
|
-
const token =
|
|
17940
|
-
if (token !== void 0
|
|
17941
|
-
|
|
18047
|
+
const { token, source } = resolveAuthTokenCandidate();
|
|
18048
|
+
if (token !== void 0) {
|
|
18049
|
+
const trimmed = token.trim();
|
|
18050
|
+
if (VALID_TOKEN_RE.test(trimmed)) {
|
|
17942
18051
|
const headers = new Headers(init?.headers);
|
|
17943
|
-
headers.set("Authorization", `Bearer ${
|
|
18052
|
+
headers.set("Authorization", `Bearer ${trimmed}`);
|
|
17944
18053
|
return fetch(url, { ...init, headers });
|
|
17945
18054
|
}
|
|
17946
18055
|
if (!_warnedInvalidToken) {
|
|
17947
18056
|
_warnedInvalidToken = true;
|
|
17948
18057
|
console.error(
|
|
17949
|
-
|
|
18058
|
+
`[tandem] authFetch: ${source} is set but invalid (must be 32+ alphanumeric chars [A-Za-z0-9_-]); sending without Authorization header`
|
|
17950
18059
|
);
|
|
17951
18060
|
}
|
|
17952
18061
|
}
|
|
@@ -17955,7 +18064,8 @@ async function authFetch(url, init) {
|
|
|
17955
18064
|
|
|
17956
18065
|
// src/shared/fetch-with-timeout.ts
|
|
17957
18066
|
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
17958
|
-
const
|
|
18067
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
18068
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
17959
18069
|
return authFetch(url, { ...init, signal });
|
|
17960
18070
|
}
|
|
17961
18071
|
function describeFetchError(err, endpoint, timeoutMs) {
|
|
@@ -17973,6 +18083,7 @@ var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
17973
18083
|
"annotation:created",
|
|
17974
18084
|
"annotation:accepted",
|
|
17975
18085
|
"annotation:dismissed",
|
|
18086
|
+
"annotation:edited",
|
|
17976
18087
|
"annotation:reply",
|
|
17977
18088
|
"chat:message",
|
|
17978
18089
|
"document:opened",
|
|
@@ -18002,6 +18113,10 @@ function formatEventContent(event) {
|
|
|
18002
18113
|
const { annotationId, textSnippet } = event.payload;
|
|
18003
18114
|
return `User dismissed annotation ${annotationId}${textSnippet ? ` ("${textSnippet}")` : ""}${doc}`;
|
|
18004
18115
|
}
|
|
18116
|
+
case "annotation:edited": {
|
|
18117
|
+
const { content } = event.payload;
|
|
18118
|
+
return `User edited annotation: "${content}"${doc}`;
|
|
18119
|
+
}
|
|
18005
18120
|
case "annotation:reply": {
|
|
18006
18121
|
const { annotationId, replyAuthor, replyText, textSnippet } = event.payload;
|
|
18007
18122
|
const who = replyAuthor === "claude" ? "Claude" : "User";
|
|
@@ -18044,6 +18159,10 @@ function formatEventMeta(event) {
|
|
|
18044
18159
|
case "annotation:dismissed":
|
|
18045
18160
|
meta.annotation_id = event.payload.annotationId;
|
|
18046
18161
|
break;
|
|
18162
|
+
case "annotation:edited":
|
|
18163
|
+
meta.annotation_id = event.payload.annotationId;
|
|
18164
|
+
meta.edited_at = String(event.payload.editedAt);
|
|
18165
|
+
break;
|
|
18047
18166
|
case "annotation:reply":
|
|
18048
18167
|
meta.annotation_id = event.payload.annotationId;
|
|
18049
18168
|
meta.reply_id = event.payload.replyId;
|