Terminal: a shell that ends while its panel is shown again is not replaced
ci / ui (push) Waiting to run
ci / check (push) Failing after 3s
ci / start (push) Failing after 2s

Showing the panel connects a frame later (the panel needs its width first). A shell that ended
before that frame -- reopened and closed by the server within one frame, as headless CI's slow
frames made happen -- was quietly replaced by a new one, against "no reconnect of its own".
The frame now connects only if no shell was open as the showing began. The cause of the
B3 failures on GitHub since 94e903e; a test makes it happen every time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 13:14:23 +00:00
1 parent effcc2c2fd
commit d4a1f13cf1
2 files changed
+23 -1

No files matched your search

+19
View File
@@ -529,6 +529,25 @@ test("B3: an open agent chat opens its own shell from the header; Send to chat f
assert.deepEqual(p.errors, []) assert.deepEqual(p.errors, [])
}) })
test("B3: a shell that ends while its panel is being shown again is not replaced by a new one", async () => {
const p = await page(b3())
await openChatRow(p, "chat2")
await p.click('#chat-tools [data-terminal-toggle]')
await p.waitForFunction(() => window.__mock.terminals().length === 1)
await p.waitForFunction(() => document.querySelector("#terminal-message").textContent === "Connected.")
await p.click("#terminal-close")
// Shown again, and the shell ends before the panel has drawn its first frame.
await p.evaluate(() => {
document.querySelector("#chat-tools [data-terminal-toggle]").click()
window.__mock.shell("control", { t: "closed", message: "The shell exited." })
})
await p.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))))
await p.waitForTimeout(200)
assert.equal(await p.evaluate(() => window.__mock.terminals().length), 1, "no new shell in its place")
assert.equal(await p.textContent("#terminal-message"), "The shell exited.")
assert.deepEqual(p.errors, [])
})
test("B3: the terminal and the activity panel take turns in the panel column", async () => { test("B3: the terminal and the activity panel take turns in the panel column", async () => {
const p = await page(b3({ "GET /chats/chat2/activity": { activity: {} } })) const p = await page(b3({ "GET /chats/chat2/activity": { activity: {} } }))
await openChatRow(p, "chat2") await openChatRow(p, "chat2")
+4 -1
View File
@@ -280,6 +280,9 @@ export async function showTerminal(on) {
ctx.layout(on) ctx.layout(on)
markOpen() markOpen()
if (!on) return if (!on) return
// Whether this showing is to open a shell: none was open as it began. One that ends meanwhile
// (before the frame below) is said, not quietly replaced.
const wanted = !session
try { try {
await load() await load()
} catch { } catch {
@@ -291,7 +294,7 @@ export async function showTerminal(on) {
// A frame later: the panel has only just been shown and has no width to fit to yet. // A frame later: the panel has only just been shown and has no width to fit to yet.
requestAnimationFrame(() => { requestAnimationFrame(() => {
refit() refit()
connect() if (wanted && terminalOpen()) connect()
term.focus() term.focus()
}) })
} }