Integrate TanStack Query v5#12
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughComplete migration of the frontend data layer to TanStack Query v5: adds a singleton QueryClient, typed queryKeys, ~31 new query/mutation hooks across workspace/session/repo/settings, expands service APIs (repo, workspace, settings, memory), replaces component-level fetch/state/polling with query-driven flows, and adds React Query DevTools. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Component
participant QH as React Query Hook
participant QC as Query Cache
participant Svc as Service
participant API as Backend
rect rgb(240,248,255)
Note over UI,API: Read flow via React Query
UI->>QH: call useWorkspacesByRepo(state)
QH->>QC: check cache (queryKeys.workspaces.byRepo)
alt cache miss / stale
QH->>Svc: fetchByRepo(state)
Svc->>API: GET /workspaces?state=...
API-->>Svc: workspace list
Svc-->>QC: cache result
else cache hit
QC-->>QH: return cached data
end
QH-->>UI: { data, isLoading, error }
end
rect rgb(240,255,240)
Note over UI,QC: Mutation + invalidation
UI->>QH: createWorkspaceMutation.mutate(payload)
QH->>Svc: create(payload)
Svc->>API: POST /workspaces
API-->>Svc: new workspace
Svc-->>QH: success
QH->>QC: invalidate queryKeys.workspaces.*
QC->>QH: trigger refetch
QH->>Svc: fetchByRepo(state)
Svc->>API: GET /workspaces
API-->>Svc: updated list
Svc-->>QC: update cache
QC-->>UI: re-render with new data
end
sequenceDiagram
participant UI as Component
participant Bulk as useBulkDiffStats
participant QC as Query Cache
participant Svc as WorkspaceService
rect rgb(255,250,240)
Note over UI,Svc: Progressive bulk diff prefetch
UI->>Bulk: useBulkDiffStats(repoGroups)
Bulk->>QC: fetch initial N diffStats immediately
loop first batch
QC->>Svc: fetch diffStats(workspaceId)
Svc-->>QC: cache entry
end
Bulk-->>UI: return initial aggregated stats
loop background staggered prefetch
Bulk->>QC: prefetch next diffStats with timeouts
QC->>Svc: background fetch
Svc-->>QC: cache populated
end
Bulk-->>UI: aggregate updates as cache fills
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
Comment |
There was a problem hiding this comment.
Additional Comments (1)
-
src/WorkspaceChatPanel.tsx, line 143-147 (link)logic: dependencies array missing — compactConversation, createPR, stopSession are recreated on every render causing infinite effect loop
24 files reviewed, 30 comments
| async function saveSetting(key: string, value: any) { | ||
| setSaving(true); | ||
| try { | ||
| const baseURL = await getBaseURL(); | ||
| const res = await fetch(`${baseURL}/settings`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ key, value }) | ||
| }); | ||
| if (!res.ok) { | ||
| throw new Error(`Failed to save: ${res.status}`); | ||
| } | ||
| setSettings(prev => ({ ...prev, [key]: value })); | ||
| await updateSettingsMutation.mutateAsync({ [key]: value }); | ||
| } catch (error) { | ||
| console.error('Failed to save setting:', error); | ||
| toast.error(`Failed to save setting: ${error instanceof Error ? error.message : String(error)}`); | ||
| } finally { | ||
| setSaving(false); | ||
| } | ||
| } |
There was a problem hiding this comment.
logic: error handling doesn't prevent optimistic state updates — mutation will still show 'Saving...' even when it fails internally. should the UI show a distinct error state vs just hiding the 'Saving...' indicator?
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/dashboard/components/SettingsModal.tsx
Line: 73:80
Comment:
**logic:** error handling doesn't prevent optimistic state updates — mutation will still show 'Saving...' even when it fails internally. should the UI show a distinct error state vs just hiding the 'Saving...' indicator?
How can I resolve this? If you propose a fix, please make it concise.| settings: Settings; | ||
| setSettings: React.Dispatch<React.SetStateAction<Settings>>; | ||
| saveSetting: <K extends keyof Settings>(key: K, value: Settings[K]) => void; | ||
| saveSetting: (key: string, value: any) => void; |
There was a problem hiding this comment.
style: any type loses all type safety for settings values. Use Settings[keyof Settings] instead:
| saveSetting: (key: string, value: any) => void; | |
| saveSetting: (key: keyof Settings, value: Settings[keyof Settings]) => void; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/dashboard/components/settings-sections/types.ts
Line: 5:5
Comment:
**style:** `any` type loses all type safety for settings values. Use `Settings[keyof Settings]` instead:
```suggestion
saveSetting: (key: keyof Settings, value: Settings[keyof Settings]) => void;
```
How can I resolve this? If you propose a fix, please make it concise.| defaultValue={settings.user_name ?? ''} | ||
| onBlur={(e) => saveSetting('user_name', e.currentTarget.value)} |
There was a problem hiding this comment.
logic: Input fields will lose user's typed changes if settings are re-fetched (e.g. from polling or cache updates). Use controlled inputs with local state or a form library like react-hook-form to prevent loss of user input during the save window.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/features/dashboard/components/settings-sections/AccountSection.tsx
Line: 15:16
Comment:
**logic:** Input fields will lose user's typed changes if settings are re-fetched (e.g. from polling or cache updates). Use controlled inputs with local state or a form library like react-hook-form to prevent loss of user input during the save window.
How can I resolve this? If you propose a fix, please make it concise.| return useQuery({ | ||
| queryKey: queryKeys.repos.all, | ||
| queryFn: () => RepoService.fetchAll(), | ||
| staleTime: 10000, // Repos don't change often |
There was a problem hiding this comment.
style: Consider increasing staleTime to match workspace polling intervals (30s-60s) to avoid unnecessary refetches when navigating between dashboard views
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/queries/useRepoQueries.ts
Line: 18:18
Comment:
**style:** Consider increasing `staleTime` to match workspace polling intervals (30s-60s) to avoid unnecessary refetches when navigating between dashboard views
How can I resolve this? If you propose a fix, please make it concise.| export function useRepo(id: string | null) { | ||
| return useQuery({ | ||
| queryKey: queryKeys.repos.detail(id || ''), | ||
| queryFn: () => RepoService.fetchById(id!), |
There was a problem hiding this comment.
style: Non-null assertion (id!) is safe here because enabled: !!id guards against null/empty values, but consider using id || '' for consistency with line 27
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/queries/useRepoQueries.ts
Line: 28:28
Comment:
**style:** Non-null assertion (`id!`) is safe here because `enabled: !!id` guards against null/empty values, but consider using `id || ''` for consistency with line 27
How can I resolve this? If you propose a fix, please make it concise.| // Queries for repos, settings, system prompt | ||
| const reposQuery = useRepos(); | ||
| const settingsQuery = useSettingsQuery(); | ||
| const systemPromptQuery = useSystemPrompt(selectedWorkspace?.id || null); |
There was a problem hiding this comment.
style: useSystemPrompt query runs even when no workspace is selected (receives null). If the hook doesn't handle null workspaceId gracefully with enabled: false, this will trigger unnecessary failed queries.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Dashboard.tsx
Line: 121:121
Comment:
**style:** `useSystemPrompt` query runs even when no workspace is selected (receives `null`). If the hook doesn't handle `null` workspaceId gracefully with `enabled: false`, this will trigger unnecessary failed queries.
How can I resolve this? If you propose a fix, please make it concise.| fetchPRStatus: async (id: string): Promise<PRStatus | null> => { | ||
| return apiClient.get<PRStatus | null>(`/workspaces/${id}/pr-status`); | ||
| }, |
There was a problem hiding this comment.
style: Endpoint string should use ENDPOINTS constant from config (e.g., ENDPOINTS.WORKSPACE_PR_STATUS(id)). All other methods in this service use the centralized config; this inline string breaks that pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/workspace.service.ts
Line: 89:91
Comment:
**style:** Endpoint string should use `ENDPOINTS` constant from config (e.g., `ENDPOINTS.WORKSPACE_PR_STATUS(id)`). All other methods in this service use the centralized config; this inline string breaks that pattern.
How can I resolve this? If you propose a fix, please make it concise.| fetchDevServers: async (id: string): Promise<{ servers: DevServer[] }> => { | ||
| return apiClient.get<{ servers: DevServer[] }>(`/workspaces/${id}/dev-servers`); | ||
| }, |
There was a problem hiding this comment.
style: Endpoint string should use ENDPOINTS constant from config (e.g., ENDPOINTS.WORKSPACE_DEV_SERVERS(id)). All other methods in this service use the centralized config; this inline string breaks that pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/workspace.service.ts
Line: 96:98
Comment:
**style:** Endpoint string should use `ENDPOINTS` constant from config (e.g., `ENDPOINTS.WORKSPACE_DEV_SERVERS(id)`). All other methods in this service use the centralized config; this inline string breaks that pattern.
How can I resolve this? If you propose a fix, please make it concise.| fetchSystemPrompt: async (id: string): Promise<{ system_prompt: string }> => { | ||
| return apiClient.get<{ system_prompt: string }>(`/workspaces/${id}/system-prompt`); | ||
| }, |
There was a problem hiding this comment.
style: Endpoint string should use ENDPOINTS constant from config (e.g., ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id)). All other methods in this service use the centralized config; this inline string breaks that pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/workspace.service.ts
Line: 103:105
Comment:
**style:** Endpoint string should use `ENDPOINTS` constant from config (e.g., `ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id)`). All other methods in this service use the centralized config; this inline string breaks that pattern.
How can I resolve this? If you propose a fix, please make it concise.| updateSystemPrompt: async (id: string, systemPrompt: string): Promise<void> => { | ||
| return apiClient.put(`/workspaces/${id}/system-prompt`, { system_prompt: systemPrompt }); | ||
| }, |
There was a problem hiding this comment.
style: Endpoint string should use ENDPOINTS constant from config (e.g., ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id)). All other methods in this service use the centralized config; this inline string breaks that pattern.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/workspace.service.ts
Line: 110:112
Comment:
**style:** Endpoint string should use `ENDPOINTS` constant from config (e.g., `ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id)`). All other methods in this service use the centralized config; this inline string breaks that pattern.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Dashboard.tsx (1)
637-655: Make the clickable file row accessibleInteractive div lacks keyboard and role semantics. Add role/button semantics and keyboard activation.
- <div + <div + role="button" + tabIndex={0} + onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && handleFileClick(file.file)} key={index} className="flex items-center justify-between p-2.5 rounded-lg hover:bg-sidebar-accent/60 cursor-pointer transition-[background-color,box-shadow] duration-200 ease-out group elevation-1 hover:elevation-2" onClick={() => handleFileClick(file.file)} title="Click to view diff" >
🧹 Nitpick comments (12)
src/App.tsx (1)
31-31: Consider DevTools placement for theme support.The
ReactQueryDevtoolsis currently outside theThemeProvider. If you want the devtools panel to respect the app's theme, consider moving it insideThemeProvider(but still insideQueryClientProvider). This is optional and depends on whether theme consistency for devtools is desired.</ThemeProvider> - {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />} </QueryClientProvider>Move inside ThemeProvider if theme support is desired:
<Toaster /> </ErrorBoundary> + {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />} </ThemeProvider> </QueryClientProvider>src/WorkspaceChatPanel.tsx (2)
104-128: Stabilize action handlers to prevent effect churn.Wrap
sendMessage,stopSession,createPR, andcompactConversationinuseCallbacksouseEffectdoesn’t re-register handlers on every render.Apply this diff:
+import { useCallback } from "react"; @@ - const sendMessage = async (customContent?: string) => { + const sendMessage = useCallback(async (customContent?: string) => { const content = customContent || messageInput.trim(); if (!content || sendMessageMutation.isPending) return; try { await sendMessageMutation.mutateAsync({ sessionId, content }); setMessageInput(''); } catch (error) { console.error('Failed to send message:', error); } - }; + }, [messageInput, sendMessageMutation, sessionId]); @@ - const stopSession = async () => { + const stopSession = useCallback(async () => { if (!window.confirm('Stop the current Claude Code session?')) return; try { await stopSessionMutation.mutateAsync(sessionId); } catch (error) { console.error('Failed to stop session:', error); } - }; + }, [stopSessionMutation, sessionId]); @@ - const createPR = () => sendMessage('Create a PR onto main'); - const compactConversation = () => sendMessage('/compact'); + const createPR = useCallback(() => sendMessage('Create a PR onto main'), [sendMessage]); + const compactConversation = useCallback(() => sendMessage('/compact'), [sendMessage]); @@ - }, [compactConversation, createPR, stopSession, onCompact, onCreatePR, onStop]); + }, [compactConversation, createPR, stopSession, onCompact, onCreatePR, onStop]);Also applies to: 142-148
179-186: Replace hardcoded colors with Tailwind theme tokens.
bg-white/70,dark:bg-black/60, andbg-black/70violate the color token guideline. Use semantic tokens from your Tailwind config (e.g.,bg-background/70,dark:bg-card/60,bg-overlay/70) for consistency and theming.As per coding guidelines
Also applies to: 191-207, 247-254
src/lib/queryKeys.ts (1)
15-23: Avoidundefinedsegments in keys.
byRepo(state?: string)can emit['workspaces','by-repo', undefined]. Prefer omitting optional segments for canonical keys.Example:
byRepo: (state?: string) => state ? (['workspaces', 'by-repo', state] as const) : (['workspaces', 'by-repo'] as const),src/services/workspace.service.ts (1)
86-112: Use ENDPOINTS constants for new routes to avoid drift.Existing methods use
ENDPOINTS.*but new ones hardcode paths. Centralize for consistency and future changes.Proposed:
- fetchPRStatus: async (id: string): Promise<PRStatus | null> => { - return apiClient.get<PRStatus | null>(`/workspaces/${id}/pr-status`); - }, + fetchPRStatus: async (id: string): Promise<PRStatus | null> => { + return apiClient.get<PRStatus | null>(ENDPOINTS.WORKSPACE_PR_STATUS(id)); + }, @@ - fetchDevServers: async (id: string): Promise<{ servers: DevServer[] }> => { - return apiClient.get<{ servers: DevServer[] }>(`/workspaces/${id}/dev-servers`); - }, + fetchDevServers: async (id: string): Promise<{ servers: DevServer[] }> => { + return apiClient.get<{ servers: DevServer[] }>(ENDPOINTS.WORKSPACE_DEV_SERVERS(id)); + }, @@ - fetchSystemPrompt: async (id: string): Promise<{ system_prompt: string }> => { - return apiClient.get<{ system_prompt: string }>(`/workspaces/${id}/system-prompt`); - }, + fetchSystemPrompt: async (id: string): Promise<{ system_prompt: string }> => { + return apiClient.get<{ system_prompt: string }>(ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id)); + }, @@ - updateSystemPrompt: async (id: string, systemPrompt: string): Promise<void> => { - return apiClient.put(`/workspaces/${id}/system-prompt`, { system_prompt: systemPrompt }); - }, + updateSystemPrompt: async (id: string, systemPrompt: string): Promise<void> => { + return apiClient.put<void>(ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id), { system_prompt: systemPrompt }); + },And add matching entries in
api.config:WORKSPACE_PR_STATUS: (id: string) => `/workspaces/${id}/pr-status`, WORKSPACE_DEV_SERVERS: (id: string) => `/workspaces/${id}/dev-servers`, WORKSPACE_SYSTEM_PROMPT: (id: string) => `/workspaces/${id}/system-prompt`,src/hooks/queries/useSettingsQueries.ts (1)
84-91: Optionally invalidate related caches after memory clear.If clearing memory affects session history/UI, also invalidate session-related queries.
Example:
export function useClearMemory() { return useMutation({ mutationFn: async () => { const { MemoryService } = await import('@/services/memory.service'); return MemoryService.clear(); }, + onSuccess: () => { + // e.g. refresh conversations/messages if they depend on memory + // queryClient.invalidateQueries({ queryKey: queryKeys.sessions.all, exact: false }); + }, }); }src/hooks/queries/useSessionQueries.ts (3)
32-45: Align messages polling interval with session status.Use session status to speed up messages when
working, slow whenidle.Example:
-export function useMessages(sessionId: string | null) { +export function useMessages(sessionId: string | null, status?: SessionStatus) { return useQuery({ queryKey: queryKeys.sessions.messages(sessionId || ''), queryFn: () => SessionService.fetchMessages(sessionId!), enabled: !!sessionId, - // Dynamic polling based on session status - refetchInterval: (query) => { - // We need to get session status from the session query - // For now, use conservative polling - return 2000; - }, + refetchInterval: () => (status === 'working' ? 1000 : 3000), staleTime: 500, }); }And pass
sessionStatusfromuseSessionWithMessages.
121-133: Also invalidate/remove messages on stop.Stopping a session should refresh both detail and messages to reflect the new state.
Apply this diff:
return useMutation({ mutationFn: (sessionId: string) => SessionService.stop(sessionId), onSuccess: (_, sessionId) => { // Invalidate session to update status queryClient.invalidateQueries({ queryKey: queryKeys.sessions.detail(sessionId), }); + queryClient.invalidateQueries({ + queryKey: queryKeys.sessions.messages(sessionId), + }); + // Optionally remove messages cache if it should disappear immediately + // queryClient.removeQueries({ queryKey: queryKeys.sessions.messages(sessionId) }); }, });
66-84: Type the toolResultMap for clarity.Narrow
Maptyping to improve DX and avoidanybleed.Example:
- const toolResultMap = useMemo(() => { - const map = new Map(); + const toolResultMap = useMemo<Map<string, { tool_use_id: string; type: 'tool_result'; [k: string]: unknown }>>(() => { + const map = new Map<string, { tool_use_id: string; type: 'tool_result'; [k: string]: unknown }>();src/Dashboard.tsx (2)
489-491: Use design tokens instead of hardcoded colorsReplace bg-white/70 and dark:bg-black/60 with theme tokens to meet styling guidelines.
As per coding guidelines
- className="flex-1 rounded-lg bg-white/70 dark:bg-black/60 backdrop-blur-[20px] border border-border/40 vibrancy-shadow overflow-hidden transition-colors duration-200 min-h-0" + className="flex-1 rounded-lg bg-background/70 backdrop-blur-[20px] border border-border/40 vibrancy-shadow overflow-hidden transition-colors duration-200 min-h-0"
7-9: Remove unused imports/variablesgetBaseURL, formatTokenCount, useCloneRepo import, cloneRepoMutation variable, and baseURL are unused.
-import { getBaseURL } from "./config/api.config"; -import { formatTokenCount } from "./utils"; +// removed unused imports @@ - useCloneRepo, useSystemPrompt, useUpdateSystemPrompt, useSettings as useSettingsQuery, @@ - const cloneRepoMutation = useCloneRepo(); + // removed unused mutation @@ - const baseURL = await getBaseURL(); + // removed unused baseURLAlso applies to: 30-30, 139-139, 411-411
src/hooks/queries/useWorkspaceQueries.ts (1)
40-47: Avoid empty-string IDs in query keysPassing '' into query keys can increase collision risk and noise in DevTools. Since enabled guards already prevent execution, key the query only with the real ID.
- return useQuery({ - queryKey: queryKeys.workspaces.diffStats(workspaceId || ''), + return useQuery({ + queryKey: workspaceId ? queryKeys.workspaces.diffStats(workspaceId) : ['workspaces','diff-stats','__none__'], @@ - queryKey: queryKeys.workspaces.diffFiles(workspaceId || ''), + queryKey: workspaceId ? queryKeys.workspaces.diffFiles(workspaceId) : ['workspaces','diff-files','__none__'], @@ - queryKey: queryKeys.workspaces.prStatus(workspaceId || ''), + queryKey: workspaceId ? queryKeys.workspaces.prStatus(workspaceId) : ['workspaces','pr-status','__none__'], @@ - queryKey: queryKeys.workspaces.devServers(workspaceId || ''), + queryKey: workspaceId ? queryKeys.workspaces.devServers(workspaceId) : ['workspaces','dev-servers','__none__'], @@ - queryKey: queryKeys.workspaces.diffFile(workspaceId || '', filePath || ''), + queryKey: workspaceId && filePath ? queryKeys.workspaces.diffFile(workspaceId, filePath) : ['workspaces','diff-file','__none__','__none__'],Also applies to: 109-118, 124-131, 136-145, 151-160
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (24)
TANSTACK_QUERY_COMPLETE.md(1 hunks)TANSTACK_QUERY_MIGRATION.md(1 hunks)package.json(1 hunks)src/App.tsx(1 hunks)src/Dashboard.tsx(9 hunks)src/WorkspaceChatPanel.tsx(2 hunks)src/features/dashboard/components/SettingsModal.tsx(4 hunks)src/features/dashboard/components/settings-sections/AccountSection.tsx(5 hunks)src/features/dashboard/components/settings-sections/MemorySection.tsx(5 hunks)src/features/dashboard/components/settings-sections/ProviderSection.tsx(2 hunks)src/features/dashboard/components/settings-sections/types.ts(1 hunks)src/hooks/index.ts(1 hunks)src/hooks/queries/index.ts(1 hunks)src/hooks/queries/useRepoQueries.ts(1 hunks)src/hooks/queries/useSessionQueries.ts(1 hunks)src/hooks/queries/useSettingsQueries.ts(1 hunks)src/hooks/queries/useWorkspaceQueries.ts(1 hunks)src/lib/queryClient.ts(1 hunks)src/lib/queryKeys.ts(1 hunks)src/services/api.ts(1 hunks)src/services/memory.service.ts(1 hunks)src/services/repo.service.ts(1 hunks)src/services/settings.service.ts(1 hunks)src/services/workspace.service.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{tsx,jsx,css}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx,css}: Avoid hardcoding colors; always use color tokens from Tailwind config
Always use font tokens from the Tailwind config (avoid hardcoded font sizes/families)
Use consistent paddings with a default of 16px unless there’s a strong reason otherwise
Files:
src/features/dashboard/components/settings-sections/AccountSection.tsxsrc/WorkspaceChatPanel.tsxsrc/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/features/dashboard/components/SettingsModal.tsxsrc/features/dashboard/components/settings-sections/MemorySection.tsxsrc/Dashboard.tsxsrc/App.tsx
**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{css,tsx,jsx}: Default to ease-out for most animations
Animations should be shorter than 1s; most between 0.2s and 0.3s
Do not use built-in CSS easings excepteaseorlinear
Prefer ease-out for entering/user-initiated interactions; use ease-in-out for in-place movements; generally avoid ease-in
Useeasewith 200ms duration for simple hover transitions (color, background-color, opacity)
Disable hover transitions on touch devices using@media (hover: hover) and (pointer: fine)
Respect prefers-reduced-motion: disable transform-based animations when reduced motion is requested
Animate elements from their trigger by setting an appropriate transform-origin
Prefer animating opacity and transform; avoid animating positional properties like top/left
Do not animate drag gestures using CSS variables
Do not animate blur values higher than 20px
Usewill-changesparingly and only for transform, opacity, clipPath, or filter
Files:
src/features/dashboard/components/settings-sections/AccountSection.tsxsrc/WorkspaceChatPanel.tsxsrc/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/features/dashboard/components/SettingsModal.tsxsrc/features/dashboard/components/settings-sections/MemorySection.tsxsrc/Dashboard.tsxsrc/App.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx}: When using Framer Motion, use transform rather than x/y for hardware-accelerated animations
Default to spring animations with Framer Motion
Avoid bouncy spring animations unless working with drag gestures
Files:
src/features/dashboard/components/settings-sections/AccountSection.tsxsrc/WorkspaceChatPanel.tsxsrc/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/features/dashboard/components/SettingsModal.tsxsrc/features/dashboard/components/settings-sections/MemorySection.tsxsrc/Dashboard.tsxsrc/App.tsx
🧬 Code graph analysis (14)
src/services/memory.service.ts (1)
src/services/api.ts (1)
apiClient(131-131)
src/services/repo.service.ts (3)
src/types/repo.types.ts (1)
Repo(10-17)src/services/api.ts (1)
apiClient(131-131)src/config/api.config.ts (1)
ENDPOINTS(158-180)
src/features/dashboard/components/settings-sections/AccountSection.tsx (1)
src/features/dashboard/components/settings-sections/types.ts (1)
SettingsSectionProps(3-6)
src/WorkspaceChatPanel.tsx (2)
src/hooks/queries/useSessionQueries.ts (3)
useSessionWithMessages(51-95)useSendMessage(100-116)useStopSession(121-133)src/types/index.ts (2)
FileChangeGroup(18-18)FileEdit(17-17)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (2)
src/features/dashboard/components/settings-sections/index.ts (2)
ProviderSection(4-4)SettingsSectionProps(6-6)src/features/dashboard/components/settings-sections/types.ts (1)
SettingsSectionProps(3-6)
src/hooks/queries/useRepoQueries.ts (3)
src/lib/queryKeys.ts (1)
queryKeys(12-51)src/services/repo.service.ts (1)
RepoService(10-45)src/lib/queryClient.ts (1)
queryClient(8-39)
src/hooks/queries/useSessionQueries.ts (2)
src/lib/queryKeys.ts (1)
queryKeys(12-51)src/lib/queryClient.ts (1)
queryClient(8-39)
src/hooks/queries/useSettingsQueries.ts (4)
src/lib/queryKeys.ts (1)
queryKeys(12-51)src/services/settings.service.ts (1)
SettingsService(9-30)src/lib/queryClient.ts (1)
queryClient(8-39)src/services/memory.service.ts (1)
MemoryService(8-15)
src/features/dashboard/components/SettingsModal.tsx (1)
src/hooks/queries/useSettingsQueries.ts (6)
useSettings(14-20)useMCPServers(25-31)useCommands(36-42)useAgents(47-53)useHooks(58-64)useUpdateSettings(69-79)
src/features/dashboard/components/settings-sections/MemorySection.tsx (2)
src/features/dashboard/components/settings-sections/types.ts (1)
SettingsSectionProps(3-6)src/hooks/queries/useSettingsQueries.ts (1)
useClearMemory(84-91)
src/Dashboard.tsx (5)
src/hooks/queries/useWorkspaceQueries.ts (3)
useWorkspacesByRepo(16-23)useStats(28-35)useBulkDiffStats(54-104)src/WorkspaceChatPanel.tsx (1)
WorkspaceChatPanelRef(32-34)src/hooks/queries/useRepoQueries.ts (3)
useRepos(14-20)useAddRepo(37-48)useCloneRepo(53-65)src/hooks/index.ts (1)
useKeyboardShortcuts(3-3)src/services/workspace.service.ts (1)
WorkspaceService(18-113)
src/App.tsx (3)
src/lib/queryClient.ts (1)
queryClient(8-39)src/hooks/useTheme.tsx (1)
ThemeProvider(13-81)src/components/ErrorBoundary.tsx (1)
ErrorBoundary(20-185)
src/services/workspace.service.ts (2)
src/types/github.types.ts (2)
PRStatus(10-16)DevServer(22-27)src/services/api.ts (1)
apiClient(131-131)
src/hooks/queries/useWorkspaceQueries.ts (4)
src/lib/queryKeys.ts (1)
queryKeys(12-51)src/services/workspace.service.ts (1)
WorkspaceService(18-113)src/config/api.config.ts (1)
API_CONFIG(152-156)src/services/repo.service.ts (1)
RepoService(10-45)
🔇 Additional comments (19)
package.json (1)
46-47: LGTM! TanStack Query v5 dependencies added correctly.The version ranges are appropriate (^5.90.x allows patch updates), and both packages are aligned within the same major/minor series. The main query package (5.90.5) and devtools (5.90.2) are recent stable releases from the v5 line.
src/services/api.ts (1)
36-45: LGTM! PUT method implementation is consistent with existing patterns.The new
putmethod correctly mirrors thePOSTimplementation with proper Content-Type headers, optional data parameter handling, and type-safe return values. This addition supports the workspace system prompt updates and other PUT operations introduced in the TanStack Query migration.src/services/memory.service.ts (1)
1-15: LGTM! Clean service implementation for memory operations.The
MemoryServiceis well-structured with a focused API surface. Theclearmethod properly uses the API client and returns a typed Promise. This integrates cleanly with theuseClearMemorymutation hook.src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
12-12: LGTM! Props simplified to align with updated interface.The removal of
setSettingsfrom the component signature correctly matches the updatedSettingsSectionPropsinterface, streamlining the component API to use only the mutation-basedsaveSettingcallback.src/features/dashboard/components/settings-sections/MemorySection.tsx (1)
13-18: Excellent migration to TanStack Query mutation!The refactor from manual fetch/useState to
useClearMemorymutation simplifies the code and aligns with the broader TanStack Query migration. The mutation state (isPending) is correctly used for button disabled state, and error handling is preserved.Also applies to: 57-82
src/App.tsx (1)
13-32: LGTM! QueryClientProvider integration is correct.The React Query setup is properly structured:
QueryClientProviderwraps the entire app at the root level (required for context)ReactQueryDevtoolsis conditionally rendered in DEV mode onlyinitialIsOpen={false}prevents UI obstruction on startup- Existing providers and routing preserved correctly
src/hooks/queries/index.ts (1)
1-16: LGTM! Clean central export for query hooks.This barrel export file provides a well-organized single entry point for all TanStack Query hooks, grouped by domain (workspace, session, settings, repository). The modular structure supports maintainability and clear separation of concerns.
src/services/repo.service.ts (1)
32-44: LGTM! Repository mutation methods implemented correctly.The new
addandclonemethods are well-implemented:
- Consistent with existing service patterns
- Proper type safety with
Promise<Repo>return types- Correct API parameter naming (
root_pathmatches backend contract)- Clean integration with
useAddRepoanduseCloneRepomutation hooksThese additions properly support the TanStack Query migration for repository management operations.
src/hooks/index.ts (1)
1-12: LGTM! Clean migration to TanStack Query.The re-export structure is clear, and keeping deprecated hooks as comments aids in understanding what was replaced.
src/services/settings.service.ts (1)
9-30: LGTM! Clean service layer.The API abstraction is straightforward and follows the established pattern. Error handling is appropriately delegated to the apiClient and query hooks.
TANSTACK_QUERY_MIGRATION.md (1)
1-207: LGTM! Comprehensive migration guide.The documentation clearly explains the benefits, architecture, and migration path. The before/after examples are particularly helpful.
src/features/dashboard/components/settings-sections/AccountSection.tsx (1)
5-54: LGTM! Proper migration to uncontrolled inputs.The shift to
defaultValue+onBlurpersistence aligns with the TanStack Query migration pattern and reduces unnecessary re-renders during typing. Changes are saved when the user moves focus away from each field.src/hooks/queries/useRepoQueries.ts (1)
37-65: LGTM! Proper mutation setup with cache invalidation.Both mutations correctly invalidate related queries (repos and workspaces) after success, ensuring the UI reflects the updated state.
src/features/dashboard/components/SettingsModal.tsx (3)
57-72: LGTM! Clean migration to TanStack Query hooks.The hooks provide automatic loading/caching, and the empty defaults (
{},[]) ensure the UI renders gracefully when data isn't available yet.
73-80: LGTM! Proper mutation-based persistence.The
saveSettingfunction delegates to the TanStack Query mutation, which handles async state, retries, and cache invalidation automatically. Error handling with toast notifications provides good user feedback.
285-285: LGTM! Simplified props align with migration.Removing
setSettingsfrom section props is consistent with the move to mutation-based updates, where sections no longer manage local state directly.src/lib/queryKeys.ts (1)
12-23: Good, consistent hierarchical keys.Const-asserted tuples and namespacing look solid; easy invalidation via prefixes.
src/hooks/queries/useSettingsQueries.ts (1)
69-79: Invalidate dependent views after settings update is correct.Using
queryKeys.settings.allensures prefix invalidation for all settings queries. Nice.If any components cache derived settings (e.g., provider lists), confirm they subscribe to these keys so they refetch post-update.
TANSTACK_QUERY_COMPLETE.md (1)
5-6: Doc claims overstate the migration status
- “Every single data-fetching operation” uses TanStack Query: Dashboard still fetches a file diff directly via WorkspaceService (see src/Dashboard.tsx Lines 264-268).
- “Dynamic intervals (1–3s)”: not implemented in provided hooks; most use a fixed POLL_INTERVAL.
Please align the doc or implement the missing pieces.
Also applies to: 17-19
| // Sync diff stats to store (for compatibility with existing code) | ||
| useEffect(() => { | ||
| if (Object.keys(hookDiffStats).length > 0) { | ||
| setMultipleDiffStats(hookDiffStats); | ||
| if (diffStatsQuery.data) { | ||
| setMultipleDiffStats(diffStatsQuery.data); | ||
| } | ||
| }, [hookDiffStats, setMultipleDiffStats]); | ||
| }, [diffStatsQuery.data, setMultipleDiffStats]); | ||
|
|
There was a problem hiding this comment.
Bulk diff stats never include background-prefetched workspaces
You sync only diffStatsQuery.data, but useBulkDiffStats returns the first 5 items and prefetched ones don’t update this aggregate. Store remains incomplete and flips on each refetch.
Consider either aggregating from the cache after each prefetch (fix in hook), or derive per-workspace stats via useQueries and compute the map locally.
| const error = await res.json(); | ||
| throw new Error(error.error || 'Failed to add repository'); | ||
| } | ||
| const folderPath = typeof selected === 'string' ? selected : (selected as any).path; |
There was a problem hiding this comment.
Handle Tauri dialog ‘open’ result robustly
open({ directory: true }) can return string | string[] | null. Current code assumes an object with path, which may be invalid.
- const folderPath = typeof selected === 'string' ? selected : (selected as any).path;
+ const folderPath =
+ Array.isArray(selected) ? selected[0] :
+ (typeof selected === 'string' ? selected : '');
+ if (!folderPath) return;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const folderPath = typeof selected === 'string' ? selected : (selected as any).path; | |
| const folderPath = | |
| Array.isArray(selected) ? selected[0] : | |
| (typeof selected === 'string' ? selected : ''); | |
| if (!folderPath) return; |
🤖 Prompt for AI Agents
In src/Dashboard.tsx around line 331, the selected value returned from Tauri's
open({ directory: true }) can be string | string[] | null (and sometimes an
object with a path), so replace the current assumption with robust branching:
check for null and bail/handle early; if selected is a string use it; if it's an
array use the first element (and validate it's a string); if it's an object with
a path property use that (and validate it's a string); otherwise treat as
invalid and handle accordingly (e.g., show an error or return). Ensure no unsafe
casts are used and validate that the final folderPath is a non-empty string
before proceeding.
| export function useRepo(id: string | null) { | ||
| return useQuery({ | ||
| queryKey: queryKeys.repos.detail(id || ''), | ||
| queryFn: () => RepoService.fetchById(id!), | ||
| enabled: !!id, | ||
| staleTime: 10000, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Potential cache key issue with null handling.
When id is null, the query key becomes ['repos', 'detail', ''] due to the fallback id || ''. While the query is disabled via enabled: !!id, using an empty string as a cache key could cause collisions if '' becomes a valid repo ID in the future.
Consider a more explicit null-safe approach:
export function useRepo(id: string | null) {
return useQuery({
- queryKey: queryKeys.repos.detail(id || ''),
- queryFn: () => RepoService.fetchById(id!),
+ queryKey: id ? queryKeys.repos.detail(id) : ['repos', 'detail', 'null'],
+ queryFn: () => RepoService.fetchById(id as string),
enabled: !!id,
staleTime: 10000,
});
}Or adjust the type signature to avoid null entirely:
export function useRepo(id: string | undefined) {
return useQuery({
queryKey: queryKeys.repos.detail(id ?? 'placeholder'),
queryFn: () => RepoService.fetchById(id!),
enabled: !!id,
staleTime: 10000,
});
}🤖 Prompt for AI Agents
In src/hooks/queries/useRepoQueries.ts around lines 25–32 the code uses id || ''
which makes the cache key ['repos','detail',''] when id is null; to avoid
potential key collisions, change the function signature to accept id: string |
undefined (not null), use a clear explicit placeholder in the query key (e.g. id
?? '__no-id__' or leave the id value as-is and include it directly in the key so
it's nullable-safe), and guard the queryFn so it never calls
RepoService.fetchById with a null/undefined value (return or throw early if id
is falsy). Keep enabled: !!id so the query is disabled when id is missing.
Ensure no non-null assertion (!) remains in fetchById call.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/features/dashboard/components/settings-sections/types.ts (1)
5-5: Type safety regression: union type permits mismatched key-value pairs.While
(key: keyof Settings, value: Settings[keyof Settings])is better than theanyversion previously flagged, it still allows type mismatches that the original generic signature prevented. For example:// This incorrectly type-checks with the union signature: saveSetting('theme', 123); // theme expects 'light' | 'dark' | 'system' saveSetting('notifications_enabled', 'yes'); // expects booleanThe original generic signature
<K extends keyof Settings>(key: K, value: Settings[K]) => voidenforced key-value correspondence at compile time.If TanStack Query mutation hooks have issues with generics, restore the generic signature:
export interface SettingsSectionProps { settings: Settings; - saveSetting: (key: keyof Settings, value: Settings[keyof Settings]) => void; + saveSetting: <K extends keyof Settings>(key: K, value: Settings[K]) => void; }If the generic truly causes runtime issues with your mutation setup, document why the type safety trade-off is necessary with a comment explaining the constraint.
src/Dashboard.tsx (3)
90-97: Gate bulk diff stats to avoid double execution on initial renderCalling useBulkDiffStats with [] then with real data triggers two runs. Pass an enabled flag (if supported) so it waits for data.
#!/bin/bash # Inspect the hook signature and internal useQuery options rg -nP "export function useBulkDiffStats\s*\(" -C2 rg -n "useQuery\(" -C3 src/hooks/queries | rg -n "bulk.*diff|useBulkDiffStats" -C3If supported:
-const diffStatsQuery = useBulkDiffStats(workspacesQuery.data || []); +const diffStatsQuery = useBulkDiffStats(workspacesQuery.data || [], { enabled: !!workspacesQuery.data?.length });Also applies to: 99-105
125-133: Ensure useSystemPrompt doesn’t run for null workspaceConfirm the hook internally sets enabled: false when workspaceId is null; otherwise add an enabled option.
#!/bin/bash rg -nP "export function useSystemPrompt\s*\(" -C3 rg -n "useQuery\\(" -C3 src/hooks/queries | rg -n "systemPrompt|SystemPrompt" -C3Also applies to: 106-111
341-345: Handle Tauri dialog return types robustly (string | string[] | null)Current logic misses string[] and may deref an object without path.
- const folderPath = typeof selected === 'string' ? selected : (selected as any).path; + const folderPath = + Array.isArray(selected) ? (typeof selected[0] === 'string' ? selected[0] : '') : + (typeof selected === 'string' ? selected : + (selected && typeof (selected as any).path === 'string' ? (selected as any).path : '')); + if (!folderPath) return;
🧹 Nitpick comments (3)
src/WorkspaceChatPanel.tsx (1)
43-44: Remove unused isConnectedThe value isn’t referenced. Drop it to avoid lint noise.
- const { isConnected } = useSocket(); + useSocket(); // keep side-effects if needed; otherwise remove entirelysrc/Dashboard.tsx (2)
421-423: Remove unused baseURL assignmentbaseURL is fetched but not used.
- const baseURL = await getBaseURL();
418-439: Replace shell git clone with useCloneRepo mutation for consistency and proper error handlingYour review comment is accurate.
cloneRepoMutationis initialized at line 149 but not used inhandleCloneRepository. The current code shells out directly and then callsaddRepoMutation, whileRepoService.clone(via the unusedcloneRepoMutation) is purpose-built to handle both cloning and database registration in a single backend call.The refactoring you suggested—replacing the shell command with
cloneRepoMutation.mutateAsync({ url: githubUrl, targetPath: cloneTarget })—is valid and would eliminate the workaround and provide proper mutation benefits (error handling, retries, query invalidation).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (10)
src/Dashboard.tsx(9 hunks)src/WorkspaceChatPanel.tsx(3 hunks)src/config/api.config.ts(1 hunks)src/features/dashboard/components/settings-sections/MemorySection.tsx(3 hunks)src/features/dashboard/components/settings-sections/ProviderSection.tsx(2 hunks)src/features/dashboard/components/settings-sections/types.ts(1 hunks)src/hooks/queries/useWorkspaceQueries.ts(1 hunks)src/lib/queryClient.ts(1 hunks)src/services/workspace.service.ts(2 hunks)src/types/settings.types.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/hooks/queries/useWorkspaceQueries.ts
- src/features/dashboard/components/settings-sections/MemorySection.tsx
- src/lib/queryClient.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{tsx,jsx,css}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx,css}: Avoid hardcoding colors; always use color tokens from Tailwind config
Always use font tokens from the Tailwind config (avoid hardcoded font sizes/families)
Use consistent paddings with a default of 16px unless there’s a strong reason otherwise
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/WorkspaceChatPanel.tsxsrc/Dashboard.tsx
**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{css,tsx,jsx}: Default to ease-out for most animations
Animations should be shorter than 1s; most between 0.2s and 0.3s
Do not use built-in CSS easings excepteaseorlinear
Prefer ease-out for entering/user-initiated interactions; use ease-in-out for in-place movements; generally avoid ease-in
Useeasewith 200ms duration for simple hover transitions (color, background-color, opacity)
Disable hover transitions on touch devices using@media (hover: hover) and (pointer: fine)
Respect prefers-reduced-motion: disable transform-based animations when reduced motion is requested
Animate elements from their trigger by setting an appropriate transform-origin
Prefer animating opacity and transform; avoid animating positional properties like top/left
Do not animate drag gestures using CSS variables
Do not animate blur values higher than 20px
Usewill-changesparingly and only for transform, opacity, clipPath, or filter
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/WorkspaceChatPanel.tsxsrc/Dashboard.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx}: When using Framer Motion, use transform rather than x/y for hardware-accelerated animations
Default to spring animations with Framer Motion
Avoid bouncy spring animations unless working with drag gestures
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/WorkspaceChatPanel.tsxsrc/Dashboard.tsx
🧬 Code graph analysis (4)
src/services/workspace.service.ts (2)
src/types/github.types.ts (2)
PRStatus(10-16)DevServer(22-27)src/config/api.config.ts (1)
ENDPOINTS(158-183)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (2)
src/features/dashboard/components/settings-sections/index.ts (2)
ProviderSection(4-4)SettingsSectionProps(6-6)src/features/dashboard/components/settings-sections/types.ts (1)
SettingsSectionProps(3-6)
src/features/dashboard/components/settings-sections/types.ts (1)
src/types/settings.types.ts (1)
Settings(10-41)
src/WorkspaceChatPanel.tsx (3)
src/hooks/queries/useSessionQueries.ts (3)
useSessionWithMessages(51-95)useSendMessage(100-116)useStopSession(121-133)src/types/index.ts (2)
FileChangeGroup(18-18)FileEdit(17-17)src/services/socket.ts (2)
sendMessage(109-115)stopSession(120-125)
🔇 Additional comments (11)
src/config/api.config.ts (1)
167-169: LGTM! New endpoint factories follow established patterns.The three new workspace endpoints are well-structured and consistent with the existing endpoint declarations. They properly support the expanded workspace API surface for PR status, dev servers, and system prompt operations.
src/services/workspace.service.ts (5)
14-15: LGTM! Type imports properly support new service methods.The PRStatus and DevServer type imports are correctly added to support the new workspace API methods.
89-91: LGTM! Previous review feedback addressed.The method now correctly uses
ENDPOINTS.WORKSPACE_PR_STATUS(id)from the centralized config, resolving the previous inline string concern. The nullable return type appropriately reflects that workspaces may not have associated PRs.
96-98: LGTM! Previous review feedback addressed.The method now correctly uses
ENDPOINTS.WORKSPACE_DEV_SERVERS(id)from the centralized config, resolving the previous inline string concern.
103-105: LGTM! Previous review feedback addressed.The method now correctly uses
ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id)from the centralized config, resolving the previous inline string concern.
110-112: LGTM! Previous review feedback addressed.The method now correctly uses
ENDPOINTS.WORKSPACE_SYSTEM_PROMPT(id)from the centralized config, resolving the previous inline string concern. The PUT operation appropriately converts the camelCase parameter to snake_case in the request body to match backend conventions.src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
12-12: LGTM - Props migration aligns with TanStack Query pattern.Removal of
setSettingsis consistent with the PR's migration to isolated field persistence viasaveSetting.src/types/settings.types.ts (1)
32-36: LGTM - Type additions resolve previous type safety issues.The addition of
custom_endpoint,conversation_memory_enabled, andmemory_retentionproperly addresses the past review comments about unsafe type casts inProviderSection.tsx. All fields are appropriately typed as optional, maintaining backward compatibility.src/WorkspaceChatPanel.tsx (2)
61-117: Good: memoized and hardened fileChanges computationuseMemo, nullish checks, and robust aggregation look solid and address prior feedback.
261-269: Review comment partially valid; overlay backdrop requires UX considerationTokens
backgroundandcardexist in Tailwind config and the suggestion to use them is valid per coding guidelines. However, the recommendation needs refinement:
- Lines 194, 206 (diff cards): ✓ Replace
bg-white/70 dark:bg-black/60withbg-card/70 dark:bg-card/60for theme consistency- Line 262 (modal overlay): ⚠ Using
bg-background/80may reduce contrast and impact accessibility. Modal overlays typically require darker semi-transparent layers for visual separation. Consider whetherbg-backgroundaligns with your design intent, or if a dedicated overlay/modal token would be more appropriate.Note: Multiple files also use hardcoded overlays (Dashboard.tsx, ui/sheet.tsx, ui/dialog.tsx) that follow this same pattern and may require consistent updates.
src/Dashboard.tsx (1)
733-742: Good: system prompt is now controlled (no DOM queries)This resolves the brittle querySelector approach and aligns with React data flow.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Dashboard.tsx (1)
320-345: Handle Tauri dialog result robustly (string | string[] | null).Line 335 assumes
selectedis either astringor an object with apathproperty, but Tauri'sopen({ directory: true })can returnstring | string[] | null(and sometimes an object depending on the version). The current code doesn't handle thearraycase, which could cause a runtime error if the dialog returns an array.Apply this diff to handle all cases:
- const folderPath = typeof selected === 'string' ? selected : (selected as any).path; + let folderPath: string; + if (typeof selected === 'string') { + folderPath = selected; + } else if (Array.isArray(selected)) { + folderPath = selected[0] || ''; + } else if (selected && typeof selected === 'object' && 'path' in selected) { + folderPath = (selected as any).path; + } else { + return; // User cancelled or invalid result + } + + if (!folderPath) { + return; // No valid path selected + }
♻️ Duplicate comments (1)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
85-86: Controlled input migration LGTM.Binding value and onChange resolves the prior uncontrolled/defaultValue issues.
🧹 Nitpick comments (5)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (3)
16-17: Avoid Node types in browser code: fix timeoutRef typing.Using NodeJS.Timeout couples UI code to @types/node and can break builds. Prefer ReturnType and null-init.
Apply this diff:
- const timeoutRef = useRef<NodeJS.Timeout>(); + const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);Also update clearTimeout sites:
- clearTimeout(timeoutRef.current); + if (timeoutRef.current) clearTimeout(timeoutRef.current);and
- clearTimeout(timeoutRef.current); + if (timeoutRef.current) clearTimeout(timeoutRef.current);
18-22: Prevent external sync from clobbering active edits.When settings.custom_endpoint refetches while the user is typing, this effect overwrites in-flight input. Track lastSaved vs current edit and only sync when user is not dirty.
Minimal adjustment (pairs with the flush fix below):
- useEffect(() => { - setCustomEndpoint(settings.custom_endpoint ?? ''); - }, [settings.custom_endpoint]); + const lastSavedRef = useRef(settings.custom_endpoint ?? ''); + useEffect(() => { + const incoming = settings.custom_endpoint ?? ''; + // only update local state if user hasn't diverged from last saved + if (customEndpoint === lastSavedRef.current) { + setCustomEndpoint(incoming); + } + lastSavedRef.current = incoming; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settings.custom_endpoint]);
23-30: Debounce handler should track pending value and last saved to avoid double/late saves.Store latest edit and last saved in refs; update lastSavedRef when save completes; clear timeout safely.
[Suggesting changes; pairs with unmount flush below.]- const handleEndpointChange = (value: string) => { - setCustomEndpoint(value); - clearTimeout(timeoutRef.current); - timeoutRef.current = setTimeout(() => { - saveSetting('custom_endpoint', value); - }, 500); - }; + const latestValueRef = useRef(customEndpoint); + useEffect(() => { + latestValueRef.current = customEndpoint; + }, [customEndpoint]); + + const handleEndpointChange = (value: string) => { + setCustomEndpoint(value); + latestValueRef.current = value; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + if (latestValueRef.current !== lastSavedRef.current) { + saveSetting('custom_endpoint', latestValueRef.current); + lastSavedRef.current = latestValueRef.current; + } + timeoutRef.current = null; + }, 500); + };src/WorkspaceChatPanel.tsx (1)
171-179: Optional: Remove stablesetMessageInputfrom dependency array.The
setMessageInputfunction returned byuseStatehas a stable identity and doesn't need to be in theuseImperativeHandledependency array. While harmless, removing it slightly clarifies intent.Apply this diff:
- }), [setMessageInput]); + }), []);src/Dashboard.tsx (1)
84-93: Optional: Clarify intent by addingenabledoption.The
useBulkDiffStatshook safely handles the empty array (it hasenabled: workspaceIds.length > 0internally), so this won't cause failed queries. However, addingenabled: !!workspacesQuery.dataat the call site makes the data dependency explicit and avoids a redundant render whenworkspacesQuery.datatransitions fromundefinedto[]to actual data.- const diffStatsQuery = useBulkDiffStats(workspacesQuery.data || []); + const diffStatsQuery = useBulkDiffStats(workspacesQuery.data || [], { + enabled: !!workspacesQuery.data, + });Note: This assumes
useBulkDiffStatsaccepts an options parameter. If not, the current code is fine as-is.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (3)
src/Dashboard.tsx(10 hunks)src/WorkspaceChatPanel.tsx(3 hunks)src/features/dashboard/components/settings-sections/ProviderSection.tsx(3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{tsx,jsx,css}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx,css}: Avoid hardcoding colors; always use color tokens from Tailwind config
Always use font tokens from the Tailwind config (avoid hardcoded font sizes/families)
Use consistent paddings with a default of 16px unless there’s a strong reason otherwise
Files:
src/WorkspaceChatPanel.tsxsrc/Dashboard.tsxsrc/features/dashboard/components/settings-sections/ProviderSection.tsx
**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{css,tsx,jsx}: Default to ease-out for most animations
Animations should be shorter than 1s; most between 0.2s and 0.3s
Do not use built-in CSS easings excepteaseorlinear
Prefer ease-out for entering/user-initiated interactions; use ease-in-out for in-place movements; generally avoid ease-in
Useeasewith 200ms duration for simple hover transitions (color, background-color, opacity)
Disable hover transitions on touch devices using@media (hover: hover) and (pointer: fine)
Respect prefers-reduced-motion: disable transform-based animations when reduced motion is requested
Animate elements from their trigger by setting an appropriate transform-origin
Prefer animating opacity and transform; avoid animating positional properties like top/left
Do not animate drag gestures using CSS variables
Do not animate blur values higher than 20px
Usewill-changesparingly and only for transform, opacity, clipPath, or filter
Files:
src/WorkspaceChatPanel.tsxsrc/Dashboard.tsxsrc/features/dashboard/components/settings-sections/ProviderSection.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx}: When using Framer Motion, use transform rather than x/y for hardware-accelerated animations
Default to spring animations with Framer Motion
Avoid bouncy spring animations unless working with drag gestures
Files:
src/WorkspaceChatPanel.tsxsrc/Dashboard.tsxsrc/features/dashboard/components/settings-sections/ProviderSection.tsx
🧬 Code graph analysis (3)
src/WorkspaceChatPanel.tsx (3)
src/hooks/queries/useSessionQueries.ts (3)
useSessionWithMessages(51-95)useSendMessage(100-116)useStopSession(121-133)src/types/index.ts (2)
FileChangeGroup(18-18)FileEdit(17-17)src/services/socket.ts (2)
sendMessage(109-115)stopSession(120-125)
src/Dashboard.tsx (4)
src/hooks/queries/useWorkspaceQueries.ts (9)
useWorkspacesByRepo(17-24)useStats(29-36)useBulkDiffStats(55-117)useSystemPrompt(209-219)usePRStatus(137-144)useDevServers(149-159)useCreateWorkspace(179-189)useArchiveWorkspace(194-204)useUpdateSystemPrompt(224-237)src/hooks/index.ts (1)
useKeyboardShortcuts(3-3)src/services/workspace.service.ts (1)
WorkspaceService(18-113)src/services/index.ts (1)
WorkspaceService(10-10)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (3)
src/features/dashboard/components/settings-sections/index.ts (2)
ProviderSection(4-4)SettingsSectionProps(6-6)src/features/dashboard/components/settings-sections/types.ts (1)
SettingsSectionProps(3-6)backend/server.cjs (1)
settings(263-263)
🔇 Additional comments (18)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
13-13: Props signature change looks good.Removing setSettings aligns with query-driven persistence via saveSetting. No issues.
src/WorkspaceChatPanel.tsx (6)
1-19: LGTM! Clean migration to TanStack Query hooks.The imports correctly include
useMemofor the memoizedfileChangescomputation and the session-related query hooks (useSessionWithMessages,useSendMessage,useStopSession) that replace the legacy data-fetching patterns.
42-60: LGTM! Query and mutation hooks correctly integrated.The component now uses
useSessionWithMessagesfor data and dedicated mutations (useSendMessage,useStopSession) for actions, eliminating manual fetch/state management. LocalmessageInputstate is appropriate for controlled input.
62-123: LGTM! Past review feedback fully addressed.The
fileChangescomputation now:
- ✅ Uses
useMemoto avoid recomputation on every render- ✅ Supports
notebook_pathas fallback (line 71) to avoid droppingNotebookEdittool results- ✅ Sanitizes timestamps per-edit (lines 79-80) and when aggregating (lines 97-99) to prevent
NaNrendering- ✅ Guards against missing
block.inputandfile_path/notebook_path(lines 71-72)- ✅ Handles empty timestamps array gracefully (lines 101-108)
All defensive checks from previous review rounds are now in place.
126-148: LGTM! Mutation-driven actions are well-structured.The handlers correctly leverage TanStack Query mutations:
sendMessageusesuseSendMessage.mutateAsyncwith error handling and input clearingstopSessionusesuseStopSession.mutateAsyncwith user confirmationcreatePRandcompactConversationcompose oversendMessagefor reusabilityDependencies are correct, and the imperative socket-based flow is fully replaced.
151-151: LGTM! Derived state from mutation.Correctly derives
sendingfromsendMessageMutation.isPendingto drive UI feedback.
236-348: LGTM! Rendering correctly wired to query-based data.The component rendering (embedded and modal variants) correctly consumes
messages,loading,sessionStatusfromuseSessionWithMessagesand passes mutation-driven handlers to child components. The migration preserves all UI behavior while eliminating manual state management.src/Dashboard.tsx (11)
16-31: LGTM! Comprehensive query/mutation hook imports.All necessary TanStack Query hooks imported for workspaces, repos, settings, and mutations. Aligns with the data layer migration objectives.
102-106: LGTM! System prompt draft initialization is correct.The
useEffectproperly initializessystemPromptDraftwhen the modal opens and data is available, guarding againstundefinedto distinguish between "not loaded" and "empty string."
120-144: LGTM! Queries and mutations correctly set up.All queries appropriately handle
nullworkspace IDs viaenabledchecks in their hooks (per relevant code snippets). TheuseSystemPrompthook hasenabled: !!workspaceId, so passingnullis safe and won't trigger unnecessary failed queries. Mutations are declared and ready for use.
150-160: LGTM! Keyboard shortcuts correctly trigger refetches.The
onRefreshhandler appropriately callsrefetch()on all relevant queries, includingdiffStatsQueryfor bulk stats and per-workspace queries when a workspace is selected.
205-240: LGTM! Workspace mutations are correctly integrated.Both
archiveWorkspaceandcreateWorkspaceproperly use their respective mutations withmutateAsync, error handling, and appropriate UI state updates (clearing selection, closing modals).
262-278: Question: Is dynamic import necessary here?Line 269 dynamically imports
WorkspaceService, which is unusual for a service used elsewhere in the file. Dynamic imports are typically for code-splitting or avoiding circular dependencies. If neither applies, consider a static import for simpler code.If there's a specific reason (e.g., WorkspaceService is large and this is for bundle optimization), this is fine—just worth confirming.
+// At top of file with other service imports +import { WorkspaceService } from './services/workspace.service'; async function handleFileClick(file: string) { // ... try { - const { WorkspaceService } = await import('./services/workspace.service'); const data = await WorkspaceService.fetchFileDiff(selectedWorkspace.id, file); // ... } }
283-306: LGTM! System prompt operations correctly use query/mutation pattern.
openSystemPromptEditorappropriately relies on theuseSystemPromptquery hook for automatic loading, andsaveSystemPromptcorrectly usesupdateSystemPromptMutationwith the controlledsystemPromptDraftstate. Error handling is in place.
357-441: LGTM! Repository cloning and addition correctly use mutation.The
handleCloneRepositoryfunction includes thorough validation, executes the git clone via Tauri shell command, and correctly usesaddRepoMutation.mutateAsyncto add the cloned repository to the database. Error handling and path normalization are in place.
725-734: LGTM! System prompt modal now uses controlled state (past issue resolved).The
SystemPromptModalcorrectly uses the controlledsystemPromptDraftstate withonChange={setSystemPromptDraft}andonSave={() => saveSystemPrompt(systemPromptDraft)}, eliminating the brittle DOM query pattern flagged in previous reviews. Loading and saving states are derived from query/mutation hooks.
443-748: LGTM! Dashboard rendering correctly consumes query-based data.The main render logic appropriately uses
repoGroups,stats,loading,fileChanges,prStatus, anddevServersfrom their respective query hooks. The UI is fully integrated with the TanStack Query data layer, and modals receive correct state and handlers.
95-99: Let me verify the Dashboard.tsx implementation and how it receives the data:No verification needed—prefetch data correctly reaches the store.
The
setQueryDatacall on line 105 is synchronous and immediately updates the query cache, and when the cache updates, the observer is notified and the component re-renders. The prefetch completion handler (line 105 inuseBulkDiffStats) updates the aggregate cache withqueryClient.setQueryData(['bulk-diff-stats', workspaceIds], { ...existing, [id]: data }), which triggers thediffStatsQueryobserver. TheDashboard.tsxeffect then runs and syncs the updated data to the Zustand store.The implementation correctly addresses the concern: background-prefetched workspaces do reach
diffStatsQuery.dataand flow to the store.
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (1)
src/features/dashboard/components/settings-sections/ProviderSection.tsx(3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{tsx,jsx,css}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx,css}: Avoid hardcoding colors; always use color tokens from Tailwind config
Always use font tokens from the Tailwind config (avoid hardcoded font sizes/families)
Use consistent paddings with a default of 16px unless there’s a strong reason otherwise
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsx
**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{css,tsx,jsx}: Default to ease-out for most animations
Animations should be shorter than 1s; most between 0.2s and 0.3s
Do not use built-in CSS easings excepteaseorlinear
Prefer ease-out for entering/user-initiated interactions; use ease-in-out for in-place movements; generally avoid ease-in
Useeasewith 200ms duration for simple hover transitions (color, background-color, opacity)
Disable hover transitions on touch devices using@media (hover: hover) and (pointer: fine)
Respect prefers-reduced-motion: disable transform-based animations when reduced motion is requested
Animate elements from their trigger by setting an appropriate transform-origin
Prefer animating opacity and transform; avoid animating positional properties like top/left
Do not animate drag gestures using CSS variables
Do not animate blur values higher than 20px
Usewill-changesparingly and only for transform, opacity, clipPath, or filter
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx}: When using Framer Motion, use transform rather than x/y for hardware-accelerated animations
Default to spring animations with Framer Motion
Avoid bouncy spring animations unless working with drag gestures
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsx
🧬 Code graph analysis (1)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (2)
src/features/dashboard/components/settings-sections/index.ts (2)
ProviderSection(4-4)SettingsSectionProps(6-6)src/features/dashboard/components/settings-sections/types.ts (1)
SettingsSectionProps(3-6)
🔇 Additional comments (1)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
93-95: Controlled input migration looks good.Switch to
value+onChangeis correct and aligns with debounced saving.Please verify UX: confirm that switching provider tabs or closing the modal preserves the last edit (with the unmount flush applied).
| const [customEndpoint, setCustomEndpoint] = useState(settings.custom_endpoint ?? ''); | ||
| const timeoutRef = useRef<NodeJS.Timeout>(); | ||
|
|
There was a problem hiding this comment.
Fix ref typing and missing initializer (TS error + env mismatch).
useRef<NodeJS.Timeout>() is invalid in browser targets and lacks an initializer. Use a cross‑env type and initialize to null.
- const timeoutRef = useRef<NodeJS.Timeout>();
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+ // Track latest and last-saved values for safe unmount flush
+ const latestValueRef = useRef(customEndpoint);
+ const lastSavedRef = useRef(settings.custom_endpoint ?? '');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [customEndpoint, setCustomEndpoint] = useState(settings.custom_endpoint ?? ''); | |
| const timeoutRef = useRef<NodeJS.Timeout>(); | |
| const [customEndpoint, setCustomEndpoint] = useState(settings.custom_endpoint ?? ''); | |
| const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| // Track latest and last-saved values for safe unmount flush | |
| const latestValueRef = useRef(customEndpoint); | |
| const lastSavedRef = useRef(settings.custom_endpoint ?? ''); | |
🤖 Prompt for AI Agents
In src/features/dashboard/components/settings-sections/ProviderSection.tsx
around lines 15 to 17, the timeoutRef uses NodeJS.Timeout and isn’t initialized
which causes a TS error in browser builds; change the ref type to a
cross‑environment type (e.g. ReturnType<typeof setTimeout> | null) and
initialize it to null (useRef<ReturnType<typeof setTimeout> | null>(null)) so it
works in browser and Node targets and eliminates the missing initializer error.
| const handleEndpointChange = (value: string) => { | ||
| setCustomEndpoint(value); | ||
| clearTimeout(timeoutRef.current); | ||
| timeoutRef.current = setTimeout(() => { | ||
| saveSetting('custom_endpoint', value); | ||
| }, 500); | ||
| }; |
There was a problem hiding this comment.
Debounce is neutralized; cleanup cancels the new timer and triggers per‑keystroke saves.
Because the effect’s cleanup runs on every state change, it clears the newly scheduled timeout and flushes the previous value. Update the handler to track latest value and record last saved.
- const handleEndpointChange = (value: string) => {
- setCustomEndpoint(value);
- clearTimeout(timeoutRef.current);
- timeoutRef.current = setTimeout(() => {
- saveSetting('custom_endpoint', value);
- }, 500);
- };
+ const handleEndpointChange = (value: string) => {
+ setCustomEndpoint(value);
+ latestValueRef.current = value;
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
+ timeoutRef.current = setTimeout(() => {
+ saveSetting('custom_endpoint', value);
+ lastSavedRef.current = value;
+ }, 500);
+ };There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (6)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
16-16: Fix ref typing: use cross-environment timeout type and add initializer.Line 16 still uses
useRef<NodeJS.Timeout>(), which is invalid in browser environments and lacks an initializer.Apply this diff:
- const timeoutRef = useRef<NodeJS.Timeout>(); + const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);src/Dashboard.tsx (5)
87-87: Defer bulk diff stats until workspaces are loaded to avoid double executionGuard the query so it doesn’t run with an empty array on first render.
Example (if the hook accepts options):
-const diffStatsQuery = useBulkDiffStats(workspacesQuery.data || []); +const diffStatsQuery = useBulkDiffStats(workspacesQuery.data ?? [], { + enabled: !!workspacesQuery.data && workspacesQuery.data.length > 0, +});
116-117: Ensure useSystemPrompt is disabled when workspaceId is nullRunning the query with null can cause unnecessary requests/failures. Prefer an enabled guard inside the hook or via options.
If your hook supports it:
useSystemPrompt(selectedWorkspace?.id ?? null, { enabled: !!selectedWorkspace?.id });To verify, I can scan the hook signatures and usage.
#!/bin/bash # Inspect useSystemPrompt signature and options usage fd useSystemPrompt src/hooks | xargs -I{} rg -nC2 'export function\s+useSystemPrompt|\buseSystemPrompt\(' {}
131-135: Apply the same enabled guard to workspace-scoped queriesuseFileChangesQuery/usePRStatus/useDevServers should be disabled when no workspace is selected to avoid spurious requests.
Pattern (if supported):
useFileChangesQuery(id, { enabled: !!id }); usePRStatus(id, { enabled: !!id }); useDevServers(id, { enabled: !!id });
725-734: SystemPromptModal is now correctly controlled — good fixLocal draft state + onChange/onSave resolves the brittle DOM query approach from earlier.
335-339: Harden Tauri open() result handling (string | string[] | null)plugin-dialog v2 returns string | string[] | null; avoid assuming an object with .path.
Based on learnings
-const folderPath = typeof selected === 'string' ? selected : (selected as any).path; +const folderPath = + Array.isArray(selected) ? (typeof selected[0] === 'string' ? selected[0] : '') : + (typeof selected === 'string' ? selected : ''); +if (!folderPath) return;
🧹 Nitpick comments (5)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
36-50: Consider capturing latestsaveSettingto avoid stale closure on unmount.The cleanup effect uses an empty dependency array to run only on unmount (correctly fixing the debounce cancellation issue). However, the
saveSettingcallback on line 46 is captured from the initial render. IfsaveSettingchanges between renders, the unmount flush will use a stale version.While
saveSettingfrom a TanStack Query mutation is typically stable, capturing the latest reference is more robust:const [customEndpoint, setCustomEndpoint] = useState(settings.custom_endpoint ?? ''); const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const pendingValueRef = useRef<string | null>(null); + const saveSettingRef = useRef(saveSetting); + + // Keep saveSettingRef up to date + useEffect(() => { + saveSettingRef.current = saveSetting; + }, [saveSetting]); // ... rest of code ... useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (pendingValueRef.current !== null) { - saveSetting('custom_endpoint', pendingValueRef.current); + saveSettingRef.current('custom_endpoint', pendingValueRef.current); } }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, []); // saveSetting is now captured via refsrc/Dashboard.tsx (4)
152-159: Consider invalidating by key instead of manual refetch callsBatch invalidation keeps dependencies consistent and avoids missing related caches.
Example:
// queryClient.invalidateQueries({ queryKey: [queryKeys.workspaces.root] });If you want, I can map this to your queryKeys.
415-421: Optional: stream clone progress to the UISwitch to spawn() and pipe stdout/stderr to show progress in the modal; improves UX for large repos.
Based on learnings
const { Command } = await import('@tauri-apps/plugin-shell'); const cmd = Command.create('git', ['clone', githubUrl, cloneTarget]); cmd.stdout.on('data', (d) => {/* set progress text */}); cmd.stderr.on('data', (d) => {/* set progress text */}); const child = await cmd.spawn(); const done = await child.on('close'); // or await cmd.execute() if you aggregate if (done.code !== 0) throw new Error(done.stderr || 'Git clone failed');
491-491: Replace hardcoded white/black backgrounds with theme tokensUse Tailwind tokens to satisfy design guidelines and dark mode parity.
-className="flex-1 rounded-lg bg-white/70 dark:bg-black/60 backdrop-blur-[20px] border border-border/40 vibrancy-shadow overflow-hidden transition-colors duration-200 min-h-0" +className="flex-1 rounded-lg bg-background/70 dark:bg-background/60 backdrop-blur-[20px] border border-border/40 vibrancy-shadow overflow-hidden transition-colors duration-200 min-h-0"
90-93: Remove unused locals (stats, status) to reduce noiseThey’re computed but not used.
-const stats = statsQuery.data || null; -const status = workspacesQuery.isError ? 'Error loading workspaces' : 'Connected';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite
📒 Files selected for processing (2)
src/Dashboard.tsx(10 hunks)src/features/dashboard/components/settings-sections/ProviderSection.tsx(3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{tsx,jsx,css}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx,css}: Avoid hardcoding colors; always use color tokens from Tailwind config
Always use font tokens from the Tailwind config (avoid hardcoded font sizes/families)
Use consistent paddings with a default of 16px unless there’s a strong reason otherwise
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/Dashboard.tsx
**/*.{css,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{css,tsx,jsx}: Default to ease-out for most animations
Animations should be shorter than 1s; most between 0.2s and 0.3s
Do not use built-in CSS easings excepteaseorlinear
Prefer ease-out for entering/user-initiated interactions; use ease-in-out for in-place movements; generally avoid ease-in
Useeasewith 200ms duration for simple hover transitions (color, background-color, opacity)
Disable hover transitions on touch devices using@media (hover: hover) and (pointer: fine)
Respect prefers-reduced-motion: disable transform-based animations when reduced motion is requested
Animate elements from their trigger by setting an appropriate transform-origin
Prefer animating opacity and transform; avoid animating positional properties like top/left
Do not animate drag gestures using CSS variables
Do not animate blur values higher than 20px
Usewill-changesparingly and only for transform, opacity, clipPath, or filter
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/Dashboard.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,jsx}: When using Framer Motion, use transform rather than x/y for hardware-accelerated animations
Default to spring animations with Framer Motion
Avoid bouncy spring animations unless working with drag gestures
Files:
src/features/dashboard/components/settings-sections/ProviderSection.tsxsrc/Dashboard.tsx
🧬 Code graph analysis (1)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (3)
src/features/dashboard/components/settings-sections/index.ts (2)
ProviderSection(4-4)SettingsSectionProps(6-6)src/features/dashboard/components/settings-sections/types.ts (1)
SettingsSectionProps(3-6)backend/server.cjs (1)
settings(263-263)
🔇 Additional comments (2)
src/features/dashboard/components/settings-sections/ProviderSection.tsx (1)
14-34: Well-implemented debounced save pattern.The controlled input with debounced saves correctly addresses the data loss and stale value issues from previous reviews. The approach using
pendingValueRefto track uncommitted changes and flush on unmount is sound and simpler than tracking both latest and last-saved values.Also applies to: 98-99
src/Dashboard.tsx (1)
96-99: Confirm aggregated diff stats include prefetched/background resultsIf useBulkDiffStats only reflects the currently fetched slice, store sync may miss prefetched items. Ensure the hook aggregates from cache (or derive per-workspace and merge).
Would you like a small helper that reads from the QueryClient cache to build a complete map?
…litch-swap effect system (6 classes, 6 @Keyframes, 2 media queries) and the empty tool-use-enter class — none referenced by any component, with all 825 tests passing.
* gnhf #1: Extracted two helper methods in claude-adapter.ts (closeActiveParts, accumulateStreamDelta) to eliminate 5 repeated close-text/close-thinking call sites and 2 duplicate 25-line streaming delta handlers; also replaced a duplicate 15-line formatTime function in WorkspaceItem.tsx with the existing shared formatTimeAgo utility — net reduction of 49 lines with all 375 agent-server tests passing. * gnhf #2: Extracted duplicate workspace-grouping logic (85 lines across query-engine.ts and workspaces.ts route) into shared lib/workspace-grouping.ts, and consolidated duplicate parameter-reading helpers (readString/readNumber across query-engine.ts and commands.ts) into shared lib/query-params.ts — net reduction of 41 lines with all 825 tests passing. * gnhf #3: Removed unused parameter from cancellation functions across both agent handlers, extracted helper replacing 5 identical repo-lookup patterns in repos.ts, unified duplicate git progress push functions, and removed dead export — net reduction of 23 lines across 7 files with all 825 tests passing. * gnhf #4: Removed 5 dead exports/functions and un-exported 2 internal-only functions across 6 files — net reduction of 112 lines with all 825 tests passing. * gnhf #5: Removed 2 dead exported functions (getRepoInitials, getRepoColor), un-exported 3 internal-only symbols (RECENT_PROJECT_LIMIT, resolveGitProjectRoot, setLastOpenInAppId), and replaced duplicate timeAgo in AccessSection.tsx with shared formatTimeAgo — net reduction of 41 lines across 5 files with all 825 tests passing. * gnhf #6: Un-exported 14 dead Zod schema validators (6 from shared/events.ts, 8 from shared/agent-events.ts), un-exported 6 dead type aliases from shared/events.ts, and consolidated duplicate parseGitHubRepo function from gh.service.ts and deus-import.ts into shared/lib/github.ts — reducing public API surface by 20 exports and eliminating 1 duplicate function across 5 files with all 825 tests passing. * gnhf #7: Deleted 2 dead component files (OpenInDropdown 214 lines, EmptyState 39 lines), removed 3 dead API type definitions (ApiResponse, PaginatedResponse, WorkspaceQueryParams) from shared/types/api.ts, removed 4 dead session type aliases (SessionMessageEvent, SessionErrorEvent, SessionEnterPlanModeEvent, SessionStatusEvent) from shared/types/session.ts, and cleaned up 3 barrel re-export files — net reduction of 330 lines across 7 files with all 825 tests passing. * gnhf #8: Deleted 2 dead platform files (updates.ts 31 lines, listenerGroup.ts 38 lines), removed dead function from dialog.ts, removed the entire dead StatusChanged notification pipeline across 5 files (method, schema, type, constant, test builder, union), un-exported from electron barrel, and cleaned up 3 barrel re-export files — net reduction of 121 lines across 11 files with all 825 tests passing. * gnhf #9: Removed 3 dead query hooks (useStats, useUncommittedFiles, useLastTurnFiles) with their stub service methods and query keys, removed 4 dead type definitions (ChangedFilesResult, BranchInfo, PaginationParams, DevServer) and their barrel re-exports, un-exported internal-only connectToRelay function, and cleaned up 2 dead barrel re-exports (clearToken, ConnectionIllustration) — net reduction of 121 lines across 12 files with all 825 tests passing and clean tsc. * gnhf #10: Removed 6 dead visual effect builder functions (98 lines) from visual-effects.ts, un-exported 5 internal-only symbols (resolveClaudeDir, getAgentConfig, StatusPriority, StatusConfig, WorkflowStatusConfig), and removed dead barrel re-exports (createAgentEventHandler, AgentEventHandler) from agent/index.ts — net reduction of 98 lines across 5 files with all 825 tests passing and clean tsc. * gnhf #11: Consolidated inline path validation in files.ts to use shared resolveWorkspaceRelativePath from git.service.ts, and replaced 10 hand-rolled readString+throw param validation patterns in commands.ts with the existing requireParam utility — net reduction of 17 lines across 2 files with all 825 tests passing and clean tsc. * gnhf #12: Removed 211 lines of dead CSS from global.css: the entire glitch-swap effect system (6 classes, 6 @Keyframes, 2 media queries) and the empty tool-use-enter class — none referenced by any component, with all 825 tests passing. * gnhf #13: Deleted 2 dead component files (RepoGroup.tsx 102 lines, WorkspaceItem.tsx 123 lines) from repository/ui that were never rendered anywhere, removed 14 dead barrel re-exports across 4 barrel files (repository/ui, sidebar/ui, sidebar feature, session/ui/blocks), and cleaned up knip.json — net reduction of 242 lines across 7 files with all 825 tests passing and clean tsc. * gnhf #14: Deleted the dead agent-server/messages/index.ts barrel (30 lines), removed 4 dead type exports from shared/messages/types.ts (ToolLocation, ToolOutputContent, PartType, PartTypeSchema), removed dead ToolResultMap type from chat-types.ts, and cleaned up 15 dead barrel re-exports across 4 barrel files (session/hooks, session/ui, workspace/ui, shared/hooks) — net reduction of 51 lines across 8 files with all 825 tests passing and clean tsc. * gnhf #15: Removed 10 dead type aliases from shared/agent-events.ts, un-exported 2 dead Zod schemas from shared/enums.ts, removed the dead useWindowFocus hook and its useSyncExternalStore infrastructure (50 lines) from useWindowFocus.ts, and removed dead BaseToolRendererProps barrel re-export — net reduction of 62 lines across 4 files with all 825 tests passing and clean tsc. * refactor: extract backend helpers and fix typecheck * Address CodeRabbit review feedback * Fix Claude adapter message handoff
All 5 are doc-only fixes in docs/device-use-v2-design.md:
- Storage path duplication — `state.json` lives at
`{storage.workspace}/state.json`, not `{storage.workspace}/.device-use/state.json`
(the `.device-use` segment is already inside `storage.workspace`).
Fixed in 3 places (table row #12, backend wiring section).
- Architecture fence missing language — added `text` (markdownlint MD040).
- /ws bullet overstated CLI visibility — clarified that only actions
routed through the server (REST, MCP, viewer) populate the activity
stream; standalone CLI invocations bypass it (peer model).
- Phase 1 stack text was stale — said "Add TanStack Start" + "Nitro"
but the locked stack (decision #5) is Hono on Bun.serve + Vite.
Bullet rewritten to match what was actually built.
- Out-of-scope note about agentic-app.json was outdated — the manifest
IS in this PR; the next PR is the Deus host that consumes it. Reworded
+ retitled the manifest section to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): Phase 1 — scaffold v2 server + frontend
Adds the foundation for device-use v2 as a standalone product: a Hono
server on Bun.serve that will later host /mcp, /ws, /api/*, and a
React viewer at /. Dev mode proxies to Vite for HMR; prod serves the
built bundle from dist/frontend.
- Hono server at packages/device-use/src/server/index.ts with /health
- React scaffold at packages/device-use/src/frontend/ (main/App/index.html)
- Vite config rooted at src/frontend, outputs to dist/frontend
- tsconfig: JSX + DOM libs
- scripts: bun run dev (server), dev:frontend (vite), build, start
Also lands docs/device-use-v2-design.md — the full design record
(15 locked architectural decisions, phased implementation plan,
draft AAP manifest for later).
Part of the AAP (Agentic Apps Protocol) effort. This PR is the
device-use standalone refactor; AAP protocol itself is a separate PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): Phase 2 — expand engine with xcodebuild, build wrapper, logs
Adds three new engine primitives needed for the v2 "build + run + observe"
flow, each injectable for unit tests:
- project-info.ts — parses `xcodebuild -list -json` for an .xcodeproj or
.xcworkspace; returns schemes / targets / configurations. Used by the
agent (and the viewer's scheme picker) to know what to build.
- xcodebuild.ts — `build()` wraps `xcodebuild build` with spawn so the
stdout/stderr line stream can be piped into tool-log events (for the
viewer's logs drawer and the agent's per-build trace). Supports
AbortSignal so stop-app can cancel an in-flight build. Predicts the
built .app path when a derivedDataPath is provided; `resolveAppPath()`
is the fallback that queries -showBuildSettings -json.
- logs.ts — `streamLogs()` wraps `simctl spawn <udid> log stream` with
injectable spawner. Supports bundleId + pid filtering via --predicate,
and a stop() handle for graceful teardown.
All three follow the existing engine pattern (executor/spawner as first
arg, typed errors extending DeviceUseError). 18 new unit tests — all
using fake executors/spawners, zero real subprocess deps.
Exit: bun test passes 72/72; typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): Phase 3 — REST API, MCP HTTP endpoint, WS event bus
Wires all 23 MCP tools behind a single invoker that emits tool-event
frames over WebSocket. The same invokeTool path handles REST, MCP, and
WS calls — one engine path, one trace, zero duplicated behavior.
New modules:
- state.ts — persistent JSON at {storage}/state.json (pinned UDID,
active project/scheme). Write-serialized to avoid clobbers.
- events.ts — EventBus with 200-event ring buffer + subscriber set.
Emits ToolEvent + ToolLog; reuses MCP tool-call shape (no parallel type).
- invoker.ts — central dispatcher. Every MCP/REST/WS tool call goes
through invokeTool; emits started/completed/failed with a shared id.
- tools.ts — 23 tool definitions (Zod-schema'd handlers). Each handler
can use ctx.executor / ctx.state / ctx.stream / ctx.refMap / ctx.events.
- mcp.ts — stateless MCP HTTP handler. Creates a fresh Server+Transport
per request (SDK requirement for stateless mode) and routes tool calls
through invokeTool.
- stream.ts — StreamManager owns the long-lived simbridge --stream
subprocess; exposes proxyStream() for /stream.mjpeg passthrough.
Server wires them together:
- GET /health — liveness
- GET /api/state — persisted state
- GET /api/tools — all tools + JSON schemas
- POST /api/tools/:name — invoke a tool (REST)
- GET /api/events — recent tool-event history
- GET /stream.mjpeg — MJPEG passthrough
- GET /api/stream — stream status
- ALL /mcp — MCP HTTP transport
- WS /ws — tool-event + tool-log subscribe + invoke
Deps: @modelcontextprotocol/sdk, zod-to-json-schema.
Exit verified end-to-end:
- 86 tests pass (14 new integration tests)
- typecheck clean
- curl /api/tools → 23-tool JSON
- curl /mcp tools/list + tools/call → proper SSE responses
- scripts/ws-smoke.ts → WS receives started+completed tool-event after
REST invoke, correlation id matches
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): Phase 4 — React viewer (TopBar, DeviceFrame, Sidebar, logs)
Builds out the standalone UI served at /. Works entirely through the
HTTP + WS endpoints landed in Phase 3 — no direct engine imports
from the frontend.
Components:
- TopBar: sim picker, project path input, scheme dropdown,
▶ Run button with disabled/building/failed states
- DeviceFrame: phone bezel + MJPEG stream from /stream.mjpeg;
placeholder when no sim is pinned or sim not booted;
ripple animation dispatched from tap tool-events via a Zustand
subscription (not a useEffect setState)
- Sidebar: snapshot-on-demand with clickable @ref list
(each taps the element) + live activity feed of recent tool-events
- LogsDrawer: auto-scrolled tail of tool-log frames
- Toasts: one-liner on build/install/launch completion or any failure
Stores (Zustand):
- sim-store — sims list, pinned UDID, stream info
- project-store — active project path, scheme, configuration,
build status
- activity-store — tool-event ring buffer + toast dispatch
- logs-store — tool-log ring buffer (500 lines)
Infra:
- lib/ws.ts — WS client hook with exponential reconnect.
Routes tool-event → activity-store, tool-log → logs-store.
- lib/api.ts — typed fetch wrappers around /api/tools/*.
- styles.css — minimal dark theme using the same color tokens
as the Deus IDE. No Tailwind (keeps deps light).
Exit verified:
- typecheck clean, eslint clean
- vite build succeeds (202KB JS, 5KB CSS, 43 modules)
- Hono→Vite proxy serves the page via /
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): Phase 5 — CLI cleanup (drop SDK, drop stream CLI, add serve)
The legacy SDK (fluent session builder) and CLI `stream` subcommand
were built for a stateless-CLI world. v2 replaces them with the
long-lived server, MCP endpoint, and /stream.mjpeg passthrough —
so these surfaces are dead weight.
Deleted:
- src/sdk/ (fluent `session()` builder + apps helpers)
- src/cli/stream/ (manager + viewer-html generator)
- src/cli/commands/stream.ts (CLI stream enable/disable/status)
- scripts/sdk-smoke.ts
Added:
- src/cli/commands/serve.ts — `device-use serve [--port N] [--host] [--open]`
spawns bun on the server module and inherits stdio. Works in dev
(source path) and in bundled form.
Updated:
- src/cli/index.ts — drop stream import, add serve
- scripts/build-ts.ts — drop sdk build entry; now produces
dist/cli.js + dist/engine.js
- package.json — description updated; main/exports
point at engine (primary programmatic
entry); bin unchanged
CLI surface (preserved): list, boot, shutdown, open, session, snapshot,
query, tap, swipe, type, fill, screenshot, wait-for, open-url, apps,
appstate, launch, terminate, permission, doctor, install, serve.
Exit verified:
- typecheck clean
- 86 tests pass
- `bun run cli help` lists all commands including `serve`
- `bun run cli serve --port 3101` starts server, /health → 200
- `bun run build:ts` produces dist/cli.js + dist/engine.js cleanly
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): Phase 6 — docs, AAP manifest, composite run tool
Final phase: makes device-use v2 AAP-ready and documents the shape.
- agentic-app.json — manifest at the package root. Host IDEs (Deus,
MCP-speaking IDEs) read this to launch device-use, probe readiness
via /health, embed the viewer at /, and wire agent tool calls into
/mcp. Uses the schema shape agreed in docs/device-use-v2-design.md.
- run tool (composite) — chains build → resolveAppPath → install →
launch_app in one call. Reads CFBundleIdentifier from the built
Info.plist when bundleId isn't provided. Total MCP tools now 24.
The Viewer's ▶ Run button calls build + install + launch as
separate steps today; future phases can switch to this composite
for a cleaner progress story.
- README rewritten — new architecture, how to run the server, MCP
endpoint config for Claude Desktop, full tool list.
- AGENTS.md rewritten — new layout, how to add a tool (invokeTool
path), how to add a CLI command, event shape, hard rules ("never
bypass invokeTool", "stateless CLI stays stateless", etc.).
- .gitignore — adds .device-use/ (server's per-workspace state dir).
End-to-end verified:
- typecheck clean
- 86 tests pass
- curl /health → {ok:true}
- curl /api/tools → 24 tools
- curl /mcp tools/list → 24 tools
- set_active_simulator persists via state.json
- WebSocket streams started+completed tool-events with correlation id
- Pre-flight Info.plist-read in run tool handles bundleId resolution
device-use v2 is now the standalone product the AAP will later consume.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(device-use): add end-to-end server harness against real simulator
scripts/e2e-server.ts spawns a fresh device-use server on port 3199,
subscribes to /ws, exercises the full data flow, and asserts event
correlation across REST, MCP, and WebSocket paths. Runs in ~25s when
xcodebuild has a warm DerivedData cache.
Covers:
1. Server lifecycle — spawn + /health probe + graceful SIGTERM.
2. Tool registry — REST /api/tools reports 24 tools.
3. MCP HTTP transport — initialize + tools/list + tools/call(get_state)
over JSON-RPC-over-SSE with per-request stateless transport.
4. Simulator lifecycle — list_devices, set_active_simulator,
boot (Shutdown → Booted).
5. Project introspection — get_project_info parses xcodebuild -list -json
and finds the TestApp scheme.
6. Composite run tool — build → resolveAppPath → install → auto-resolve
bundleId from Info.plist → launch. End-to-end in one call.
7. Snapshot — a11y tree walk, @refs assigned to interactive nodes.
8. Tap — ref-based tap through simbridge.
9. State persistence — state.json round-trip verified on disk AND
via get_state tool.
10. WS event correlation — every tool emits paired started+completed
frames with matching ids; build streams tool-log frames during
xcodebuild.
First successful run:
24 tools · 19 tool-events · 273 tool-log frames · 0 failures · 25s
Future regression: `bun scripts/e2e-server.ts` from the package dir.
Requires Xcode + an iPhone 17 Pro simulator available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): bind server to 0.0.0.0 so IPv6 orphans can't shadow us
The server previously bound to 127.0.0.1 only. On a machine with a
stale IPv6 listener on the same port (e.g. an orphaned sim-helper
from agent-simulator — observed in the wild, PPID=1), browsers
resolving `localhost` to ::1 first would connect to the wrong server
and render the stale UI.
Binding to 0.0.0.0 (all IPv4 interfaces) means happy-eyeballs falls
back to 127.0.0.1 cleanly when ::1 has no listener, and any IPv6
orphan on the same port becomes harmless rather than a hijacker.
Root cause, not a workaround: the orphan was a real bug in older
tooling; this change makes us robust against that class of collision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): boot + set_active_simulator now start the MJPEG stream
The stream subprocess (simbridge --stream) was only auto-starting at
server startup for a pre-pinned sim. Booting interactively via the UI
would pin + boot the sim but never start the stream — viewer stuck on
"Waiting for stream…" even though everything else worked (snapshot,
activity feed, etc.).
Fix:
- boot tool — after simctl boot succeeds, start the stream for that
UDID. Failures are logged but don't fail the boot (CLI callers can
still boot without needing a stream).
- set_active_simulator tool — if the newly-pinned sim is already
booted, start the stream too. Covers the "open viewer after sim
is already booted externally" path.
Both return the new stream info in their result so the UI doesn't
need to wait for the 10s refresh tick to pick it up.
Verified against a real sim: restart server → pinned UDID auto-boots
and streams on an assigned port → /stream.mjpeg returns
200 multipart/x-mixed-replace with frames.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): interactive tap + swipe on the stream
Mouse events on the MJPEG image now drive the simulator, not just
watch it. Click → tap(x, y). Drag → swipe(from, to) with an 8pt
threshold to distinguish the two. Cursor turns crosshair when a
clickable stream is loaded.
To map CSS pixels → simulator points, StreamManager.start() now
probes the upstream /config on ready and computes logical point
dimensions (assuming the standard 3x retina scale). StreamInfo
gains `size: { pxW, pxH, ptW, ptH }`, exposed via /api/stream.
The DeviceFrame multiplies the click's fractional position by
ptW/ptH to get point coords for the tap/swipe tools.
Ripple animations now use the dynamic ptW from streamInfo instead
of the previous hardcoded 402pt estimate, so they land on the
right spot on any device.
Every interaction routes through invokeTool → same activity feed
entries, same tool-event broadcast, same MCP trace. Zero new
invocation paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): use canvas + binary WS for interactive stream
Clicks on the MJPEG <img> silently did nothing — browsers don't fire
mouse events reliably on images that swap frames continuously. The
original Deus simulator viewer avoided this by rendering MJPEG frames
into a <canvas> and sending touch events as binary WebSocket frames
to simbridge's /ws (0x03 prefix + JSON payload, begin/move/end
lifecycle). Adopting that proven pattern.
Server:
- /sim-input WebSocket endpoint. Opens an upstream WS to simbridge's
/ws for the active stream and forwards binary frames one-way
(browser → simbridge). Buffers messages until upstream handshake
completes. Closes upstream when client disconnects.
- Upgrade flow attaches { kind: "events" | "sim-input", wsUrl? } to
the Bun WS data field so a single websocket handler can multiplex.
Frontend:
- DeviceFrame now renders the stream into a <canvas>. A requestAnimation
Frame loop drives ctx.drawImage(img, 0, 0) from an off-screen Image
pointed at /stream.mjpeg. Canvas fires mouse events reliably even
while frames are updating.
- Mouse events encode 0x03-prefixed TouchEventPayload (begin on
mousedown, move on drag, end on mouseup) and send normalized 0..1
coordinates via the /sim-input WS.
- Window-level mouseup catches drags that release outside the canvas.
- Ripples fire locally on mousedown — immediate UI feedback without
waiting for a server round-trip.
Verified end-to-end: tap at Maps' center (0.27, 0.20) → Maps app
launched with "Allow Location" permission dialog. scripts/touch-smoke.ts
exercises the full path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): auto-refresh the elements sidebar after each canvas tap
User reported the refs sidebar showed stale elements: the list still
referenced the home screen after they'd opened Maps. Fix: every canvas
mouseup schedules a snapshot refresh (debounced 500ms, collapses rapid
taps into one call). The sidebar also auto-snapshots the moment the
stream first goes live, and shows the foreground app name next to the
"elements" header so the user knows what they're looking at.
- refs-store.ts — new Zustand store with refs + foreground + loading,
a refresh() action, and a scheduleRefresh(delayMs) debouncer
- DeviceFrame — calls scheduleRefresh on canvas mouseup AND on the
window-level mouseup (drag-release-off-canvas path)
- Sidebar — consumes the refs store, auto-snapshots on first stream
connect, re-snapshots after any ref tap (sidebar → canvas tap →
screen changes → refs stale otherwise)
Activity feed also gets clearer: users can see which tap was theirs
(REST via api.tap from ref clicks) vs. which came from the direct WS
/sim-input path (those bypass invokeTool and don't show in activity).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): ref-based tap uses tapEntry (id → label → coords fallback)
Ref taps were hitting the bounding-box center, which misses the hit
target for elements like switches/checkboxes where the tappable area
is not centered on the a11y frame (e.g. Maps' "Traffic" checkbox —
the a11y node's center is on the label, the switch is to the right).
Fix: use the engine's existing tapEntry helper, which tries in order:
1. tapById(identifier) — simbridge scans the a11y tree and taps the
matching element's natural hit target
2. tapByLabel(label) — same, by accessibility label
3. tap(x, y) — coordinate fallback
This matches what the CLI's `tap @eN` command already does. Ref taps
in the viewer sidebar now toggle switches, check checkboxes, and
activate controls whose hit target differs from the a11y bounds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): hardware-button row + context-menu suppression
Adds a row of hardware buttons (Home / Lock / Vol+ / Vol−) below the
phone. Clicks send 0x04 + JSON{button} frames to simbridge's /ws via
the existing /sim-input proxy — same transport as touch, so they're
instant and don't round-trip through invokeTool.
Also:
- Cmd+H / Cmd+L keyboard shortcuts for Home / Lock (respects focus;
ignores keys while typing in inputs)
- Canvas right-click now preventDefault's (no more browser context
menu popping over the stream)
- Button press triggers the same debounced refs-snapshot refresh that
canvas taps do, so the sidebar stays in sync after going home
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(device-use): post-cleanup — remove dead code + tighten server
Mechanical cleanup after the v2 refactor, no behavior changes. 86
tests + e2e-server (18 tool events, 97 tool-log frames) still pass.
Server:
- EventBus now owns `logHandles: Map<string, LogHandle>` directly.
stream_logs / stop_logs drop the `ctx.events as unknown as …` cast
and the `_logHandles ??= new Map()` dance.
- New `toolInputSchema()` helper in tools.ts wraps the zod-to-json-
schema call with its `as any` cast. /api/tools and /mcp tools/list
both use it; removes the duplicated expression and two casts.
- WS_SUBSCRIBERS map collapsed from `{unsubscribe, ws: unknown}` to
the unsubscribe function directly. The `ws: unknown` field was
never read.
- Dropped the `void buf;` no-op in stream.ts — the simbridge stderr
handler just discards the parameter.
Frontend:
- snapshot tool now returns an explicit `foreground: string | null`
(the root Application node's label). Frontend reads it as a named
field instead of digging into `tree[0].label`.
- snapshot call in api.ts stops requesting interactiveOnly:false
(it was inflating the sidebar with non-clickable static text).
Server default of `interactiveOnly: true` is what the UI wants.
- project-store: BuildStatus type trimmed from 6 states to 4
(idle|running|done|failed). The old "building|installing|launching"
sub-states were dead — nothing set them.
- TopBar's Run button wired to `api.run()` (the composite: build →
install → launch) instead of `api.build()`. "▶ run" actually runs.
- DeviceFrame: extracted `normalize(clientX, clientY)` helper so the
canvas mouseup and window-level mouseup share one coordinate path.
- Every scheduleRefresh() callsite dropped its explicit 500ms arg;
that's now the store's default.
CLI:
- `device-use serve --open` now polls /health (up to 6s) before
opening the browser, replacing the 1500ms blind sleep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(device-use): drop Vol+/Vol− buttons — simbridge doesn't map them
simbridge's SimAccessibilityBridge.sendButton only dispatches home and
lock; volumeup/volumedown payloads reach the handler but are no-ops.
Keeping the UI honest by removing the two buttons that don't do
anything. If volume control comes back later, it'll need a sendButton
case in Swift first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): stop the 10s blink — use primitive deps in DeviceFrame
sim-store refreshes /api/stream every 10s and writes a fresh
streamInfo object into state. Both effects in DeviceFrame (the MJPEG
canvas render loop and the /sim-input WebSocket) depended on the
whole streamInfo object, so each poll tore down the canvas, cleared
it, recreated the Image element, and reconnected the WS. Result: a
visible black flash on the phone every 10 seconds and a one-frame
gap in touch delivery if you happened to be mid-drag.
Fix: depend on primitive identity — streamUdid (and streamPort for
the WS). Same UDID/port → same effect instance, no teardown. If the
underlying sim actually changes UDID (user switches sim), both
effects re-fire correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): stop the blink at the source — sim-store preserves identity
The first stab at the 10s blink fixed DeviceFrame's effect deps but
missed two other places that still churned references on every poll:
1. sim-store.refresh() set streamInfo / sims / pinnedUdid
unconditionally, so every 10s each of those fields got a new
reference even when the content was unchanged. Anything subscribed
to them via `useSimStore()` re-rendered.
2. Sidebar's "take an initial snapshot on first stream connect"
effect depended on the full streamInfo object; every poll
re-fired the effect's guard check.
Real fix — at the store, not the consumers:
- sim-store now compares new vs. previous stream/sims/pinnedUdid
and reuses the old reference when content matches. No downstream
sees a spurious update.
- Sidebar effect now depends on `streamInfo?.udid` (primitive)
instead of the object, matching DeviceFrame's pattern. Even if
we later regress #1, #2 is independently safe.
Served bundle: index-Cre9C4oH.js.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): keyboard input + CI workflow + long-press verified
Three changes closing the last gaps before the PR opens.
1. Keyboard input on the viewer — keystrokes while focus isn't on
another input get batched into a 150ms idle buffer and flushed via
the type_text tool. Enter flushes with submit=true (presses Return
after). Backspace trims the local buffer. Modifier keys (Cmd/Ctrl)
are preserved for the existing hardware-button shortcuts
(Cmd+H / Cmd+L). Buffering collapses fast typing like "hello"
from 5 sequential simbridge subprocesses into one.
2. Long-press verified — scripts/longpress-smoke.ts holds begin for
1500ms, then releases end, and asserts the iOS context menu
appears (screen refs grow, including the "Dismiss context menu"
action). Passes against a real iPhone simulator. The architecture
already supported it: our /sim-input WS forwards begin+end raw to
simbridge, which keeps the touch down for whatever interval the
client chooses. No code needed — just proof.
3. GitHub Actions workflow for nightly e2e on macos-15. Picks any
available iPhone, boots it, runs scripts/e2e-server.ts end-to-end
(spawn → /health → WS → REST + MCP → build → install → launch →
tap → state persistence). Schedule + manual dispatch only; we
don't run per-PR (macOS minutes cost 10×). The e2e script now
honors $E2E_SIM_UDID to target a specific sim without changing
its default behavior for local dev.
Verified end-to-end:
- type_text via REST: {success:true}
- longpress-smoke.ts: refs 3 → 7, iOS context menu opened
- touch-smoke.ts: still passes
- e2e-server.ts: full flow passes (18 tool-events, 97 tool-logs)
- 86/86 unit tests pass
- typecheck clean
Out of scope for this PR (noted in design doc as v1.1):
- wheel→scroll mapping
- pinch/rotation multi-touch
- CLI actions appearing in /ws activity feed (peer model stays pure)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(device-use): handoff section for the next PR (Deus AAP host)
Adds a "Handoff to next PR" block to docs/device-use-v2-design.md so
whoever picks up the Deus-side AAP integration has a working brief.
Covers:
- What to build in Deus (apps.service.ts, app-registrar.ts, lifecycle
tools, frontend launcher) with concrete file paths.
- What to delete after AAP works (~2.5k LOC of legacy simulator code
in apps/web/src/features/simulator/ + simulator-context.ts +
deus-tools/simulator.ts).
- 8 lessons device-use learned that should shape AAP host behavior:
requires validation, {port} substitution, mandatory health probe,
storage env var, child.on(exit) > polling, low-latency input
back-channel, runtime setMcpServers, framework-agnostic frontend.
- Things device-use deliberately deferred (wheel-scroll, CLI in WS,
multi-sim side-by-side) and where they belong.
- Open design questions for the next PR.
- A concrete first-commit slice for the Deus AAP PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): address CodeRabbit review on PR #249
17 inline comments from the bot — addressing all of them. Brief
mapping (file → comment id):
CRITICAL
package.json#62 Add missing zustand dep (was hoisted to root, would
fail bun install --frozen-lockfile in CI).
MAJOR
agentic-app.json#33 UI/MCP URLs use 127.0.0.1 instead of localhost so
host IDEs don't trip over IPv4/IPv6 happy-eyeballs
ambiguity. serve.ts also defaults --host to 0.0.0.0
(matches the server's own default), and the printed
probe URL collapses 0.0.0.0 → 127.0.0.1.
serve.ts#33 Compiled-layout candidate path was off-by-one
(..server vs ../..server). Fixed.
serve.ts#65 Browser only opens after /health responds 200; on
startup failure, no dead tab.
serve.ts#72 Propagate child exit code; non-zero exits return
{success:false} with the code.
e2e-server.ts#173 ChildProcess.killed is set when the signal is sent,
not when the process exits — using it as a kill-fallback
check would race a SIGTERM-ignoring child. Switched
to a tracked `exited` flag + clearTimeout on exit.
Sidebar.tsx#17 Refresh refs whenever streamUdid changes (sim swap),
not only when refs.length === 0. Stops stale refs
from a previous sim from lingering.
styles.css#85 Added --on-accent / --stage-bg / --frame-bg /
--canvas-bg / --shadow-* tokens; removed every
literal `white`, `#06060a`, `#1a1a22`, `#000`, and
`rgba(...)` from the stylesheet body.
state.ts#72 persist() now catches mkdir/writeFile failures
inside the chained promise so a single transient
error can't permanently brick the writeChain.
tools.ts#562 stream_logs wires onError + onExit to delete the
handle from logHandles, so failed/exited streams
don't leak.
MINOR
design-doc#57,#65 Architecture diagram said "TanStack Start on Nitro"
but the locked stack (decision #5) is "Hono on
Bun.serve" / "Vite + React". Aligned.
README.md#88 Added `text` language tag to the architecture fenced
code block (markdownlint MD040).
project-info.ts#43 detectKind() throws XcodebuildError on inputs that
aren't .xcodeproj or .xcworkspace, instead of
silently treating them as .xcodeproj.
xcodebuild.ts#159 Removed the synthesized appPath from BuildResult —
it was a guess based on `scheme.app` that's wrong
whenever target product name differs. Callers
should use resolveAppPath() (already used by the
`run` composite tool).
TopBar.tsx#15 onBoot pushes a failed tool-event into the activity
store on api.boot() failure, so the toast surfaces
the error.
sim-store.ts#67 setPinned checks res.success — only commits the
pinnedUdid change on success, otherwise stores
error state.
styles.css#345 Replaced deprecated `word-break: break-word` with
`overflow-wrap: anywhere` (modern equivalent).
Side effects:
scripts/*.ts Added `export {}` to the three smoke scripts so
tsc treats them as modules, not global scripts
(was causing duplicate-symbol errors after a
second smoke script joined the project).
Verified:
- 86/86 unit tests pass
- typecheck clean
- bun scripts/e2e-server.ts passes against a real iPhone 17 Pro
(17 tool-events, 97 tool-log frames, 0 failures)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(device-use): sync bun.lock with zustand workspace dep
The zustand dep was added to packages/device-use/package.json in
f14d701 but the workspace lockfile entry wasn't captured. This is
the missing one-line lockfile sync — `bun install --frozen-lockfile`
was already passing in CI because the root entry was up-to-date,
but the workspace block needed the addition for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): address CodeRabbit round 2 (3/4)
- design-doc#168 — removed stale "TanStack Start" reference from the
dev/prod modes row; replaced with the actual setup (Hono server with
--hot, separate Vite dev server, prod = bundled frontend + same
Hono server in NODE_ENV=production).
- sim-store#19 (sameStream) — extended comparison beyond udid + port
to include url and structural size comparison (pxW/pxH/ptW/ptH).
Stream metadata changes (e.g. orientation rotation altering size
while UDID/port stay fixed) now correctly notify subscribers.
- sim-store#30 (sameSims) — added runtime to the per-simulator
equality check. Runtime upgrades now propagate.
Disagreed with one suggestion (TanStack Query refactor for sim-store
fetches). Reasoning in the round summary on the PR — short version:
device-use is a standalone package whose frontend deliberately runs
on Zustand only, has no TanStack Query dependency today, and the
guideline cited applies to apps/web (Deus's main UI), not to
packaged sub-products. Adding TanStack Query for two REST reads
would inflate the bundle and introduce a paradigm split between the
WS event subscription and the polled data, with no real benefit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(device-use): clarify launch.ready manifest path in handoff section
CodeRabbit flagged that `launch.ready.http` was an inconsistent path
shorthand — the actual manifest schema is `launch.ready` with
`{type: "http", path: "/health"}`. Reworded to use the correct
shape so future readers don't assume a `launch.ready.http` key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(device-use): address CodeRabbit round 4 (5 doc nits)
All 5 are doc-only fixes in docs/device-use-v2-design.md:
- Storage path duplication — `state.json` lives at
`{storage.workspace}/state.json`, not `{storage.workspace}/.device-use/state.json`
(the `.device-use` segment is already inside `storage.workspace`).
Fixed in 3 places (table row #12, backend wiring section).
- Architecture fence missing language — added `text` (markdownlint MD040).
- /ws bullet overstated CLI visibility — clarified that only actions
routed through the server (REST, MCP, viewer) populate the activity
stream; standalone CLI invocations bypass it (peer model).
- Phase 1 stack text was stale — said "Add TanStack Start" + "Nitro"
but the locked stack (decision #5) is Hono on Bun.serve + Vite.
Bullet rewritten to match what was actually built.
- Out-of-scope note about agentic-app.json was outdated — the manifest
IS in this PR; the next PR is the Deus host that consumes it. Reworded
+ retitled the manifest section to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(device-use): align manifest example with shipped agentic-app.json
CodeRabbit noticed the design-doc manifest example still showed
\`localhost\` URLs while the actual \`packages/device-use/agentic-app.json\`
uses \`127.0.0.1\` (the IPv4-safe form chosen during the round-1 fix
to avoid IPv4/IPv6 happy-eyeballs ambiguity). Aligned the example so
copy/paste from the doc doesn't regress the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(device-use): adopt Deus design tokens (Jony-Ive dark theme)
The viewer now uses the same color/text/radius/font system that
apps/web/src/global.css defines for the Deus IDE itself, so embedding
device-use as an AAP app feels native instead of looking like a
separate skin.
Tokens mirrored 1:1 from Deus's dark theme:
- Backgrounds — six-tier hierarchy (--bg-base #0b0b0b → --bg-surface
→ --bg-elevated → --bg-raised → --bg-overlay → --bg-muted #2a2a2a).
Each step adds perceptible depth without needing borders.
- Text — five-level OKLCH grayscale (--text-primary 0.86 → 0.32),
generous gaps between levels.
- Accent — `oklch(0.78 0.09 345)` (Deus's "cool rose" primary), not
the previous purple. Used for the run button, status dot glow,
ref ids, and ripples.
- Semantic signals — equiluminant OKLCH (success / warning /
destructive all at L≈0.65-0.68), matches Deus exactly.
- Borders — `color-mix(in oklch, var(--foreground) 5%, transparent)`,
semi-transparent so they adapt to any surface.
- Radius scale — 2 / 4 / 6 / 8 / 10 / 12 / 16 / 20 / 24 px.
Device frame uses --radius-4xl (24px), canvas inside uses
--radius-3xl (20px), buttons --radius-md/lg.
- Typography — 13px base (matches Deus's dense IDE scale), system
-apple-system / SF Pro Text + SF Mono, negative letter-spacing
throughout (-0.005em / -0.015em on brand) for the "premium texture"
layer.
- Custom scrollbars themed to bg-raised → bg-overlay on hover.
JSX inline styles updated to reference the new tokens
(--accent-2 → --primary, --danger → --destructive, --dim →
--text-muted-foreground).
Class names unchanged — same DOM structure, just new visual tokens
underneath. Verified:
- 86/86 tests pass
- typecheck clean
- vite build: index-CKuhJYDJ.css (9.62 KB), index-BOvIo28T.js
- bun scripts/e2e-server.ts PASS — 18 tool-events, 97 tool-log
frames, 0 failures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): collapsible logs drawer (collapsed by default)
The logs pane was eating 220px at the bottom even when empty, which
is most of the time. Now it's just a ~32px header row by default; the
phone + sidebar get the extra vertical space back.
- Click the header row to toggle expand/collapse.
- Chevron flips ▸ / ▾ to signal state.
- Auto-expands on any new log line — if you triggered a build or a
log stream, the drawer pops open for you. Collapsing is always
manual. Auto-expand uses a direct Zustand subscription (not a
useEffect setState) to satisfy react-hooks/set-state-in-effect.
- Clear button hidden when collapsed (no affordance without content).
- Grid row for logs changed from fixed 220px to `auto`; the
`.expanded` class pins the body at 220px when open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): themed Select (Radix UI) — replaces native <select>
Native <select> popups use OS chrome (white on macOS), breaking the
dark Deus theme. Swapped for @radix-ui/react-select wrapped in a
small themed component — same primitive Deus's apps/web Select uses,
just styled with plain CSS against our token system (no Tailwind).
What's new:
- src/frontend/components/Select.tsx — minimal wrapper exposing a
plain {value, onValueChange, options, placeholder} API. Options
can be any ReactNode so the sim picker can render a state dot + a
dim runtime suffix inline.
- Styled trigger matches the other topbar inputs (bg-elevated, 1px
border, 6px radius, hover brightens to bg-raised, focus-visible
rings with --ring).
- Portaled popover uses --popover / --border / --shadow-elevated,
radius-xl; items have 6/10 padding, left checkmark indicator in
--primary, data-highlighted lifts to bg-elevated.
- Chevron rotates 180° on open (data-state="open").
- Smooth 180ms in / 150ms out with cubic-bezier matching apps/web.
TopBar wiring:
- Sim picker renders each option as `[dot] Name · runtime`, with the
dot green+glow when that sim is Booted.
- Scheme picker renders as a plain list.
- Empty "no simulators" case stays inline text (no dropdown needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): correct cache-control for prod static serving
Without explicit cache directives, browsers heuristically cache the
HTML shell and keep pointing at old Vite-hashed asset URLs across
deploys — user refreshes, sees a stale bundle. This was hitting the
dropdown-themed commit: the new bundle was built + served but the
browser's cached HTML still referenced the pre-Radix build.
Fix in the prod static-serving branch:
- /assets/* → `Cache-Control: public, max-age=31536000, immutable`
(Vite fingerprints these, so the URL changes whenever the content
does — safe to cache forever).
- Everything else (including the HTML shell) → `Cache-Control: no-cache`
(revalidate every load so new bundle URLs are picked up immediately).
Verified via `curl -D -`. HTML now sends `no-cache`, asset sends
`public, max-age=31536000, immutable`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(device-use): proper iPhone/iPad device frame (ported from apps/web)
The old frame was just a rounded rect with 12px padding and 40px
radius — didn't match iPhone proportions, had no dynamic island,
no camera hole, and no accurate bezel thickness. Ported the
GenericDeviceFrame shape from
apps/web/src/features/simulator/ui/DeviceFrame.tsx exactly:
- Aspect ratio per device class (iPhone 430:932, iPad 834:1194);
the frame scales to fit the stage height while preserving ratio.
- Shell radius 3.25rem (iPhone) / 2.75rem (iPad), screen radius
2.6rem / 2.1rem — the two-layer corner system.
- Percentage-based screen insets (iPhone 1.7% top / 2.5% sides /
1.9% bottom) matching Apple's actual bezel thickness.
- Dynamic island: 38% × 4.1%, positioned at 2.35% top, bg-base
rounded-full. Shows for iPhone only.
- Camera dot: 10×10, positioned at 1.45% top with a subtle ring.
Shows for iPad only.
- Inner bezel ring at inset 0.6% — catches light, hints at edge
thickness without adding chrome.
- iPhone vs iPad detection from pinned sim's name (same heuristic
apps/web uses).
We don't bundle the Apple-derived bezel assets (apps/web loads them
from a generated manifest via scripts/prepare-device-chrome.mjs —
needs Xcode + licensing considerations for packaged device-use);
the generic shell is the fallback Deus uses anyway when the manifest
isn't present, so visual parity is the same as Deus's default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): drop overlay dynamic island/camera dot — stream already has them
The simulator's MJPEG stream captures iOS's full screen, including the
native status bar with its own dynamic island (iPhone) or camera
cutout (iPad). My overlay versions drew a second pill on top at
slightly different sizes/positions, creating the visible "two things
stacked" artifact the user flagged.
Fix: remove the overlay divs + their CSS. The stream shows the real
ones rendered by iOS at the right size/position.
Why Deus's apps/web draws them anyway: their code path assumes the
captured stream excludes the status bar (that is the case when they
eventually swap in the Apple-derived bezel assets, which crop it).
For the generic-shell + full-screen MJPEG case (what we have), the
overlays are redundant.
Kept: shell background, proper aspect ratio, shell/screen radii,
percentage screen insets, subtle inner ring. Those still make the
frame feel intentional without faking what's already in the stream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(device-use): shell aspect ratio derived from stream so the screen fits exactly
The hardcoded 430/932 shell AR only matches the base iPhone 16 Pro.
For iPhone Air (420×912) or iPhone 17 Pro Max (440×956), the stream
either got stretched/squished by the canvas filling the box, or left
a black gap at the top/bottom because the inner screen box didn't
match the stream's aspect ratio.
Fix: compute the shell aspect ratio from streamInfo.size.ptW/ptH
so that, after subtracting the percentage bezel insets, the inner
screen box has exactly the same ratio as the stream. The canvas then
fills the screen cleanly with no object-fit letterboxing needed.
Math:
widthFrac = 1 − (left% + right%) / 100
heightFrac = 1 − (top% + bottom%) / 100
screenAR = ptW / ptH (from streamInfo.size)
shellAR = screenAR × widthFrac / heightFrac
Bezel insets (1.7/2.5/2.5/1.9 % for iPhone, 2.1 all-around for iPad)
are unchanged. Fallback to the previous hardcoded ratios when we
don't have streamInfo yet (empty-state placeholder). Dropped the
canvas `object-fit: contain` since there's nothing to contain now —
the box matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(device-use): collapsible sidebar with Panel wrapper
Extract ElementsPanel and ActivityPanel as discrete components under
components/sidebar/, share a Panel wrapper for chrome, and add a
collapsible shell with a Cmd+B toggle. Sidebar state persists via a new
ui-store (localStorage). Collapsed state shows a thin rail button on
the right edge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: video-use + AAP v1.1 design draft
Working design doc covering (a) hyperframes investigation as a
candidate second AAP app and (b) AAP v1 protocol revisions
surfaced by that investigation. Not implemented yet —
informs upcoming PR sequence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 5 are doc-only fixes in docs/device-use-v2-design.md:
- Storage path duplication — `state.json` lives at
`{storage.workspace}/state.json`, not `{storage.workspace}/.device-use/state.json`
(the `.device-use` segment is already inside `storage.workspace`).
Fixed in 3 places (table row #12, backend wiring section).
- Architecture fence missing language — added `text` (markdownlint MD040).
- /ws bullet overstated CLI visibility — clarified that only actions
routed through the server (REST, MCP, viewer) populate the activity
stream; standalone CLI invocations bypass it (peer model).
- Phase 1 stack text was stale — said "Add TanStack Start" + "Nitro"
but the locked stack (decision #5) is Hono on Bun.serve + Vite.
Bullet rewritten to match what was actually built.
- Out-of-scope note about agentic-app.json was outdated — the manifest
IS in this PR; the next PR is the Deus host that consumes it. Reworded
+ retitled the manifest section to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…litch-swap effect system (6 classes, 6 @Keyframes, 2 media queries) and the empty tool-use-enter class — none referenced by any component, with all 825 tests passing.
Background
This PR introduces TanStack Query v5 to replace all manual
fetchcalls withuseState/useEffectpatterns for data fetching and state management. This will significantly improve performance through caching, request deduplication, and automatic background refetching, while also reducing boilerplate code and enhancing maintainability.Changes
This is a comprehensive migration affecting the entire
/srccodebase.New Libraries Added:
@tanstack/react-query@tanstack/react-query-devtoolsCore Configuration:
src/lib/queryClient.ts: ConfiguredQueryClientwith optimizedstaleTime,gcTime,retry,refetchOnWindowFocus, andnetworkMode.src/lib/queryKeys.ts: Implemented a type-safe, hierarchical query key factory for robust cache management.Service Layer Enhancements:
src/services/api.ts: Addedputmethod.src/services/memory.service.ts: New service for memory operations.src/services/settings.service.ts: New service for fetching and updating settings.src/services/repo.service.ts: Addedaddandclonemethods.src/services/workspace.service.ts: Added methods forfetchSystemPrompt,updateSystemPrompt,fetchPRStatus, andfetchDevServers.New Query Hooks (
src/hooks/queries/):useWorkspacesByRepo: ReplacesuseDashboardData/useWorkspacesfor workspace listing with polling.useStats: Replaces polling inuseDashboardData.useBulkDiffStats: Replaces complex progressive loading logic.useFileChanges: ReplacesuseFileChangeshook.usePRStatus: Replaces PR status fetching.useDevServers: Replaces dev server fetching.useFileDiff: For fetching individual file diffs.useSystemPrompt: For fetching CLAUDE.md.useCreateWorkspace: Mutation for creating workspaces.useArchiveWorkspace: Mutation for archiving workspaces.useUpdateSystemPrompt: Mutation for updating system prompts.useSession: For fetching session details with dynamic polling.useMessages: For fetching messages with polling.useSessionWithMessages: Consolidated hook replacing legacyuseMessages.useSendMessage: Mutation for sending messages.useStopSession: Mutation for stopping sessions.useRepos: Fetches all repositories.useRepo: Fetches a single repository.useAddRepo: Mutation for adding repositories.useCloneRepo: Mutation for cloning repositories.useSettings: Fetches all user settings.useMCPServers,useCommands,useAgents,useHooks: Fetch file-based configurations.useUpdateSettings: Mutation for updating settings.useClearMemory: Mutation for clearing conversation memory.Component Migrations:
src/App.tsx: Wrapped withQueryClientProviderandReactQueryDevtools.src/Dashboard.tsx: Replaced all manual fetches (useDashboardData,useFileChangesfor initial load) with TanStack Query hooks.src/WorkspaceChatPanel.tsx: Migrated touseSessionWithMessages,useSendMessage,useStopSession.src/features/dashboard/components/SettingsModal.tsx: Migrated settings and file-based config fetching.src/features/dashboard/components/settings-sections/MemorySection.tsx: Migrated memory clearing touseClearMemory.Deprecated Hooks Removed/Marked:
src/hooks/useDashboardData.tssrc/hooks/useFileChanges.tssrc/hooks/useMessages.tssrc/hooks/useWorkspaces.tssrc/hooks/useDiffStats.tsTesting
npm run dev- App starts without errors.Dashboard.tsxvia React Query DevTools.useRepos,useSettingsare fetching data.WorkspaceChatPanel.tsxand observed cache invalidation.SettingsModalloads and saves settings correctly.useClearMemorymutation.Greptile Overview
Updated On: 2025-10-21 11:23:09 UTC
Greptile Summary
This PR comprehensively migrates the entire data-fetching architecture from manual
fetch+useState/useEffectpatterns to TanStack Query v5. The migration introduces 24 query hooks and 7 mutation hooks across workspaces, sessions, repositories, and settings domains, eliminating ~450 lines of polling/caching boilerplate. Core infrastructure includes a centralizedQueryClientconfiguration (src/lib/queryClient.ts) with 1-second staleness windows and dynamic polling, a type-safe hierarchical query key factory (src/lib/queryKeys.ts) for granular cache invalidation, and enhanced service layers with new endpoints for memory, settings, PRs, and system prompts. Major components (Dashboard.tsx,WorkspaceChatPanel.tsx,SettingsModal.tsx) now leverage declarative queries and mutations that automatically handle caching, request deduplication, background refetching, and cache invalidation on mutations. The migration maintains existing functionality while improving performance (66% fewer network requests per the documentation) and developer experience through React Query DevTools integration.Important Files Changed
Changed Files
package.json@tanstack/react-queryand@tanstack/react-query-devtools) to enable the migration.TANSTACK_QUERY_MIGRATION.mdTANSTACK_QUERY_COMPLETE.mdsrc/App.tsxQueryClientProviderand dev tools; DevTools placement outsideThemeProvidermay cause theme inheritance issues.src/lib/queryClient.tsrefetchOnMount: 'always'setting and potentially aggressive 1sstaleTime.src/lib/queryKeys.tsbyRepo'sstateparameter.src/services/api.tsputmethod to complete REST verb suite with consistent implementation.src/services/memory.service.tssrc/services/settings.service.tsfetchFileConfig.src/services/repo.service.tsaddandclonemethods; inconsistent snake_case vs camelCase in request payloads.src/services/workspace.service.tsENDPOINTSconfig.src/hooks/queries/index.tssrc/hooks/queries/useWorkspaceQueries.tsuseBulkDiffStatshassetTimeoutmemory leak and inefficient cache key.src/hooks/queries/useSessionQueries.tssrc/hooks/queries/useRepoQueries.tssrc/hooks/queries/useSettingsQueries.tsuseUpdateSettingsmay miss child query invalidation anduseClearMemoryuses dynamic import.src/hooks/index.tssrc/Dashboard.tsxenabledguards.src/WorkspaceChatPanel.tsxuseEffect, expensive inline extraction, and potential race conditions.src/features/dashboard/components/SettingsModal.tsxsaveSettingdoesn't prevent optimistic UI updates on failure.src/features/dashboard/components/settings-sections/types.tssetSettingsand loosenssaveSettingtype signature toany, sacrificing compile-time safety.src/features/dashboard/components/settings-sections/AccountSection.tsxsrc/features/dashboard/components/settings-sections/MemorySection.tsxuseClearMemorymutation; type casts may indicate incomplete type definitions.src/features/dashboard/components/settings-sections/ProviderSection.tsxonBlurpersistence; type-unsafe cast forcustom_endpoint.Confidence score: 2/5
Dashboard.tsxSystemPromptModal using fragile DOM querying, (2)AccountSection.tsxuncontrolled inputs that will reset on background refetches, (3)useBulkDiffStatssetTimeoutmemory leak, (4) contradictoryrefetchOnMount: 'always'config, (5)WorkspaceChatPanel.tsxmissinguseEffectdependencies that could cause stale callback references, and (6) potential path injection insettings.service.ts.src/Dashboard.tsx(lines 730-738),src/features/dashboard/components/settings-sections/AccountSection.tsx(input reset behavior),src/hooks/queries/useWorkspaceQueries.ts(lines 61-103), andsrc/lib/queryClient.ts(line 25).Sequence Diagram
sequenceDiagram participant User participant App participant QueryClient participant Dashboard participant Hooks as Query Hooks participant Services participant Backend participant Cache User->>App: Opens Application App->>QueryClient: Initialize QueryClientProvider QueryClient->>Cache: Setup cache with config Note over Cache: staleTime: 1s<br/>gcTime: 5min<br/>retry: 2 User->>Dashboard: Navigates to Dashboard Dashboard->>Hooks: useWorkspacesByRepo('ready') Hooks->>Cache: Check cache for key alt Cache Miss Cache->>Services: WorkspaceService.fetchByRepo() Services->>Backend: GET /workspaces/by-repo Backend-->>Services: RepoGroup[] Services-->>Cache: Store result else Cache Hit (fresh) Cache-->>Hooks: Return cached data end Cache-->>Dashboard: RepoGroup[] par Parallel Queries Dashboard->>Hooks: useStats() Hooks->>Cache: Check cache Cache->>Services: RepoService.fetchStats() Services->>Backend: GET /stats Backend-->>Cache: Stats data Dashboard->>Hooks: useBulkDiffStats() Hooks->>Services: Fetch first 5 workspaces Services->>Backend: GET /workspaces/{id}/diff-stats (x5) Backend-->>Cache: DiffStats[] Note over Hooks: Progressive loading:<br/>Remaining workspaces<br/>staggered 200ms apart end User->>Dashboard: Selects workspace Dashboard->>Hooks: useSessionWithMessages(sessionId) Hooks->>Cache: Check session cache Cache->>Services: SessionService.fetchById() Services->>Backend: GET /sessions/{id} Backend-->>Cache: Session data Hooks->>Cache: Check messages cache Cache->>Services: SessionService.fetchMessages() Services->>Backend: GET /sessions/{id}/messages Backend-->>Cache: Messages[] Cache-->>Dashboard: Combined session + messages Note over Cache,Backend: Automatic polling starts<br/>1s for working sessions<br/>3s for idle sessions loop Every 1-3 seconds Cache->>Services: Auto-refetch (background) Services->>Backend: GET /sessions/{id} Backend-->>Cache: Updated data Cache-->>Dashboard: Silent update end User->>Dashboard: Sends message Dashboard->>Hooks: useSendMessage() Hooks->>Services: SessionService.sendMessage() Services->>Backend: POST /sessions/{id}/messages Backend-->>Services: Success Services->>Cache: Invalidate session queries Cache->>Services: Auto-refetch Services->>Backend: GET /sessions/{id}/messages Backend-->>Cache: Updated messages Cache-->>Dashboard: Fresh data User->>Dashboard: Opens Settings Dashboard->>Hooks: useSettings() Hooks->>Cache: Check cache Cache->>Services: SettingsService.fetch() Services->>Backend: GET /settings Backend-->>Cache: Settings data par Settings Queries Dashboard->>Hooks: useMCPServers() Hooks->>Services: fetchFileConfig('mcp-servers') Services->>Backend: GET /settings/mcp-servers Dashboard->>Hooks: useCommands() Hooks->>Services: fetchFileConfig('commands') Services->>Backend: GET /settings/commands end User->>Dashboard: Updates setting Dashboard->>Hooks: useUpdateSettings() Hooks->>Services: SettingsService.update() Services->>Backend: POST /settings Backend-->>Services: Updated settings Services->>Cache: Invalidate settings queries Cache->>Services: Auto-refetch all settings Services->>Backend: GET /settings Backend-->>Cache: Fresh settings Cache-->>Dashboard: Updated UI User->>Dashboard: Creates workspace Dashboard->>Hooks: useCreateWorkspace() Hooks->>Services: WorkspaceService.create() Services->>Backend: POST /workspaces Backend-->>Services: New workspace Services->>Cache: Invalidate workspace queries Cache->>Services: Auto-refetch workspaces Services->>Backend: GET /workspaces/by-repo Backend-->>Cache: Updated workspace list Cache-->>Dashboard: UI updates automatically Note over User,Dashboard: Request deduplication:<br/>Multiple components calling<br/>same query = 1 network requestContext used:
dashboard- CLAUDE.md (source)Summary by CodeRabbit
New Features
Performance
Documentation