Keep forked chats focused in new tabs (#2038)

* fix(app): focus explicitly opened workspace tabs

Explicit tab navigation could be overwritten by attention-aware workspace
navigation when the visible agent had just finished. Make workspace navigation
own both normal attention selection and explicit targets so callers cannot
sequence them inconsistently.

* test(app): strengthen forked tab focus assertion
This commit is contained in:
Mohamed Boudra
2026-07-13 15:38:09 +02:00
committed by GitHub
parent fe9a486ac1
commit f350c716b9
18 changed files with 156 additions and 139 deletions
@@ -2,7 +2,7 @@ import { router, type Href } from "expo-router";
import { useSessionStore } from "@/stores/session-store";
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
import { resolveNavigateToAgent, type NavigateToAgentInput } from "./resolve";
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
export type { NavigateToAgentInput } from "./resolve";
@@ -69,7 +69,7 @@ export function navigateToAgent(input: NavigateToAgentInput): string {
navigateToHostAgent: (route) => {
router.navigate(route as Href);
},
navigateToPreparedWorkspaceTab,
navigateToWorkspace,
restoreArchivedWorkspace: ({ serverId, agentId, workspaceId }) => {
restoreArchivedWorkspace(serverId, agentId, workspaceId);
},
@@ -4,7 +4,7 @@ import {
type AgentNavTarget,
type NavigateToAgentDeps,
} from "@/utils/navigate-to-agent/resolve";
import type { NavigateToPreparedWorkspaceTabInput } from "@/utils/prepare-workspace-tab";
import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store";
const SERVER_ID = "server-1";
const WORKSPACE_ID = "workspace-1";
@@ -14,7 +14,7 @@ interface RecordedHostNav {
route: string;
}
interface RecordedTabNav extends NavigateToPreparedWorkspaceTabInput {}
interface RecordedTabNav extends NavigateToWorkspaceInput {}
interface RecordedRestore {
serverId: string;
@@ -40,7 +40,7 @@ function createFakeNavigators(target: AgentNavTarget): {
navigateToHostAgent: (route) => {
hostNavigations.push({ route });
},
navigateToPreparedWorkspaceTab: (input) => {
navigateToWorkspace: (input) => {
tabNavigations.push(input);
return `/h/${input.serverId}/workspace/${input.workspaceId}`;
},
@@ -1,6 +1,6 @@
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
import type { NavigateToPreparedWorkspaceTabInput } from "@/utils/prepare-workspace-tab";
import type { NavigateToWorkspaceInput } from "@/stores/navigation-active-workspace-store";
export interface NavigateToAgentInput {
serverId: string;
@@ -18,7 +18,7 @@ export interface AgentNavTarget {
export interface NavigateToAgentDeps {
readAgentNavTarget: (input: { serverId: string; agentId: string }) => AgentNavTarget;
navigateToHostAgent: (route: string) => void;
navigateToPreparedWorkspaceTab: (input: NavigateToPreparedWorkspaceTabInput) => string;
navigateToWorkspace: (input: NavigateToWorkspaceInput) => string;
restoreArchivedWorkspace: (input: {
serverId: string;
agentId: string;
@@ -49,7 +49,7 @@ export function resolveNavigateToAgent(
workspaceId,
});
return deps.navigateToPreparedWorkspaceTab({
return deps.navigateToWorkspace({
serverId: input.serverId,
workspaceId,
target: { kind: "agent", agentId: input.agentId },
@@ -9,8 +9,8 @@ vi.mock("expo-router", () => ({
router: { navigate: vi.fn() },
}));
vi.mock("@/utils/workspace-navigation", () => ({
navigateToPreparedWorkspaceTab: vi.fn(() => ""),
vi.mock("@/stores/navigation-active-workspace-store", () => ({
navigateToWorkspace: vi.fn(() => ""),
}));
vi.mock("@/runtime/host-runtime", () => ({
@@ -3,7 +3,6 @@ import {
buildWorkspaceTabPersistenceKey,
type WorkspaceTabTarget,
} from "@/stores/workspace-tabs-store";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
export interface PrepareWorkspaceTabInput {
serverId: string;
@@ -12,17 +11,11 @@ export interface PrepareWorkspaceTabInput {
pin?: boolean;
}
export type NavigateToPreparedWorkspaceTabInput = PrepareWorkspaceTabInput;
export interface PrepareWorkspaceTabDeps {
openTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
pinAgent: (workspaceKey: string, agentId: string) => void;
}
export interface NavigateToPreparedWorkspaceTabDeps extends PrepareWorkspaceTabDeps {
navigateToWorkspace: (serverId: string, workspaceId: string) => void;
}
function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
if (target.kind !== "draft" || target.draftId.trim() !== "new") {
return target;
@@ -33,7 +26,7 @@ function getPreparedTarget(target: WorkspaceTabTarget): WorkspaceTabTarget {
export function prepareWorkspaceTab(
input: PrepareWorkspaceTabInput,
deps: PrepareWorkspaceTabDeps,
): string {
): void {
const target = getPreparedTarget(input.target);
const key =
buildWorkspaceTabPersistenceKey({
@@ -46,15 +39,4 @@ export function prepareWorkspaceTab(
if (input.pin && target.kind === "agent") {
deps.pinAgent(key, target.agentId);
}
return buildHostWorkspaceRoute(input.serverId, input.workspaceId);
}
export function navigateToPreparedWorkspaceTab(
input: NavigateToPreparedWorkspaceTabInput,
deps: NavigateToPreparedWorkspaceTabDeps,
): string {
const route = prepareWorkspaceTab(input, deps);
deps.navigateToWorkspace(input.serverId, input.workspaceId);
return route;
}
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
import { navigateToPreparedWorkspaceTab, prepareWorkspaceTab } from "@/utils/prepare-workspace-tab";
import { prepareWorkspaceTab } from "@/utils/prepare-workspace-tab";
const SERVER_ID = "server-1";
const WORKSPACE_ID = "/repo/worktree";
@@ -16,11 +16,6 @@ interface RecordedPin {
agentId: string;
}
interface RecordedNavigation {
serverId: string;
workspaceId: string;
}
function createFakeLayout() {
const openedTabs: RecordedOpenedTab[] = [];
const pinnedAgents: RecordedPin[] = [];
@@ -37,21 +32,11 @@ function createFakeLayout() {
};
}
function createFakeNavigator() {
const navigations: RecordedNavigation[] = [];
return {
navigations,
navigateToWorkspace: (serverId: string, workspaceId: string) => {
navigations.push({ serverId, workspaceId });
},
};
}
describe("prepareWorkspaceTab", () => {
it("opens and focuses an agent tab", () => {
const layout = createFakeLayout();
const route = prepareWorkspaceTab(
prepareWorkspaceTab(
{
serverId: SERVER_ID,
workspaceId: WORKSPACE_ID,
@@ -60,30 +45,9 @@ describe("prepareWorkspaceTab", () => {
layout,
);
expect(route).toBe("/h/server-1/workspace/b64_L3JlcG8vd29ya3RyZWU");
expect(layout.openedTabs).toEqual([
{ key: "server-1:/repo/worktree", target: { kind: "agent", agentId: AGENT_ID } },
]);
expect(layout.pinnedAgents).toEqual([]);
});
it("prepares a tab and navigates through the workspace navigation helper", () => {
const layout = createFakeLayout();
const navigator = createFakeNavigator();
const route = navigateToPreparedWorkspaceTab(
{
serverId: SERVER_ID,
workspaceId: WORKSPACE_ID,
target: { kind: "agent", agentId: AGENT_ID },
},
{ ...layout, navigateToWorkspace: navigator.navigateToWorkspace },
);
expect(route).toBe("/h/server-1/workspace/b64_L3JlcG8vd29ya3RyZWU");
expect(layout.openedTabs).toEqual([
{ key: "server-1:/repo/worktree", target: { kind: "agent", agentId: AGENT_ID } },
]);
expect(navigator.navigations).toEqual([{ serverId: SERVER_ID, workspaceId: WORKSPACE_ID }]);
});
});
+3 -16
View File
@@ -1,16 +1,10 @@
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
import {
prepareWorkspaceTab as prepareWorkspaceTabPure,
navigateToPreparedWorkspaceTab as navigateToPreparedWorkspaceTabPure,
type PrepareWorkspaceTabInput,
type NavigateToPreparedWorkspaceTabInput,
} from "./prepare-workspace-tab";
export type {
PrepareWorkspaceTabInput,
NavigateToPreparedWorkspaceTabInput,
} from "./prepare-workspace-tab";
export type { PrepareWorkspaceTabInput } from "./prepare-workspace-tab";
function layoutStoreDeps() {
const store = useWorkspaceLayoutStore.getState();
@@ -20,13 +14,6 @@ function layoutStoreDeps() {
};
}
export function prepareWorkspaceTab(input: PrepareWorkspaceTabInput): string {
return prepareWorkspaceTabPure(input, layoutStoreDeps());
}
export function navigateToPreparedWorkspaceTab(input: NavigateToPreparedWorkspaceTabInput): string {
return navigateToPreparedWorkspaceTabPure(input, {
...layoutStoreDeps(),
navigateToWorkspace,
});
export function prepareWorkspaceTab(input: PrepareWorkspaceTabInput): void {
prepareWorkspaceTabPure(input, layoutStoreDeps());
}