mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(transport): leave a pane's mode through the seat's address (#2644)
leavePaneMode passes the seat's session name to cancelPaneMode, as the text and Enter writes do. The guard resolves the seat and writes to its bound pane; the mode reads stay on the bound pane. Before this, the cancel addressed the raw pane ID, and when a stale binding in another rig named the same reused ID, resolveGuardTarget matched two bindings and refused the cancel (guard_target_unknown), so the send failed pane_in_mode with no text sent. A new session-transport cell covers the stale same-pane binding, asserting delivery and submission, the cancel's target, the reads on the pane, and resolveGuardTarget on both forms. Not changed: reconciling stale bindings, and a person scrolling again between leaving the mode and the Enter.
This commit is contained in:
1 parent
cfbe5372db
commit
892eb55b5e
2 files changed
+38
-1
No files matched your search
@@ -2904,7 +2904,10 @@ export class SessionTransport {
|
||||
let failure = "";
|
||||
const failed = (message: string) => { failure = ` Leaving it failed: ${message.trim().replace(/\s+/g, " ").replace(/\.$/, "")}.`; };
|
||||
try {
|
||||
const left = typeof this.tmuxAdapter.cancelPaneMode === "function" ? await this.tmuxAdapter.cancelPaneMode(readTarget) : undefined;
|
||||
// Leave through the seat's address, as the text and Enter writes do; the guard writes to its bound pane. A raw
|
||||
// pane id can match a stale binding elsewhere (tmux reuses ids after a server restart), and the guard refuses an
|
||||
// ambiguous target before writing anything, so the mode was never left.
|
||||
const left = typeof this.tmuxAdapter.cancelPaneMode === "function" ? await this.tmuxAdapter.cancelPaneMode(sessionName) : undefined;
|
||||
if (left && !left.ok) failed(left.message);
|
||||
} catch (error) { failed(error instanceof Error ? error.message : String(error)); }
|
||||
if (await readPaneInMode() !== true) return null;
|
||||
|
||||
@@ -694,6 +694,40 @@ describe("SessionTransport", () => {
|
||||
expect(callOrder).toEqual(["cancelPaneMode", "sendText", "sendKeys:Enter"]);
|
||||
});
|
||||
|
||||
it("leaves the mode through the seat's address when a stale binding elsewhere names the same pane", async () => {
|
||||
const { node } = seedCanonicalRig();
|
||||
sessionRegistry.updateBinding(node.id, { tmuxPane: "%27" });
|
||||
// tmux reuses pane ids after a server restart: another rig's binding can still name %27.
|
||||
const studio = rigRepo.createRig("studio-build");
|
||||
const stale = rigRepo.addNode(studio.id, "sdk.impl", { role: "worker", runtime: "claude-code" });
|
||||
sessionRegistry.registerSession(stale.id, "sdk-impl@studio-build");
|
||||
sessionRegistry.updateBinding(stale.id, { tmuxSession: "sdk-impl@studio-build", tmuxPane: "%27" });
|
||||
// The guard can't resolve the raw pane id to one seat, so a write addressed to it is refused before any key.
|
||||
expect(resolveGuardTarget(db, "%27")).toBeNull();
|
||||
expect(resolveGuardTarget(db, "dev-impl@my-rig")).toMatchObject({ nodeId: node.id, pane: "%27" });
|
||||
const callOrder: string[] = [];
|
||||
const tmux = modalPane({ leaves: true, callOrder });
|
||||
const modeReads: string[] = [];
|
||||
let left = false;
|
||||
tmux.isPaneInMode = vi.fn(async (target: string) => { modeReads.push(target); return !left; });
|
||||
const cancelTargets: string[] = [];
|
||||
// As the adapter's guarded write does: an unresolvable target writes nothing, so the pane stays in its mode.
|
||||
tmux.cancelPaneMode = vi.fn(async (target: string) => {
|
||||
cancelTargets.push(target); callOrder.push("cancelPaneMode");
|
||||
if (!resolveGuardTarget(db, target)) return { ok: false as const, code: "guard_target_unknown", message: `Cannot establish managed input target ${target}; no input written.` };
|
||||
left = true;
|
||||
return { ok: true as const };
|
||||
});
|
||||
const transport = createTransport(tmux);
|
||||
|
||||
const result = await transport.send("dev-impl@my-rig", "hello");
|
||||
// The person's harm first: the message is delivered and submitted, not refused as pane_in_mode.
|
||||
expect(result).toMatchObject({ ok: true });
|
||||
expect(callOrder).toEqual(["cancelPaneMode", "sendText", "sendKeys:Enter"]);
|
||||
expect(cancelTargets).toEqual(["dev-impl@my-rig"]);
|
||||
expect(modeReads.every(target => target === "%27")).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves the mode before a submitOnly Enter", async () => {
|
||||
seedCanonicalRig();
|
||||
const callOrder: string[] = [];
|
||||
|
||||
Reference in new issue
Block a user