From 1ad7845f51320c9be258626270fee32079616e27 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Fri, 4 Sep 2026 19:38:09 +0100 Subject: [PATCH] fix(maestro): refresh attach header when a session is renamed Header name/command were baked into innerHTML at connection creation and never revisited. ConnectionPool.mount() reused the cached connection on later calls but dropped the fresh info, so a header never picked up haiku autoname's rename. --- web/src/attach.ts | 25 +++++++++++++++++++------ web/src/connections.test.ts | 1 + web/src/connections.ts | 1 + 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/web/src/attach.ts b/web/src/attach.ts index 948d633..1886642 100644 --- a/web/src/attach.ts +++ b/web/src/attach.ts @@ -43,6 +43,10 @@ export interface AttachConnection { * handling — the leader's literal Ctrl-K escape hatch * (docs/COMMAND-BAR.md). */ sendInput(data: string): void; + /** Refresh the header's name/command (e.g. after autoname renames the + * session) — the connection outlives mount/unmount, so this is the only + * way an already-open header picks up a new name. */ + updateInfo(info: AttachInfo): void; /** Fully disconnect: close the WS, dispose the terminal, remove the element. */ dispose(): void; } @@ -85,21 +89,25 @@ export function createAttachConnection( el.className = "attach"; // Useful identity over a bare UUID: name · command. The full id stays in the // tooltip for when you really need it. - const titleHtml = info - ? `${escapeHtml(info.name)}` + - (info.command && info.command !== info.name ? commandFragment(info.command) : "") - : `session ${escapeHtml(sessionId.slice(0, 8))}`; - const titleAttr = info ? `${info.name} · ${info.command} (${sessionId})` : sessionId; + const titleMarkup = (i?: AttachInfo): { html: string; attr: string } => ({ + html: i + ? `${escapeHtml(i.name)}` + + (i.command && i.command !== i.name ? commandFragment(i.command) : "") + : `session ${escapeHtml(sessionId.slice(0, 8))}`, + attr: i ? `${i.name} · ${i.command} (${sessionId})` : sessionId, + }); + const initialTitle = titleMarkup(info); el.innerHTML = `
- ${titleHtml} + ${initialTitle.html} connecting…
`; + const titleEl = el.querySelector(".attach-title") as HTMLSpanElement; const body = el.querySelector(".attach-body") as HTMLDivElement; const stateEl = el.querySelector(".attach-state") as HTMLSpanElement; const disconnectBtn = el.querySelector(".disconnect") as HTMLButtonElement; @@ -499,6 +507,11 @@ export function createAttachConnection( // The closure above: the same bytes-out path keystrokes and the mobile key // bar already take. sendInput, + updateInfo(next: AttachInfo) { + const { html, attr } = titleMarkup(next); + titleEl.innerHTML = html; + titleEl.title = attr; + }, dispose() { if (disposed) return; disposed = true; diff --git a/web/src/connections.test.ts b/web/src/connections.test.ts index 9464504..54cb3ec 100644 --- a/web/src/connections.test.ts +++ b/web/src/connections.test.ts @@ -16,6 +16,7 @@ vi.mock("./attach.js", () => ({ sendInput(data: string) { sent.push({ id, data }); }, + updateInfo() {}, dispose() {}, }), })); diff --git a/web/src/connections.ts b/web/src/connections.ts index 1f17afe..c78d0a5 100644 --- a/web/src/connections.ts +++ b/web/src/connections.ts @@ -79,6 +79,7 @@ export class ConnectionPool { if (conn) { this.cache.delete(id); this.cache.set(id, conn); + if (info) conn.updateInfo(info); } else { conn = createAttachConnection( id,