Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton 9f279cafbc fix(tui): reconcile shells after reconnect 2026-07-09 22:24:18 -04:00
2 changed files with 23 additions and 5 deletions
+14 -4
View File
@@ -50,6 +50,7 @@ type LocationData = {
reference?: ReferenceInfo[] reference?: ReferenceInfo[]
// Currently running shell commands for this location, keyed by shell id. Entries are removed // Currently running shell commands for this location, keyed by shell id. Entries are removed
// once the command exits or is deleted, so this only ever holds in-flight shells. // once the command exits or is deleted, so this only ever holds in-flight shells.
shellLocation?: LocationRef
shell?: Record<string, Shell> shell?: Record<string, Shell>
skill?: SkillInfo[] skill?: SkillInfo[]
} }
@@ -754,6 +755,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
case "shell.created": case "shell.created":
setStore("location", locationKey(event.location ?? defaultLocation()), (data) => ({ setStore("location", locationKey(event.location ?? defaultLocation()), (data) => ({
...data, ...data,
shellLocation: event.location ?? defaultLocation(),
shell: { ...data?.shell, [event.data.info.id]: event.data.info }, shell: { ...data?.shell, [event.data.info.id]: event.data.info },
})) }))
break break
@@ -901,6 +903,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
const key = locationKey(result.location) const key = locationKey(result.location)
setStore("location", key, { setStore("location", key, {
...store.location[key], ...store.location[key],
shellLocation: { directory: result.location.directory, workspaceID: result.location.workspaceID },
shell: Object.fromEntries(result.data.map((info) => [info.id, info])), shell: Object.fromEntries(result.data.map((info) => [info.id, info])),
}) })
}, },
@@ -1066,12 +1069,19 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
), ),
) )
const refreshed = await Promise.allSettled( const refreshed = await Promise.allSettled(
Array.from(locations) [
.filter(([location]) => location !== key) ...Array.from(locations)
.map(([, location]) => result.session.form.refresh("global", location)), .filter(([location]) => location !== key)
.map(([, location]) => result.session.form.refresh("global", location)),
...Object.values(store.location).flatMap((data) =>
data.shellLocation && data.shell && locationKey(data.shellLocation) !== key
? [result.shell.refresh(data.shellLocation)]
: [],
),
],
) )
for (const failure of refreshed.filter((item) => item.status === "rejected")) for (const failure of refreshed.filter((item) => item.status === "rejected"))
console.error("Failed to refresh global forms", failure.reason) console.error("Failed to refresh location data", failure.reason)
}) })
.finally(() => { .finally(() => {
bootstrapping = undefined bootstrapping = undefined
+9 -1
View File
@@ -1315,15 +1315,18 @@ test("refreshes references after updates", async () => {
test("keeps shell state scoped to location", async () => { test("keeps shell state scoped to location", async () => {
const events = createEventStream() const events = createEventStream()
const other = "/tmp/opencode/other" const other = "/tmp/opencode/other"
let otherRequests = 0
let otherRunning = true
const calls = createFetch((url) => { const calls = createFetch((url) => {
if (url.pathname !== "/api/shell") return if (url.pathname !== "/api/shell") return
const requestDirectory = url.searchParams.get("location[directory]") const requestDirectory = url.searchParams.get("location[directory]")
if (requestDirectory === other) otherRequests++
return json({ return json({
location: { location: {
directory: requestDirectory ?? directory, directory: requestDirectory ?? directory,
project: { id: "proj_test", directory: requestDirectory ?? directory }, project: { id: "proj_test", directory: requestDirectory ?? directory },
}, },
data: [ data: requestDirectory === other && !otherRunning ? [] : [
{ {
id: requestDirectory === other ? "sh_other" : "sh_default", id: requestDirectory === other ? "sh_other" : "sh_default",
status: "running", status: "running",
@@ -1383,6 +1386,11 @@ test("keeps shell state scoped to location", async () => {
}) })
await wait(() => data.shell.list({ directory: other }).some((shell) => shell.id === "sh_live_other")) await wait(() => data.shell.list({ directory: other }).some((shell) => shell.id === "sh_live_other"))
expect(data.shell.list().map((shell) => shell.id)).toEqual(["sh_default"]) expect(data.shell.list().map((shell) => shell.id)).toEqual(["sh_default"])
otherRunning = false
events.disconnect()
await wait(() => otherRequests === 2, 4000)
await wait(() => data.shell.list({ directory: other }).length === 0)
} finally { } finally {
app.renderer.destroy() app.renderer.destroy()
} }