feat: add live model and thinking option preferences UI (#8)

* feat: add live model and thinking option preferences UI

* refactor: remove agent variant support and clean up thinking options

* feat: live model + thinking presets (RPC)
This commit is contained in:
Mohamed Boudra
2026-02-04 08:18:15 +07:00
committed by GitHub
parent 03f383b908
commit 469a25c724
50 changed files with 1976 additions and 894 deletions
+36 -115
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
AGENT_PROVIDER_DEFINITIONS,
type AgentProviderDefinition,
@@ -76,11 +77,6 @@ type UseAgentFormStateResult = {
isModelLoading: boolean;
modelError: string | null;
refreshProviderModels: () => void;
queueProviderModelFetch: (
serverId: string | null,
options?: { cwd?: string; delayMs?: number }
) => void;
clearQueuedProviderModelRequest: (serverId: string | null) => void;
workingDirIsEmpty: boolean;
persistFormPreferences: () => Promise<void>;
};
@@ -278,20 +274,40 @@ export function useAgentFormState(
}
}, [isVisible]);
// Get session state for provider models
// Session state for provider model listing
const sessionState = useSessionStore((state) =>
formState.serverId ? state.sessions[formState.serverId] : undefined
);
const providerModels = sessionState?.providerModels;
const requestProviderModels = sessionState?.methods?.requestProviderModels;
const getSessionState = useCallback(
(serverId: string) => useSessionStore.getState().sessions[serverId] ?? null,
[]
);
const client = sessionState?.client ?? null;
const isConnected = sessionState?.connection?.isConnected ?? false;
// Get available models for current provider
const modelState = providerModels?.get(formState.provider);
const availableModels = modelState?.models ?? null;
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 providerModelsQuery = useQuery({
queryKey: ["providerModels", formState.serverId, formState.provider, debouncedCwd],
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(formState.provider, {
cwd: debouncedCwd,
});
if (payload.error) {
throw new Error(payload.error);
}
return payload.models ?? [];
},
});
const availableModels = providerModelsQuery.data ?? null;
// Combine initialValues with initialServerId for resolution
const combinedInitialValues = useMemo((): FormInitialValues | undefined => {
@@ -360,11 +376,6 @@ export function useAgentFormState(
updatePreferences,
]);
// Provider model request timers
const providerModelRequestTimersRef = useRef<
Map<string, ReturnType<typeof setTimeout>>
>(new Map());
// User setters - mark fields as modified and persist to preferences
const setSelectedServerIdFromUser = useCallback(
(value: string | null) => {
@@ -432,14 +443,8 @@ export function useAgentFormState(
}, []);
const refreshProviderModels = useCallback(() => {
if (!requestProviderModels) {
return;
}
const trimmed = formState.workingDir.trim();
requestProviderModels(formState.provider, {
cwd: trimmed.length > 0 ? trimmed : undefined,
});
}, [requestProviderModels, formState.provider, formState.workingDir]);
void providerModelsQuery.refetch();
}, [providerModelsQuery]);
const persistFormPreferences = useCallback(async () => {
await updatePreferences({
@@ -461,91 +466,11 @@ export function useAgentFormState(
updateProviderPreferences,
]);
const clearQueuedProviderModelRequest = useCallback((serverId: string | null) => {
if (!serverId) {
return;
}
const timer = providerModelRequestTimersRef.current.get(serverId);
if (timer) {
clearTimeout(timer);
providerModelRequestTimersRef.current.delete(serverId);
}
}, []);
const queueProviderModelFetch = useCallback(
(
serverId: string | null,
options?: { cwd?: string; delayMs?: number }
) => {
if (!serverId || !getSessionState) {
clearQueuedProviderModelRequest(serverId);
return;
}
const sessionState = getSessionState(serverId);
if (!sessionState?.connection?.isConnected) {
clearQueuedProviderModelRequest(serverId);
return;
}
const currentState = sessionState.providerModels?.get(formState.provider);
if (currentState?.models?.length || currentState?.isLoading) {
clearQueuedProviderModelRequest(serverId);
return;
}
const delayMs = options?.delayMs ?? 0;
const trigger = () => {
providerModelRequestTimersRef.current.delete(serverId);
sessionState.methods?.requestProviderModels(formState.provider, {
...(options?.cwd ? { cwd: options.cwd } : {}),
});
};
clearQueuedProviderModelRequest(serverId);
if (delayMs > 0) {
providerModelRequestTimersRef.current.set(serverId, setTimeout(trigger, delayMs));
} else {
trigger();
}
},
[clearQueuedProviderModelRequest, getSessionState, formState.provider]
);
useEffect(() => {
return () => {
providerModelRequestTimersRef.current.forEach((timer) => {
clearTimeout(timer);
});
providerModelRequestTimersRef.current.clear();
};
}, []);
useEffect(() => {
if (!isVisible || !isTargetDaemonReady || !formState.serverId) {
clearQueuedProviderModelRequest(formState.serverId);
return;
}
const trimmed = formState.workingDir.trim();
queueProviderModelFetch(formState.serverId, {
cwd: trimmed.length > 0 ? trimmed : undefined,
delayMs: 180,
});
return () => {
clearQueuedProviderModelRequest(formState.serverId);
};
}, [
clearQueuedProviderModelRequest,
isTargetDaemonReady,
isVisible,
queueProviderModelFetch,
formState.serverId,
formState.workingDir,
]);
const agentDefinition = providerDefinitionMap.get(formState.provider);
const modeOptions = agentDefinition?.modes ?? [];
const isModelLoading = modelState?.isLoading ?? false;
const modelError = modelState?.error ?? null;
const isModelLoading = providerModelsQuery.isLoading || providerModelsQuery.isFetching;
const modelError =
providerModelsQuery.error instanceof Error ? providerModelsQuery.error.message : null;
const workingDirIsEmpty = !formState.workingDir.trim();
@@ -571,8 +496,6 @@ export function useAgentFormState(
isModelLoading,
modelError,
refreshProviderModels,
queueProviderModelFetch,
clearQueuedProviderModelRequest,
workingDirIsEmpty,
persistFormPreferences,
}),
@@ -595,8 +518,6 @@ export function useAgentFormState(
isModelLoading,
modelError,
refreshProviderModels,
queueProviderModelFetch,
clearQueuedProviderModelRequest,
workingDirIsEmpty,
persistFormPreferences,
]
@@ -0,0 +1,113 @@
import { useCallback } from "react";
import { useSessionStore } from "@/stores/session-store";
import {
createInitDeferred,
getInitDeferred,
getInitKey,
rejectInitDeferred,
} from "@/utils/agent-initialization";
const INIT_TIMEOUT_MS = 10000;
export function useAgentInitialization(serverId: string) {
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
const setInitializingAgents = useSessionStore((state) => state.setInitializingAgents);
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
const clearAgentStreamHead = useSessionStore((state) => state.clearAgentStreamHead);
const ensureAgentIsInitialized = useCallback(
(agentId: string): Promise<void> => {
const key = getInitKey(serverId, agentId);
const existing = getInitDeferred(key);
if (existing) {
return existing.promise;
}
const deferred = createInitDeferred(key);
const timeoutId = setTimeout(() => {
rejectInitDeferred(key, new Error(`Agent initialization timed out after ${INIT_TIMEOUT_MS}ms`));
}, INIT_TIMEOUT_MS);
setInitializingAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentId, true);
return next;
});
setAgentStreamTail(serverId, (prev) => {
const next = new Map(prev);
next.set(agentId, []);
return next;
});
clearAgentStreamHead(serverId, agentId);
if (!client) {
clearTimeout(timeoutId);
setInitializingAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentId, false);
return next;
});
rejectInitDeferred(key, new Error("Host is not connected"));
return deferred.promise;
}
client
.initializeAgent(agentId)
.then(() => {
clearTimeout(timeoutId);
})
.catch((error) => {
clearTimeout(timeoutId);
setInitializingAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentId, false);
return next;
});
rejectInitDeferred(
key,
error instanceof Error ? error : new Error(String(error))
);
});
return deferred.promise;
},
[clearAgentStreamHead, client, serverId, setAgentStreamTail, setInitializingAgents]
);
const refreshAgent = useCallback(
async (agentId: string) => {
if (!client) {
throw new Error("Host is not connected");
}
setInitializingAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentId, true);
return next;
});
setAgentStreamTail(serverId, (prev) => {
const next = new Map(prev);
next.set(agentId, []);
return next;
});
clearAgentStreamHead(serverId, agentId);
try {
await client.refreshAgent(agentId);
} catch (error) {
setInitializingAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentId, false);
return next;
});
throw error;
}
},
[clearAgentStreamHead, client, serverId, setAgentStreamTail, setInitializingAgents]
);
return { ensureAgentIsInitialized, refreshAgent };
}
@@ -5,6 +5,7 @@ import { useSessionStore } from "@/stores/session-store";
import type { AgentDirectoryEntry } from "@/types/agent-directory";
import type { Agent } from "@/stores/session-store";
import { isPerfLoggingEnabled } from "@/utils/perf";
import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots";
export interface AggregatedAgent extends AgentDirectoryEntry {
serverId: string;
@@ -32,11 +33,11 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
})
);
const sessionMethods = useSessionStore(
const sessionClients = useSessionStore(
useShallow((state) => {
const result: Record<string, NonNullable<typeof state.sessions[string]["methods"]> | undefined> = {};
const result: Record<string, NonNullable<typeof state.sessions[string]["client"]> | null> = {};
for (const [serverId, session] of Object.entries(state.sessions)) {
result[serverId] = session.methods ?? undefined;
result[serverId] = session.client ?? null;
}
return result;
})
@@ -44,13 +45,43 @@ export function useAggregatedAgents(): AggregatedAgentsResult {
const refreshAll = useCallback(() => {
console.log('[useAggregatedAgents] Manual refresh triggered for all sessions');
for (const [serverId, methods] of Object.entries(sessionMethods)) {
if (methods?.refreshSession) {
console.log(`[useAggregatedAgents] Refreshing session ${serverId}`);
methods.refreshSession();
for (const [serverId, client] of Object.entries(sessionClients)) {
if (!client) {
continue;
}
void (async () => {
try {
console.log(`[useAggregatedAgents] Refreshing session ${serverId}`);
const agentsList = await client.fetchAgents({
filter: { labels: { ui: "true" } },
});
const agents = new Map();
const pendingPermissions = new Map();
const agentLastActivity = new Map();
for (const snapshot of agentsList) {
const agent = normalizeAgentSnapshot(snapshot, serverId);
agents.set(agent.id, agent);
agentLastActivity.set(agent.id, agent.lastActivityAt);
for (const request of agent.pendingPermissions) {
const key = derivePendingPermissionKey(agent.id, request);
pendingPermissions.set(key, { key, agentId: agent.id, request });
}
}
const store = useSessionStore.getState();
store.setAgents(serverId, agents);
for (const [agentId, timestamp] of agentLastActivity.entries()) {
store.setAgentLastActivity(agentId, timestamp);
}
store.setPendingPermissions(serverId, pendingPermissions);
} catch (error) {
console.warn("[useAggregatedAgents] Failed to refresh session", { serverId, error });
}
})();
}
}, [sessionMethods]);
}, [sessionClients]);
const result = useMemo(() => {
const allAgents: AggregatedAgent[] = [];
@@ -0,0 +1,206 @@
import { useCallback } from "react";
import { useSessionStore, type AgentFileExplorerState } from "@/stores/session-store";
function createExplorerState(): AgentFileExplorerState {
return {
directories: new Map(),
files: new Map(),
isLoading: false,
lastError: null,
pendingRequest: null,
currentPath: ".",
history: ["."],
lastVisitedPath: ".",
};
}
function pushHistory(history: string[], path: string): string[] {
const normalizedHistory = history.length === 0 ? ["."] : history;
const last = normalizedHistory[normalizedHistory.length - 1];
if (last === path) {
return normalizedHistory;
}
return [...normalizedHistory, path];
}
export function useFileExplorerActions(serverId: string) {
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
const setFileExplorer = useSessionStore((state) => state.setFileExplorer);
const updateExplorerState = useCallback(
(agentId: string, updater: (prev: AgentFileExplorerState) => AgentFileExplorerState) => {
setFileExplorer(serverId, (prev) => {
const next = new Map(prev);
const current = next.get(agentId) ?? createExplorerState();
next.set(agentId, updater(current));
return next;
});
},
[serverId, setFileExplorer]
);
const requestDirectoryListing = useCallback(
(agentId: string, path: string, options?: { recordHistory?: boolean }) => {
const normalizedPath = path && path.length > 0 ? path : ".";
const shouldRecordHistory = options?.recordHistory ?? true;
updateExplorerState(agentId, (state) => ({
...state,
isLoading: true,
lastError: null,
pendingRequest: { path: normalizedPath, mode: "list" },
currentPath: normalizedPath,
history: shouldRecordHistory ? pushHistory(state.history, normalizedPath) : state.history,
lastVisitedPath: normalizedPath,
}));
if (!client) {
updateExplorerState(agentId, (state) => ({
...state,
isLoading: false,
lastError: "Host is not connected",
pendingRequest: null,
}));
return;
}
void client
.exploreFileSystem(agentId, normalizedPath, "list")
.then((payload) => {
updateExplorerState(agentId, (state) => {
const nextState: AgentFileExplorerState = {
...state,
isLoading: false,
lastError: payload.error ?? null,
pendingRequest: null,
directories: state.directories,
files: state.files,
};
if (!payload.error && payload.directory) {
const directories = new Map(state.directories);
directories.set(payload.directory.path, payload.directory);
nextState.directories = directories;
}
return nextState;
});
})
.catch((error) => {
updateExplorerState(agentId, (state) => ({
...state,
isLoading: false,
lastError: error instanceof Error ? error.message : "Failed to list directory",
pendingRequest: null,
}));
});
},
[client, updateExplorerState]
);
const requestFilePreview = useCallback(
(agentId: string, path: string) => {
const normalizedPath = path && path.length > 0 ? path : ".";
updateExplorerState(agentId, (state) => ({
...state,
isLoading: true,
pendingRequest: { path: normalizedPath, mode: "file" },
}));
if (!client) {
updateExplorerState(agentId, (state) => ({
...state,
isLoading: false,
lastError: "Host is not connected",
pendingRequest: null,
}));
return;
}
void client
.exploreFileSystem(agentId, normalizedPath, "file")
.then((payload) => {
updateExplorerState(agentId, (state) => {
const nextState: AgentFileExplorerState = {
...state,
isLoading: false,
pendingRequest: null,
directories: state.directories,
files: state.files,
};
if (!payload.error && payload.file) {
const files = new Map(state.files);
files.set(payload.file.path, payload.file);
nextState.files = files;
} else if (payload.error) {
nextState.lastError = payload.error;
}
return nextState;
});
})
.catch(() => {
updateExplorerState(agentId, (state) => ({
...state,
isLoading: false,
pendingRequest: null,
}));
});
},
[client, updateExplorerState]
);
const requestFileDownloadToken = useCallback(
async (agentId: string, path: string) => {
if (!client) {
throw new Error("Host is not connected");
}
const payload = await client.requestDownloadToken(agentId, path);
if (payload.error) {
throw new Error(payload.error);
}
return payload;
},
[client]
);
const navigateExplorerBack = useCallback(
(agentId: string) => {
let targetPath: string | null = null;
updateExplorerState(agentId, (state) => {
if (state.history.length <= 1) {
return state;
}
const nextHistory = state.history.slice(0, -1);
targetPath = nextHistory[nextHistory.length - 1] ?? ".";
return {
...state,
isLoading: true,
lastError: null,
pendingRequest: { path: targetPath, mode: "list" },
currentPath: targetPath,
history: nextHistory,
lastVisitedPath: targetPath,
};
});
if (!targetPath) {
return null;
}
requestDirectoryListing(agentId, targetPath, { recordHistory: false });
return targetPath;
},
[requestDirectoryListing, updateExplorerState]
);
return {
requestDirectoryListing,
requestFilePreview,
requestFileDownloadToken,
navigateExplorerBack,
};
}