tandem-editor 0.12.0 → 0.13.5

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.
Files changed (38) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +121 -9
  4. package/README.md +142 -237
  5. package/dist/channel/index.js +158 -69
  6. package/dist/channel/index.js.map +1 -1
  7. package/dist/cli/index.js +656 -148
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/client/assets/CoworkSettings-BEvEpByR.js +3 -0
  10. package/dist/client/assets/CoworkSettings-C-1BnhBH.css +1 -0
  11. package/dist/client/assets/index-CXkPU0mo.css +1 -0
  12. package/dist/client/assets/index-D6kdw28e.js +238 -0
  13. package/dist/client/assets/index-Dm_QtxGQ.js +1 -0
  14. package/dist/client/assets/webview-KiZyy_pC.js +1 -0
  15. package/dist/client/fonts/OFL-Hanuman.txt +93 -0
  16. package/dist/client/fonts/OFL-InterTight.txt +93 -0
  17. package/dist/client/fonts/OFL-JetBrainsMono.txt +93 -0
  18. package/dist/client/fonts/OFL-SNPro.txt +93 -0
  19. package/dist/client/fonts/OFL-Sono.txt +93 -0
  20. package/dist/client/fonts/OFL-SourceSerif4.txt +93 -0
  21. package/dist/client/fonts/hanuman-latin.woff2 +0 -0
  22. package/dist/client/fonts/jetbrains-mono-latin.woff2 +0 -0
  23. package/dist/client/fonts/sn-pro-latin.woff2 +0 -0
  24. package/dist/client/fonts/sono-latin.woff2 +0 -0
  25. package/dist/client/fonts/source-serif-4-latin.woff2 +0 -0
  26. package/dist/client/index.html +341 -17
  27. package/dist/client/logo.png +0 -0
  28. package/dist/monitor/index.js +235 -161
  29. package/dist/monitor/index.js.map +1 -1
  30. package/dist/server/index.js +25323 -21754
  31. package/dist/server/index.js.map +1 -1
  32. package/package.json +11 -3
  33. package/sample/welcome.md +6 -6
  34. package/skills/tandem/SKILL.md +15 -0
  35. package/dist/client/assets/CoworkSettings-C0cS9R7L.js +0 -3
  36. package/dist/client/assets/index-Dn5JwXA3.css +0 -1
  37. package/dist/client/assets/index-n-vFW5By.js +0 -299
  38. package/dist/client/assets/webview-Bhf-n_os.js +0 -1
@@ -18007,6 +18007,7 @@ var TANDEM_REPO_URL = "https://github.com/bloknayrb/tandem";
18007
18007
  var TANDEM_ISSUES_NEW_URL = `${TANDEM_REPO_URL}/issues/new`;
18008
18008
  var MAX_FILE_SIZE = 50 * 1024 * 1024;
18009
18009
  var SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
18010
+ var TANDEM_MODE_DEFAULT = "tandem";
18010
18011
  var CHANNEL_MAX_RETRIES = 5;
18011
18012
  var CHANNEL_RETRY_DELAY_MS = 2e3;
18012
18013
  var CHANNEL_CONNECT_FETCH_TIMEOUT_MS = 1e4;
@@ -18219,52 +18220,82 @@ var ToolErrorCodeSchema = external_exports.enum([
18219
18220
  var ChannelErrorCodeSchema = external_exports.enum(["CHANNEL_CONNECT_FAILED", "MONITOR_CONNECT_FAILED"]);
18220
18221
  var CHANNEL_CONNECT_FAILED = "CHANNEL_CONNECT_FAILED";
18221
18222
 
18222
- // src/channel/event-bridge.ts
18223
+ // src/shared/sse-consumer.ts
18223
18224
  var AWARENESS_DEBOUNCE_MS = 500;
18225
+ var AWARENESS_CLEAR_MS = 3e3;
18224
18226
  var MODE_CACHE_TTL_MS = 2e3;
18225
- async function startEventBridge(mcp, tandemUrl) {
18227
+ var STABLE_CONNECTION_MS = 6e4;
18228
+ var RETRY_MAX_DELAY_MS = 3e4;
18229
+ var shutdownTimers = { awarenessTimer: null, clearAwarenessTimer: null, lastDocumentId: null };
18230
+ var outstandingAwareness = /* @__PURE__ */ new Set();
18231
+ function trackAwareness(p) {
18232
+ outstandingAwareness.add(p);
18233
+ p.finally(() => outstandingAwareness.delete(p));
18234
+ }
18235
+ var cachedMode = TANDEM_MODE_DEFAULT;
18236
+ var cachedModeAt = 0;
18237
+ var cachedModeFailedAt = 0;
18238
+ var _modeRefreshInFlight = null;
18239
+ async function runEventConsumer(opts) {
18240
+ await getCachedMode(opts.tandemUrl, opts.logPrefix).catch(() => {
18241
+ });
18226
18242
  let retries = 0;
18227
18243
  let lastEventId;
18228
18244
  while (retries < CHANNEL_MAX_RETRIES) {
18229
18245
  try {
18230
- await connectAndStream(mcp, tandemUrl, lastEventId, (id) => {
18231
- lastEventId = id;
18232
- retries = 0;
18246
+ await connectAndStreamOnce(opts, lastEventId, {
18247
+ onEventId: (id) => {
18248
+ lastEventId = id;
18249
+ },
18250
+ onStable: () => {
18251
+ retries = 0;
18252
+ }
18233
18253
  });
18234
18254
  } catch (err) {
18235
18255
  retries++;
18236
18256
  console.error(
18237
- `[Channel] SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,
18257
+ `${opts.logPrefix} SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,
18238
18258
  err instanceof Error ? err.message : err
18239
18259
  );
18240
18260
  if (retries >= CHANNEL_MAX_RETRIES) {
18241
- console.error("[Channel] SSE connection exhausted, reporting error and exiting");
18261
+ console.error(`${opts.logPrefix} SSE connection exhausted, reporting error and exiting`);
18242
18262
  try {
18243
18263
  await fetchWithTimeout(
18244
- `${tandemUrl}${API_CHANNEL_ERROR}`,
18264
+ `${opts.tandemUrl}${API_CHANNEL_ERROR}`,
18245
18265
  {
18246
18266
  method: "POST",
18247
18267
  headers: { "Content-Type": "application/json" },
18248
18268
  body: JSON.stringify({
18249
- error: CHANNEL_CONNECT_FAILED,
18250
- message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`
18269
+ error: opts.errorCode,
18270
+ message: `${opts.logPrefix} lost connection after ${CHANNEL_MAX_RETRIES} retries.`
18251
18271
  })
18252
18272
  },
18253
18273
  CHANNEL_ERROR_REPORT_TIMEOUT_MS
18254
18274
  );
18255
18275
  } catch (reportErr) {
18256
18276
  console.error(
18257
- "[Channel] Could not report failure to server:",
18277
+ `${opts.logPrefix} Could not report failure to server:`,
18258
18278
  describeFetchError(reportErr, API_CHANNEL_ERROR, CHANNEL_ERROR_REPORT_TIMEOUT_MS)
18259
18279
  );
18260
18280
  }
18281
+ opts.onExhaustion?.();
18261
18282
  process.exit(1);
18262
18283
  }
18263
- await new Promise((r) => setTimeout(r, CHANNEL_RETRY_DELAY_MS));
18284
+ const delay = Math.min(CHANNEL_RETRY_DELAY_MS * 2 ** (retries - 1), RETRY_MAX_DELAY_MS);
18285
+ console.error(
18286
+ `${opts.logPrefix} Retrying in ${delay}ms (attempt ${retries}/${CHANNEL_MAX_RETRIES})...`
18287
+ );
18288
+ await new Promise((r) => setTimeout(r, delay));
18264
18289
  }
18265
18290
  }
18291
+ console.error(
18292
+ `${opts.logPrefix} Retry loop exited unexpectedly (retries=${retries}/${CHANNEL_MAX_RETRIES})`
18293
+ );
18294
+ process.exit(1);
18266
18295
  }
18267
- async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18296
+ async function connectAndStreamOnce(opts, lastEventId, cb) {
18297
+ const onStable = cb.onStable ?? (() => {
18298
+ });
18268
18299
  const headers = { Accept: "text/event-stream" };
18269
18300
  if (lastEventId) headers["Last-Event-ID"] = lastEventId;
18270
18301
  const connectCtrl = new AbortController();
@@ -18274,12 +18305,16 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18274
18305
  );
18275
18306
  let res;
18276
18307
  try {
18277
- res = await authFetch(`${tandemUrl}${API_EVENTS}`, { headers, signal: connectCtrl.signal });
18308
+ res = await authFetch(`${opts.tandemUrl}${API_EVENTS}`, {
18309
+ headers,
18310
+ signal: connectCtrl.signal
18311
+ });
18278
18312
  } finally {
18279
18313
  clearTimeout(connectTimer);
18280
18314
  }
18281
18315
  if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);
18282
18316
  if (!res.body) throw new Error("SSE endpoint returned no body");
18317
+ const stableTimer = setTimeout(onStable, STABLE_CONNECTION_MS);
18283
18318
  const reader = res.body.getReader();
18284
18319
  const decoder = new TextDecoder();
18285
18320
  let buffer = "";
@@ -18292,13 +18327,10 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18292
18327
  });
18293
18328
  }
18294
18329
  }, CHANNEL_SSE_INACTIVITY_TIMEOUT_MS / 4);
18295
- let awarenessTimer = null;
18296
- let clearAwarenessTimer = null;
18297
18330
  let pendingAwareness = null;
18298
- const AWARENESS_CLEAR_MS = 3e3;
18299
- function clearAwareness(documentId) {
18300
- fetchWithTimeout(
18301
- `${tandemUrl}${API_CHANNEL_AWARENESS}`,
18331
+ function clearAwarenessNow(documentId) {
18332
+ const p = fetchWithTimeout(
18333
+ `${opts.tandemUrl}${API_CHANNEL_AWARENESS}`,
18302
18334
  {
18303
18335
  method: "POST",
18304
18336
  headers: { "Content-Type": "application/json" },
@@ -18311,7 +18343,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18311
18343
  CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
18312
18344
  ).catch((err) => {
18313
18345
  console.error(
18314
- "[Channel] clearAwareness failed (non-fatal):",
18346
+ `${opts.logPrefix} Awareness clear failed:`,
18315
18347
  describeFetchError(
18316
18348
  err,
18317
18349
  `${API_CHANNEL_AWARENESS} clear`,
@@ -18319,13 +18351,15 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18319
18351
  )
18320
18352
  );
18321
18353
  });
18354
+ trackAwareness(p);
18322
18355
  }
18323
18356
  function flushAwareness() {
18324
18357
  if (!pendingAwareness) return;
18325
18358
  const event = pendingAwareness;
18326
18359
  pendingAwareness = null;
18327
- fetchWithTimeout(
18328
- `${tandemUrl}${API_CHANNEL_AWARENESS}`,
18360
+ if (event.documentId) shutdownTimers.lastDocumentId = event.documentId;
18361
+ const p = fetchWithTimeout(
18362
+ `${opts.tandemUrl}${API_CHANNEL_AWARENESS}`,
18329
18363
  {
18330
18364
  method: "POST",
18331
18365
  headers: { "Content-Type": "application/json" },
@@ -18338,7 +18372,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18338
18372
  CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
18339
18373
  ).catch((err) => {
18340
18374
  console.error(
18341
- "[Channel] Awareness update failed:",
18375
+ `${opts.logPrefix} Awareness update failed:`,
18342
18376
  describeFetchError(
18343
18377
  err,
18344
18378
  `${API_CHANNEL_AWARENESS} update`,
@@ -18346,13 +18380,17 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18346
18380
  )
18347
18381
  );
18348
18382
  });
18349
- if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
18350
- clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);
18383
+ trackAwareness(p);
18384
+ if (shutdownTimers.clearAwarenessTimer) clearTimeout(shutdownTimers.clearAwarenessTimer);
18385
+ shutdownTimers.clearAwarenessTimer = setTimeout(
18386
+ () => clearAwarenessNow(event.documentId),
18387
+ AWARENESS_CLEAR_MS
18388
+ );
18351
18389
  }
18352
18390
  function scheduleAwareness(event) {
18353
18391
  pendingAwareness = event;
18354
- if (awarenessTimer) clearTimeout(awarenessTimer);
18355
- awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
18392
+ if (shutdownTimers.awarenessTimer) clearTimeout(shutdownTimers.awarenessTimer);
18393
+ shutdownTimers.awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
18356
18394
  }
18357
18395
  try {
18358
18396
  while (true) {
@@ -18380,84 +18418,135 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18380
18418
  else if (line.startsWith("data: ")) data = line.slice(6);
18381
18419
  }
18382
18420
  if (!data) continue;
18383
- let event;
18421
+ let raw;
18384
18422
  try {
18385
- event = parseTandemEvent(JSON.parse(data));
18386
- } catch {
18423
+ raw = JSON.parse(data);
18424
+ } catch (err) {
18387
18425
  console.error(
18388
- "[Channel] Malformed SSE event data (skipping), eventId=%s:",
18389
- eventId,
18390
- data.slice(0, 200)
18426
+ `${opts.logPrefix} SSE JSON parse failed (eventId=${eventId ?? "none"}, len=${data.length}): ${err instanceof Error ? err.message : err}. Tail:`,
18427
+ data.slice(Math.max(0, data.length - 200))
18391
18428
  );
18392
- if (eventId) onEventId(eventId);
18429
+ if (eventId) cb.onEventId(eventId);
18393
18430
  continue;
18394
18431
  }
18432
+ const event = parseTandemEvent(raw);
18395
18433
  if (!event) {
18396
18434
  console.error(
18397
- "[Channel] Invalid SSE event structure (skipping), eventId=%s:",
18398
- eventId,
18399
- data.slice(0, 200)
18435
+ `${opts.logPrefix} SSE event failed validation (eventId=${eventId ?? "none"}): shape mismatch`
18400
18436
  );
18401
- if (eventId) onEventId(eventId);
18437
+ if (eventId) cb.onEventId(eventId);
18402
18438
  continue;
18403
18439
  }
18404
18440
  if (event.type !== "chat:message") {
18405
- const mode = await getCachedMode(tandemUrl);
18406
- if (mode === "solo") {
18407
- console.error(`[Channel] Solo mode: suppressed ${event.type} event`);
18408
- if (eventId) onEventId(eventId);
18441
+ refreshMode(opts.tandemUrl, opts.logPrefix);
18442
+ if (getModeSync() === "solo") {
18443
+ console.error(`${opts.logPrefix} Solo mode: suppressed ${event.type} event`);
18444
+ if (eventId) cb.onEventId(eventId);
18409
18445
  continue;
18410
18446
  }
18411
18447
  }
18412
18448
  try {
18413
- await mcp.notification({
18414
- method: "notifications/claude/channel",
18415
- params: {
18416
- content: formatEventContent(event),
18417
- meta: formatEventMeta(event)
18418
- }
18419
- });
18449
+ await opts.onEvent(event, eventId);
18420
18450
  } catch (err) {
18421
- console.error("[Channel] MCP notification failed (transport broken?):", err);
18451
+ console.error(`${opts.logPrefix} onEvent failed (transport broken?):`, err);
18422
18452
  throw err;
18423
18453
  }
18424
- if (eventId) onEventId(eventId);
18454
+ if (eventId) cb.onEventId(eventId);
18425
18455
  scheduleAwareness(event);
18426
18456
  }
18427
18457
  }
18428
18458
  } finally {
18459
+ clearTimeout(stableTimer);
18429
18460
  clearInterval(watchdog);
18430
- if (awarenessTimer) clearTimeout(awarenessTimer);
18431
- if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
18461
+ if (shutdownTimers.awarenessTimer) clearTimeout(shutdownTimers.awarenessTimer);
18462
+ if (shutdownTimers.clearAwarenessTimer) clearTimeout(shutdownTimers.clearAwarenessTimer);
18463
+ shutdownTimers.awarenessTimer = null;
18464
+ shutdownTimers.clearAwarenessTimer = null;
18465
+ pendingAwareness = null;
18432
18466
  }
18433
18467
  }
18434
- var cachedMode = "tandem";
18435
- var cachedModeAt = 0;
18436
- async function getCachedMode(tandemUrl) {
18437
- const now = Date.now();
18438
- if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
18468
+ async function fetchMode(tandemUrl) {
18439
18469
  try {
18440
18470
  const res = await fetchWithTimeout(
18441
18471
  `${tandemUrl}${API_MODE}`,
18442
18472
  {},
18443
18473
  CHANNEL_MODE_FETCH_TIMEOUT_MS
18444
18474
  );
18445
- if (res.ok) {
18446
- const { mode } = await res.json();
18447
- cachedMode = mode;
18448
- } else {
18449
- console.error(`[Channel] Mode check returned ${res.status}, using cached: "${cachedMode}"`);
18450
- }
18451
- cachedModeAt = now;
18475
+ if (!res.ok) return { ok: false, reason: `status ${res.status}` };
18476
+ const body = await res.json();
18477
+ const parsed = TandemModeSchema.safeParse(body.mode);
18478
+ if (!parsed.success) return { ok: false, reason: `invalid mode ${JSON.stringify(body.mode)}` };
18479
+ return { ok: true, mode: parsed.data };
18452
18480
  } catch (err) {
18481
+ return { ok: false, reason: describeFetchError(err, API_MODE, CHANNEL_MODE_FETCH_TIMEOUT_MS) };
18482
+ }
18483
+ }
18484
+ async function getCachedMode(tandemUrl, logPrefix = "[Tandem]") {
18485
+ const now = Date.now();
18486
+ if (now - cachedModeAt < MODE_CACHE_TTL_MS && cachedModeAt !== 0) return cachedMode;
18487
+ const result = await fetchMode(tandemUrl);
18488
+ if (!result.ok) {
18489
+ if (cachedModeAt !== 0) {
18490
+ console.error(
18491
+ `${logPrefix} Mode check failed (${result.reason}), preserving last known mode '${cachedMode}'`
18492
+ );
18493
+ return cachedMode;
18494
+ }
18453
18495
  console.error(
18454
- "[Channel] Mode check failed, delivering event (fail-open):",
18455
- describeFetchError(err, API_MODE, CHANNEL_MODE_FETCH_TIMEOUT_MS)
18496
+ `${logPrefix} Mode check failed (${result.reason}), no prior mode \u2014 using cold-start default '${TANDEM_MODE_DEFAULT}'`
18456
18497
  );
18457
- cachedModeAt = now;
18498
+ cachedMode = TANDEM_MODE_DEFAULT;
18499
+ return TANDEM_MODE_DEFAULT;
18458
18500
  }
18501
+ cachedMode = result.mode;
18502
+ cachedModeAt = now;
18503
+ return cachedMode;
18504
+ }
18505
+ function getModeSync() {
18459
18506
  return cachedMode;
18460
18507
  }
18508
+ function refreshMode(tandemUrl, logPrefix) {
18509
+ if (_modeRefreshInFlight) return;
18510
+ const now = Date.now();
18511
+ if (now - cachedModeAt < MODE_CACHE_TTL_MS) return;
18512
+ if (now - cachedModeFailedAt < MODE_CACHE_TTL_MS) return;
18513
+ _modeRefreshInFlight = (async () => {
18514
+ try {
18515
+ const result = await fetchMode(tandemUrl);
18516
+ if (result.ok) {
18517
+ cachedMode = result.mode;
18518
+ cachedModeAt = Date.now();
18519
+ cachedModeFailedAt = 0;
18520
+ } else {
18521
+ cachedModeFailedAt = Date.now();
18522
+ console.error(
18523
+ `${logPrefix} Background mode refresh failed (${result.reason}), keeping cached`
18524
+ );
18525
+ }
18526
+ } finally {
18527
+ _modeRefreshInFlight = null;
18528
+ }
18529
+ })().catch((err) => {
18530
+ console.error(`${logPrefix} refreshMode unexpected error:`, err);
18531
+ cachedModeFailedAt = Date.now();
18532
+ });
18533
+ }
18534
+
18535
+ // src/channel/event-bridge.ts
18536
+ async function startEventBridge(mcp, tandemUrl) {
18537
+ return runEventConsumer({
18538
+ tandemUrl,
18539
+ logPrefix: "[Channel]",
18540
+ errorCode: CHANNEL_CONNECT_FAILED,
18541
+ onEvent: (event) => mcp.notification({
18542
+ method: "notifications/claude/channel",
18543
+ params: {
18544
+ content: formatEventContent(event),
18545
+ meta: formatEventMeta(event)
18546
+ }
18547
+ })
18548
+ });
18549
+ }
18461
18550
 
18462
18551
  // src/channel/run.ts
18463
18552
  async function runChannel(opts = {}) {