Fix optimistic workspace creation on Android

This commit is contained in:
Mohamed Boudra
2026-04-22 11:03:35 +07:00
parent 7b9cc3d423
commit 42cdabdf57
30 changed files with 1533 additions and 165 deletions
@@ -112,10 +112,18 @@ describe("navigation active workspace store", () => {
serverId: "server-1",
workspaceId: "workspace-a",
});
expect(store.getLastNavigationWorkspaceRouteSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-a",
});
store.syncNavigationActiveWorkspace(createNavigationPathRef("/h/server-1/sessions"));
expect(store.getNavigationActiveWorkspaceSelection()).toBeNull();
expect(store.getLastNavigationWorkspaceRouteSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-a",
});
});
it("clears stale workspace params when navigation sync reports a non-workspace path", async () => {
@@ -134,4 +142,35 @@ describe("navigation active workspace store", () => {
expect(store.getNavigationActiveWorkspaceSelection()).toBeNull();
});
it("uses a one-shot workspace route override when returning to a retained shell", async () => {
installWindowStub("/h/server-1/workspace/workspace-a");
const store = await import("@/stores/navigation-active-workspace-store");
store.syncNavigationActiveWorkspace(
createNavigationPathRef("/h/server-1/workspace/workspace-a"),
);
store.overrideNextNavigationWorkspaceRouteSelection({
serverId: "server-1",
workspaceId: "workspace-b",
});
store.syncNavigationActiveWorkspace(
createNavigationPathRef("/h/server-1/workspace/workspace-a"),
);
expect(store.getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-b",
});
store.syncNavigationActiveWorkspace(
createNavigationPathRef("/h/server-1/workspace/workspace-a"),
);
expect(store.getNavigationActiveWorkspaceSelection()).toEqual({
serverId: "server-1",
workspaceId: "workspace-a",
});
});
});
@@ -39,6 +39,8 @@ interface NavigationObserverRef {
}
let snapshot: ActiveWorkspaceSelection | null = null;
let lastWorkspaceRouteSelection: ActiveWorkspaceSelection | null = null;
let nextWorkspaceRouteSelectionOverride: ActiveWorkspaceSelection | null = null;
const listeners = new Set<() => void>();
function subscribe(listener: () => void): () => void {
@@ -165,7 +167,18 @@ function getActiveWorkspaceForNavigationSync(
}
export function syncNavigationActiveWorkspace(navigationRef: NavigationObserverRef) {
emitIfChanged(getActiveWorkspaceForNavigationSync(navigationRef.current?.getCurrentRoute()));
const route = navigationRef.current?.getCurrentRoute();
const routeState = classifyNavigationWorkspaceRoute(route);
if (routeState.kind === "workspace") {
lastWorkspaceRouteSelection = routeState.selection;
if (nextWorkspaceRouteSelectionOverride) {
const overrideSelection = nextWorkspaceRouteSelectionOverride;
nextWorkspaceRouteSelectionOverride = null;
emitIfChanged(overrideSelection);
return;
}
}
emitIfChanged(getActiveWorkspaceForNavigationSync(route));
}
export function activateNavigationWorkspaceSelection(
@@ -180,6 +193,14 @@ export function getNavigationActiveWorkspaceSelection(): ActiveWorkspaceSelectio
return getSnapshot();
}
export function getLastNavigationWorkspaceRouteSelection(): ActiveWorkspaceSelection | null {
return lastWorkspaceRouteSelection;
}
export function overrideNextNavigationWorkspaceRouteSelection(next: ActiveWorkspaceSelection) {
nextWorkspaceRouteSelectionOverride = next;
}
export function syncBrowserActiveWorkspaceFromLocation() {
emitIfChanged(getBrowserLocationWorkspace());
}
@@ -0,0 +1,66 @@
import { create } from "zustand";
import type { ComposerAttachment } from "@/attachments/types";
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
export interface PendingWorkspaceDraftSubmission {
serverId: string;
workspaceId: string;
draftId: string;
text: string;
attachments: ComposerAttachment[];
cwd: string;
provider: AgentProvider;
modeId?: string;
model?: string;
thinkingOptionId?: string;
featureValues?: Record<string, unknown>;
allowEmptyText?: boolean;
}
type WorkspaceDraftSubmissionState = {
pendingByDraftId: Record<string, PendingWorkspaceDraftSubmission>;
setPending: (submission: PendingWorkspaceDraftSubmission) => void;
consumePending: (input: {
serverId: string;
workspaceId: string;
draftId: string;
}) => PendingWorkspaceDraftSubmission | null;
};
function matchesPendingSubmission(
pending: PendingWorkspaceDraftSubmission | null | undefined,
input: { serverId: string; workspaceId: string; draftId: string },
): pending is PendingWorkspaceDraftSubmission {
return (
pending?.serverId === input.serverId &&
pending.workspaceId === input.workspaceId &&
pending.draftId === input.draftId
);
}
export const useWorkspaceDraftSubmissionStore = create<WorkspaceDraftSubmissionState>(
(set, get) => ({
pendingByDraftId: {},
setPending: (submission) =>
set((state) => ({
pendingByDraftId: {
...state.pendingByDraftId,
[submission.draftId]: submission,
},
})),
consumePending: (input) => {
const pending = get().pendingByDraftId[input.draftId];
if (!matchesPendingSubmission(pending, input)) {
return null;
}
set((state) => {
if (!matchesPendingSubmission(state.pendingByDraftId[input.draftId], input)) {
return state;
}
const { [input.draftId]: _removed, ...rest } = state.pendingByDraftId;
return { pendingByDraftId: rest };
});
return pending;
},
}),
);