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:
@@ -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 }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user