diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 26ee2f0481..524c69164a 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -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 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 diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 7f675dd40c..dff26f4c78 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -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() }