tmux-ide 2.9.16 → 2.9.17

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/bin/cli.js CHANGED
@@ -12403,7 +12403,10 @@ __export(tui_binary_exports, {
12403
12403
  MAX_TUI_COMPRESSED_BYTES: () => MAX_TUI_COMPRESSED_BYTES,
12404
12404
  MIN_TUI_BINARY_BYTES: () => MIN_TUI_BINARY_BYTES,
12405
12405
  RELEASE_REPO: () => RELEASE_REPO,
12406
- TUI_DOWNLOAD_TIMEOUT_MS: () => TUI_DOWNLOAD_TIMEOUT_MS,
12406
+ TUI_DOWNLOAD_MAX_ATTEMPTS: () => TUI_DOWNLOAD_MAX_ATTEMPTS,
12407
+ TUI_DOWNLOAD_RETRY_DELAY_MS: () => TUI_DOWNLOAD_RETRY_DELAY_MS,
12408
+ TUI_DOWNLOAD_STALL_TIMEOUT_MS: () => TUI_DOWNLOAD_STALL_TIMEOUT_MS,
12409
+ TUI_DOWNLOAD_TOTAL_TIMEOUT_MS: () => TUI_DOWNLOAD_TOTAL_TIMEOUT_MS,
12407
12410
  bunTargetForTag: () => bunTargetForTag,
12408
12411
  downloadTuiBinary: () => downloadTuiBinary,
12409
12412
  downloadedTuiPath: () => downloadedTuiPath,
@@ -12548,30 +12551,54 @@ function purgeCachedBinary(path2) {
12548
12551
  }
12549
12552
  }
12550
12553
  }
12551
- async function readBoundedResponse(fetchImpl, url, maxBytes, timeoutMs) {
12554
+ function isTransientHttpStatus(status2) {
12555
+ return status2 === 408 || status2 === 429 || status2 >= 500;
12556
+ }
12557
+ async function readBoundedResponse(fetchImpl, url, maxBytes, policy) {
12552
12558
  const controller = new AbortController();
12553
- const timer = setTimeout(() => controller.abort(), timeoutMs);
12559
+ let total = 0;
12560
+ let timeoutMessage = null;
12561
+ const abortWith = (message) => () => {
12562
+ timeoutMessage = message();
12563
+ controller.abort();
12564
+ };
12565
+ const overallTimer = setTimeout(
12566
+ abortWith(() => `download did not finish within ${policy.totalTimeoutMs}ms`),
12567
+ Math.max(0, policy.deadline - Date.now())
12568
+ );
12569
+ const onStall = abortWith(
12570
+ () => `download stalled: no data for ${policy.stallTimeoutMs}ms after ${total} bytes`
12571
+ );
12572
+ let stallTimer = setTimeout(onStall, policy.stallTimeoutMs);
12573
+ const markProgress = () => {
12574
+ clearTimeout(stallTimer);
12575
+ stallTimer = setTimeout(onStall, policy.stallTimeoutMs);
12576
+ };
12554
12577
  try {
12555
12578
  const response3 = await fetchImpl(url, { signal: controller.signal });
12556
12579
  if (!response3.ok) {
12557
- throw new Error(`HTTP ${response3.status} ${response3.statusText}`);
12580
+ const message = `HTTP ${response3.status} ${response3.statusText}`;
12581
+ if (isTransientHttpStatus(response3.status)) throw new Error(message);
12582
+ throw new PermanentDownloadError(message);
12558
12583
  }
12584
+ markProgress();
12559
12585
  const declaredLength = Number(response3.headers.get("content-length"));
12560
12586
  if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
12561
- throw new Error(`declared size ${declaredLength} exceeds ${maxBytes} bytes`);
12587
+ throw new PermanentDownloadError(`declared size ${declaredLength} exceeds ${maxBytes} bytes`);
12562
12588
  }
12563
12589
  if (!response3.body) throw new Error("response has no body");
12564
12590
  const chunks = [];
12565
- let total = 0;
12566
12591
  const reader = response3.body.getReader();
12567
12592
  try {
12568
12593
  while (true) {
12569
12594
  const { done, value } = await reader.read();
12570
12595
  if (done) break;
12596
+ if (value.byteLength === 0) continue;
12597
+ markProgress();
12571
12598
  total += value.byteLength;
12572
12599
  if (total > maxBytes) {
12573
12600
  await reader.cancel();
12574
- throw new Error(`download exceeds ${maxBytes} bytes`);
12601
+ throw new PermanentDownloadError(`download exceeds ${maxBytes} bytes`);
12575
12602
  }
12576
12603
  chunks.push(Buffer.from(value));
12577
12604
  }
@@ -12580,12 +12607,27 @@ async function readBoundedResponse(fetchImpl, url, maxBytes, timeoutMs) {
12580
12607
  }
12581
12608
  return Buffer.concat(chunks, total);
12582
12609
  } catch (error) {
12583
- if (controller.signal.aborted) {
12584
- throw new Error(`download timed out after ${timeoutMs}ms`, { cause: error });
12585
- }
12610
+ if (timeoutMessage) throw new Error(timeoutMessage, { cause: error });
12586
12611
  throw error;
12587
12612
  } finally {
12588
- clearTimeout(timer);
12613
+ clearTimeout(overallTimer);
12614
+ clearTimeout(stallTimer);
12615
+ controller.abort();
12616
+ }
12617
+ }
12618
+ async function readWithRetries(fetchImpl, url, maxBytes, policy, log) {
12619
+ for (let attempt = 1; ; attempt++) {
12620
+ try {
12621
+ return await readBoundedResponse(fetchImpl, url, maxBytes, policy);
12622
+ } catch (error) {
12623
+ const delayMs = policy.retryDelayMs * attempt;
12624
+ if (error instanceof PermanentDownloadError || attempt >= policy.maxAttempts || Date.now() + delayMs >= policy.deadline) {
12625
+ throw error;
12626
+ }
12627
+ const message = error instanceof Error ? error.message : String(error);
12628
+ log(`download attempt ${attempt}/${policy.maxAttempts} failed (${message}); retrying`);
12629
+ await new Promise((resolve45) => setTimeout(resolve45, delayMs));
12630
+ }
12589
12631
  }
12590
12632
  }
12591
12633
  async function downloadTuiBinary(opts = {}) {
@@ -12606,14 +12648,18 @@ async function downloadTuiBinary(opts = {}) {
12606
12648
  const limits = {
12607
12649
  minBinaryBytes: opts.limits?.minBinaryBytes ?? MIN_TUI_BINARY_BYTES,
12608
12650
  maxCompressedBytes: opts.limits?.maxCompressedBytes ?? MAX_TUI_COMPRESSED_BYTES,
12609
- maxBinaryBytes: opts.limits?.maxBinaryBytes ?? MAX_TUI_BINARY_BYTES,
12610
- timeoutMs: opts.timeoutMs ?? TUI_DOWNLOAD_TIMEOUT_MS
12651
+ maxBinaryBytes: opts.limits?.maxBinaryBytes ?? MAX_TUI_BINARY_BYTES
12611
12652
  };
12653
+ const totalTimeoutMs = opts.totalTimeoutMs ?? TUI_DOWNLOAD_TOTAL_TIMEOUT_MS;
12612
12654
  mkdirSync5(dirname9(dest), { recursive: true });
12655
+ const lockNotice = setTimeout(
12656
+ () => log("waiting for another tmux-ide download of this version to finish"),
12657
+ TUI_DOWNLOAD_LOCK_NOTICE_MS
12658
+ );
12613
12659
  const releaseLock = await acquirePrivateOperationLock(
12614
12660
  downloadLockPath(dest),
12615
- limits.timeoutMs * 2 + 5e3
12616
- );
12661
+ totalTimeoutMs + TUI_DOWNLOAD_LOCK_MARGIN_MS
12662
+ ).finally(() => clearTimeout(lockNotice));
12617
12663
  const suffix = `${process.pid}.${randomUUID2()}.tmp`;
12618
12664
  const binaryTmp = `${dest}.${suffix}`;
12619
12665
  const checksumTmp = `${checksumPath(dest)}.${suffix}`;
@@ -12622,21 +12668,31 @@ async function downloadTuiBinary(opts = {}) {
12622
12668
  if (cached2) return { path: dest, bytes: cached2.bytes };
12623
12669
  purgeCachedBinary(dest);
12624
12670
  const fetchImpl = opts.fetch ?? globalThis.fetch;
12671
+ const policy = {
12672
+ stallTimeoutMs: opts.stallTimeoutMs ?? TUI_DOWNLOAD_STALL_TIMEOUT_MS,
12673
+ deadline: Date.now() + totalTimeoutMs,
12674
+ totalTimeoutMs,
12675
+ maxAttempts: opts.maxAttempts ?? TUI_DOWNLOAD_MAX_ATTEMPTS,
12676
+ retryDelayMs: opts.retryDelayMs ?? TUI_DOWNLOAD_RETRY_DELAY_MS
12677
+ };
12625
12678
  log(`downloading checksum manifest ${checksumUrl}`);
12626
12679
  let manifestBytes;
12627
12680
  let gz;
12628
12681
  try {
12629
- manifestBytes = await readBoundedResponse(
12682
+ manifestBytes = await readWithRetries(
12630
12683
  fetchImpl,
12631
12684
  checksumUrl,
12632
12685
  MAX_CHECKSUM_MANIFEST_BYTES,
12633
- limits.timeoutMs
12686
+ policy,
12687
+ log
12634
12688
  );
12635
- gz = await readBoundedResponse(fetchImpl, url, limits.maxCompressedBytes, limits.timeoutMs);
12689
+ log(`downloading ${url}`);
12690
+ gz = await readWithRetries(fetchImpl, url, limits.maxCompressedBytes, policy, log);
12636
12691
  } catch (error) {
12637
12692
  const message = error instanceof Error ? error.message : String(error);
12693
+ const hint = error instanceof PermanentDownloadError ? "Check that the release published both exact-version assets." : "Check your network connection, then retry with `tmux-ide update --tui-binary`, or run the installer again if tmux-ide is not installed yet.";
12638
12694
  throw new Error(
12639
- `could not download the TUI binary and checksum for v${version} (${message}). Check that the release published both exact-version assets.`,
12695
+ `could not download the TUI binary and checksum for v${version} (${message}). ${hint}`,
12640
12696
  { cause: error }
12641
12697
  );
12642
12698
  }
@@ -12681,7 +12737,7 @@ async function downloadTuiBinary(opts = {}) {
12681
12737
  releaseLock();
12682
12738
  }
12683
12739
  }
12684
- var RELEASE_REPO, MIN_TUI_BINARY_BYTES, MAX_TUI_COMPRESSED_BYTES, MAX_TUI_BINARY_BYTES, TUI_DOWNLOAD_TIMEOUT_MS, MAX_CHECKSUM_MANIFEST_BYTES, SUPPORTED;
12740
+ var RELEASE_REPO, MIN_TUI_BINARY_BYTES, MAX_TUI_COMPRESSED_BYTES, MAX_TUI_BINARY_BYTES, TUI_DOWNLOAD_STALL_TIMEOUT_MS, TUI_DOWNLOAD_TOTAL_TIMEOUT_MS, TUI_DOWNLOAD_MAX_ATTEMPTS, TUI_DOWNLOAD_RETRY_DELAY_MS, MAX_CHECKSUM_MANIFEST_BYTES, TUI_DOWNLOAD_LOCK_NOTICE_MS, TUI_DOWNLOAD_LOCK_MARGIN_MS, SUPPORTED, PermanentDownloadError;
12685
12741
  var init_tui_binary = __esm({
12686
12742
  "packages/daemon/src/lib/tui-binary.ts"() {
12687
12743
  "use strict";
@@ -12692,13 +12748,20 @@ var init_tui_binary = __esm({
12692
12748
  MIN_TUI_BINARY_BYTES = 10 * 1024 * 1024;
12693
12749
  MAX_TUI_COMPRESSED_BYTES = 128 * 1024 * 1024;
12694
12750
  MAX_TUI_BINARY_BYTES = 256 * 1024 * 1024;
12695
- TUI_DOWNLOAD_TIMEOUT_MS = 3e4;
12751
+ TUI_DOWNLOAD_STALL_TIMEOUT_MS = 3e4;
12752
+ TUI_DOWNLOAD_TOTAL_TIMEOUT_MS = 15 * 6e4;
12753
+ TUI_DOWNLOAD_MAX_ATTEMPTS = 3;
12754
+ TUI_DOWNLOAD_RETRY_DELAY_MS = 1e3;
12696
12755
  MAX_CHECKSUM_MANIFEST_BYTES = 4 * 1024;
12756
+ TUI_DOWNLOAD_LOCK_NOTICE_MS = 2e3;
12757
+ TUI_DOWNLOAD_LOCK_MARGIN_MS = 5e3;
12697
12758
  SUPPORTED = {
12698
12759
  "darwin-arm64": "darwin-arm64",
12699
12760
  "linux-x64": "linux-x64",
12700
12761
  "linux-arm64": "linux-arm64"
12701
12762
  };
12763
+ PermanentDownloadError = class extends Error {
12764
+ };
12702
12765
  }
12703
12766
  });
12704
12767
 
@@ -13082,7 +13145,7 @@ var require_package = __commonJS({
13082
13145
  "package.json"(exports, module) {
13083
13146
  module.exports = {
13084
13147
  name: "tmux-ide",
13085
- version: "2.9.16",
13148
+ version: "2.9.17",
13086
13149
  description: "The open-source workspace for coding agents: run Claude Code, Codex and other agents in tmux with live status",
13087
13150
  type: "module",
13088
13151
  bin: {
@@ -27074,10 +27137,13 @@ function isVisibleFleetSession(name) {
27074
27137
  function readAdoptedSessionNames(runTmux2 = _tmuxRunner) {
27075
27138
  let raw;
27076
27139
  try {
27077
- raw = runTmux2(["list-sessions", "-F", `#{session_name} #{${ADOPTED_OPTION}}`]);
27140
+ raw = runTmux2(ADOPTED_SESSIONS_ARGS);
27078
27141
  } catch {
27079
27142
  return null;
27080
27143
  }
27144
+ return parseAdoptedSessionNames(raw);
27145
+ }
27146
+ function parseAdoptedSessionNames(raw) {
27081
27147
  const names = [];
27082
27148
  for (const line of raw.split("\n")) {
27083
27149
  if (!line) continue;
@@ -27096,16 +27162,51 @@ function readAdoptedFleet(registry = getDefaultWorkspaceRegistry(), runTmux2 = _
27096
27162
  const nativeEpoch = nativeServerEpoch?.() ?? null;
27097
27163
  const adopted = readAdoptedSessionNames(runTmux2);
27098
27164
  if (adopted === null) return null;
27099
- const adoptedSet = new Set(adopted);
27100
- if (adoptedSet.size === 0) return [];
27101
- const appCreatedSessions = new Set(registry.list().map((workspace) => workspace.sessionName));
27102
- const panesBySession = /* @__PURE__ */ new Map();
27165
+ if (adopted.length === 0) return [];
27103
27166
  let panesRaw;
27104
27167
  try {
27105
- panesRaw = runTmux2(["list-panes", "-a", "-F", FLEET_PANE_FORMAT]);
27168
+ panesRaw = runTmux2(FLEET_PANES_ARGS);
27106
27169
  } catch {
27107
27170
  panesRaw = "";
27108
27171
  }
27172
+ return assembleAdoptedFleet(
27173
+ adopted,
27174
+ panesRaw,
27175
+ registry,
27176
+ nativeEpoch,
27177
+ resolveInteractionEndpoint,
27178
+ nativeServerEpoch
27179
+ );
27180
+ }
27181
+ async function readAdoptedFleetAsync(registry, runTmux2, resolveInteractionEndpoint, nativeServerEpoch) {
27182
+ const nativeEpoch = nativeServerEpoch?.() ?? null;
27183
+ let sessionsRaw;
27184
+ try {
27185
+ sessionsRaw = await runTmux2(ADOPTED_SESSIONS_ARGS);
27186
+ } catch {
27187
+ return null;
27188
+ }
27189
+ const adopted = parseAdoptedSessionNames(sessionsRaw);
27190
+ if (adopted.length === 0) return [];
27191
+ let panesRaw;
27192
+ try {
27193
+ panesRaw = await runTmux2(FLEET_PANES_ARGS);
27194
+ } catch {
27195
+ panesRaw = "";
27196
+ }
27197
+ return assembleAdoptedFleet(
27198
+ adopted,
27199
+ panesRaw,
27200
+ registry,
27201
+ nativeEpoch,
27202
+ resolveInteractionEndpoint,
27203
+ nativeServerEpoch
27204
+ );
27205
+ }
27206
+ function assembleAdoptedFleet(adopted, panesRaw, registry, nativeEpoch, resolveInteractionEndpoint, nativeServerEpoch) {
27207
+ const adoptedSet = new Set(adopted);
27208
+ const appCreatedSessions = new Set(registry.list().map((workspace) => workspace.sessionName));
27209
+ const panesBySession = /* @__PURE__ */ new Map();
27109
27210
  for (const line of panesRaw.split("\n")) {
27110
27211
  if (!line) continue;
27111
27212
  const fields = line.split(FLEET_FIELD_SEPARATOR);
@@ -27193,7 +27294,7 @@ function buildProjectDetail(info) {
27193
27294
  panes: info.panes.map(({ semanticPaneId: _semanticPaneId, ...pane }) => pane)
27194
27295
  };
27195
27296
  }
27196
- var _tmuxRunner, FLEET_FIELD_SEPARATOR, FLEET_LINE_SENTINEL, FLEET_PANE_FORMAT;
27297
+ var _tmuxRunner, ADOPTED_SESSIONS_ARGS, FLEET_FIELD_SEPARATOR, FLEET_LINE_SENTINEL, FLEET_PANE_FORMAT, FLEET_PANES_ARGS;
27197
27298
  var init_discovery = __esm({
27198
27299
  "packages/daemon/src/command-center/discovery.ts"() {
27199
27300
  "use strict";
@@ -27210,6 +27311,7 @@ var init_discovery = __esm({
27210
27311
  maxBuffer: 1024 * 1024,
27211
27312
  stdio: ["ignore", "pipe", "ignore"]
27212
27313
  }).trim();
27314
+ ADOPTED_SESSIONS_ARGS = ["list-sessions", "-F", `#{session_name} #{${ADOPTED_OPTION}}`];
27213
27315
  FLEET_FIELD_SEPARATOR = "|tmux-ide-fleet-field-v1|";
27214
27316
  FLEET_LINE_SENTINEL = "tmux-ide-fleet-v1";
27215
27317
  FLEET_PANE_FORMAT = [
@@ -27231,6 +27333,7 @@ var init_discovery = __esm({
27231
27333
  "#{@tmux_ide_team}",
27232
27334
  FLEET_LINE_SENTINEL
27233
27335
  ].join(FLEET_FIELD_SEPARATOR);
27336
+ FLEET_PANES_ARGS = ["list-panes", "-a", "-F", FLEET_PANE_FORMAT];
27234
27337
  }
27235
27338
  });
27236
27339
 
@@ -43804,11 +43907,14 @@ function initialize() {
43804
43907
  }
43805
43908
  return native;
43806
43909
  }
43910
+ function nativeFnv64() {
43911
+ return initialize();
43912
+ }
43807
43913
  function createBufferedFnv64(initialState = OFFSET_BASIS) {
43808
43914
  const hasher = initialize();
43809
43915
  return hasher ? new BufferedHasher(hasher, initialState) : null;
43810
43916
  }
43811
- var SCRATCH_BYTES, OFFSET_BASIS, initialized, native, BufferedHasher;
43917
+ var SCRATCH_BYTES, OFFSET_BASIS, initialized, native, FNV64_OFFSET_BASIS, BufferedHasher;
43812
43918
  var init_terminal_fnv64_wasm = __esm({
43813
43919
  "packages/core/src/terminal-fnv64-wasm.ts"() {
43814
43920
  "use strict";
@@ -43817,6 +43923,7 @@ var init_terminal_fnv64_wasm = __esm({
43817
43923
  OFFSET_BASIS = 0xcbf29ce484222325n;
43818
43924
  initialized = false;
43819
43925
  native = null;
43926
+ FNV64_OFFSET_BASIS = OFFSET_BASIS;
43820
43927
  BufferedHasher = class {
43821
43928
  #native;
43822
43929
  #buffer = new Uint8Array(SCRATCH_BYTES);
@@ -43954,6 +44061,12 @@ function hashTerminalReplicaRowCached(row, onMiss, encodingCache) {
43954
44061
  const cached2 = ROW_HASH_CACHE.get(row);
43955
44062
  if (cached2) return cached2;
43956
44063
  onMiss?.();
44064
+ const native2 = nativeFnv64();
44065
+ if (native2) {
44066
+ const digest4 = hashRowBytes(row, native2);
44067
+ if (isTerminalReplicaRowDeeplyFrozen(row)) ROW_HASH_CACHE.set(row, digest4);
44068
+ return digest4;
44069
+ }
43957
44070
  const hash = encodingCache?.createHash() ?? new CanonicalFnv64();
43958
44071
  let canonicalBytes = 10 + String(row.cells.length).length;
43959
44072
  hash.ascii("a2:");
@@ -44017,6 +44130,10 @@ async function hashTerminalReplicaRowCooperatively(row, yieldControl, bytesPerSl
44017
44130
  }
44018
44131
  function isTerminalReplicaRowDeeplyFrozen(row) {
44019
44132
  if (DEEPLY_FROZEN_ROWS.has(row)) return true;
44133
+ if (isOwnedTerminalReplicaRow(row)) {
44134
+ DEEPLY_FROZEN_ROWS.add(row);
44135
+ return true;
44136
+ }
44020
44137
  if (!Object.isFrozen(row) || !Object.isFrozen(row.cells)) return false;
44021
44138
  for (const cell of row.cells) {
44022
44139
  if (!Object.isFrozen(cell) || !Object.isFrozen(cell.foreground) || !Object.isFrozen(cell.background))
@@ -44025,7 +44142,74 @@ function isTerminalReplicaRowDeeplyFrozen(row) {
44025
44142
  DEEPLY_FROZEN_ROWS.add(row);
44026
44143
  return true;
44027
44144
  }
44028
- var ROW_HASH_CACHE, DEEPLY_FROZEN_ROWS, UTF8_ENCODER, SHORT_ASCII_HEADERS, DEFAULT_COLOR_KEYS, DEFAULT_COLOR_KEY_ORDER, CanonicalFnv64, compactColorKey, TerminalReplicaRunEncodingCache, DECODED_DEFAULT_COLOR;
44145
+ function canonicalNumber(value) {
44146
+ const text2 = String(value);
44147
+ return `d${text2.length}:${text2};`;
44148
+ }
44149
+ function canonicalColor(color3) {
44150
+ if (color3.kind === "default") return "o1:s4:kind;s7:default;;";
44151
+ if (color3.kind === "indexed")
44152
+ return `o2:s5:index;${canonicalNumber(color3.index)}s4:kind;s7:indexed;;`;
44153
+ return `o2:s4:kind;s3:rgb;s5:value;${canonicalNumber(color3.value)};`;
44154
+ }
44155
+ function encodeCell(cell) {
44156
+ const grapheme = cell.grapheme;
44157
+ let length = grapheme.length;
44158
+ for (let index = 0; index < grapheme.length; index += 1)
44159
+ if (grapheme.charCodeAt(index) > 127) {
44160
+ length = UTF8_ENCODER.encode(grapheme).byteLength;
44161
+ break;
44162
+ }
44163
+ return UTF8_ENCODER.encode(
44164
+ `a5:s${length}:${grapheme};${canonicalNumber(cell.width)}${canonicalColor(
44165
+ cell.foreground
44166
+ )}${canonicalColor(cell.background)}${canonicalNumber(cell.attributes)};`
44167
+ );
44168
+ }
44169
+ function remember(cache3, key2, bytes) {
44170
+ if (cache3.size >= CELL_BYTES_LIMIT) cache3.clear();
44171
+ cache3.set(key2, bytes);
44172
+ return bytes;
44173
+ }
44174
+ function cellBytes(cell) {
44175
+ const { grapheme, width, attributes, foreground, background } = cell;
44176
+ if (width === 1 && attributes === 0 && foreground.kind === "default" && background.kind === "default")
44177
+ return PLAIN_CELL_BYTES.get(grapheme) ?? remember(PLAIN_CELL_BYTES, grapheme, encodeCell(cell));
44178
+ const key2 = `${width}\0${attributes}\0${canonicalColor(foreground)}\0${canonicalColor(
44179
+ background
44180
+ )}\0${grapheme}`;
44181
+ return STYLED_CELL_BYTES.get(key2) ?? remember(STYLED_CELL_BYTES, key2, encodeCell(cell));
44182
+ }
44183
+ function hashRowBytes(row, native2) {
44184
+ const scratch = native2.scratch;
44185
+ const capacity = scratch.byteLength;
44186
+ let state = FNV64_OFFSET_BASIS;
44187
+ let length = 0;
44188
+ const write2 = (bytes) => {
44189
+ if (bytes.byteLength <= capacity - length) {
44190
+ scratch.set(bytes, length);
44191
+ length += bytes.byteLength;
44192
+ return;
44193
+ }
44194
+ let offset = 0;
44195
+ while (offset < bytes.byteLength) {
44196
+ const count = Math.min(capacity - length, bytes.byteLength - offset);
44197
+ scratch.set(bytes.subarray(offset, offset + count), length);
44198
+ length += count;
44199
+ offset += count;
44200
+ if (length === capacity) {
44201
+ state = BigInt.asUintN(64, native2.update(state, length));
44202
+ length = 0;
44203
+ }
44204
+ }
44205
+ };
44206
+ write2(ASCII_ENCODER.encode(`a2:${row.wrapped ? "b1;" : "b0;"}a${row.cells.length}:`));
44207
+ for (const cell of row.cells) write2(cellBytes(cell));
44208
+ write2(ROW_TRAILER);
44209
+ state = BigInt.asUintN(64, native2.update(state, length));
44210
+ return state.toString(16).padStart(16, "0");
44211
+ }
44212
+ var ROW_HASH_CACHE, DEEPLY_FROZEN_ROWS, UTF8_ENCODER, SHORT_ASCII_HEADERS, DEFAULT_COLOR_KEYS, DEFAULT_COLOR_KEY_ORDER, CanonicalFnv64, compactColorKey, TerminalReplicaRunEncodingCache, DECODED_DEFAULT_COLOR, ASCII_ENCODER, CELL_BYTES_LIMIT, PLAIN_CELL_BYTES, STYLED_CELL_BYTES, ROW_TRAILER;
44029
44213
  var init_terminal_replica_hash_cache = __esm({
44030
44214
  "packages/core/src/terminal-replica-hash-cache.ts"() {
44031
44215
  "use strict";
@@ -44272,6 +44456,11 @@ var init_terminal_replica_hash_cache = __esm({
44272
44456
  }
44273
44457
  };
44274
44458
  DECODED_DEFAULT_COLOR = Object.freeze({ kind: "default" });
44459
+ ASCII_ENCODER = new TextEncoder();
44460
+ CELL_BYTES_LIMIT = 4096;
44461
+ PLAIN_CELL_BYTES = /* @__PURE__ */ new Map();
44462
+ STYLED_CELL_BYTES = /* @__PURE__ */ new Map();
44463
+ ROW_TRAILER = ASCII_ENCODER.encode(";;");
44275
44464
  }
44276
44465
  });
44277
44466
 
@@ -44891,6 +45080,16 @@ function resolveFrameHash(update, options, profile) {
44891
45080
  return hash;
44892
45081
  }
44893
45082
  function terminalReplicaFrameIdentity(update) {
45083
+ if (update.type === "terminal.patch") {
45084
+ const { rows, history: history2, historyDelta, ...rest2 } = update.patch;
45085
+ return [
45086
+ "terminal-replica-patch-frame-v2",
45087
+ { ...update, patch: rest2 },
45088
+ rows.map(({ index, row }) => [index, hashTerminalReplicaRow(row)]),
45089
+ history2 === void 0 ? null : hashTerminalReplicaRows(history2),
45090
+ historyDelta === void 0 ? null : [historyDelta.trim, hashTerminalReplicaRows(historyDelta.append)]
45091
+ ];
45092
+ }
44894
45093
  if (update.type !== "terminal.seed") return update;
44895
45094
  const { grid, history, ...rest } = update.snapshot;
44896
45095
  return [
@@ -76428,6 +76627,8 @@ var init_tmux_server_owners = __esm({
76428
76627
  #tail = Promise.resolve();
76429
76628
  #disposed = false;
76430
76629
  #disposePromise = null;
76630
+ #queuedRefresh = null;
76631
+ #queuedRefreshTail = null;
76431
76632
  #serial(work) {
76432
76633
  const run = this.#tail.then(() => {
76433
76634
  if (this.#disposed) throw new TmuxServerScopeError("disposed");
@@ -76528,10 +76729,19 @@ var init_tmux_server_owners = __esm({
76528
76729
  this.#entries.set(scope2.serverId, { registration, scope: scope2, observation: observation2, owner });
76529
76730
  }
76530
76731
  refresh() {
76531
- return this.#serial(async () => {
76732
+ if (this.#queuedRefresh && this.#tail === this.#queuedRefreshTail) return this.#queuedRefresh;
76733
+ const queued = this.#serial(async () => {
76734
+ if (this.#queuedRefresh === queued) this.#queuedRefresh = null;
76532
76735
  for (const entry of this.#entries.values()) await this.#refresh(entry);
76533
76736
  return this.list();
76534
76737
  });
76738
+ this.#queuedRefresh = queued;
76739
+ this.#queuedRefreshTail = this.#tail;
76740
+ const settle = () => {
76741
+ if (this.#queuedRefresh === queued) this.#queuedRefresh = null;
76742
+ };
76743
+ queued.then(settle, settle);
76744
+ return queued;
76535
76745
  }
76536
76746
  current(scopeInput) {
76537
76747
  const scope2 = TmuxServerScopeSchemaZ.parse(scopeInput);
@@ -80687,6 +80897,22 @@ var init_daemon_shutdown = __esm({
80687
80897
  }
80688
80898
  });
80689
80899
 
80900
+ // packages/daemon/src/lib/catalog-fleet-read.ts
80901
+ async function readCatalogFleet(registry, runners, resolveInteractionEndpoint, nativeServerEpoch) {
80902
+ return runners.fence.active ? readAdoptedFleet(registry, runners.fencedSync, resolveInteractionEndpoint, nativeServerEpoch) : readAdoptedFleetAsync(
80903
+ registry,
80904
+ runners.generationFencedAsync,
80905
+ resolveInteractionEndpoint,
80906
+ nativeServerEpoch
80907
+ );
80908
+ }
80909
+ var init_catalog_fleet_read = __esm({
80910
+ "packages/daemon/src/lib/catalog-fleet-read.ts"() {
80911
+ "use strict";
80912
+ init_discovery();
80913
+ }
80914
+ });
80915
+
80690
80916
  // packages/daemon/src/lib/workspace-open.ts
80691
80917
  import { createHash as createHash25 } from "node:crypto";
80692
80918
  import { realpathSync as realpathSync19, statSync as statSync17 } from "node:fs";
@@ -84873,7 +85099,7 @@ function createSessionFaviconResolver(options = {}) {
84873
85099
  const missTtlMs = options.missTtlMs ?? 3e4;
84874
85100
  const maxEntries = options.maxEntries ?? 256;
84875
85101
  const cache3 = /* @__PURE__ */ new Map();
84876
- const remember = (sessionPath, entry) => {
85102
+ const remember2 = (sessionPath, entry) => {
84877
85103
  cache3.delete(sessionPath);
84878
85104
  cache3.set(sessionPath, entry);
84879
85105
  while (cache3.size > maxEntries) cache3.delete(cache3.keys().next().value);
@@ -84918,18 +85144,18 @@ function createSessionFaviconResolver(options = {}) {
84918
85144
  if (!sessionPath || !isAbsolute24(sessionPath) || sessionPath.includes("\0")) return null;
84919
85145
  const cached2 = cache3.get(sessionPath);
84920
85146
  if (cached2?.kind === "miss" && now() - cached2.at < missTtlMs) {
84921
- remember(sessionPath, cached2);
85147
+ remember2(sessionPath, cached2);
84922
85148
  return null;
84923
85149
  }
84924
85150
  if (cached2?.kind === "hit") {
84925
85151
  const stats = await lstatOrNull(io, cached2.file);
84926
85152
  if (stats && stats.isFile() && stats.mtimeMs === cached2.mtimeMs && stats.size === cached2.size) {
84927
- remember(sessionPath, cached2);
85153
+ remember2(sessionPath, cached2);
84928
85154
  return cached2.favicon;
84929
85155
  }
84930
85156
  }
84931
85157
  const entry = await scan(sessionPath);
84932
- remember(sessionPath, entry);
85158
+ remember2(sessionPath, entry);
84933
85159
  return entry.kind === "hit" ? entry.favicon : null;
84934
85160
  };
84935
85161
  }
@@ -96230,15 +96456,21 @@ async function startEmbeddedDaemonGeneration(opts) {
96230
96456
  paneStreamRuntime,
96231
96457
  catalogLiveSessions: () => discoverLiveSessionSummaries(catalogTmuxRunner),
96232
96458
  catalogFleet: async () => {
96233
- const fleet = readAdoptedFleet(
96459
+ const endpoint = (sessionName2, pane) => pane.semanticPaneId ? interactionEvidence?.captureInventoryEndpoint(
96460
+ sessionName2,
96461
+ pane.runtimePaneId,
96462
+ pane.semanticPaneId
96463
+ ) ?? null : null;
96464
+ const epoch = () => observationSelector?.nativeServerEpoch ?? null;
96465
+ const fleet = await readCatalogFleet(
96234
96466
  workspaceRegistry,
96235
- nativeGenerationTmuxRunner,
96236
- (sessionName2, pane) => pane.semanticPaneId ? interactionEvidence?.captureInventoryEndpoint(
96237
- sessionName2,
96238
- pane.runtimePaneId,
96239
- pane.semanticPaneId
96240
- ) ?? null : null,
96241
- () => observationSelector?.nativeServerEpoch ?? null
96467
+ {
96468
+ fence: sessionMutationFence,
96469
+ fencedSync: nativeGenerationTmuxRunner,
96470
+ generationFencedAsync: (args) => fleetFactsTmuxRunner(args)
96471
+ },
96472
+ endpoint,
96473
+ epoch
96242
96474
  );
96243
96475
  if (!fleet) return null;
96244
96476
  const names = await readTeamMemberships(
@@ -96690,6 +96922,7 @@ var init_daemon_embed = __esm({
96690
96922
  init_workspace_registry();
96691
96923
  init_workspace_pane_creation2();
96692
96924
  init_discovery();
96925
+ init_catalog_fleet_read();
96693
96926
  init_workspace_open2();
96694
96927
  init_workspace_promotion2();
96695
96928
  init_workspace_open_handoff2();
@@ -98009,13 +98242,13 @@ async function manageDaemonService(action, executable, deps2 = defaults()) {
98009
98242
  refused("The service manager loaded a different definition; refusing to control it");
98010
98243
  return state;
98011
98244
  };
98012
- const ready = async (previousInstance) => {
98245
+ const ready = async (previousInstance, retiredPid) => {
98013
98246
  const deadline = Date.now() + deps2.waitMs;
98014
98247
  do {
98015
98248
  const state = deps2.inspect();
98016
98249
  if (!boundTo(state, plan.supervisionId))
98017
98250
  refused("Service reservation changed during startup");
98018
- if (state.status === "valid" && state.info.instanceId !== previousInstance) {
98251
+ if (state.status === "valid" && state.info.instanceId !== previousInstance && state.info.pid !== retiredPid) {
98019
98252
  const identity4 = await deps2.probe(state.info);
98020
98253
  const service2 = await inspectManager();
98021
98254
  if (identity4 && service2.active && service2.pid === state.info.pid && identity4.pid === state.info.pid && identity4.instanceId === state.info.instanceId && identity4.startedAt === state.info.startedAt && identity4.protocolVersion === state.info.protocolVersion)
@@ -98116,9 +98349,13 @@ async function manageDaemonService(action, executable, deps2 = defaults()) {
98116
98349
  }
98117
98350
  const before = deps2.inspect();
98118
98351
  if (action === "install" && service.active) return await ready();
98119
- if (service.loaded) await manager.restart(service.pid !== null);
98352
+ const current = service.loaded ? await inspectManager() : service;
98353
+ if (current.loaded) await manager.restart(current.pid !== null);
98120
98354
  else await manager.install();
98121
- return await ready(before.status === "valid" ? before.info.instanceId : void 0);
98355
+ return await ready(
98356
+ before.status === "valid" ? before.info.instanceId : void 0,
98357
+ current.loaded ? current.pid : null
98358
+ );
98122
98359
  } catch (error) {
98123
98360
  if (!activationAttempted && wroteRecord) {
98124
98361
  if (wroteUnit && ownedFile(plan.unitPath) === contents) unlinkSync9(plan.unitPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.16",
3
+ "version": "2.9.17",
4
4
  "description": "The open-source workspace for coding agents: run Claude Code, Codex and other agents in tmux with live status",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@ export interface BufferedFnv64 {
10
10
  digest(): string;
11
11
  }
12
12
 
13
- interface NativeHasher {
13
+ export interface NativeHasher {
14
14
  readonly scratch: Uint8Array;
15
15
  readonly update: (state: bigint, length: number) => bigint;
16
16
  }
@@ -49,6 +49,13 @@ function initialize(): NativeHasher | null {
49
49
  return native;
50
50
  }
51
51
 
52
+ /** Direct access to the WASM scratch buffer and its update, for one synchronous
53
+ * hash at a time. Callers must not yield between filling the scratch and update. */
54
+ export function nativeFnv64(): NativeHasher | null {
55
+ return initialize();
56
+ }
57
+ export const FNV64_OFFSET_BASIS = OFFSET_BASIS;
58
+
52
59
  /** Internal optional accelerator. Canonical serialization remains the caller's responsibility. */
53
60
  export function createBufferedFnv64(initialState = OFFSET_BASIS): BufferedFnv64 | null {
54
61
  const hasher = initialize();