Update files

This commit is contained in:
Mohamed Boudra
2026-02-10 10:50:54 +07:00
parent 7a21a5cce1
commit 7001928f4f
33 changed files with 3490 additions and 304 deletions
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { __private__ } from "./use-agent-form-state";
import type { AgentModelDefinition } from "@server/server/agent/agent-sdk-types";
describe("useAgentFormState", () => {
describe("__private__.combineInitialValues", () => {
@@ -35,5 +36,115 @@ describe("useAgentFormState", () => {
});
});
});
});
describe("__private__.resolveFormState", () => {
const codexModels: AgentModelDefinition[] = [
{
provider: "codex",
id: "gpt-5.3-codex",
label: "gpt-5.3-codex",
isDefault: true,
defaultThinkingOptionId: "xhigh",
thinkingOptions: [
{ id: "low", label: "low" },
{ id: "xhigh", label: "xhigh", isDefault: true },
],
},
];
it("auto-selects the model's default thinking option when none is configured", () => {
const resolved = __private__.resolveFormState(
undefined,
{ provider: "codex" },
codexModels,
{
serverId: false,
provider: false,
modeId: false,
model: false,
thinkingOptionId: false,
workingDir: false,
},
{
serverId: null,
provider: "codex",
modeId: "",
model: "",
thinkingOptionId: "",
workingDir: "",
},
new Set<string>()
);
expect(resolved.thinkingOptionId).toBe("xhigh");
});
it("keeps provider thinking preference when it is valid for the effective model", () => {
const resolved = __private__.resolveFormState(
undefined,
{
provider: "codex",
providerPreferences: {
codex: {
thinkingOptionId: "low",
},
},
},
codexModels,
{
serverId: false,
provider: false,
modeId: false,
model: false,
thinkingOptionId: false,
workingDir: false,
},
{
serverId: null,
provider: "codex",
modeId: "",
model: "",
thinkingOptionId: "",
workingDir: "",
},
new Set<string>()
);
expect(resolved.thinkingOptionId).toBe("low");
});
it("falls back to model default when saved thinking preference is invalid", () => {
const resolved = __private__.resolveFormState(
undefined,
{
provider: "codex",
providerPreferences: {
codex: {
thinkingOptionId: "medium",
},
},
},
codexModels,
{
serverId: false,
provider: false,
modeId: false,
model: false,
thinkingOptionId: false,
workingDir: false,
},
{
serverId: null,
provider: "codex",
modeId: "",
model: "",
thinkingOptionId: "",
workingDir: "",
},
new Set<string>()
);
expect(resolved.thinkingOptionId).toBe("xhigh");
});
});
});
+106 -6
View File
@@ -19,6 +19,7 @@ export interface FormInitialValues {
provider?: AgentProvider;
modeId?: string | null;
model?: string | null;
thinkingOptionId?: string | null;
workingDir?: string;
}
@@ -28,6 +29,7 @@ interface UserModifiedFields {
provider: boolean;
modeId: boolean;
model: boolean;
thinkingOptionId: boolean;
workingDir: boolean;
}
@@ -36,6 +38,7 @@ const INITIAL_USER_MODIFIED: UserModifiedFields = {
provider: false,
modeId: false,
model: false,
thinkingOptionId: false,
workingDir: false,
};
@@ -45,6 +48,7 @@ interface FormState {
provider: AgentProvider;
modeId: string;
model: string;
thinkingOptionId: string;
workingDir: string;
}
@@ -67,6 +71,8 @@ type UseAgentFormStateResult = {
setModeFromUser: (modeId: string) => void;
selectedModel: string;
setModelFromUser: (modelId: string) => void;
selectedThinkingOptionId: string;
setThinkingOptionFromUser: (thinkingOptionId: string) => void;
workingDir: string;
setWorkingDir: (value: string) => void;
setWorkingDirFromUser: (value: string) => void;
@@ -75,6 +81,7 @@ type UseAgentFormStateResult = {
agentDefinition?: AgentProviderDefinition;
modeOptions: AgentMode[];
availableModels: AgentModelDefinition[];
availableThinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
isModelLoading: boolean;
modelError: string | null;
refreshProviderModels: () => void;
@@ -91,6 +98,32 @@ const DEFAULT_PROVIDER: AgentProvider = fallbackDefinition?.id ?? "claude";
const DEFAULT_MODE_FOR_DEFAULT_PROVIDER =
fallbackDefinition?.defaultModeId ?? "";
function resolveDefaultModel(
availableModels: AgentModelDefinition[] | null
): AgentModelDefinition | null {
if (!availableModels || availableModels.length === 0) {
return null;
}
return availableModels.find((model) => model.isDefault) ?? availableModels[0] ?? null;
}
function resolveEffectiveModel(
availableModels: AgentModelDefinition[] | null,
modelId: string
): AgentModelDefinition | null {
if (!availableModels || availableModels.length === 0) {
return null;
}
const normalizedModelId = modelId.trim();
if (!normalizedModelId) {
return resolveDefaultModel(availableModels);
}
return (
availableModels.find((model) => model.id === normalizedModelId) ??
resolveDefaultModel(availableModels)
);
}
/**
* Pure function that resolves form state from multiple data sources.
* Priority: explicit (URL params) > preferences > provider defaults > fallback
@@ -175,7 +208,43 @@ function resolveFormState(
}
}
// 4. Resolve serverId (independent)
// 4. Resolve thinking option (depends on effective model)
const initialThinkingOptionId =
typeof initialValues?.thinkingOptionId === "string"
? initialValues.thinkingOptionId.trim()
: "";
const preferredThinkingOptionId =
providerPrefs?.thinkingOptionId?.trim() ?? "";
if (!userModified.thinkingOptionId) {
if (initialThinkingOptionId.length > 0) {
result.thinkingOptionId = initialThinkingOptionId;
} else if (preferredThinkingOptionId.length > 0) {
result.thinkingOptionId = preferredThinkingOptionId;
} else {
result.thinkingOptionId = "";
}
}
// Validate thinking option once model metadata is available.
if (availableModels) {
const effectiveModel = resolveEffectiveModel(availableModels, result.model);
const thinkingOptions = effectiveModel?.thinkingOptions ?? [];
if (thinkingOptions.length === 0) {
result.thinkingOptionId = "";
} else {
const thinkingIds = new Set(thinkingOptions.map((option) => option.id));
const defaultThinkingOptionId =
effectiveModel?.defaultThinkingOptionId ??
thinkingOptions[0]?.id ??
"";
if (!result.thinkingOptionId || !thinkingIds.has(result.thinkingOptionId)) {
result.thinkingOptionId = defaultThinkingOptionId;
}
}
}
// 5. Resolve serverId (independent)
// Only use stored serverId if the host still exists in the registry
if (!userModified.serverId) {
if (initialValues?.serverId !== undefined) {
@@ -186,7 +255,7 @@ function resolveFormState(
// else keep current
}
// 5. Resolve workingDir (independent)
// 6. Resolve workingDir (independent)
if (!userModified.workingDir) {
if (initialValues?.workingDir !== undefined) {
result.workingDir = initialValues.workingDir;
@@ -258,6 +327,7 @@ export function useAgentFormState(
provider: DEFAULT_PROVIDER,
modeId: DEFAULT_MODE_FOR_DEFAULT_PROVIDER,
model: "",
thinkingOptionId: "",
workingDir: "",
}));
const formStateRef = useRef(formState);
@@ -342,6 +412,7 @@ export function useAgentFormState(
resolved.provider !== formStateRef.current.provider ||
resolved.modeId !== formStateRef.current.modeId ||
resolved.model !== formStateRef.current.model ||
resolved.thinkingOptionId !== formStateRef.current.thinkingOptionId ||
resolved.workingDir !== formStateRef.current.workingDir
) {
setFormState(resolved);
@@ -430,6 +501,7 @@ export function useAgentFormState(
provider,
modeId: providerPrefs?.mode ?? providerDef?.defaultModeId ?? "",
model: providerPrefs?.model ?? "",
thinkingOptionId: providerPrefs?.thinkingOptionId ?? "",
}));
},
[preferences?.providerPreferences, updatePreferences]
@@ -453,6 +525,15 @@ export function useAgentFormState(
[formState.provider, updateProviderPreferences]
);
const setThinkingOptionFromUser = useCallback(
(thinkingOptionId: string) => {
setFormState((prev) => ({ ...prev, thinkingOptionId }));
setUserModified((prev) => ({ ...prev, thinkingOptionId: true }));
void updateProviderPreferences(formState.provider, { thinkingOptionId });
},
[formState.provider, updateProviderPreferences]
);
const setWorkingDir = useCallback((value: string) => {
setFormState((prev) => ({ ...prev, workingDir: value }));
}, []);
@@ -475,27 +556,40 @@ export function useAgentFormState(
}, [providerModelsQuery]);
const persistFormPreferences = useCallback(async () => {
const providerPreferenceUpdates: {
mode: string;
model: string;
thinkingOptionId?: string;
} = {
mode: formState.modeId,
model: formState.model,
};
if (userModified.thinkingOptionId) {
providerPreferenceUpdates.thinkingOptionId = formState.thinkingOptionId;
}
await updatePreferences({
workingDir: formState.workingDir,
provider: formState.provider,
serverId: formState.serverId ?? undefined,
});
await updateProviderPreferences(formState.provider, {
mode: formState.modeId,
model: formState.model,
});
await updateProviderPreferences(formState.provider, providerPreferenceUpdates);
}, [
formState.modeId,
formState.model,
formState.provider,
formState.serverId,
formState.thinkingOptionId,
formState.workingDir,
userModified.thinkingOptionId,
updatePreferences,
updateProviderPreferences,
]);
const agentDefinition = providerDefinitionMap.get(formState.provider);
const modeOptions = agentDefinition?.modes ?? [];
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
const isModelLoading = providerModelsQuery.isLoading || providerModelsQuery.isFetching;
const modelError =
providerModelsQuery.error instanceof Error ? providerModelsQuery.error.message : null;
@@ -513,6 +607,8 @@ export function useAgentFormState(
setModeFromUser,
selectedModel: formState.model,
setModelFromUser,
selectedThinkingOptionId: formState.thinkingOptionId,
setThinkingOptionFromUser,
workingDir: formState.workingDir,
setWorkingDir,
setWorkingDirFromUser,
@@ -521,6 +617,7 @@ export function useAgentFormState(
agentDefinition,
modeOptions,
availableModels: availableModels ?? [],
availableThinkingOptions,
isModelLoading,
modelError,
refreshProviderModels,
@@ -532,17 +629,20 @@ export function useAgentFormState(
formState.provider,
formState.modeId,
formState.model,
formState.thinkingOptionId,
formState.workingDir,
setSelectedServerId,
setSelectedServerIdFromUser,
setProviderFromUser,
setModeFromUser,
setModelFromUser,
setThinkingOptionFromUser,
setWorkingDir,
setWorkingDirFromUser,
agentDefinition,
modeOptions,
availableModels,
availableThinkingOptions,
isModelLoading,
modelError,
refreshProviderModels,
+161 -15
View File
@@ -4,10 +4,20 @@ import { router, usePathname } from "expo-router";
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
import {
checkoutStatusQueryKey,
type CheckoutStatusPayload,
} from "@/hooks/use-checkout-status-query";
import { queryClient } from "@/query/query-client";
import {
clearCommandCenterFocusRestoreElement,
takeCommandCenterFocusRestoreElement,
} from "@/utils/command-center-focus-restore";
import {
buildNewAgentRoute,
resolveNewAgentWorkingDir,
} from "@/utils/new-agent-routing";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { focusWithRetries } from "@/utils/web-focus";
function agentKey(agent: Pick<AggregatedAgent, "serverId" | "id">): string {
@@ -41,6 +51,74 @@ function parseAgentKeyFromPathname(pathname: string): string | null {
return `${match[1]}:${match[2]}`;
}
function parseAgentRouteFromPathname(
pathname: string
): { serverId: string; agentId: string } | null {
const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)/);
if (!match) return null;
const [, serverId, agentId] = match;
if (!serverId || !agentId) return null;
return { serverId, agentId };
}
type CommandCenterActionDefinition = {
id: string;
title: string;
icon?: "plus" | "settings";
shortcutKeys?: ShortcutKey[];
keywords: string[];
buildRoute: (params: { newAgentRoute: string }) => string;
};
const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
{
id: "new-agent",
title: "New agent",
icon: "plus",
shortcutKeys: ["mod", "alt", "N"],
keywords: ["new", "new agent", "create", "start", "launch", "agent"],
buildRoute: ({ newAgentRoute }) => newAgentRoute,
},
{
id: "settings",
title: "Settings",
icon: "settings",
keywords: ["settings", "preferences", "config", "configuration"],
buildRoute: () => "/settings",
},
];
function matchesActionQuery(
query: string,
action: CommandCenterActionDefinition
): boolean {
const normalized = query.trim().toLowerCase();
if (!normalized) return true;
if (action.title.toLowerCase().includes(normalized)) {
return true;
}
return action.keywords.some((keyword) => keyword.includes(normalized));
}
export type CommandCenterActionItem = {
kind: "action";
id: string;
title: string;
icon?: "plus" | "settings";
route: string;
shortcutKeys?: ShortcutKey[];
};
export type CommandCenterItem =
| {
kind: "action";
action: CommandCenterActionItem;
}
| {
kind: "agent";
agent: AggregatedAgent;
};
export function useCommandCenter() {
const pathname = usePathname();
const { agents } = useAggregatedAgents();
@@ -53,7 +131,7 @@ export function useCommandCenter() {
const [query, setQuery] = useState("");
const [activeIndex, setActiveIndex] = useState(0);
const results = useMemo(() => {
const agentResults = useMemo(() => {
const filtered = agents.filter((agent) => isMatch(agent, query));
filtered.sort(sortAgents);
return filtered;
@@ -64,11 +142,62 @@ export function useCommandCenter() {
[pathname]
);
const newAgentRoute = useMemo(() => {
const routeAgent = parseAgentRouteFromPathname(pathname);
if (!routeAgent) {
return "/agent";
}
const { serverId, agentId } = routeAgent;
const currentAgent = useSessionStore.getState().sessions[serverId]?.agents?.get(agentId);
const cwd = currentAgent?.cwd?.trim();
if (!cwd) {
return "/agent";
}
const checkout =
queryClient.getQueryData<CheckoutStatusPayload>(
checkoutStatusQueryKey(serverId, cwd)
) ?? null;
const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
return buildNewAgentRoute(workingDir);
}, [pathname]);
const actionItems = useMemo(() => {
return COMMAND_CENTER_ACTIONS.filter((action) =>
matchesActionQuery(query, action)
).map<CommandCenterActionItem>((action) => ({
kind: "action",
id: action.id,
title: action.title,
icon: action.icon,
route: action.buildRoute({ newAgentRoute }),
shortcutKeys: action.shortcutKeys,
}));
}, [newAgentRoute, query]);
const items = useMemo(() => {
const next: CommandCenterItem[] = [];
for (const action of actionItems) {
next.push({
kind: "action",
action,
});
}
for (const agent of agentResults) {
next.push({
kind: "agent",
agent,
});
}
return next;
}, [actionItems, agentResults]);
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
const handleSelect = useCallback(
const handleSelectAgent = useCallback(
(agent: AggregatedAgent) => {
didNavigateRef.current = true;
const session = useSessionStore.getState().sessions[agent.serverId];
@@ -86,6 +215,24 @@ export function useCommandCenter() {
[pathname, requestFocusChatInput, setOpen]
);
const handleSelectAction = useCallback((action: CommandCenterActionItem) => {
didNavigateRef.current = true;
clearCommandCenterFocusRestoreElement();
setOpen(false);
router.push(action.route as any);
}, [setOpen]);
const handleSelectItem = useCallback(
(item: CommandCenterItem) => {
if (item.kind === "action") {
handleSelectAction(item.action);
return;
}
handleSelectAgent(item.agent);
},
[handleSelectAction, handleSelectAgent]
);
useEffect(() => {
const prevOpen = prevOpenRef.current;
prevOpenRef.current = open;
@@ -126,10 +273,10 @@ export function useCommandCenter() {
useEffect(() => {
if (!open) return;
if (activeIndex >= results.length) {
setActiveIndex(results.length > 0 ? results.length - 1 : 0);
if (activeIndex >= items.length) {
setActiveIndex(items.length > 0 ? items.length - 1 : 0);
}
}, [activeIndex, open, results.length]);
}, [activeIndex, items.length, open]);
useEffect(() => {
if (!open) return;
@@ -152,21 +299,21 @@ export function useCommandCenter() {
}
if (key === "Enter") {
if (results.length === 0) return;
if (items.length === 0) return;
event.preventDefault();
const index = Math.max(0, Math.min(activeIndex, results.length - 1));
handleSelect(results[index]!);
const index = Math.max(0, Math.min(activeIndex, items.length - 1));
handleSelectItem(items[index]!);
return;
}
if (key === "ArrowDown" || key === "ArrowUp") {
if (results.length === 0) return;
if (items.length === 0) return;
event.preventDefault();
setActiveIndex((current) => {
const delta = key === "ArrowDown" ? 1 : -1;
const next = current + delta;
if (next < 0) return results.length - 1;
if (next >= results.length) return 0;
if (next < 0) return items.length - 1;
if (next >= items.length) return 0;
return next;
});
}
@@ -175,7 +322,7 @@ export function useCommandCenter() {
// react-native-web can stop propagation on key events, so listen in capture phase.
window.addEventListener("keydown", handler, true);
return () => window.removeEventListener("keydown", handler, true);
}, [activeIndex, handleClose, handleSelect, open, results]);
}, [activeIndex, handleClose, handleSelectItem, items, open]);
return {
open,
@@ -184,9 +331,8 @@ export function useCommandCenter() {
setQuery,
activeIndex,
setActiveIndex,
results,
items,
handleClose,
handleSelect,
handleSelectItem,
};
}
@@ -10,6 +10,7 @@ const FORM_PREFERENCES_QUERY_KEY = ["form-preferences"];
const providerPreferencesSchema = z.object({
model: z.string().optional(),
mode: z.string().optional(),
thinkingOptionId: z.string().optional(),
});
const formPreferencesSchema = z.object({
@@ -3,8 +3,18 @@ import { Platform } from "react-native";
import { usePathname, useRouter } from "expo-router";
import { getIsTauri } from "@/constants/layout";
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
import { useSessionStore } from "@/stores/session-store";
import { parseSidebarAgentKey } from "@/utils/sidebar-shortcuts";
import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
import {
checkoutStatusQueryKey,
type CheckoutStatusPayload,
} from "@/hooks/use-checkout-status-query";
import { queryClient } from "@/query/query-client";
import {
buildNewAgentRoute,
resolveNewAgentWorkingDir,
} from "@/utils/new-agent-routing";
export function useGlobalKeyboardNav({
enabled,
@@ -81,6 +91,29 @@ export function useGlobalKeyboardNav({
navigate(`/agent/${serverId}/${agentId}` as any);
};
const navigateToNewAgent = () => {
let target = "/agent";
if (selectedAgentId) {
const separatorIndex = selectedAgentId.indexOf(":");
if (separatorIndex > 0) {
const serverId = selectedAgentId.slice(0, separatorIndex);
const agentId = selectedAgentId.slice(separatorIndex + 1);
const agent = useSessionStore.getState().sessions[serverId]?.agents?.get(agentId);
const cwd = agent?.cwd?.trim();
if (cwd) {
const checkout =
queryClient.getQueryData<CheckoutStatusPayload>(
checkoutStatusQueryKey(serverId, cwd)
) ?? null;
const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
target = buildNewAgentRoute(workingDir);
}
}
}
router.push(target as any);
};
const handleKeyDown = (event: KeyboardEvent) => {
if (!shouldHandle()) {
return;
@@ -103,9 +136,28 @@ export function useGlobalKeyboardNav({
}
}
const isMod = event.metaKey || event.ctrlKey;
const isKeyN = event.code === "KeyN" || lowerKey === "n";
// Cmd/Ctrl+Alt+N: new agent (web + Tauri)
// Note: intentionally works even when focus is inside an input/textarea.
if (isMod && event.altKey && !event.shiftKey && isKeyN) {
event.preventDefault();
navigateToNewAgent();
return;
}
// Cmd/Ctrl+N: new agent (Tauri only)
// Note: intentionally works even when focus is inside an input/textarea.
if (isTauri && isMod && !event.altKey && !event.shiftKey && isKeyN) {
event.preventDefault();
navigateToNewAgent();
return;
}
// Cmd+B: toggle sidebar
if (
(event.metaKey || event.ctrlKey) &&
isMod &&
(event.code === "KeyB" || lowerKey === "b")
) {
// The MessageInput already handles Cmd+B inside editable fields. If we also
@@ -121,7 +173,7 @@ export function useGlobalKeyboardNav({
// Cmd+.: toggle sidebar (VS Code quick-fix muscle memory)
// Note: intentionally works even when focus is inside an input/textarea.
if (
(event.metaKey || event.ctrlKey) &&
isMod &&
(event.code === "Period" || key === ".")
) {
// Ignore while command center is open.
@@ -137,7 +189,7 @@ export function useGlobalKeyboardNav({
if (
selectedAgentId &&
toggleFileExplorer &&
(event.metaKey || event.ctrlKey) &&
isMod &&
(event.code === "KeyE" || lowerKey === "e")
) {
// Same double-toggle issue as Cmd+B when focus is inside a text input.
@@ -168,7 +220,7 @@ export function useGlobalKeyboardNav({
}
// Cmd+K: command center
if ((event.metaKey || event.ctrlKey) && lowerKey === "k") {
if (isMod && lowerKey === "k") {
event.preventDefault();
const s = useKeyboardNavStore.getState();
if (!s.commandCenterOpen) {
@@ -205,7 +257,7 @@ export function useGlobalKeyboardNav({
}
// Cmd/Ctrl+number: Tauri only (avoid browser tab switching)
if (isTauri && (event.metaKey || event.ctrlKey)) {
if (isTauri && isMod) {
event.preventDefault();
navigateToSidebarShortcut(digit);
}