- Rewrite useWorkspaceTools hook to use @tanstack/react-query + Eden - Add QueryClientProvider to app layout - Remove syncWorkspaceTools/listWorkspaceToolsForContext from database layer - Remove workspace tool inventory sync from service and worker - Tools are now fetched directly from the API, not materialized to Convex
Unified · Split
| @@ -27,7 +27,6 @@ type MutationName = · 26 unmodified lines | ||
| 27 | 27 | | "database:upsertAccessPolicy" |
| 28 | 28 | | "database:upsertCredential" |
| 29 | 29 | | "database:upsertToolSource" |
| 30 | − | "database:syncWorkspaceTools" | |
| 31 | 30 | | "database:deleteToolSource" |
| 32 | 31 | | "database:createTaskEvent"; |
| 33 | 32 | |
| @@ -46,7 +45,6 @@ type QueryName = · 12 unmodified lines | ||
| 46 | 45 | | "database:resolveCredential" |
| 47 | 46 | | "database:listToolSources" |
| 48 | 47 | | "database:listToolSourceWorkspaceUpdates" |
| 49 | − | "database:listWorkspaceToolsForContext" | |
| 50 | 48 | | "database:listTaskEvents"; |
| 51 | 49 | |
| 52 | 50 | export class ExecutorDatabase { |
| @@ -225,21 +223,6 @@ export class ExecutorDatabase { · 172 unmodified lines | ||
| 225 | 223 | return await this.query("database:listToolSourceWorkspaceUpdates", {}); |
| 226 | 224 | } |
| 227 | 225 | |
| 228 | − async syncWorkspaceTools(params: { | |
| 229 | − workspaceId: string; | |
| 230 | − tools: ToolDescriptor[]; | |
| 231 | − }): Promise<boolean> { | |
| 232 | − return await this.mutation("database:syncWorkspaceTools", params); | |
| 233 | − } | |
| 234 | − | |
| 235 | − async listWorkspaceToolsForContext(params: { | |
| 236 | − workspaceId: string; | |
| 237 | − actorId?: string; | |
| 238 | − clientId?: string; | |
| 239 | − }): Promise<ToolDescriptor[]> { | |
| 240 | − return await this.query("database:listWorkspaceToolsForContext", params); | |
| 241 | − } | |
| 242 | − | |
| 243 | 226 | async deleteToolSource(workspaceId: string, sourceId: string): Promise<boolean> { |
| 244 | 227 | return await this.mutation("database:deleteToolSource", { workspaceId, sourceId }); |
| 245 | 228 | } |
| @@ -131,7 +131,6 @@ export class ExecutorService { · 130 unmodified lines | ||
| 131 | 131 | string, |
| 132 | 132 | { signature: string; loadedAt: number; tools: Map<string, ToolDefinition> } |
| 133 | 133 | >(); |
| 134 | − private readonly workspaceToolInventorySignatures = new Map<string, string>(); | |
| 135 | 134 | private readonly workspaceToolLoadWarnings = new Map<string, string[]>(); |
| 136 | 135 | private readonly inFlightTaskIds = new Set<string>(); |
| 137 | 136 | private readonly autoExecuteTasks: boolean; |
| @@ -491,9 +490,6 @@ export class ExecutorService { · 353 unmodified lines | ||
| 491 | 490 | const signature = sourceSignature(workspaceId, sources); |
| 492 | 491 | const cached = this.workspaceToolCache.get(workspaceId); |
| 493 | 492 | if (cached && cached.signature === signature) { |
| 494 | − if (this.workspaceToolInventorySignatures.get(workspaceId) !== signature) { | |
| 495 | − await this.syncWorkspaceToolInventory(workspaceId, cached.tools, signature); | |
| 496 | − } | |
| 497 | 493 | return cached.tools; |
| 498 | 494 | } |
| 499 | 495 | |
| @@ -531,32 +527,9 @@ export class ExecutorService { · 31 unmodified lines | ||
| 531 | 527 | tools: merged, |
| 532 | 528 | }); |
| 533 | 529 | |
| 534 | − await this.syncWorkspaceToolInventory(workspaceId, merged, signature); | |
| 535 | − | |
| 536 | 530 | return merged; |
| 537 | 531 | } |
| 538 | 532 | |
| 539 | − private async syncWorkspaceToolInventory( | |
| 540 | − workspaceId: string, | |
| 541 | − tools: Map<string, ToolDefinition>, | |
| 542 | − signature: string, | |
| 543 | − ): Promise<void> { | |
| 544 | − const inventory = [...tools.values()].map((tool) => ({ | |
| 545 | − path: tool.path, | |
| 546 | − description: tool.description, | |
| 547 | − approval: tool.approval, | |
| 548 | − source: tool.source, | |
| 549 | − argsType: tool.metadata?.argsType, | |
| 550 | − returnsType: tool.metadata?.returnsType, | |
| 551 | − })); | |
| 552 | − | |
| 553 | − await this.db.syncWorkspaceTools({ | |
| 554 | − workspaceId, | |
| 555 | − tools: inventory, | |
| 556 | − }); | |
| 557 | − this.workspaceToolInventorySignatures.set(workspaceId, signature); | |
| 558 | − } | |
| 559 | − | |
| 560 | 533 | private getToolDecision( |
| 561 | 534 | task: TaskRecord, |
| 562 | 535 | tool: ToolDefinition, |
| @@ -51,7 +51,6 @@ const convexClient = new ConvexClient(convexUrl, { · 50 unmodified lines | ||
| 51 | 51 | }); |
| 52 | 52 | |
| 53 | 53 | let draining = false; |
| 54 | −let syncingToolInventories = false; | |
| 55 | 54 | |
| 56 | 55 | async function drainQueue(trigger: string): Promise<void> { |
| 57 | 56 | if (draining) { |
| @@ -79,30 +78,6 @@ async function drainQueue(trigger: string): Promise<void> { · 21 unmodified lines | ||
| 79 | 78 | } |
| 80 | 79 | } |
| 81 | 80 | |
| 82 | −async function syncWorkspaceToolInventories(trigger: string): Promise<void> { | |
| 83 | − if (syncingToolInventories) { | |
| 84 | − return; | |
| 85 | − } | |
| 86 | − | |
| 87 | − syncingToolInventories = true; | |
| 88 | − try { | |
| 89 | − const updates = await service.listToolSourceWorkspaceUpdates(); | |
| 90 | − if (updates.length === 0) { | |
| 91 | − return; | |
| 92 | − } | |
| 93 | − | |
| 94 | − console.log(`[worker] ${trigger}: syncing tool inventories for ${updates.length} workspace(s)`); | |
| 95 | − for (const update of updates) { | |
| 96 | − await service.refreshWorkspaceTools(update.workspaceId); | |
| 97 | − } | |
| 98 | − } catch (error) { | |
| 99 | − const message = error instanceof Error ? error.message : String(error); | |
| 100 | − console.error(`[worker] tool inventory sync failed: ${message}`); | |
| 101 | − } finally { | |
| 102 | − syncingToolInventories = false; | |
| 103 | − } | |
| 104 | −} | |
| 105 | − | |
| 106 | 81 | const queueSubscription = convexClient.onUpdate( |
| 107 | 82 | api.database.listQueuedTaskIds, |
| 108 | 83 | { limit: 1 }, |
| @@ -116,31 +91,15 @@ const queueSubscription = convexClient.onUpdate( · 7 unmodified lines | ||
| 116 | 91 | }, |
| 117 | 92 | ); |
| 118 | 93 | |
| 119 | −const toolSourceSubscription = convexClient.onUpdate( | |
| 120 | − api.database.listToolSourceWorkspaceUpdates, | |
| 121 | − {}, | |
| 122 | − (updates) => { | |
| 123 | − if (updates.length > 0) { | |
| 124 | − void syncWorkspaceToolInventories("onUpdate"); | |
| 125 | − } | |
| 126 | − }, | |
| 127 | − (error) => { | |
| 128 | − console.warn(`[worker] tool source watcher error: ${error.message}`); | |
| 129 | − }, | |
| 130 | −); | |
| 131 | − | |
| 132 | 94 | const interval = setInterval(() => { |
| 133 | 95 | void drainQueue("interval"); |
| 134 | − void syncWorkspaceToolInventories("interval"); | |
| 135 | 96 | }, pollMs); |
| 136 | 97 | |
| 137 | 98 | await drainQueue("startup"); |
| 138 | −await syncWorkspaceToolInventories("startup"); | |
| 139 | 99 | |
| 140 | 100 | function shutdown(signal: string): void { |
| 141 | 101 | console.log(`[worker] received ${signal}, shutting down...`); |
| 142 | 102 | queueSubscription.unsubscribe(); |
| 143 | − toolSourceSubscription.unsubscribe(); | |
| 144 | 103 | clearInterval(interval); |
| 145 | 104 | void convexClient.close(); |
| 146 | 105 | process.exit(0); |
| @@ -2,6 +2,7 @@ import type { Metadata } from "next"; · 1 unmodified line | ||
| 2 | 2 | import { JetBrains_Mono, Inter } from "next/font/google"; |
| 3 | 3 | import { Toaster } from "sonner"; |
| 4 | 4 | import { AppConvexProvider } from "@/lib/convex-provider"; |
| 5 | +import { QueryProvider } from "@/lib/query-provider"; | |
| 5 | 6 | import { SessionProvider } from "@/lib/session-context"; |
| 6 | 7 | import "./globals.css"; |
| 7 | 8 | |
| @@ -31,9 +32,11 @@ export default function RootLayout({ · 23 unmodified lines | ||
| 31 | 32 | className={`${inter.variable} ${jetbrainsMono.variable} antialiased`} |
| 32 | 33 | > |
| 33 | 34 | <AppConvexProvider> |
| 34 | − <SessionProvider> | |
| 35 | − {children} | |
| 36 | − </SessionProvider> | |
| 35 | + <QueryProvider> | |
| 36 | + <SessionProvider> | |
| 37 | + {children} | |
| 38 | + </SessionProvider> | |
| 39 | + </QueryProvider> | |
| 37 | 40 | </AppConvexProvider> |
| 38 | 41 | <Toaster |
| 39 | 42 | theme="dark" |
| @@ -251,7 +251,7 @@ export function DashboardView() { · 250 unmodified lines | ||
| 251 | 251 | </div> |
| 252 | 252 | |
| 253 | 253 | {/* Tools summary */} |
| 254 | − {tools && tools.length > 0 && ( | |
| 254 | + {tools.length > 0 && ( | |
| 255 | 255 | <Card className="bg-card border-border"> |
| 256 | 256 | <CardHeader className="pb-3"> |
| 257 | 257 | <CardTitle className="text-sm font-medium flex items-center gap-2"> |
| @@ -304,24 +304,13 @@ function AddSourceDialog({ · 303 unmodified lines | ||
| 304 | 304 | config: Record<string, unknown>, |
| 305 | 305 | ) => { |
| 306 | 306 | if (!context) return; |
| 307 | − // Write the tool source to the database | |
| 308 | 307 | await upsertToolSource({ |
| 309 | 308 | workspaceId: context.workspaceId, |
| 310 | 309 | name: sourceName, |
| 311 | 310 | type: sourceType, |
| 312 | 311 | config, |
| 313 | 312 | }); |
| 314 | − toast.success(`Source "${sourceName}" added`); | |
| 315 | − | |
| 316 | − // Trigger a server-side tool refresh so the workspace tools inventory | |
| 317 | − // is updated immediately (instead of waiting for the worker poll). | |
| 318 | − // Fetching the tools list forces the server to load external sources | |
| 319 | − // and sync the workspaceTools table. | |
| 320 | − try { | |
| 321 | − await fetch(`/api/tools?workspaceId=${encodeURIComponent(context.workspaceId)}`); | |
| 322 | − } catch { | |
| 323 | − // Server-side refresh is best-effort; the worker will eventually sync. | |
| 324 | − } | |
| 313 | + toast.success(`Source "${sourceName}" added — loading tools…`); | |
| 325 | 314 | }; |
| 326 | 315 | |
| 327 | 316 | const handlePresetAdd = async (preset: ApiPreset) => { |
| @@ -763,11 +752,9 @@ export function ToolsView() { · 435 unmodified lines | ||
| 763 | 752 | </TabsTrigger> |
| 764 | 753 | <TabsTrigger value="inventory" className="text-xs data-[state=active]:bg-background"> |
| 765 | 754 | Inventory |
| 766 | − {tools && ( | |
| 767 | − <span className="ml-1.5 text-[10px] font-mono text-muted-foreground"> | |
| 768 | − {tools.length} | |
| 769 | − </span> | |
| 770 | − )} | |
| 755 | + <span className="ml-1.5 text-[10px] font-mono text-muted-foreground"> | |
| 756 | + {toolsLoading ? "…" : tools.length} | |
| 757 | + </span> | |
| 771 | 758 | </TabsTrigger> |
| 772 | 759 | </TabsList> |
| 773 | 760 | |
| @@ -818,11 +805,9 @@ export function ToolsView() { · 44 unmodified lines | ||
| 818 | 805 | <CardTitle className="text-sm font-medium flex items-center gap-2"> |
| 819 | 806 | <Zap className="h-4 w-4 text-primary" /> |
| 820 | 807 | Available Tools |
| 821 | − {tools && ( | |
| 822 | − <span className="text-[10px] font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded"> | |
| 823 | − {tools.length} | |
| 824 | − </span> | |
| 825 | − )} | |
| 808 | + <span className="text-[10px] font-mono bg-primary/10 text-primary px-1.5 py-0.5 rounded"> | |
| 809 | + {toolsLoading ? "…" : tools.length} | |
| 810 | + </span> | |
| 826 | 811 | </CardTitle> |
| 827 | 812 | </CardHeader> |
| 828 | 813 | <CardContent className="pt-0"> |
| @@ -833,7 +818,7 @@ export function ToolsView() { · 4 unmodified lines | ||
| 833 | 818 | ))} |
| 834 | 819 | </div> |
| 835 | 820 | ) : ( |
| 836 | − <ToolInventory tools={tools ?? []} /> | |
| 821 | + <ToolInventory tools={tools} /> | |
| 837 | 822 | )} |
| 838 | 823 | </CardContent> |
| 839 | 824 | </Card> |
| @@ -1,7 +1,10 @@ | ||
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | −import { useQuery } from "convex/react"; | |
| 4 | −import { convexApi } from "../lib/convex-api"; | |
| 3 | +import { useQuery as useTanstackQuery } from "@tanstack/react-query"; | |
| 4 | +import { useQuery as useConvexQuery } from "convex/react"; | |
| 5 | +import { convexApi } from "@/lib/convex-api"; | |
| 6 | +import { executor } from "@/lib/executor-client"; | |
| 7 | +import type { ToolDescriptor } from "@/lib/types"; | |
| 5 | 8 | |
| 6 | 9 | interface WorkspaceContext { |
| 7 | 10 | workspaceId: string; |
| @@ -9,20 +12,45 @@ interface WorkspaceContext { · 1 unmodified line | ||
| 9 | 12 | clientId?: string; |
| 10 | 13 | } |
| 11 | 14 | |
| 15 | +/** | |
| 16 | + * Fetches tool metadata from the server API (`GET /api/tools`) via Eden Treaty, | |
| 17 | + * cached and deduplicated by TanStack Query. | |
| 18 | + * | |
| 19 | + * Automatically re-fetches when the Convex `toolSources` subscription changes | |
| 20 | + * (the reactive value is included in the query key). | |
| 21 | + */ | |
| 12 | 22 | export function useWorkspaceTools(context: WorkspaceContext | null) { |
| 13 | − const tools = useQuery( | |
| 14 | − convexApi.database.listWorkspaceToolsForContext, | |
| 15 | − context | |
| 16 | − ? { | |
| 17 | − workspaceId: context.workspaceId, | |
| 18 | − actorId: context.actorId, | |
| 19 | − clientId: context.clientId, | |
| 20 | − } | |
| 21 | − : "skip", | |
| 23 | + // Watch tool sources reactively so we invalidate when sources change | |
| 24 | + const toolSources = useConvexQuery( | |
| 25 | + convexApi.database.listToolSources, | |
| 26 | + context ? { workspaceId: context.workspaceId } : "skip", | |
| 22 | 27 | ); |
| 23 | 28 | |
| 29 | + const { data, isLoading } = useTanstackQuery({ | |
| 30 | + queryKey: [ | |
| 31 | + "workspace-tools", | |
| 32 | + context?.workspaceId, | |
| 33 | + context?.actorId, | |
| 34 | + context?.clientId, | |
| 35 | + toolSources, | |
| 36 | + ], | |
| 37 | + queryFn: async () => { | |
| 38 | + if (!context) return []; | |
| 39 | + const { data, error } = await executor.api.tools.get({ | |
| 40 | + query: { | |
| 41 | + workspaceId: context.workspaceId, | |
| 42 | + ...(context.actorId && { actorId: context.actorId }), | |
| 43 | + ...(context.clientId && { clientId: context.clientId }), | |
| 44 | + }, | |
| 45 | + }); | |
| 46 | + if (error) throw error; | |
| 47 | + return data as ToolDescriptor[]; | |
| 48 | + }, | |
| 49 | + enabled: !!context, | |
| 50 | + }); | |
| 51 | + | |
| 24 | 52 | return { |
| 25 | − tools, | |
| 26 | − loading: !!context && tools === undefined, | |
| 53 | + tools: data ?? [], | |
| 54 | + loading: !!context && isLoading, | |
| 27 | 55 | }; |
| 28 | 56 | } |
| @@ -0,0 +1,9 @@ | ||
| 1 | +import { QueryClient } from "@tanstack/react-query"; | |
| 2 | + | |
| 3 | +export const queryClient = new QueryClient({ | |
| 4 | + defaultOptions: { | |
| 5 | + queries: { | |
| 6 | + staleTime: 30_000, // tools don't change that often | |
| 7 | + }, | |
| 8 | + }, | |
| 9 | +}); | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { QueryClientProvider } from "@tanstack/react-query"; | |
| 4 | +import { queryClient } from "./query-client"; | |
| 5 | +import type { ReactNode } from "react"; | |
| 6 | + | |
| 7 | +export function QueryProvider({ children }: { children: ReactNode }) { | |
| 8 | + return ( | |
| 9 | + <QueryClientProvider client={queryClient}> | |
| 10 | + {children} | |
| 11 | + </QueryClientProvider> | |
| 12 | + ); | |
| 13 | +} | |