refactor(app): remove launcher tabs from workspace flow
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery, useQueries } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
AGENT_PROVIDER_DEFINITIONS,
|
||||
type AgentProviderDefinition,
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "@server/server/agent/agent-sdk-types";
|
||||
import { useHosts } from "@/runtime/host-runtime";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useProviderModels } from "@/hooks/use-provider-models";
|
||||
import {
|
||||
useFormPreferences,
|
||||
mergeProviderPreferences,
|
||||
@@ -409,75 +410,11 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
[providerDefinitions],
|
||||
);
|
||||
|
||||
const [debouncedCwd, setDebouncedCwd] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
const trimmed = formState.workingDir.trim();
|
||||
const next = trimmed.length > 0 ? trimmed : undefined;
|
||||
const timer = setTimeout(() => setDebouncedCwd(next), 180);
|
||||
return () => clearTimeout(timer);
|
||||
}, [formState.workingDir]);
|
||||
const { allProviderModels, isLoading: isAllModelsLoading } = useProviderModels(
|
||||
formState.serverId ?? "",
|
||||
);
|
||||
|
||||
const providerModelsQuery = useQuery({
|
||||
queryKey: ["providerModels", formState.serverId, formState.provider],
|
||||
enabled: Boolean(
|
||||
isVisible &&
|
||||
isTargetDaemonReady &&
|
||||
formState.serverId &&
|
||||
client &&
|
||||
isConnected &&
|
||||
providerDefinitionMap.has(formState.provider),
|
||||
),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
const payload = await client.listProviderModels(formState.provider, {
|
||||
cwd: debouncedCwd,
|
||||
});
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return payload.models ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
const availableModels = providerModelsQuery.data ?? null;
|
||||
|
||||
const allProviderModelQueries = useQueries({
|
||||
queries: providerDefinitions.map((def) => ({
|
||||
queryKey: ["providerModels", formState.serverId, def.id],
|
||||
enabled: Boolean(
|
||||
isVisible && isTargetDaemonReady && formState.serverId && client && isConnected,
|
||||
),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
const payload = await client.listProviderModels(def.id as AgentProvider, {
|
||||
cwd: debouncedCwd,
|
||||
});
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return payload.models ?? [];
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const allProviderModels = useMemo(() => {
|
||||
const map = new Map<string, AgentModelDefinition[]>();
|
||||
for (let i = 0; i < providerDefinitions.length; i++) {
|
||||
const query = allProviderModelQueries[i];
|
||||
if (query?.data) {
|
||||
map.set(providerDefinitions[i]!.id, query.data);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [allProviderModelQueries, providerDefinitions]);
|
||||
|
||||
const isAllModelsLoading = allProviderModelQueries.some((q) => q.isLoading);
|
||||
const availableModels = allProviderModels.get(formState.provider) ?? null;
|
||||
|
||||
// Combine initialValues with initialServerId for resolution
|
||||
const combinedInitialValues = useMemo((): FormInitialValues | undefined => {
|
||||
@@ -687,9 +624,12 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
setFormState((prev) => ({ ...prev, serverId: value }));
|
||||
}, []);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const refreshProviderModels = useCallback(() => {
|
||||
void providerModelsQuery.refetch();
|
||||
}, [providerModelsQuery]);
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["providerModels", formState.serverId, formState.provider],
|
||||
});
|
||||
}, [queryClient, formState.serverId, formState.provider]);
|
||||
|
||||
const persistFormPreferences = useCallback(async () => {
|
||||
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
||||
@@ -726,9 +666,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
|
||||
const resolvedModelId = effectiveModel?.id ?? formState.model;
|
||||
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
|
||||
const isModelLoading = providerModelsQuery.isLoading || providerModelsQuery.isFetching;
|
||||
const modelError =
|
||||
providerModelsQuery.error instanceof Error ? providerModelsQuery.error.message : null;
|
||||
const isModelLoading = !availableModels && isAllModelsLoading;
|
||||
const modelError: string | null = null;
|
||||
|
||||
const workingDirIsEmpty = !formState.workingDir.trim();
|
||||
|
||||
|
||||
@@ -32,10 +32,19 @@ import {
|
||||
collectAllTabs,
|
||||
useWorkspaceLayoutStore,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
import { generateDraftId } from "@/stores/draft-keys";
|
||||
|
||||
const SERVER_ID = "server-1";
|
||||
const WORKSPACE_ID = "/repo/project";
|
||||
|
||||
function createOpenDraftTab() {
|
||||
return (workspaceKey: string) =>
|
||||
useWorkspaceLayoutStore.getState().openTab(workspaceKey, {
|
||||
kind: "draft",
|
||||
draftId: generateDraftId(),
|
||||
});
|
||||
}
|
||||
|
||||
describe("openProjectDirectly", () => {
|
||||
beforeEach(() => {
|
||||
replaceRoute.mockReset();
|
||||
@@ -51,11 +60,7 @@ describe("openProjectDirectly", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("opens the workspace directly, marks workspaces hydrated, and seeds a launcher tab", async () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
);
|
||||
|
||||
it("opens the workspace directly, marks workspaces hydrated, and seeds a draft tab", async () => {
|
||||
const result = await openProjectDirectly({
|
||||
serverId: SERVER_ID,
|
||||
projectPath: WORKSPACE_ID,
|
||||
@@ -82,7 +87,7 @@ describe("openProjectDirectly", () => {
|
||||
},
|
||||
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
|
||||
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
|
||||
openLauncherTab: useWorkspaceLayoutStore.getState().openLauncherTab,
|
||||
openDraftTab: createOpenDraftTab(),
|
||||
replaceRoute,
|
||||
});
|
||||
|
||||
@@ -106,10 +111,7 @@ describe("openProjectDirectly", () => {
|
||||
expect(layout.root.kind).toBe("pane");
|
||||
const tabs = collectAllTabs(layout.root);
|
||||
expect(tabs).toHaveLength(1);
|
||||
expect(tabs[0]?.target).toEqual({
|
||||
kind: "launcher",
|
||||
launcherId: "11111111-1111-1111-1111-111111111111",
|
||||
});
|
||||
expect(tabs[0]?.target.kind).toBe("draft");
|
||||
expect(replaceRoute).toHaveBeenCalledWith("/h/server-1/workspace/MQ");
|
||||
});
|
||||
|
||||
@@ -127,7 +129,7 @@ describe("openProjectDirectly", () => {
|
||||
},
|
||||
mergeWorkspaces: useSessionStore.getState().mergeWorkspaces,
|
||||
setHasHydratedWorkspaces: useSessionStore.getState().setHasHydratedWorkspaces,
|
||||
openLauncherTab: useWorkspaceLayoutStore.getState().openLauncherTab,
|
||||
openDraftTab: createOpenDraftTab(),
|
||||
replaceRoute,
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
useWorkspaceLayoutStore,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
import { generateDraftId } from "@/stores/draft-keys";
|
||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
|
||||
interface OpenProjectDirectlyInput {
|
||||
@@ -16,7 +17,7 @@ interface OpenProjectDirectlyInput {
|
||||
client: Pick<DaemonClient, "openProject"> | null;
|
||||
mergeWorkspaces: (serverId: string, workspaces: Iterable<WorkspaceDescriptor>) => void;
|
||||
setHasHydratedWorkspaces: (serverId: string, hydrated: boolean) => void;
|
||||
openLauncherTab: (workspaceKey: string) => string | null;
|
||||
openDraftTab: (workspaceKey: string) => string | null;
|
||||
replaceRoute: (route: string) => void;
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ export async function openProjectDirectly(input: OpenProjectDirectlyInput): Prom
|
||||
return false;
|
||||
}
|
||||
|
||||
input.openLauncherTab(workspaceKey);
|
||||
input.openDraftTab(workspaceKey);
|
||||
input.replaceRoute(buildHostWorkspaceRoute(normalizedServerId, workspace.id));
|
||||
return true;
|
||||
}
|
||||
@@ -65,7 +66,11 @@ export function useOpenProject(serverId: string | null): (path: string) => Promi
|
||||
client,
|
||||
mergeWorkspaces,
|
||||
setHasHydratedWorkspaces,
|
||||
openLauncherTab: useWorkspaceLayoutStore.getState().openLauncherTab,
|
||||
openDraftTab: (workspaceKey: string) =>
|
||||
useWorkspaceLayoutStore.getState().openTab(workspaceKey, {
|
||||
kind: "draft",
|
||||
draftId: generateDraftId(),
|
||||
}),
|
||||
replaceRoute: (route) => {
|
||||
router.replace(route as any);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useMemo } from "react";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import {
|
||||
AGENT_PROVIDER_DEFINITIONS,
|
||||
type AgentProviderDefinition,
|
||||
} from "@server/server/agent/provider-manifest";
|
||||
import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
|
||||
const STALE_TIME = 5 * 60 * 1000;
|
||||
|
||||
export function useProviderModels(serverId: string) {
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const enabled = Boolean(serverId && client && isConnected);
|
||||
|
||||
const queries = useQueries({
|
||||
queries: AGENT_PROVIDER_DEFINITIONS.map((def) => ({
|
||||
queryKey: ["providerModels", serverId, def.id] as const,
|
||||
enabled,
|
||||
staleTime: STALE_TIME,
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
const payload = await client.listProviderModels(def.id as AgentProvider);
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return payload.models ?? [];
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const allProviderModels = useMemo(() => {
|
||||
const map = new Map<string, AgentModelDefinition[]>();
|
||||
for (let i = 0; i < AGENT_PROVIDER_DEFINITIONS.length; i++) {
|
||||
const query = queries[i];
|
||||
if (query?.data) {
|
||||
map.set(AGENT_PROVIDER_DEFINITIONS[i]!.id, query.data);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [queries]);
|
||||
|
||||
const isLoading = queries.some((q) => q.isLoading);
|
||||
|
||||
return { allProviderModels, isLoading };
|
||||
}
|
||||
Reference in New Issue
Block a user