Fix workspace tab focus from route open intent
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import "@/styles/unistyles";
|
||||
import { polyfillCrypto } from "@/polyfills/crypto";
|
||||
import { Stack, usePathname, useRouter } from "expo-router";
|
||||
import { Stack, useGlobalSearchParams, usePathname, useRouter } from "expo-router";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||
import { GestureHandlerRootView, Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
@@ -47,7 +47,7 @@ import { buildNotificationRoute } from "@/utils/notification-routing";
|
||||
import {
|
||||
buildHostRootRoute,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceOpenIntentFromPathname,
|
||||
parseWorkspaceOpenIntent,
|
||||
} from "@/utils/host-routes";
|
||||
import { getTauri } from "@/utils/tauri";
|
||||
import { PerfDiagnosticsProvider } from "@/runtime/perf-diagnostics";
|
||||
@@ -376,6 +376,7 @@ function OfferLinkListener({
|
||||
|
||||
function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const params = useGlobalSearchParams<{ open?: string | string[] }>();
|
||||
useFaviconStatus();
|
||||
|
||||
// Parse selectedAgentKey directly from pathname
|
||||
@@ -383,7 +384,8 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||
const selectedAgentKey = useMemo(() => {
|
||||
const workspaceMatch = pathname.match(/^\/h\/([^/]+)\/workspace\/[^/]+(?:\/|$)/);
|
||||
const workspaceServerId = workspaceMatch?.[1]?.trim() ?? "";
|
||||
const openIntent = parseHostWorkspaceOpenIntentFromPathname(pathname);
|
||||
const openValue = Array.isArray(params.open) ? params.open[0] : params.open;
|
||||
const openIntent = parseWorkspaceOpenIntent(openValue);
|
||||
if (workspaceServerId && openIntent?.kind === "agent") {
|
||||
const agentId = openIntent.agentId.trim();
|
||||
return agentId ? `${workspaceServerId}:${agentId}` : undefined;
|
||||
@@ -391,7 +393,7 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
||||
|
||||
const match = parseHostAgentRouteFromPathname(pathname);
|
||||
return match ? `${match.serverId}:${match.agentId}` : undefined;
|
||||
}, [pathname]);
|
||||
}, [params.open, pathname]);
|
||||
|
||||
return (
|
||||
<AppContainer selectedAgentId={selectedAgentKey}>{children}</AppContainer>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useLocalSearchParams, usePathname, useRouter } from "expo-router";
|
||||
import { Platform } from "react-native";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useFormPreferences } from "@/hooks/use-form-preferences";
|
||||
import {
|
||||
@@ -13,11 +12,7 @@ const HOST_ROOT_REDIRECT_DELAY_MS = 300;
|
||||
|
||||
export default function HostIndexRoute() {
|
||||
const router = useRouter();
|
||||
const routerPathname = usePathname();
|
||||
const pathname =
|
||||
Platform.OS === "web" && typeof window !== "undefined"
|
||||
? window.location.pathname
|
||||
: routerPathname;
|
||||
const pathname = usePathname();
|
||||
const params = useLocalSearchParams<{ serverId?: string }>();
|
||||
const serverId = typeof params.serverId === "string" ? params.serverId : "";
|
||||
const { preferences, isLoading: preferencesLoading } = useFormPreferences();
|
||||
@@ -37,11 +32,8 @@ export default function HostIndexRoute() {
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
if (Platform.OS === "web" && typeof window !== "undefined") {
|
||||
const currentPathname = window.location.pathname;
|
||||
if (currentPathname !== rootRoute && currentPathname !== `${rootRoute}/`) {
|
||||
return;
|
||||
}
|
||||
if (pathname !== rootRoute && pathname !== `${rootRoute}/`) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleAgents = sessionAgents
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { usePathname } from 'expo-router'
|
||||
import { useGlobalSearchParams, usePathname } from 'expo-router'
|
||||
import { WorkspaceScreen } from '@/screens/workspace/workspace-screen'
|
||||
import {
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
parseHostWorkspaceOpenIntentFromPathname,
|
||||
parseWorkspaceOpenIntent,
|
||||
} from '@/utils/host-routes'
|
||||
|
||||
export default function HostWorkspaceLayout() {
|
||||
const expoPathname = usePathname()
|
||||
const resolvedPathname = expoPathname
|
||||
const activeRoute = parseHostWorkspaceRouteFromPathname(resolvedPathname)
|
||||
const params = useGlobalSearchParams<{ open?: string | string[] }>()
|
||||
const activeRoute = parseHostWorkspaceRouteFromPathname(expoPathname)
|
||||
const serverId = activeRoute?.serverId ?? ''
|
||||
const workspaceId = activeRoute?.workspaceId ?? ''
|
||||
const openIntent = parseHostWorkspaceOpenIntentFromPathname(resolvedPathname)
|
||||
const openValue = Array.isArray(params.open) ? params.open[0] : params.open
|
||||
const openIntent = parseWorkspaceOpenIntent(openValue)
|
||||
|
||||
return (
|
||||
<WorkspaceScreen
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { useLocalSearchParams, usePathname, useRouter } from "expo-router";
|
||||
import { useUnistyles } from "react-native-unistyles";
|
||||
import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen";
|
||||
@@ -9,11 +9,7 @@ import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
const routerPathname = usePathname();
|
||||
const pathname =
|
||||
Platform.OS === "web" && typeof window !== "undefined"
|
||||
? window.location.pathname
|
||||
: routerPathname;
|
||||
const pathname = usePathname();
|
||||
const params = useLocalSearchParams<{ serverId?: string }>();
|
||||
const { theme } = useUnistyles();
|
||||
const { daemons, isLoading: registryLoading } = useDaemonRegistry();
|
||||
|
||||
@@ -38,11 +38,7 @@ export function useKeyboardShortcuts({
|
||||
toggleFileExplorer?: () => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const routerPathname = usePathname();
|
||||
const pathname =
|
||||
Platform.OS === "web" && typeof window !== "undefined"
|
||||
? window.location.pathname
|
||||
: routerPathname;
|
||||
const pathname = usePathname();
|
||||
const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -327,7 +323,6 @@ export function useKeyboardShortcuts({
|
||||
pathname,
|
||||
resetModifiers,
|
||||
router,
|
||||
routerPathname,
|
||||
selectedAgentId,
|
||||
toggleAgentList,
|
||||
toggleFileExplorer,
|
||||
|
||||
@@ -576,8 +576,18 @@ function WorkspaceScreenContent({
|
||||
tabs: uiTabs,
|
||||
tabOrder,
|
||||
focusedTabId,
|
||||
preferredTarget:
|
||||
openIntent?.kind === "agent"
|
||||
? { kind: "agent", agentId: openIntent.agentId }
|
||||
: openIntent?.kind === "terminal"
|
||||
? { kind: "terminal", terminalId: openIntent.terminalId }
|
||||
: openIntent?.kind === "draft"
|
||||
? { kind: "draft", draftId: openIntent.draftId }
|
||||
: openIntent?.kind === "file"
|
||||
? { kind: "file", path: openIntent.path }
|
||||
: null,
|
||||
}),
|
||||
[focusedTabId, tabOrder, terminals, uiTabs, workspaceAgents]
|
||||
[focusedTabId, openIntent, tabOrder, terminals, uiTabs, workspaceAgents]
|
||||
);
|
||||
const activeTabId = tabModel.activeTabId;
|
||||
|
||||
|
||||
@@ -151,6 +151,23 @@ describe("deriveWorkspaceTabModel", () => {
|
||||
).toBe("agent_agent-b");
|
||||
});
|
||||
|
||||
it("prefers the route-selected target over stale focused tab state", () => {
|
||||
const model = deriveWorkspaceTabModel({
|
||||
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
|
||||
terminals: [],
|
||||
tabs: [
|
||||
{ tabId: "agent_agent-a", target: { kind: "agent", agentId: "agent-a" }, createdAt: 1 },
|
||||
{ tabId: "agent_agent-b", target: { kind: "agent", agentId: "agent-b" }, createdAt: 2 },
|
||||
],
|
||||
tabOrder: ["agent_agent-a", "agent_agent-b"],
|
||||
focusedTabId: "agent_agent-a",
|
||||
preferredTarget: { kind: "agent", agentId: "agent-b" },
|
||||
});
|
||||
|
||||
expect(model.activeTabId).toBe("agent_agent-b");
|
||||
expect(model.activeTab?.target).toEqual({ kind: "agent", agentId: "agent-b" });
|
||||
});
|
||||
|
||||
it("re-resolves active content for a new workspace when prior focused tab is not available", () => {
|
||||
const model = deriveWorkspaceTabModel({
|
||||
workspaceAgents: [makeAgent({ id: "workspace-b-agent", title: "B" })],
|
||||
@@ -237,4 +254,24 @@ describe("deriveWorkspaceTabModel", () => {
|
||||
expect(upgradedDescriptor.label).toBe("Ready title");
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers a retargeted tab when the route selects its upgraded agent target", () => {
|
||||
const model = deriveWorkspaceTabModel({
|
||||
workspaceAgents: [makeAgent({ id: "agent-1", title: "Ready title" })],
|
||||
terminals: [],
|
||||
tabs: [
|
||||
{
|
||||
tabId: "draft_abc",
|
||||
target: { kind: "agent", agentId: "agent-1" },
|
||||
createdAt: 1,
|
||||
},
|
||||
],
|
||||
tabOrder: ["draft_abc"],
|
||||
focusedTabId: "some_other_tab",
|
||||
preferredTarget: { kind: "agent", agentId: "agent-1" },
|
||||
});
|
||||
|
||||
expect(model.activeTabId).toBe("draft_abc");
|
||||
expect(model.activeTab?.descriptor.tabId).toBe("draft_abc");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,25 @@ function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function tabTargetsEqual(left: WorkspaceTabTarget, right: WorkspaceTabTarget): boolean {
|
||||
if (left.kind !== right.kind) {
|
||||
return false;
|
||||
}
|
||||
if (left.kind === "draft" && right.kind === "draft") {
|
||||
return left.draftId === right.draftId;
|
||||
}
|
||||
if (left.kind === "agent" && right.kind === "agent") {
|
||||
return left.agentId === right.agentId;
|
||||
}
|
||||
if (left.kind === "terminal" && right.kind === "terminal") {
|
||||
return left.terminalId === right.terminalId;
|
||||
}
|
||||
if (left.kind === "file" && right.kind === "file") {
|
||||
return left.path === right.path;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function formatProviderLabel(provider: Agent["provider"]): string {
|
||||
if (provider === "claude") {
|
||||
return "Claude";
|
||||
@@ -130,6 +149,7 @@ export function deriveWorkspaceTabModel(input: {
|
||||
tabs: WorkspaceTab[];
|
||||
tabOrder: string[];
|
||||
focusedTabId?: string | null;
|
||||
preferredTarget?: WorkspaceTabTarget | null;
|
||||
}): WorkspaceTabModel {
|
||||
const tabsById = new Map<string, WorkspaceDerivedTab>();
|
||||
const agentsById = new Map(input.workspaceAgents.map((agent) => [agent.id, agent]));
|
||||
@@ -234,9 +254,20 @@ export function deriveWorkspaceTabModel(input: {
|
||||
|
||||
const openTabIds = new Set(tabs.map((tab) => tab.descriptor.tabId));
|
||||
const focusedTabId = trimNonEmpty(input.focusedTabId);
|
||||
const preferredTarget = input.preferredTarget ?? null;
|
||||
const preferredTabId = (() => {
|
||||
if (!preferredTarget) {
|
||||
return null;
|
||||
}
|
||||
const matchingTab =
|
||||
tabs.find((tab) => tabTargetsEqual(tab.target, preferredTarget)) ?? null;
|
||||
return matchingTab?.descriptor.tabId ?? buildWorkspaceTabId(preferredTarget);
|
||||
})();
|
||||
|
||||
const activeTabId =
|
||||
focusedTabId && openTabIds.has(focusedTabId)
|
||||
preferredTabId && openTabIds.has(preferredTabId)
|
||||
? preferredTabId
|
||||
: focusedTabId && openTabIds.has(focusedTabId)
|
||||
? focusedTabId
|
||||
: tabs[0]?.descriptor.tabId ?? null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user