Unified · Split
| @@ -35,7 +35,7 @@ export function TaskMessage({ agentTaskId, prompt, workspaceId }: TaskMessagePro · 34 unmodified lines | ||
| 35 | 35 | |
| 36 | 36 | // Reactive queries — no polling! |
| 37 | 37 | const agentTask = useQuery(api.database.getAgentTask, { agentTaskId }); |
| 38 | − const pendingApprovals = useQuery(api.database.listPendingApprovals, { workspaceId }); | |
| 38 | + const pendingApprovals = useQuery(api.workspace.listPendingApprovals, { workspaceId }); | |
| 39 | 39 | |
| 40 | 40 | const status = agentTask?.status ?? "running"; |
| 41 | 41 | const isDone = status !== "running"; |
| @@ -36,7 +36,7 @@ if (PINNED_WORKSPACE_ID && PINNED_ACTOR_ID) { · 35 unmodified lines | ||
| 36 | 36 | const RETRY_DELAY_MS = 2000; |
| 37 | 37 | for (let attempt = 1; ; attempt++) { |
| 38 | 38 | try { |
| 39 | − const bootstrap = await convex.mutation(api.database.bootstrapAnonymousSession, { | |
| 39 | + const bootstrap = await convex.mutation(api.workspace.bootstrapAnonymousSession, { | |
| 40 | 40 | sessionId: ANON_SESSION_ID, |
| 41 | 41 | }); |
| 42 | 42 | ctx = { |
| @@ -94,6 +94,7 @@ · 93 unmodified lines | ||
| 94 | 94 | "@monaco-editor/react": "^4.7.0", |
| 95 | 95 | "@streamdown/code": "^1.0.1", |
| 96 | 96 | "@tanstack/react-query": "^5.80.7", |
| 97 | + "@tanstack/react-virtual": "^3.13.18", | |
| 97 | 98 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 98 | 99 | "class-variance-authority": "^0.7.1", |
| 99 | 100 | "clsx": "^2.1.1", |
| @@ -955,6 +956,10 @@ · 855 unmodified lines | ||
| 955 | 956 | |
| 956 | 957 | "@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="], |
| 957 | 958 | |
| 959 | + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A=="], | |
| 960 | + | |
| 961 | + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="], | |
| 962 | + | |
| 958 | 963 | "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], |
| 959 | 964 | |
| 960 | 965 | "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], |
| @@ -16,7 +16,7 @@ · 15 unmodified lines | ||
| 16 | 16 | "@monaco-editor/react": "^4.7.0", |
| 17 | 17 | "@streamdown/code": "^1.0.1", |
| 18 | 18 | "@tanstack/react-query": "^5.80.7", |
| 19 | − | |
| 19 | + "@tanstack/react-virtual": "^3.13.18", | |
| 20 | 20 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 21 | 21 | "class-variance-authority": "^0.7.1", |
| 22 | 22 | "clsx": "^2.1.1", |
| @@ -5,8 +5,10 @@ import { · 4 unmodified lines | ||
| 5 | 5 | useMemo, |
| 6 | 6 | useCallback, |
| 7 | 7 | useEffect, |
| 8 | + useRef, | |
| 8 | 9 | memo, |
| 9 | 10 | } from "react"; |
| 11 | +import { useVirtualizer } from "@tanstack/react-virtual"; | |
| 10 | 12 | import { |
| 11 | 13 | Search, |
| 12 | 14 | ChevronRight, |
| @@ -1197,44 +1199,98 @@ export function ToolExplorer({ · 1184 unmodified lines | ||
| 1197 | 1199 | </div> |
| 1198 | 1200 | |
| 1199 | 1201 | {/* Scrollable list */} |
| 1200 | − <ScrollArea className="flex-1 rounded-md border border-border/30 bg-background/30"> | |
| 1201 | − {viewMode === "flat" ? ( | |
| 1202 | − flatTools.length === 0 ? ( | |
| 1202 | + {viewMode === "flat" ? ( | |
| 1203 | + flatTools.length === 0 ? ( | |
| 1204 | + <div className="flex-1 rounded-md border border-border/30 bg-background/30"> | |
| 1205 | + <EmptyState hasSearch={!!search} /> | |
| 1206 | + </div> | |
| 1207 | + ) : ( | |
| 1208 | + <VirtualFlatList | |
| 1209 | + tools={flatTools} | |
| 1210 | + selectedKeys={selectedKeys} | |
| 1211 | + onSelectTool={toggleSelectTool} | |
| 1212 | + /> | |
| 1213 | + ) | |
| 1214 | + ) : ( | |
| 1215 | + <ScrollArea className="flex-1 rounded-md border border-border/30 bg-background/30"> | |
| 1216 | + {treeGroups.length === 0 ? ( | |
| 1203 | 1217 | <EmptyState hasSearch={!!search} /> |
| 1204 | 1218 | ) : ( |
| 1205 | 1219 | <div className="p-1"> |
| 1206 | − {flatTools.map((tool) => ( | |
| 1207 | − <SelectableToolRow | |
| 1208 | − key={tool.path} | |
| 1209 | − tool={tool} | |
| 1210 | − label={tool.path} | |
| 1220 | + {treeGroups.map((group) => ( | |
| 1221 | + <GroupNode | |
| 1222 | + key={group.key} | |
| 1223 | + group={group} | |
| 1211 | 1224 | depth={0} |
| 1225 | + expandedKeys={expandedKeys} | |
| 1226 | + onToggle={toggleExpand} | |
| 1212 | 1227 | selectedKeys={selectedKeys} |
| 1228 | + onSelectGroup={toggleSelectGroup} | |
| 1213 | 1229 | onSelectTool={toggleSelectTool} |
| 1230 | + search={search} | |
| 1214 | 1231 | /> |
| 1215 | 1232 | ))} |
| 1216 | 1233 | </div> |
| 1217 | − ) | |
| 1218 | − ) : treeGroups.length === 0 ? ( | |
| 1219 | − <EmptyState hasSearch={!!search} /> | |
| 1220 | − ) : ( | |
| 1221 | − <div className="p-1"> | |
| 1222 | − {treeGroups.map((group) => ( | |
| 1223 | − <GroupNode | |
| 1224 | − key={group.key} | |
| 1225 | − group={group} | |
| 1226 | − depth={0} | |
| 1227 | − expandedKeys={expandedKeys} | |
| 1228 | − onToggle={toggleExpand} | |
| 1229 | − selectedKeys={selectedKeys} | |
| 1230 | − onSelectGroup={toggleSelectGroup} | |
| 1231 | − onSelectTool={toggleSelectTool} | |
| 1232 | − search={search} | |
| 1233 | − /> | |
| 1234 | − ))} | |
| 1234 | + )} | |
| 1235 | + </ScrollArea> | |
| 1236 | + )} | |
| 1237 | + </div> | |
| 1238 | + </div> | |
| 1239 | + ); | |
| 1240 | +} | |
| 1241 | + | |
| 1242 | +// ── Virtualized flat list — only renders visible rows ── | |
| 1243 | + | |
| 1244 | +const ROW_HEIGHT = 32; | |
| 1245 | + | |
| 1246 | +function VirtualFlatList({ | |
| 1247 | + tools, | |
| 1248 | + selectedKeys, | |
| 1249 | + onSelectTool, | |
| 1250 | +}: { | |
| 1251 | + tools: ToolDescriptor[]; | |
| 1252 | + selectedKeys: Set<string>; | |
| 1253 | + onSelectTool: (path: string, e: React.MouseEvent) => void; | |
| 1254 | +}) { | |
| 1255 | + const scrollRef = useRef<HTMLDivElement>(null); | |
| 1256 | + | |
| 1257 | + const virtualizer = useVirtualizer({ | |
| 1258 | + count: tools.length, | |
| 1259 | + getScrollElement: () => scrollRef.current, | |
| 1260 | + estimateSize: () => ROW_HEIGHT, | |
| 1261 | + overscan: 20, | |
| 1262 | + }); | |
| 1263 | + | |
| 1264 | + return ( | |
| 1265 | + <div | |
| 1266 | + ref={scrollRef} | |
| 1267 | + className="flex-1 rounded-md border border-border/30 bg-background/30 overflow-y-auto" | |
| 1268 | + > | |
| 1269 | + <div | |
| 1270 | + className="relative w-full p-1" | |
| 1271 | + style={{ height: virtualizer.getTotalSize() }} | |
| 1272 | + > | |
| 1273 | + {virtualizer.getVirtualItems().map((vItem) => { | |
| 1274 | + const tool = tools[vItem.index]; | |
| 1275 | + return ( | |
| 1276 | + <div | |
| 1277 | + key={tool.path} | |
| 1278 | + className="absolute left-0 right-0 px-1" | |
| 1279 | + style={{ | |
| 1280 | + height: vItem.size, | |
| 1281 | + transform: `translateY(${vItem.start}px)`, | |
| 1282 | + }} | |
| 1283 | + > | |
| 1284 | + <SelectableToolRow | |
| 1285 | + tool={tool} | |
| 1286 | + label={tool.path} | |
| 1287 | + depth={0} | |
| 1288 | + selectedKeys={selectedKeys} | |
| 1289 | + onSelectTool={onSelectTool} | |
| 1290 | + /> | |
| 1235 | 1291 | </div> |
| 1236 | − )} | |
| 1237 | − </ScrollArea> | |
| 1292 | + ); | |
| 1293 | + })} | |
| 1238 | 1294 | </div> |
| 1239 | 1295 | </div> |
| 1240 | 1296 | ); |
| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | import { v } from "convex/values"; |
| 2 | 2 | import type { Doc } from "./_generated/dataModel"; |
| 3 | −import type { MutationCtx, QueryCtx } from "./_generated/server"; | |
| 3 | +import { mutation, | |
| 4 | +query, | |
| 5 | +type MutationCtx, type QueryCtx } from "./_generated/server"; | |
| 4 | 6 | import { internalMutation, internalQuery } from "./_generated/server"; |
| 5 | 7 | import { ensureUniqueSlug } from "../lib/slug"; |
| 6 | 8 | import type { TaskStatus } from "@executor/contracts"; |
| @@ -647,7 +649,7 @@ async function getAgentTaskDoc(ctx: { db: QueryCtx["db"] }, agentTaskId: string) · 640 unmodified lines | ||
| 647 | 649 | .unique(); |
| 648 | 650 | } |
| 649 | 651 | |
| 650 | −export const createAgentTask = internalMutation({ | |
| 652 | +export const createAgentTask = mutation({ | |
| 651 | 653 | args: { |
| 652 | 654 | id: v.string(), |
| 653 | 655 | prompt: v.string(), |
| @@ -680,7 +682,7 @@ export const createAgentTask = internalMutation({ · 26 unmodified lines | ||
| 680 | 682 | }, |
| 681 | 683 | }); |
| 682 | 684 | |
| 683 | −export const getAgentTask = internalQuery({ | |
| 685 | +export const getAgentTask = query({ | |
| 684 | 686 | args: { agentTaskId: v.string() }, |
| 685 | 687 | handler: async (ctx, args) => { |
| 686 | 688 | const doc = await getAgentTaskDoc(ctx, args.agentTaskId); |
| @@ -688,7 +690,7 @@ export const getAgentTask = internalQuery({ · 1 unmodified line | ||
| 688 | 690 | }, |
| 689 | 691 | }); |
| 690 | 692 | |
| 691 | −export const updateAgentTask = internalMutation({ | |
| 693 | +export const updateAgentTask = mutation({ | |
| 692 | 694 | args: { |
| 693 | 695 | agentTaskId: v.string(), |
| 694 | 696 | status: v.optional(agentTaskStatusValidator), |