tandem-editor 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.14.1] - 2026-06-11
11
+
12
+ ### Added
13
+
14
+ - **Motion — rail open/close + hover-reveal floating mode (Phase 4 / #798, scenes A8·A12)** — the side rails (left outline, right annotations/chat) now **ease** between open and closed instead of snapping, and gain an optional **hover-reveal** mode that lands the A8 rail-reveal + A12 peek-collapse work previously deferred from the mode-toggle and tab-close clusters. (1) **Width/shadow transition** (`360ms`/`280ms` on the `--tandem-ease-out`-shaped bezier) on the always-mounted dual-layer shell. Because `display:none` can't transition and is load-bearing (it kills the editor scroll-pop and drops the rail from the Tab order at rest), the collapse-direction `display:none` is deferred by a per-side `animating` flag until the width transition ends (cleared on the shell's *own* `width` transitionend — guarded `e.target === e.currentTarget` so the floating panel's bubbled transform/opacity transitionends can't clear it early — with a 400ms timeout backstop), so the content clips away with the shrinking width then drops out cleanly. (2) **Hover-reveal floating mode** (new `railHoverReveal` setting, default on; `appearance-rail-hover-reveal` toggle): hovering a *closed* rail floats its full panel **over** the editor while the 14px collapsed shell stays in flow, so the editor never reflows; the panel slides in from the window edge (`@keyframes`, 280ms). Clicking the edge/peek zone while floating **pins** it (reusing the existing `toggle*Panel`, which pins when the rail isn't visible; the transient float flag is cleared before the visibility commit so there's no intermediate frame). The float is transient — it auto-hides when *neither* the pointer nor focus is inside, with a `focusout` path that closes the focus-sticky case (e.g. the outline `jumpTo` moving focus into the editor) so it can't strand open. Hover timers are plain non-`$state` refs (clear-before-schedule, unmount-only cleanup). When the setting is off, the classic 14→28px peek-grow returns (CSS-gated on a `body.tandem-rail-hover-reveal` class). Both behaviors fully honor reduced motion (the in-app `reduceMotion` setting via `body.tandem-reduce-motion` + OS `prefers-reduced-motion`): the width snaps (the JS also skips the `animating` flag, so `display:none` applies synchronously) and the float-in keyframe is disabled. (3) **Rail drop shadows** strengthened slightly in light + dark (`--tandem-rail-shadow-left/right`) so the floating chrome reads with more depth; a new `--tandem-z-rail-float` token sits the float above the editor and the right rail's at-rest `z-index`, below dropdowns so in-panel menus still escape. Schema v14→v15 (pure version bump; missing field defaults on, explicit `false` preserved). New testids `rail-float-left`/`rail-float-right` (present only while floating); every existing rail testid (`left-outline-rail`, `peek-strip-*`, `panel-edge-collapse-*`, `*-resize-handle`) preserved. `.svelte`/`index.html`/settings-only: no `src/server/` changes, no annotation-model change. Plan reviewed pre-code by svelte-migration + general adversarial agents (which caught the transitionend bubble guard, the explicit `normalizeKnownFields` edit + schema-bump convention, the plain-`let` timer discipline, and the focus-sticky stuck-open hole before any code).
15
+
16
+ ### Fixed
17
+
18
+ - **"Relaunch Claude" works on the desktop app — the reaper binary is now bundled, and launcher errors are no longer swallowed** — the auto-launcher spawns Claude Code *through* the `tandem-reaper` binary (so Claude dies when Tandem does), but the reaper was never packaged into the Tauri desktop app: it was absent from `externalBin` and no build step compiled it. So **every** desktop build since the launcher shipped (PR #800) failed "Relaunch Claude" / "Start fresh" with `tandem-reaper binary not found` — and the failure was invisible because the launcher API returned a hardcoded `"relaunch failed"` to the UI while logging the real reason only to stderr (the toast read *"Relaunch failed: relaunch failed."*). Now: (1) a new `scripts/build-reaper.mjs` compiles the `reaper/` crate into `src-tauri/binaries/tandem-reaper-<triple>[.exe]`, wired into the release/webdriver workflows, the CI `tauri_build` stubs, `npm run dev:tauri`, and a new `npm run build:reaper`; `tandem-reaper` is added to `externalBin` and the macOS release verification now asserts it's bundled (the check that would have caught this). (2) The launcher API surfaces the **real** error reason (bounded to 300 chars) instead of the static label; for the missing-reaper case it returns a friendly message (*"Claude launcher binary missing — reinstall Tandem to restore it."*, which the toast renders verbatim) plus a stable `REAPER_NOT_FOUND` code for programmatic use. `reaperPath()` already resolves adjacent to the sidecar, which is exactly where `externalBin` lands the binary. (3) **Async spawn failures now surface too** — a reaper that exists at check time but can't actually exec (wrong arch, not executable, a swap after the check) reports its failure via `spawn()`'s *asynchronous* `error` event, which fired *after* the relaunch route had already returned `{ ok: true }`; the supervisor now awaits the immediate spawn outcome (the `spawn` event, or an early `error`/`exit`) so that class of failure reaches the toast — mapped to the same `REAPER_NOT_FOUND` hint — instead of looking like success. **Also fixed:** `tandem doctor` no longer false-flags a valid `store.lock` as "unparseable content" — it now reads both the bare-integer-PID and JSON-object (`{"pid":…}`) lock formats, and tolerates a malformed JSON lock (wrong-typed or absent `pid`) as unparseable rather than mis-reading it. **macOS:** the reaper's kqueue watch was ported to the `nix` 0.29 `Kqueue::kevent` method (the old free-function + `as_raw_fd` form no longer compiles, and a `0` timeout would have busy-polled a core), so it now blocks on `EVFILT_PROC`/`NOTE_EXIT` correctly — the v0.14.1 macOS build is the first to bundle a reaper that compiles. (Desktop end-to-end relaunch click is a manual Tauri pass.)
19
+ - **Cowork now detects a direct-download Claude Desktop install and self-heals workspaces that appear after enable (Windows; ADR-044)** — Cowork detection only scanned the MSIX Store layout (`%LOCALAPPDATA%\Packages\Claude_*\…`), so the common direct-download Claude Desktop install — whose sessions live at `%APPDATA%\Claude\local-agent-mode-sessions` — always reported "Not detected on this computer". Detection now scans **both roots** (MSIX + Roaming), dedupes exact aliases, and caps workspaces per root. The MSIX package match is publisher-anchored (`Claude_*` | `AnthropicPBC.Claude*`) rather than a loose `contains("Claude")`, so a foreign package sharing the sandbox can never receive the token, and a workspace-shape guard (UUID-or-`cowork_plugins`-marker) keeps plugin-registry writes off `skills-plugin\…` siblings under the Roaming root. `check_acl` now **fails closed** and allows only the Roaming Claude sessions subtree (where the token already lives via `claude_desktop_config.json` — no new exposure class). A 5-minute background **heal pass** installs into workspaces that appear after Cowork is enabled (read-only precheck, never any firewall/UAC work), recording its per-process attempt only on **terminal** outcomes (success / insecure-ACL) so a transiently-failing workspace (locked / schema-drift) stays retryable instead of being stranded until restart. Write-time path revalidation now covers the non-handle write paths too (enable / rescan / heal / apply-to-all / orphan-reconcile), closing the residual #433 scan→write TOCTOU. The wizard's undetected state carries an honest sub-reason (no Claude / no workspaces yet / blocked), and **Check again** refetches Cowork. Windows-only; no `src/server/` or annotation-model change.
20
+ - **`tandem doctor` no longer flags a valid lockfile as "unparseable content"** — the annotation store-lock check still parsed the lock as a bare PID (`parseInt`), but locks have been v2 JSON (`{pid, startedAtMs, app}`) since #1077. A healthy lock therefore reported a spurious warning ("unparseable content: `{…}`"). The doctor now reuses the store's `parseLockfile` (which reads both v2 JSON and legacy raw-PID formats) so a live lock reports the holder PID and liveness correctly. The pure lock format/parsing moved to `src/server/annotations/lockfile.ts` so the CLI doctor can read a lock without pulling in the store's file-io/notifications/platform dependency graph; `store.ts` re-exports it unchanged.
21
+
10
22
  ## [0.14.0] - 2026-06-10
11
23
 
12
24
  ### Added
package/README.md CHANGED
@@ -115,7 +115,7 @@ See [docs/security.md](docs/security.md) for the full security model.
115
115
 
116
116
  ## Where Tandem is headed
117
117
 
118
- Tandem is on the way to a v1.0 release. Recent releases added support for multiple AI providers and in-app configuration for connections and models. Work still in progress covers improvements to how Word documents round-trip through the editor, turnkey setup on macOS and Linux, and final polish. Tandem is free during the public beta; at v1.0 it moves to a one-time paid license, and beta users are grandfathered with a free license. The full plan lives in [docs/roadmap.md](docs/roadmap.md).
118
+ Tandem is on the way to a v1.0 release. Today the supported AI integration is Claude (Claude Code / Claude Desktop) over MCP, set up with a one-click in-app wizard. Local models (Ollama, LM Studio) are committed for v1.0 and in active development (#1123) — they'll use the same one-time license as everything else; cloud API-key providers (OpenAI, Gemini) follow in v1.1. Recent releases added Word document write-back (edit a `.docx` and save it back as a real Word file — comments you've sent to your AI are written back as native Word comments) and pre-overwrite backups with in-product restore. Work still in progress covers turnkey setup on macOS and Linux, licensing, and final polish. Tandem is free during the public beta; at v1.0 it moves to a one-time paid license, and beta users are grandfathered with a free license. The full plan lives in [docs/roadmap.md](docs/roadmap.md).
119
119
 
120
120
  ## Documentation
121
121
 
@@ -163,7 +163,7 @@ Client compatibility:
163
163
  | **Claude Desktop** (local app) | Supported via the Cowork bridge. Channel push N/A. |
164
164
  | **claude.ai web chat** | Not supported. Would require exposing the local server publicly via a tunnel, which is outside scope. |
165
165
  | **Other MCP-capable clients** (Cursor, Continue.dev, LM Studio, Ollama, …) | Best-effort, MCP-contract-compatible, not validated. |
166
- | **Non-MCP AIs** (ChatGPT direct, Gemini direct, etc.) | Not supported today. Multi-provider support is in progress via the Agent SDK adapter ([ADR-038 §3](docs/decisions.md#adr-038-mcp-first-integration-policy-claude-as-default-integration)); not yet shippable. |
166
+ | **Non-MCP AIs** | Not supported today. **Local models** (Ollama / LM Studio via OpenAI-compatible endpoints) are committed for v1.0 ([ADR-039](docs/decisions.md#adr-039-non-mcp-model-providers-local-slice-v10-cloud-slice-v11), tracked in #1123); cloud providers (ChatGPT direct, Gemini direct) follow in v1.1. |
167
167
 
168
168
  ### MCP tools at a glance
169
169
 
package/dist/cli/index.js CHANGED
@@ -2396,6 +2396,33 @@ var init_channel = __esm({
2396
2396
  }
2397
2397
  });
2398
2398
 
2399
+ // src/server/annotations/lockfile.ts
2400
+ function parseLockfile(raw) {
2401
+ const trimmed = raw.trim();
2402
+ if (trimmed.startsWith("{")) {
2403
+ try {
2404
+ const parsed = JSON.parse(trimmed);
2405
+ const pid2 = parsed.pid;
2406
+ if (typeof pid2 !== "number" || !Number.isInteger(pid2) || pid2 <= 0) return null;
2407
+ return {
2408
+ pid: pid2,
2409
+ startedAtMs: typeof parsed.startedAtMs === "number" ? parsed.startedAtMs : void 0,
2410
+ app: typeof parsed.app === "string" ? parsed.app : void 0
2411
+ };
2412
+ } catch {
2413
+ return null;
2414
+ }
2415
+ }
2416
+ const pid = Number.parseInt(trimmed, 10);
2417
+ if (!Number.isFinite(pid) || pid <= 0) return null;
2418
+ return { pid };
2419
+ }
2420
+ var init_lockfile = __esm({
2421
+ "src/server/annotations/lockfile.ts"() {
2422
+ "use strict";
2423
+ }
2424
+ });
2425
+
2399
2426
  // src/cli/doctor.ts
2400
2427
  var doctor_exports = {};
2401
2428
  __export(doctor_exports, {
@@ -2742,8 +2769,8 @@ function checkAnnotationStore(r) {
2742
2769
  }
2743
2770
  try {
2744
2771
  const raw = readFileSync3(lockPath, "utf-8").trim();
2745
- const pid = Number.parseInt(raw, 10);
2746
- if (!Number.isFinite(pid)) {
2772
+ const lock = parseLockfile(raw);
2773
+ if (lock === null) {
2747
2774
  r.warn(
2748
2775
  `Annotation store lock at ${lockPath} has unparseable content: "${raw}"`,
2749
2776
  "Restart Tandem or delete the lock file if no server is running.",
@@ -2751,6 +2778,7 @@ function checkAnnotationStore(r) {
2751
2778
  );
2752
2779
  return;
2753
2780
  }
2781
+ const { pid } = lock;
2754
2782
  if (isPidLive(pid)) {
2755
2783
  r.pass(`Annotation store lock held by live PID ${pid}`, void 0, {
2756
2784
  lockHeld: true,
@@ -2875,6 +2903,7 @@ var Recorder;
2875
2903
  var init_doctor = __esm({
2876
2904
  "src/cli/doctor.ts"() {
2877
2905
  "use strict";
2906
+ init_lockfile();
2878
2907
  init_constants();
2879
2908
  Recorder = class {
2880
2909
  failures = 0;
@@ -3151,7 +3180,7 @@ process.once("unhandledRejection", (reason) => {
3151
3180
  `);
3152
3181
  process.exit(1);
3153
3182
  });
3154
- var version = true ? "0.14.0" : "0.0.0-dev";
3183
+ var version = true ? "0.14.1" : "0.0.0-dev";
3155
3184
  var args = process.argv.slice(2);
3156
3185
  if (args.includes("--help") || args.includes("-h")) {
3157
3186
  console.log(`tandem v${version}