Unified · Split
| @@ -35,7 +35,7 @@ export function TaskMessage({ agentTaskId, prompt, workspaceId }: TaskMessagePro · 34 unmodified lines | |||
| 35 | 35 | ||
| 36 | // Reactive queries — no polling! | 36 | // Reactive queries — no polling! |
| 37 | const agentTask = useQuery(api.database.getAgentTask, { agentTaskId }); | 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 | const status = agentTask?.status ?? "running"; | 40 | const status = agentTask?.status ?? "running"; |
| 41 | const isDone = status !== "running"; | 41 | const isDone = status !== "running"; |
| @@ -36,7 +36,7 @@ if (PINNED_WORKSPACE_ID && PINNED_ACTOR_ID) { · 35 unmodified lines | |||
| 36 | const RETRY_DELAY_MS = 2000; | 36 | const RETRY_DELAY_MS = 2000; |
| 37 | for (let attempt = 1; ; attempt++) { | 37 | for (let attempt = 1; ; attempt++) { |
| 38 | try { | 38 | try { |
| 39 | const bootstrap = await convex.mutation(api.database.bootstrapAnonymousSession, { | 39 | const bootstrap = await convex.mutation(api.workspace.bootstrapAnonymousSession, { |
| 40 | sessionId: ANON_SESSION_ID, | 40 | sessionId: ANON_SESSION_ID, |
| 41 | }); | 41 | }); |
| 42 | ctx = { | 42 | ctx = { |
| @@ -94,6 +94,7 @@ · 93 unmodified lines | |||
| 94 | "@monaco-editor/react": "^4.7.0", | 94 | "@monaco-editor/react": "^4.7.0", |
| 95 | "@streamdown/code": "^1.0.1", | 95 | "@streamdown/code": "^1.0.1", |
| 96 | "@tanstack/react-query": "^5.80.7", | 96 | "@tanstack/react-query": "^5.80.7", |
| 97 | "@tanstack/react-virtual": "^3.13.18", | ||
| 97 | "@workos-inc/authkit-nextjs": "^2.13.0", | 98 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 98 | "class-variance-authority": "^0.7.1", | 99 | "class-variance-authority": "^0.7.1", |
| 99 | "clsx": "^2.1.1", | 100 | "clsx": "^2.1.1", |
| @@ -955,6 +956,10 @@ · 855 unmodified lines | |||
| 955 | 956 | ||
| 956 | "@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 | "@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 | "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], | 963 | "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], |
| 959 | 964 | ||
| 960 | "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], | 965 | "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], |
| @@ -16,7 +16,7 @@ · 15 unmodified lines | |||
| 16 | "@monaco-editor/react": "^4.7.0", | 16 | "@monaco-editor/react": "^4.7.0", |
| 17 | "@streamdown/code": "^1.0.1", | 17 | "@streamdown/code": "^1.0.1", |
| 18 | "@tanstack/react-query": "^5.80.7", | 18 | "@tanstack/react-query": "^5.80.7", |
| 19 | 19 | "@tanstack/react-virtual": "^3.13.18", | |
| 20 | "@workos-inc/authkit-nextjs": "^2.13.0", | 20 | "@workos-inc/authkit-nextjs": "^2.13.0", |
| 21 | "class-variance-authority": "^0.7.1", | 21 | "class-variance-authority": "^0.7.1", |
| 22 | "clsx": "^2.1.1", | 22 | "clsx": "^2.1.1", |
| @@ -5,8 +5,10 @@ import { · 4 unmodified lines | |||
| 5 | useMemo, | 5 | useMemo, |
| 6 | useCallback, | 6 | useCallback, |
| 7 | useEffect, | 7 | useEffect, |
| 8 | useRef, | ||
| 8 | memo, | 9 | memo, |
| 9 | } from "react"; | 10 | } from "react"; |
| 11 | import { useVirtualizer } from "@tanstack/react-virtual"; | ||
| 10 | import { | 12 | import { |
| 11 | Search, | 13 | Search, |
| 12 | ChevronRight, | 14 | ChevronRight, |
| @@ -1197,44 +1199,98 @@ export function ToolExplorer({ · 1184 unmodified lines | |||
| 1197 | </div> | 1199 | </div> |
| 1198 | 1200 | ||
| 1199 | {/* Scrollable list */} | 1201 | {/* Scrollable list */} |
| 1200 | <ScrollArea className="flex-1 rounded-md border border-border/30 bg-background/30"> | 1202 | {viewMode === "flat" ? ( |
| 1201 | {viewMode === "flat" ? ( | 1203 | flatTools.length === 0 ? ( |
| 1202 | 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 | <EmptyState hasSearch={!!search} /> | 1217 | <EmptyState hasSearch={!!search} /> |
| 1204 | ) : ( | 1218 | ) : ( |
| 1205 | <div className="p-1"> | 1219 | <div className="p-1"> |
| 1206 | {flatTools.map((tool) => ( | 1220 | {treeGroups.map((group) => ( |
| 1207 | <SelectableToolRow | 1221 | <GroupNode |
| 1208 | key={tool.path} | 1222 | key={group.key} |
| 1209 | tool={tool} | 1223 | group={group} |
| 1210 | label={tool.path} | ||
| 1211 | depth={0} | 1224 | depth={0} |
| 1225 | expandedKeys={expandedKeys} | ||
| 1226 | onToggle={toggleExpand} | ||
| 1212 | selectedKeys={selectedKeys} | 1227 | selectedKeys={selectedKeys} |
| 1228 | onSelectGroup={toggleSelectGroup} | ||
| 1213 | onSelectTool={toggleSelectTool} | 1229 | onSelectTool={toggleSelectTool} |
| 1230 | search={search} | ||
| 1214 | /> | 1231 | /> |
| 1215 | ))} | 1232 | ))} |
| 1216 | </div> | 1233 | </div> |
| 1217 | ) | 1234 | )} |
| 1218 | ) : treeGroups.length === 0 ? ( | 1235 | </ScrollArea> |
| 1219 | <EmptyState hasSearch={!!search} /> | 1236 | )} |
| 1220 | ) : ( | 1237 | </div> |
| 1221 | <div className="p-1"> | 1238 | </div> |
| 1222 | {treeGroups.map((group) => ( | 1239 | ); |
| 1223 | <GroupNode | 1240 | } |
| 1224 | key={group.key} | 1241 | |
| 1225 | group={group} | 1242 | // ── Virtualized flat list — only renders visible rows ── |
| 1226 | depth={0} | 1243 | |
| 1227 | expandedKeys={expandedKeys} | 1244 | const ROW_HEIGHT = 32; |
| 1228 | onToggle={toggleExpand} | 1245 | |
| 1229 | selectedKeys={selectedKeys} | 1246 | function VirtualFlatList({ |
| 1230 | onSelectGroup={toggleSelectGroup} | 1247 | tools, |
| 1231 | onSelectTool={toggleSelectTool} | 1248 | selectedKeys, |
| 1232 | search={search} | 1249 | onSelectTool, |
| 1233 | /> | 1250 | }: { |
| 1234 | ))} | 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 | </div> | 1291 | </div> |
| 1236 | )} | 1292 | ); |
| 1237 | </ScrollArea> | 1293 | })} |
| 1238 | </div> | 1294 | </div> |
| 1239 | </div> | 1295 | </div> |
| 1240 | ); | 1296 | ); |
| @@ -1,6 +1,8 @@ | |||
| 1 | import { v } from "convex/values"; | 1 | import { v } from "convex/values"; |
| 2 | import type { Doc } from "./_generated/dataModel"; | 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 | import { internalMutation, internalQuery } from "./_generated/server"; | 6 | import { internalMutation, internalQuery } from "./_generated/server"; |
| 5 | import { ensureUniqueSlug } from "../lib/slug"; | 7 | import { ensureUniqueSlug } from "../lib/slug"; |
| 6 | import type { TaskStatus } from "@executor/contracts"; | 8 | import type { TaskStatus } from "@executor/contracts"; |
| @@ -647,7 +649,7 @@ async function getAgentTaskDoc(ctx: { db: QueryCtx["db"] }, agentTaskId: string) · 640 unmodified lines | |||
| 647 | .unique(); | 649 | .unique(); |
| 648 | } | 650 | } |
| 649 | 651 | ||
| 650 | export const createAgentTask = internalMutation({ | 652 | export const createAgentTask = mutation({ |
| 651 | args: { | 653 | args: { |
| 652 | id: v.string(), | 654 | id: v.string(), |
| 653 | prompt: v.string(), | 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 | args: { agentTaskId: v.string() }, | 686 | args: { agentTaskId: v.string() }, |
| 685 | handler: async (ctx, args) => { | 687 | handler: async (ctx, args) => { |
| 686 | const doc = await getAgentTaskDoc(ctx, args.agentTaskId); | 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 | args: { | 694 | args: { |
| 693 | agentTaskId: v.string(), | 695 | agentTaskId: v.string(), |
| 694 | status: v.optional(agentTaskStatusValidator), | 696 | status: v.optional(agentTaskStatusValidator), |