fix: lock workspace navigation tab sync
This commit is contained in:
@@ -27,10 +27,15 @@ type ArchiveTabDaemonClient = {
|
|||||||
modeId: string;
|
modeId: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
title: string;
|
title: string;
|
||||||
initialPrompt: string;
|
initialPrompt?: string;
|
||||||
}): Promise<{ id: string }>;
|
}): Promise<{ id: string }>;
|
||||||
archiveAgent(agentId: string): Promise<{ archivedAt: string }>;
|
archiveAgent(agentId: string): Promise<{ archivedAt: string }>;
|
||||||
waitForFinish(agentId: string, timeout?: number): Promise<{ status: string }>;
|
waitForFinish(agentId: string, timeout?: number): Promise<{ status: string }>;
|
||||||
|
waitForAgentUpsert(
|
||||||
|
agentId: string,
|
||||||
|
predicate: (snapshot: { status: string }) => boolean,
|
||||||
|
timeout?: number,
|
||||||
|
): Promise<{ status: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDaemonPort(): string {
|
function getDaemonPort(): string {
|
||||||
@@ -110,16 +115,17 @@ export async function createIdleAgent(
|
|||||||
const created = await client.createAgent({
|
const created = await client.createAgent({
|
||||||
provider: "opencode",
|
provider: "opencode",
|
||||||
model: "opencode/gpt-5-nano",
|
model: "opencode/gpt-5-nano",
|
||||||
modeId: "default",
|
modeId: "bypassPermissions",
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
title: input.title,
|
title: input.title,
|
||||||
initialPrompt: "Reply with exactly READY.",
|
|
||||||
});
|
});
|
||||||
const finished = await client.waitForFinish(created.id, 120_000);
|
const snapshot = await client.waitForAgentUpsert(
|
||||||
if (finished.status !== "idle") {
|
created.id,
|
||||||
throw new Error(
|
(agent) => agent.status === "idle",
|
||||||
`Expected agent ${created.id} to become idle, got ${finished.status}. Error: ${JSON.stringify((finished as Record<string, unknown>).error ?? "unknown")}`,
|
30_000,
|
||||||
);
|
);
|
||||||
|
if (snapshot.status !== "idle") {
|
||||||
|
throw new Error(`Expected agent ${created.id} to become idle, got ${snapshot.status}.`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: created.id,
|
id: created.id,
|
||||||
|
|||||||
@@ -22,6 +22,28 @@ export async function waitForWorkspaceTabsVisible(page: Page): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getVisibleWorkspaceAgentTabIds(page: Page): Promise<string[]> {
|
||||||
|
const allTabIds = await getWorkspaceTabTestIds(page);
|
||||||
|
return allTabIds.filter((id) => id.startsWith("workspace-tab-agent_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectOnlyWorkspaceAgentTabsVisible(
|
||||||
|
page: Page,
|
||||||
|
expectedAgentIds: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
const expected = new Set(expectedAgentIds.map((id) => `workspace-tab-agent_${id}`));
|
||||||
|
const visible = await getVisibleWorkspaceAgentTabIds(page);
|
||||||
|
const unexpected = visible.filter((id) => !expected.has(id));
|
||||||
|
|
||||||
|
expect(unexpected).toEqual([]);
|
||||||
|
expect(visible.length).toBe(expected.size);
|
||||||
|
for (const expectedId of expectedAgentIds) {
|
||||||
|
await expect(page.getByTestId(`workspace-tab-agent_${expectedId}`)).toBeVisible({
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureWorkspaceAgentPaneVisible(page: Page): Promise<void> {
|
export async function ensureWorkspaceAgentPaneVisible(page: Page): Promise<void> {
|
||||||
const toggle = page.getByTestId("workspace-explorer-toggle").first();
|
const toggle = page.getByTestId("workspace-explorer-toggle").first();
|
||||||
if (!(await toggle.isVisible().catch(() => false))) {
|
if (!(await toggle.isVisible().catch(() => false))) {
|
||||||
|
|||||||
@@ -46,6 +46,29 @@ function workspaceRowLocator(page: Page, serverId: string, workspacePath: string
|
|||||||
return page.locator(ids.join(",")).first();
|
return page.locator(ids.join(",")).first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function expectSidebarWorkspaceSelected(input: {
|
||||||
|
page: Page;
|
||||||
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
selected?: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
const row = workspaceRowLocator(input.page, input.serverId, input.workspaceId);
|
||||||
|
await expect(row).toBeVisible({ timeout: 30_000 });
|
||||||
|
const expected = input.selected === false ? "false" : "true";
|
||||||
|
|
||||||
|
const hasDataSelected = await row.getAttribute("data-selected");
|
||||||
|
if (hasDataSelected !== null) {
|
||||||
|
await expect(row).toHaveAttribute("data-selected", expected, {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(row).toHaveAttribute("aria-selected", expected, {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function switchWorkspaceViaSidebar(input: {
|
export async function switchWorkspaceViaSidebar(input: {
|
||||||
page: Page;
|
page: Page;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||||
|
import { expect, test } from "./fixtures";
|
||||||
|
import { gotoAppShell } from "./helpers/app";
|
||||||
|
import {
|
||||||
|
archiveAgentFromDaemon,
|
||||||
|
connectArchiveTabDaemonClient,
|
||||||
|
createIdleAgent,
|
||||||
|
expectWorkspaceTabHidden,
|
||||||
|
expectWorkspaceTabVisible,
|
||||||
|
openWorkspaceWithAgents,
|
||||||
|
} from "./helpers/archive-tab";
|
||||||
|
import {
|
||||||
|
archiveLocalWorkspaceFromDaemon,
|
||||||
|
connectNewWorkspaceDaemonClient,
|
||||||
|
openProjectViaDaemon,
|
||||||
|
} from "./helpers/new-workspace";
|
||||||
|
import { createTempGitRepo } from "./helpers/workspace";
|
||||||
|
import {
|
||||||
|
getVisibleWorkspaceAgentTabIds,
|
||||||
|
expectOnlyWorkspaceAgentTabsVisible,
|
||||||
|
waitForWorkspaceTabsVisible,
|
||||||
|
} from "./helpers/workspace-tabs";
|
||||||
|
import {
|
||||||
|
expectSidebarWorkspaceSelected,
|
||||||
|
expectWorkspaceHeader,
|
||||||
|
switchWorkspaceViaSidebar,
|
||||||
|
waitForSidebarHydration,
|
||||||
|
} from "./helpers/workspace-ui";
|
||||||
|
|
||||||
|
test.describe("Workspace navigation regression", () => {
|
||||||
|
test.describe.configure({ timeout: 240_000 });
|
||||||
|
|
||||||
|
test("sidebar navigation and reload keep workspace selection and tabs aligned", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const serverId = process.env.E2E_SERVER_ID;
|
||||||
|
if (!serverId) {
|
||||||
|
throw new Error("E2E_SERVER_ID is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceClient = await connectNewWorkspaceDaemonClient();
|
||||||
|
const archiveClient = await connectArchiveTabDaemonClient();
|
||||||
|
const workspaceIds = new Set<string>();
|
||||||
|
const agentIds: string[] = [];
|
||||||
|
const firstRepo = await createTempGitRepo("workspace-nav-reg-a-");
|
||||||
|
const secondRepo = await createTempGitRepo("workspace-nav-reg-b-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const firstWorkspace = await openProjectViaDaemon(workspaceClient, firstRepo.path);
|
||||||
|
const secondWorkspace = await openProjectViaDaemon(workspaceClient, secondRepo.path);
|
||||||
|
workspaceIds.add(firstWorkspace.workspaceId);
|
||||||
|
workspaceIds.add(secondWorkspace.workspaceId);
|
||||||
|
|
||||||
|
const firstAgent = await createIdleAgent(archiveClient, {
|
||||||
|
cwd: firstRepo.path,
|
||||||
|
title: `workspace-nav-a-${Date.now()}`,
|
||||||
|
});
|
||||||
|
const secondAgent = await createIdleAgent(archiveClient, {
|
||||||
|
cwd: secondRepo.path,
|
||||||
|
title: `workspace-nav-b-${Date.now()}`,
|
||||||
|
});
|
||||||
|
agentIds.push(firstAgent.id, secondAgent.id);
|
||||||
|
|
||||||
|
await gotoAppShell(page);
|
||||||
|
await waitForSidebarHydration(page);
|
||||||
|
await openWorkspaceWithAgents(page, [firstAgent, secondAgent]);
|
||||||
|
|
||||||
|
await switchWorkspaceViaSidebar({
|
||||||
|
page,
|
||||||
|
serverId,
|
||||||
|
targetWorkspacePath: firstWorkspace.workspaceId,
|
||||||
|
});
|
||||||
|
await waitForWorkspaceTabsVisible(page);
|
||||||
|
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, firstWorkspace.workspaceId), {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
await expectSidebarWorkspaceSelected({
|
||||||
|
page,
|
||||||
|
serverId,
|
||||||
|
workspaceId: firstWorkspace.workspaceId,
|
||||||
|
});
|
||||||
|
await expectSidebarWorkspaceSelected({
|
||||||
|
page,
|
||||||
|
serverId,
|
||||||
|
workspaceId: secondWorkspace.workspaceId,
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
await expectWorkspaceHeader(page, {
|
||||||
|
title: firstWorkspace.workspaceName,
|
||||||
|
subtitle: firstWorkspace.projectDisplayName,
|
||||||
|
});
|
||||||
|
await expectWorkspaceTabVisible(page, firstAgent.id);
|
||||||
|
await expectWorkspaceTabHidden(page, secondAgent.id);
|
||||||
|
await expectOnlyWorkspaceAgentTabsVisible(page, [firstAgent.id]);
|
||||||
|
await expect(getVisibleWorkspaceAgentTabIds(page)).resolves.toEqual([
|
||||||
|
`workspace-tab-agent_${firstAgent.id}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await switchWorkspaceViaSidebar({
|
||||||
|
page,
|
||||||
|
serverId,
|
||||||
|
targetWorkspacePath: secondWorkspace.workspaceId,
|
||||||
|
});
|
||||||
|
await waitForWorkspaceTabsVisible(page);
|
||||||
|
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, secondWorkspace.workspaceId), {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
await expectSidebarWorkspaceSelected({
|
||||||
|
page,
|
||||||
|
serverId,
|
||||||
|
workspaceId: secondWorkspace.workspaceId,
|
||||||
|
});
|
||||||
|
await expectSidebarWorkspaceSelected({
|
||||||
|
page,
|
||||||
|
serverId,
|
||||||
|
workspaceId: firstWorkspace.workspaceId,
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
await expectWorkspaceHeader(page, {
|
||||||
|
title: secondWorkspace.workspaceName,
|
||||||
|
subtitle: secondWorkspace.projectDisplayName,
|
||||||
|
});
|
||||||
|
await expectWorkspaceTabVisible(page, secondAgent.id);
|
||||||
|
await expectWorkspaceTabHidden(page, firstAgent.id);
|
||||||
|
await expectOnlyWorkspaceAgentTabsVisible(page, [secondAgent.id]);
|
||||||
|
await expect(getVisibleWorkspaceAgentTabIds(page)).resolves.toEqual([
|
||||||
|
`workspace-tab-agent_${secondAgent.id}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await waitForSidebarHydration(page);
|
||||||
|
await waitForWorkspaceTabsVisible(page);
|
||||||
|
await expect(page).toHaveURL(buildHostWorkspaceRoute(serverId, secondWorkspace.workspaceId), {
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
await expectSidebarWorkspaceSelected({
|
||||||
|
page,
|
||||||
|
serverId,
|
||||||
|
workspaceId: secondWorkspace.workspaceId,
|
||||||
|
});
|
||||||
|
await expectWorkspaceHeader(page, {
|
||||||
|
title: secondWorkspace.workspaceName,
|
||||||
|
subtitle: secondWorkspace.projectDisplayName,
|
||||||
|
});
|
||||||
|
await expectWorkspaceTabVisible(page, secondAgent.id);
|
||||||
|
await expectWorkspaceTabHidden(page, firstAgent.id);
|
||||||
|
await expectOnlyWorkspaceAgentTabsVisible(page, [secondAgent.id]);
|
||||||
|
await expect(getVisibleWorkspaceAgentTabIds(page)).resolves.toEqual([
|
||||||
|
`workspace-tab-agent_${secondAgent.id}`,
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
for (const agentId of agentIds) {
|
||||||
|
await archiveAgentFromDaemon(archiveClient, agentId).catch(() => undefined);
|
||||||
|
}
|
||||||
|
for (const workspaceId of workspaceIds) {
|
||||||
|
await archiveLocalWorkspaceFromDaemon(workspaceClient, workspaceId).catch(() => undefined);
|
||||||
|
}
|
||||||
|
await archiveClient.close().catch(() => undefined);
|
||||||
|
await workspaceClient.close().catch(() => undefined);
|
||||||
|
await secondRepo.cleanup();
|
||||||
|
await firstRepo.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -811,7 +811,17 @@ function RootStack() {
|
|||||||
getId={({ params }) => {
|
getId={({ params }) => {
|
||||||
const serverId = getRouteParamValue(params?.serverId);
|
const serverId = getRouteParamValue(params?.serverId);
|
||||||
const workspaceId = getRouteParamValue(params?.workspaceId);
|
const workspaceId = getRouteParamValue(params?.workspaceId);
|
||||||
return serverId && workspaceId ? `${serverId}:${workspaceId}` : undefined;
|
const openIntent = getRouteParamValue(
|
||||||
|
params && typeof params === "object" && "open" in params
|
||||||
|
? (params as { open?: string | string[] }).open
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
if (!serverId || !workspaceId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return openIntent
|
||||||
|
? `${serverId}:${workspaceId}:open:${openIntent}`
|
||||||
|
: `${serverId}:${workspaceId}`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen name="h/[serverId]/agent/[agentId]" options={{ gestureEnabled: false }} />
|
<Stack.Screen name="h/[serverId]/agent/[agentId]" options={{ gestureEnabled: false }} />
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useIsFocused, useNavigation } from "@react-navigation/native";
|
||||||
import {
|
import {
|
||||||
useGlobalSearchParams,
|
useGlobalSearchParams,
|
||||||
useLocalSearchParams,
|
useLocalSearchParams,
|
||||||
usePathname,
|
usePathname,
|
||||||
|
useRouter,
|
||||||
useRootNavigationState,
|
useRootNavigationState,
|
||||||
} from "expo-router";
|
} from "expo-router";
|
||||||
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
||||||
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||||
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
||||||
import {
|
import {
|
||||||
|
buildHostWorkspaceRoute,
|
||||||
decodeWorkspaceIdFromPathSegment,
|
decodeWorkspaceIdFromPathSegment,
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
parseWorkspaceOpenIntent,
|
parseWorkspaceOpenIntent,
|
||||||
@@ -44,6 +47,29 @@ function getOpenIntentTarget(openIntent: WorkspaceOpenIntent): WorkspaceTabTarge
|
|||||||
return { kind: "draft", draftId: openIntent.draftId };
|
return { kind: "draft", draftId: openIntent.draftId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripOpenSearchParamFromBrowserUrl() {
|
||||||
|
if (!isWeb || typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (!url.searchParams.has("open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
url.searchParams.delete("open");
|
||||||
|
window.history.replaceState(null, "", url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConsumedOpenIntent(input: {
|
||||||
|
navigation: { setParams: (...args: any[]) => void };
|
||||||
|
router: { replace: (...args: any[]) => void };
|
||||||
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}) {
|
||||||
|
input.router.replace(buildHostWorkspaceRoute(input.serverId, input.workspaceId));
|
||||||
|
input.navigation.setParams({ open: undefined });
|
||||||
|
stripOpenSearchParamFromBrowserUrl();
|
||||||
|
}
|
||||||
|
|
||||||
export default function HostWorkspaceLayout() {
|
export default function HostWorkspaceLayout() {
|
||||||
return (
|
return (
|
||||||
<HostRouteBootstrapBoundary>
|
<HostRouteBootstrapBoundary>
|
||||||
@@ -53,6 +79,9 @@ export default function HostWorkspaceLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HostWorkspaceLayoutContent() {
|
function HostWorkspaceLayoutContent() {
|
||||||
|
const isFocused = useIsFocused();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const router = useRouter();
|
||||||
const rootNavigationState = useRootNavigationState();
|
const rootNavigationState = useRootNavigationState();
|
||||||
const consumedIntentRef = useRef<string | null>(null);
|
const consumedIntentRef = useRef<string | null>(null);
|
||||||
const [intentConsumed, setIntentConsumed] = useState(false);
|
const [intentConsumed, setIntentConsumed] = useState(false);
|
||||||
@@ -73,12 +102,22 @@ function HostWorkspaceLayoutContent() {
|
|||||||
if (!openValue) {
|
if (!openValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!rootNavigationState?.key) {
|
if (!rootNavigationState?.key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const consumptionKey = `${serverId}:${workspaceId}:${openValue}`;
|
const consumptionKey = `${serverId}:${workspaceId}:${openValue}`;
|
||||||
if (consumedIntentRef.current === consumptionKey) {
|
if (consumedIntentRef.current === consumptionKey) {
|
||||||
|
clearConsumedOpenIntent({
|
||||||
|
navigation,
|
||||||
|
router,
|
||||||
|
serverId,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
setIntentConsumed(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
consumedIntentRef.current = consumptionKey;
|
consumedIntentRef.current = consumptionKey;
|
||||||
@@ -96,16 +135,15 @@ function HostWorkspaceLayoutContent() {
|
|||||||
// Expo Router's replace ignores query-param-only changes (findDivergentState
|
// Expo Router's replace ignores query-param-only changes (findDivergentState
|
||||||
// skips search params). Strip ?open from the browser URL directly so the
|
// skips search params). Strip ?open from the browser URL directly so the
|
||||||
// address bar reflects the clean workspace route.
|
// address bar reflects the clean workspace route.
|
||||||
if (isWeb && typeof window !== "undefined") {
|
clearConsumedOpenIntent({
|
||||||
const url = new URL(window.location.href);
|
navigation,
|
||||||
if (url.searchParams.has("open")) {
|
router,
|
||||||
url.searchParams.delete("open");
|
serverId,
|
||||||
window.history.replaceState(null, "", url.toString());
|
workspaceId,
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
setIntentConsumed(true);
|
setIntentConsumed(true);
|
||||||
}, [openValue, rootNavigationState?.key, serverId, workspaceId]);
|
}, [isFocused, navigation, openValue, rootNavigationState?.key, router, serverId, workspaceId]);
|
||||||
|
|
||||||
if (openValue && !intentConsumed) {
|
if (openValue && !intentConsumed) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1009,6 +1009,9 @@ function WorkspaceRowInner({
|
|||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
disabled={isArchiving}
|
disabled={isArchiving}
|
||||||
|
aria-selected={selected}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityState={{ selected }}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
styles.workspaceRow,
|
styles.workspaceRow,
|
||||||
isDragging && styles.workspaceRowDragging,
|
isDragging && styles.workspaceRowDragging,
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ import {
|
|||||||
} from "@/screens/workspace/workspace-header-source";
|
} from "@/screens/workspace/workspace-header-source";
|
||||||
import {
|
import {
|
||||||
deriveWorkspaceAgentVisibility,
|
deriveWorkspaceAgentVisibility,
|
||||||
shouldPruneWorkspaceAgentTab,
|
|
||||||
workspaceAgentVisibilityEqual,
|
workspaceAgentVisibilityEqual,
|
||||||
} from "@/screens/workspace/workspace-agent-visibility";
|
} from "@/screens/workspace/workspace-agent-visibility";
|
||||||
import { deriveWorkspacePaneState } from "@/screens/workspace/workspace-pane-state";
|
import { deriveWorkspacePaneState } from "@/screens/workspace/workspace-pane-state";
|
||||||
@@ -1010,7 +1009,6 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminalIds = new Set(terminals.map((terminal) => terminal.id));
|
|
||||||
const hasActivePendingDraftCreateInWorkspace = uiTabs.some((tab) => {
|
const hasActivePendingDraftCreateInWorkspace = uiTabs.some((tab) => {
|
||||||
if (tab.target.kind !== "draft") {
|
if (tab.target.kind !== "draft") {
|
||||||
return false;
|
return false;
|
||||||
@@ -1019,56 +1017,16 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
return pending?.serverId === normalizedServerId && pending.lifecycle === "active";
|
return pending?.serverId === normalizedServerId && pending.lifecycle === "active";
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const agentId of workspaceAgentVisibility.activeAgentIds) {
|
reconcileWorkspaceTabs(persistenceKey, {
|
||||||
if (hiddenAgentIds.has(agentId)) {
|
agentsHydrated: hasHydratedAgents,
|
||||||
continue;
|
terminalsHydrated: terminalsQuery.isSuccess,
|
||||||
}
|
activeAgentIds: Array.from(workspaceAgentVisibility.activeAgentIds),
|
||||||
const representedByTarget = uiTabs.some(
|
knownAgentIds: Array.from(workspaceAgentVisibility.knownAgentIds),
|
||||||
(tab) => tab.target.kind === "agent" && tab.target.agentId === agentId,
|
standaloneTerminalIds: terminals.map((terminal) => terminal.id),
|
||||||
);
|
hasActivePendingDraftCreate: hasActivePendingDraftCreateInWorkspace,
|
||||||
const representedByDeterministicTabId = uiTabs.some(
|
});
|
||||||
(tab) => tab.tabId === `agent_${agentId}`,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
hasActivePendingDraftCreateInWorkspace &&
|
|
||||||
!representedByTarget &&
|
|
||||||
!representedByDeterministicTabId
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ensureWorkspaceTab({ kind: "agent", agentId });
|
|
||||||
}
|
|
||||||
for (const terminal of terminals) {
|
|
||||||
ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
const canPruneAgentTabs = hasHydratedAgents;
|
|
||||||
const canPruneTerminalTabs = terminalsQuery.isSuccess;
|
|
||||||
for (const tab of uiTabs) {
|
|
||||||
if (
|
|
||||||
canPruneAgentTabs &&
|
|
||||||
tab.target.kind === "agent" &&
|
|
||||||
!pinnedAgentIds.has(tab.target.agentId) &&
|
|
||||||
shouldPruneWorkspaceAgentTab({
|
|
||||||
agentId: tab.target.agentId,
|
|
||||||
agentsHydrated: hasHydratedAgents,
|
|
||||||
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
|
|
||||||
activeAgentIds: workspaceAgentVisibility.activeAgentIds,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
canPruneTerminalTabs &&
|
|
||||||
tab.target.kind === "terminal" &&
|
|
||||||
!terminalIds.has(tab.target.terminalId)
|
|
||||||
) {
|
|
||||||
closeWorkspaceTabWithCleanup({ tabId: tab.tabId, target: tab.target });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
hasHydratedAgents,
|
hasHydratedAgents,
|
||||||
hiddenAgentIds,
|
|
||||||
pendingByDraftId,
|
pendingByDraftId,
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
reconcileWorkspaceTabs,
|
reconcileWorkspaceTabs,
|
||||||
|
|||||||
Reference in New Issue
Block a user