tmux-ide 2.9.0-beta.27 → 2.9.0-beta.28

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
@@ -11279,7 +11279,7 @@ var require_package = __commonJS({
11279
11279
  "package.json"(exports, module) {
11280
11280
  module.exports = {
11281
11281
  name: "tmux-ide",
11282
- version: "2.9.0-beta.27",
11282
+ version: "2.9.0-beta.28",
11283
11283
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
11284
11284
  type: "module",
11285
11285
  bin: {
@@ -29081,6 +29081,38 @@ var init_session_channel = __esm({
29081
29081
  this.windowLinkAuthority = new WindowLinkAuthority(liveSessionId, identity2.runtimeSessionId);
29082
29082
  if (this.latestWindowStage) this.windowLinkAuthority.reconcile(this.latestWindowStage.links);
29083
29083
  }
29084
+ /** Capture this attached connection once; never resolve a replacement mid-operation. */
29085
+ paneResizeTransport() {
29086
+ const identity2 = this.attachedIdentity;
29087
+ const generation = this.attachedServerGeneration;
29088
+ const assertCurrent = () => {
29089
+ if (this.disposed || !identity2 || !generation || this.attachedIdentity !== identity2 || this.attachedServerGeneration !== generation)
29090
+ throw new Error("Resize control connection retired");
29091
+ };
29092
+ assertCurrent();
29093
+ return async (args) => {
29094
+ assertCurrent();
29095
+ const listing = args.length === 6 && args[0] === "list-panes" && args[1] === "-s" && args[2] === "-t" && args[3] === `=${identity2.sessionName}` && args[4] === "-F";
29096
+ const resize = args.length === 11 && args[0] === "resize-pane" && args[1] === "-t" && /^%[0-9]+$/u.test(args[2]) && (args[3] === "-x" || args[3] === "-y") && /^[1-9][0-9]*$/u.test(args[4]) && args[5] === ";" && args[6] === "display-message" && args[7] === "-p" && args[8] === "-t" && args[9] === args[2] && args[10] === (args[3] === "-x" ? "#{pane_width}" : "#{pane_height}");
29097
+ if (!listing && !resize) throw new Error("Invalid resize control command");
29098
+ const pinned = listing ? [...args.slice(0, 3), identity2.runtimeSessionId, ...args.slice(4)] : args;
29099
+ const command2 = pinned.map((arg, index) => resize && index === 5 ? ";" : tmuxSingleQuote(arg)).join(" ");
29100
+ const lines = listing ? await this.io.request(command2) : await new Promise((resolve41, reject) => {
29101
+ this.io.commandListInline(
29102
+ command2,
29103
+ 2,
29104
+ 1,
29105
+ (reply) => reply.ok ? resolve41(reply.lines) : reject(new Error("Resize control command failed"))
29106
+ );
29107
+ });
29108
+ assertCurrent();
29109
+ return lines.map((line) => {
29110
+ const decoded = decodeControlReplyUtf8(line);
29111
+ if (decoded === null) throw new Error("Invalid resize control reply");
29112
+ return decoded;
29113
+ }).join("\n");
29114
+ };
29115
+ }
29084
29116
  async executeWindowLinkAction(request3) {
29085
29117
  const authority = this.windowLinkAuthority;
29086
29118
  if (!authority || this.disposed) throw new WindowLinkResolutionError("window_link_stale");
@@ -31552,6 +31584,16 @@ var init_mirror_service = __esm({
31552
31584
  }
31553
31585
  };
31554
31586
  }
31587
+ paneResizeTransport(session) {
31588
+ const entry = this.channels.get(session);
31589
+ if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
31590
+ const run = entry.channel.paneResizeTransport();
31591
+ return (args) => {
31592
+ if (entry.retired || this.channels.get(session) !== entry)
31593
+ return Promise.reject(new Error("Resize mirror session retired"));
31594
+ return run(args);
31595
+ };
31596
+ }
31555
31597
  async executeWindowLinkAction(session, request3) {
31556
31598
  const entry = this.channels.get(session);
31557
31599
  if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
@@ -51699,6 +51741,10 @@ var init_registry2 = __esm({
51699
51741
  return (this.#sessions.get(session)?.trustedInventoryQualified() ?? false) && this.#mirror.hasRetainedSession(session);
51700
51742
  }
51701
51743
  /** Called by the serialized semantic mutation executor, never by a transport directly. */
51744
+ paneResizeTransport(session) {
51745
+ if (this.#disposed) throw new Error("Session runtime disposed");
51746
+ return this.#mirror.paneResizeTransport(session);
51747
+ }
51702
51748
  executeWindowLinkAction(session, request3) {
51703
51749
  if (this.#disposed) return Promise.reject(new Error("Session runtime disposed"));
51704
51750
  return this.#mirror.executeWindowLinkAction(session, request3);
@@ -62112,7 +62158,7 @@ function parseMultiplexerPaneRows(output) {
62112
62158
  const rows = [];
62113
62159
  for (const line of output.split("\n")) {
62114
62160
  const fields = line.split(" ");
62115
- if (fields.length !== 9) {
62161
+ if (fields.length !== 9 && fields.length !== 11) {
62116
62162
  throw new WorkspaceMultiplexerError("workspace_unavailable", {
62117
62163
  reason: "pane_listing_shape"
62118
62164
  });
@@ -62135,6 +62181,11 @@ function parseMultiplexerPaneRows(output) {
62135
62181
  }
62136
62182
  const count = Number(paneCount);
62137
62183
  const index = Number(paneIndex);
62184
+ const geometry = fields.length === 11 ? { width: Number(fields[9]), height: Number(fields[10]) } : {};
62185
+ if (fields.length === 11 && (!Number.isSafeInteger(geometry.width) || geometry.width < 1 || !Number.isSafeInteger(geometry.height) || geometry.height < 1))
62186
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
62187
+ reason: "pane_listing_geometry"
62188
+ });
62138
62189
  if (!Number.isInteger(count) || count < 1 || !Number.isInteger(index) || index < 0) {
62139
62190
  throw new WorkspaceMultiplexerError("workspace_unavailable", {
62140
62191
  reason: "pane_listing_shape"
@@ -62149,7 +62200,8 @@ function parseMultiplexerPaneRows(output) {
62149
62200
  windowPaneCount: count,
62150
62201
  windowZoomed: zoomed === "1",
62151
62202
  paneActive: active2 === "1",
62152
- creationId: creationId === "" ? null : creationId
62203
+ creationId: creationId === "" ? null : creationId,
62204
+ ...geometry
62153
62205
  });
62154
62206
  }
62155
62207
  const unique = /* @__PURE__ */ new Map();
@@ -62296,6 +62348,41 @@ var init_workspace_multiplexer_verbs = __esm({
62296
62348
  mutate(raw, timing) {
62297
62349
  return this.#mutate(raw, timing);
62298
62350
  }
62351
+ /** Retained control transport; owned by the same serialized semantic lane. */
62352
+ async mutateResize(raw, transport) {
62353
+ if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
62354
+ const request3 = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
62355
+ if (request3.expectedDaemonInstanceId !== this.#daemonInstanceId)
62356
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch");
62357
+ const intent = request3.intent;
62358
+ if (intent.verb !== "workspace.pane.resize") throw new TypeError("Expected pane resize");
62359
+ const workspace = this.#registry.get(intent.workspaceName);
62360
+ if (!workspace) throw new WorkspaceMultiplexerError("workspace_not_found");
62361
+ const envelope = {
62362
+ operationId: request3.operationId,
62363
+ daemonInstanceId: this.#daemonInstanceId,
62364
+ workspaceName: intent.workspaceName
62365
+ };
62366
+ try {
62367
+ const run = transport(workspace.sessionName);
62368
+ const steps = this.#resizeSteps(intent, workspace.sessionName, envelope);
62369
+ let next = steps.next();
62370
+ while (!next.done) {
62371
+ if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
62372
+ const observed = await run(next.value);
62373
+ if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
62374
+ next = steps.next(observed);
62375
+ }
62376
+ return WorkspaceMultiplexerMutationResultSchemaZ.parse(next.value);
62377
+ } catch (cause) {
62378
+ if (cause instanceof WorkspaceMultiplexerError) throw cause;
62379
+ throw new WorkspaceMultiplexerError(
62380
+ "mutation_unverified",
62381
+ { operationId: request3.operationId },
62382
+ cause
62383
+ );
62384
+ }
62385
+ }
62299
62386
  /** Runs only inside the existing semantic mutation lane; never rediscovers a stale link. */
62300
62387
  async mutateWindowLink(raw, execute2) {
62301
62388
  if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
@@ -63201,14 +63288,7 @@ var init_workspace_multiplexer_verbs = __esm({
63201
63288
  // resize
63202
63289
  // -------------------------------------------------------------------------
63203
63290
  /** The pane's own size on one axis, in cells, read straight from tmux. */
63204
- #paneCells(paneId, axis) {
63205
- const observed = this.#io.runTmux([
63206
- "display-message",
63207
- "-p",
63208
- "-t",
63209
- paneId,
63210
- axis === "cols" ? "#{pane_width}" : "#{pane_height}"
63211
- ]);
63291
+ #paneCells(observed) {
63212
63292
  const cells = Number(observed);
63213
63293
  if (!Number.isInteger(cells) || cells < 1) {
63214
63294
  throw new WorkspaceMultiplexerError("mutation_unverified", { reason: "pane_size_shape" });
@@ -63226,7 +63306,21 @@ var init_workspace_multiplexer_verbs = __esm({
63226
63306
  * whole view exists to remove.
63227
63307
  */
63228
63308
  #resize(intent, sessionName, envelope) {
63229
- const rows = this.#panes(sessionName);
63309
+ const steps = this.#resizeSteps(intent, sessionName, envelope);
63310
+ let next = steps.next();
63311
+ while (!next.done) next = steps.next(this.#io.runTmux(next.value));
63312
+ return next.value;
63313
+ }
63314
+ *#resizeSteps(intent, sessionName, envelope) {
63315
+ const output = yield [
63316
+ "list-panes",
63317
+ "-s",
63318
+ "-t",
63319
+ `=${sessionName}`,
63320
+ "-F",
63321
+ `${PANE_FIELDS} #{pane_width} #{pane_height}`
63322
+ ];
63323
+ const rows = parseMultiplexerPaneRows(output);
63230
63324
  const pane = resolvePaneRow(rows, intent.semanticPaneId);
63231
63325
  if (pane.windowPaneCount < 2) {
63232
63326
  throw new WorkspaceMultiplexerError("single_pane_window", {
@@ -63238,7 +63332,7 @@ var init_workspace_multiplexer_verbs = __esm({
63238
63332
  semanticPaneId: intent.semanticPaneId
63239
63333
  });
63240
63334
  }
63241
- const before = this.#paneCells(pane.paneId, intent.axis);
63335
+ const before = this.#paneCells(intent.axis === "cols" ? pane.width : pane.height);
63242
63336
  if (before === intent.cells) {
63243
63337
  return {
63244
63338
  ...envelope,
@@ -63249,14 +63343,20 @@ var init_workspace_multiplexer_verbs = __esm({
63249
63343
  cells: before
63250
63344
  };
63251
63345
  }
63252
- this.#io.runTmux([
63346
+ const observed = yield [
63253
63347
  "resize-pane",
63254
63348
  "-t",
63255
63349
  pane.paneId,
63256
63350
  intent.axis === "cols" ? "-x" : "-y",
63257
- String(intent.cells)
63258
- ]);
63259
- const after = this.#paneCells(pane.paneId, intent.axis);
63351
+ String(intent.cells),
63352
+ ";",
63353
+ "display-message",
63354
+ "-p",
63355
+ "-t",
63356
+ pane.paneId,
63357
+ intent.axis === "cols" ? "#{pane_width}" : "#{pane_height}"
63358
+ ];
63359
+ const after = this.#paneCells(observed);
63260
63360
  return {
63261
63361
  ...envelope,
63262
63362
  verb: "workspace.pane.resize",
@@ -63522,6 +63622,15 @@ async function createNativeTmuxServerOwner(options) {
63522
63622
  (session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action)
63523
63623
  );
63524
63624
  }
63625
+ if (intent.verb === "workspace.pane.resize") {
63626
+ return multiplexer.mutateResize(
63627
+ { operationId, expectedDaemonInstanceId: generation, intent },
63628
+ (session) => {
63629
+ if (!sessionRuntimeRegistry) throw new Error("Session runtime unavailable");
63630
+ return sessionRuntimeRegistry.paneResizeTransport(session);
63631
+ }
63632
+ );
63633
+ }
63525
63634
  return multiplexer.mutate(
63526
63635
  { operationId, expectedDaemonInstanceId: generation, intent },
63527
63636
  timing
@@ -81352,6 +81461,15 @@ async function startEmbeddedDaemonGeneration(opts) {
81352
81461
  }
81353
81462
  );
81354
81463
  }
81464
+ if (intent.verb === "workspace.pane.resize") {
81465
+ return workspaceMultiplexer.mutateResize(
81466
+ { operationId, expectedDaemonInstanceId: instanceId, intent },
81467
+ (session) => {
81468
+ if (!sessionRuntimeRegistry) throw new Error("Session runtime unavailable");
81469
+ return sessionRuntimeRegistry.paneResizeTransport(session);
81470
+ }
81471
+ );
81472
+ }
81355
81473
  return workspaceMultiplexer.mutate(
81356
81474
  { operationId, expectedDaemonInstanceId: instanceId, intent },
81357
81475
  timing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.27",
3
+ "version": "2.9.0-beta.28",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -915,6 +915,13 @@ async function startEmbeddedDaemonGeneration(opts) {
915
915
  return sessionRuntimeRegistry.executeWindowLinkAction(session, action);
916
916
  });
917
917
  }
918
+ if (intent.verb === "workspace.pane.resize") {
919
+ return workspaceMultiplexer.mutateResize({ operationId, expectedDaemonInstanceId: instanceId, intent }, (session) => {
920
+ if (!sessionRuntimeRegistry)
921
+ throw new Error("Session runtime unavailable");
922
+ return sessionRuntimeRegistry.paneResizeTransport(session);
923
+ });
924
+ }
918
925
  return workspaceMultiplexer.mutate({ operationId, expectedDaemonInstanceId: instanceId, intent }, timing);
919
926
  };
920
927
  const runtimeTracePath = process.env.TMUX_IDE_SESSION_RUNTIME_TRACE_LOG;
@@ -122,6 +122,13 @@ export async function createNativeTmuxServerOwner(options) {
122
122
  intent.verb === "workspace.pane.select") {
123
123
  return multiplexer.mutateWindowLink({ operationId, expectedDaemonInstanceId: generation, intent }, (session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action));
124
124
  }
125
+ if (intent.verb === "workspace.pane.resize") {
126
+ return multiplexer.mutateResize({ operationId, expectedDaemonInstanceId: generation, intent }, (session) => {
127
+ if (!sessionRuntimeRegistry)
128
+ throw new Error("Session runtime unavailable");
129
+ return sessionRuntimeRegistry.paneResizeTransport(session);
130
+ });
131
+ }
125
132
  return multiplexer.mutate({ operationId, expectedDaemonInstanceId: generation, intent }, timing);
126
133
  },
127
134
  publishReceipt: (receipt) => ({
@@ -96,7 +96,7 @@ export function parseMultiplexerPaneRows(output) {
96
96
  const rows = [];
97
97
  for (const line of output.split("\n")) {
98
98
  const fields = line.split("\t");
99
- if (fields.length !== 9) {
99
+ if (fields.length !== 9 && fields.length !== 11) {
100
100
  throw new WorkspaceMultiplexerError("workspace_unavailable", {
101
101
  reason: "pane_listing_shape",
102
102
  });
@@ -109,6 +109,15 @@ export function parseMultiplexerPaneRows(output) {
109
109
  }
110
110
  const count = Number(paneCount);
111
111
  const index = Number(paneIndex);
112
+ const geometry = fields.length === 11 ? { width: Number(fields[9]), height: Number(fields[10]) } : {};
113
+ if (fields.length === 11 &&
114
+ (!Number.isSafeInteger(geometry.width) ||
115
+ geometry.width < 1 ||
116
+ !Number.isSafeInteger(geometry.height) ||
117
+ geometry.height < 1))
118
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
119
+ reason: "pane_listing_geometry",
120
+ });
112
121
  if (!Number.isInteger(count) || count < 1 || !Number.isInteger(index) || index < 0) {
113
122
  throw new WorkspaceMultiplexerError("workspace_unavailable", {
114
123
  reason: "pane_listing_shape",
@@ -124,6 +133,7 @@ export function parseMultiplexerPaneRows(output) {
124
133
  windowZoomed: zoomed === "1",
125
134
  paneActive: active === "1",
126
135
  creationId: creationId === "" ? null : creationId,
136
+ ...geometry,
127
137
  });
128
138
  }
129
139
  // list-panes -s repeats each backing for its session links. Identical native
@@ -222,6 +232,45 @@ export class WorkspaceMultiplexerAuthority {
222
232
  mutate(raw, timing) {
223
233
  return this.#mutate(raw, timing);
224
234
  }
235
+ /** Retained control transport; owned by the same serialized semantic lane. */
236
+ async mutateResize(raw, transport) {
237
+ if (this.#disposed)
238
+ throw new WorkspaceMultiplexerError("workspace_unavailable");
239
+ const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
240
+ if (request.expectedDaemonInstanceId !== this.#daemonInstanceId)
241
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch");
242
+ const intent = request.intent;
243
+ if (intent.verb !== "workspace.pane.resize")
244
+ throw new TypeError("Expected pane resize");
245
+ const workspace = this.#registry.get(intent.workspaceName);
246
+ if (!workspace)
247
+ throw new WorkspaceMultiplexerError("workspace_not_found");
248
+ const envelope = {
249
+ operationId: request.operationId,
250
+ daemonInstanceId: this.#daemonInstanceId,
251
+ workspaceName: intent.workspaceName,
252
+ };
253
+ try {
254
+ const run = transport(workspace.sessionName);
255
+ const steps = this.#resizeSteps(intent, workspace.sessionName, envelope);
256
+ let next = steps.next();
257
+ while (!next.done) {
258
+ if (this.#disposed)
259
+ throw new WorkspaceMultiplexerError("workspace_unavailable");
260
+ const observed = await run(next.value);
261
+ if (this.#disposed)
262
+ throw new WorkspaceMultiplexerError("workspace_unavailable");
263
+ next = steps.next(observed);
264
+ }
265
+ return WorkspaceMultiplexerMutationResultSchemaZ.parse(next.value);
266
+ }
267
+ catch (cause) {
268
+ if (cause instanceof WorkspaceMultiplexerError)
269
+ throw cause;
270
+ // A missing reply may follow an effect. Never fall back or replay.
271
+ throw new WorkspaceMultiplexerError("mutation_unverified", { operationId: request.operationId }, cause);
272
+ }
273
+ }
225
274
  /** Runs only inside the existing semantic mutation lane; never rediscovers a stale link. */
226
275
  async mutateWindowLink(raw, execute) {
227
276
  if (this.#disposed)
@@ -1180,14 +1229,7 @@ export class WorkspaceMultiplexerAuthority {
1180
1229
  // resize
1181
1230
  // -------------------------------------------------------------------------
1182
1231
  /** The pane's own size on one axis, in cells, read straight from tmux. */
1183
- #paneCells(paneId, axis) {
1184
- const observed = this.#io.runTmux([
1185
- "display-message",
1186
- "-p",
1187
- "-t",
1188
- paneId,
1189
- axis === "cols" ? "#{pane_width}" : "#{pane_height}",
1190
- ]);
1232
+ #paneCells(observed) {
1191
1233
  const cells = Number(observed);
1192
1234
  if (!Number.isInteger(cells) || cells < 1) {
1193
1235
  throw new WorkspaceMultiplexerError("mutation_unverified", { reason: "pane_size_shape" });
@@ -1205,7 +1247,22 @@ export class WorkspaceMultiplexerAuthority {
1205
1247
  * whole view exists to remove.
1206
1248
  */
1207
1249
  #resize(intent, sessionName, envelope) {
1208
- const rows = this.#panes(sessionName);
1250
+ const steps = this.#resizeSteps(intent, sessionName, envelope);
1251
+ let next = steps.next();
1252
+ while (!next.done)
1253
+ next = steps.next(this.#io.runTmux(next.value));
1254
+ return next.value;
1255
+ }
1256
+ *#resizeSteps(intent, sessionName, envelope) {
1257
+ const output = yield [
1258
+ "list-panes",
1259
+ "-s",
1260
+ "-t",
1261
+ `=${sessionName}`,
1262
+ "-F",
1263
+ `${PANE_FIELDS}\t#{pane_width}\t#{pane_height}`,
1264
+ ];
1265
+ const rows = parseMultiplexerPaneRows(output);
1209
1266
  const pane = resolvePaneRow(rows, intent.semanticPaneId);
1210
1267
  if (pane.windowPaneCount < 2) {
1211
1268
  throw new WorkspaceMultiplexerError("single_pane_window", {
@@ -1219,7 +1276,7 @@ export class WorkspaceMultiplexerAuthority {
1219
1276
  semanticPaneId: intent.semanticPaneId,
1220
1277
  });
1221
1278
  }
1222
- const before = this.#paneCells(pane.paneId, intent.axis);
1279
+ const before = this.#paneCells(intent.axis === "cols" ? pane.width : pane.height);
1223
1280
  if (before === intent.cells) {
1224
1281
  return {
1225
1282
  ...envelope,
@@ -1230,14 +1287,22 @@ export class WorkspaceMultiplexerAuthority {
1230
1287
  cells: before,
1231
1288
  };
1232
1289
  }
1233
- this.#io.runTmux([
1290
+ // Keep mutation and authoritative readback in one tmux command queue.
1291
+ // Avoid two extra synchronous client processes on every pointer update.
1292
+ const observed = yield [
1234
1293
  "resize-pane",
1235
1294
  "-t",
1236
1295
  pane.paneId,
1237
1296
  intent.axis === "cols" ? "-x" : "-y",
1238
1297
  String(intent.cells),
1239
- ]);
1240
- const after = this.#paneCells(pane.paneId, intent.axis);
1298
+ ";",
1299
+ "display-message",
1300
+ "-p",
1301
+ "-t",
1302
+ pane.paneId,
1303
+ intent.axis === "cols" ? "#{pane_width}" : "#{pane_height}",
1304
+ ];
1305
+ const after = this.#paneCells(observed);
1241
1306
  return {
1242
1307
  ...envelope,
1243
1308
  verb: "workspace.pane.resize",
@@ -149,6 +149,17 @@ export class MirrorService {
149
149
  },
150
150
  };
151
151
  }
152
+ paneResizeTransport(session) {
153
+ const entry = this.channels.get(session);
154
+ if (!entry || entry.retired)
155
+ throw new Error(`Mirror session ${session} is unavailable`);
156
+ const run = entry.channel.paneResizeTransport();
157
+ return (args) => {
158
+ if (entry.retired || this.channels.get(session) !== entry)
159
+ return Promise.reject(new Error("Resize mirror session retired"));
160
+ return run(args);
161
+ };
162
+ }
152
163
  async executeWindowLinkAction(session, request) {
153
164
  const entry = this.channels.get(session);
154
165
  if (!entry || entry.retired)
@@ -389,6 +389,64 @@ export class SessionChannel {
389
389
  if (this.latestWindowStage)
390
390
  this.windowLinkAuthority.reconcile(this.latestWindowStage.links);
391
391
  }
392
+ /** Capture this attached connection once; never resolve a replacement mid-operation. */
393
+ paneResizeTransport() {
394
+ const identity = this.attachedIdentity;
395
+ const generation = this.attachedServerGeneration;
396
+ const assertCurrent = () => {
397
+ if (this.disposed ||
398
+ !identity ||
399
+ !generation ||
400
+ this.attachedIdentity !== identity ||
401
+ this.attachedServerGeneration !== generation)
402
+ throw new Error("Resize control connection retired");
403
+ };
404
+ assertCurrent();
405
+ return async (args) => {
406
+ assertCurrent();
407
+ const listing = args.length === 6 &&
408
+ args[0] === "list-panes" &&
409
+ args[1] === "-s" &&
410
+ args[2] === "-t" &&
411
+ args[3] === `=${identity.sessionName}` &&
412
+ args[4] === "-F";
413
+ const resize = args.length === 11 &&
414
+ args[0] === "resize-pane" &&
415
+ args[1] === "-t" &&
416
+ /^%[0-9]+$/u.test(args[2]) &&
417
+ (args[3] === "-x" || args[3] === "-y") &&
418
+ /^[1-9][0-9]*$/u.test(args[4]) &&
419
+ args[5] === ";" &&
420
+ args[6] === "display-message" &&
421
+ args[7] === "-p" &&
422
+ args[8] === "-t" &&
423
+ args[9] === args[2] &&
424
+ args[10] === (args[3] === "-x" ? "#{pane_width}" : "#{pane_height}");
425
+ if (!listing && !resize)
426
+ throw new Error("Invalid resize control command");
427
+ // A runtime session ID prevents a rename/replacement from redirecting the lookup.
428
+ const pinned = listing
429
+ ? [...args.slice(0, 3), identity.runtimeSessionId, ...args.slice(4)]
430
+ : args;
431
+ const command = pinned
432
+ .map((arg, index) => (resize && index === 5 ? ";" : tmuxSingleQuote(arg)))
433
+ .join(" ");
434
+ const lines = listing
435
+ ? await this.io.request(command)
436
+ : await new Promise((resolve, reject) => {
437
+ this.io.commandListInline(command, 2, 1, (reply) => reply.ok ? resolve(reply.lines) : reject(new Error("Resize control command failed")));
438
+ });
439
+ assertCurrent();
440
+ return lines
441
+ .map((line) => {
442
+ const decoded = decodeControlReplyUtf8(line);
443
+ if (decoded === null)
444
+ throw new Error("Invalid resize control reply");
445
+ return decoded;
446
+ })
447
+ .join("\n");
448
+ };
449
+ }
392
450
  async executeWindowLinkAction(request) {
393
451
  const authority = this.windowLinkAuthority;
394
452
  if (!authority || this.disposed)
@@ -259,6 +259,11 @@ export class SessionRuntimeRegistry {
259
259
  this.#mirror.hasRetainedSession(session));
260
260
  }
261
261
  /** Called by the serialized semantic mutation executor, never by a transport directly. */
262
+ paneResizeTransport(session) {
263
+ if (this.#disposed)
264
+ throw new Error("Session runtime disposed");
265
+ return this.#mirror.paneResizeTransport(session);
266
+ }
262
267
  executeWindowLinkAction(session, request) {
263
268
  if (this.#disposed)
264
269
  return Promise.reject(new Error("Session runtime disposed"));
@@ -1343,6 +1343,15 @@ async function startEmbeddedDaemonGeneration(
1343
1343
  },
1344
1344
  );
1345
1345
  }
1346
+ if (intent.verb === "workspace.pane.resize") {
1347
+ return workspaceMultiplexer.mutateResize(
1348
+ { operationId, expectedDaemonInstanceId: instanceId, intent },
1349
+ (session) => {
1350
+ if (!sessionRuntimeRegistry) throw new Error("Session runtime unavailable");
1351
+ return sessionRuntimeRegistry.paneResizeTransport(session);
1352
+ },
1353
+ );
1354
+ }
1346
1355
  return workspaceMultiplexer.mutate(
1347
1356
  { operationId, expectedDaemonInstanceId: instanceId, intent },
1348
1357
  timing,
@@ -165,6 +165,15 @@ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwner
165
165
  (session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action),
166
166
  );
167
167
  }
168
+ if (intent.verb === "workspace.pane.resize") {
169
+ return multiplexer.mutateResize(
170
+ { operationId, expectedDaemonInstanceId: generation, intent },
171
+ (session) => {
172
+ if (!sessionRuntimeRegistry) throw new Error("Session runtime unavailable");
173
+ return sessionRuntimeRegistry.paneResizeTransport(session);
174
+ },
175
+ );
176
+ }
168
177
  return multiplexer.mutate(
169
178
  { operationId, expectedDaemonInstanceId: generation, intent },
170
179
  timing,
@@ -146,6 +146,8 @@ export interface MultiplexerPaneRow {
146
146
  readonly windowZoomed: boolean;
147
147
  readonly paneActive: boolean;
148
148
  readonly creationId: string | null;
149
+ readonly width?: number;
150
+ readonly height?: number;
149
151
  }
150
152
 
151
153
  const PANE_FIELDS = [
@@ -173,7 +175,7 @@ export function parseMultiplexerPaneRows(output: string): readonly MultiplexerPa
173
175
  const rows: MultiplexerPaneRow[] = [];
174
176
  for (const line of output.split("\n")) {
175
177
  const fields = line.split("\t");
176
- if (fields.length !== 9) {
178
+ if (fields.length !== 9 && fields.length !== 11) {
177
179
  throw new WorkspaceMultiplexerError("workspace_unavailable", {
178
180
  reason: "pane_listing_shape",
179
181
  });
@@ -196,6 +198,18 @@ export function parseMultiplexerPaneRows(output: string): readonly MultiplexerPa
196
198
  }
197
199
  const count = Number(paneCount);
198
200
  const index = Number(paneIndex);
201
+ const geometry =
202
+ fields.length === 11 ? { width: Number(fields[9]), height: Number(fields[10]) } : {};
203
+ if (
204
+ fields.length === 11 &&
205
+ (!Number.isSafeInteger(geometry.width) ||
206
+ geometry.width! < 1 ||
207
+ !Number.isSafeInteger(geometry.height) ||
208
+ geometry.height! < 1)
209
+ )
210
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
211
+ reason: "pane_listing_geometry",
212
+ });
199
213
  if (!Number.isInteger(count) || count < 1 || !Number.isInteger(index) || index < 0) {
200
214
  throw new WorkspaceMultiplexerError("workspace_unavailable", {
201
215
  reason: "pane_listing_shape",
@@ -211,6 +225,7 @@ export function parseMultiplexerPaneRows(output: string): readonly MultiplexerPa
211
225
  windowZoomed: zoomed === "1",
212
226
  paneActive: active === "1",
213
227
  creationId: creationId === "" ? null : creationId,
228
+ ...geometry,
214
229
  });
215
230
  }
216
231
  // list-panes -s repeats each backing for its session links. Identical native
@@ -364,6 +379,46 @@ export class WorkspaceMultiplexerAuthority {
364
379
  return this.#mutate(raw, timing);
365
380
  }
366
381
 
382
+ /** Retained control transport; owned by the same serialized semantic lane. */
383
+ async mutateResize(
384
+ raw: WorkspaceMultiplexerMutationRequest,
385
+ transport: (session: string) => (args: readonly string[]) => Promise<string>,
386
+ ): Promise<WorkspaceMultiplexerMutationResult> {
387
+ if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
388
+ const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
389
+ if (request.expectedDaemonInstanceId !== this.#daemonInstanceId)
390
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch");
391
+ const intent = request.intent;
392
+ if (intent.verb !== "workspace.pane.resize") throw new TypeError("Expected pane resize");
393
+ const workspace = this.#registry.get(intent.workspaceName);
394
+ if (!workspace) throw new WorkspaceMultiplexerError("workspace_not_found");
395
+ const envelope = {
396
+ operationId: request.operationId,
397
+ daemonInstanceId: this.#daemonInstanceId,
398
+ workspaceName: intent.workspaceName,
399
+ };
400
+ try {
401
+ const run = transport(workspace.sessionName);
402
+ const steps = this.#resizeSteps(intent, workspace.sessionName, envelope);
403
+ let next = steps.next();
404
+ while (!next.done) {
405
+ if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
406
+ const observed = await run(next.value);
407
+ if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
408
+ next = steps.next(observed);
409
+ }
410
+ return WorkspaceMultiplexerMutationResultSchemaZ.parse(next.value);
411
+ } catch (cause) {
412
+ if (cause instanceof WorkspaceMultiplexerError) throw cause;
413
+ // A missing reply may follow an effect. Never fall back or replay.
414
+ throw new WorkspaceMultiplexerError(
415
+ "mutation_unverified",
416
+ { operationId: request.operationId },
417
+ cause,
418
+ );
419
+ }
420
+ }
421
+
367
422
  /** Runs only inside the existing semantic mutation lane; never rediscovers a stale link. */
368
423
  async mutateWindowLink(
369
424
  raw: WorkspaceMultiplexerMutationRequest,
@@ -1445,14 +1500,7 @@ export class WorkspaceMultiplexerAuthority {
1445
1500
  // -------------------------------------------------------------------------
1446
1501
 
1447
1502
  /** The pane's own size on one axis, in cells, read straight from tmux. */
1448
- #paneCells(paneId: string, axis: "cols" | "rows"): number {
1449
- const observed = this.#io.runTmux([
1450
- "display-message",
1451
- "-p",
1452
- "-t",
1453
- paneId,
1454
- axis === "cols" ? "#{pane_width}" : "#{pane_height}",
1455
- ]);
1503
+ #paneCells(observed: string | number | undefined): number {
1456
1504
  const cells = Number(observed);
1457
1505
  if (!Number.isInteger(cells) || cells < 1) {
1458
1506
  throw new WorkspaceMultiplexerError("mutation_unverified", { reason: "pane_size_shape" });
@@ -1475,7 +1523,26 @@ export class WorkspaceMultiplexerAuthority {
1475
1523
  sessionName: string,
1476
1524
  envelope: { operationId: string; daemonInstanceId: string; workspaceName: string },
1477
1525
  ): WorkspaceMultiplexerMutationResult {
1478
- const rows = this.#panes(sessionName);
1526
+ const steps = this.#resizeSteps(intent, sessionName, envelope);
1527
+ let next = steps.next();
1528
+ while (!next.done) next = steps.next(this.#io.runTmux(next.value));
1529
+ return next.value;
1530
+ }
1531
+
1532
+ *#resizeSteps(
1533
+ intent: Extract<WorkspaceMultiplexerIntent, { verb: "workspace.pane.resize" }>,
1534
+ sessionName: string,
1535
+ envelope: { operationId: string; daemonInstanceId: string; workspaceName: string },
1536
+ ): Generator<readonly string[], WorkspaceMultiplexerMutationResult, string> {
1537
+ const output = yield [
1538
+ "list-panes",
1539
+ "-s",
1540
+ "-t",
1541
+ `=${sessionName}`,
1542
+ "-F",
1543
+ `${PANE_FIELDS}\t#{pane_width}\t#{pane_height}`,
1544
+ ];
1545
+ const rows = parseMultiplexerPaneRows(output);
1479
1546
  const pane = resolvePaneRow(rows, intent.semanticPaneId);
1480
1547
  if (pane.windowPaneCount < 2) {
1481
1548
  throw new WorkspaceMultiplexerError("single_pane_window", {
@@ -1489,7 +1556,7 @@ export class WorkspaceMultiplexerAuthority {
1489
1556
  semanticPaneId: intent.semanticPaneId,
1490
1557
  });
1491
1558
  }
1492
- const before = this.#paneCells(pane.paneId, intent.axis);
1559
+ const before = this.#paneCells(intent.axis === "cols" ? pane.width : pane.height);
1493
1560
  if (before === intent.cells) {
1494
1561
  return {
1495
1562
  ...envelope,
@@ -1500,14 +1567,22 @@ export class WorkspaceMultiplexerAuthority {
1500
1567
  cells: before,
1501
1568
  };
1502
1569
  }
1503
- this.#io.runTmux([
1570
+ // Keep mutation and authoritative readback in one tmux command queue.
1571
+ // Avoid two extra synchronous client processes on every pointer update.
1572
+ const observed = yield [
1504
1573
  "resize-pane",
1505
1574
  "-t",
1506
1575
  pane.paneId,
1507
1576
  intent.axis === "cols" ? "-x" : "-y",
1508
1577
  String(intent.cells),
1509
- ]);
1510
- const after = this.#paneCells(pane.paneId, intent.axis);
1578
+ ";",
1579
+ "display-message",
1580
+ "-p",
1581
+ "-t",
1582
+ pane.paneId,
1583
+ intent.axis === "cols" ? "#{pane_width}" : "#{pane_height}",
1584
+ ];
1585
+ const after = this.#paneCells(observed);
1511
1586
  return {
1512
1587
  ...envelope,
1513
1588
  verb: "workspace.pane.resize",
@@ -294,6 +294,17 @@ export class MirrorService {
294
294
  };
295
295
  }
296
296
 
297
+ paneResizeTransport(session: string) {
298
+ const entry = this.channels.get(session);
299
+ if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
300
+ const run = entry.channel.paneResizeTransport();
301
+ return (args: readonly string[]) => {
302
+ if (entry.retired || this.channels.get(session) !== entry)
303
+ return Promise.reject(new Error("Resize mirror session retired"));
304
+ return run(args);
305
+ };
306
+ }
307
+
297
308
  async executeWindowLinkAction(
298
309
  session: string,
299
310
  request: { action: "select" | "unlink"; target?: WindowLinkTarget; paneId?: string },
@@ -718,6 +718,69 @@ export class SessionChannel {
718
718
  if (this.latestWindowStage) this.windowLinkAuthority.reconcile(this.latestWindowStage.links);
719
719
  }
720
720
 
721
+ /** Capture this attached connection once; never resolve a replacement mid-operation. */
722
+ paneResizeTransport(): (args: readonly string[]) => Promise<string> {
723
+ const identity = this.attachedIdentity;
724
+ const generation = this.attachedServerGeneration;
725
+ const assertCurrent = () => {
726
+ if (
727
+ this.disposed ||
728
+ !identity ||
729
+ !generation ||
730
+ this.attachedIdentity !== identity ||
731
+ this.attachedServerGeneration !== generation
732
+ )
733
+ throw new Error("Resize control connection retired");
734
+ };
735
+ assertCurrent();
736
+ return async (args) => {
737
+ assertCurrent();
738
+ const listing =
739
+ args.length === 6 &&
740
+ args[0] === "list-panes" &&
741
+ args[1] === "-s" &&
742
+ args[2] === "-t" &&
743
+ args[3] === `=${identity!.sessionName}` &&
744
+ args[4] === "-F";
745
+ const resize =
746
+ args.length === 11 &&
747
+ args[0] === "resize-pane" &&
748
+ args[1] === "-t" &&
749
+ /^%[0-9]+$/u.test(args[2]!) &&
750
+ (args[3] === "-x" || args[3] === "-y") &&
751
+ /^[1-9][0-9]*$/u.test(args[4]!) &&
752
+ args[5] === ";" &&
753
+ args[6] === "display-message" &&
754
+ args[7] === "-p" &&
755
+ args[8] === "-t" &&
756
+ args[9] === args[2] &&
757
+ args[10] === (args[3] === "-x" ? "#{pane_width}" : "#{pane_height}");
758
+ if (!listing && !resize) throw new Error("Invalid resize control command");
759
+ // A runtime session ID prevents a rename/replacement from redirecting the lookup.
760
+ const pinned = listing
761
+ ? [...args.slice(0, 3), identity!.runtimeSessionId, ...args.slice(4)]
762
+ : args;
763
+ const command = pinned
764
+ .map((arg, index) => (resize && index === 5 ? ";" : tmuxSingleQuote(arg)))
765
+ .join(" ");
766
+ const lines = listing
767
+ ? await this.io.request(command)
768
+ : await new Promise<string[]>((resolve, reject) => {
769
+ this.io.commandListInline(command, 2, 1, (reply) =>
770
+ reply.ok ? resolve(reply.lines) : reject(new Error("Resize control command failed")),
771
+ );
772
+ });
773
+ assertCurrent();
774
+ return lines
775
+ .map((line) => {
776
+ const decoded = decodeControlReplyUtf8(line);
777
+ if (decoded === null) throw new Error("Invalid resize control reply");
778
+ return decoded;
779
+ })
780
+ .join("\n");
781
+ };
782
+ }
783
+
721
784
  async executeWindowLinkAction(request: {
722
785
  action: "select" | "unlink";
723
786
  target?: WindowLinkTarget;
@@ -551,6 +551,11 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
551
551
  }
552
552
 
553
553
  /** Called by the serialized semantic mutation executor, never by a transport directly. */
554
+ paneResizeTransport(session: string) {
555
+ if (this.#disposed) throw new Error("Session runtime disposed");
556
+ return this.#mirror.paneResizeTransport(session);
557
+ }
558
+
554
559
  executeWindowLinkAction(
555
560
  session: string,
556
561
  request: Parameters<MirrorService["executeWindowLinkAction"]>[1],