From 99651b2d500047eaa5405b064c31543ca47240f2 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:42:27 +1000 Subject: [PATCH] fix(app): refresh queued inputs when the connection returns (#47573) --- .../src/session/session-resolution.test.ts | 56 +++++++++++++------ .../app/src/session/session-resolution.ts | 8 ++- .../test-browser/session-resolution.test.ts | 9 +++ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/app/src/session/session-resolution.test.ts b/packages/app/src/session/session-resolution.test.ts index 90426d4767f..52fed891e32 100644 --- a/packages/app/src/session/session-resolution.test.ts +++ b/packages/app/src/session/session-resolution.test.ts @@ -2,30 +2,54 @@ import { describe, expect, test } from "bun:test" import { createRoot } from "solid-js" import { createSessionResolution } from "./session-resolution" +function store() { + const syncs = { session: 0, message: 0, pending: 0 } + const sessions = { + get: () => undefined, + sync: () => { + syncs.session++ + return Promise.resolve() + }, + message: { + sync: () => { + syncs.message++ + return Promise.resolve() + }, + }, + pending: { + sync: () => { + syncs.pending++ + return Promise.resolve() + }, + }, + } + return { syncs, sessions } +} + describe("session resolution", () => { test("waits for a route session ID", () => { createRoot((dispose) => { - const syncs = { session: 0, message: 0 } - const sessions = { - get: () => undefined, - sync: () => { - syncs.session++ - return Promise.resolve() - }, - message: { - sync: () => { - syncs.message++ - return Promise.resolve() - }, - }, - } + const input = store() const session = createSessionResolution( () => undefined, - () => sessions, + () => input.sessions, ) expect(session()).toBeUndefined() - expect(syncs).toEqual({ session: 0, message: 0 }) + expect(input.syncs).toEqual({ session: 0, message: 0, pending: 0 }) + dispose() + }) + }) + + test("starts the transcript and queued input reads with metadata", () => { + createRoot((dispose) => { + const input = store() + createSessionResolution( + () => "ses_open", + () => input.sessions, + { children: true }, + ) + expect(input.syncs).toEqual({ session: 1, message: 1, pending: 1 }) dispose() }) }) diff --git a/packages/app/src/session/session-resolution.ts b/packages/app/src/session/session-resolution.ts index 7778636be6e..ec43e50091c 100644 --- a/packages/app/src/session/session-resolution.ts +++ b/packages/app/src/session/session-resolution.ts @@ -7,6 +7,9 @@ type SessionStore = { message: { sync: (id: string) => Promise } + pending: { + sync: (id: string) => Promise + } } type Resolution = { id: string; store: SessionStore } & ( @@ -52,8 +55,11 @@ export function createSessionResolution( onCleanup(() => { stale = true }) - // The timeline owns message errors; metadata resolution stays independent. + // The timeline owns message errors; metadata resolution stays independent. Queued inputs + // ride along so a reconnect refreshes them with the transcript instead of leaving the + // pre-disconnect queue on screen. void store.message.sync(id).catch(() => undefined) + void store.pending.sync(id).catch(() => undefined) if (cached() && !options?.children && !options?.connected) { setStatus({ id, store, state: "settled" }) return diff --git a/packages/app/test-browser/session-resolution.test.ts b/packages/app/test-browser/session-resolution.test.ts index 1c71c36d55d..c1851f75705 100644 --- a/packages/app/test-browser/session-resolution.test.ts +++ b/packages/app/test-browser/session-resolution.test.ts @@ -17,9 +17,11 @@ function createFixture(initial: Record = {}) { const deferred = new Map>() const resolves: string[] = [] const messages = { syncs: [] as string[], ...Promise.withResolvers() } + const pending = { syncs: [] as string[] } return { resolves, messages, + pending, sessions: { get: (id: string) => cache()[id], sync: (id: string) => { @@ -34,6 +36,12 @@ function createFixture(initial: Record = {}) { return messages.promise }, }, + pending: { + sync: (id: string) => { + pending.syncs.push(id) + return Promise.resolve() + }, + }, }, settle(id: string, directory = `/dir/${id}`) { setCache({ ...cache(), [id]: { id, directory } }) @@ -86,6 +94,7 @@ test("refreshes the current session on reconnect while keeping cached content vi expect(fixture.resolves).toEqual(["ses_a", "ses_a"]) expect(current()).toEqual(sessionOf("ses_a")) expect(fixture.messages.syncs).toEqual(["ses_a", "ses_a"]) + expect(fixture.pending.syncs).toEqual(["ses_a", "ses_a"]) fixture.settle("ses_a", "/worktrees/moved") await flush() expect(current()?.directory).toBe("/worktrees/moved")