feat(a2ui): Implement A2UI backend with question handling and WebSocket support

- Added A2UITypes for defining question structures and answers.
- Created A2UIWebSocketHandler for managing WebSocket connections and message handling.
- Developed ask-question tool for interactive user questions via A2UI.
- Introduced platformUtils for platform detection and shell command handling.
- Centralized TypeScript types in index.ts for better organization.
- Implemented compatibility checks for hook templates based on platform requirements.
This commit is contained in:
catlog22
2026-01-31 15:27:12 +08:00
parent 4e009bb03a
commit 715ef12c92
163 changed files with 19495 additions and 715 deletions

View File

@@ -16,6 +16,8 @@ import {
} from '../lib/api';
import type { SessionMetadata } from '../types/store';
import { dashboardStatsKeys } from './useDashboardStats';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { workspaceQueryKeys } from '@/lib/queryKeys';
// Query key factory
export const sessionsKeys = {
@@ -80,12 +82,16 @@ export interface UseSessionsReturn {
export function useSessions(options: UseSessionsOptions = {}): UseSessionsReturn {
const { filter, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
// Only enable query when projectPath is available
const queryEnabled = enabled && !!projectPath;
const query = useQuery({
queryKey: sessionsKeys.list(filter),
queryKey: workspaceQueryKeys.sessionsList(projectPath),
queryFn: fetchSessions,
staleTime,
enabled,
enabled: queryEnabled,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
@@ -130,7 +136,7 @@ export function useSessions(options: UseSessionsOptions = {}): UseSessionsReturn
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
await queryClient.invalidateQueries({ queryKey: workspaceQueryKeys.sessions(projectPath) });
};
return {
@@ -163,14 +169,8 @@ export function useCreateSession(): UseCreateSessionReturn {
const mutation = useMutation({
mutationFn: createSession,
onSuccess: (newSession) => {
// Update sessions cache
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return { activeSessions: [newSession], archivedSessions: [] };
return {
...old,
activeSessions: [newSession, ...old.activeSessions],
};
});
// Invalidate sessions cache to trigger refetch
queryClient.invalidateQueries({ queryKey: ['workspace'] });
// Invalidate dashboard stats
queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
},
@@ -198,19 +198,9 @@ export function useUpdateSession(): UseUpdateSessionReturn {
const mutation = useMutation({
mutationFn: ({ sessionId, input }: { sessionId: string; input: UpdateSessionInput }) =>
updateSession(sessionId, input),
onSuccess: (updatedSession) => {
// Update sessions cache
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return old;
return {
activeSessions: old.activeSessions.map((s) =>
s.session_id === updatedSession.session_id ? updatedSession : s
),
archivedSessions: old.archivedSessions.map((s) =>
s.session_id === updatedSession.session_id ? updatedSession : s
),
};
});
onSuccess: () => {
// Invalidate sessions cache to trigger refetch
queryClient.invalidateQueries({ queryKey: ['workspace'] });
},
});
@@ -235,43 +225,9 @@ export function useArchiveSession(): UseArchiveSessionReturn {
const mutation = useMutation({
mutationFn: archiveSession,
onMutate: async (sessionId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: sessionsKeys.all });
// Snapshot previous value
const previousSessions = queryClient.getQueryData<SessionsResponse>(sessionsKeys.list());
// Optimistically update
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return old;
const session = old.activeSessions.find((s) => s.session_id === sessionId);
if (!session) return old;
const archivedSession: SessionMetadata = {
...session,
status: 'archived',
location: 'archived',
updated_at: new Date().toISOString(),
};
return {
activeSessions: old.activeSessions.filter((s) => s.session_id !== sessionId),
archivedSessions: [archivedSession, ...old.archivedSessions],
};
});
return { previousSessions };
},
onError: (_error, _sessionId, context) => {
// Rollback on error
if (context?.previousSessions) {
queryClient.setQueryData(sessionsKeys.list(), context.previousSessions);
}
},
onSettled: () => {
onSuccess: () => {
// Invalidate to ensure sync with server
queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
queryClient.invalidateQueries({ queryKey: ['workspace'] });
queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
},
});
@@ -297,33 +253,9 @@ export function useDeleteSession(): UseDeleteSessionReturn {
const mutation = useMutation({
mutationFn: deleteSession,
onMutate: async (sessionId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: sessionsKeys.all });
// Snapshot previous value
const previousSessions = queryClient.getQueryData<SessionsResponse>(sessionsKeys.list());
// Optimistically remove
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return old;
return {
activeSessions: old.activeSessions.filter((s) => s.session_id !== sessionId),
archivedSessions: old.archivedSessions.filter((s) => s.session_id !== sessionId),
};
});
return { previousSessions };
},
onError: (_error, _sessionId, context) => {
// Rollback on error
if (context?.previousSessions) {
queryClient.setQueryData(sessionsKeys.list(), context.previousSessions);
}
},
onSettled: () => {
onSuccess: () => {
// Invalidate to ensure sync with server
queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
queryClient.invalidateQueries({ queryKey: ['workspace'] });
queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
},
});