Compare 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[]
// 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.
shellLocation?: LocationRef
shell?: Record<string, Shell>
skill?: SkillInfo[]
}
@@ -754,6 +755,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
case "shell.created":
setStore("location", locationKey(event.location ?? defaultLocation()), (data) => ({
...data,
shellLocation: event.location ?? defaultLocation(),
shell: { ...data?.shell, [event.data.info.id]: event.data.info },
}))
break
@@ -901,6 +903,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
const key = locationKey(result.location)
setStore("location", key, {
...store.location[key],
shellLocation: { directory: result.location.directory, workspaceID: result.location.workspaceID },
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(
Array.from(locations)
.filter(([location]) => location !== key)
.map(([, location]) => result.session.form.refresh("global", location)),
[
...Array.from(locations)
.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"))
console.error("Failed to refresh global forms", failure.reason)
console.error("Failed to refresh location data", failure.reason)
})
.finally(() => {
bootstrapping = undefined
+9 -1
View File
@@ -1315,15 +1315,18 @@ test("refreshes references after updates", async () => {
test("keeps shell state scoped to location", async () => {
const events = createEventStream()
const other = "/tmp/opencode/other"
let otherRequests = 0
let otherRunning = true
const calls = createFetch((url) => {
if (url.pathname !== "/api/shell") return
const requestDirectory = url.searchParams.get("location[directory]")
if (requestDirectory === other) otherRequests++
return json({
location: {
directory: requestDirectory ?? directory,
project: { id: "proj_test", directory: requestDirectory ?? directory },
},
data: [
data: requestDirectory === other && !otherRunning ? [] : [
{
id: requestDirectory === other ? "sh_other" : "sh_default",
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"))
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 {
app.renderer.destroy()
}