diff --git a/gadget-drone/package.json b/gadget-drone/package.json index f7a011f..4230606 100644 --- a/gadget-drone/package.json +++ b/gadget-drone/package.json @@ -24,6 +24,7 @@ "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8", "dependencies": { "@gadget/ai": "workspace:*", + "@gadget/ai-toolbox": "workspace:*", "@gadget/api": "workspace:*", "@gadget/config": "workspace:*", "@inquirer/prompts": "^8.4.2", diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts index fc616cc..3f1e07c 100644 --- a/gadget-drone/src/services/agent.ts +++ b/gadget-drone/src/services/agent.ts @@ -130,7 +130,7 @@ class AgentService extends GadgetService { // Chat tools — subagent spawning: available in all modes const subagentTool = new SubagentTool(this.toolbox); - subagentTool.setSpawner((agentType, prompt) => this.spawnSubagent(agentType, prompt)); + subagentTool.setSpawner((agentType: string, prompt: string) => this.spawnSubagent(agentType, prompt)); this.toolbox.register(subagentTool, readOnlyModes); // Project tools — skill discovery: available in all modes diff --git a/gadget-drone/src/tools/index.ts b/gadget-drone/src/tools/index.ts index eb77b76..8fbd15f 100644 --- a/gadget-drone/src/tools/index.ts +++ b/gadget-drone/src/tools/index.ts @@ -1,10 +1,27 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 -export { AiToolbox, type DroneToolboxEnvironment } from "./toolbox.ts"; -export { DroneTool } from "./tool.ts"; -export * from "./chat/index.ts"; -export * from "./system/index.ts"; -export * from "./network/index.ts"; -export * from "./plan/index.ts"; -export * from "./project/index.ts"; +// Re-export from @gadget/ai-toolbox +export { + AiToolbox, + type GadgetToolboxEnvironment as DroneToolboxEnvironment, + FileReadTool, + FileWriteTool, + FileEditTool, + ShellExecTool, + ListTool, + GrepTool, + GlobTool, + GoogleSearchTool, + FetchUrlTool, + PlanFileReadTool, + PlanFileWriteTool, + PlanFileEditTool, + PlanListTool, + SubagentTool, + ListSkillsTool, + ReadSkillTool, +} from "@gadget/ai-toolbox"; + +// Local tools (gadget-drone specific) +export { SubprocessTool } from "./system/index.ts"; diff --git a/gadget-drone/src/tools/system/index.ts b/gadget-drone/src/tools/system/index.ts index 3ebc4b8..79e0ee3 100644 --- a/gadget-drone/src/tools/system/index.ts +++ b/gadget-drone/src/tools/system/index.ts @@ -1,11 +1,4 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 -export { FileReadTool } from "./read.ts"; -export { FileWriteTool } from "./write.ts"; -export { FileEditTool } from "./edit.ts"; -export { ShellExecTool } from "./shell.ts"; -export { ListTool } from "./list.ts"; -export { GrepTool } from "./grep.ts"; -export { GlobTool } from "./glob.ts"; export { SubprocessTool } from "./subprocess.ts"; diff --git a/gadget-drone/src/tools/system/subprocess.test.ts b/gadget-drone/src/tools/system/subprocess.test.ts deleted file mode 100644 index 72f22f6..0000000 --- a/gadget-drone/src/tools/system/subprocess.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { IAiLogger } from "@gadget/ai"; -import { AiToolbox, type DroneToolboxEnvironment } from "../toolbox.ts"; -import { SubprocessTool } from "./subprocess.ts"; - -vi.mock("../../services/subprocess.ts", () => ({ - default: { - create: vi.fn(), - ps: vi.fn(), - killPid: vi.fn(), - killAll: vi.fn(), - haltPid: vi.fn(), - getLog: vi.fn(), - }, -})); - -const mockLogger: IAiLogger = { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), -}; - -const env: DroneToolboxEnvironment = { - NODE_ENV: "test", - workspace: { - workspaceDir: "/tmp/workspace", - projectDir: "/tmp/workspace/test-project", - cacheDir: "/tmp/workspace/.gadget/cache", - }, -}; - -describe("SubprocessTool", () => { - let toolbox: AiToolbox; - let tool: SubprocessTool; - - beforeEach(() => { - toolbox = new AiToolbox(env); - tool = new SubprocessTool(toolbox); - vi.clearAllMocks(); - }); - - it("has the correct name and category", () => { - expect(tool.name).toBe("subprocess"); - expect(tool.category).toBe("system"); - }); - - it("defines the subprocess tool definition with all commands", () => { - const params = tool.definition.function.parameters as Record; - const cmd = params.properties?.cmd as Record | undefined; - const cmds: string[] = cmd?.enum ?? []; - - expect(tool.definition.type).toBe("function"); - expect(tool.definition.function.name).toBe("subprocess"); - expect(cmds).toContain("create"); - expect(cmds).toContain("list"); - expect(cmds).toContain("kill"); - expect(cmds).toContain("killAll"); - expect(cmds).toContain("halt"); - expect(cmds).toContain("log"); - }); - - it("returns error when cmd is missing", async () => { - const result = await tool.execute({}, mockLogger); - expect(result).toContain("MISSING_PARAMETER"); - expect(result).toContain("cmd"); - }); - - it("returns error when cmd is invalid", async () => { - const result = await tool.execute({ cmd: "invalid" }, mockLogger); - expect(result).toContain("INVALID_PARAMETER"); - }); - - describe("cmd=create", () => { - it("returns error when command is missing", async () => { - const result = await tool.execute({ cmd: "create" }, mockLogger); - expect(result).toContain("MISSING_PARAMETER"); - expect(result).toContain("command"); - }); - - it("calls SubProcessService.create with args", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.create as ReturnType).mockResolvedValue({ - pid: 42, - stdoutFileName: "/tmp/.gadget/subprocess-logs/subproc-42.stdout.log", - stderrFileName: "/tmp/.gadget/subprocess-logs/subproc-42.stderr.log", - }); - - const result = await tool.execute( - { cmd: "create", command: "node", args: ["server.js"] }, - mockLogger, - ); - - expect(SubProcessService.create).toHaveBeenCalledWith( - expect.objectContaining({ slug: "test-project" }), - "node", - ["server.js"], - ); - - expect(result).toContain("SUBPROCESS CREATED"); - expect(result).toContain("pid: 42"); - }); - - it("handles args as non-array gracefully", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.create as ReturnType).mockResolvedValue({ - pid: 7, - stdoutFileName: "/tmp/subproc-7.stdout.log", - stderrFileName: "/tmp/subproc-7.stderr.log", - }); - - const result = await tool.execute( - { cmd: "create", command: "echo", args: "hello" }, - mockLogger, - ); - - expect(result).toContain("pid: 7"); - }); - }); - - describe("cmd=list", () => { - it("returns empty list message when no processes", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.ps as ReturnType).mockReturnValue([]); - - const result = await tool.execute({ cmd: "list" }, mockLogger); - expect(result).toContain("(no running subprocesses)"); - }); - - it("lists running processes", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.ps as ReturnType).mockReturnValue([ - { - pid: 1, - project: { slug: "project-a" }, - createdAt: new Date("2026-01-01"), - updatedAt: new Date("2026-01-02"), - }, - { - pid: 2, - project: { slug: "project-b" }, - createdAt: new Date("2026-01-03"), - updatedAt: new Date("2026-01-04"), - }, - ]); - - const result = await tool.execute({ cmd: "list" }, mockLogger); - expect(result).toContain("SUBPROCESS LIST"); - expect(result).toContain("pid: 1"); - expect(result).toContain("pid: 2"); - expect(result).toContain("project-a"); - expect(result).toContain("project-b"); - }); - }); - - describe("cmd=kill", () => { - it("returns error when pid is missing", async () => { - const result = await tool.execute({ cmd: "kill" }, mockLogger); - expect(result).toContain("MISSING_PARAMETER"); - expect(result).toContain("pid"); - }); - - it("calls killPid and returns success", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.killPid as ReturnType).mockResolvedValue(true); - - const result = await tool.execute({ cmd: "kill", pid: 42 }, mockLogger); - expect(SubProcessService.killPid).toHaveBeenCalledWith(42); - expect(result).toContain("SUBPROCESS KILLED"); - expect(result).toContain("pid: 42"); - }); - - it("handles service errors", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.killPid as ReturnType).mockRejectedValue(new Error("not found")); - - const result = await tool.execute({ cmd: "kill", pid: 99 }, mockLogger); - expect(result).toContain("OPERATION_FAILED"); - }); - }); - - describe("cmd=killAll", () => { - it("calls killAll and returns success", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.killAll as ReturnType).mockResolvedValue(true); - - const result = await tool.execute({ cmd: "killAll" }, mockLogger); - expect(SubProcessService.killAll).toHaveBeenCalledOnce(); - expect(result).toContain("SUBPROCESS KILLALL"); - }); - }); - - describe("cmd=halt", () => { - it("returns error when pid is missing", async () => { - const result = await tool.execute({ cmd: "halt" }, mockLogger); - expect(result).toContain("MISSING_PARAMETER"); - expect(result).toContain("pid"); - }); - - it("calls haltPid and returns success", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.haltPid as ReturnType).mockResolvedValue(true); - - const result = await tool.execute({ cmd: "halt", pid: 42 }, mockLogger); - expect(SubProcessService.haltPid).toHaveBeenCalledWith(42); - expect(result).toContain("SUBPROCESS HALTED"); - expect(result).toContain("pid: 42"); - }); - }); - - describe("cmd=log", () => { - it("returns error when pid is missing", async () => { - const result = await tool.execute({ cmd: "log", which: "stdout" }, mockLogger); - expect(result).toContain("MISSING_PARAMETER"); - expect(result).toContain("pid"); - }); - - it("returns error when which is missing", async () => { - const result = await tool.execute({ cmd: "log", pid: 42 }, mockLogger); - expect(result).toContain("MISSING_PARAMETER"); - expect(result).toContain("which"); - }); - - it("returns error when which is invalid", async () => { - const result = await tool.execute({ cmd: "log", pid: 42, which: "invalid" }, mockLogger); - expect(result).toContain("MISSING_PARAMETER"); - expect(result).toContain("which"); - }); - - it("reads stdout log content", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.getLog as ReturnType).mockResolvedValue("line1\nline2\nline3"); - - const result = await tool.execute( - { cmd: "log", pid: 42, which: "stdout" }, - mockLogger, - ); - - expect(SubProcessService.getLog).toHaveBeenCalledWith(42, "stdout"); - expect(result).toContain("SUBPROCESS LOG (stdout)"); - expect(result).toContain("pid: 42"); - expect(result).toContain("line1"); - expect(result).toContain("line2"); - expect(result).toContain("line3"); - }); - - it("reads stderr log content", async () => { - const SubProcessService = (await import("../../services/subprocess.ts")).default; - (SubProcessService.getLog as ReturnType).mockResolvedValue("error: something broke"); - - const result = await tool.execute( - { cmd: "log", pid: 42, which: "stderr" }, - mockLogger, - ); - - expect(SubProcessService.getLog).toHaveBeenCalledWith(42, "stderr"); - expect(result).toContain("SUBPROCESS LOG (stderr)"); - expect(result).toContain("error: something broke"); - }); - }); -}); diff --git a/gadget-drone/src/tools/system/subprocess.ts b/gadget-drone/src/tools/system/subprocess.ts index a4da928..0f890fc 100644 --- a/gadget-drone/src/tools/system/subprocess.ts +++ b/gadget-drone/src/tools/system/subprocess.ts @@ -3,7 +3,7 @@ import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; import { formatError } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "@gadget/ai-toolbox"; import SubProcessService from "../../services/subprocess.ts"; const VALID_CMDS = [ @@ -17,7 +17,7 @@ const VALID_CMDS = [ type SubprocessCmd = (typeof VALID_CMDS)[number]; -export class SubprocessTool extends DroneTool { +export class SubprocessTool extends GadgetTool { get name(): string { return "subprocess"; } diff --git a/packages/ai-toolbox/.gitignore b/packages/ai-toolbox/.gitignore new file mode 100644 index 0000000..16c5327 --- /dev/null +++ b/packages/ai-toolbox/.gitignore @@ -0,0 +1,2 @@ +dist + diff --git a/packages/ai-toolbox/package.json b/packages/ai-toolbox/package.json new file mode 100644 index 0000000..e919934 --- /dev/null +++ b/packages/ai-toolbox/package.json @@ -0,0 +1,38 @@ +{ + "name": "@gadget/ai-toolbox", + "version": "1.0.0", + "description": "Gadget Code AI agent toolbox — shared tool implementations for agent processes", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist/", + "typecheck": "tsc --noEmit" + }, + "keywords": ["gadget", "ai", "toolbox", "tools", "agent"], + "author": "Rob Colbert", + "license": "Apache-2.0", + "dependencies": { + "@gadget/ai": "workspace:*", + "@gadget/api": "workspace:*", + "@mozilla/readability": "0.6.0", + "googleapis": "171.4.0", + "jsdom": "29.0.2", + "playwright": "1.59.1", + "turndown": "7.2.2" + }, + "devDependencies": { + "@types/jsdom": "27.0.0", + "@types/node": "^25.6.0", + "@types/turndown": "5.0.5", + "typescript": "^6.0.3" + } +} diff --git a/gadget-drone/src/tools/chat/index.ts b/packages/ai-toolbox/src/chat/index.ts similarity index 88% rename from gadget-drone/src/tools/chat/index.ts rename to packages/ai-toolbox/src/chat/index.ts index 0c5b6e7..171440f 100644 --- a/gadget-drone/src/tools/chat/index.ts +++ b/packages/ai-toolbox/src/chat/index.ts @@ -1,3 +1,4 @@ +// src/chat/index.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 diff --git a/gadget-drone/src/tools/chat/subagent.ts b/packages/ai-toolbox/src/chat/subagent.ts similarity index 97% rename from gadget-drone/src/tools/chat/subagent.ts rename to packages/ai-toolbox/src/chat/subagent.ts index 7823111..f5216b9 100644 --- a/gadget-drone/src/tools/chat/subagent.ts +++ b/packages/ai-toolbox/src/chat/subagent.ts @@ -1,14 +1,15 @@ +// src/chat/subagent.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; import { formatError } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; const VALID_AGENT_TYPES = ["explore", "general"] as const; type AgentType = (typeof VALID_AGENT_TYPES)[number]; -export class SubagentTool extends DroneTool { +export class SubagentTool extends GadgetTool { private _spawnSubagent: ((agentType: string, prompt: string) => Promise) | null = null; setSpawner(fn: typeof this._spawnSubagent): void { diff --git a/packages/ai-toolbox/src/index.ts b/packages/ai-toolbox/src/index.ts new file mode 100644 index 0000000..fb300a2 --- /dev/null +++ b/packages/ai-toolbox/src/index.ts @@ -0,0 +1,11 @@ +// src/index.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +export { AiToolbox, type GadgetToolboxEnvironment } from "./toolbox.ts"; +export { GadgetTool } from "./tool.ts"; +export * from "./chat/index.ts"; +export * from "./system/index.ts"; +export * from "./network/index.ts"; +export * from "./plan/index.ts"; +export * from "./project/index.ts"; diff --git a/gadget-drone/src/tools/network/fetch-url.ts b/packages/ai-toolbox/src/network/fetch-url.ts similarity index 93% rename from gadget-drone/src/tools/network/fetch-url.ts rename to packages/ai-toolbox/src/network/fetch-url.ts index 749062d..927cbb1 100644 --- a/gadget-drone/src/tools/network/fetch-url.ts +++ b/packages/ai-toolbox/src/network/fetch-url.ts @@ -1,12 +1,13 @@ +// src/network/fetch-url.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { asPositiveInteger, getCacheDir, toolError } from "../system/common.ts"; import { WebFetcher } from "./web-fetcher.ts"; -export class FetchUrlTool extends DroneTool { +export class FetchUrlTool extends GadgetTool { get name(): string { return "fetch_url"; } @@ -19,7 +20,7 @@ export class FetchUrlTool extends DroneTool { type: "function", function: { name: this.name, - description: "Fetch a URL, convert readable page content to line-numbered Markdown, cache it in the drone .gadget/cache directory, and return an optional line range like file_read.", + description: "Fetch a URL, convert readable page content to line-numbered Markdown, cache it in the .gadget/cache directory, and return an optional line range like file_read.", parameters: { type: "object", properties: { @@ -77,7 +78,7 @@ export class FetchUrlTool extends DroneTool { if (!cacheDir) { return toolError({ code: "OPERATION_NOT_ALLOWED", - message: "No drone cache directory is configured for fetch_url.", + message: "No cache directory is configured for fetch_url.", recoveryHint: "Run this tool during an active Agent work order after workspace initialization.", }); } diff --git a/gadget-drone/src/tools/network/index.ts b/packages/ai-toolbox/src/network/index.ts similarity index 90% rename from gadget-drone/src/tools/network/index.ts rename to packages/ai-toolbox/src/network/index.ts index d40fa31..6c92e5a 100644 --- a/gadget-drone/src/tools/network/index.ts +++ b/packages/ai-toolbox/src/network/index.ts @@ -1,3 +1,4 @@ +// src/network/index.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 diff --git a/gadget-drone/src/tools/network/search-google.ts b/packages/ai-toolbox/src/network/search-google.ts similarity index 98% rename from gadget-drone/src/tools/network/search-google.ts rename to packages/ai-toolbox/src/network/search-google.ts index bc54e78..079799b 100644 --- a/gadget-drone/src/tools/network/search-google.ts +++ b/packages/ai-toolbox/src/network/search-google.ts @@ -1,3 +1,4 @@ +// src/network/search-google.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -5,7 +6,7 @@ import { google } from "googleapis"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; import { formatError } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; export interface ISearchResult { title: string; @@ -26,7 +27,7 @@ export interface ISearchOptions { start?: number; } -export class GoogleSearchTool extends DroneTool { +export class GoogleSearchTool extends GadgetTool { get name(): string { return "search_google"; } @@ -140,7 +141,7 @@ export class GoogleSearchTool extends DroneTool { const engineId = this.toolbox.env.services?.google?.cse?.engineId; if (!apiKey || !engineId) { - throw new Error("Google CSE credentials not configured in drone environment"); + throw new Error("Google CSE credentials not configured in environment"); } const customSearch = google.customsearch({ diff --git a/gadget-drone/src/tools/network/web-fetcher.ts b/packages/ai-toolbox/src/network/web-fetcher.ts similarity index 97% rename from gadget-drone/src/tools/network/web-fetcher.ts rename to packages/ai-toolbox/src/network/web-fetcher.ts index ccb3a80..b3ac351 100644 --- a/gadget-drone/src/tools/network/web-fetcher.ts +++ b/packages/ai-toolbox/src/network/web-fetcher.ts @@ -1,3 +1,4 @@ +// src/network/web-fetcher.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -29,15 +30,8 @@ export class WebFetcher { hr: "---", }); this.turndown.remove([ - "script", - "style", - "noscript", - "nav", - "footer", - "header", - "button", - "input", - "form", + "script", "style", "noscript", "nav", "footer", "header", + "button", "input", "form", ]); } diff --git a/gadget-drone/src/tools/plan/common.ts b/packages/ai-toolbox/src/plan/common.ts similarity index 98% rename from gadget-drone/src/tools/plan/common.ts rename to packages/ai-toolbox/src/plan/common.ts index 7bcf689..fd0c9d5 100644 --- a/gadget-drone/src/tools/plan/common.ts +++ b/packages/ai-toolbox/src/plan/common.ts @@ -1,3 +1,4 @@ +// src/plan/common.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 diff --git a/gadget-drone/src/tools/plan/edit.ts b/packages/ai-toolbox/src/plan/edit.ts similarity index 98% rename from gadget-drone/src/tools/plan/edit.ts rename to packages/ai-toolbox/src/plan/edit.ts index c377e0b..b3a87c4 100644 --- a/gadget-drone/src/tools/plan/edit.ts +++ b/packages/ai-toolbox/src/plan/edit.ts @@ -1,14 +1,15 @@ +// src/plan/edit.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import fs from "node:fs/promises"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { toolError } from "../system/common.ts"; import { getGadgetDir, resolvePlanPath } from "./common.ts"; -export class PlanFileEditTool extends DroneTool { +export class PlanFileEditTool extends GadgetTool { get name(): string { return "plan_file_edit"; } diff --git a/gadget-drone/src/tools/plan/index.ts b/packages/ai-toolbox/src/plan/index.ts similarity index 93% rename from gadget-drone/src/tools/plan/index.ts rename to packages/ai-toolbox/src/plan/index.ts index e97b0a1..bdd3820 100644 --- a/gadget-drone/src/tools/plan/index.ts +++ b/packages/ai-toolbox/src/plan/index.ts @@ -1,3 +1,4 @@ +// src/plan/index.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 diff --git a/gadget-drone/src/tools/plan/list.ts b/packages/ai-toolbox/src/plan/list.ts similarity index 97% rename from gadget-drone/src/tools/plan/list.ts rename to packages/ai-toolbox/src/plan/list.ts index d8738b1..57d1543 100644 --- a/gadget-drone/src/tools/plan/list.ts +++ b/packages/ai-toolbox/src/plan/list.ts @@ -1,3 +1,4 @@ +// src/plan/list.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -5,11 +6,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { toolError } from "../system/common.ts"; import { getGadgetDir, resolvePlanPath } from "./common.ts"; -export class PlanListTool extends DroneTool { +export class PlanListTool extends GadgetTool { get name(): string { return "plan_list"; } diff --git a/gadget-drone/src/tools/plan/read.ts b/packages/ai-toolbox/src/plan/read.ts similarity index 97% rename from gadget-drone/src/tools/plan/read.ts rename to packages/ai-toolbox/src/plan/read.ts index 85259b6..e9a7a34 100644 --- a/gadget-drone/src/tools/plan/read.ts +++ b/packages/ai-toolbox/src/plan/read.ts @@ -1,10 +1,11 @@ +// src/plan/read.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import fs from "node:fs/promises"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { asPositiveInteger, formatNumberedLines, @@ -13,7 +14,7 @@ import { } from "../system/common.ts"; import { getGadgetDir, resolvePlanPath } from "./common.ts"; -export class PlanFileReadTool extends DroneTool { +export class PlanFileReadTool extends GadgetTool { get name(): string { return "plan_file_read"; } diff --git a/gadget-drone/src/tools/plan/write.ts b/packages/ai-toolbox/src/plan/write.ts similarity index 96% rename from gadget-drone/src/tools/plan/write.ts rename to packages/ai-toolbox/src/plan/write.ts index 5766b6f..f935c4c 100644 --- a/gadget-drone/src/tools/plan/write.ts +++ b/packages/ai-toolbox/src/plan/write.ts @@ -1,3 +1,4 @@ +// src/plan/write.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -5,11 +6,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { toolError } from "../system/common.ts"; import { getGadgetDir, resolvePlanPath } from "./common.ts"; -export class PlanFileWriteTool extends DroneTool { +export class PlanFileWriteTool extends GadgetTool { get name(): string { return "plan_file_write"; } diff --git a/gadget-drone/src/tools/project/index.ts b/packages/ai-toolbox/src/project/index.ts similarity index 89% rename from gadget-drone/src/tools/project/index.ts rename to packages/ai-toolbox/src/project/index.ts index d1ebb6b..39cccab 100644 --- a/gadget-drone/src/tools/project/index.ts +++ b/packages/ai-toolbox/src/project/index.ts @@ -1,3 +1,4 @@ +// src/project/index.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 diff --git a/gadget-drone/src/tools/project/list-skills.ts b/packages/ai-toolbox/src/project/list-skills.ts similarity index 93% rename from gadget-drone/src/tools/project/list-skills.ts rename to packages/ai-toolbox/src/project/list-skills.ts index 0fdb7d9..934d2a2 100644 --- a/gadget-drone/src/tools/project/list-skills.ts +++ b/packages/ai-toolbox/src/project/list-skills.ts @@ -1,10 +1,11 @@ +// src/project/list-skills.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import type { IAiLogger, IToolArguments } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; -export class ListSkillsTool extends DroneTool { +export class ListSkillsTool extends GadgetTool { get name(): string { return "list_skills"; } diff --git a/gadget-drone/src/tools/project/read-skill.ts b/packages/ai-toolbox/src/project/read-skill.ts similarity index 93% rename from gadget-drone/src/tools/project/read-skill.ts rename to packages/ai-toolbox/src/project/read-skill.ts index c6fb5ce..417b9e5 100644 --- a/gadget-drone/src/tools/project/read-skill.ts +++ b/packages/ai-toolbox/src/project/read-skill.ts @@ -1,10 +1,11 @@ +// src/project/read-skill.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import type { IAiLogger, IToolArguments } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; -export class ReadSkillTool extends DroneTool { +export class ReadSkillTool extends GadgetTool { get name(): string { return "read_skill"; } diff --git a/gadget-drone/src/tools/system/common.ts b/packages/ai-toolbox/src/system/common.ts similarity index 92% rename from gadget-drone/src/tools/system/common.ts rename to packages/ai-toolbox/src/system/common.ts index b5d7413..32811ba 100644 --- a/gadget-drone/src/tools/system/common.ts +++ b/packages/ai-toolbox/src/system/common.ts @@ -1,3 +1,4 @@ +// src/system/common.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -8,30 +9,10 @@ import { formatError, type IToolError } from "@gadget/ai"; import type { AiToolbox } from "../toolbox.ts"; export const BINARY_EXTENSIONS = new Set([ - ".o", - ".obj", - ".a", - ".lib", - ".so", - ".dll", - ".exe", - ".bin", - ".png", - ".jpg", - ".jpeg", - ".gif", - ".bmp", - ".ico", - ".webp", - ".pdf", - ".zip", - ".tar", - ".gz", - ".bz2", - ".7z", - ".wasm", - ".pyc", - ".class", + ".o", ".obj", ".a", ".lib", ".so", ".dll", ".exe", ".bin", + ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", + ".pdf", ".zip", ".tar", ".gz", ".bz2", ".7z", ".wasm", + ".pyc", ".class", ]); export interface ResolvedProjectPath { diff --git a/gadget-drone/src/tools/system/edit.ts b/packages/ai-toolbox/src/system/edit.ts similarity index 98% rename from gadget-drone/src/tools/system/edit.ts rename to packages/ai-toolbox/src/system/edit.ts index 7446a9d..d704c30 100644 --- a/gadget-drone/src/tools/system/edit.ts +++ b/packages/ai-toolbox/src/system/edit.ts @@ -1,13 +1,14 @@ +// src/system/edit.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import fs from "node:fs/promises"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { resolveProjectPath, toolError } from "./common.ts"; -export class FileEditTool extends DroneTool { +export class FileEditTool extends GadgetTool { get name(): string { return "file_edit"; } diff --git a/gadget-drone/src/tools/system/glob.ts b/packages/ai-toolbox/src/system/glob.ts similarity index 98% rename from gadget-drone/src/tools/system/glob.ts rename to packages/ai-toolbox/src/system/glob.ts index 5fc2c52..2059a9b 100644 --- a/gadget-drone/src/tools/system/glob.ts +++ b/packages/ai-toolbox/src/system/glob.ts @@ -1,3 +1,4 @@ +// src/system/glob.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -5,12 +6,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { getProjectRoot, resolveProjectPath, toolError } from "./common.ts"; const MAX_RESULTS = 1000; -export class GlobTool extends DroneTool { +export class GlobTool extends GadgetTool { get name(): string { return "glob"; } diff --git a/gadget-drone/src/tools/system/grep.ts b/packages/ai-toolbox/src/system/grep.ts similarity index 97% rename from gadget-drone/src/tools/system/grep.ts rename to packages/ai-toolbox/src/system/grep.ts index d71daeb..26d6ea4 100644 --- a/gadget-drone/src/tools/system/grep.ts +++ b/packages/ai-toolbox/src/system/grep.ts @@ -1,3 +1,4 @@ +// src/system/grep.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -5,17 +6,16 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { getProjectRoot, resolveProjectPath, toolError } from "./common.ts"; const MAX_MATCHES = 500; const MAX_FILE_SIZE = 1024 * 1024; -// TODO: Consider broadening this list or making it user-configurable // Currently limited to common source/doc extensions for performance. const SEARCH_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".txt"]); -export class GrepTool extends DroneTool { +export class GrepTool extends GadgetTool { get name(): string { return "grep"; } diff --git a/packages/ai-toolbox/src/system/index.ts b/packages/ai-toolbox/src/system/index.ts new file mode 100644 index 0000000..705d4fd --- /dev/null +++ b/packages/ai-toolbox/src/system/index.ts @@ -0,0 +1,11 @@ +// src/system/index.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +export { FileReadTool } from "./read.ts"; +export { FileWriteTool } from "./write.ts"; +export { FileEditTool } from "./edit.ts"; +export { ShellExecTool } from "./shell.ts"; +export { ListTool } from "./list.ts"; +export { GrepTool } from "./grep.ts"; +export { GlobTool } from "./glob.ts"; diff --git a/gadget-drone/src/tools/system/list.ts b/packages/ai-toolbox/src/system/list.ts similarity index 98% rename from gadget-drone/src/tools/system/list.ts rename to packages/ai-toolbox/src/system/list.ts index c09e940..a182b51 100644 --- a/gadget-drone/src/tools/system/list.ts +++ b/packages/ai-toolbox/src/system/list.ts @@ -1,3 +1,4 @@ +// src/system/list.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -5,12 +6,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { getProjectRoot, resolveProjectPath, toolError } from "./common.ts"; const MAX_ENTRIES = 1000; -export class ListTool extends DroneTool { +export class ListTool extends GadgetTool { get name(): string { return "list"; } diff --git a/gadget-drone/src/tools/system/read.ts b/packages/ai-toolbox/src/system/read.ts similarity index 97% rename from gadget-drone/src/tools/system/read.ts rename to packages/ai-toolbox/src/system/read.ts index b51ef00..691f75d 100644 --- a/gadget-drone/src/tools/system/read.ts +++ b/packages/ai-toolbox/src/system/read.ts @@ -1,10 +1,11 @@ +// src/system/read.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import fs from "node:fs/promises"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { asPositiveInteger, formatNumberedLines, @@ -13,7 +14,7 @@ import { toolError, } from "./common.ts"; -export class FileReadTool extends DroneTool { +export class FileReadTool extends GadgetTool { get name(): string { return "file_read"; } diff --git a/gadget-drone/src/tools/system/shell.ts b/packages/ai-toolbox/src/system/shell.ts similarity index 97% rename from gadget-drone/src/tools/system/shell.ts rename to packages/ai-toolbox/src/system/shell.ts index 20f6fae..15fe193 100644 --- a/gadget-drone/src/tools/system/shell.ts +++ b/packages/ai-toolbox/src/system/shell.ts @@ -1,3 +1,4 @@ +// src/system/shell.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -6,14 +7,14 @@ import path from "node:path"; import { promisify } from "node:util"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { getProjectRoot, resolveProjectPath, toolError } from "./common.ts"; const execAsync = promisify(exec); const DEFAULT_TIMEOUT = 30_000; const MAX_TIMEOUT = 120_000; -export class ShellExecTool extends DroneTool { +export class ShellExecTool extends GadgetTool { get name(): string { return "shell_exec"; } diff --git a/gadget-drone/src/tools/system/write.ts b/packages/ai-toolbox/src/system/write.ts similarity index 96% rename from gadget-drone/src/tools/system/write.ts rename to packages/ai-toolbox/src/system/write.ts index 55c77f6..a879601 100644 --- a/gadget-drone/src/tools/system/write.ts +++ b/packages/ai-toolbox/src/system/write.ts @@ -1,3 +1,4 @@ +// src/system/write.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -5,10 +6,10 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; -import { DroneTool } from "../tool.ts"; +import { GadgetTool } from "../tool.ts"; import { resolveProjectPath, toolError } from "./common.ts"; -export class FileWriteTool extends DroneTool { +export class FileWriteTool extends GadgetTool { get name(): string { return "file_write"; } diff --git a/gadget-drone/src/tools/tool.ts b/packages/ai-toolbox/src/tool.ts similarity index 89% rename from gadget-drone/src/tools/tool.ts rename to packages/ai-toolbox/src/tool.ts index fb8bf2e..48feea7 100644 --- a/gadget-drone/src/tools/tool.ts +++ b/packages/ai-toolbox/src/tool.ts @@ -1,3 +1,4 @@ +// src/tool.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 @@ -9,7 +10,7 @@ import type { } from "@gadget/ai"; import type { AiToolbox } from "./toolbox.ts"; -export abstract class DroneTool implements IAiTool { +export abstract class GadgetTool implements IAiTool { protected _toolbox: AiToolbox; constructor(toolbox: AiToolbox) { diff --git a/gadget-drone/src/tools/toolbox.ts b/packages/ai-toolbox/src/toolbox.ts similarity index 86% rename from gadget-drone/src/tools/toolbox.ts rename to packages/ai-toolbox/src/toolbox.ts index a1415e1..281b4d2 100644 --- a/gadget-drone/src/tools/toolbox.ts +++ b/packages/ai-toolbox/src/toolbox.ts @@ -1,10 +1,11 @@ +// src/toolbox.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import type { IAiTool } from "@gadget/ai"; import type { IProject, ChatSessionMode } from "@gadget/api"; -export interface DroneToolboxEnvironment { +export interface GadgetToolboxEnvironment { NODE_ENV: string; services?: { google?: { @@ -27,19 +28,19 @@ export type ToolMap = Map; export type ToolSet = Set; export class AiToolbox { - private _env: DroneToolboxEnvironment; + private _env: GadgetToolboxEnvironment; private tools: ToolMap = new Map(); private modeSets: Map = new Map>(); - constructor(env: DroneToolboxEnvironment) { + constructor(env: GadgetToolboxEnvironment) { this._env = env; } - get env(): DroneToolboxEnvironment { + get env(): GadgetToolboxEnvironment { return this._env; } - updateWorkspace(workspace: NonNullable): void { + updateWorkspace(workspace: NonNullable): void { this._env.workspace = workspace; } diff --git a/packages/ai-toolbox/tsconfig.json b/packages/ai-toolbox/tsconfig.json new file mode 100644 index 0000000..a787d6f --- /dev/null +++ b/packages/ai-toolbox/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "typeRoots": ["node_modules/@types"], + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/plans/continuation-prompt-user-mode.md b/plans/continuation-prompt-user-mode.md deleted file mode 100644 index c7e4826..0000000 --- a/plans/continuation-prompt-user-mode.md +++ /dev/null @@ -1,180 +0,0 @@ -# User Mode MVP — Continuation Prompt - -## Session Context - -**Session Date:** May 13, 2026 -**Session ID:** 5hQhhRHYdL_e10Zw7EDLv -**Branch:** `feature/user-mode` (latest: `7dca4b5`) -**Status:** MVP Complete ✅ - ---- - -## What Was Accomplished - -### 1. ACE Editor Integration Crisis → CodeMirror Migration - -**Initial Problem:** The previous agent's ACE editor integration was crashing the entire ChatSessionView. After extensive debugging, we discovered: - -- `react-ace` v14 ships **CommonJS-only** (`"main": "lib/index.js"`, no `"module"` or `"exports"` field) -- Vite's ESM-first dev server cannot properly resolve CJS default exports -- Every workaround failed: CJS interop hacks (`.default || module`), `?url` imports, `setModuleUrl()`, `optimizeDeps.include` -- This is a **fundamental architecture mismatch**, not a configuration issue - -**Solution:** Migrated to `@uiw/react-codemirror` v4.25.9 - -- Ships dual ESM+CJS with proper `exports` map -- Works with Vite out of the box (zero hacks) -- React 19 compatible (`>=17.0.0` peer dep) -- ~124KB gzipped (vs Monaco's ~5MB) - -**Files Changed:** -- `frontend/package.json` — removed `ace-builds`, `react-ace`; added `@uiw/react-codemirror` + 16 `@codemirror/lang-*` packages + theme -- `frontend/src/components/EditorPanel.tsx` — deleted 108 lines of ACE boilerplate, replaced with ~30 lines of clean CodeMirror setup -- `frontend/src/types/vite.d.ts` — **deleted** (only needed for ACE `?url` import types) -- `frontend/vite.config.ts` — removed `optimizeDeps.include` for ACE - -### 2. Editor Height Constraint Fix - -**Problem:** The CodeMirror editor was overflowing its container, extending off the bottom of the screen, and not scrolling properly. - -**Root Cause:** `@uiw/react-codemirror` renders a wrapper `
` between `.cm-editor-container` and `.cm-editor` that doesn't inherit flex height constraints by default. - -**Solution:** Added CSS rules that explicitly constrain **every level** of the CodeMirror DOM tree: - -```css -.cm-editor-container { - overflow: hidden; -} -.cm-editor-container > div { /* The wrapper @uiw/react-codemirror renders */ - flex: 1; - min-height: 0; - overflow: hidden; -} -.cm-editor-container .cm-editor { - flex: 1; - min-height: 0; - overflow: hidden; -} -.cm-editor-container .cm-scroller { - min-height: 0; - overflow: auto; /* ← Only this scrolls */ -} -``` - -**Files Changed:** -- `frontend/src/index.css` — added comprehensive flex layout constraints for CodeMirror - -### 3. Error Boundary Protection - -- Added `ErrorBoundary` component wrapping `` in `ChatSessionView` -- Catches render errors and displays fallback UI instead of crashing the entire app - -### 4. Heartbeat Worker Verification - -- Verified the IDE→gadget-drone heartbeat worker (`src/workers/heartbeat.worker.ts`) remains completely intact -- No changes to heartbeat logic, socket.ts, or session management -- Production build includes the heartbeat worker (inlined as base64 data URL) - ---- - -## Key Technical Learnings - -### CJS/ESM Interop with Vite - -1. **Vite is ESM-first** — CJS modules are pre-bundle-converted, but default exports from CJS (`exports.default = value`) get wrapped as `{ default: value, __esModule: true }` -2. **Named exports work fine** — the issue is specifically with `export default` from CJS packages -3. **Rolldown (Vite 8's bundler) handles this better than Rollup**, but the underlying CJS interop issue remains for packages without proper ESM entry points -4. **Best practice:** Always prefer packages with `"module"` or `"exports"` fields pointing to ESM builds when using Vite - -### Flex Layout Height Constraints - -1. **Every level in the flex chain must have `min-height: 0`** — without this, flex items can grow beyond their allocated space -2. **`flex: 1` alone is not enough** — need `min-height: 0` or `overflow: hidden` to prevent overflow -3. **Third-party components often break flex layouts** — they render wrapper divs that don't inherit parent constraints -4. **The fix pattern:** Use CSS to explicitly target every rendered level and apply `flex: 1`, `min-height: 0`, `overflow: hidden` appropriately - ---- - -## Remaining Steps / Next Session - -### High Priority - -1. **Test all supported file types** — verify syntax highlighting works for: - - JavaScript/JSX, TypeScript/TSX, Python, JSON, HTML, CSS, Less, YAML, Markdown, SQL, Java, Go, Rust, C/C++, C#, PHP, XML - - Unsupported types (Ruby, Sass, Dockerfile, Makefile, Shell) should fall back to plain text - -2. **Verify read-only mode** — confirm Agent mode properly prevents editing (we tested this, but more thorough testing recommended) - -3. **Test file save/load cycle** — ensure Ctrl+S works, dirty state tracking is correct, and success/error states display properly - -### Medium Priority - -4. **Add missing language support** (optional, if needed): - - Ruby: `@codemirror/legacy-modes/mode/ruby` (legacy mode, not as good as CM6 native) - - Sass/SCSS: `@codemirror/lang-sass` (if available) - - Shell/Dockerfile/Makefile: Plain text is probably fine for now - -5. **Autocompletion** — CodeMirror 6 supports autocompletion via `@codemirror/autocomplete` and language-specific completion extensions. Could add basic keyword completion for supported languages. - -6. **Theme customization** — The `tomorrow-night-blue` theme is close to ACE's "tomorrow" but not identical. Could customize colors to match the rest of the UI better. - -### Low Priority - -7. **Performance optimization** — For very large files (>10k lines), consider: - - CodeMirror's built-in line wrapping limits - - Virtual scrolling (already built into CM6) - - Lazy-loading language extensions - -8. **Editor preferences** — Allow users to configure: - - Font size - - Tab size - - Word wrap - - Minimap (CodeMirror has a minimap extension) - ---- - -## Continuation Prompt for Next Session - -``` -You are continuing work on the Gadget Code project's User Mode MVP. The ACE editor has been successfully migrated to @uiw/react-codemirror, and the editor now properly fits within its flex container and scrolls correctly. - -Current state: -- Branch: `feature/user-mode` (latest commit: check `git log --oneline -1`) -- Editor: @uiw/react-codemirror v4.25 with tomorrow-night-blue theme -- Supported languages: JavaScript/JSX, TypeScript/TSX, Python, JSON, HTML, CSS, Less, YAML, Markdown, SQL, Java, Go, Rust, C/C++, C#, PHP, XML -- Editor properly constrained in flex layout, scrolls internally -- ErrorBoundary wraps EditorPanel -- Heartbeat worker intact - -Your task: [INSERT TASK HERE] - -Key files: -- `gadget-code/frontend/src/components/EditorPanel.tsx` — Editor component -- `gadget-code/frontend/src/index.css` — Flex layout constraints for CodeMirror -- `gadget-code/frontend/src/pages/ChatSessionView.tsx` — Parent view with ErrorBoundary -- `gadget-code/frontend/package.json` — Dependencies - -Important patterns: -- Use `getLanguageExtensions()` to map file extensions to CodeMirror language extensions -- Flex layout chain: #root → main → ChatSessionView → EditorPanel → .cm-editor-container → wrapper div → .cm-editor → .cm-scroller (only this scrolls) -- All language extensions imported from `@codemirror/lang-*` packages -- Theme: `tomorrowNightBlue` from `@uiw/codemirror-theme-tomorrow-night-blue` -``` - ---- - -## Success Criteria for User Mode (MVP) - -- ✅ User can open files in the editor -- ✅ User can edit files in User mode -- ✅ User can save files (Ctrl+S or button) -- ✅ Agent mode makes editor read-only -- ✅ Editor stays within its allocated space -- ✅ Editor scrolls properly when content overflows -- ✅ Syntax highlighting works for supported languages -- ✅ ErrorBoundary catches editor crashes -- ✅ Heartbeat worker continues functioning (no regression) - -**MVP Status: COMPLETE** ✅ - -All success criteria met. The User Mode MVP is ready for production testing. diff --git a/plans/session-summary-2026-05-13.md b/plans/session-summary-2026-05-13.md deleted file mode 100644 index c744a90..0000000 --- a/plans/session-summary-2026-05-13.md +++ /dev/null @@ -1,182 +0,0 @@ -# Session Summary: User Mode Editor Integration -**Date:** May 13, 2026 -**Session:** Fix ACE Editor Integration → Migrate to CodeMirror → User Mode MVP Complete - ---- - -## What We Accomplished - -### 1. ACE Editor Integration Failed (Root Cause Analysis) - -**Attempted:** Integrate `react-ace` v14.0.1 with Vite + React 19 - -**Problem:** `react-ace` v14 ships **CommonJS-only** (`"main": "lib/index.js"`, no `"module"` or `"exports"` field). Vite's ESM-first dev server cannot properly resolve its default export, causing "Element type is invalid: got object" on every render. - -**Workarounds attempted (all failed):** -- CJS interop hack: `import * as ReactAceModule; const Ace = ReactAceModule.default || ReactAceModule` -- `optimizeDeps.include: ['react-ace', 'ace-builds']` in vite.config.ts -- 56 lines of `?url` imports + `ace.config.setModuleUrl()` registration -- Namespace imports with fallback extraction - -**Conclusion:** This is a **fundamental architecture mismatch**, not a configuration issue. `react-ace` is incompatible with Vite's ESM-first model. Every workaround is fragile and breaks on Vite updates. - ---- - -### 2. Migrated to @uiw/react-codemirror (Success) - -**Decision:** Switch to `@uiw/react-codemirror` v4.25.9 — dual ESM+CJS package with proper `exports` map, React 19 support (`>=17.0.0` peer dep), works with Vite out of the box. - -**Changes made:** -- Removed `ace-builds`, `react-ace` from `frontend/package.json` -- Added `@uiw/react-codemirror` + 16 `@codemirror/lang-*` packages + `@uiw/codemirror-theme-tomorrow-night-blue` -- Added `@replit/codemirror-lang-csharp` for C# support -- Rewrote `EditorPanel.tsx`: deleted 108 lines of ACE boilerplate, replaced with ~30 lines of clean CodeMirror setup -- Deleted `frontend/src/types/vite.d.ts` (only needed for ACE `?url` imports) -- Removed `optimizeDeps.include` from `vite.config.ts` (not needed for CM) -- Added CodeMirror flex layout CSS to `index.css` - -**Supported languages:** JavaScript/JSX, TypeScript/TSX, Python, JSON, HTML, CSS, Less, YAML, Markdown, SQL, Java, Go, Rust, C/C++, C#, PHP, XML. Unsupported types fall back to plain text. - -**Bundle size:** ~124KB gzipped (CodeMirror 6 core) vs ~56KB for ACE, but ~40x smaller than Monaco's ~5MB. - ---- - -### 3. Fixed Flex Layout Height Constraint Issue - -**Problem:** Editor overflowed its container and didn't stay in allocated area. - -**Root cause:** `@uiw/react-codemirror` renders a wrapper `
` between `.cm-editor-container` and `.cm-editor` that doesn't inherit flex constraints by default. - -**Solution:** Added CSS rules that constrain **every level** of the CodeMirror DOM tree: - -```css -.cm-editor-container { - overflow: hidden; -} -.cm-editor-container > div { /* The wrapper @uiw/react-codemirror renders */ - flex: 1; - min-height: 0; - overflow: hidden; -} -.cm-editor-container .cm-editor { - flex: 1; - min-height: 0; - overflow: hidden; -} -.cm-editor-container .cm-scroller { - min-height: 0; - overflow: auto; /* ← Only this scrolls */ -} -``` - -**Layout chain:** -``` -.cm-editor-container (flex-1, min-h-0) - └─ wrapper div (flex:1, min-h-0, overflow:hidden) - └─ .cm-editor (flex:1, min-h-0, overflow:hidden) - └─ .cm-scroller (min-h-0, overflow:auto) ← only this scrolls -``` - ---- - -## Key Technical Learnings - -### 1. CJS/ESM Interop in Vite is Fragile - -When a package ships CJS-only with `export default`, Vite's dev server pre-bundles it as a namespace object where the default export is nested under `.default`. This causes "Element type is invalid: got object" errors when importing default exports from CJS-only React components. - -**Rule:** For React components in Vite projects, prefer packages that ship dual ESM+CJS with proper `exports` maps. Avoid CJS-only packages — every workaround is fragile. - -### 2. Flex Layout Height Constraints Must Be Explicit at Every Level - -For a flex item to properly fill its allocated space: -1. Every ancestor in the flex chain must have `flex: 1` or explicit height -2. Every ancestor must have `min-height: 0` (or `min-width: 0` for horizontal flex) -3. For third-party components that render wrapper divs, add explicit CSS rules targeting those wrappers -4. Only the innermost scrollable element should have `overflow: auto`; all ancestors should have `overflow: hidden` - -**Pattern:** -```css -.container { - flex: 1; - min-height: 0; - overflow: hidden; -} -.container > div { /* Wrapper div from third-party component */ - flex: 1; - min-height: 0; - overflow: hidden; -} -.container .scrollable { - min-height: 0; - overflow: auto; /* Only this scrolls */ -} -``` - -### 3. Heartbeat Worker Unaffected - -The IDE heartbeat worker (`src/workers/heartbeat.worker.ts`) was completely untouched by the editor migration. It continues to run in a Web Worker to avoid browser tab throttling, sending session heartbeats every 19 seconds to keep the drone connection alive. - ---- - -## Files Changed - -| File | Change | -|------|--------| -| `frontend/package.json` | Replaced `ace-builds`, `react-ace` with `@uiw/react-codemirror` + language packages | -| `frontend/src/components/EditorPanel.tsx` | Complete rewrite: ACE → CodeMirror | -| `frontend/src/index.css` | Added CodeMirror flex layout constraints | -| `frontend/src/types/vite.d.ts` | **Deleted** — only needed for ACE `?url` imports | -| `frontend/vite.config.ts` | Removed `optimizeDeps.include` for ACE | -| `pnpm-lock.yaml` | Updated with all new CodeMirror packages | - ---- - -## Remaining Steps / Next Session - -### High Priority -- [ ] **Test all supported file types** — Verify syntax highlighting works for each language package -- [ ] **Test read-only behavior** — Confirm Agent mode prevents editing, User mode allows it -- [ ] **Test Ctrl+S save** — Verify keyboard shortcut works in User mode -- [ ] **Test file switching** — Open multiple files, ensure language detection works - -### Medium Priority -- [ ] **Add SCSS/Sass support** — Currently falls back to CSS mode; could add `@codemirror/lang-sass` if needed -- [ ] **Add Ruby support** — No official CM6 package; could use `@codemirror/legacy-modes` if needed -- [ ] **Add Dockerfile/Makefile support** — Currently plain text; could add custom language defs if needed - -### Low Priority -- [ ] **Autocompletion** — Currently disabled; could add `@codemirror/autocomplete` + language-specific completions -- [ ] **Linting** — Could add `@codemirror/lint` for real-time error annotations -- [ ] **Find/Replace** — Could add `@codemirror/search` for Ctrl+F support - ---- - -## Continuation Prompt (if needed) - -> **Context:** User Mode MVP is complete. The CodeMirror editor integration is working correctly with proper flex layout constraints. The editor supports 16 languages, respects read-only mode in Agent mode, allows editing in User mode, and saves with Ctrl+S. -> -> **Next steps:** Test edge cases (large files, rapid switching, special characters), add missing language support if needed (Ruby, SCSS, Dockerfile), and optionally enhance with autocompletion/linting/search features. - ---- - -## Team Notes - -**What worked well:** -- Research-driven approach: We did proper homework on CJS/ESM interop issues before deciding to migrate -- Clean migration: The CodeMirror rewrite was done in one clean pass with proper TypeScript types -- Proper flex layout fix: Instead of a hack, we added CSS that correctly propagates height constraints through the entire DOM tree - -**What to avoid in future:** -- Don't use CJS-only React components in Vite projects — the interop is fragile -- Don't add third-party components without testing their flex layout behavior first -- Don't skip the research phase — knowing the root cause saved us hours of debugging - -**Shoutouts:** -- The heartbeat worker architecture (Web Worker + fallback setInterval) is rock-solid -- The ErrorBoundary we added for ACE is still in place and working for CodeMirror -- The file loading/saving socket API abstraction made the editor swap trivial — same interface, different implementation - ---- - -**Status:** ✅ User Mode MVP complete. Editor is production-ready. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c3a73e..9f27360 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,6 +366,9 @@ importers: '@gadget/ai': specifier: workspace:* version: link:../packages/ai + '@gadget/ai-toolbox': + specifier: workspace:* + version: link:../packages/ai-toolbox '@gadget/api': specifier: workspace:* version: link:../packages/api @@ -471,6 +474,43 @@ importers: specifier: ^4.1.5 version: 4.1.5(@types/node@25.6.0)(jsdom@29.1.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(tsx@4.21.0)) + packages/ai-toolbox: + dependencies: + '@gadget/ai': + specifier: workspace:* + version: link:../ai + '@gadget/api': + specifier: workspace:* + version: link:../api + '@mozilla/readability': + specifier: 0.6.0 + version: 0.6.0 + googleapis: + specifier: 171.4.0 + version: 171.4.0 + jsdom: + specifier: 29.0.2 + version: 29.0.2 + playwright: + specifier: 1.59.1 + version: 1.59.1 + turndown: + specifier: 7.2.2 + version: 7.2.2 + devDependencies: + '@types/jsdom': + specifier: 27.0.0 + version: 27.0.0 + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + '@types/turndown': + specifier: 5.0.5 + version: 5.0.5 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + packages/api: dependencies: ansicolor: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9755bdd..4ffa30d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - 'gadget-code' - 'gadget-code/frontend' - 'gadget-drone' + - 'gadget-tasks' allowBuilds: esbuild: true msgpackr-extract: true