tmux-ide 2.9.0-beta.26 → 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 +135 -17
- package/package.json +1 -1
- package/packages/daemon/dist/lib/daemon-embed.js +7 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +7 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +79 -14
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +11 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +58 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +5 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +8 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +18 -11
- package/packages/daemon/src/lib/daemon-embed.ts +9 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +9 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +89 -14
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +11 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +63 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +5 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +16 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +19 -6
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.
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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"));
|
|
@@ -167,6 +167,14 @@ export function terminalPaneSeparators(frames, paneBorderStatus) {
|
|
|
167
167
|
}
|
|
168
168
|
return Object.freeze(separators);
|
|
169
169
|
}
|
|
170
|
+
/** Highlight the observed divider, never the pointer's unconfirmed target. */
|
|
171
|
+
export function terminalPaneObservedResizeGuide(frames, paneBorderStatus, preview) {
|
|
172
|
+
const separator = terminalPaneSeparators(frames, paneBorderStatus).find((candidate) => candidate.paneId === preview.semanticPaneId &&
|
|
173
|
+
candidate.axis === (preview.axis === "cols" ? "x" : "y"));
|
|
174
|
+
return separator
|
|
175
|
+
? terminalPaneResizePreview(separator, separator.position, separator.position).guide
|
|
176
|
+
: null;
|
|
177
|
+
}
|
|
170
178
|
export function terminalPaneResizePreview(separator, pointer, origin) {
|
|
171
179
|
const scale = separator.nativeCellsPerDisplayCell ?? 1;
|
|
172
180
|
const total = separator.initialCells + separator.siblingCells;
|
|
@@ -16,7 +16,7 @@ import { PaneTitleBar } from "../workspace/terminal-pane-header.jsx";
|
|
|
16
16
|
import { PANE_ACTION_MENU_ITEMS, PaneActionMenu } from "../workspace/pane-action-menu.jsx";
|
|
17
17
|
import { createApplicationPaneMenuOwner } from "./application-pane-menu-owner.js";
|
|
18
18
|
import { extractTerminalSelection, terminalMouseActionSupported, terminalGestureLeaseMatches, terminalSelectionCell, terminalMouseInput, } from "./terminal-selection.js";
|
|
19
|
-
import { terminalAgentStatusLabel, terminalPaneDisplayTitle, terminalPaneResizePreview, terminalPaneSeparatorAt, terminalPaneSeparators, terminalWindowAgentIndicator, terminalWindowPane, terminalWindowTitle, } from "./application-terminal-workspace-policy.js";
|
|
19
|
+
import { terminalAgentStatusLabel, terminalPaneDisplayTitle, terminalPaneResizePreview, terminalPaneObservedResizeGuide, terminalPaneSeparatorAt, terminalPaneSeparators, terminalWindowAgentIndicator, terminalWindowPane, terminalWindowTitle, } from "./application-terminal-workspace-policy.js";
|
|
20
20
|
export { terminalAgentStatusLabel, terminalPaneChromeLabel, terminalPaneDisplayTitle, } from "./application-terminal-workspace-policy.js";
|
|
21
21
|
export function safeApplicationMouseIngressMicros(now = () => performance.now()) {
|
|
22
22
|
try {
|
|
@@ -304,14 +304,19 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
304
304
|
top: localY + 1 + height <= bottom ? localY + 1 : Math.max(topOffset(), localY - height),
|
|
305
305
|
});
|
|
306
306
|
};
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
307
|
+
const observedGuide = (preview) => terminalPaneObservedResizeGuide(visibleFrames(), layout().current?.paneBorderStatus ?? "off", preview);
|
|
308
|
+
const globalPreview = (preview) => {
|
|
309
|
+
const guide = observedGuide(preview) ?? preview.guide;
|
|
310
|
+
return Object.freeze({
|
|
311
|
+
...preview,
|
|
312
|
+
guide,
|
|
313
|
+
globalGuide: Object.freeze({
|
|
314
|
+
...guide,
|
|
315
|
+
x: guide.x + (props.originX ?? 0),
|
|
316
|
+
y: guide.y + (props.originY ?? 0) + topOffset(),
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
};
|
|
315
320
|
// tmux retains one active pane per window even while that window is hidden.
|
|
316
321
|
// Keep those native terminal surfaces presentation-ready while the host has
|
|
317
322
|
// focus; switching the visible window then changes only composition, not
|
|
@@ -1189,8 +1194,10 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
1189
1194
|
};
|
|
1190
1195
|
const guide = createMemo(() => {
|
|
1191
1196
|
const active = resizePreview();
|
|
1192
|
-
if (active)
|
|
1193
|
-
|
|
1197
|
+
if (active) {
|
|
1198
|
+
const rect = observedGuide(active);
|
|
1199
|
+
return rect ? { rect, active: true } : null;
|
|
1200
|
+
}
|
|
1194
1201
|
const hovered = hoveredSeparator();
|
|
1195
1202
|
return hovered
|
|
1196
1203
|
? {
|
|
@@ -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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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],
|
|
@@ -278,6 +278,22 @@ export function terminalPaneSeparators(
|
|
|
278
278
|
return Object.freeze(separators);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
/** Highlight the observed divider, never the pointer's unconfirmed target. */
|
|
282
|
+
export function terminalPaneObservedResizeGuide(
|
|
283
|
+
frames: readonly OpenTuiPaneFrame[],
|
|
284
|
+
paneBorderStatus: "top" | "bottom" | "off",
|
|
285
|
+
preview: ApplicationPaneResizePreview,
|
|
286
|
+
): ResizeGuideRect | null {
|
|
287
|
+
const separator = terminalPaneSeparators(frames, paneBorderStatus).find(
|
|
288
|
+
(candidate) =>
|
|
289
|
+
candidate.paneId === preview.semanticPaneId &&
|
|
290
|
+
candidate.axis === (preview.axis === "cols" ? "x" : "y"),
|
|
291
|
+
);
|
|
292
|
+
return separator
|
|
293
|
+
? terminalPaneResizePreview(separator, separator.position, separator.position).guide
|
|
294
|
+
: null;
|
|
295
|
+
}
|
|
296
|
+
|
|
281
297
|
export function terminalPaneResizePreview(
|
|
282
298
|
separator: ApplicationPaneSeparator,
|
|
283
299
|
pointer: number,
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
terminalAgentStatusLabel,
|
|
68
68
|
terminalPaneDisplayTitle,
|
|
69
69
|
terminalPaneResizePreview,
|
|
70
|
+
terminalPaneObservedResizeGuide,
|
|
70
71
|
terminalPaneSeparatorAt,
|
|
71
72
|
terminalPaneSeparators,
|
|
72
73
|
terminalWindowAgentIndicator,
|
|
@@ -593,15 +594,24 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
593
594
|
top: localY + 1 + height <= bottom ? localY + 1 : Math.max(topOffset(), localY - height),
|
|
594
595
|
});
|
|
595
596
|
};
|
|
596
|
-
const
|
|
597
|
-
|
|
597
|
+
const observedGuide = (preview: ApplicationPaneResizePreview) =>
|
|
598
|
+
terminalPaneObservedResizeGuide(
|
|
599
|
+
visibleFrames(),
|
|
600
|
+
layout().current?.paneBorderStatus ?? "off",
|
|
601
|
+
preview,
|
|
602
|
+
);
|
|
603
|
+
const globalPreview = (preview: ApplicationPaneResizePreview): ApplicationPaneResizePreview => {
|
|
604
|
+
const guide = observedGuide(preview) ?? preview.guide;
|
|
605
|
+
return Object.freeze({
|
|
598
606
|
...preview,
|
|
607
|
+
guide,
|
|
599
608
|
globalGuide: Object.freeze({
|
|
600
|
-
...
|
|
601
|
-
x:
|
|
602
|
-
y:
|
|
609
|
+
...guide,
|
|
610
|
+
x: guide.x + (props.originX ?? 0),
|
|
611
|
+
y: guide.y + (props.originY ?? 0) + topOffset(),
|
|
603
612
|
}),
|
|
604
613
|
});
|
|
614
|
+
};
|
|
605
615
|
// tmux retains one active pane per window even while that window is hidden.
|
|
606
616
|
// Keep those native terminal surfaces presentation-ready while the host has
|
|
607
617
|
// focus; switching the visible window then changes only composition, not
|
|
@@ -1626,7 +1636,10 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1626
1636
|
};
|
|
1627
1637
|
const guide = createMemo(() => {
|
|
1628
1638
|
const active = resizePreview();
|
|
1629
|
-
if (active)
|
|
1639
|
+
if (active) {
|
|
1640
|
+
const rect = observedGuide(active);
|
|
1641
|
+
return rect ? { rect, active: true } : null;
|
|
1642
|
+
}
|
|
1630
1643
|
const hovered = hoveredSeparator();
|
|
1631
1644
|
return hovered
|
|
1632
1645
|
? {
|