From c572499acc307797dd94f0528cf4c22d943296cf Mon Sep 17 00:00:00 2001 From: Eivind Jonassen Date: Tue, 29 Sep 2026 12:58:06 +0200 Subject: [PATCH] Keep removed worktrees retired and simplify agent changes Treat generic event locations as discovery hints rather than outstanding attention. Probe historical directories before location initialization, and prevent refreshes from bypassing pending directory and active-session checks. Preserve fresh permission and form hints until authoritative reconciliation. Restrict steering cards to user prompts and display agent changes inline without Show/Hide controls. Cover refresh, remount, fresh attention, and internal mode reminders with regression tests. --- .../screens/session-execution-panel.test.tsx | 63 +++++++ .../src/screens/session-execution-panel.tsx | 7 +- .../mobile/src/screens/session-transcript.tsx | 11 +- .../connection-event-query-bridge.test.ts | 106 ++++++++---- .../state/connection-event-query-bridge.ts | 20 +++ .../src/state/connection-runtime-context.tsx | 9 +- .../src/state/followed-projects-context.tsx | 146 ++++++++++------ .../workspace-selection-context.test.tsx | 156 +++++++++++++++++- 8 files changed, 420 insertions(+), 98 deletions(-) diff --git a/apps/mobile/src/screens/session-execution-panel.test.tsx b/apps/mobile/src/screens/session-execution-panel.test.tsx index f35360d..121c549 100644 --- a/apps/mobile/src/screens/session-execution-panel.test.tsx +++ b/apps/mobile/src/screens/session-execution-panel.test.tsx @@ -69,6 +69,69 @@ test("renders active execution and mutable queued inbox work", () => { expect(callbacks.onCancelInbox).toHaveBeenCalledWith("msg_queued"); }); +test("agent-switch reminders do not become actionable steering cards", () => { + render( + You are in Plan mode." }, + }, + ]} + />, + ); + expect(screen.queryByText("STEERING")).toBeNull(); + expect(screen.queryByRole("button", { name: "Queue next" })).toBeNull(); + expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull(); +}); + +test("keeps a real steering prompt actionable alongside internal mode reminders", () => { + render( + , + ); + expect(screen.getAllByText("STEERING")).toHaveLength(1); + expect(screen.getByText("Please check the tests")).toBeOnTheScreen(); + fireEvent.press(screen.getByRole("button", { name: "Queue next" })); + expect(callbacks.onQueueInbox).toHaveBeenLastCalledWith("msg_followup"); + fireEvent.press(screen.getByRole("button", { name: "Cancel" })); + expect(callbacks.onCancelInbox).toHaveBeenLastCalledWith("msg_followup"); +}); + test("keeps unknown delivery visible until reconciliation finds the stable ID", () => { const admission: PromptAdmission = { durable: false, diff --git a/apps/mobile/src/screens/session-execution-panel.tsx b/apps/mobile/src/screens/session-execution-panel.tsx index 235e2d7..e7e0a1d 100644 --- a/apps/mobile/src/screens/session-execution-panel.tsx +++ b/apps/mobile/src/screens/session-execution-panel.tsx @@ -56,6 +56,9 @@ export function SessionExecutionPanel({ replyingPermissionId?: string | undefined; }) { const { height } = useWindowDimensions(); + // Agent-mode reminders and other control inputs are server-owned inbox work, + // not user prompts that should offer steering or cancellation controls. + const promptInbox = inbox.filter((item) => item.type === "user"); const inboxIds = new Set(inbox.map((item) => item.id)); const localOverlays = admissions.filter( (admission) => @@ -64,7 +67,7 @@ export function SessionExecutionPanel({ ); if ( !active && - inbox.length === 0 && + promptInbox.length === 0 && localOverlays.length === 0 && permissions.length === 0 && !formRequests @@ -166,7 +169,7 @@ export function SessionExecutionPanel({ ))} - {inbox.map((item) => ( + {promptInbox.map((item) => ( diff --git a/apps/mobile/src/screens/session-transcript.tsx b/apps/mobile/src/screens/session-transcript.tsx index b0e397d..73893c8 100644 --- a/apps/mobile/src/screens/session-transcript.tsx +++ b/apps/mobile/src/screens/session-transcript.tsx @@ -498,7 +498,16 @@ export const SessionTranscriptRow = memo(function SessionTranscriptRow({ ); case "agent-switched": - return ; + return ( + + + Agent changed to{" "} + + {sanitizeTranscriptText(capitalize(message.agent), 256)} + + + + ); case "model-switched": return ; case "location-switched": diff --git a/apps/mobile/src/state/connection-event-query-bridge.test.ts b/apps/mobile/src/state/connection-event-query-bridge.test.ts index 61ffd9f..160b3b2 100644 --- a/apps/mobile/src/state/connection-event-query-bridge.test.ts +++ b/apps/mobile/src/state/connection-event-query-bridge.test.ts @@ -340,50 +340,84 @@ test("invalidates only the affected session inbox for inbox events", () => { queryClient.clear(); }); -test("projects permission requests immediately while scheduling REST reconciliation", () => { - const queryClient = new QueryClient(); - const invalidate = jest.spyOn(queryClient, "invalidateQueries"); - const bridge = new ConnectionEventQueryBridge(queryClient, "connection-1", (callback) => - callback(), - ); - const location = { directory: "/workspace" }; - const key = openCodeQueryKeys.permissions("connection-1", location); - queryClient.setQueryData(key, { data: [], location }); - - bridge.apply({ - created: 1, - data: { - action: "shell", - id: "per_test", - resources: ["redacted command"], - sessionID: "session-1", - }, - id: "event-permission", - location, - type: "permission.asked", - }); +test.each([true, false])( + "projects permission requests while scheduling REST reconciliation, existing cache: %s", + (cached) => { + const queryClient = new QueryClient(); + const invalidate = jest.spyOn(queryClient, "invalidateQueries"); + const bridge = new ConnectionEventQueryBridge(queryClient, "connection-1", (callback) => + callback(), + ); + const location = { directory: "/workspace" }; + const key = openCodeQueryKeys.permissions("connection-1", location); + if (cached) queryClient.setQueryData(key, { data: [], location }); - expect(queryClient.getQueryData(key)).toEqual({ - data: [ - { + bridge.apply({ + created: 1, + data: { action: "shell", id: "per_test", resources: ["redacted command"], sessionID: "session-1", }, - ], - location, - }); - const query = queryClient.getQueryCache().find({ queryKey: key }); - expect(query && invalidate.mock.calls[0]?.[0]?.predicate?.(query)).toBe(true); + id: "event-permission", + location, + type: "permission.asked", + }); + + expect(queryClient.getQueryData(key)).toEqual({ + data: [ + { + action: "shell", + id: "per_test", + resources: ["redacted command"], + sessionID: "session-1", + }, + ], + location, + }); + const query = queryClient.getQueryCache().find({ queryKey: key }); + expect(query && invalidate.mock.calls[0]?.[0]?.predicate?.(query)).toBe(true); - bridge.apply({ - created: 2, - data: { reply: "once", requestID: "per_test", sessionID: "session-1" }, - id: "event-permission-replied", + bridge.apply({ + created: 2, + data: { reply: "once", requestID: "per_test", sessionID: "session-1" }, + id: "event-permission-replied", + location, + type: "permission.replied", + }); + expect(queryClient.getQueryData(key)).toEqual({ data: [], location }); + queryClient.clear(); + }, +); + +test("fresh form hints protect cold locations until authoritative reconciliation", () => { + const queryClient = new QueryClient(); + const location = { directory: "/workspace/retired" }; + const key = openCodeQueryKeys.forms("connection-1", location); + const bridge = new ConnectionEventQueryBridge(queryClient, "connection-1", (flush) => flush()); + const event = { + type: "form.created", + id: "event-form", + created: 1, location, - type: "permission.replied", - }); + data: { + form: { + id: "form_test", + sessionID: "ses_test", + title: "Input", + fields: [{ key: "answer", type: "string" }], + }, + }, + } satisfies OpenCodeEvent; + bridge.apply(event); + bridge.apply(event); + expect(queryClient.getQueryData(key)).toEqual({ data: [event.data.form], location }); + expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true); + expect( + queryClient.getQueryData(openCodeQueryKeys.forms("connection-2", location)), + ).toBeUndefined(); + queryClient.setQueryData(key, { data: [], location }); expect(queryClient.getQueryData(key)).toEqual({ data: [], location }); queryClient.clear(); }); diff --git a/apps/mobile/src/state/connection-event-query-bridge.ts b/apps/mobile/src/state/connection-event-query-bridge.ts index 351032f..f286612 100644 --- a/apps/mobile/src/state/connection-event-query-bridge.ts +++ b/apps/mobile/src/state/connection-event-query-bridge.ts @@ -1,4 +1,5 @@ import type { + FormInfo, LocationRef, OpenCodeEvent, PermissionRequest, @@ -54,6 +55,17 @@ export class ConnectionEventQueryBridge { const next = reduceActiveSessions(current, event); if (next !== current) this.queryClient.setQueryData(activeKey, next); reducePermissionQueries(this.queryClient, this.connectionId, event); + if (event.type === "form.created" && event.location) { + const form = event.data.form; + const location = event.location; + const key = openCodeQueryKeys.forms(this.connectionId, location); + this.queryClient.setQueryData<{ data: FormInfo[]; location: unknown }>(key, (current) => ({ + location: current?.location ?? location, + data: current?.data.some((item) => item.id === form.id) + ? current.data + : [...(current?.data ?? []), form], + })); + } const root = eventInvalidationRoot(event); if (root) { @@ -217,6 +229,14 @@ function reducePermissionQueries( ) { return; } + if (event.type === "permission.asked") { + const key = openCodeQueryKeys.permissions(connectionId, event.location); + if (!queryClient.getQueryData(key)) { + // A fresh request protects an otherwise retired location until the exact + // location's authoritative REST snapshot replaces this volatile hint. + queryClient.setQueryData(key, { data: [event.data], location: event.location }); + } + } const queries = queryClient.getQueryCache().findAll({ predicate: (query) => matchesRoot(query, connectionId, "permissions", event.location), }); diff --git a/apps/mobile/src/state/connection-runtime-context.tsx b/apps/mobile/src/state/connection-runtime-context.tsx index 0381d6a..c4e9e6a 100644 --- a/apps/mobile/src/state/connection-runtime-context.tsx +++ b/apps/mobile/src/state/connection-runtime-context.tsx @@ -162,7 +162,14 @@ export function ConnectionRuntimeProvider({ children }: { children: ReactNode }) bridge.apply(event); const eventLocation = event.location; if (eventLocation) { - setEventLocations((current) => appendEventLocation(current, eventLocation)); + setEventLocations((current) => { + const next = appendEventLocation(current, eventLocation); + // Reconsider retired locations when actual attention arrives, + // even if a generic event already introduced the location. + return event.type === "permission.asked" || event.type === "form.created" + ? [...next] + : next; + }); } diagnosticsRef.current = appendDiagnostic(diagnosticsRef.current, { atMs: Date.now(), diff --git a/apps/mobile/src/state/followed-projects-context.tsx b/apps/mobile/src/state/followed-projects-context.tsx index 15dde31..a317d81 100644 --- a/apps/mobile/src/state/followed-projects-context.tsx +++ b/apps/mobile/src/state/followed-projects-context.tsx @@ -345,19 +345,83 @@ export function FollowedProjectsProvider({ children }: { children: ReactNode }) }); const eventLocations = uniqueLocations(runtime.eventLocations); + const activeLocationsResolved = + activeSessionsQuery.isSuccess && + (activeSessionIDs.length === 0 || + (activeAncestryQuery.isSuccess && activeAncestryQuery.data.failures.length === 0)); + const activeLocationsPending = + activeSessionsQuery.isPending || (activeSessionIDs.length > 0 && activeAncestryQuery.isPending); + const protectedLocationKeys = new Set( + [ + ...availableFollowedProjectIDs.flatMap((projectID) => { + const project = projectByID.get(projectID); + return project ? [{ directory: project.canonical }] : []; + }), + ...runtime.attentionLocations, + ...Object.values(activeAncestryQuery.data?.sessions ?? {}).map((session) => session.location), + ...(location ? [location] : []), + ].map(locationKey), + ); + function historicalCandidate(candidate: LocationRef) { + if (protectedLocationKeys.has(locationKey(candidate)) || candidate.workspaceID) + return undefined; + const session = rootSessions.find( + (item) => locationKey(item.location) === locationKey(candidate), + ); + const projectID = session?.projectID ?? inferLocationProjectID(candidate, projects); + const project = projectID ? projectByID.get(projectID) : undefined; + if (!project || !candidate.directory.startsWith(`${project.canonical.replace(/\/$/, "")}/`)) + return undefined; + // Actual attention takes precedence over retirement; generic event locations + // are discovery hints, not evidence of outstanding work. + const permissionSnapshot = queryClient.getQueryData<{ data: PermissionRequest[] }>( + openCodeQueryKeys.permissions(connectionID ?? unresolvedConnectionID, candidate), + ); + const formSnapshot = queryClient.getQueryData<{ data: FormInfo[] }>( + openCodeQueryKeys.forms(connectionID ?? unresolvedConnectionID, candidate), + ); + if (permissionSnapshot?.data.length || formSnapshot?.data.length) return undefined; + return { location: candidate, root: { directory: project.canonical } }; + } const eventLocationsQuery = useQuery({ - enabled: Boolean(client && connectionID && eventLocations.length), + enabled: Boolean( + client && + connectionID && + eventLocations.length && + preferenceReady && + !projectsQuery.isPending && + !activeSessionsQuery.isPending && + (activeSessionIDs.length === 0 || !activeAncestryQuery.isPending), + ), queryFn: async ({ signal }) => { if (!client) throw new Error("CONNECTION_NOT_READY"); const settled = await Promise.allSettled( - eventLocations.map((eventLocation) => - getOpenCodeLocation(client, eventLocation, { signal }), - ), + eventLocations.map(async (eventLocation) => { + const candidate = historicalCandidate(eventLocation); + // Resolving a location initializes it on the server. Check through the + // live project root before touching a retired historical directory. + if (candidate && activeLocationsResolved) { + let exists: boolean | undefined; + try { + exists = await openCodeDirectoryExists( + client, + candidate.root, + eventLocation.directory, + { signal }, + ); + } catch { + // An unavailable probe is not proof that the directory is gone. + if (signal.aborted) throw new Error("LOCATION_CHECK_ABORTED"); + } + if (exists === false) return undefined; + } + return getOpenCodeLocation(client, eventLocation, { signal }); + }), ); return { failures: settled.filter((result) => result.status === "rejected").length, locations: settled.flatMap((result) => - result.status === "fulfilled" ? [result.value] : [], + result.status === "fulfilled" && result.value ? [result.value] : [], ), }; }, @@ -412,42 +476,9 @@ export function FollowedProjectsProvider({ children }: { children: ReactNode }) : []; const candidateLocations = uniqueLocations([...previousLocations, ...discoveredLocations]); discoveredLocationsRef.current = { scopeKey: followedScopeKey, locations: candidateLocations }; - const protectedLocationKeys = new Set( - [ - ...availableFollowedProjectIDs.flatMap((projectID) => { - const project = projectByID.get(projectID); - return project ? [{ directory: project.canonical }] : []; - }), - ...runtime.attentionLocations, - ...runtime.eventLocations, - ...Object.values(activeAncestryQuery.data?.sessions ?? {}).map((session) => session.location), - ...(location ? [location] : []), - ].map(locationKey), - ); const historicalCandidates = candidateLocations.flatMap((candidate) => { - if (protectedLocationKeys.has(locationKey(candidate)) || candidate.workspaceID) return []; - const session = rootSessions.find( - (item) => locationKey(item.location) === locationKey(candidate), - ); - const project = session - ? projectByID.get(session.projectID) - : projects.find( - (project) => - availableFollowedProjectIDs.includes(project.id) && - project.sandboxes.includes(candidate.directory), - ); - if (!project || !candidate.directory.startsWith(`${project.canonical.replace(/\/$/, "")}/`)) - return []; - // Previously observed blocked work takes precedence over retirement. Active - // session lists alone do not cover permission- or form-blocked sessions. - const permissionSnapshot = queryClient.getQueryData<{ data: PermissionRequest[] }>( - openCodeQueryKeys.permissions(connectionID ?? unresolvedConnectionID, candidate), - ); - const formSnapshot = queryClient.getQueryData<{ data: FormInfo[] }>( - openCodeQueryKeys.forms(connectionID ?? unresolvedConnectionID, candidate), - ); - if (permissionSnapshot?.data.length || formSnapshot?.data.length) return []; - return [{ location: candidate, root: { directory: project.canonical } }]; + const historical = historicalCandidate(candidate); + return historical ? [historical] : []; }); const directoryQueries = useQueries({ queries: historicalCandidates.map((candidate) => ({ @@ -472,22 +503,23 @@ export function FollowedProjectsProvider({ children }: { children: ReactNode }) directoryQueries[index], ]), ); - const activeLocationsResolved = - activeSessionsQuery.isSuccess && - (activeSessionIDs.length === 0 || - (activeAncestryQuery.isSuccess && activeAncestryQuery.data.failures.length === 0)); const knownLocations = candidateLocations.filter((candidate) => { const query = directoryQueryByLocation.get(locationKey(candidate)); return !activeLocationsResolved || !query?.isSuccess || query.data !== false; }); + const canCheckInteractions = (candidate: LocationRef) => { + const probe = directoryQueryByLocation.get(locationKey(candidate)); + return ( + !probe || + (!probe.isPending && + !activeLocationsPending && + !(activeLocationsResolved && probe.data === false)) + ); + }; const interactionQueries = useQueries({ queries: knownLocations.flatMap((knownLocation) => [ { - enabled: Boolean( - client && - connectionID && - !directoryQueryByLocation.get(locationKey(knownLocation))?.isPending, - ), + enabled: Boolean(client && connectionID && canCheckInteractions(knownLocation)), queryFn: ({ signal }: { signal: AbortSignal }) => { if (!client) throw new Error("CONNECTION_NOT_READY"); return listOpenCodePermissionRequests(client, knownLocation, { signal }); @@ -498,11 +530,7 @@ export function FollowedProjectsProvider({ children }: { children: ReactNode }) ), }, { - enabled: Boolean( - client && - connectionID && - !directoryQueryByLocation.get(locationKey(knownLocation))?.isPending, - ), + enabled: Boolean(client && connectionID && canCheckInteractions(knownLocation)), queryFn: ({ signal }: { signal: AbortSignal }) => { if (!client) throw new Error("CONNECTION_NOT_READY"); return listOpenCodeFormRequests(client, knownLocation, { signal }); @@ -877,7 +905,15 @@ export function FollowedProjectsProvider({ children }: { children: ReactNode }) ...(availableFollowedProjectIDs.length && normalizedSearch ? [searchSessionsQuery.refetch()] : []), - ...allInteractionQueries.map((query) => query.refetch()), + // refetch() bypasses enabled. Do not initialize a historical + // location while its renewed directory probe is still pending. + ...knownLocations.flatMap((knownLocation, index) => { + if (!canCheckInteractions(knownLocation)) return []; + return interactionQueries + .slice(index * 2, index * 2 + 2) + .map((query) => query.refetch()); + }), + ...supplementalInteractionQueries.map((query) => query.refetch()), ]), replyPermission, ...(permissionReplyMutation.isPending && permissionReplyMutation.variables diff --git a/apps/mobile/src/state/workspace-selection-context.test.tsx b/apps/mobile/src/state/workspace-selection-context.test.tsx index 6e3f633..a78c143 100644 --- a/apps/mobile/src/state/workspace-selection-context.test.tsx +++ b/apps/mobile/src/state/workspace-selection-context.test.tsx @@ -8,7 +8,7 @@ import type { import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen, waitFor } from "@testing-library/react-native"; import { Pressable, Text } from "react-native"; - +import { ConnectionEventQueryBridge } from "./connection-event-query-bridge"; import { FollowedProjectsProvider, useFollowedProjects } from "./followed-projects-context"; import { openCodeQueryKeys } from "./open-code-query-keys"; @@ -628,16 +628,166 @@ test.each([false, true])( }, ); -test.each(["notification", "event", "cached-form"])( +test("does not resurrect a retired worktree from generic event hints when returning to the project", async () => { + const directory = "/b/.worktree/retired"; + mockListProjectSessions.mockImplementation(async (_client, projectID) => ({ + cursor: {}, + data: [ + projectID === "project-b" + ? mockSession("ses_retired", projectID, directory, 2) + : mockSession("ses_alpha", projectID, "/a", 3), + ], + })); + mockDirectoryExists.mockImplementation( + async (_client, _root, candidate) => candidate !== directory, + ); + const permissions = mockListPermissions.getMockImplementation(); + mockListPermissions.mockImplementation(async (client, location, options) => { + if (location.directory === directory) throw new Error("Location no longer exists"); + if (!permissions) throw new Error("fixture"); + return permissions(client, location, options); + }); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: Infinity } }, + }); + const tree = () => ( + + + + + + ); + let view = render(tree()); + try { + await waitFor(() => expect(screen.getByText("complete:1:1:0")).toBeOnTheScreen()); + mockEventLocations = [{ directory }]; + mockRevision += 1; + view.rerender(tree()); + fireEvent.press(screen.getByRole("button", { name: "Refresh sessions" })); + await waitFor(() => expect(screen.getByText("freshness:current")).toBeOnTheScreen()); + expect(screen.queryByText(/^unavailable:/)).toBeNull(); + expect(mockListPermissions.mock.calls.some((call) => call[1].directory === directory)).toBe( + false, + ); + expect(mockGetOpenCodeLocation.mock.calls.some((call) => call[1].directory === directory)).toBe( + false, + ); + view.unmount(); + view = render(tree()); + await waitFor(() => expect(screen.getByText("complete:1:1:0")).toBeOnTheScreen()); + expect(screen.queryByText(/^unavailable:/)).toBeNull(); + expect(mockListPermissions.mock.calls.some((call) => call[1].directory === directory)).toBe( + false, + ); + expect(mockGetOpenCodeLocation.mock.calls.some((call) => call[1].directory === directory)).toBe( + false, + ); + } finally { + view.unmount(); + queryClient.clear(); + } +}); + +test("fresh attention can reopen a retired location and an empty REST snapshot retires it again", async () => { + const directory = "/b/.worktree/retired"; + mockEventLocations = [{ directory }]; + mockDirectoryExists.mockImplementation( + async (_client, _root, candidate) => candidate !== directory, + ); + mockListProjectSessions.mockImplementation(async (_client, projectID) => ({ + cursor: {}, + data: [ + projectID === "project-b" + ? mockSession("ses_retired", projectID, directory, 2) + : mockSession("ses_alpha", projectID, "/a", 3), + ], + })); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: Infinity } }, + }); + const tree = () => ( + + + + + + ); + const view = render(tree()); + try { + await waitFor(() => expect(screen.getByText("complete:1:1:0")).toBeOnTheScreen()); + expect(mockListForms.mock.calls.some((call) => call[1].directory === directory)).toBe(false); + const bridge = new ConnectionEventQueryBridge(queryClient, "connection-1", (flush) => flush()); + bridge.apply({ + type: "form.created", + id: "evt_new", + created: 2, + location: { directory }, + data: { + form: { + id: "form_new", + sessionID: "ses_retired", + title: "Input", + fields: [{ key: "answer", type: "string" }], + }, + }, + }); + view.rerender(tree()); + await waitFor(() => + expect(mockListForms.mock.calls.some((call) => call[1].directory === directory)).toBe(true), + ); + await waitFor(() => expect(screen.getByText("complete:1:1:0")).toBeOnTheScreen()); + const calls = mockListForms.mock.calls.filter((call) => call[1].directory === directory).length; + view.rerender(tree()); + fireEvent.press(screen.getByRole("button", { name: "Refresh sessions" })); + await waitFor(() => expect(screen.getByText("freshness:current")).toBeOnTheScreen()); + expect(mockListForms.mock.calls.filter((call) => call[1].directory === directory)).toHaveLength( + calls, + ); + } finally { + view.unmount(); + queryClient.clear(); + } +}); + +test.each(["notification", "permission-event", "form-event", "cached-form"])( "keeps %s attention at a missing historical directory", async (source) => { const location = { directory: "/b/sub" }; mockDirectoryExists.mockResolvedValue(false); if (source === "notification") mockAttentionLocations = [location]; - if (source === "event") mockEventLocations = [location]; const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } }, }); + if (source === "permission-event" || source === "form-event") { + mockEventLocations = [location]; + const bridge = new ConnectionEventQueryBridge(queryClient, "connection-1", (flush) => + flush(), + ); + bridge.apply( + source === "permission-event" + ? { + type: "permission.asked", + id: "evt_attention", + created: 1, + location, + data: { id: "per_beta", sessionID: "ses_beta", action: "shell", resources: [] }, + } + : { + type: "form.created", + id: "evt_attention", + created: 1, + location, + data: { + form: { + fields: [{ key: "answer", type: "string" }], + id: "form_beta", + sessionID: "ses_beta", + title: "Input", + }, + }, + }, + ); + } if (source === "cached-form") { queryClient.setQueryData(openCodeQueryKeys.forms("connection-1", location), { data: [{ fields: [], id: "form_beta", sessionID: "ses_beta", title: "Input" }],