diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c79ab7f5..d660073e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,8 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - name: Install all dependencies (parallel) if: steps.cache.outputs.cache-hit != 'true' @@ -35,6 +36,7 @@ jobs: cd apps/desktop && npm ci & cd apps/ade-cli && npm ci & cd apps/web && npm ci & + cd apps/ade-code && npm ci & wait # ── Secret scanning (no deps needed) ─────────────────────────────────── @@ -63,7 +65,8 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: cd apps/desktop && npm run typecheck typecheck-ade-cli: @@ -80,7 +83,8 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: cd apps/ade-cli && npm run typecheck typecheck-web: @@ -97,9 +101,28 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: cd apps/web && npm run typecheck + typecheck-ade-code: + needs: install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: actions/cache/restore@v4 + with: + path: | + apps/desktop/node_modules + apps/ade-cli/node_modules + apps/web/node_modules + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} + - run: cd apps/ade-code && npm run typecheck + lint-desktop: needs: install runs-on: ubuntu-latest @@ -114,7 +137,8 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: cd apps/desktop && npm run lint test-desktop: @@ -135,7 +159,8 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: cd apps/desktop && npx vitest run --shard=${{ matrix.shard }}/8 test-ade-cli: @@ -152,9 +177,28 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: cd apps/ade-cli && npm test + test-ade-code: + needs: install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: actions/cache/restore@v4 + with: + path: | + apps/desktop/node_modules + apps/ade-cli/node_modules + apps/web/node_modules + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} + - run: cd apps/ade-code && npm test + build: needs: install runs-on: ubuntu-latest @@ -169,10 +213,12 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: cd apps/desktop && npm run build - run: cd apps/ade-cli && npm run build - run: cd apps/web && npm run build + - run: cd apps/ade-code && npm run build validate-docs: needs: install @@ -188,7 +234,8 @@ jobs: apps/desktop/node_modules apps/ade-cli/node_modules apps/web/node_modules - key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json') }} + apps/ade-code/node_modules + key: nm-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/ade-code/package-lock.json') }} - run: node scripts/validate-docs.mjs # ── Windows build smoke (self-contained — no shared cache) ──────────── @@ -235,9 +282,11 @@ jobs: - typecheck-desktop - typecheck-ade-cli - typecheck-web + - typecheck-ade-code - lint-desktop - test-desktop - test-ade-cli + - test-ade-code - build - validate-docs - build-win diff --git a/.gitignore b/.gitignore index 007cc49a..988ba157 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ __pycache__/ # Build outputs /apps/ade-cli/dist/ +/apps/ade-code/dist/ /apps/desktop/release/ /apps/desktop/dist/ /apps/desktop/vendor/crsqlite/darwin-x64/ @@ -62,3 +63,4 @@ ios-signing/ /.playwright-mcp /.codex-derived-data package-lock.json +!/apps/ade-code/package-lock.json diff --git a/apps/ade-cli/README.md b/apps/ade-cli/README.md index a5f5569f..ca9d1e76 100644 --- a/apps/ade-cli/README.md +++ b/apps/ade-cli/README.md @@ -70,6 +70,8 @@ ade shell start --lane lane-id -- npm test ade shell start-cli codex --lane lane-id --permission-mode edit --message "fix failing tests" ade shell start-cli --provider claude --lane lane-id --permission-mode default ade chat create --lane lane-id --model gpt-5.5 +ade code +ade --socket /path/to/ade.sock code ade tests run --lane lane-id --suite unit --wait ade proof list --arg ownerKind=chat --arg ownerId=session-id ade help ios-sim preview-render @@ -95,6 +97,8 @@ ade cursor cloud me Use typed commands first. They validate common arguments and provide stable JSON fields or readable text summaries. Use `ade help ` for exact flags, `ade actions list --text` to discover the full service-backed action catalog, and `ade actions run ` only when there is no typed command for the workflow yet. +**`ade code`** starts the terminal Work chat client (`apps/ade-code`). Build it with `npm run build` inside that directory, install the `ade-code` package, or point **`ADE_CODE_EXECUTABLE`** at `dist/cli.js`. Unlike other commands that auto-pick the desktop socket from the project layout during `executePlan`, **`ade code` only forwards `--socket` when you pass global `--socket` to `ade`** (for example `ade --socket /path/to/ade.sock code`). Without that, the TUI runs in **embedded** headless mode instead of opening a socket implicitly. + The `prs path-to-merge` and `prs pipeline save` commands persist a partial `PipelineSettings` patch via `issue_inventory.savePipelineSettings` before launching the resolver. The Path to Merge orchestrator reads these from saved settings, so the same flags work either way: | Flag | PipelineSettings field | Values | diff --git a/apps/ade-cli/src/adeRpcServer.test.ts b/apps/ade-cli/src/adeRpcServer.test.ts index a3cddf02..a11ea38a 100644 --- a/apps/ade-cli/src/adeRpcServer.test.ts +++ b/apps/ade-cli/src/adeRpcServer.test.ts @@ -1100,6 +1100,75 @@ function createFakePathExecutable(dir: string, name: string): string { } describe("adeRpcServer", () => { + it("routes app/navigate through the runtime navigation service", async () => { + const { runtime } = createRuntime(); + const navigate = vi.fn(async () => ({ ok: true, mode: "desktop", windowId: 7 })); + runtime.appNavigationService = { navigate }; + const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" }); + await initialize(handler, { role: "cto" }); + + const result = await handler({ + jsonrpc: "2.0", + id: 2, + method: "app/navigate", + params: { + source: "ade-code", + target: { kind: "lane", sessionId: "chat-1", laneId: "lane-1" }, + }, + }); + + expect(result).toEqual({ ok: true, mode: "desktop", windowId: 7 }); + expect(navigate).toHaveBeenCalledWith({ + source: "ade-code", + target: { kind: "lane", sessionId: "chat-1", laneId: "lane-1" }, + }); + }); + + it("reports app/navigate unavailable in headless runtime", async () => { + const { runtime } = createRuntime(); + const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" }); + await initialize(handler, { role: "cto" }); + + const result = await handler({ + jsonrpc: "2.0", + id: 2, + method: "app/navigate", + params: { + source: "ade-code", + target: { kind: "work" }, + }, + }); + + expect(result).toEqual({ + ok: false, + mode: "unavailable", + message: "Desktop navigation is unavailable in this runtime.", + }); + }); + + it("rejects malformed app/navigate targets before calling the runtime service", async () => { + const { runtime } = createRuntime(); + const navigate = vi.fn(async () => ({ ok: true, mode: "desktop", windowId: 7 })); + runtime.appNavigationService = { navigate }; + const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" }); + await initialize(handler, { role: "cto" }); + + await expect(handler({ + jsonrpc: "2.0", + id: 2, + method: "app/navigate", + params: { + source: "ade-code", + target: { kind: "lane" }, + }, + })).rejects.toMatchObject({ + code: JsonRpcErrorCode.invalidParams, + message: "app/navigate target 'lane' requires laneId.", + }); + + expect(navigate).not.toHaveBeenCalled(); + }); + it("treats requested privileged roles as external without trusted env identity", async () => { const { runtime } = createRuntime(); const handler = createAdeRpcRequestHandler({ runtime, serverVersion: "test" }); diff --git a/apps/ade-cli/src/adeRpcServer.ts b/apps/ade-cli/src/adeRpcServer.ts index 0edbb899..33558f43 100644 --- a/apps/ade-cli/src/adeRpcServer.ts +++ b/apps/ade-cli/src/adeRpcServer.ts @@ -39,6 +39,7 @@ import { type DockLayout, type GraphPersistedState, type MergeMethod, + type AppNavigationRequest, } from "../../desktop/src/shared/types"; import type { PrActionRun, PrCheck, PrComment, PrReviewThread } from "../../desktop/src/shared/types/prs"; import { resolveAdeLayout } from "../../desktop/src/shared/adeLayout"; @@ -7344,6 +7345,48 @@ export function createAdeRpcRequestHandler(args: { return await readResource(runtime, uri); } + if (method === "app/navigate") { + const target = safeObject(params.target); + const kind = asOptionalTrimmedString(target.kind); + if (!kind) { + throw new JsonRpcError(JsonRpcErrorCode.invalidParams, "app/navigate requires target.kind."); + } + if (kind !== "work" && kind !== "chat" && kind !== "lane" && kind !== "pr" && kind !== "route") { + throw new JsonRpcError(JsonRpcErrorCode.invalidParams, `Unsupported app navigation target kind: ${kind}.`); + } + if (kind === "lane" && !asOptionalTrimmedString(target.laneId)) { + throw new JsonRpcError(JsonRpcErrorCode.invalidParams, "app/navigate target 'lane' requires laneId."); + } + if (kind === "route" && !asOptionalTrimmedString(target.route)) { + throw new JsonRpcError(JsonRpcErrorCode.invalidParams, "app/navigate target 'route' requires route."); + } + const normalizedTarget: Record = { kind }; + const sessionId = asOptionalTrimmedString(target.sessionId); + const laneId = asOptionalTrimmedString(target.laneId); + if ((kind === "work" || kind === "chat" || kind === "lane") && sessionId) normalizedTarget.sessionId = sessionId; + if ((kind === "work" || kind === "chat" || kind === "lane" || kind === "pr") && laneId) normalizedTarget.laneId = laneId; + if (kind === "pr") { + const prId = asOptionalTrimmedString(target.prId); + if (prId) normalizedTarget.prId = prId; + if (typeof target.prNumber === "number") normalizedTarget.prNumber = target.prNumber; + } + if (kind === "route") { + normalizedTarget.route = asOptionalTrimmedString(target.route); + } + const request = { + target: normalizedTarget, + source: asOptionalTrimmedString(params.source) ?? "ade-rpc", + } as AppNavigationRequest; + if (!runtime.appNavigationService) { + return { + ok: false, + mode: "unavailable", + message: "Desktop navigation is unavailable in this runtime.", + }; + } + return await runtime.appNavigationService.navigate(request); + } + if (method === "shutdown") { return {}; } diff --git a/apps/ade-cli/src/bootstrap.ts b/apps/ade-cli/src/bootstrap.ts index 0184df25..67292c94 100644 --- a/apps/ade-cli/src/bootstrap.ts +++ b/apps/ade-cli/src/bootstrap.ts @@ -33,7 +33,7 @@ import type { createRebaseSuggestionService } from "../../desktop/src/main/servi import type { createAutoRebaseService } from "../../desktop/src/main/services/lanes/autoRebaseService"; import { createProcessService } from "../../desktop/src/main/services/processes/processService"; import { augmentProcessPathWithShellAndKnownCliDirs, setPathEnvValue } from "../../desktop/src/main/services/ai/cliExecutableResolver"; -import type { createAgentChatService } from "../../desktop/src/main/services/chat/agentChatService"; +import { createAgentChatService } from "../../desktop/src/main/services/chat/agentChatService"; import type { createPrService } from "../../desktop/src/main/services/prs/prService"; import type { createPrSummaryService } from "../../desktop/src/main/services/prs/prSummaryService"; import type { createQueueLandingService } from "../../desktop/src/main/services/prs/queueLandingService"; @@ -81,6 +81,7 @@ import { import { createMacosVmService } from "../../desktop/src/main/services/macosVm/macosVmService"; import type { BuiltInBrowserService } from "../../desktop/src/main/services/builtInBrowser/builtInBrowserService"; import type { createFileService } from "../../desktop/src/main/services/files/fileService"; +import type { AppNavigationRequest, AppNavigationResult } from "../../desktop/src/shared/types"; import { createAutomationService, type AutomationAdeActionRegistry, @@ -191,6 +192,9 @@ export type AdeRuntime = { budgetCapService?: ReturnType | null; sessionDeltaService?: ReturnType | null; autoUpdateService?: ReturnType | null; + appNavigationService?: { + navigate(args: AppNavigationRequest): Promise; + } | null; eventBuffer: EventBuffer; dispose: () => void; }; @@ -300,12 +304,18 @@ function createHeadlessAdeCliAgentEnv(baseEnv: NodeJS.ProcessEnv = process.env): return next; } -export async function createAdeRuntime(args: { projectRoot: string; workspaceRoot?: string } | string): Promise { +export async function createAdeRuntime(args: { + projectRoot: string; + workspaceRoot?: string; + chatRuntime?: "headless-stub" | "agent"; + runtimeProfile?: "full" | "chat"; +} | string): Promise { const resolvedArgs = typeof args === "string" ? { projectRoot: args, workspaceRoot: args } : args; const projectRoot = path.resolve(resolvedArgs.projectRoot); const workspaceRoot = path.resolve(resolvedArgs.workspaceRoot ?? resolvedArgs.projectRoot); + const chatOnlyRuntime = resolvedArgs.runtimeProfile === "chat"; if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) { throw new Error(`Project root does not exist: ${projectRoot}`); } @@ -570,53 +580,59 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo orchestratorService, logger, }); - const iosSimulatorService = createIosSimulatorService({ - projectRoot, - logger, - }); + const iosSimulatorService = chatOnlyRuntime + ? null + : createIosSimulatorService({ + projectRoot, + logger, + }); // Late-bound chat session lookup. agentChatService is created after // appControlService below, so we capture a holder that the resolveLaneId // closure reads at call time. The chat session store lives in agentChatService // (getSessionSummary), not in sessionService (which holds terminal sessions). const agentChatServiceHolder: { current: ReturnType | null } = { current: null }; - const appControlService = createAppControlService({ - projectRoot, - logger, - ptyService, - resolveLaneId: async ({ cwd, projectRoot: requestedProjectRoot, laneId, chatSessionId }) => { - const explicitLaneId = laneId?.trim(); - if (explicitLaneId) return explicitLaneId; - const chatId = chatSessionId?.trim(); - if (chatId && agentChatServiceHolder.current) { - const chatSession = await agentChatServiceHolder.current.getSessionSummary(chatId).catch(() => null); - if (chatSession?.laneId) return chatSession.laneId; - } - const targetRoot = path.resolve(cwd || requestedProjectRoot || projectRoot); - const lanes = await laneService.list({ includeArchived: false }); - const matchingLane = lanes.find((lane) => { - const worktreePath = path.resolve(lane.worktreePath); - const attachedRootPath = lane.attachedRootPath ? path.resolve(lane.attachedRootPath) : null; - return ( - targetRoot === worktreePath - || targetRoot.startsWith(`${worktreePath}${path.sep}`) - || (attachedRootPath !== null - && (targetRoot === attachedRootPath - || targetRoot.startsWith(`${attachedRootPath}${path.sep}`))) - ); + const appControlService = chatOnlyRuntime + ? null + : createAppControlService({ + projectRoot, + logger, + ptyService, + resolveLaneId: async ({ cwd, projectRoot: requestedProjectRoot, laneId, chatSessionId }) => { + const explicitLaneId = laneId?.trim(); + if (explicitLaneId) return explicitLaneId; + const chatId = chatSessionId?.trim(); + if (chatId && agentChatServiceHolder.current) { + const chatSession = await agentChatServiceHolder.current.getSessionSummary(chatId).catch(() => null); + if (chatSession?.laneId) return chatSession.laneId; + } + const targetRoot = path.resolve(cwd || requestedProjectRoot || projectRoot); + const lanes = await laneService.list({ includeArchived: false }); + const matchingLane = lanes.find((lane) => { + const worktreePath = path.resolve(lane.worktreePath); + const attachedRootPath = lane.attachedRootPath ? path.resolve(lane.attachedRootPath) : null; + return ( + targetRoot === worktreePath + || targetRoot.startsWith(`${worktreePath}${path.sep}`) + || (attachedRootPath !== null + && (targetRoot === attachedRootPath + || targetRoot.startsWith(`${attachedRootPath}${path.sep}`))) + ); + }); + return matchingLane?.id ?? lanes[0]?.id ?? null; + }, + }); + const macosVmService = chatOnlyRuntime + ? null + : createMacosVmService({ + projectRoot, + logger, + resolveLanes: async () => laneService.list({ includeArchived: false }), + onEvent: (event) => pushEvent("runtime", { + ...(event as unknown as Record), + type: "macos_vm", + eventType: event.type, + }), }); - return matchingLane?.id ?? lanes[0]?.id ?? null; - }, - }); - const macosVmService = createMacosVmService({ - projectRoot, - logger, - resolveLanes: async () => laneService.list({ includeArchived: false }), - onEvent: (event) => pushEvent("runtime", { - ...(event as unknown as Record), - type: "macos_vm", - eventType: event.type, - }), - }); const aiOrchestratorService = createAiOrchestratorService({ db, @@ -654,7 +670,57 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo openExternal: async () => {}, }); - const agentChatService = headlessLinearServices.agentChatService as unknown as ReturnType | null; + let automationServiceRef: ReturnType | null = null; + let agentChatService = headlessLinearServices.agentChatService as unknown as ReturnType | null; + if (resolvedArgs.chatRuntime === "agent") { + agentChatService = createAgentChatService({ + projectRoot, + adeDir: paths.adeDir, + transcriptsDir: paths.transcriptsDir, + projectId, + memoryService, + fileService: headlessLinearServices.fileService, + workerAgentService, + workerHeartbeatService: headlessLinearServices.workerHeartbeatService, + linearIssueTracker: headlessLinearServices.linearIssueTracker, + flowPolicyService: headlessLinearServices.flowPolicyService, + getMissionService: () => missionService, + getAiOrchestratorService: () => aiOrchestratorService, + getLinearDispatcherService: () => headlessLinearServices.linearDispatcherService, + linearClient: headlessLinearServices.linearClient, + linearCredentials: headlessLinearServices.linearCredentialService as never, + prService: headlessLinearServices.prService, + issueInventoryService, + processService, + getTestService: () => testService, + ptyService, + getAutomationService: () => automationServiceRef, + getGitService: () => gitService, + conflictService, + getWorkerBudgetService: () => workerBudgetService, + getMissionBudgetService: () => missionBudgetService, + computerUseArtifactBrokerService, + laneService, + sessionService, + projectConfigService, + aiIntegrationService, + ctoStateService, + logger, + appVersion: "ade-cli", + getAdeCliAgentEnv: createHeadlessAdeCliAgentEnv, + onEvent: (event) => { + aiOrchestratorService.onAgentChatEvent(event); + pushEvent("runtime", event as unknown as Record); + }, + onSessionEnded: (event) => { + pushEvent("runtime", { type: "agent_chat_session_ended", ...event }); + }, + getDirtyFileTextForPath: () => undefined, + }); + if (typeof (headlessLinearServices.prService as { setAgentChatService?: (svc: unknown) => void }).setAgentChatService === "function") { + (headlessLinearServices.prService as { setAgentChatService: (svc: unknown) => void }).setAgentChatService(agentChatService as never); + } + } agentChatServiceHolder.current = agentChatService; const automationService = createAutomationService({ db, @@ -670,6 +736,7 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo aiOrchestratorService, onEvent: (event) => pushEvent("runtime", { ...event, source: "automations" }), }); + automationServiceRef = automationService; const automationPlannerService = createAutomationPlannerService({ logger, projectRoot, @@ -730,9 +797,9 @@ export async function createAdeRuntime(args: { projectRoot: string; workspaceRoo const swallow = (fn: () => void) => { try { fn(); } catch { /* ignore */ } }; swallow(() => automationService.dispose()); swallow(() => processService.disposeAll()); - swallow(() => iosSimulatorService.dispose()); - swallow(() => appControlService.dispose()); - swallow(() => macosVmService.dispose()); + swallow(() => iosSimulatorService?.dispose()); + swallow(() => appControlService?.dispose()); + swallow(() => macosVmService?.dispose()); swallow(() => headlessLinearServices.dispose()); swallow(() => aiOrchestratorService.dispose()); swallow(() => testService.disposeAll()); diff --git a/apps/ade-cli/src/cli.test.ts b/apps/ade-cli/src/cli.test.ts index 3b738cc9..4711a081 100644 --- a/apps/ade-cli/src/cli.test.ts +++ b/apps/ade-cli/src/cli.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { + buildAdeCodeArgs, buildCliPlan, findProjectRoots, formatOutput, @@ -47,6 +48,36 @@ describe("ADE CLI", () => { expect(parsed.command).toEqual(["actions", "run", "git.stageFile", "--arg", "laneId=lane-1"]); }); + it("maps ade code to the terminal Work chat launcher", () => { + const parsed = parseCliArgs(["--project-root", "/tmp/project", "code", "--print-state"]); + expect(parsed.options.projectRoot).toBe("/tmp/project"); + expect(parsed.command).toEqual(["code", "--print-state"]); + + const plan = buildCliPlan(parsed.command); + expect(plan).toEqual({ kind: "ade-code", rest: ["--print-state"] }); + }); + + it("forwards resolved roots and socket intent to ade code", () => { + const args = buildAdeCodeArgs(["--print-state"], { + ...baseResolveOpts(), + projectRoot: "/tmp/project", + workspaceRoot: null, + headless: false, + requireSocket: true, + }); + + expect(args).toEqual([ + "--project-root", + "/tmp/project", + "--workspace-root", + "/tmp/project", + "--socket", + "/tmp/project/.ade/ade.sock", + "--require-socket", + "--print-state", + ]); + }); + it("preserves command-local value flags that overlap global flags", () => { const parsed = parseCliArgs(["files", "write", "src/index.ts", "--text", "hello"]); expect(parsed.options.text).toBe(false); diff --git a/apps/ade-cli/src/cli.ts b/apps/ade-cli/src/cli.ts index b72c4e42..ee6cb50e 100644 --- a/apps/ade-cli/src/cli.ts +++ b/apps/ade-cli/src/cli.ts @@ -104,6 +104,7 @@ type FormatterId = type CliPlan = | { kind: "help"; text: string } | { kind: "execute"; label: string; steps: InvocationStep[]; visualizer?: "lanes"; summary?: "status" | "doctor" | "auth"; formatter?: FormatterId; preferHeadless?: boolean } + | { kind: "ade-code"; rest: string[] } | { kind: "cursor-cloud"; rest: string[] } | { kind: "mcp" }; @@ -345,6 +346,7 @@ const TOP_LEVEL_HELP = `${ADE_BANNER} $ ade help Display help for a command $ ade auth status Check local ADE CLI readiness + $ ade code Open ADE Work chat in the terminal $ ade doctor Inspect project, socket, runtime, and tool availability $ ade lanes list | show | create | child Work with lanes and lane stacks $ ade git status | commit | push | stash Run ADE-aware git operations @@ -752,6 +754,17 @@ const IOS_SIMULATOR_HELP_ALIASES: Record = { }; const HELP_BY_COMMAND: Record = { + code: `${ADE_BANNER} + ADE Code + + Launch the terminal-native ADE Work chat. It shares lanes, chat sessions, + transcript state, and slash commands with desktop ADE. + + $ ade code Start the TUI for the current project + $ ade code --print-state Smoke-test attach/embed state + $ ade code --embedded Force the embedded runtime fallback + $ ade --project-root code Launch against a specific ADE project +`, lanes: `${ADE_BANNER} Lanes @@ -4147,6 +4160,10 @@ function buildCliPlan(command: string[]): CliPlan { if (primary === "version" || primary === "--version" || primary === "-v") { return { kind: "help", text: `ade ${VERSION}\n` }; } + if (primary === "code") { + const rest = args; + return { kind: "ade-code", rest }; + } if (primary === "status") { return { kind: "execute", label: "status", summary: "status", steps: [{ key: "ping", method: "ping" }] }; } @@ -4311,6 +4328,56 @@ function commandExists(command: string): boolean { return result.status === 0 && result.stdout.trim().length > 0; } +function resolveAdeCodeLaunch(): { command: string; args: string[] } { + const explicit = process.env.ADE_CODE_EXECUTABLE?.trim(); + if (explicit) return { command: explicit, args: [] }; + + const siblingDist = path.resolve(CLI_PACKAGE_ROOT, "..", "ade-code", "dist", "cli.js"); + if (fs.existsSync(siblingDist)) { + return { command: process.execPath, args: [siblingDist] }; + } + + if (commandExists("ade-code")) { + return { command: "ade-code", args: [] }; + } + + throw new CliUsageError("ade code could not find ade-code. Build apps/ade-code or install the ade-code binary."); +} + +function resolveAdeCodeSocketPath(projectRoot: string): string { + return process.env.ADE_RPC_URL?.trim() + || process.env.ADE_RPC_SOCKET_PATH?.trim() + || path.join(projectRoot, ".ade", "ade.sock"); +} + +function buildAdeCodeArgs(rest: string[], options: GlobalOptions): string[] { + const roots = resolveRoots(options); + return [ + "--project-root", + roots.projectRoot, + "--workspace-root", + roots.workspaceRoot, + ...(options.headless ? ["--embedded"] : []), + ...(options.requireSocket ? ["--socket", resolveAdeCodeSocketPath(roots.projectRoot), "--require-socket"] : []), + ...rest, + ]; +} + +function runAdeCode(rest: string[], options: GlobalOptions): { output: string; exitCode: number } { + const launch = resolveAdeCodeLaunch(); + const args = [ + ...launch.args, + ...buildAdeCodeArgs(rest, options), + ]; + const result = spawnSync(launch.command, args, { + cwd: process.cwd(), + env: process.env, + stdio: "inherit", + }); + if (result.error) throw result.error; + return { output: "", exitCode: typeof result.status === "number" ? result.status : 1 }; +} + function runLocalCommand(command: string, args: string[], cwd: string): { ok: boolean; stdout: string; stderr: string } { const result = spawnSync(command, args, { cwd, @@ -6750,6 +6817,9 @@ async function runCli(argv: string[]): Promise<{ output: string; exitCode: numbe await runMcpServer({ ...parsed.options, headless: true, requireSocket: false }); return { output: "", exitCode: 0 }; } + if (plan.kind === "ade-code") { + return runAdeCode(plan.rest, parsed.options); + } const result = await executePlan(plan, parsed.options); return { output: formatOutput(result, parsed.options, inferFormatter(plan)), exitCode: 0 }; } finally { @@ -6808,6 +6878,7 @@ if (/(^|[/\\])cli\.(?:ts|js|cjs)$/.test(process.argv[1] ?? "")) { export { buildCliPlan, + buildAdeCodeArgs, findProjectRoots, formatOutput, graphWaitState, diff --git a/apps/ade-code/package-lock.json b/apps/ade-code/package-lock.json new file mode 100644 index 00000000..20cdd297 --- /dev/null +++ b/apps/ade-code/package-lock.json @@ -0,0 +1,4907 @@ +{ + "name": "ade-code", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ade-code", + "version": "0.0.0", + "dependencies": { + "@cursor/sdk": "^1.0.9", + "ink": "^5.2.1", + "ink-text-input": "^6.0.0", + "node-cron": "^3.0.3", + "node-pty": "^1.1.0", + "react": "^18.3.1", + "sql.js": "^1.13.0", + "yaml": "^2.8.2" + }, + "bin": { + "ade-code": "dist/cli.cjs" + }, + "devDependencies": { + "@types/node": "^22.19.18", + "@types/react": "^18.3.18", + "ink-testing-library": "^4.0.0", + "tsup": "^8.3.5", + "tsx": "^4.20.6", + "typescript": "^5.7.3", + "vitest": "^0.34.6" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.7.0.tgz", + "integrity": "sha512-iNKdJRi69YP3mq6AePRT8F/HrxWCewrhxnLMNm0vpqXAR8biwzRtO6Hjx80C6UvtKJ5sFmffQT7I4Baecz389w==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@connectrpc/connect-node": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.7.0.tgz", + "integrity": "sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==", + "license": "Apache-2.0", + "dependencies": { + "undici": "^5.28.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.7.0" + } + }, + "node_modules/@cursor/sdk": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk/-/sdk-1.0.12.tgz", + "integrity": "sha512-jGx0wFY1N9uIdIKr303CfM6m/dLXmRCUnU/0yNP/oiOpkBXqgqaThGbgYbcOeVrYonMZc/DZJ9EydXOEPJLcbg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@bufbuild/protobuf": "1.10.0", + "@connectrpc/connect": "^1.6.1", + "@connectrpc/connect-node": "^1.6.1", + "@statsig/js-client": "3.31.0", + "sqlite3": "^5.1.7", + "zod": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@cursor/sdk-darwin-arm64": "1.0.12", + "@cursor/sdk-darwin-x64": "1.0.12", + "@cursor/sdk-linux-arm64": "1.0.12", + "@cursor/sdk-linux-x64": "1.0.12", + "@cursor/sdk-win32-x64": "1.0.12" + } + }, + "node_modules/@cursor/sdk-darwin-arm64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-arm64/-/sdk-darwin-arm64-1.0.12.tgz", + "integrity": "sha512-AOFx+aX+4SntAeC66YncHACXk5duxp+HzDrxxF4Tl93N6nLjHaHEKSAXbt87ivL34MCHop4v/3c70QzBhamB2g==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cursor/sdk-darwin-x64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-x64/-/sdk-darwin-x64-1.0.12.tgz", + "integrity": "sha512-/ZDAYFUrnPd8hAGRky9ZGcROqZSZ2b5W+aEjTdINzLhJ8x5ZNXtjaz0ZYSHabOn2BeErjXgTcq+4bX2/To4C1A==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cursor/sdk-linux-arm64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-arm64/-/sdk-linux-arm64-1.0.12.tgz", + "integrity": "sha512-kAxNqiB3dPtlW9fVjjIZEdbIGEGLA9moOM3zYwsXh8J1Qw942nJYMGDGR4o8x0zglwZ24a1JpovvZamrCaC3Yw==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cursor/sdk-linux-x64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-x64/-/sdk-linux-x64-1.0.12.tgz", + "integrity": "sha512-RmBiBCPKMZC5McDerGk2Rk4P47xz2A+uzRoRgH6sMoOjklc33ry11iAZC0D5F5xH85chgY878086A/Q8+XrAuA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cursor/sdk-win32-x64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-win32-x64/-/sdk-win32-x64-1.0.12.tgz", + "integrity": "sha512-uH4shdHrKOdtNLapy1uuScJ9lL2Pc8zc9I9ZKC6b6bx+0UX6xLAqjPP7dqVPfO6D9u61yLq1Hs86XOLs5ZVkPA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@statsig/client-core": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/client-core/-/client-core-3.31.0.tgz", + "integrity": "sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==", + "license": "ISC" + }, + "node_modules/@statsig/js-client": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/js-client/-/js-client-3.31.0.tgz", + "integrity": "sha512-LFa5E0LjT6sTfZv3sNGoyRLSZ1078+agdgOA+Vm1ecjG+KbSOfBLTW7hMwimrJ29slRwbYDzbtKaPJo/R37N2g==", + "license": "ISC", + "dependencies": { + "@statsig/client-core": "3.31.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-subset": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", + "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/chai": "<5.2.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", + "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@vitest/expect": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "0.34.6", + "p-limit": "^4.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.4.3", + "loupe": "^2.3.6", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-testing-library": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz", + "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "optional": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.4.0", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.10", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.3.3", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/apps/ade-code/package.json b/apps/ade-code/package.json new file mode 100644 index 00000000..d244eb95 --- /dev/null +++ b/apps/ade-code/package.json @@ -0,0 +1,41 @@ +{ + "name": "ade-code", + "version": "0.0.0", + "description": "Terminal-native ADE Work chat client", + "type": "module", + "bin": { + "ade-code": "dist/cli.js" + }, + "files": [ + "dist/**/*", + "README.md" + ], + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "dev": "tsx src/cli.tsx", + "build": "tsup", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@cursor/sdk": "^1.0.9", + "ink": "^5.2.1", + "ink-text-input": "^6.0.0", + "node-cron": "^3.0.3", + "node-pty": "^1.1.0", + "react": "^18.3.1", + "sql.js": "^1.13.0", + "yaml": "^2.8.2" + }, + "devDependencies": { + "@types/node": "^22.19.18", + "@types/react": "^18.3.18", + "ink-testing-library": "^4.0.0", + "tsup": "^8.3.5", + "tsx": "^4.20.6", + "typescript": "^5.7.3", + "vitest": "^0.34.6" + } +} diff --git a/apps/ade-code/src/__tests__/adeApi.test.ts b/apps/ade-code/src/__tests__/adeApi.test.ts new file mode 100644 index 00000000..5c261b29 --- /dev/null +++ b/apps/ade-code/src/__tests__/adeApi.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import type { AgentChatEventEnvelope } from "../../../desktop/src/shared/types/chat"; +import { latestTokenStats } from "../adeApi"; + +function envelope( + sequence: number, + event: AgentChatEventEnvelope["event"], +): AgentChatEventEnvelope { + return { + sessionId: "s1", + timestamp: `2026-01-01T12:00:0${sequence}.000Z`, + sequence, + event, + }; +} + +describe("latestTokenStats", () => { + it("tracks streaming state, context percentage, token counts, and cost", () => { + const events = [ + envelope(1, { type: "status", turnStatus: "started" }), + envelope(2, { + type: "tokens", + turnId: "turn-1", + inputTokens: 2_000, + outputTokens: 500, + totalTokens: 2_500, + contextWindow: 10_000, + } as AgentChatEventEnvelope["event"]), + envelope(3, { + type: "done", + turnId: "turn-1", + status: "completed", + usage: { inputTokens: 2_100, outputTokens: 700 }, + costUsd: 0.42, + }), + ]; + + expect(latestTokenStats(events)).toEqual({ + percent: 25, + streaming: false, + inputTokens: 2_100, + outputTokens: 700, + costUsd: 0.42, + }); + }); +}); diff --git a/apps/ade-code/src/__tests__/commands.test.ts b/apps/ade-code/src/__tests__/commands.test.ts new file mode 100644 index 00000000..b6c197f9 --- /dev/null +++ b/apps/ade-code/src/__tests__/commands.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { commandPlacement, parseCommand, paletteCommands } from "../commands"; + +describe("commands", () => { + it("parses multi-word ADE commands before generic slash commands", () => { + const parsed = parseCommand("/linear pull ADE-123"); + expect(parsed?.name).toBe("/linear pull"); + expect(parsed?.args).toBe("ADE-123"); + expect(parsed ? commandPlacement(parsed) : null).toBe("right"); + }); + + it("routes the generic ADE action escape hatch to the right pane", () => { + const parsed = parseCommand("/ade git.listBranches {\"laneId\":\"lane-1\"}"); + expect(parsed?.name).toBe("/ade"); + expect(parsed?.args).toBe("git.listBranches {\"laneId\":\"lane-1\"}"); + expect(parsed ? commandPlacement(parsed) : null).toBe("right"); + }); + + it("routes user-defined commands to chat", () => { + const parsed = parseCommand("/ship now", [ + { name: "/ship", description: "Ship it", source: "sdk" }, + ]); + expect(parsed?.userCommand?.name).toBe("/ship"); + expect(parsed ? commandPlacement(parsed) : null).toBe("chat"); + }); + + it("lets local project commands override ADE built-ins on exact name", () => { + const parsed = parseCommand("/status please", [ + { name: "/status", description: "Project status prompt", source: "local" }, + ]); + expect(parsed?.spec).toBeNull(); + expect(parsed?.userCommand?.name).toBe("/status"); + expect(parsed ? commandPlacement(parsed) : null).toBe("chat"); + }); + + it("tags built-ins and user commands in the palette", () => { + const rows = paletteCommands("/ship", [ + { name: "/ship", description: "Ship it", source: "sdk" }, + ]); + expect(rows).toContainEqual(expect.objectContaining({ name: "/ship", source: "user" })); + }); +}); diff --git a/apps/ade-code/src/__tests__/connection.test.ts b/apps/ade-code/src/__tests__/connection.test.ts new file mode 100644 index 00000000..c81e4985 --- /dev/null +++ b/apps/ade-code/src/__tests__/connection.test.ts @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { connectToAde } from "../connection"; +import type { ProjectLaunchContext } from "../types"; + +const embedded = vi.hoisted(() => { + const requests: Array<{ jsonrpc: string; id: number; method: string; params?: unknown }> = []; + const runtime = { + dispose: vi.fn(), + agentChatService: { + subscribeToEvents: vi.fn(() => vi.fn()), + }, + }; + const handler = Object.assign( + vi.fn(async (message: { jsonrpc: string; id: number; method: string; params?: unknown }) => { + requests.push(message); + return { ok: true, method: message.method }; + }), + { dispose: vi.fn() }, + ); + + return { + requests, + runtime, + handler, + createAdeRuntime: vi.fn(async () => runtime), + createAdeRpcRequestHandler: vi.fn(() => handler), + }; +}); + +vi.mock("../../../ade-cli/src/bootstrap", () => ({ + createAdeRuntime: embedded.createAdeRuntime, +})); + +vi.mock("../../../ade-cli/src/adeRpcServer", () => ({ + createAdeRpcRequestHandler: embedded.createAdeRpcRequestHandler, +})); + +const project: ProjectLaunchContext = { + launchCwd: "/tmp/ade-code", + projectRoot: "/tmp/ade-code", + workspaceRoot: "/tmp/ade-code", + laneHint: null, +}; + +describe("connectToAde embedded mode", () => { + beforeEach(() => { + embedded.requests.length = 0; + embedded.runtime.dispose.mockClear(); + embedded.runtime.agentChatService.subscribeToEvents.mockClear(); + embedded.handler.mockClear(); + embedded.handler.dispose.mockClear(); + embedded.createAdeRuntime.mockClear(); + embedded.createAdeRpcRequestHandler.mockClear(); + }); + + it("uses unique JSON-RPC ids for direct embedded requests", async () => { + const connection = await connectToAde({ + project, + forceEmbedded: true, + }); + + try { + await Promise.all([ + connection.request("ade/actions/list"), + connection.request("ping"), + ]); + } finally { + await connection.close(); + } + + expect(embedded.requests.map((request) => request.method)).toEqual([ + "ade/initialize", + "ade/initialized", + "ade/actions/list", + "ping", + ]); + expect(embedded.requests.map((request) => request.id)).toEqual([1, 2, 3, 4]); + expect(new Set(embedded.requests.map((request) => request.id)).size).toBe(4); + }); +}); diff --git a/apps/ade-code/src/__tests__/format.test.ts b/apps/ade-code/src/__tests__/format.test.ts new file mode 100644 index 00000000..a751521b --- /dev/null +++ b/apps/ade-code/src/__tests__/format.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from "vitest"; +import { latestExpandableFailureId, renderChatLines, renderObject } from "../format"; + +describe("renderChatLines", () => { + it("renders compact rule-separated chat turns", () => { + const lines = renderChatLines({ + activeSession: null, + notices: [], + events: [ + { + sessionId: "s1", + timestamp: "2026-01-01T12:00:00.000Z", + sequence: 1, + event: { type: "user_message", text: "hello" }, + }, + { + sessionId: "s1", + timestamp: "2026-01-01T12:00:01.000Z", + sequence: 2, + event: { type: "text", text: "hi" }, + }, + ], + }); + expect(lines.map((line) => line.tone)).toEqual(["user", "assistant"]); + expect(lines[0]?.header).toContain("you"); + expect(lines[1]?.header).toContain("ade"); + }); + + it("renders non-JSON-safe objects without throwing", () => { + const value: { self?: unknown } = {}; + value.self = value; + expect(renderObject(value)).toBe("[object Object]"); + }); + + it("renders tool, edit, and compaction events compactly", () => { + const lines = renderChatLines({ + activeSession: null, + notices: [], + events: [ + { + sessionId: "s1", + timestamp: "2026-01-01T12:00:00.000Z", + sequence: 1, + event: { type: "tool_call", tool: "read", args: { path: "src/app.ts" }, itemId: "tool-1" }, + }, + { + sessionId: "s1", + timestamp: "2026-01-01T12:00:01.000Z", + sequence: 2, + event: { + type: "file_change", + path: "src/app.ts", + kind: "modify", + status: "completed", + itemId: "edit-1", + diff: "+hello\n-world", + }, + }, + { + sessionId: "s1", + timestamp: "2026-01-01T12:00:02.000Z", + sequence: 3, + event: { type: "context_compact", trigger: "auto" }, + }, + ], + }); + + expect(lines).toEqual([ + expect.objectContaining({ tone: "tool", body: expect.stringContaining("> read") }), + expect.objectContaining({ tone: "tool", body: expect.stringContaining("> edit src/app.ts") }), + expect.objectContaining({ tone: "notice", body: expect.stringContaining("context compacted") }), + ]); + }); + + it("summarizes command pass and fail counts when present", () => { + const events = [{ + sessionId: "s1", + timestamp: "2026-01-01T12:00:00.000Z", + sequence: 1, + event: { + type: "command", + command: "vitest", + cwd: "/repo", + output: "Test Files 1 failed | Tests 7 passed, 1 failed", + itemId: "cmd-1", + status: "failed", + exitCode: 1, + durationMs: 2100, + }, + }] as const; + const lines = renderChatLines({ + activeSession: null, + notices: [], + events: [...events], + }); + + expect(lines[0]).toEqual(expect.objectContaining({ + tone: "error", + body: expect.stringContaining("7 passed · 1 failed"), + })); + expect(lines[0]?.body).toContain("↵ expands"); + expect(latestExpandableFailureId([...events])).toBe("1:command:2026-01-01T12:00:00.000Z"); + }); + + it("renders expanded failed tool output when requested", () => { + const events = [{ + sessionId: "s1", + timestamp: "2026-01-01T12:00:00.000Z", + sequence: 1, + event: { + type: "tool_result", + tool: "read", + result: { error: "Permission denied", path: "/repo/secret" }, + itemId: "tool-1", + status: "failed", + }, + }] as const; + const id = latestExpandableFailureId([...events]); + const lines = renderChatLines({ + activeSession: null, + notices: [], + events: [...events], + expandedLineIds: new Set(id ? [id] : []), + }); + + expect(lines[0]).toEqual(expect.objectContaining({ + tone: "error", + body: expect.stringContaining("Permission denied"), + })); + expect(lines[0]?.body).not.toContain("↵ expands"); + }); +}); diff --git a/apps/ade-code/src/__tests__/heartbeat.test.ts b/apps/ade-code/src/__tests__/heartbeat.test.ts new file mode 100644 index 00000000..44de87b1 --- /dev/null +++ b/apps/ade-code/src/__tests__/heartbeat.test.ts @@ -0,0 +1,64 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { startTuiHeartbeat, type TuiHeartbeat } from "../heartbeat"; + +const heartbeats: TuiHeartbeat[] = []; + +function tempProjectRoot(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "ade-code-heartbeat-")); +} + +function heartbeatFile(projectRoot: string): string { + return path.join(projectRoot, ".ade", "cache", "ade-code", "clients", `${process.pid}.json`); +} + +afterEach(() => { + for (const heartbeat of heartbeats.splice(0)) { + heartbeat.stop(); + } +}); + +describe("startTuiHeartbeat", () => { + it("shares process cleanup handlers across active heartbeats", () => { + const exitListeners = process.listenerCount("exit"); + const sigintListeners = process.listenerCount("SIGINT"); + const firstRoot = tempProjectRoot(); + const secondRoot = tempProjectRoot(); + + const first = startTuiHeartbeat(firstRoot); + heartbeats.push(first); + const second = startTuiHeartbeat(secondRoot); + heartbeats.push(second); + + expect(process.listenerCount("exit")).toBe(exitListeners + 1); + expect(process.listenerCount("SIGINT")).toBe(sigintListeners + 1); + expect(fs.existsSync(heartbeatFile(firstRoot))).toBe(true); + expect(fs.existsSync(heartbeatFile(secondRoot))).toBe(true); + + first.stop(); + expect(process.listenerCount("exit")).toBe(exitListeners + 1); + expect(process.listenerCount("SIGINT")).toBe(sigintListeners + 1); + expect(fs.existsSync(heartbeatFile(firstRoot))).toBe(false); + expect(fs.existsSync(heartbeatFile(secondRoot))).toBe(true); + + second.stop(); + expect(process.listenerCount("exit")).toBe(exitListeners); + expect(process.listenerCount("SIGINT")).toBe(sigintListeners); + expect(fs.existsSync(heartbeatFile(secondRoot))).toBe(false); + }); + + it("makes stop idempotent", () => { + const exitListeners = process.listenerCount("exit"); + const projectRoot = tempProjectRoot(); + const heartbeat = startTuiHeartbeat(projectRoot); + heartbeats.push(heartbeat); + + heartbeat.stop(); + heartbeat.stop(); + + expect(process.listenerCount("exit")).toBe(exitListeners); + expect(fs.existsSync(heartbeatFile(projectRoot))).toBe(false); + }); +}); diff --git a/apps/ade-code/src/__tests__/jsonRpcClient.test.ts b/apps/ade-code/src/__tests__/jsonRpcClient.test.ts new file mode 100644 index 00000000..40b31310 --- /dev/null +++ b/apps/ade-code/src/__tests__/jsonRpcClient.test.ts @@ -0,0 +1,102 @@ +import fs from "node:fs"; +import net from "node:net"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { JsonRpcClient } from "../jsonRpcClient"; + +function listen(server: net.Server, socketPath: string): Promise { + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(socketPath, () => { + server.off("error", reject); + resolve(); + }); + }); +} + +function closeServer(server: net.Server): Promise { + return new Promise((resolve) => server.close(() => resolve())); +} + +describe("JsonRpcClient", () => { + it("handles framed notifications before JSONL responses", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-code-jsonrpc-")); + const socketPath = path.join(tmpDir, "rpc.sock"); + let resolveServerSocket: (socket: net.Socket) => void = () => {}; + const serverSocketReady = new Promise((resolve) => { + resolveServerSocket = resolve; + }); + const server = net.createServer((socket) => { + resolveServerSocket(socket); + socket.on("data", (chunk) => { + const text = String(chunk); + const match = /"id":(\d+)/.exec(text); + const id = match ? Number.parseInt(match[1]!, 10) : 1; + socket.write(`${JSON.stringify({ jsonrpc: "2.0", id, result: { ok: true } })}\n`); + }); + }); + + await listen(server, socketPath); + const client = await JsonRpcClient.connect(socketPath); + const socket = await serverSocketReady; + try { + const notification = new Promise((resolve) => { + client.onNotification("chat/event", resolve); + }); + const payload = JSON.stringify({ + jsonrpc: "2.0", + method: "chat/event", + params: { sessionId: "s1" }, + }); + socket.write(`Content-Length: ${Buffer.byteLength(payload, "utf8")}\r\n\r\n${payload}`); + + await expect(notification).resolves.toEqual({ sessionId: "s1" }); + await expect(client.request("ping")).resolves.toEqual({ ok: true }); + } finally { + client.close(); + socket.destroy(); + await closeServer(server); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it("honors byte-based Content-Length framing for unicode payloads", async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-code-jsonrpc-")); + const socketPath = path.join(tmpDir, "rpc.sock"); + let resolveServerSocket: (socket: net.Socket) => void = () => {}; + const serverSocketReady = new Promise((resolve) => { + resolveServerSocket = resolve; + }); + const server = net.createServer((socket) => { + resolveServerSocket(socket); + }); + + await listen(server, socketPath); + const client = await JsonRpcClient.connect(socketPath); + const socket = await serverSocketReady; + try { + const notification = new Promise((resolve) => { + client.onNotification("chat/event", resolve); + }); + const payload = JSON.stringify({ + jsonrpc: "2.0", + method: "chat/event", + params: { message: "héllo ✅" }, + }); + const framed = Buffer.concat([ + Buffer.from(`Content-Length: ${Buffer.byteLength(payload, "utf8")}\r\n\r\n`, "ascii"), + Buffer.from(payload, "utf8"), + ]); + socket.write(framed.subarray(0, 20)); + socket.write(framed.subarray(20)); + + await expect(notification).resolves.toEqual({ message: "héllo ✅" }); + } finally { + client.close(); + socket.destroy(); + await closeServer(server); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/ade-code/src/__tests__/linearCommands.test.ts b/apps/ade-code/src/__tests__/linearCommands.test.ts new file mode 100644 index 00000000..c0fd6139 --- /dev/null +++ b/apps/ade-code/src/__tests__/linearCommands.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { buildLinearToolRequest, parseLinearArgs } from "../linearCommands"; + +describe("linear command routing", () => { + it("parses flags and quoted values", () => { + expect(parseLinearArgs("run cancel run-1 --reason \"not ready\" --launch false")).toEqual({ + positionals: ["run", "cancel", "run-1"], + options: { reason: "not ready", launch: false }, + }); + }); + + it("routes sync dashboard and queue resolution", () => { + expect(buildLinearToolRequest("sync dashboard")).toEqual({ + kind: "tool", + title: "Linear sync dashboard", + toolName: "getLinearSyncDashboard", + args: {}, + }); + expect(buildLinearToolRequest("sync resolve queue-1 approve --note ok")).toEqual({ + kind: "tool", + title: "Linear sync resolve", + toolName: "resolveLinearSyncQueueItem", + args: { + queueItemId: "queue-1", + action: "approve", + note: "ok", + }, + }); + }); + + it("routes worker handoff and reports usage for missing fields", () => { + expect(buildLinearToolRequest("route worker LIN-123 agent-1")).toEqual({ + kind: "tool", + title: "Linear route worker", + toolName: "routeLinearIssueToWorker", + args: { issueId: "LIN-123", agentId: "agent-1" }, + }); + expect(buildLinearToolRequest("run cancel run-1")).toEqual({ + kind: "usage", + title: "Linear run cancel", + body: "Usage: /linear run cancel --reason ", + }); + }); +}); diff --git a/apps/ade-code/src/__tests__/pendingInput.test.ts b/apps/ade-code/src/__tests__/pendingInput.test.ts new file mode 100644 index 00000000..d1764ba9 --- /dev/null +++ b/apps/ade-code/src/__tests__/pendingInput.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "vitest"; +import type { AgentChatEventEnvelope, PendingInputRequest } from "../../../desktop/src/shared/types/chat"; +import { buildPendingInputAnswers, latestPendingApproval } from "../pendingInput"; + +const baseRequest: PendingInputRequest = { + requestId: "req-1", + source: "codex", + kind: "structured_question", + title: "Pick path", + questions: [{ + id: "path", + question: "Which path?", + options: [ + { label: "Recommended", value: "recommended" }, + { label: "Manual", value: "manual" }, + ], + allowsFreeform: true, + }], + allowsFreeform: true, + blocking: true, + canProceedWithoutAnswer: false, +}; + +describe("pendingInput", () => { + it("maps option numbers to structured answers", () => { + expect(buildPendingInputAnswers(baseRequest, "2")).toEqual({ path: "manual" }); + }); + + it("keeps multi-select answers as arrays", () => { + const request: PendingInputRequest = { + ...baseRequest, + questions: [{ + ...baseRequest.questions[0]!, + multiSelect: true, + }], + }; + expect(buildPendingInputAnswers(request, "1, Manual")).toEqual({ path: ["recommended", "manual"] }); + }); + + it("returns the latest unresolved pending input request", () => { + const events: AgentChatEventEnvelope[] = [ + { + sessionId: "s1", + timestamp: "2026-01-01T00:00:00.000Z", + sequence: 1, + event: { + type: "approval_request", + itemId: "item-1", + kind: "tool_call", + description: "Need input", + detail: { request: { ...baseRequest, itemId: "item-1" } }, + }, + }, + { + sessionId: "s1", + timestamp: "2026-01-01T00:00:01.000Z", + sequence: 2, + event: { + type: "pending_input_resolved", + itemId: "item-1", + resolution: "accepted", + }, + }, + ]; + expect(latestPendingApproval(events)).toBeNull(); + + events.push({ + sessionId: "s1", + timestamp: "2026-01-01T00:00:02.000Z", + sequence: 3, + event: { + type: "approval_request", + itemId: "item-2", + kind: "tool_call", + description: "Need input", + detail: { request: { ...baseRequest, requestId: "req-2", itemId: "item-2" } }, + }, + }); + expect(latestPendingApproval(events)).toEqual(expect.objectContaining({ + itemId: "item-2", + mode: "question", + highStakes: false, + })); + }); + + it("flags destructive or external-impact approvals as high stakes", () => { + const events: AgentChatEventEnvelope[] = [{ + sessionId: "s1", + timestamp: "2026-01-01T00:00:00.000Z", + sequence: 1, + event: { + type: "approval_request", + itemId: "item-1", + kind: "tool_call", + description: "Force-push the main branch to production", + detail: { command: "git push --force origin main" }, + }, + }]; + + expect(latestPendingApproval(events)).toEqual(expect.objectContaining({ + itemId: "item-1", + mode: "approval", + highStakes: true, + })); + }); +}); diff --git a/apps/ade-code/src/__tests__/project.test.ts b/apps/ade-code/src/__tests__/project.test.ts new file mode 100644 index 00000000..93872bdf --- /dev/null +++ b/apps/ade-code/src/__tests__/project.test.ts @@ -0,0 +1,51 @@ +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { chooseInitialLane } from "../project"; +import type { LaneSummary } from "../../../desktop/src/shared/types/lanes"; + +function lane(overrides: Partial): LaneSummary { + return { + id: "main", + name: "main", + laneType: "primary", + baseRef: "main", + branchRef: "main", + worktreePath: "/repo", + parentLaneId: null, + childCount: 0, + stackDepth: 0, + parentStatus: null, + isEditProtected: false, + status: { dirty: false, ahead: 0, behind: 0, remoteBehind: 0, rebaseInProgress: false }, + color: null, + icon: null, + tags: [], + createdAt: new Date(0).toISOString(), + ...overrides, + }; +} + +describe("chooseInitialLane", () => { + it("prefers the ADE worktree lane hint", () => { + const lanes = [ + lane({ id: "main", name: "main", laneType: "primary", worktreePath: "/repo" }), + lane({ id: "feature-a", name: "Feature A", laneType: "worktree", branchRef: "feature/a", worktreePath: "/repo/.ade/worktrees/feature-a" }), + ]; + expect(chooseInitialLane(lanes, { + workspaceRoot: "/repo/.ade/worktrees/feature-a", + laneHint: "feature-a", + })?.id).toBe("feature-a"); + }); + + it("falls back to matching the workspace path", () => { + const worktreePath = path.resolve("/repo/.ade/worktrees/feature-b"); + const lanes = [ + lane({ id: "main", laneType: "primary", worktreePath: "/repo" }), + lane({ id: "feature-b", laneType: "worktree", worktreePath }), + ]; + expect(chooseInitialLane(lanes, { + workspaceRoot: path.join(worktreePath, "apps/desktop"), + laneHint: null, + })?.id).toBe("feature-b"); + }); +}); diff --git a/apps/ade-code/src/adeApi.ts b/apps/ade-code/src/adeApi.ts new file mode 100644 index 00000000..683515f9 --- /dev/null +++ b/apps/ade-code/src/adeApi.ts @@ -0,0 +1,204 @@ +import { getDefaultModelDescriptor, type ModelProviderGroup } from "../../desktop/src/shared/modelRegistry"; +import type { + AgentChatEventEnvelope, + AgentChatFileRef, + AgentChatModelInfo, + AgentChatProvider, + AgentChatSession, + AgentChatSessionSummary, + AgentChatSlashCommand, +} from "../../desktop/src/shared/types/chat"; +import type { LaneSummary } from "../../desktop/src/shared/types/lanes"; +import type { AdeCodeConnection, ChatHistorySnapshot, CreatedChat, NavigateRequest, NavigateResult } from "./types"; + +export async function listLanes(connection: AdeCodeConnection): Promise { + return await connection.action("lane", "list", { + includeArchived: false, + includeStatus: true, + }); +} + +export async function listChatSessions( + connection: AdeCodeConnection, + laneId?: string | null, +): Promise { + const argsList = laneId ? [laneId] : []; + return await connection.actionList("chat", "listSessions", argsList); +} + +export async function getChatHistory( + connection: AdeCodeConnection, + sessionId: string, + maxEvents = 500, +): Promise { + return await connection.actionList("chat", "getChatEventHistory", [sessionId, { maxEvents }]); +} + +export async function getSlashCommands( + connection: AdeCodeConnection, + sessionId: string | null, +): Promise { + if (!sessionId) return []; + return await connection.action("chat", "getSlashCommands", { sessionId }); +} + +export async function getAvailableModels( + connection: AdeCodeConnection, + provider: AgentChatProvider, +): Promise { + return await connection.action("chat", "getAvailableModels", { + provider, + activateRuntime: false, + }); +} + +export async function createChatSession(args: { + connection: AdeCodeConnection; + laneId: string; + title?: string | null; + provider?: ModelProviderGroup; + modelId?: string | null; + reasoningEffort?: string | null; +}): Promise { + const provider = args.provider ?? "codex"; + const descriptor = args.modelId + ? null + : getDefaultModelDescriptor(provider); + const modelId = args.modelId ?? descriptor?.id ?? null; + const model = descriptor?.providerModelId ?? descriptor?.shortId ?? (provider === "claude" ? "sonnet" : "gpt-5.5"); + return await args.connection.action("chat", "createSession", { + laneId: args.laneId, + provider, + model, + ...(modelId ? { modelId } : {}), + ...(args.title?.trim() ? { title: args.title.trim() } : {}), + ...(args.reasoningEffort ? { reasoningEffort: args.reasoningEffort } : {}), + surface: "work", + }); +} + +export async function sendChatMessage( + connection: AdeCodeConnection, + sessionId: string, + text: string, + attachments: AgentChatFileRef[] = [], +): Promise { + await connection.action("chat", "sendMessage", { + sessionId, + text, + ...(attachments.length ? { attachments } : {}), + }); +} + +export async function approveToolUse(args: { + connection: AdeCodeConnection; + sessionId: string; + itemId: string; + decision: "accept" | "accept_for_session" | "decline" | "cancel"; + responseText?: string | null; +}): Promise { + await args.connection.action("chat", "approveToolUse", { + sessionId: args.sessionId, + itemId: args.itemId, + decision: args.decision, + ...(args.responseText ? { responseText: args.responseText } : {}), + }); +} + +export async function respondToInput(args: { + connection: AdeCodeConnection; + sessionId: string; + itemId: string; + decision?: "accept" | "accept_for_session" | "decline" | "cancel"; + answers?: Record; + responseText?: string | null; +}): Promise { + await args.connection.action("chat", "respondToInput", { + sessionId: args.sessionId, + itemId: args.itemId, + ...(args.decision ? { decision: args.decision } : {}), + ...(args.answers ? { answers: args.answers } : {}), + ...(args.responseText ? { responseText: args.responseText } : {}), + }); +} + +export async function interruptChat(connection: AdeCodeConnection, sessionId: string): Promise { + await connection.action("chat", "interrupt", { sessionId }); +} + +export async function resumeChat(connection: AdeCodeConnection, sessionId: string): Promise { + return await connection.action("chat", "resumeSession", { sessionId }); +} + +export async function renameChat(connection: AdeCodeConnection, sessionId: string, title: string): Promise { + return await connection.action("chat", "updateSession", { + sessionId, + title, + manuallyNamed: true, + }); +} + +export async function updateChatModel(args: { + connection: AdeCodeConnection; + sessionId: string; + modelId?: string | null; + reasoningEffort?: string | null; +}): Promise { + return await args.connection.action("chat", "updateSession", { + sessionId: args.sessionId, + ...(args.modelId !== undefined ? { modelId: args.modelId } : {}), + ...(args.reasoningEffort !== undefined ? { reasoningEffort: args.reasoningEffort } : {}), + }); +} + +export async function navigateDesktop(connection: AdeCodeConnection, request: NavigateRequest): Promise { + return await connection.request("app/navigate", request); +} + +export function newestSession(sessions: AgentChatSessionSummary[]): AgentChatSessionSummary | null { + return [...sessions].sort((left, right) => ( + new Date(right.lastActivityAt ?? right.startedAt).getTime() + - new Date(left.lastActivityAt ?? left.startedAt).getTime() + ))[0] ?? null; +} + +export type TokenStats = { + percent: number | null; + streaming: boolean; + inputTokens: number | null; + outputTokens: number | null; + costUsd: number | null; +}; + +export function latestTokenStats(events: AgentChatEventEnvelope[]): TokenStats { + let percent: number | null = null; + let streaming = false; + let inputTokens: number | null = null; + let outputTokens: number | null = null; + let costUsd: number | null = null; + for (const envelope of events) { + const event = envelope.event as Record; + if (event.type === "status" && event.turnStatus === "started") streaming = true; + if (event.type === "done" || (event.type === "status" && event.turnStatus === "completed")) streaming = false; + if (event.type === "tokens") { + inputTokens = typeof event.inputTokens === "number" ? event.inputTokens : inputTokens; + outputTokens = typeof event.outputTokens === "number" ? event.outputTokens : outputTokens; + const used = typeof event.totalTokens === "number" + ? event.totalTokens + : inputTokens != null || outputTokens != null + ? (inputTokens ?? 0) + (outputTokens ?? 0) + : null; + const limit = typeof event.contextWindow === "number" ? event.contextWindow : null; + if (used != null && limit != null && limit > 0) { + percent = Math.max(0, Math.min(100, Math.round((used / limit) * 100))); + } + } + if (event.type === "done") { + const usage = event.usage && typeof event.usage === "object" ? event.usage as Record : null; + inputTokens = typeof usage?.inputTokens === "number" ? usage.inputTokens : inputTokens; + outputTokens = typeof usage?.outputTokens === "number" ? usage.outputTokens : outputTokens; + costUsd = typeof event.costUsd === "number" ? event.costUsd : costUsd; + } + } + return { percent, streaming, inputTokens, outputTokens, costUsd }; +} diff --git a/apps/ade-code/src/app.tsx b/apps/ade-code/src/app.tsx new file mode 100644 index 00000000..b23bd847 --- /dev/null +++ b/apps/ade-code/src/app.tsx @@ -0,0 +1,1542 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { spawn } from "node:child_process"; +import path from "node:path"; +import { Box, Text, useApp, useInput } from "ink"; +import { getDefaultModelDescriptor } from "../../desktop/src/shared/modelRegistry"; +import type { + AgentChatEventEnvelope, + AgentChatFileRef, + AgentChatModelInfo, + AgentChatSessionSummary, + AgentChatSlashCommand, +} from "../../desktop/src/shared/types/chat"; +import type { LaneSummary } from "../../desktop/src/shared/types/lanes"; +import { + approveToolUse, + createChatSession, + getAvailableModels, + getChatHistory, + getSlashCommands, + interruptChat, + latestTokenStats, + listChatSessions, + listLanes, + navigateDesktop, + newestSession, + renameChat, + respondToInput, + resumeChat, + sendChatMessage, + updateChatModel, +} from "./adeApi"; +import { paletteCommands, parseCommand } from "./commands"; +import { connectToAde } from "./connection"; +import { Drawer } from "./components/Drawer"; +import { ChatView } from "./components/ChatView"; +import { Header } from "./components/Header"; +import { RightPane } from "./components/RightPane"; +import { SlashPalette } from "./components/SlashPalette"; +import { MentionPalette } from "./components/MentionPalette"; +import { ApprovalPrompt } from "./components/ApprovalPrompt"; +import { chooseInitialLane } from "./project"; +import { latestExpandableFailureId, renderObject, summarizeDiffChanges } from "./format"; +import { startTuiHeartbeat, type TuiHeartbeat } from "./heartbeat"; +import { buildLinearToolRequest } from "./linearCommands"; +import { buildPendingInputAnswers, latestPendingApproval } from "./pendingInput"; +import type { + AdeCodeConnection, + AdeCodeModelState, + LocalNotice, + MentionSuggestion, + PendingApproval, + ProjectLaunchContext, + RightPaneContent, + RuntimeMode, +} from "./types"; + +const PURPLE = "#A78BFA"; +const EFFORTS = ["low", "medium", "high", "xhigh"]; +const PROVIDERS = new Set(["codex", "claude", "opencode", "cursor", "droid"]); +const DESKTOP_COMMAND_ROUTES: Record = { + "/app-control": "/app-control", + "/browser": "/browser", + "/computer": "/proof", + "/computer-use": "/proof", + "/ios": "/ios-sim", + "/ios-sim": "/ios-sim", + "/macos-vm": "/macos-vm", + "/mission": "/missions", + "/missions": "/missions", + "/pencil": "/pencil", + "/proof": "/proof", +}; + +type AdeCodeAppProps = { + project: ProjectLaunchContext; + forceEmbedded?: boolean; + requireSocket?: boolean; + socketPath?: string | null; +}; + +function initialModelState(): AdeCodeModelState { + const descriptor = getDefaultModelDescriptor("codex"); + return { + provider: "codex", + model: descriptor?.providerModelId ?? "gpt-5.5", + modelId: descriptor?.id ?? null, + displayName: descriptor?.displayName ?? "GPT-5.5", + reasoningEffort: "medium", + }; +} + +function noticeId(): string { + return `${Date.now()}:${Math.random().toString(36).slice(2)}`; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function routeRows(value: unknown): string[] { + if (Array.isArray(value)) return value.slice(0, 16).map((entry) => { + const record = entry && typeof entry === "object" ? entry as Record : {}; + return String(record.title ?? record.name ?? record.branchRef ?? record.id ?? JSON.stringify(entry)).slice(0, 90); + }); + const record = value && typeof value === "object" ? value as Record : {}; + const list = Object.values(record).find(Array.isArray); + return Array.isArray(list) ? routeRows(list) : renderObject(value, 12).split(/\r?\n/); +} + +function compactNumber(value: number): string { + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}m`; + if (value >= 1_000) return `${(value / 1_000).toFixed(1)}k`; + return String(value); +} + +function formatTokenSummary(stats: ReturnType): string | null { + const parts: string[] = []; + if (stats.inputTokens != null) parts.push(`in ${compactNumber(stats.inputTokens)}`); + if (stats.outputTokens != null) parts.push(`out ${compactNumber(stats.outputTokens)}`); + if (stats.costUsd != null) parts.push(`$${stats.costUsd.toFixed(2)}`); + return parts.length ? parts.join(" · ") : null; +} + +function desktopRouteForCommand(commandName: string | null | undefined): string | null { + if (!commandName) return null; + return DESKTOP_COMMAND_ROUTES[commandName] ?? null; +} + +function splitFirstArg(input: string): { first: string; rest: string } { + const trimmed = input.trim(); + const match = trimmed.match(/^(\S+)(?:\s+([\s\S]*))?$/); + return { + first: match?.[1] ?? "", + rest: match?.[2]?.trim() ?? "", + }; +} + +function parseAdeActionArgs(input: string): Record { + const trimmed = input.trim(); + if (!trimmed) return {}; + const parsed = JSON.parse(trimmed) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("/ade action arguments must be a JSON object."); + } + return parsed as Record; +} + +function printableInput(input: string): string { + return input.replace(/[\u0000-\u001f\u007f]/g, ""); +} + +function inputBeforeLineBreak(input: string): string | null { + const index = input.search(/[\r\n]/); + return index === -1 ? null : input.slice(0, index); +} + +function activeMention(value: string): { start: number; query: string } | null { + const match = value.match(/(^|\s)@([^\s@]*)$/); + if (!match || match.index == null) return null; + return { + start: match.index + match[1].length, + query: match[2] ?? "", + }; +} + +function useTerminalDimensions(): [number, number] { + const read = (): [number, number] => [ + process.stdout.columns ?? 120, + process.stdout.rows ?? 40, + ]; + const [dimensions, setDimensions] = useState<[number, number]>(read); + useEffect(() => { + const handleResize = () => setDimensions(read()); + process.stdout.on("resize", handleResize); + return () => { + process.stdout.off("resize", handleResize); + }; + }, []); + return dimensions; +} + +export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath }: AdeCodeAppProps) { + const { exit } = useApp(); + const [columns, rows] = useTerminalDimensions(); + const [connection, setConnection] = useState(null); + const [mode, setMode] = useState("connecting"); + const [lanes, setLanes] = useState([]); + const [sessions, setSessions] = useState([]); + const [activeLaneId, setActiveLaneId] = useState(null); + const [activeSessionId, setActiveSessionId] = useState(null); + const [events, setEvents] = useState([]); + const [notices, setNotices] = useState([]); + const [slashCommands, setSlashCommands] = useState([]); + const [models, setModels] = useState([]); + const [modelState, setModelState] = useState(initialModelState); + const [rightPane, setRightPane] = useState({ kind: "empty" }); + const [formValues, setFormValues] = useState>({}); + const [formFieldIndex, setFormFieldIndex] = useState(0); + const [rightSelectionIndex, setRightSelectionIndex] = useState(0); + const [drawerOpen, setDrawerOpen] = useState(false); + const [rightOpen, setRightOpen] = useState(false); + const [prompt, setPrompt] = useState(""); + const [error, setError] = useState(null); + const [tuiCount, setTuiCount] = useState(1); + const [contextPercent, setContextPercent] = useState(null); + const [tokenSummary, setTokenSummary] = useState(null); + const [streaming, setStreaming] = useState(false); + const [desktopDriving, setDesktopDriving] = useState(false); + const [clearedAt, setClearedAt] = useState(null); + const [expandedLineIds, setExpandedLineIds] = useState>(() => new Set()); + const [mentionSuggestions, setMentionSuggestions] = useState([]); + const [mentionIndex, setMentionIndex] = useState(0); + const [selectedMentions, setSelectedMentions] = useState([]); + const [slashIndex, setSlashIndex] = useState(0); + const [drawerSection, setDrawerSection] = useState<"lanes" | "chats">("lanes"); + const [drawerLaneId, setDrawerLaneId] = useState(null); + const [selectedDrawerLaneId, setSelectedDrawerLaneId] = useState(null); + const [selectedDrawerChatId, setSelectedDrawerChatId] = useState(null); + + const connectionRef = useRef(null); + const activeLaneIdRef = useRef(null); + const activeSessionIdRef = useRef(null); + const lastLocalSendAtRef = useRef(0); + const eventCountRef = useRef(0); + const heartbeatRef = useRef(null); + + const projectName = path.basename(project.projectRoot); + const activeLane = useMemo( + () => lanes.find((lane) => lane.id === activeLaneId) ?? null, + [activeLaneId, lanes], + ); + const activeSession = useMemo( + () => sessions.find((session) => session.sessionId === activeSessionId) ?? null, + [activeSessionId, sessions], + ); + const latestFailedLineId = useMemo(() => latestExpandableFailureId(events), [events]); + const drawerLaneRows = useMemo(() => lanes.slice(0, 10), [lanes]); + const drawerLaneSessions = useMemo( + () => sessions.filter((session) => session.laneId === drawerLaneId), + [drawerLaneId, sessions], + ); + const selectedLaneIndex = useMemo(() => { + const targetId = selectedDrawerLaneId ?? drawerLaneId ?? activeLaneId; + const index = drawerLaneRows.findIndex((lane) => lane.id === targetId); + return index >= 0 ? index : 0; + }, [activeLaneId, drawerLaneId, drawerLaneRows, selectedDrawerLaneId]); + const selectedChatIndex = useMemo(() => { + const targetId = selectedDrawerChatId + ?? (drawerLaneId === activeLaneId ? activeSessionId : null); + const index = drawerLaneSessions.findIndex((session) => session.sessionId === targetId); + return index >= 0 ? index : 0; + }, [activeLaneId, activeSessionId, drawerLaneId, drawerLaneSessions, selectedDrawerChatId]); + const activeMentionRange = useMemo(() => activeMention(prompt), [prompt]); + const slashRows = useMemo(() => ( + prompt.startsWith("/") ? paletteCommands(prompt, slashCommands) : [] + ), [prompt, slashCommands]); + const pendingApproval = useMemo(() => latestPendingApproval(events), [events]); + const activeFormField = rightPane.kind === "form" + ? rightPane.fields[formFieldIndex] ?? rightPane.fields[0] ?? null + : null; + + useEffect(() => { + activeLaneIdRef.current = activeLaneId; + }, [activeLaneId]); + + useEffect(() => { + activeSessionIdRef.current = activeSessionId; + }, [activeSessionId]); + + useEffect(() => { + if (!drawerLaneId || !lanes.some((lane) => lane.id === drawerLaneId)) { + setDrawerLaneId(activeLaneId); + } + }, [activeLaneId, drawerLaneId, lanes]); + + useEffect(() => { + if (selectedDrawerLaneId && drawerLaneRows.some((lane) => lane.id === selectedDrawerLaneId)) return; + setSelectedDrawerLaneId(drawerLaneId ?? activeLaneId ?? drawerLaneRows[0]?.id ?? null); + }, [activeLaneId, drawerLaneId, drawerLaneRows, selectedDrawerLaneId]); + + useEffect(() => { + if (selectedDrawerChatId && drawerLaneSessions.some((session) => session.sessionId === selectedDrawerChatId)) return; + const activeChatInDrawer = drawerLaneSessions.find((session) => session.sessionId === activeSessionId); + setSelectedDrawerChatId(activeChatInDrawer?.sessionId ?? drawerLaneSessions[0]?.sessionId ?? null); + }, [activeSessionId, drawerLaneSessions, selectedDrawerChatId]); + + useEffect(() => { + setSlashIndex(0); + }, [prompt]); + + const addNotice = useCallback((text: string, tone: LocalNotice["tone"] = "info") => { + setNotices((prev) => [ + ...prev.slice(-10), + { id: noticeId(), timestamp: new Date().toISOString(), text, tone }, + ]); + }, []); + + const openForm = useCallback((content: Extract) => { + const nextValues = Object.fromEntries(content.fields.map((field) => [field.name, field.initialValue ?? ""])); + setFormValues(nextValues); + setFormFieldIndex(0); + setPrompt(content.fields[0]?.initialValue ?? ""); + setRightPane(content); + setRightOpen(true); + }, []); + + useEffect(() => { + const range = activeMentionRange; + const conn = connectionRef.current; + const laneId = activeLaneIdRef.current; + if (!range) { + setMentionSuggestions([]); + setMentionIndex(0); + return; + } + let cancelled = false; + const query = range.query.toLowerCase(); + const localSuggestions: MentionSuggestion[] = [ + ...lanes.map((lane) => ({ + kind: "lane" as const, + label: lane.name, + insertText: `@lane:${lane.id}`, + detail: lane.branchRef ?? lane.id, + })), + ...sessions.slice(0, 30).map((session) => ({ + kind: "chat" as const, + label: session.title ?? session.sessionId, + insertText: `@chat:${session.sessionId}`, + detail: session.laneId, + })), + ].filter((suggestion) => ( + !query + || suggestion.label.toLowerCase().includes(query) + || suggestion.insertText.toLowerCase().includes(query) + || suggestion.detail?.toLowerCase().includes(query) + )); + + const loadRemoteSuggestions = async () => { + const remote: MentionSuggestion[] = []; + if (conn && laneId) { + const [files, commits, prs] = await Promise.all([ + query + ? conn.action>("file", "quickOpen", { + workspaceId: laneId, + query, + limit: 5, + }).catch(() => []) + : Promise.resolve([]), + conn.action>>("git", "listRecentCommits", { + laneId, + limit: 8, + }).catch(() => []), + conn.action>>("pr", "listAll", { laneId }).catch(() => []), + ]); + remote.push(...files.map((file) => ({ + kind: "file" as const, + label: file.path, + insertText: `@file:${file.path}`, + detail: "file", + filePath: file.path, + }))); + remote.push(...commits + .filter((commit) => { + const subject = String(commit.subject ?? commit.message ?? ""); + const sha = String(commit.shortSha ?? commit.sha ?? ""); + return !query || subject.toLowerCase().includes(query) || sha.toLowerCase().includes(query); + }) + .slice(0, 5) + .map((commit) => { + const sha = String(commit.shortSha ?? commit.sha ?? "commit"); + return { + kind: "commit" as const, + label: String(commit.subject ?? commit.message ?? sha), + insertText: `@commit:${sha}`, + detail: sha, + }; + })); + remote.push(...prs + .filter((pr) => { + const title = String(pr.title ?? ""); + const number = String(pr.number ?? pr.prNumber ?? ""); + return !query || title.toLowerCase().includes(query) || number.includes(query); + }) + .slice(0, 5) + .map((pr) => { + const id = String(pr.id ?? pr.prId ?? pr.number ?? "pr"); + return { + kind: "pr" as const, + label: String(pr.title ?? `PR ${id}`), + insertText: `@pr:${id}`, + detail: pr.number != null ? `#${String(pr.number)}` : id, + }; + })); + } + if (cancelled) return; + const next = [...localSuggestions, ...remote].slice(0, 10); + setMentionSuggestions(next); + setMentionIndex((index) => Math.min(index, Math.max(0, next.length - 1))); + }; + void loadRemoteSuggestions(); + return () => { + cancelled = true; + }; + }, [activeMentionRange, lanes, sessions]); + + const refreshState = useCallback(async () => { + const conn = connectionRef.current; + if (!conn) return; + const nextLanes = await listLanes(conn); + const nextLane = nextLanes.find((lane) => lane.id === activeLaneIdRef.current) + ?? chooseInitialLane(nextLanes, project); + const nextLaneId = nextLane?.id ?? null; + const nextSessions = await listChatSessions(conn); + const laneSessions = nextSessions.filter((session) => session.laneId === nextLaneId); + const nextSession = nextSessions.find((session) => session.sessionId === activeSessionIdRef.current) + ?? newestSession(laneSessions); + const nextSessionId = nextSession?.sessionId ?? null; + let nextEvents: AgentChatEventEnvelope[] = []; + if (nextSessionId) { + const history = await getChatHistory(conn, nextSessionId); + nextEvents = clearedAt + ? history.events.filter((event) => event.timestamp > clearedAt) + : history.events; + const stats = latestTokenStats(history.events); + setContextPercent(stats.percent); + setTokenSummary(formatTokenSummary(stats)); + setStreaming(stats.streaming || nextSession?.status === "active"); + const previousCount = eventCountRef.current; + eventCountRef.current = history.events.length; + if (previousCount > 0 && history.events.length > previousCount && Date.now() - lastLocalSendAtRef.current > 4_000) { + setDesktopDriving(true); + setTimeout(() => setDesktopDriving(false), 3_000); + } + } + const nextProvider = nextSession?.provider ?? "codex"; + const nextCommands = await getSlashCommands(conn, nextSessionId).catch(() => []); + const nextModels = await getAvailableModels(conn, nextProvider).catch(() => []); + const activeModel = nextModels.find((model) => model.modelId === nextSession?.modelId || model.id === nextSession?.modelId) + ?? nextModels.find((model) => model.isDefault) + ?? null; + setLanes(nextLanes); + setSessions(nextSessions); + setActiveLaneId(nextLaneId); + setActiveSessionId(nextSessionId); + setEvents(nextEvents); + setSlashCommands(nextCommands); + setModels(nextModels); + setTuiCount(heartbeatRef.current?.readCount() ?? 1); + setModelState({ + provider: PROVIDERS.has(nextProvider) ? nextProvider as AdeCodeModelState["provider"] : "codex", + model: nextSession?.model ?? activeModel?.id ?? modelState.model, + modelId: nextSession?.modelId ?? activeModel?.modelId ?? activeModel?.id ?? modelState.modelId, + displayName: activeModel?.displayName ?? nextSession?.model ?? modelState.displayName, + reasoningEffort: nextSession?.reasoningEffort ?? modelState.reasoningEffort, + }); + }, [clearedAt, modelState.displayName, modelState.model, modelState.modelId, modelState.reasoningEffort, project]); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + const conn = await connectToAde({ project, forceEmbedded, requireSocket, socketPath }); + if (cancelled) { + await conn.close(); + return; + } + heartbeatRef.current = startTuiHeartbeat(project.projectRoot); + connectionRef.current = conn; + setConnection(conn); + setMode(conn.mode); + await refreshState(); + } catch (err) { + heartbeatRef.current?.stop(); + heartbeatRef.current = null; + setError(err instanceof Error ? err.message : String(err)); + } + })(); + return () => { + cancelled = true; + heartbeatRef.current?.stop(); + heartbeatRef.current = null; + const conn = connectionRef.current; + connectionRef.current = null; + void conn?.close().catch(() => {}); + }; + }, [forceEmbedded, project, requireSocket, socketPath]); + + useEffect(() => { + if (!connection) return; + return connection.onChatEvent((envelope) => { + if (envelope.sessionId !== activeSessionIdRef.current) { + void refreshState().catch(() => undefined); + return; + } + if (clearedAt && envelope.timestamp <= clearedAt) return; + setEvents((prev) => { + const key = `${envelope.sequence ?? ""}:${envelope.timestamp}:${envelope.event.type}`; + if (prev.some((entry) => `${entry.sequence ?? ""}:${entry.timestamp}:${entry.event.type}` === key)) return prev; + return [...prev, envelope].slice(-500); + }); + const event = envelope.event as Record; + if (event.type === "status" && event.turnStatus === "started") setStreaming(true); + if (event.type === "done" || (event.type === "status" && event.turnStatus === "completed")) setStreaming(false); + if (Date.now() - lastLocalSendAtRef.current > 4_000) { + setDesktopDriving(true); + setTimeout(() => setDesktopDriving(false), 3_000); + } + }); + }, [clearedAt, connection, refreshState]); + + useEffect(() => { + if (!connection) return; + const timer = setInterval(() => { + void refreshState().catch((err) => { + setError(err instanceof Error ? err.message : String(err)); + }); + }, 1_000); + return () => clearInterval(timer); + }, [connection, refreshState]); + + const ensureActiveSession = useCallback(async (): Promise => { + const conn = connectionRef.current; + const laneId = activeLaneIdRef.current; + if (!conn || !laneId) return null; + if (activeSessionIdRef.current) return activeSessionIdRef.current; + const created = await createChatSession({ connection: conn, laneId }); + setActiveSessionId(created.id); + await refreshState(); + return created.id; + }, [refreshState]); + + const resolvePendingApproval = useCallback(async ( + approval: PendingApproval, + decision: "accept" | "decline" | "cancel" | "accept_for_session", + responseText?: string | null, + ) => { + const conn = connectionRef.current; + const sessionId = activeSessionIdRef.current; + if (!conn || !sessionId) return; + await approveToolUse({ + connection: conn, + sessionId, + itemId: approval.itemId, + decision, + responseText, + }); + addNotice(decision === "accept" || decision === "accept_for_session" ? "Approved request." : "Declined request.", "info"); + await refreshState(); + }, [addNotice, refreshState]); + + const answerPendingInput = useCallback(async (approval: PendingApproval, text: string) => { + const conn = connectionRef.current; + const sessionId = activeSessionIdRef.current; + if (!conn || !sessionId) return; + const trimmed = text.trim(); + const lowered = trimmed.toLowerCase(); + if (lowered === "deny" || lowered === "decline" || lowered === "cancel") { + await respondToInput({ + connection: conn, + sessionId, + itemId: approval.itemId, + decision: lowered === "cancel" ? "cancel" : "decline", + }); + addNotice("Declined request.", "info"); + await refreshState(); + return; + } + await respondToInput({ + connection: conn, + sessionId, + itemId: approval.itemId, + decision: "accept", + answers: buildPendingInputAnswers(approval.request, trimmed), + responseText: trimmed, + }); + addNotice("Answered request.", "success"); + await refreshState(); + }, [addNotice, refreshState]); + + const runRightCommand = useCallback(async (name: string, args: string) => { + const conn = connectionRef.current; + if (!conn) return; + const laneId = activeLaneIdRef.current; + const sessionId = activeSessionIdRef.current; + setRightOpen(true); + + if (name === "/help") { + setRightPane({ kind: "help", title: "Help" }); + return; + } + if (name === "/status") { + setRightPane({ + kind: "status", + rows: [ + ["project", project.projectRoot], + ["workspace", project.workspaceRoot], + ["lane", activeLane?.name ?? laneId ?? "none"], + ["chat", activeSession?.title ?? activeSession?.sessionId ?? "none"], + ["runtime", mode], + ["socket", conn.socketPath ?? "embedded"], + ], + }); + return; + } + if (name === "/new chat") { + if (!laneId) { + setRightPane({ kind: "details", title: "New chat", body: "No active lane is available." }); + return; + } + if (!args) { + openForm({ + kind: "form", + title: "New chat", + command: "new-chat", + fields: [ + { name: "title", label: "Title", placeholder: "Untitled chat" }, + { name: "message", label: "First message", placeholder: "Optional" }, + ], + }); + return; + } + const created = await createChatSession({ connection: conn, laneId, title: args }); + setActiveSessionId(created.id); + addNotice(`Created chat "${args}".`, "success"); + await refreshState(); + return; + } + if (name === "/new lane") { + if (!args) { + openForm({ + kind: "form", + title: "New lane", + command: "new-lane", + fields: [ + { name: "name", label: "Name", required: true, placeholder: "feature-name" }, + { name: "baseBranch", label: "Base branch", placeholder: "default" }, + ], + }); + return; + } + const created = await conn.action("lane", "create", { name: args }); + setActiveLaneId(created.id); + setRightPane({ kind: "details", title: "New lane", body: renderObject(created, 20) }); + await refreshState(); + return; + } + if (name === "/rename") { + if (!sessionId) { + setRightPane({ kind: "details", title: "Rename chat", body: "No active chat is selected." }); + return; + } + if (!args) { + openForm({ + kind: "form", + title: "Rename chat", + command: "rename", + fields: [ + { name: "title", label: "Title", required: true, initialValue: activeSession?.title ?? "" }, + ], + }); + return; + } + await renameChat(conn, sessionId, args); + addNotice(`Renamed chat to "${args}".`, "success"); + await refreshState(); + return; + } + if (name === "/diff") { + if (!laneId) { + setRightPane({ kind: "details", title: "Diff", body: "No active lane is selected." }); + return; + } + const diff = await conn.action("diff", "getChanges", { laneId }); + setRightPane({ kind: "diff", title: "Diff", files: summarizeDiffChanges(diff) }); + return; + } + if (name === "/log") { + if (!laneId) { + setRightPane({ kind: "details", title: "Recent commits", body: "No active lane is selected." }); + return; + } + const log = await conn.action("git", "listRecentCommits", { laneId, limit: 12 }); + setRightPane({ kind: "list", title: "Recent commits", rows: routeRows(log), emptyText: "No commits." }); + return; + } + if (name.startsWith("/pr")) { + const prs = await conn.action>>("pr", "listAll", laneId ? { laneId } : {}); + const activePr = prs[0] ?? null; + const prId = activePr ? String(activePr.id ?? activePr.prId ?? "") : ""; + if (name === "/pr") { + const ahead = activeLane?.status?.ahead ?? 0; + setRightPane({ + kind: "details", + title: "PR", + body: activePr + ? renderObject(activePr, 24) + : `No PR is linked to this lane yet.\n${ahead > 0 ? `${ahead} commit${ahead === 1 ? "" : "s"} ahead of base.\n` : ""}Run /pr open to create a draft.`, + }); + return; + } + if (name === "/pr open") { + if (activePr) { + await navigateDesktop(conn, { + source: "ade-code", + target: { + kind: "pr", + prId, + laneId, + prNumber: typeof activePr.number === "number" ? activePr.number : null, + }, + }); + setRightPane({ kind: "details", title: "PR open", body: renderObject(activePr, 24) }); + return; + } + if (!laneId) { + setRightPane({ kind: "details", title: "PR open", body: "No active lane is selected." }); + return; + } + if (!args) { + openForm({ + kind: "form", + title: "Open PR", + command: "pr-open", + fields: [ + { name: "title", label: "Title", required: true, placeholder: activeLane?.name ?? "Draft PR" }, + { name: "body", label: "Body", placeholder: "Optional" }, + ], + }); + return; + } + const created = await conn.action("pr", "createFromLane", { + laneId, + title: args, + body: "", + draft: true, + }); + setRightPane({ kind: "details", title: "PR open", body: renderObject(created, 24) }); + return; + } + if (!prId) { + setRightPane({ kind: "details", title: name.slice(1), body: "No PR is linked to this lane yet." }); + return; + } + const pr = name === "/pr checks" + ? await conn.actionList("pr", "getChecks", [prId]).catch((err) => ({ error: err instanceof Error ? err.message : String(err) })) + : await Promise.all([ + conn.actionList("pr", "getReviews", [prId]).catch((err) => ({ error: err instanceof Error ? err.message : String(err) })), + conn.actionList("pr", "getReviewThreads", [prId]).catch((err) => ({ error: err instanceof Error ? err.message : String(err) })), + ]).then(([reviews, threads]) => ({ reviews, threads })); + setRightPane({ kind: "details", title: name.slice(1), body: renderObject(pr, 24) }); + return; + } + if (name === "/linear list") { + const linear = await conn.action("linear_issue_tracker", "listIssues", { limit: 20 }); + setRightPane({ kind: "list", title: "Linear", rows: routeRows(linear), emptyText: "No Linear issues." }); + return; + } + if (name === "/linear status") { + const status = await conn.action("linear_issue_tracker", "getStatus", {}); + setRightPane({ kind: "details", title: "Linear status", body: renderObject(status, 24) }); + return; + } + if (name === "/linear pull") { + if (!args) { + setRightPane({ kind: "details", title: "Linear pull", body: "Usage: /linear pull <issue-id>" }); + return; + } + const issue = await conn.actionList("linear_issue_tracker", "fetchIssueById", [args]); + if (!issue) { + setRightPane({ kind: "details", title: "Linear pull", body: `Linear issue ${args} was not found.` }); + return; + } + const targetSessionId = await ensureActiveSession(); + const issueContext = `Linear issue context:\n${renderObject(issue, 28)}`; + if (targetSessionId) { + await sendChatMessage(conn, targetSessionId, issueContext); + } + setRightPane({ kind: "details", title: "Linear pull", body: issueContext }); + return; + } + if (name === "/linear comment") { + const parsed = splitFirstArg(args); + if (!parsed.first || !parsed.rest) { + setRightPane({ kind: "details", title: "Linear comment", body: "Usage: /linear comment <issue-id> <text>" }); + return; + } + const result = await conn.actionList("linear_issue_tracker", "createComment", [parsed.first, parsed.rest]); + setRightPane({ kind: "details", title: "Linear comment", body: renderObject(result, 12) }); + addNotice(`Commented on ${parsed.first}.`, "success"); + return; + } + if (name === "/linear assign") { + const parsed = splitFirstArg(args); + if (!parsed.first || !parsed.rest) { + setRightPane({ kind: "details", title: "Linear assign", body: "Usage: /linear assign <issue-id> <user-id|none>" }); + return; + } + const normalizedAssignee = parsed.rest.toLowerCase(); + const assigneeId = normalizedAssignee === "none" || normalizedAssignee === "null" || normalizedAssignee === "unassigned" + ? null + : parsed.rest; + await conn.actionList("linear_issue_tracker", "updateIssueAssignee", [parsed.first, assigneeId]); + setRightPane({ + kind: "details", + title: "Linear assign", + body: assigneeId ? `Assigned ${parsed.first} to ${assigneeId}.` : `Cleared assignee for ${parsed.first}.`, + }); + addNotice(`Updated ${parsed.first}.`, "success"); + return; + } + if (name === "/linear" || name.startsWith("/linear ")) { + const linearInput = `${name.slice("/linear".length)} ${args}`.trim(); + const request = buildLinearToolRequest(linearInput); + if (request.kind === "usage") { + setRightPane({ kind: "details", title: request.title, body: request.body }); + return; + } + const result = await conn.tool(request.toolName, request.args); + setRightPane({ kind: "details", title: request.title, body: renderObject(result, 24) }); + return; + } + if (name === "/memory") { + const query = args || "project"; + const result = await conn.tool("memory_search", { query, scope: "project", limit: 10 }); + setRightPane({ kind: "details", title: "Memory", body: renderObject(result, 24) }); + return; + } + if (name === "/forget") { + setRightPane({ kind: "details", title: "Forget", body: "Memory lifecycle controls are available in desktop. Run /open to continue there." }); + return; + } + if (name === "/chats") { + const laneSessions = sessions.filter((session) => session.laneId === laneId); + const selectedIndex = Math.max(0, laneSessions.findIndex((session) => session.sessionId === sessionId)); + setRightSelectionIndex(selectedIndex); + setRightPane({ + kind: "list", + title: "Chats", + rows: laneSessions.map((session) => `${session.sessionId === sessionId ? "●" : "○"} ${session.title ?? session.sessionId}`), + emptyText: "No chats in this lane.", + action: { kind: "switch-chat", ids: laneSessions.map((session) => session.sessionId) }, + }); + return; + } + if (name === "/switch") { + const query = args.toLowerCase(); + if (!query) { + const selectedIndex = Math.max(0, lanes.findIndex((lane) => lane.id === laneId)); + setRightSelectionIndex(selectedIndex); + setRightPane({ + kind: "list", + title: "Switch", + rows: lanes.map((lane) => `${lane.id === laneId ? "●" : "○"} ${lane.name}`), + emptyText: "No lanes.", + action: { kind: "switch-lane", ids: lanes.map((lane) => lane.id) }, + }); + return; + } + const lane = lanes.find((entry) => entry.id.toLowerCase() === query || entry.name.toLowerCase().includes(query)); + if (lane) { + setActiveLaneId(lane.id); + setDrawerLaneId(lane.id); + setSelectedDrawerLaneId(lane.id); + const session = newestSession(sessions.filter((entry) => entry.laneId === lane.id)); + setActiveSessionId(session?.sessionId ?? null); + setSelectedDrawerChatId(session?.sessionId ?? null); + addNotice(`Switched to lane ${lane.name}.`, "success"); + } else { + setRightPane({ kind: "details", title: "Switch", body: `No lane matched "${args}".` }); + } + return; + } + if (name === "/resume") { + if (!sessionId) { + setRightPane({ kind: "details", title: "Resume", body: "No active chat is selected." }); + return; + } + await resumeChat(conn, sessionId); + addNotice("Resumed chat.", "success"); + await refreshState(); + return; + } + if (name === "/model") { + if (args && sessionId) { + await updateChatModel({ connection: conn, sessionId, modelId: args }); + addNotice(`Model set to ${args}.`, "success"); + await refreshState(); + return; + } + setRightSelectionIndex(Math.max(0, models.findIndex((model) => ( + model.id === modelState.modelId || model.modelId === modelState.modelId + )))); + setRightPane({ kind: "models", models, activeModelId: modelState.modelId }); + return; + } + if (name === "/effort") { + if (args && sessionId) { + await updateChatModel({ connection: conn, sessionId, reasoningEffort: args }); + addNotice(`Effort set to ${args}.`, "success"); + await refreshState(); + return; + } + setRightSelectionIndex(Math.max(0, EFFORTS.findIndex((effort) => effort === modelState.reasoningEffort))); + setRightPane({ kind: "effort", efforts: EFFORTS, activeEffort: modelState.reasoningEffort }); + return; + } + if (name === "/system") { + setRightPane({ + kind: "details", + title: "System", + body: renderObject({ mode, project, socketPath: conn.socketPath, pid: process.pid }, 24), + }); + return; + } + if (name === "/ade") { + const parsed = splitFirstArg(args); + const [domain, action] = parsed.first.split(".", 2); + if (!domain || !action) { + setRightPane({ + kind: "details", + title: "ADE action", + body: "Usage: /ade <domain.action> [json-object-args]", + }); + return; + } + const result = await conn.action(domain, action, parseAdeActionArgs(parsed.rest)); + setRightPane({ kind: "details", title: `ADE ${domain}.${action}`, body: renderObject(result, 24) }); + } + }, [activeLane?.name, activeSession?.sessionId, activeSession?.title, addNotice, ensureActiveSession, lanes, mode, modelState.modelId, modelState.reasoningEffort, models, openForm, project, refreshState, sessions]); + + const runInlineCommand = useCallback(async (name: string, args: string) => { + const conn = connectionRef.current; + if (!conn) return; + const laneId = activeLaneIdRef.current; + const sessionId = activeSessionIdRef.current; + if (name === "/quit") { + exit(); + return; + } + if (name === "/clear") { + setClearedAt(new Date().toISOString()); + setEvents([]); + addNotice("Local transcript view cleared. The durable chat remains in ADE.", "info"); + return; + } + if (name === "/end") { + if (!sessionId) { + addNotice("No active chat is selected.", "error"); + return; + } + await conn.action("chat", "dispose", { sessionId }); + addNotice("Ended active chat runtime.", "success"); + await refreshState(); + return; + } + if (name === "/commit") { + if (!laneId) { + addNotice("No active lane is selected.", "error"); + return; + } + if (!args) { + addNotice("Usage: /commit <message>", "error"); + return; + } + const result = await conn.action("git", "commit", { laneId, message: args }); + addNotice(`Commit complete: ${renderObject(result, 4).replace(/\n/g, " ")}`, "success"); + return; + } + if (name === "/push") { + if (!laneId) { + addNotice("No active lane is selected.", "error"); + return; + } + const result = await conn.action("git", "push", { laneId }); + addNotice(`Push complete: ${renderObject(result, 4).replace(/\n/g, " ")}`, "success"); + return; + } + if (name === "/remember") { + if (!args) { + addNotice("Usage: /remember <durable fact>", "error"); + return; + } + const result = await conn.tool("memory_add", { + content: args, + scope: "project", + category: "decision", + importance: "medium", + }); + addNotice(`Memory saved: ${renderObject(result, 4).replace(/\n/g, " ")}`, "success"); + return; + } + if (name === "/open") { + const target = sessionId + ? { kind: "chat" as const, sessionId, laneId } + : laneId + ? { kind: "lane" as const, laneId } + : { kind: "work" as const }; + const result = await navigateDesktop(conn, { source: "ade-code", target }); + if (result.ok) { + addNotice("Opened ADE desktop at this context.", "success"); + return; + } + if (process.platform === "darwin") { + spawn("open", [ + "-a", + "ADE", + "--env", + `ADE_PROJECT_ROOT=${project.projectRoot}`, + project.projectRoot, + ], { stdio: "ignore", detached: true }).unref(); + addNotice(result.message ?? "Desktop route unavailable; launched ADE.", "info"); + for (let attempt = 0; attempt < 8; attempt += 1) { + await delay(750); + const attached = await connectToAde({ project, forceEmbedded: false, socketPath }).catch(() => null); + if (!attached || attached.mode !== "attached") { + await attached?.close().catch(() => {}); + continue; + } + const retry = await navigateDesktop(attached, { source: "ade-code", target }).catch(() => null); + if (!retry?.ok) { + await attached.close().catch(() => {}); + continue; + } + const previous = connectionRef.current; + connectionRef.current = attached; + setConnection(attached); + setMode(attached.mode); + await previous?.close().catch(() => {}); + addNotice("Attached to desktop and opened this context.", "success"); + await refreshState(); + return; + } + } else { + addNotice(result.message ?? "Desktop route unavailable from this runtime.", "error"); + } + } + }, [addNotice, exit, project, refreshState, socketPath]); + + const submitRightForm = useCallback(async ( + form: Extract<RightPaneContent, { kind: "form" }>, + values: Record<string, string>, + ) => { + const conn = connectionRef.current; + const laneId = activeLaneIdRef.current; + const sessionId = activeSessionIdRef.current; + if (!conn) return; + + const requireField = (name: string, label: string): string | null => { + const value = values[name]?.trim() ?? ""; + if (value) return value; + addNotice(`${label} is required.`, "error"); + return null; + }; + + if (form.command === "new-chat") { + if (!laneId) return; + const title = values.title?.trim() || null; + const message = values.message?.trim() ?? ""; + const created = await createChatSession({ connection: conn, laneId, title }); + setActiveSessionId(created.id); + if (message) { + await sendChatMessage(conn, created.id, message); + } + setRightOpen(false); + setRightPane({ kind: "empty" }); + addNotice(title ? `Created chat "${title}".` : "Created chat.", "success"); + await refreshState(); + return; + } + + if (form.command === "new-lane") { + const name = requireField("name", "Name"); + if (!name) return; + const baseBranch = values.baseBranch?.trim(); + const created = await conn.action<LaneSummary>("lane", "create", { + name, + ...(baseBranch ? { baseBranch } : {}), + }); + setActiveLaneId(created.id); + setActiveSessionId(null); + setRightOpen(false); + setRightPane({ kind: "empty" }); + addNotice(`Created lane ${created.name}.`, "success"); + await refreshState(); + return; + } + + if (form.command === "rename") { + if (!sessionId) return; + const title = requireField("title", "Title"); + if (!title) return; + await renameChat(conn, sessionId, title); + setRightOpen(false); + setRightPane({ kind: "empty" }); + addNotice(`Renamed chat to "${title}".`, "success"); + await refreshState(); + return; + } + + if (form.command === "pr-open") { + if (!laneId) return; + const title = requireField("title", "Title"); + if (!title) return; + const body = values.body?.trim() ?? ""; + const created = await conn.action("pr", "createFromLane", { + laneId, + title, + body, + draft: true, + }); + setRightPane({ kind: "details", title: "PR open", body: renderObject(created, 24) }); + addNotice("Created draft PR.", "success"); + await refreshState(); + } + }, [addNotice, refreshState]); + + const submitPrompt = useCallback(async (value: string) => { + const text = value.trim(); + if (!text && rightPane.kind !== "form") return; + const conn = connectionRef.current; + if (!conn) return; + try { + if (desktopDriving && streaming && !text.startsWith("/") && rightPane.kind !== "form") { + addNotice("Desktop is driving this chat; draft kept locally until the stream settles.", "info"); + return; + } + setPrompt(""); + setError(null); + if (pendingApproval?.mode === "approval") { + const lowered = text.toLowerCase(); + if (pendingApproval.highStakes) { + if (lowered === "approve" || lowered === "deny") { + await resolvePendingApproval(pendingApproval, lowered === "approve" ? "accept" : "decline"); + return; + } + addNotice("Type approve or deny to resolve the high-stakes request.", "error"); + return; + } + if (lowered === "approve" || lowered === "a" || lowered === "deny" || lowered === "d") { + await resolvePendingApproval(pendingApproval, lowered === "approve" || lowered === "a" ? "accept" : "decline"); + return; + } + addNotice("Press a to approve or d to deny this request.", "error"); + return; + } + if (pendingApproval?.mode === "question") { + await answerPendingInput(pendingApproval, value); + return; + } + if (rightPane.kind === "form" && !text.startsWith("/")) { + const field = activeFormField; + const values = field ? { ...formValues, [field.name]: value } : formValues; + setFormValues(values); + await submitRightForm(rightPane, values); + return; + } + const parsed = parseCommand(text, slashCommands); + if (text.startsWith("/") && parsed && !parsed.spec && !parsed.userCommand && slashRows.length) { + const selected = slashRows[slashIndex] ?? slashRows[0]; + if (selected) { + const selectedCommand = parseCommand(selected.name, slashCommands); + if (selectedCommand?.spec?.placement === "inline") { + await runInlineCommand(selectedCommand.name, selectedCommand.args); + return; + } + if (selectedCommand?.spec?.placement === "right") { + await runRightCommand(selectedCommand.name, selectedCommand.args); + return; + } + const sessionId = await ensureActiveSession(); + if (sessionId) { + await sendChatMessage(conn, sessionId, selected.name); + await refreshState(); + } + return; + } + } + if (parsed?.spec?.placement === "inline") { + await runInlineCommand(parsed.name, parsed.args); + return; + } + if (parsed?.spec?.placement === "right") { + await runRightCommand(parsed.name, parsed.args); + return; + } + const desktopRoute = desktopRouteForCommand(parsed?.name); + if (desktopRoute) { + const result = await navigateDesktop(conn, { + source: "ade-code", + target: { kind: "route", route: desktopRoute }, + }); + if (result.ok) { + addNotice(`Opened ADE desktop for ${parsed?.name}.`, "success"); + return; + } + await runInlineCommand("/open", ""); + addNotice(`${parsed?.name} is a desktop-only surface; opened ADE desktop.`, "info"); + return; + } + const sessionId = await ensureActiveSession(); + if (!sessionId) { + addNotice("No active lane is available for chat.", "error"); + return; + } + lastLocalSendAtRef.current = Date.now(); + const attachments: AgentChatFileRef[] = selectedMentions + .filter((mention) => mention.kind === "file" && mention.filePath && text.includes(mention.insertText)) + .map((mention) => ({ type: "file", path: mention.filePath! })); + await sendChatMessage(conn, sessionId, text, attachments); + await refreshState(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + addNotice(message, "error"); + } + }, [activeFormField, addNotice, answerPendingInput, desktopDriving, ensureActiveSession, formValues, pendingApproval, refreshState, resolvePendingApproval, rightPane, runInlineCommand, runRightCommand, selectedMentions, slashCommands, slashIndex, slashRows, streaming, submitRightForm]); + + const insertMention = useCallback((suggestion: MentionSuggestion) => { + const range = activeMention(prompt); + if (!range) return; + setPrompt(`${prompt.slice(0, range.start)}${suggestion.insertText} ${prompt.slice(range.start + range.query.length + 1)}`); + setSelectedMentions((prev) => { + if (prev.some((entry) => entry.insertText === suggestion.insertText)) return prev; + return [...prev, suggestion].slice(-12); + }); + setMentionSuggestions([]); + setMentionIndex(0); + }, [prompt]); + + const insertSlashCommand = useCallback(() => { + const selected = slashRows[slashIndex] ?? slashRows[0]; + if (!selected) return; + setPrompt(`${selected.name}${selected.argumentHint ? " " : ""}`); + }, [slashIndex, slashRows]); + + useInput((input, key) => { + if (pendingApproval?.mode === "approval" && !pendingApproval.highStakes && (input === "a" || input === "d")) { + void resolvePendingApproval(pendingApproval, input === "a" ? "accept" : "decline") + .catch((err) => addNotice(err instanceof Error ? err.message : String(err), "error")); + return; + } + if (rightPane.kind === "form" && key.tab) { + const fields = rightPane.fields; + const currentField = fields[formFieldIndex] ?? fields[0]; + const nextValues = currentField ? { ...formValues, [currentField.name]: prompt } : formValues; + const nextIndex = fields.length ? (formFieldIndex + 1) % fields.length : 0; + setFormValues(nextValues); + setFormFieldIndex(nextIndex); + setPrompt(fields[nextIndex] ? nextValues[fields[nextIndex]!.name] ?? "" : ""); + return; + } + if (rightOpen && (rightPane.kind === "models" || rightPane.kind === "effort" || (rightPane.kind === "list" && rightPane.action)) && key.upArrow) { + const max = rightPane.kind === "models" + ? rightPane.models.length + : rightPane.kind === "effort" + ? rightPane.efforts.length + : rightPane.rows.length; + setRightSelectionIndex((index) => (index <= 0 ? Math.max(0, max - 1) : index - 1)); + return; + } + if (rightOpen && (rightPane.kind === "models" || rightPane.kind === "effort" || (rightPane.kind === "list" && rightPane.action)) && key.downArrow) { + const max = rightPane.kind === "models" + ? rightPane.models.length + : rightPane.kind === "effort" + ? rightPane.efforts.length + : rightPane.rows.length; + setRightSelectionIndex((index) => (max > 0 ? (index + 1) % max : 0)); + return; + } + if (rightOpen && rightPane.kind === "list" && rightPane.action && key.return) { + const selectedId = rightPane.action.ids[rightSelectionIndex] ?? rightPane.action.ids[0] ?? null; + if (!selectedId) return; + if (rightPane.action.kind === "switch-lane") { + const lane = lanes.find((entry) => entry.id === selectedId); + if (!lane) return; + setActiveLaneId(lane.id); + setDrawerLaneId(lane.id); + setSelectedDrawerLaneId(lane.id); + const session = newestSession(sessions.filter((entry) => entry.laneId === lane.id)); + setActiveSessionId(session?.sessionId ?? null); + setSelectedDrawerChatId(session?.sessionId ?? null); + addNotice(`Switched to lane ${lane.name}.`, "success"); + return; + } + const session = sessions.find((entry) => entry.sessionId === selectedId); + if (!session) return; + setActiveLaneId(session.laneId); + setDrawerLaneId(session.laneId); + setSelectedDrawerLaneId(session.laneId); + setActiveSessionId(session.sessionId); + setSelectedDrawerChatId(session.sessionId); + addNotice(`Switched to chat ${session.title ?? session.sessionId}.`, "success"); + return; + } + if (rightOpen && (rightPane.kind === "models" || rightPane.kind === "effort") && key.return) { + const conn = connectionRef.current; + const sessionId = activeSessionIdRef.current; + if (!conn || !sessionId) { + addNotice("Create or select a chat before changing model settings.", "error"); + return; + } + if (rightPane.kind === "models") { + const model = rightPane.models[rightSelectionIndex] ?? rightPane.models[0]; + if (!model) return; + const modelId = model.modelId ?? model.id; + void updateChatModel({ connection: conn, sessionId, modelId }) + .then(() => { + addNotice(`Model set to ${model.displayName}.`, "success"); + return refreshState(); + }) + .catch((err) => addNotice(err instanceof Error ? err.message : String(err), "error")); + return; + } + const effort = rightPane.efforts[rightSelectionIndex] ?? rightPane.efforts[0]; + if (!effort) return; + void updateChatModel({ connection: conn, sessionId, reasoningEffort: effort }) + .then(() => { + addNotice(`Effort set to ${effort}.`, "success"); + return refreshState(); + }) + .catch((err) => addNotice(err instanceof Error ? err.message : String(err), "error")); + return; + } + if (key.upArrow && activeMentionRange && mentionSuggestions.length) { + setMentionIndex((index) => (index <= 0 ? mentionSuggestions.length - 1 : index - 1)); + return; + } + if (key.downArrow && activeMentionRange && mentionSuggestions.length) { + setMentionIndex((index) => (index + 1) % mentionSuggestions.length); + return; + } + if (key.tab && activeMentionRange && mentionSuggestions.length) { + insertMention(mentionSuggestions[mentionIndex] ?? mentionSuggestions[0]!); + return; + } + if (key.upArrow && slashRows.length) { + setSlashIndex((index) => (index <= 0 ? slashRows.length - 1 : index - 1)); + return; + } + if (key.downArrow && slashRows.length) { + setSlashIndex((index) => (index + 1) % slashRows.length); + return; + } + if (key.tab && slashRows.length) { + insertSlashCommand(); + return; + } + if (drawerOpen && key.tab) { + setDrawerSection((section) => section === "lanes" ? "chats" : "lanes"); + return; + } + if (drawerOpen && key.upArrow) { + if (drawerSection === "lanes") { + const nextIndex = Math.max(0, selectedLaneIndex - 1); + setSelectedDrawerLaneId(drawerLaneRows[nextIndex]?.id ?? null); + } else { + const nextIndex = Math.max(0, selectedChatIndex - 1); + setSelectedDrawerChatId(drawerLaneSessions[nextIndex]?.sessionId ?? null); + } + return; + } + if (drawerOpen && key.downArrow) { + if (drawerSection === "lanes") { + const nextIndex = Math.min(Math.max(0, drawerLaneRows.length - 1), selectedLaneIndex + 1); + setSelectedDrawerLaneId(drawerLaneRows[nextIndex]?.id ?? null); + } else { + const nextIndex = Math.min(Math.max(0, drawerLaneSessions.length - 1), selectedChatIndex + 1); + setSelectedDrawerChatId(drawerLaneSessions[nextIndex]?.sessionId ?? null); + } + return; + } + if (drawerOpen && key.return) { + if (drawerSection === "lanes") { + const lane = drawerLaneRows[selectedLaneIndex]; + if (lane) { + setDrawerLaneId(lane.id); + setSelectedDrawerLaneId(lane.id); + setDrawerSection("chats"); + setSelectedDrawerChatId(sessions.find((session) => session.laneId === lane.id)?.sessionId ?? null); + } + } else { + const session = drawerLaneSessions[selectedChatIndex]; + if (session) { + setActiveLaneId(session.laneId); + setDrawerLaneId(session.laneId); + setSelectedDrawerLaneId(session.laneId); + setActiveSessionId(session.sessionId); + setSelectedDrawerChatId(session.sessionId); + } + } + return; + } + if (key.ctrl && input === "b") { + setDrawerOpen((value) => !value); + return; + } + if (key.ctrl && input === "j") { + setRightOpen((value) => !value); + return; + } + if (key.escape) { + if (desktopDriving) setDesktopDriving(false); + else if (rightOpen) setRightOpen(false); + else if (drawerOpen) setDrawerOpen(false); + else setPrompt(""); + return; + } + if (key.ctrl && input === "c") { + const conn = connectionRef.current; + const sessionId = activeSessionIdRef.current; + if (streaming && conn && sessionId) { + void interruptChat(conn, sessionId) + .then(() => addNotice("Interrupted chat.", "info")) + .catch((err) => addNotice(err instanceof Error ? err.message : String(err), "error")); + return; + } + exit(); + return; + } + if (key.return && !prompt.trim() && latestFailedLineId && !pendingApproval && rightPane.kind !== "form" && !slashRows.length) { + setExpandedLineIds((prev) => { + const next = new Set(prev); + if (next.has(latestFailedLineId)) next.delete(latestFailedLineId); + else next.add(latestFailedLineId); + return next; + }); + return; + } + const linePrefix = inputBeforeLineBreak(input); + if (key.return || linePrefix != null) { + const suffix = linePrefix == null ? "" : printableInput(linePrefix); + void submitPrompt(`${prompt}${suffix}`); + return; + } + if (key.backspace || key.delete) { + handlePromptChange(prompt.slice(0, -1)); + return; + } + if (!key.ctrl && input) { + const suffix = printableInput(input); + if (suffix) handlePromptChange(`${prompt}${suffix}`); + } + }); + + const handlePromptChange = useCallback((value: string) => { + if (value === "?") { + setRightPane({ kind: "help", title: "Help" }); + setRightOpen(true); + setPrompt(""); + return; + } + if (rightPane.kind === "form" && activeFormField) { + setFormValues((prev) => ({ ...prev, [activeFormField.name]: value })); + } + setPrompt(value); + }, [activeFormField, rightPane]); + + const centerWidth = Math.max(40, columns - (drawerOpen ? 30 : 0) - (rightOpen ? 40 : 0)); + const laneName = activeLane?.name ?? "main"; + + if (error && !connection) { + return ( + <Box flexDirection="column"> + <Text color="red">ade-code failed to start</Text> + <Text>{error}</Text> + </Box> + ); + } + + return ( + <Box flexDirection="column" height={rows}> + <Header + projectName={projectName} + lane={activeLane} + model={modelState} + mode={mode} + tuiCount={tuiCount} + /> + {desktopDriving ? ( + <Text color="yellow">Desktop is driving this chat; transcript is syncing here.</Text> + ) : null} + {streaming ? ( + <Text color={PURPLE}>● streaming live{tokenSummary ? ` · ${tokenSummary}` : ""} · ctrl-c interrupts</Text> + ) : null} + {contextPercent != null ? ( + <Text dimColor> + context {contextPercent}% {"█".repeat(Math.max(1, Math.round(contextPercent / 10))).padEnd(10, "░")} + {tokenSummary && !streaming ? ` · ${tokenSummary}` : ""} + </Text> + ) : null} + <Box flexGrow={1} minHeight={8}> + {drawerOpen ? ( + <Drawer + lanes={lanes} + sessions={sessions} + activeLaneId={activeLaneId} + activeSessionId={activeSessionId} + browsingLaneId={drawerLaneId ?? activeLaneId} + selectedLaneIndex={drawerSection === "lanes" ? selectedLaneIndex : -1} + selectedChatIndex={drawerSection === "chats" ? selectedChatIndex : -1} + /> + ) : null} + <Box width={centerWidth} flexDirection="column"> + {pendingApproval?.highStakes ? ( + <ApprovalPrompt approval={pendingApproval} modal /> + ) : ( + <> + <ChatView + events={events} + notices={notices} + activeSession={activeSession} + projectName={projectName} + laneName={laneName} + expandedLineIds={expandedLineIds} + /> + <ApprovalPrompt approval={pendingApproval} /> + </> + )} + </Box> + {rightOpen ? ( + <RightPane + content={rightPane} + formValues={formValues} + activeFormField={formFieldIndex} + selectedIndex={rightSelectionIndex} + /> + ) : null} + </Box> + <MentionPalette suggestions={mentionSuggestions} selectedIndex={mentionIndex} /> + <SlashPalette query={prompt} userCommands={slashCommands} selectedIndex={slashIndex} /> + {error ? <Text color="red">{error}</Text> : null} + <Box borderStyle="single" borderColor="gray" paddingX={1}> + <Text color={PURPLE}>› </Text> + <Text>{prompt}</Text> + <Text inverse> </Text> + </Box> + <Text dimColor> + [ {drawerOpen ? "▴" : "▾"} lanes & chats ^b ] [ {rightOpen ? "◂" : "▸"} right pane ^j ] / commands + </Text> + </Box> + ); +} diff --git a/apps/ade-code/src/cli.tsx b/apps/ade-code/src/cli.tsx new file mode 100644 index 00000000..1f096e00 --- /dev/null +++ b/apps/ade-code/src/cli.tsx @@ -0,0 +1,141 @@ +#!/usr/bin/env node +import React from "react"; +import { render } from "ink"; + +type CliOptions = { + help: boolean; + printState: boolean; + forceEmbedded: boolean; + requireSocket: boolean; + projectRoot: string | null; + workspaceRoot: string | null; + socketPath: string | null; +}; + +function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = { + help: false, + printState: false, + forceEmbedded: false, + requireSocket: false, + projectRoot: null, + workspaceRoot: null, + socketPath: null, + }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--help" || arg === "-h") options.help = true; + else if (arg === "--print-state") options.printState = true; + else if (arg === "--embedded") options.forceEmbedded = true; + else if (arg === "--require-socket") options.requireSocket = true; + else if (arg === "--project-root") options.projectRoot = argv[++i] ?? null; + else if (arg === "--workspace-root") options.workspaceRoot = argv[++i] ?? null; + else if (arg === "--socket") options.socketPath = argv[++i] ?? null; + } + return options; +} + +function printHelp(): void { + process.stdout.write(`ade-code + +Terminal-native ADE Work chat. + +Usage: + ade-code [--project-root <path>] [--workspace-root <path>] [--socket <path>] + ade-code --embedded + ade-code --require-socket + ade-code --print-state + +Keys: + ctrl-b toggle lanes and chats + ctrl-j toggle right pane + ? help when it is the first and only prompt character + / command palette +`); +} + +function writeStdout(value: string): Promise<void> { + return new Promise((resolve, reject) => { + process.stdout.write(value, (error) => { + if (error) reject(error); + else resolve(); + }); + }); +} + +function suppressTerminalWarnings(): void { + if (process.env.ADE_CODE_SHOW_WARNINGS === "1") return; + const originalEmitWarning = process.emitWarning; + process.emitWarning = function emitAdeCodeWarning(warning: string | Error, ...args: unknown[]): void { + const message = warning instanceof Error ? warning.message : String(warning); + const type = typeof args[0] === "string" ? args[0] : ""; + if (type === "ExperimentalWarning" && message.includes("SQLite is an experimental feature")) { + return; + } + (originalEmitWarning as (...innerArgs: unknown[]) => void).call(process, warning, ...args); + } as typeof process.emitWarning; +} + +async function printState(options: CliOptions): Promise<void> { + suppressTerminalWarnings(); + const { listChatSessions, listLanes } = await import("./adeApi"); + const { connectToAde } = await import("./connection"); + const { detectProjectLaunchContext } = await import("./project"); + const project = detectProjectLaunchContext({ + projectRoot: options.projectRoot, + workspaceRoot: options.workspaceRoot, + }); + const connection = await connectToAde({ + project, + forceEmbedded: options.forceEmbedded, + requireSocket: options.requireSocket, + socketPath: options.socketPath, + }); + try { + const lanes = await listLanes(connection); + const sessions = await listChatSessions(connection); + await writeStdout(`${JSON.stringify({ + mode: connection.mode, + projectRoot: project.projectRoot, + workspaceRoot: project.workspaceRoot, + laneCount: lanes.length, + chatCount: sessions.length, + socketPath: connection.socketPath, + }, null, 2)}\n`); + } finally { + await connection.close(); + } +} + +async function main(): Promise<void> { + const options = parseArgs(process.argv.slice(2)); + if (options.help) { + printHelp(); + return; + } + if (options.printState) { + await printState(options); + process.exit(0); + } + suppressTerminalWarnings(); + const { AdeCodeApp } = await import("./app"); + const { detectProjectLaunchContext } = await import("./project"); + const project = detectProjectLaunchContext({ + projectRoot: options.projectRoot, + workspaceRoot: options.workspaceRoot, + }); + render( + <AdeCodeApp + project={project} + forceEmbedded={options.forceEmbedded} + requireSocket={options.requireSocket} + socketPath={options.socketPath} + />, + ); +} + +void main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`ade-code: ${message}\n`); + process.exit(1); +}); diff --git a/apps/ade-code/src/commands.ts b/apps/ade-code/src/commands.ts new file mode 100644 index 00000000..3ca4dca3 --- /dev/null +++ b/apps/ade-code/src/commands.ts @@ -0,0 +1,138 @@ +import type { AgentChatSlashCommand } from "../../desktop/src/shared/types/chat"; + +export type CommandPlacement = "inline" | "right" | "overlay" | "chat"; + +export type BuiltinCommand = { + name: string; + description: string; + placement: CommandPlacement; + argumentHint?: string; +}; + +export const BUILTIN_COMMANDS: BuiltinCommand[] = [ + { name: "/commit", description: "Commit lane changes", placement: "inline", argumentHint: "[message]" }, + { name: "/push", description: "Push the active lane branch", placement: "inline" }, + { name: "/clear", description: "Clear the local terminal transcript view", placement: "inline" }, + { name: "/end", description: "End the active chat runtime", placement: "inline" }, + { name: "/open", description: "Open this ADE context in desktop", placement: "inline" }, + { name: "/quit", description: "Exit ade-code", placement: "inline" }, + { name: "/remember", description: "Write durable ADE memory", placement: "inline", argumentHint: "<fact>" }, + { name: "/new lane", description: "Create a new lane", placement: "right" }, + { name: "/new chat", description: "Create a new chat", placement: "right", argumentHint: "[title]" }, + { name: "/rename", description: "Rename the active chat", placement: "right", argumentHint: "[title]" }, + { name: "/status", description: "Show project, lane, and runtime state", placement: "right" }, + { name: "/diff", description: "Show active lane diff", placement: "right" }, + { name: "/log", description: "Show recent commits", placement: "right" }, + { name: "/pr", description: "Show pull request state", placement: "right" }, + { name: "/pr open", description: "Create or open a PR for the active lane", placement: "right" }, + { name: "/pr review", description: "Show PR reviews", placement: "right" }, + { name: "/pr checks", description: "Show PR checks", placement: "right" }, + { name: "/linear", description: "Run Linear workflow, route, sync, or ingress commands", placement: "right", argumentHint: "<group>" }, + { name: "/linear list", description: "List Linear work", placement: "right" }, + { name: "/linear workflows", description: "List Linear workflow runs", placement: "right" }, + { name: "/linear run", description: "Inspect or resolve a Linear run", placement: "right", argumentHint: "<status|resolve|cancel|reroute>" }, + { name: "/linear route", description: "Route a Linear issue", placement: "right", argumentHint: "<cto|mission|worker>" }, + { name: "/linear sync", description: "Operate Linear sync", placement: "right", argumentHint: "<dashboard|run|queue|resolve|detail>" }, + { name: "/linear ingress", description: "Inspect Linear ingress", placement: "right", argumentHint: "<status|events|webhook>" }, + { name: "/linear pull", description: "Pull a Linear ticket into chat context", placement: "right", argumentHint: "<id>" }, + { name: "/linear comment", description: "Comment on a Linear ticket", placement: "right", argumentHint: "<id> <text>" }, + { name: "/linear status", description: "Show Linear sync status", placement: "right" }, + { name: "/linear assign", description: "Assign a Linear ticket", placement: "right", argumentHint: "<id> <user>" }, + { name: "/memory", description: "Search ADE memory", placement: "right", argumentHint: "[query]" }, + { name: "/forget", description: "Open memory management", placement: "right" }, + { name: "/chats", description: "List chats in the active lane", placement: "right" }, + { name: "/switch", description: "Switch lane or chat", placement: "right", argumentHint: "[lane|chat]" }, + { name: "/resume", description: "Resume the active ended chat", placement: "right" }, + { name: "/help", description: "Show keymap and command help", placement: "right" }, + { name: "/model", description: "Pick the active chat model", placement: "right" }, + { name: "/effort", description: "Pick reasoning effort", placement: "right" }, + { name: "/system", description: "Show system and runtime details", placement: "right" }, + { name: "/ade", description: "Run an allowlisted ADE action", placement: "right", argumentHint: "<domain.action> [json]" }, +]; + +export type ParsedCommand = { + name: string; + args: string; + spec: BuiltinCommand | null; + userCommand: AgentChatSlashCommand | null; +}; + +function normalizeSlashName(value: string): string { + return value.trim().replace(/\s+/g, " "); +} + +export function parseCommand(input: string, userCommands: AgentChatSlashCommand[] = []): ParsedCommand | null { + const trimmed = input.trim(); + if (!trimmed.startsWith("/")) return null; + const [first = ""] = trimmed.split(/\s+/, 1); + const exactLocalCommand = userCommands.find((command) => command.source === "local" && command.name === first) ?? null; + if (exactLocalCommand) { + return { + name: first, + args: trimmed.slice(first.length).trim(), + spec: null, + userCommand: exactLocalCommand, + }; + } + const candidates = [...BUILTIN_COMMANDS] + .sort((left, right) => right.name.length - left.name.length); + for (const spec of candidates) { + const name = normalizeSlashName(spec.name); + if (trimmed === name || trimmed.startsWith(`${name} `)) { + return { + name, + args: trimmed.slice(name.length).trim(), + spec, + userCommand: null, + }; + } + } + + const userCommand = userCommands.find((command) => command.name === first) ?? null; + if (userCommand) { + return { + name: first, + args: trimmed.slice(first.length).trim(), + spec: null, + userCommand, + }; + } + + return { + name: first, + args: trimmed.slice(first.length).trim(), + spec: null, + userCommand: null, + }; +} + +export function paletteCommands( + query: string, + userCommands: AgentChatSlashCommand[] = [], +): Array<{ name: string; description: string; source: "ade" | "user"; argumentHint?: string }> { + const normalizedQuery = query.trim().toLowerCase(); + const builtins = BUILTIN_COMMANDS.map((command) => ({ + name: command.name, + description: command.description, + source: "ade" as const, + argumentHint: command.argumentHint, + })); + const users = userCommands.map((command) => ({ + name: command.name, + description: command.description, + source: "user" as const, + argumentHint: command.argumentHint, + })); + return [...builtins, ...users] + .filter((command) => { + if (!normalizedQuery || normalizedQuery === "/") return true; + return `${command.name} ${command.description}`.toLowerCase().includes(normalizedQuery.replace(/^\//, "")); + }) + .slice(0, 9); +} + +export function commandPlacement(command: ParsedCommand): CommandPlacement { + if (command.spec) return command.spec.placement; + if (command.userCommand) return "chat"; + return "chat"; +} diff --git a/apps/ade-code/src/components/ApprovalPrompt.tsx b/apps/ade-code/src/components/ApprovalPrompt.tsx new file mode 100644 index 00000000..73a6a00b --- /dev/null +++ b/apps/ade-code/src/components/ApprovalPrompt.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { Box, Text } from "ink"; +import type { PendingApproval } from "../types"; + +export function ApprovalPrompt({ + approval, + modal = false, +}: { + approval: PendingApproval | null; + modal?: boolean; +}) { + if (!approval) return null; + const question = approval.request?.questions[0] ?? null; + const card = ( + <Box + borderStyle="single" + borderColor={approval.highStakes ? "red" : "yellow"} + paddingX={1} + paddingY={modal ? 1 : 0} + flexDirection="column" + width={modal ? 60 : undefined} + > + <Text color={approval.highStakes ? "red" : "yellow"}> + {approval.mode === "question" + ? "Input requested" + : approval.highStakes + ? "High-stakes approval required" + : "Approval required"} + </Text> + <Text>{question?.question ?? approval.description}</Text> + {question?.options?.length ? ( + <Box flexDirection="column"> + {question.options.slice(0, 6).map((option, index) => ( + <Text key={option.value} dimColor> + {index + 1}. {option.label}{option.description ? ` - ${option.description}` : ""} + </Text> + ))} + </Box> + ) : null} + {approval.mode === "question" ? ( + <Text dimColor>Type an answer, option number/value, deny, or cancel.</Text> + ) : approval.highStakes ? ( + <Text dimColor>Type approve or deny, then press enter.</Text> + ) : ( + <Text dimColor>Press a to approve, d to deny.</Text> + )} + </Box> + ); + if (!modal) return card; + return ( + <Box flexGrow={1} alignItems="center" justifyContent="center"> + {card} + </Box> + ); +} diff --git a/apps/ade-code/src/components/ChatView.tsx b/apps/ade-code/src/components/ChatView.tsx new file mode 100644 index 00000000..96006bd8 --- /dev/null +++ b/apps/ade-code/src/components/ChatView.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Box, Text } from "ink"; +import type { AgentChatEventEnvelope, AgentChatSessionSummary } from "../../../desktop/src/shared/types/chat"; +import type { LocalNotice } from "../types"; +import { renderChatLines } from "../format"; + +const COLORS = { + user: "#A78BFA", + assistant: "white", + tool: "cyan", + error: "red", + notice: "gray", + reasoning: "gray", + approval: "yellow", +} as const; + +export function BootHero({ + projectName, + laneName, +}: { + projectName: string; + laneName: string; +}) { + return ( + <Box flexDirection="column" alignItems="center" paddingY={1}> + <Text color="#A78BFA">██▄ ██▄ ██▀</Text> + <Text color="#A78BFA">█ █ █ █ █▀ </Text> + <Text color="#A78BFA">██▀ ██▀ ██▄</Text> + <Text dimColor>code · v0.1</Text> + <Text dimColor>{projectName} · {laneName}</Text> + <Text dimColor>type to chat · / for commands</Text> + <Text dimColor>try: inspect the current diff</Text> + <Text dimColor>try: @file then ask for a focused review</Text> + <Text dimColor>try: /status or /new chat</Text> + </Box> + ); +} + +export function ChatView({ + events, + notices, + activeSession, + projectName, + laneName, + expandedLineIds, +}: { + events: AgentChatEventEnvelope[]; + notices: LocalNotice[]; + activeSession: AgentChatSessionSummary | null; + projectName: string; + laneName: string; + expandedLineIds?: Set<string>; +}) { + const lines = renderChatLines({ events, notices, activeSession, expandedLineIds, maxLines: 64 }); + if (!lines.length) { + return <BootHero projectName={projectName} laneName={laneName} />; + } + return ( + <Box flexDirection="column" paddingX={1}> + {lines.map((line) => ( + <Box key={line.id} flexDirection="column" marginBottom={line.header ? 1 : 0}> + {line.header ? <Text color={COLORS[line.tone]}>{line.header}</Text> : null} + <Text color={COLORS[line.tone]}>{line.body}</Text> + </Box> + ))} + </Box> + ); +} diff --git a/apps/ade-code/src/components/Drawer.tsx b/apps/ade-code/src/components/Drawer.tsx new file mode 100644 index 00000000..2d5b7d45 --- /dev/null +++ b/apps/ade-code/src/components/Drawer.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Box, Text } from "ink"; +import type { AgentChatSessionSummary } from "../../../desktop/src/shared/types/chat"; +import type { LaneSummary } from "../../../desktop/src/shared/types/lanes"; +import { formatLaneLabel, formatSessionLabel } from "../format"; + +const PURPLE = "#A78BFA"; +const AMBER = "#F59E0B"; + +export function Drawer({ + lanes, + sessions, + activeLaneId, + activeSessionId, + browsingLaneId, + selectedLaneIndex, + selectedChatIndex, +}: { + lanes: LaneSummary[]; + sessions: AgentChatSessionSummary[]; + activeLaneId: string | null; + activeSessionId: string | null; + browsingLaneId: string | null; + selectedLaneIndex: number; + selectedChatIndex: number; +}) { + const browsingLane = lanes.find((lane) => lane.id === browsingLaneId) ?? null; + const laneSessions = sessions.filter((session) => session.laneId === browsingLaneId).slice(0, 12); + return ( + <Box width={28} flexDirection="column" borderStyle="single" borderColor="gray" paddingX={1}> + <Text bold>LANES</Text> + {lanes.slice(0, 10).map((lane, index) => ( + <Text key={lane.id} color={lane.id === activeLaneId ? AMBER : lane.id === browsingLaneId ? "white" : undefined}> + {index === selectedLaneIndex ? "›" : " "} {lane.id === activeLaneId ? "●" : lane.id === browsingLaneId ? "◐" : "○"} {formatLaneLabel(lane).slice(0, 20)} + </Text> + ))} + <Text dimColor>+ new lane</Text> + <Text dimColor>{"─".repeat(24)}</Text> + <Text bold>CHATS · {browsingLane?.name ?? "no lane"}</Text> + {laneSessions.length === 0 ? ( + <Text dimColor>No chats in lane.</Text> + ) : laneSessions.map((session, index) => ( + <Text key={session.sessionId} color={session.sessionId === activeSessionId ? PURPLE : undefined}> + {index === selectedChatIndex ? "›" : " "} {session.sessionId === activeSessionId ? "●" : " "} {formatSessionLabel(session).slice(0, 20)} + </Text> + ))} + <Text dimColor>+ new chat</Text> + <Text dimColor>enter opens selected · arrows move</Text> + </Box> + ); +} diff --git a/apps/ade-code/src/components/Header.tsx b/apps/ade-code/src/components/Header.tsx new file mode 100644 index 00000000..fab0834b --- /dev/null +++ b/apps/ade-code/src/components/Header.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { Box, Text } from "ink"; +import type { LaneSummary } from "../../../desktop/src/shared/types/lanes"; +import type { AdeCodeModelState, RuntimeMode } from "../types"; +import { formatLaneLabel } from "../format"; + +const PURPLE = "#A78BFA"; +const AMBER = "#F59E0B"; + +export function Header({ + projectName, + lane, + model, + mode, + tuiCount, +}: { + projectName: string; + lane: LaneSummary | null; + model: AdeCodeModelState; + mode: RuntimeMode | "connecting"; + tuiCount: number; +}) { + const modeColor = mode === "attached" ? "green" : mode === "embedded" ? "yellow" : "gray"; + return ( + <Box borderStyle="single" borderColor="gray" paddingX={1}> + <Text color={PURPLE}>▌ ADE</Text> + <Text dimColor> ◆ </Text> + <Text>{projectName}</Text> + <Text dimColor> ▌ </Text> + <Text color={AMBER}>{formatLaneLabel(lane)}</Text> + <Text dimColor> ▲ </Text> + <Text color={PURPLE}>{model.displayName}</Text> + <Text dimColor> ● </Text> + <Text color={modeColor}>{mode}</Text> + <Text dimColor>{` · ⏵ ${tuiCount} tui${tuiCount === 1 ? "" : "s"}`}</Text> + </Box> + ); +} diff --git a/apps/ade-code/src/components/MentionPalette.tsx b/apps/ade-code/src/components/MentionPalette.tsx new file mode 100644 index 00000000..5477c91b --- /dev/null +++ b/apps/ade-code/src/components/MentionPalette.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Box, Text } from "ink"; +import type { MentionSuggestion } from "../types"; + +const COLORS: Record<MentionSuggestion["kind"], string> = { + lane: "#F59E0B", + chat: "#A78BFA", + pr: "cyan", + file: "green", + commit: "yellow", +}; + +export function MentionPalette({ + suggestions, + selectedIndex, +}: { + suggestions: MentionSuggestion[]; + selectedIndex: number; +}) { + if (!suggestions.length) return null; + return ( + <Box flexDirection="column" borderStyle="single" borderColor="gray" paddingX={1}> + {suggestions.slice(0, 8).map((suggestion, index) => ( + <Text key={`${suggestion.kind}:${suggestion.insertText}`}> + <Text color={index === selectedIndex ? "#A78BFA" : "gray"}>{index === selectedIndex ? "›" : " "}</Text> + <Text color={COLORS[suggestion.kind]}> {suggestion.kind.padEnd(6)}</Text> + <Text> {suggestion.label.slice(0, 28).padEnd(28)}</Text> + <Text dimColor> {suggestion.detail ?? ""}</Text> + </Text> + ))} + <Text dimColor>tab inserts selected reference</Text> + </Box> + ); +} diff --git a/apps/ade-code/src/components/RightPane.tsx b/apps/ade-code/src/components/RightPane.tsx new file mode 100644 index 00000000..002b5347 --- /dev/null +++ b/apps/ade-code/src/components/RightPane.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { Box, Text } from "ink"; +import type { RightPaneContent } from "../types"; + +function HelpPane() { + return ( + <Box flexDirection="column"> + <Text bold>Help</Text> + <Text dimColor>ctrl-b toggles lanes and chats</Text> + <Text dimColor>ctrl-j toggles this pane</Text> + <Text dimColor>esc closes the active side pane</Text> + <Text dimColor>ctrl-c interrupts a running chat; press again to quit</Text> + <Text dimColor>/ opens commands, @ opens references, tab inserts selected</Text> + </Box> + ); +} + +export function RightPane({ + content, + formValues = {}, + activeFormField = 0, + selectedIndex = 0, +}: { + content: RightPaneContent; + formValues?: Record<string, string>; + activeFormField?: number; + selectedIndex?: number; +}) { + return ( + <Box width={38} flexDirection="column" borderStyle="single" borderColor="gray" paddingX={1}> + {content.kind === "empty" ? ( + <Text dimColor>Run /status, /diff, /model, or /help.</Text> + ) : null} + {content.kind === "help" ? <HelpPane /> : null} + {content.kind === "status" ? ( + <Box flexDirection="column"> + <Text bold>Status</Text> + {content.rows.map(([key, value]) => ( + <Text key={key}><Text dimColor>{key.padEnd(10)}</Text> {value}</Text> + ))} + </Box> + ) : null} + {content.kind === "list" ? ( + <Box flexDirection="column"> + <Text bold>{content.title}</Text> + {content.rows.length ? content.rows.map((row, index) => ( + <Text key={`${content.action?.ids[index] ?? row}:${index}`} color={content.action && index === selectedIndex ? "#A78BFA" : undefined}> + {content.action ? `${index === selectedIndex ? "›" : " "} ${row}` : row} + </Text> + )) : <Text dimColor>{content.emptyText ?? "No data."}</Text>} + {content.action && content.rows.length ? <Text dimColor>arrows move · enter opens</Text> : null} + </Box> + ) : null} + {content.kind === "details" ? ( + <Box flexDirection="column"> + <Text bold>{content.title}</Text> + <Text>{content.body}</Text> + </Box> + ) : null} + {content.kind === "diff" ? ( + <Box flexDirection="column"> + <Text bold>{content.title}</Text> + {content.files.length ? content.files.map((file) => ( + <Box key={file.path} flexDirection="column" marginBottom={1}> + <Text color="cyan">{file.path} <Text dimColor>+{file.additions ?? 0} -{file.deletions ?? 0}</Text></Text> + {file.body ? <Text dimColor>{file.body.split(/\r?\n/).slice(0, 8).join("\n")}</Text> : null} + </Box> + )) : <Text dimColor>No changes.</Text>} + </Box> + ) : null} + {content.kind === "models" ? ( + <Box flexDirection="column"> + <Text bold>Model</Text> + {content.models.map((model, index) => ( + <Text key={model.id} color={(model.modelId ?? model.id) === content.activeModelId ? "#A78BFA" : undefined}> + {index === selectedIndex ? "›" : " "} {(model.modelId ?? model.id) === content.activeModelId ? "●" : "○"} {model.displayName} + </Text> + ))} + <Text dimColor>arrows move · enter applies</Text> + </Box> + ) : null} + {content.kind === "effort" ? ( + <Box flexDirection="column"> + <Text bold>Effort</Text> + {content.efforts.map((effort, index) => ( + <Text key={effort} color={effort === content.activeEffort ? "#A78BFA" : undefined}> + {index === selectedIndex ? "›" : " "} {effort === content.activeEffort ? "●" : "○"} {effort} + </Text> + ))} + <Text dimColor>arrows move · enter applies</Text> + </Box> + ) : null} + {content.kind === "form" ? ( + <Box flexDirection="column"> + <Text bold>{content.title}</Text> + {content.fields.map((field, index) => { + const value = formValues[field.name]?.trim(); + return ( + <Text key={field.name} color={index === activeFormField ? "#A78BFA" : undefined}> + {index === activeFormField ? "›" : " "} {field.label} + {field.required ? " *" : ""}: {value || field.placeholder || ""} + </Text> + ); + })} + <Text dimColor>tab moves fields · enter submits · / runs a command</Text> + </Box> + ) : null} + </Box> + ); +} diff --git a/apps/ade-code/src/components/SlashPalette.tsx b/apps/ade-code/src/components/SlashPalette.tsx new file mode 100644 index 00000000..0bcd376f --- /dev/null +++ b/apps/ade-code/src/components/SlashPalette.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Box, Text } from "ink"; +import type { AgentChatSlashCommand } from "../../../desktop/src/shared/types/chat"; +import { paletteCommands } from "../commands"; + +export function SlashPalette({ + query, + userCommands, + selectedIndex, +}: { + query: string; + userCommands: AgentChatSlashCommand[]; + selectedIndex: number; +}) { + const rows = paletteCommands(query, userCommands); + if (!query.startsWith("/")) return null; + return ( + <Box flexDirection="column" borderStyle="single" borderColor="gray" paddingX={1}> + {rows.map((row, index) => ( + <Text key={`${row.source}:${row.name}`}> + <Text color={index === selectedIndex ? "#A78BFA" : "gray"}>{index === selectedIndex ? "›" : " "}</Text> + <Text color={row.source === "user" ? "#A78BFA" : "gray"}>{row.source}</Text> + <Text> {row.name.padEnd(16)} </Text> + <Text dimColor>{row.description}</Text> + </Text> + ))} + </Box> + ); +} diff --git a/apps/ade-code/src/connection.ts b/apps/ade-code/src/connection.ts new file mode 100644 index 00000000..e5dcd997 --- /dev/null +++ b/apps/ade-code/src/connection.ts @@ -0,0 +1,212 @@ +import fs from "node:fs"; +import { resolveAdeLayout } from "../../desktop/src/shared/adeLayout"; +import { JsonRpcClient } from "./jsonRpcClient"; +import type { AdeCodeConnection, ProjectLaunchContext } from "./types"; +import type { AgentChatEventEnvelope } from "../../desktop/src/shared/types/chat"; + +type RpcResponseEnvelope<T> = + | T + | { + ok: false; + error: { message?: string }; + }; + +type AdeRpcRequest = <T>(method: string, params?: unknown) => Promise<T>; + +type AdeActionHelpers = Pick<AdeCodeConnection, "tool" | "action" | "actionList">; + +type EmbeddedRuntime = { + dispose: () => void; + agentChatService?: { + subscribeToEvents?: (callback: (event: AgentChatEventEnvelope) => void) => () => void; + }; +}; + +type DirectHandler = { + (message: unknown): Promise<unknown>; + dispose: () => void; +}; + +type CreateEmbeddedRuntime = (args: { + projectRoot: string; + workspaceRoot: string; + chatRuntime: "agent"; + runtimeProfile: "chat"; +}) => Promise<EmbeddedRuntime>; + +type CreateEmbeddedRpcRequestHandler = (args: { runtime: EmbeddedRuntime; serverVersion: string }) => DirectHandler; + +async function loadEmbeddedAdeCli(): Promise<{ + createAdeRuntime: (args: { + projectRoot: string; + workspaceRoot: string; + chatRuntime: "agent"; + runtimeProfile: "chat"; + }) => Promise<EmbeddedRuntime>; + createAdeRpcRequestHandler: CreateEmbeddedRpcRequestHandler; +}> { + const [bootstrap, rpc] = await Promise.all([ + import("../../ade-cli/src/bootstrap"), + import("../../ade-cli/src/adeRpcServer"), + ]); + return { + createAdeRuntime: bootstrap.createAdeRuntime as unknown as CreateEmbeddedRuntime, + createAdeRpcRequestHandler: rpc.createAdeRpcRequestHandler as unknown as CreateEmbeddedRpcRequestHandler, + }; +} + +function unwrapActionResult<T>(payload: RpcResponseEnvelope<unknown>, domain: string, action: string): T { + if (payload && typeof payload === "object" && "ok" in payload && payload.ok === false) { + const error = (payload as { error?: { message?: string } }).error; + const message = typeof error?.message === "string" + ? error.message + : `ADE action failed: ${domain}.${action}`; + throw new Error(message); + } + const record = payload as { result?: unknown }; + return record.result as T; +} + +function createAdeActionHelpers(request: AdeRpcRequest): AdeActionHelpers { + return { + tool: async <T>(name: string, toolArgs?: Record<string, unknown>): Promise<T> => { + const payload = await request<unknown>("ade/actions/call", { + name, + arguments: toolArgs ?? {}, + }); + if (payload && typeof payload === "object" && "ok" in payload && payload.ok === false) { + const error = (payload as { error?: { message?: string } }).error; + const message = typeof error?.message === "string" ? error.message : `ADE tool failed: ${name}`; + throw new Error(message); + } + return payload as T; + }, + action: async <T>(domain: string, action: string, actionArgs?: Record<string, unknown>): Promise<T> => { + const payload = await request<unknown>("ade/actions/call", { + name: "run_ade_action", + arguments: { domain, action, args: actionArgs ?? {} }, + }); + return unwrapActionResult<T>(payload, domain, action); + }, + actionList: async <T>(domain: string, action: string, argsList: unknown[]): Promise<T> => { + const payload = await request<unknown>("ade/actions/call", { + name: "run_ade_action", + arguments: { domain, action, argsList }, + }); + return unwrapActionResult<T>(payload, domain, action); + }, + }; +} + +async function initialize(request: AdeRpcRequest): Promise<void> { + await request("ade/initialize", { + protocolVersion: "2025-06-18", + clientName: "ade-code", + identity: { + role: "cto", + callerId: `ade-code:${process.pid}`, + }, + }); + await request("ade/initialized"); +} + +async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> { + let timer: NodeJS.Timeout | null = null; + try { + return await Promise.race([ + promise, + new Promise<T>((_, reject) => { + timer = setTimeout(() => reject(new Error(message)), timeoutMs); + timer.unref(); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +export async function connectToAde(args: { + project: ProjectLaunchContext; + forceEmbedded?: boolean; + requireSocket?: boolean; + socketPath?: string | null; +}): Promise<AdeCodeConnection> { + const layout = resolveAdeLayout(args.project.projectRoot); + const socketPath = args.socketPath?.trim() || process.env.ADE_RPC_SOCKET_PATH?.trim() || layout.socketPath; + + if (args.forceEmbedded && args.requireSocket) { + throw new Error("Cannot use embedded mode when a desktop socket is required."); + } + + if (!args.forceEmbedded && socketPath && (args.requireSocket || fs.existsSync(socketPath))) { + let client: JsonRpcClient | null = null; + try { + client = await JsonRpcClient.connect(socketPath); + const connectedClient = client; + const request: AdeRpcRequest = <T>(method: string, params?: unknown) => connectedClient.request<T>(method, params); + await withTimeout(initialize(request), 3000, "ADE RPC socket did not finish initialization."); + return { + mode: "attached", + projectRoot: args.project.projectRoot, + workspaceRoot: args.project.workspaceRoot, + socketPath, + request, + ...createAdeActionHelpers(request), + onChatEvent: (callback: (event: AgentChatEventEnvelope) => void) => ( + connectedClient.onNotification("chat/event", (params) => callback(params as AgentChatEventEnvelope)) + ), + close: async () => connectedClient.close(), + }; + } catch (error) { + client?.close(); + if (args.requireSocket) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`ADE RPC socket is required but unavailable at ${socketPath}: ${message}`); + } + // Fall through to embedded mode; a stale socket should not strand the TUI. + } + } + + if (args.requireSocket) { + throw new Error(`ADE RPC socket is required but unavailable at ${socketPath}.`); + } + + const { createAdeRuntime, createAdeRpcRequestHandler } = await loadEmbeddedAdeCli(); + const runtime = await createAdeRuntime({ + projectRoot: args.project.projectRoot, + workspaceRoot: args.project.workspaceRoot, + chatRuntime: "agent", + runtimeProfile: "chat", + }); + const handler: DirectHandler = createAdeRpcRequestHandler({ + runtime, + serverVersion: "ade-code", + }); + let nextRequestId = 1; + const request: AdeRpcRequest = async <T>(method: string, params?: unknown): Promise<T> => { + return await handler({ + jsonrpc: "2.0", + id: nextRequestId++, + method, + params, + }) as T; + }; + await initialize(request); + const chatEvents = typeof runtime.agentChatService?.subscribeToEvents === "function" + ? runtime.agentChatService.subscribeToEvents.bind(runtime.agentChatService) + : (() => () => {}); + + return { + mode: "embedded", + projectRoot: args.project.projectRoot, + workspaceRoot: args.project.workspaceRoot, + socketPath: null, + request, + ...createAdeActionHelpers(request), + onChatEvent: (callback) => chatEvents(callback), + close: async () => { + handler.dispose(); + runtime.dispose(); + }, + }; +} diff --git a/apps/ade-code/src/format.ts b/apps/ade-code/src/format.ts new file mode 100644 index 00000000..fb3896db --- /dev/null +++ b/apps/ade-code/src/format.ts @@ -0,0 +1,234 @@ +import path from "node:path"; +import type { AgentChatEventEnvelope, AgentChatSessionSummary } from "../../desktop/src/shared/types/chat"; +import type { LaneSummary } from "../../desktop/src/shared/types/lanes"; +import type { LocalNotice } from "./types"; + +function timeLabel(value: string): string { + const d = new Date(value); + if (Number.isNaN(d.getTime())) return "--:--"; + return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +function singleLine(value: unknown, max = 96): string { + const text = (() => { + if (typeof value === "string") return value; + try { + return JSON.stringify(value); + } catch { + return String(value); + } + })(); + return (text ?? "") + .replace(/\s+/g, " ") + .trim() + .slice(0, max); +} + +function summarizeCommandOutput(output: unknown): string { + const text = singleLine(output, 160); + const passed = /\b(\d+)\s+passed\b/i.exec(text)?.[1]; + const failed = /\b(\d+)\s+failed\b/i.exec(text)?.[1]; + if (passed || failed) { + return [ + passed ? `${passed} passed` : null, + failed ? `${failed} failed` : null, + ].filter(Boolean).join(" · "); + } + return text; +} + +export function compactPath(value: string, max = 42): string { + if (value.length <= max) return value; + const base = path.basename(value); + if (base.length + 3 >= max) return `...${base.slice(-(max - 3))}`; + return `.../${base}`; +} + +export type RenderedChatLine = { + id: string; + tone: "user" | "assistant" | "tool" | "error" | "notice" | "reasoning" | "approval"; + header?: string; + body: string; +}; + +export function chatEventLineId(envelope: AgentChatEventEnvelope, index = 0): string { + return `${envelope.sequence ?? index}:${envelope.event.type}:${envelope.timestamp}`; +} + +function isFailedExpandableEvent(envelope: AgentChatEventEnvelope): boolean { + const event = envelope.event; + if (event.type === "tool_result") return event.status === "failed"; + if (event.type === "file_change") return event.status === "failed"; + if (event.type === "command") return event.status === "failed" || (event.exitCode ?? 0) !== 0; + return false; +} + +function multiLine(value: unknown, maxLines = 18): string { + if (typeof value === "string") return value.split(/\r?\n/).slice(0, maxLines).join("\n"); + return renderObject(value, maxLines); +} + +export function latestExpandableFailureId(events: AgentChatEventEnvelope[]): string | null { + for (let index = events.length - 1; index >= 0; index -= 1) { + const envelope = events[index]!; + if (isFailedExpandableEvent(envelope)) return chatEventLineId(envelope, index); + } + return null; +} + +export function renderChatLines(args: { + events: AgentChatEventEnvelope[]; + notices: LocalNotice[]; + activeSession: AgentChatSessionSummary | null; + expandedLineIds?: Set<string>; + maxLines?: number; +}): RenderedChatLine[] { + const lines: RenderedChatLine[] = []; + for (const notice of args.notices) { + lines.push({ + id: notice.id, + tone: notice.tone === "error" ? "error" : "notice", + header: `- ade-code · ${timeLabel(notice.timestamp)} ${"-".repeat(20)}`, + body: notice.text, + }); + } + for (const [index, envelope] of args.events.entries()) { + const event = envelope.event; + const id = chatEventLineId(envelope, index); + const expanded = args.expandedLineIds?.has(id) ?? false; + if (event.type === "user_message") { + lines.push({ + id, + tone: "user", + header: `- you · ${timeLabel(envelope.timestamp)} ${"-".repeat(32)}`, + body: event.displayText ?? event.text, + }); + continue; + } + if (event.type === "text") { + lines.push({ + id, + tone: "assistant", + header: `- ade · ${timeLabel(envelope.timestamp)} · ${args.activeSession?.model ?? "model"} ${"-".repeat(18)}`, + body: event.text, + }); + continue; + } + if (event.type === "reasoning") { + lines.push({ + id, + tone: "reasoning", + body: `thinking ${singleLine(event.text, 120)}`, + }); + continue; + } + if (event.type === "tool_call") { + lines.push({ + id, + tone: "tool", + body: `> ${event.tool} ${singleLine(event.args, 96)}`, + }); + continue; + } + if (event.type === "tool_result") { + const failed = event.status === "failed"; + lines.push({ + id, + tone: failed ? "error" : "tool", + body: failed && expanded + ? `x ${event.tool}\n${multiLine(event.result, 18)}` + : `${failed ? "x" : "✓"} ${event.tool} ${singleLine(event.result, 120)}${failed ? " ↵ expands" : ""}`, + }); + continue; + } + if (event.type === "file_change") { + const diffLines = event.diff.split(/\r?\n/).slice(0, event.status === "failed" && expanded ? 24 : 10).join("\n"); + lines.push({ + id, + tone: event.status === "failed" ? "error" : "tool", + body: `> edit ${compactPath(event.path)} ${event.kind}${event.status === "failed" && !expanded ? " ↵ expands" : ""}\n${diffLines}`, + }); + continue; + } + if (event.type === "command") { + const failed = event.status === "failed" || (event.exitCode ?? 0) !== 0; + lines.push({ + id, + tone: failed ? "error" : "tool", + body: failed && expanded + ? `x run ${event.command} ${event.durationMs ? `${event.durationMs}ms` : ""}\n${multiLine(event.output, 24)}` + : `${failed ? "x" : "✓"} run ${event.command} ${event.durationMs ? `${event.durationMs}ms` : ""}${failed ? " ↵ expands" : ""}\n${summarizeCommandOutput(event.output)}`, + }); + continue; + } + if (event.type === "approval_request") { + const record = event as unknown as Record<string, unknown>; + const files = Array.isArray(record.files) ? record.files : []; + const additions = typeof record.totalAdditions === "number" ? record.totalAdditions : 0; + const deletions = typeof record.totalDeletions === "number" ? record.totalDeletions : 0; + lines.push({ + id, + tone: "approval", + body: `approval needed ${files.length} files +${additions} -${deletions}`, + }); + continue; + } + if (event.type === "context_compact") { + const preTokens = typeof event.preTokens === "number" ? ` · before ${event.preTokens.toLocaleString()} tokens` : ""; + lines.push({ + id, + tone: "notice", + body: `- context compacted · ${event.trigger}${preTokens} ${"-".repeat(24)}`, + }); + continue; + } + if (event.type === "system_notice") { + lines.push({ + id, + tone: "notice", + body: singleLine((event as { message?: unknown }).message, 160), + }); + } + } + return lines.slice(-(args.maxLines ?? 80)); +} + +export function formatLaneLabel(lane: LaneSummary | null): string { + if (!lane) return "no lane"; + const dirty = lane.status?.dirty ? "*" : ""; + const ahead = lane.status?.ahead ? ` ${lane.status.ahead}↑` : ""; + return `${lane.name}${dirty}${ahead}`; +} + +export function formatSessionLabel(session: AgentChatSessionSummary): string { + const label = (session.title ?? session.goal ?? session.summary ?? session.sessionId).trim(); + const state = session.awaitingInput ? " ?" : session.status === "active" ? " ●" : ""; + return `${label}${state}`; +} + +export function renderObject(value: unknown, maxLines = 24): string { + if (value == null) return "No data."; + if (typeof value === "string") return value; + try { + return JSON.stringify(value, null, 2).split(/\r?\n/).slice(0, maxLines).join("\n"); + } catch { + return String(value); + } +} + +export function summarizeDiffChanges(value: unknown): Array<{ path: string; additions?: number; deletions?: number; body?: string }> { + const record = value && typeof value === "object" ? value as Record<string, unknown> : {}; + const files = Array.isArray(record.files) ? record.files : Array.isArray(record.changes) ? record.changes : []; + return files + .map((entry) => { + const item = entry && typeof entry === "object" ? entry as Record<string, unknown> : {}; + const filePath = String(item.path ?? item.filePath ?? item.relativePath ?? "unknown"); + return { + path: filePath, + additions: typeof item.additions === "number" ? item.additions : undefined, + deletions: typeof item.deletions === "number" ? item.deletions : undefined, + body: typeof item.diff === "string" ? item.diff : undefined, + }; + }) + .slice(0, 20); +} diff --git a/apps/ade-code/src/heartbeat.ts b/apps/ade-code/src/heartbeat.ts new file mode 100644 index 00000000..1832ecbc --- /dev/null +++ b/apps/ade-code/src/heartbeat.ts @@ -0,0 +1,129 @@ +import fs from "node:fs"; +import path from "node:path"; + +const STALE_MS = 20_000; + +export type TuiHeartbeat = { + count: number; + stop: () => void; + readCount: () => number; +}; + +const EXIT_CODES_BY_SIGNAL: Partial<Record<NodeJS.Signals, number>> = { + SIGHUP: 129, + SIGINT: 130, + SIGTERM: 143, +}; +const EXIT_SIGNALS = Object.keys(EXIT_CODES_BY_SIGNAL) as NodeJS.Signals[]; +const activeHeartbeatCleanups = new Set<() => void>(); +const signalHandlers = new Map<NodeJS.Signals, () => void>(); +let processHandlersRegistered = false; + +function safeUnlink(filePath: string): void { + try { + fs.unlinkSync(filePath); + } catch { + // ignore + } +} + +function cleanupAndCount(dir: string, now = Date.now()): number { + let count = 0; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith(".json")) continue; + const filePath = path.join(dir, entry.name); + try { + const raw = JSON.parse(fs.readFileSync(filePath, "utf8")) as { updatedAt?: number; pid?: number }; + const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0; + const pid = typeof raw.pid === "number" ? raw.pid : 0; + const stale = now - updatedAt > STALE_MS || (pid > 0 && pid !== process.pid && !processExists(pid)); + if (stale) { + safeUnlink(filePath); + } else { + count += 1; + } + } catch { + safeUnlink(filePath); + } + } + return count; +} + +function processExists(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function cleanupActiveHeartbeats(): void { + for (const cleanup of Array.from(activeHeartbeatCleanups)) { + cleanup(); + } +} + +function onProcessExit(): void { + cleanupActiveHeartbeats(); +} + +function ensureProcessHandlers(): void { + if (processHandlersRegistered) return; + process.once("exit", onProcessExit); + for (const signal of EXIT_SIGNALS) { + const handler = () => { + cleanupActiveHeartbeats(); + process.exit(EXIT_CODES_BY_SIGNAL[signal] ?? 1); + }; + signalHandlers.set(signal, handler); + process.once(signal, handler); + } + processHandlersRegistered = true; +} + +function removeProcessHandlersIfIdle(): void { + if (!processHandlersRegistered || activeHeartbeatCleanups.size > 0) return; + process.removeListener("exit", onProcessExit); + for (const [signal, handler] of signalHandlers) { + process.removeListener(signal, handler); + } + signalHandlers.clear(); + processHandlersRegistered = false; +} + +export function startTuiHeartbeat(projectRoot: string): TuiHeartbeat { + const dir = path.join(projectRoot, ".ade", "cache", "ade-code", "clients"); + fs.mkdirSync(dir, { recursive: true }); + const filePath = path.join(dir, `${process.pid}.json`); + const startedAt = new Date().toISOString(); + const write = () => { + fs.writeFileSync(filePath, JSON.stringify({ + pid: process.pid, + startedAt, + updatedAt: Date.now(), + }), "utf8"); + }; + write(); + const timer = setInterval(() => { + write(); + cleanupAndCount(dir); + }, 5_000); + timer.unref?.(); + let stopped = false; + const stop = () => { + if (stopped) return; + stopped = true; + clearInterval(timer); + activeHeartbeatCleanups.delete(stop); + safeUnlink(filePath); + removeProcessHandlersIfIdle(); + }; + activeHeartbeatCleanups.add(stop); + ensureProcessHandlers(); + return { + count: cleanupAndCount(dir), + stop, + readCount: () => cleanupAndCount(dir), + }; +} diff --git a/apps/ade-code/src/jsonRpcClient.ts b/apps/ade-code/src/jsonRpcClient.ts new file mode 100644 index 00000000..461d86aa --- /dev/null +++ b/apps/ade-code/src/jsonRpcClient.ts @@ -0,0 +1,187 @@ +import net from "node:net"; + +type PendingRequest = { + resolve: (value: unknown) => void; + reject: (reason: unknown) => void; +}; + +type JsonRpcResponse = { + jsonrpc: "2.0"; + id: number | string | null; + result?: unknown; + error?: { + code: number; + message: string; + data?: unknown; + }; + method?: string; + params?: unknown; +}; + +export class JsonRpcClient { + private nextId = 1; + private buffer = Buffer.alloc(0); + private pending = new Map<number, PendingRequest>(); + private notificationHandlers = new Map<string, Set<(params: unknown) => void>>(); + private closed = false; + + constructor(private readonly socket: net.Socket) { + socket.on("data", (chunk: Buffer | string) => this.handleData(chunk)); + socket.on("error", (error) => this.rejectAll(error)); + socket.on("close", () => { + this.closed = true; + this.rejectAll(new Error("ADE RPC socket closed.")); + }); + } + + static connect(socketPath: string): Promise<JsonRpcClient> { + return new Promise((resolve, reject) => { + const socket = socketPath.startsWith("tcp://") + ? (() => { + const parsed = new URL(socketPath); + return net.createConnection({ + host: parsed.hostname || "127.0.0.1", + port: Number.parseInt(parsed.port, 10), + }); + })() + : net.createConnection(socketPath); + const cleanup = () => { + socket.off("connect", onConnect); + socket.off("error", onError); + }; + const onConnect = () => { + cleanup(); + resolve(new JsonRpcClient(socket)); + }; + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + socket.once("connect", onConnect); + socket.once("error", onError); + }); + } + + request<T = unknown>(method: string, params?: unknown): Promise<T> { + if (this.closed) return Promise.reject(new Error("ADE RPC socket is closed.")); + const id = this.nextId++; + const payload = { + jsonrpc: "2.0", + id, + method, + ...(params !== undefined ? { params } : {}), + }; + return new Promise<T>((resolve, reject) => { + this.pending.set(id, { + resolve: (value) => resolve(value as T), + reject, + }); + this.socket.write(`${JSON.stringify(payload)}\n`, "utf8", (error) => { + if (!error) return; + this.pending.delete(id); + reject(error); + }); + }); + } + + close(): void { + this.closed = true; + this.rejectAll(new Error("ADE RPC socket closed.")); + this.socket.end(); + this.socket.destroy(); + } + + onNotification(method: string, handler: (params: unknown) => void): () => void { + const handlers = this.notificationHandlers.get(method) ?? new Set<(params: unknown) => void>(); + handlers.add(handler); + this.notificationHandlers.set(method, handlers); + return () => { + handlers.delete(handler); + if (handlers.size === 0) this.notificationHandlers.delete(method); + }; + } + + private handleData(chunk: Buffer | string): void { + this.buffer = Buffer.concat([ + this.buffer, + Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8"), + ]); + while (true) { + const next = this.takeNextPayload(); + if (!next) return; + const line = next.trim(); + if (!line) continue; + let parsed: JsonRpcResponse | JsonRpcResponse[] | null = null; + try { + parsed = JSON.parse(line) as JsonRpcResponse | JsonRpcResponse[]; + } catch { + continue; + } + const responses = Array.isArray(parsed) ? parsed : [parsed]; + for (const response of responses) this.handleResponse(response); + } + } + + private takeNextPayload(): string | null { + while (this.buffer.length && /\s/.test(String.fromCharCode(this.buffer[0]!))) { + this.buffer = this.buffer.subarray(1); + } + if (!this.buffer.length) return null; + const first = String.fromCharCode(this.buffer[0]!); + if (first === "{" || first === "[") { + const idx = this.buffer.indexOf(0x0a); + if (idx < 0) return null; + const payload = this.buffer.subarray(0, idx).toString("utf8"); + this.buffer = this.buffer.subarray(idx + 1); + return payload; + } + + const crlfBoundary = this.buffer.indexOf("\r\n\r\n"); + const lfBoundary = this.buffer.indexOf("\n\n"); + const boundary = crlfBoundary >= 0 + ? { index: crlfBoundary, length: 4 } + : lfBoundary >= 0 + ? { index: lfBoundary, length: 2 } + : null; + if (!boundary) return null; + const header = this.buffer.subarray(0, boundary.index).toString("ascii"); + const match = /^content-length\s*:\s*(\d+)\s*$/im.exec(header); + if (!match) { + this.buffer = this.buffer.subarray(boundary.index + boundary.length); + return ""; + } + const length = Number.parseInt(match[1]!, 10); + const bodyStart = boundary.index + boundary.length; + const bodyEnd = bodyStart + length; + if (this.buffer.length < bodyEnd) return null; + const payload = this.buffer.subarray(bodyStart, bodyEnd).toString("utf8"); + this.buffer = this.buffer.subarray(bodyEnd); + return payload; + } + + private handleResponse(response: JsonRpcResponse): void { + if (typeof response.id !== "number") { + if (typeof response.method === "string") { + for (const handler of this.notificationHandlers.get(response.method) ?? []) { + handler(response.params); + } + } + return; + } + const pending = this.pending.get(response.id); + if (!pending) return; + this.pending.delete(response.id); + if (response.error) { + pending.reject(new Error(response.error.message)); + return; + } + pending.resolve(response.result); + } + + private rejectAll(error: Error): void { + for (const pending of this.pending.values()) { + pending.reject(error); + } + this.pending.clear(); + } +} diff --git a/apps/ade-code/src/linearCommands.ts b/apps/ade-code/src/linearCommands.ts new file mode 100644 index 00000000..02e5f948 --- /dev/null +++ b/apps/ade-code/src/linearCommands.ts @@ -0,0 +1,201 @@ +export type LinearToolRequest = + | { + kind: "tool"; + title: string; + toolName: string; + args: Record<string, unknown>; + } + | { + kind: "usage"; + title: string; + body: string; + }; + +type ParsedArgs = { + positionals: string[]; + options: Record<string, unknown>; +}; + +function tokenize(input: string): string[] { + const tokens: string[] = []; + const pattern = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^']*)'|(\S+)/g; + let match: RegExpExecArray | null; + while ((match = pattern.exec(input)) != null) { + tokens.push(match[1]?.replace(/\\"/g, "\"") ?? match[2] ?? match[3] ?? ""); + } + return tokens; +} + +function toCamelCase(value: string): string { + return value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()); +} + +function parseScalar(value: string): unknown { + if (value === "true") return true; + if (value === "false") return false; + if (/^-?\d+$/.test(value)) return Number(value); + return value; +} + +export function parseLinearArgs(input: string): ParsedArgs { + const positionals: string[] = []; + const options: Record<string, unknown> = {}; + const tokens = tokenize(input); + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index] ?? ""; + if (token.startsWith("--")) { + const key = toCamelCase(token.slice(2)); + const next = tokens[index + 1]; + if (next && !next.startsWith("--")) { + options[key] = parseScalar(next); + index += 1; + } else { + options[key] = true; + } + } else { + positionals.push(token); + } + } + return { positionals, options }; +} + +function optionString(options: Record<string, unknown>, ...names: string[]): string | null { + for (const name of names) { + const value = options[name]; + if (typeof value === "string" && value.trim()) return value.trim(); + } + return null; +} + +function optionBoolean(options: Record<string, unknown>, name: string): boolean | undefined { + const value = options[name]; + return typeof value === "boolean" ? value : undefined; +} + +function usage(title: string, body: string): LinearToolRequest { + return { kind: "usage", title, body }; +} + +function compactArgs(args: Record<string, unknown>): Record<string, unknown> { + return Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined)); +} + +function tool(title: string, toolName: string, args: Record<string, unknown> = {}): LinearToolRequest { + return { kind: "tool", title, toolName, args: compactArgs(args) }; +} + +export function buildLinearToolRequest(input: string): LinearToolRequest { + const parsed = parseLinearArgs(input); + const [group = "workflows", modeArg, ...rest] = parsed.positionals; + const options = parsed.options; + + if (group === "workflows") { + return tool("Linear workflows", "listLinearWorkflows"); + } + + if (group === "run") { + const mode = modeArg ?? "status"; + const runId = optionString(options, "runId", "run") ?? rest[0] ?? null; + if (mode === "status") { + if (!runId) return usage("Linear run", "Usage: /linear run status <run-id>"); + return tool("Linear run status", "getLinearRunStatus", { runId }); + } + if (mode === "resolve") { + const action = optionString(options, "action") ?? rest[1] ?? null; + if (!runId || !action) return usage("Linear run resolve", "Usage: /linear run resolve <run-id> <approve|reject|retry|resume|complete>"); + return tool("Linear run resolve", "resolveLinearRunAction", { + runId, + action, + note: optionString(options, "note") ?? undefined, + }); + } + if (mode === "cancel") { + const reason = optionString(options, "reason") ?? rest.slice(1).join(" "); + if (!runId || !reason) return usage("Linear run cancel", "Usage: /linear run cancel <run-id> --reason <reason>"); + return tool("Linear run cancel", "cancelLinearRun", { runId, reason }); + } + if (mode === "reroute") { + const target = optionString(options, "target") ?? rest[1] ?? null; + const reason = optionString(options, "reason") ?? rest.slice(2).join(" "); + if (!runId || !target || !reason) return usage("Linear run reroute", "Usage: /linear run reroute <run-id> <cto|mission|worker> --reason <reason>"); + return tool("Linear run reroute", "rerouteLinearRun", { + runId, + target, + reason, + laneId: optionString(options, "laneId", "lane") ?? undefined, + reuseExisting: optionBoolean(options, "reuseExisting"), + launch: optionBoolean(options, "launch"), + runMode: optionString(options, "runMode") ?? undefined, + agentId: optionString(options, "agentId", "agent") ?? undefined, + taskKey: optionString(options, "taskKey") ?? undefined, + }); + } + return usage("Linear run", "Usage: /linear run <status|resolve|cancel|reroute> ..."); + } + + if (group === "route") { + const mode = modeArg ?? "cto"; + const issueId = optionString(options, "issueId", "issue") ?? rest[0] ?? null; + if (!issueId) return usage("Linear route", "Usage: /linear route <cto|mission|worker> <issue-id>"); + if (mode === "cto") { + return tool("Linear route cto", "routeLinearIssueToCto", { + issueId, + laneId: optionString(options, "laneId", "lane") ?? undefined, + reuseExisting: optionBoolean(options, "reuseExisting"), + }); + } + if (mode === "mission") { + return tool("Linear route mission", "routeLinearIssueToMission", { + issueId, + laneId: optionString(options, "laneId", "lane") ?? undefined, + launch: optionBoolean(options, "launch"), + runMode: optionString(options, "runMode") ?? undefined, + }); + } + if (mode === "worker") { + const agentId = optionString(options, "agentId", "agent") ?? rest[1] ?? null; + if (!agentId) return usage("Linear route worker", "Usage: /linear route worker <issue-id> <agent-id>"); + return tool("Linear route worker", "routeLinearIssueToWorker", { + issueId, + agentId, + taskKey: optionString(options, "taskKey") ?? undefined, + }); + } + return usage("Linear route", "Usage: /linear route <cto|mission|worker> ..."); + } + + if (group === "sync") { + const mode = modeArg ?? "dashboard"; + if (mode === "dashboard") return tool("Linear sync dashboard", "getLinearSyncDashboard"); + if (mode === "run") return tool("Linear sync run", "runLinearSyncNow"); + if (mode === "queue") return tool("Linear sync queue", "listLinearSyncQueue"); + if (mode === "detail") { + const runId = optionString(options, "runId", "run") ?? rest[0] ?? null; + if (!runId) return usage("Linear sync detail", "Usage: /linear sync detail <run-id>"); + return tool("Linear sync detail", "getLinearWorkflowRunDetail", { runId }); + } + if (mode === "resolve") { + const queueItemId = optionString(options, "queueItemId", "queueItem") ?? rest[0] ?? null; + const action = optionString(options, "action") ?? rest[1] ?? null; + if (!queueItemId || !action) return usage("Linear sync resolve", "Usage: /linear sync resolve <queue-item-id> <approve|reject|retry|resume|complete>"); + return tool("Linear sync resolve", "resolveLinearSyncQueueItem", { + queueItemId, + action, + note: optionString(options, "note") ?? undefined, + employeeOverride: optionString(options, "employeeOverride") ?? undefined, + laneId: optionString(options, "laneId", "lane") ?? undefined, + }); + } + return usage("Linear sync", "Usage: /linear sync <dashboard|run|queue|resolve|detail> ..."); + } + + if (group === "ingress") { + const mode = modeArg ?? "status"; + if (mode === "status") return tool("Linear ingress status", "getLinearIngressStatus"); + if (mode === "events") return tool("Linear ingress events", "listLinearIngressEvents", { limit: options.limit ?? undefined }); + if (mode === "webhook") return tool("Linear ingress webhook", "ensureLinearWebhook", { force: optionBoolean(options, "force") }); + return usage("Linear ingress", "Usage: /linear ingress <status|events|webhook>"); + } + + return usage("Linear", "Usage: /linear <workflows|run|route|sync|ingress> ..."); +} diff --git a/apps/ade-code/src/pendingInput.ts b/apps/ade-code/src/pendingInput.ts new file mode 100644 index 00000000..ad174030 --- /dev/null +++ b/apps/ade-code/src/pendingInput.ts @@ -0,0 +1,99 @@ +import type { + AgentChatEventEnvelope, + PendingInputOption, + PendingInputQuestion, + PendingInputRequest, +} from "../../desktop/src/shared/types/chat"; +import { renderObject } from "./format"; +import type { PendingApproval } from "./types"; + +function looksHighStakesApproval(description: string, detail: unknown): boolean { + const text = `${description} ${renderObject(detail, 8)}`.toLowerCase(); + return /\b(drop|delete|destroy|force[- ]push|production|prod|schema|credential|secret|external|publish|release)\b/.test(text); +} + +function isPendingInputRequest(value: unknown): value is PendingInputRequest { + const record = value && typeof value === "object" ? value as Record<string, unknown> : null; + return Boolean( + record + && typeof record.requestId === "string" + && typeof record.kind === "string" + && Array.isArray(record.questions), + ); +} + +function requestFromApprovalEvent(event: Record<string, unknown>): PendingInputRequest | undefined { + const detail = event.detail && typeof event.detail === "object" ? event.detail as Record<string, unknown> : null; + const request = detail?.request; + return isPendingInputRequest(request) ? request : undefined; +} + +function isApprovalMode(request: PendingInputRequest | undefined): boolean { + return !request || request.kind === "approval" || request.kind === "permissions" || request.kind === "plan_approval"; +} + +export function latestPendingApproval(events: AgentChatEventEnvelope[]): PendingApproval | null { + const resolved = new Set<string>(); + for (const envelope of events) { + const event = envelope.event as Record<string, unknown>; + if (event.type === "pending_input_resolved" && typeof event.itemId === "string") { + resolved.add(event.itemId); + } + } + for (let index = events.length - 1; index >= 0; index -= 1) { + const event = events[index]?.event as Record<string, unknown> | undefined; + if (!event || event.type !== "approval_request" || typeof event.itemId !== "string") continue; + if (resolved.has(event.itemId)) continue; + const request = requestFromApprovalEvent(event); + const description = typeof event.description === "string" ? event.description : "Approve this tool request?"; + const mode = isApprovalMode(request) ? "approval" : "question"; + return { + itemId: event.itemId, + description, + highStakes: mode === "approval" && ( + request?.kind === "permissions" + || request?.kind === "plan_approval" + || looksHighStakesApproval(description, event.detail) + ), + mode, + ...(request ? { request } : {}), + }; + } + return null; +} + +function optionMatches(input: string, option: PendingInputOption, index: number): boolean { + const normalized = input.trim().toLowerCase(); + return normalized === String(index + 1) + || normalized === option.value.toLowerCase() + || normalized === option.label.toLowerCase(); +} + +function answerForQuestion(question: PendingInputQuestion, text: string): string | string[] { + const trimmed = text.trim(); + if (!question.options?.length) return trimmed; + const values = trimmed.split(",").map((entry) => entry.trim()).filter(Boolean); + const matched = values.map((value) => { + const option = question.options?.find((candidate, index) => optionMatches(value, candidate, index)); + return option?.value ?? value; + }); + if (question.multiSelect) return matched; + return matched[0] ?? trimmed; +} + +export function buildPendingInputAnswers( + request: PendingInputRequest | undefined, + text: string, +): Record<string, string | string[]> | undefined { + const questions = request?.questions ?? []; + if (questions.length === 0) return undefined; + if (questions.length === 1) { + const question = questions[0]!; + return { [question.id]: answerForQuestion(question, text) }; + } + const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + return Object.fromEntries(questions.map((question, index) => [ + question.id, + answerForQuestion(question, lines[index] ?? text), + ])); +} diff --git a/apps/ade-code/src/project.ts b/apps/ade-code/src/project.ts new file mode 100644 index 00000000..68d28d20 --- /dev/null +++ b/apps/ade-code/src/project.ts @@ -0,0 +1,114 @@ +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import type { LaneSummary } from "../../desktop/src/shared/types/lanes"; +import type { ProjectLaunchContext } from "./types"; + +function normalizeRoot(value: string): string { + return path.resolve(value); +} + +function findGitRoot(cwd: string): string | null { + try { + const stdout = execFileSync("git", ["rev-parse", "--show-toplevel"], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + const root = stdout.trim(); + return root ? path.resolve(root) : null; + } catch { + return null; + } +} + +function findAdeWorktreeContext(cwd: string): Pick<ProjectLaunchContext, "projectRoot" | "workspaceRoot" | "laneHint"> | null { + const resolved = path.resolve(cwd); + const parts = resolved.split(path.sep); + for (let i = parts.length - 1; i >= 0; i -= 1) { + if (parts[i] !== ".ade" || parts[i + 1] !== "worktrees" || !parts[i + 2]) continue; + const rootParts = parts.slice(0, i); + const projectRoot = rootParts.length === 0 ? path.sep : rootParts.join(path.sep); + const laneHint = parts[i + 2] ?? null; + const workspaceRoot = findGitRoot(resolved) ?? path.join(projectRoot, ".ade", "worktrees", laneHint ?? ""); + return { + projectRoot: normalizeRoot(projectRoot), + workspaceRoot: normalizeRoot(workspaceRoot), + laneHint, + }; + } + return null; +} + +export function detectProjectLaunchContext(args: { + cwd?: string; + projectRoot?: string | null; + workspaceRoot?: string | null; +} = {}): ProjectLaunchContext { + const launchCwd = normalizeRoot(args.cwd ?? process.cwd()); + const explicitProjectRoot = args.projectRoot?.trim(); + const explicitWorkspaceRoot = args.workspaceRoot?.trim(); + const worktree = findAdeWorktreeContext(launchCwd); + const gitRoot = findGitRoot(launchCwd); + + const projectRoot = normalizeRoot( + explicitProjectRoot + ?? worktree?.projectRoot + ?? gitRoot + ?? launchCwd, + ); + const workspaceRoot = normalizeRoot( + explicitWorkspaceRoot + ?? worktree?.workspaceRoot + ?? gitRoot + ?? projectRoot, + ); + + if (!fs.existsSync(projectRoot)) { + throw new Error(`Project root does not exist: ${projectRoot}`); + } + if (!fs.existsSync(workspaceRoot)) { + throw new Error(`Workspace root does not exist: ${workspaceRoot}`); + } + + return { + launchCwd, + projectRoot, + workspaceRoot, + laneHint: worktree?.laneHint ?? null, + }; +} + +export function chooseInitialLane( + lanes: LaneSummary[], + context: Pick<ProjectLaunchContext, "workspaceRoot" | "laneHint">, +): LaneSummary | null { + if (!lanes.length) return null; + const hint = context.laneHint?.trim(); + if (hint) { + const byHint = lanes.find((lane) => ( + lane.id === hint + || lane.name === hint + || lane.branchRef === hint + || path.basename(lane.worktreePath) === hint + )); + if (byHint) return byHint; + } + + const workspaceRoot = normalizeRoot(context.workspaceRoot); + const byPath = [...lanes] + .sort((left, right) => normalizeRoot(right.worktreePath).length - normalizeRoot(left.worktreePath).length) + .find((lane) => { + const worktreePath = normalizeRoot(lane.worktreePath); + const attachedRootPath = lane.attachedRootPath ? normalizeRoot(lane.attachedRootPath) : null; + return ( + workspaceRoot === worktreePath + || workspaceRoot.startsWith(`${worktreePath}${path.sep}`) + || (attachedRootPath !== null + && (workspaceRoot === attachedRootPath || workspaceRoot.startsWith(`${attachedRootPath}${path.sep}`))) + ); + }); + if (byPath) return byPath; + + return lanes.find((lane) => lane.laneType === "primary") ?? lanes[0] ?? null; +} diff --git a/apps/ade-code/src/types.ts b/apps/ade-code/src/types.ts new file mode 100644 index 00000000..03dc95cc --- /dev/null +++ b/apps/ade-code/src/types.ts @@ -0,0 +1,130 @@ +import type { AppNavigationRequest, AppNavigationResult } from "../../desktop/src/shared/types/core"; +import type { + AgentChatEventEnvelope, + AgentChatModelInfo, + AgentChatSession, + AgentChatSessionSummary, + AgentChatSlashCommand, + PendingInputRequest, +} from "../../desktop/src/shared/types/chat"; +import type { LaneSummary } from "../../desktop/src/shared/types/lanes"; + +export type RuntimeMode = "attached" | "embedded"; + +export type ProjectLaunchContext = { + launchCwd: string; + projectRoot: string; + workspaceRoot: string; + laneHint: string | null; +}; + +export type ChatHistorySnapshot = { + sessionId: string; + events: AgentChatEventEnvelope[]; + truncated: boolean; +}; + +export type RunAdeActionResult<T = unknown> = { + domain: string; + action: string; + result: T; + statusHints?: Record<string, unknown>; +}; + +export type AdeCodeConnection = { + mode: RuntimeMode; + projectRoot: string; + workspaceRoot: string; + socketPath: string | null; + request<T = unknown>(method: string, params?: unknown): Promise<T>; + tool<T = unknown>(name: string, args?: Record<string, unknown>): Promise<T>; + action<T = unknown>(domain: string, action: string, args?: Record<string, unknown>): Promise<T>; + actionList<T = unknown>(domain: string, action: string, argsList: unknown[]): Promise<T>; + onChatEvent(callback: (event: AgentChatEventEnvelope) => void): () => void; + close(): Promise<void>; +}; + +export type AdeCodeModelState = { + provider: "codex" | "claude" | "opencode" | "cursor" | "droid"; + model: string; + modelId: string | null; + displayName: string; + reasoningEffort: string | null; +}; + +export type RightPaneContent = + | { kind: "empty" } + | { kind: "help"; title: string } + | { kind: "status"; rows: Array<[string, string]> } + | { + kind: "list"; + title: string; + rows: string[]; + emptyText?: string; + action?: { + kind: "switch-lane" | "switch-chat"; + ids: string[]; + }; + } + | { kind: "details"; title: string; body: string } + | { kind: "diff"; title: string; files: Array<{ path: string; additions?: number; deletions?: number; body?: string }> } + | { kind: "models"; models: AgentChatModelInfo[]; activeModelId: string | null } + | { kind: "effort"; efforts: string[]; activeEffort: string | null } + | { + kind: "form"; + title: string; + command: "new-chat" | "new-lane" | "rename" | "pr-open"; + fields: Array<{ + name: string; + label: string; + required?: boolean; + placeholder?: string; + initialValue?: string; + }>; + }; + +export type LocalNotice = { + id: string; + timestamp: string; + tone: "info" | "error" | "success"; + text: string; +}; + +export type MentionSuggestion = { + kind: "lane" | "chat" | "pr" | "file" | "commit"; + label: string; + insertText: string; + detail?: string; + filePath?: string; +}; + +export type PendingApproval = { + itemId: string; + description: string; + highStakes: boolean; + mode: "approval" | "question"; + request?: PendingInputRequest; +}; + +export type ShellData = { + project: ProjectLaunchContext; + lanes: LaneSummary[]; + sessions: AgentChatSessionSummary[]; + activeLaneId: string | null; + activeSessionId: string | null; + activeSession: AgentChatSessionSummary | null; + events: AgentChatEventEnvelope[]; + notices: LocalNotice[]; + slashCommands: AgentChatSlashCommand[]; + models: AgentChatModelInfo[]; + modelState: AdeCodeModelState; + rightPane: RightPaneContent; + tuiCount: number; + contextPercent: number | null; + desktopDriving: boolean; + streaming: boolean; +}; + +export type CreatedChat = AgentChatSession; +export type NavigateRequest = AppNavigationRequest; +export type NavigateResult = AppNavigationResult; diff --git a/apps/ade-code/tsconfig.json b/apps/ade-code/tsconfig.json new file mode 100644 index 00000000..4fdc544f --- /dev/null +++ b/apps/ade-code/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src"] +} diff --git a/apps/ade-code/tsup.config.ts b/apps/ade-code/tsup.config.ts new file mode 100644 index 00000000..fdd8bb59 --- /dev/null +++ b/apps/ade-code/tsup.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: { + cli: "src/cli.tsx", + }, + format: ["esm"], + platform: "node", + target: "node22", + outDir: "dist", + sourcemap: true, + clean: true, + splitting: false, + banner: { + js: "import { createRequire as __adeCreateRequire } from 'node:module'; const require = __adeCreateRequire(import.meta.url);", + }, + external: ["node-pty", "sql.js", "node:sqlite", "@cursor/sdk", "sqlite3"], + esbuildOptions(options) { + options.alias = { + ...(options.alias ?? {}), + sqlite: "node:sqlite", + }; + }, +}); diff --git a/apps/ade-code/vitest.config.ts b/apps/ade-code/vitest.config.ts new file mode 100644 index 00000000..840e944d --- /dev/null +++ b/apps/ade-code/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts", "src/**/*.test.tsx"], + }, +}); diff --git a/apps/desktop/scripts/dev.cjs b/apps/desktop/scripts/dev.cjs index 8161f91a..c56e07a6 100644 --- a/apps/desktop/scripts/dev.cjs +++ b/apps/desktop/scripts/dev.cjs @@ -193,7 +193,20 @@ function terminateChild(child, signal) { } } +async function ensureDevIcon() { + const generator = path.join(__dirname, "generate-dev-icon.cjs"); + if (!fs.existsSync(generator)) return; + const result = cp.spawnSync(process.execPath, [generator], { + cwd: projectRoot, + stdio: "inherit", + }); + if (result.status !== 0) { + process.stderr.write("[ade] dev icon generation failed; falling back to default icon\n"); + } +} + async function main() { + await ensureDevIcon(); const devPort = await choosePort(5173, 32); const devServerUrl = `http://localhost:${devPort}`; const remoteDebugPortRaw = diff --git a/apps/desktop/scripts/generate-dev-icon.cjs b/apps/desktop/scripts/generate-dev-icon.cjs new file mode 100644 index 00000000..52f63c5e --- /dev/null +++ b/apps/desktop/scripts/generate-dev-icon.cjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); +const path = require("node:path"); + +const projectRoot = path.resolve(__dirname, ".."); +const inputPath = path.join(projectRoot, "build", "icon.png"); +const outputPath = path.join(projectRoot, "build", "icon.dev.png"); + +const ADE_PURPLE = [127, 65, 238]; +const ADE_WHITE = [255, 255, 255]; +// Background swap target: matches the renderer's `backgroundColor` (#0F0D14) so +// the dev icon visually ties to the actual app window. +const DEV_BG = [15, 13, 20]; + +async function main() { + if (!fs.existsSync(inputPath)) { + throw new Error(`source icon not found: ${inputPath}`); + } + + if (fs.existsSync(outputPath)) { + const inputStat = fs.statSync(inputPath); + const outputStat = fs.statSync(outputPath); + if (outputStat.mtimeMs >= inputStat.mtimeMs) { + return; + } + } + + const sharp = require("sharp"); + const { data, info } = await sharp(inputPath).raw().toBuffer({ resolveWithObject: true }); + const channels = info.channels; + const out = Buffer.alloc(data.length); + + for (let i = 0; i < data.length; i += channels) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = channels === 4 ? data[i + 3] : 255; + + if (a < 8) { + out[i] = r; + out[i + 1] = g; + out[i + 2] = b; + if (channels === 4) out[i + 3] = a; + continue; + } + + // Decompose pixel as a non-negative combination of ADE purple, white, and black: + // (r, g, b) = wp * PURPLE + ww * WHITE + wd * BLACK + // Solving with PURPLE = (127, 65, 238), WHITE = (255, 255, 255): + // r - g = 62 * wp → wp = (r - g) / 62 + // g = 65*wp + 255*ww → ww = (g - 65*wp) / 255 + let wp = (r - g) / 62; + if (wp < 0) wp = 0; + if (wp > 1) wp = 1; + let ww = (g - 65 * wp) / 255; + if (ww < 0) ww = 0; + if (ww > 1) ww = 1; + + // Reproject the original pixel onto the {PURPLE, WHITE, BLACK} subspace and + // capture any residual so unrelated colors (e.g. shadow tints) survive. + const projR = wp * ADE_PURPLE[0] + ww * ADE_WHITE[0]; + const projG = wp * ADE_PURPLE[1] + ww * ADE_WHITE[1]; + const projB = wp * ADE_PURPLE[2] + ww * ADE_WHITE[2]; + const resR = r - projR; + const resG = g - projG; + const resB = b - projB; + + // Swap purple bg out for the dev bg, and white text out for purple. The + // black/shadow contribution flows through `res*` so antialiased edges stay + // smooth (and existing dark-shadow pixels just blend toward the dark bg). + let nr = wp * DEV_BG[0] + ww * ADE_PURPLE[0] + resR; + let ng = wp * DEV_BG[1] + ww * ADE_PURPLE[1] + resG; + let nb = wp * DEV_BG[2] + ww * ADE_PURPLE[2] + resB; + + if (nr < 0) nr = 0; + else if (nr > 255) nr = 255; + if (ng < 0) ng = 0; + else if (ng > 255) ng = 255; + if (nb < 0) nb = 0; + else if (nb > 255) nb = 255; + + out[i] = Math.round(nr); + out[i + 1] = Math.round(ng); + out[i + 2] = Math.round(nb); + if (channels === 4) out[i + 3] = a; + } + + await sharp(out, { raw: info }).png().toFile(outputPath); + process.stdout.write(`[generate-dev-icon] wrote ${path.relative(projectRoot, outputPath)}\n`); +} + +main().catch((err) => { + process.stderr.write(`[generate-dev-icon] failed: ${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +}); diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index d402d70a..7f2afede 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -1,4 +1,5 @@ -import { app, BrowserWindow, dialog, nativeImage, protocol, safeStorage, shell } from "electron"; +import { app, BrowserWindow, dialog, Menu, nativeImage, protocol, safeStorage, shell } from "electron"; +import { AsyncLocalStorage } from "node:async_hooks"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -394,15 +395,22 @@ function isAllowedAdeBrowserWebviewNavigation(rawUrl: string): boolean { async function createWindow(args: { logger?: Logger; + onCreated?: (win: BrowserWindow) => void; onCloseRequested?: (win: BrowserWindow, event: Electron.Event) => void; } = {}): Promise<BrowserWindow> { - // Load the app icon from the build directory. + // Load the app icon from the build directory. In dev (`npm run dev` sets + // VITE_DEV_SERVER_URL) prefer the inverted icon so the dock/window icon makes + // it obvious at a glance that this is a dev build, not the installed app. const iconDir = path.join(__dirname, "../../build"); const icoPath = path.join(iconDir, "icon.ico"); const pngPath = path.join(iconDir, "icon.png"); + const devPngPath = path.join(iconDir, "icon.dev.png"); const icnsPath = path.join(iconDir, "icon.icns"); + const isDev = !!process.env.VITE_DEV_SERVER_URL; let icon: Electron.NativeImage; - if (process.platform === "win32" && fs.existsSync(icoPath)) { + if (isDev && fs.existsSync(devPngPath)) { + icon = nativeImage.createFromPath(devPngPath); + } else if (process.platform === "win32" && fs.existsSync(icoPath)) { icon = nativeImage.createFromPath(icoPath); } else if (fs.existsSync(pngPath)) { icon = nativeImage.createFromPath(pngPath); @@ -429,6 +437,8 @@ async function createWindow(args: { }, }); + args.onCreated?.(win); + win.webContents.on("will-attach-webview", (event, webPreferences, params) => { const src = typeof params.src === "string" ? params.src : ""; if (!isAllowedAdeBrowserWebviewSource(src)) { @@ -685,6 +695,19 @@ protocol.registerSchemesAsPrivileged([ }, ]); +let pendingProjectOpenFiles: string[] = []; +let handleProjectOpenFile: ((filePath: string) => void) | null = null; + +app.on("open-file", (event, filePath) => { + event.preventDefault(); + if (!filePath) return; + if (handleProjectOpenFile) { + handleProjectOpenFile(filePath); + return; + } + pendingProjectOpenFiles.push(filePath); +}); + app.whenReady().then(async () => { /** Canonical artifacts dir for the active project; ade-artifact:// only serves under this path. */ let adeArtifactAllowedDir: string | null = null; @@ -922,14 +945,25 @@ app.whenReady().then(async () => { } const envRoot = process.env.ADE_PROJECT_ROOT; + const pendingStartupProjectRoot = + pendingProjectOpenFiles + .map((filePath) => normalizeProjectPath(filePath)) + .find((filePath) => isLikelyRepoRoot(filePath)) ?? null; + if (pendingStartupProjectRoot) { + pendingProjectOpenFiles = pendingProjectOpenFiles.filter( + (filePath) => normalizeProjectPath(filePath) !== pendingStartupProjectRoot, + ); + } const devFallbackProject = process.env.VITE_DEV_SERVER_URL ? path.resolve(process.cwd(), "..", "..") : fallbackProjectRoot; - const startupUserSelected = Boolean(envRoot && envRoot.trim().length); + const startupUserSelected = Boolean((envRoot && envRoot.trim().length) || pendingStartupProjectRoot); const initialCandidate = envRoot && envRoot.trim().length ? normalizeProjectPath(envRoot) + : pendingStartupProjectRoot + ? pendingStartupProjectRoot : devFallbackProject; const broadcast = (channel: string, payload: unknown) => { @@ -958,6 +992,8 @@ app.whenReady().then(async () => { const projectContexts = new Map<string, AppContext>(); const projectInitPromises = new Map<string, Promise<AppContext>>(); const closeContextPromises = new Map<string, Promise<void>>(); + const windowProjectRoots = new Map<number, string | null>(); + const ipcWindowScope = new AsyncLocalStorage<number | null>(); const rpcSocketCleanupByRoot = new Map<string, () => void>(); const projectLastActivatedAt = new Map<string, number>(); const mobileSyncHandoffLeases = new Map<string, number>(); @@ -969,9 +1005,35 @@ app.whenReady().then(async () => { let mobileSyncSelectedRoot: string | null = null; let dormantContext!: AppContext; let projectContextRebalancePromise: Promise<void> = Promise.resolve(); + const closeWindowWithoutQuitPrompt = new Set<number>(); + + const currentIpcWindowId = (): number | null => + ipcWindowScope.getStore() ?? null; - const emitProjectChanged = (project: ProjectInfo | null): void => { - broadcast(IPC.appProjectChanged, project); + const projectForRoot = (projectRoot: string | null): ProjectInfo | null => { + if (!projectRoot) return null; + return projectContexts.get(projectRoot)?.project ?? null; + }; + + const rootsBoundToWindows = (): Set<string> => { + const roots = new Set<string>(); + for (const root of windowProjectRoots.values()) { + if (root) roots.add(root); + } + return roots; + }; + + const emitProjectChangedToWindow = ( + windowId: number | null, + project: ProjectInfo | null, + ): void => { + const win = windowId == null ? null : BrowserWindow.fromId(windowId); + if (!win || win.isDestroyed()) return; + try { + win.webContents.send(IPC.appProjectChanged, project); + } catch { + // ignore + } }; const firstAvailableRecentProjectRoot = (): string | null => { @@ -1024,7 +1086,7 @@ app.whenReady().then(async () => { return next; }; - const setActiveProject = (projectRoot: string | null): void => { + const setForegroundProject = (projectRoot: string | null): void => { activeProjectRoot = projectRoot ? normalizeProjectRoot(projectRoot) : null; void reconcileSyncHostContexts().then(() => { notifyMobileSyncProjectCatalogChanged(); @@ -1046,7 +1108,41 @@ app.whenReady().then(async () => { } }; + const bindWindowToProject = ( + windowId: number | null, + projectRoot: string | null, + options: { emit?: boolean; foreground?: boolean } = {}, + ): void => { + const normalizedRoot = projectRoot ? normalizeProjectRoot(projectRoot) : null; + if (windowId != null) { + windowProjectRoots.set(windowId, normalizedRoot); + } + if (options.foreground ?? true) { + setForegroundProject(normalizedRoot); + } + if (normalizedRoot) { + projectLastActivatedAt.set(normalizedRoot, Date.now()); + const ctx = projectContexts.get(normalizedRoot); + if (ctx) { + persistRecentProject(ctx.project, { recordLastProject: false, preserveRecentOrder: true }); + } + } + if (options.emit !== false) { + emitProjectChangedToWindow(windowId, projectForRoot(normalizedRoot)); + } + }; + const getActiveContext = (): AppContext => { + const windowId = currentIpcWindowId(); + if (windowId != null) { + const windowProjectRoot = windowProjectRoots.get(windowId) ?? null; + if (windowProjectRoot) { + const ctx = projectContexts.get(windowProjectRoot); + if (ctx) return ctx; + windowProjectRoots.set(windowId, null); + } + return dormantContext; + } if (activeProjectRoot) { const ctx = projectContexts.get(activeProjectRoot); if (ctx) return ctx; @@ -1060,9 +1156,15 @@ app.whenReady().then(async () => { channel: string, payload: unknown, ): void => { - if (!activeProjectRoot) return; - if (normalizeProjectRoot(projectRoot) !== activeProjectRoot) return; - broadcast(channel, payload); + const normalizedRoot = normalizeProjectRoot(projectRoot); + for (const win of BrowserWindow.getAllWindows()) { + if (windowProjectRoots.get(win.id) !== normalizedRoot) continue; + try { + win.webContents.send(channel, payload); + } catch { + // ignore + } + } }; const hasActiveProjectWorkloads = async ( @@ -1183,12 +1285,14 @@ app.whenReady().then(async () => { }; const rebalanceProjectContexts = async (): Promise<void> => { - const currentActiveRoot = activeProjectRoot; - if (!currentActiveRoot) return; + const activeRoots = rootsBoundToWindows(); + if (activeProjectRoot) activeRoots.add(activeProjectRoot); + if (activeRoots.size === 0) return; + const currentActiveRoot = activeProjectRoot ?? [...activeRoots][0] ?? null; const idleRoots: string[] = []; for (const [projectRoot, ctx] of projectContexts.entries()) { - if (projectRoot === currentActiveRoot) continue; + if (activeRoots.has(projectRoot)) continue; if (await hasActiveProjectWorkloads(projectRoot, ctx)) { ctx.logger.info("project.context_retained", { projectRoot, @@ -1209,12 +1313,17 @@ app.whenReady().then(async () => { ); for (const projectRoot of idleRoots) { - if (activeProjectRoot !== currentActiveRoot) { + const nextActiveRoots = rootsBoundToWindows(); + if (activeProjectRoot) nextActiveRoots.add(activeProjectRoot); + const stillSameActiveSet = + nextActiveRoots.size === activeRoots.size + && [...activeRoots].every((root) => nextActiveRoots.has(root)); + if (!stillSameActiveSet) { return; } const ctx = projectContexts.get(projectRoot); if (!ctx) continue; - if (projectRoot === activeProjectRoot) continue; + if (rootsBoundToWindows().has(projectRoot) || projectRoot === activeProjectRoot) continue; if (warmRoots.has(projectRoot)) { ctx.logger.info("project.context_retained", { projectRoot, @@ -3644,6 +3753,33 @@ app.whenReady().then(async () => { budgetCapService, sessionDeltaService, autoUpdateService, + appNavigationService: { + navigate: async (request) => { + const normalizedRoot = normalizeProjectRoot(projectRoot); + let targetWindow = BrowserWindow.getAllWindows() + .find((win) => !win.isDestroyed() && windowProjectRoots.get(win.id) === normalizedRoot) ?? null; + if (!targetWindow) { + const opened = await openAdeWindow({ projectRoot }); + targetWindow = opened.windowId != null ? BrowserWindow.fromId(opened.windowId) : null; + } + if (!targetWindow || targetWindow.isDestroyed()) { + return { + ok: false, + mode: "unavailable" as const, + message: "No ADE desktop window is available for this project.", + }; + } + if (targetWindow.isMinimized()) targetWindow.restore(); + targetWindow.show(); + targetWindow.focus(); + targetWindow.webContents.send(IPC.appNavigate, request); + return { + ok: true, + mode: "desktop" as const, + windowId: targetWindow.id, + }; + }, + }, issueInventoryService, eventBuffer: rpcEventBuffer, dispose: () => {}, // desktop manages service lifecycle @@ -3705,8 +3841,15 @@ app.whenReady().then(async () => { }, }); stop = startJsonRpcServer(rpcHandler, transport, { nonFatal: true }); + const unsubscribeChatEvents = rpcRuntime.agentChatService?.subscribeToEvents((event) => { + stop?.notify("chat/event", event); + }) ?? (() => {}); + let removedConnection = false; const removeConnection = (): void => { + if (removedConnection) return; + removedConnection = true; activeRpcConnections.delete(conn); + unsubscribeChatEvents(); }; conn.once("close", removeConnection); conn.once("end", removeConnection); @@ -4275,7 +4418,7 @@ app.whenReady().then(async () => { for (const root of roots) { await closeProjectContext(root); } - setActiveProject(null); + setForegroundProject(null); }; async function mobileProjectSummaryForContext( @@ -4597,12 +4740,11 @@ app.whenReady().then(async () => { const existing = projectContexts.get(repoRoot); if (existing) { existing.hasUserSelectedProject = true; - setActiveProject(repoRoot); persistRecentProject(existing.project, { recordLastProject: true, preserveRecentOrder: isKnownRecentProject, }); - emitProjectChanged(existing.project); + bindWindowToProject(currentIpcWindowId(), repoRoot, { emit: true, foreground: true }); scheduleProjectContextRebalance(); projectOpenLogger.info("project.open.done", { selectedPath, @@ -4651,12 +4793,11 @@ app.whenReady().then(async () => { const ctx = await initPromise; ctx.hasUserSelectedProject = true; - setActiveProject(repoRoot); persistRecentProject(ctx.project, { recordLastProject: true, recordRecent: false, }); - emitProjectChanged(ctx.project); + bindWindowToProject(currentIpcWindowId(), repoRoot, { emit: true, foreground: true }); scheduleProjectContextRebalance(); projectOpenLogger.info("project.open.done", { selectedPath, @@ -4680,22 +4821,34 @@ app.whenReady().then(async () => { const closeProjectByPath = async (projectRoot: string): Promise<void> => { const normalizedRoot = normalizeProjectRoot(projectRoot); const wasActive = activeProjectRoot === normalizedRoot; + for (const [windowId, root] of windowProjectRoots) { + if (root === normalizedRoot) { + windowProjectRoots.set(windowId, null); + emitProjectChangedToWindow(windowId, null); + } + } await closeProjectContext(normalizedRoot); if (wasActive) { + setForegroundProject(firstOpenWindowProjectRoot()); dormantContext = createDormantProjectContext(normalizedRoot); - emitProjectChanged(null); } }; const closeCurrentProject = async () => { const current = getActiveContext(); const previousRoot = current.project?.rootPath ?? ""; + const windowId = currentIpcWindowId(); + if (windowId != null) { + bindWindowToProject(windowId, null, { emit: true, foreground: true }); + dormantContext = createDormantProjectContext(previousRoot); + scheduleProjectContextRebalance(); + return; + } if (activeProjectRoot) { await closeProjectContext(activeProjectRoot); } - setActiveProject(null); + setForegroundProject(null); dormantContext = createDormantProjectContext(previousRoot); - emitProjectChanged(null); }; dormantContext = createDormantProjectContext(); @@ -4843,7 +4996,7 @@ app.whenReady().then(async () => { } catch { // ignore } - setActiveProject(null); + setForegroundProject(null); dormantContext = createDormantProjectContext(previousRoot); try { @@ -4861,38 +5014,85 @@ app.whenReady().then(async () => { }); }; - const confirmQuitWarning = (): boolean => { - if (quitWarningAcknowledged || shutdownRequested) return true; - const options = { + const showWindowCloseWarning = ( + ownerWindow: BrowserWindow | null | undefined, + options: { + buttons: string[]; + title: string; + message: string; + detail: string; + rememberQuitAcknowledgement?: boolean; + }, + ): boolean => { + if (shutdownRequested) return true; + if (options.rememberQuitAcknowledgement && quitWarningAcknowledged) return true; + const dialogOptions = { type: "warning" as const, - buttons: ["Keep ADE open", "Quit ADE"], + buttons: options.buttons, defaultId: 0, cancelId: 0, noLink: true, - title: "Quit ADE?", - message: "Save your work before closing ADE.", - detail: - "Quitting ADE will end any running agents and stop background processes started by ADE, including OpenCode servers, terminal sessions, and test runs.", + title: options.title, + message: options.message, + detail: options.detail, }; - const parentWindow = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]; + const parentWindow = + ownerWindow && !ownerWindow.isDestroyed() + ? ownerWindow + : BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0]; const response = parentWindow - ? dialog.showMessageBoxSync(parentWindow, options) - : dialog.showMessageBoxSync(options); + ? dialog.showMessageBoxSync(parentWindow, dialogOptions) + : dialog.showMessageBoxSync(dialogOptions); if (response !== 1) { return false; } - quitWarningAcknowledged = true; + if (options.rememberQuitAcknowledgement) { + quitWarningAcknowledged = true; + } return true; }; + const confirmQuitWarning = (ownerWindow?: BrowserWindow | null): boolean => + showWindowCloseWarning(ownerWindow, { + buttons: ["Keep ADE open", "Quit ADE"], + title: "Quit ADE?", + message: "Save your work before closing ADE.", + detail: + "Quitting ADE will end any running agents and stop background processes started by ADE, including OpenCode servers, terminal sessions, and test runs.", + rememberQuitAcknowledgement: true, + }); + + const confirmCloseWindowWarning = (ownerWindow: BrowserWindow): boolean => + showWindowCloseWarning(ownerWindow, { + buttons: ["Keep window open", "Close window"], + title: "Close ADE window?", + message: "Close this ADE window?", + detail: + "ADE will keep running in other windows. Active agents and background processes continue unless you quit ADE.", + rememberQuitAcknowledgement: false, + }); + + const closeWindowWithoutPrompt = (win: BrowserWindow): void => { + closeWindowWithoutQuitPrompt.add(win.id); + win.close(); + if (!win.isDestroyed()) { + closeWindowWithoutQuitPrompt.delete(win.id); + } + }; + const handleMainWindowCloseRequested = ( - _win: BrowserWindow, + win: BrowserWindow, event: Electron.Event, ): void => { if (shutdownRequested) return; - if (BrowserWindow.getAllWindows().length > 1) return; + if (closeWindowWithoutQuitPrompt.delete(win.id)) return; event.preventDefault(); - if (!confirmQuitWarning()) return; + if (BrowserWindow.getAllWindows().filter((openWindow) => !openWindow.isDestroyed()).length > 1) { + if (!confirmCloseWindowWarning(win)) return; + closeWindowWithoutPrompt(win); + return; + } + if (!confirmQuitWarning(win)) return; requestAppShutdown({ reason: "window_close", exitCode: 0 }); }; @@ -4977,6 +5177,169 @@ app.whenReady().then(async () => { }); }); + const firstOpenWindowProjectRoot = (): string | null => { + for (const win of BrowserWindow.getAllWindows()) { + const root = windowProjectRoots.get(win.id); + if (root) return root; + } + return null; + }; + + const registerWindowSession = (win: BrowserWindow, projectRoot: string | null = null): void => { + windowProjectRoots.set(win.id, projectRoot ? normalizeProjectRoot(projectRoot) : null); + win.on("focus", () => { + setForegroundProject(windowProjectRoots.get(win.id) ?? null); + builtInBrowserService.attachToWindow(win); + }); + win.on("closed", () => { + const previousRoot = windowProjectRoots.get(win.id) ?? null; + windowProjectRoots.delete(win.id); + if (activeProjectRoot === previousRoot) { + setForegroundProject(firstOpenWindowProjectRoot()); + } + scheduleProjectContextRebalance(); + }); + }; + + const getWindowSession = (windowId: number | null): { windowId: number | null; project: ProjectInfo | null } => { + if (windowId == null) { + return { windowId: null, project: projectForRoot(activeProjectRoot) }; + } + return { + windowId, + project: projectForRoot(windowProjectRoots.get(windowId) ?? null), + }; + }; + + const openAdeWindow = async ( + args: { projectRoot?: string | null } = {}, + ): Promise<{ windowId: number | null; project: ProjectInfo | null }> => { + const win = await createWindow({ + logger: getActiveContext().logger, + onCreated: (createdWindow) => registerWindowSession(createdWindow, null), + onCloseRequested: handleMainWindowCloseRequested, + }); + builtInBrowserService.attachToWindow(win); + if (args.projectRoot) { + await ipcWindowScope.run(win.id, async () => { + await switchProjectFromDialog(args.projectRoot!); + }); + } else { + emitProjectChangedToWindow(win.id, null); + } + return getWindowSession(win.id); + }; + + const openProjectFileRequest = async (filePath: string): Promise<void> => { + const projectRoot = normalizeProjectPath(filePath); + if (!isLikelyRepoRoot(projectRoot)) return; + const normalizedRoot = normalizeProjectRoot(projectRoot); + const existing = BrowserWindow.getAllWindows() + .find((win) => !win.isDestroyed() && windowProjectRoots.get(win.id) === normalizedRoot) ?? null; + if (existing) { + if (existing.isMinimized()) existing.restore(); + existing.show(); + existing.focus(); + return; + } + await openAdeWindow({ projectRoot: normalizedRoot }); + }; + + handleProjectOpenFile = (filePath) => { + void openProjectFileRequest(filePath).catch((error) => { + getActiveContext().logger.warn("project.open_file_request_failed", { + filePath, + error: error instanceof Error ? error.message : String(error), + }); + }); + }; + + for (const filePath of pendingProjectOpenFiles.splice(0)) { + handleProjectOpenFile(filePath); + } + + const closeAdeWindow = async (windowId: number | null): Promise<{ closed: boolean }> => { + if (windowId == null) return { closed: false }; + const win = BrowserWindow.fromId(windowId); + if (!win || win.isDestroyed()) return { closed: false }; + closeWindowWithoutPrompt(win); + return { closed: true }; + }; + + const installApplicationMenu = (): void => { + const template: Electron.MenuItemConstructorOptions[] = [ + ...(process.platform === "darwin" + ? [{ + label: app.name, + submenu: [ + { role: "about" as const }, + { type: "separator" as const }, + { role: "hide" as const }, + { role: "hideOthers" as const }, + { role: "unhide" as const }, + { type: "separator" as const }, + { role: "quit" as const }, + ], + }] + : []), + { + label: "File", + submenu: [ + { + label: "New window", + accelerator: "CommandOrControl+N", + click: () => { + void openAdeWindow(); + }, + }, + { type: "separator" }, + { role: "close" }, + ], + }, + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(process.platform === "darwin" + ? [ + { type: "separator" as const }, + { role: "front" as const }, + ] + : []), + ], + }, + ]; + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + }; + + installApplicationMenu(); + registerIpc({ getCtx: () => { const ctx = getActiveContext(); @@ -4989,6 +5352,11 @@ app.whenReady().then(async () => { return getMobileSyncService(); }, resolveSyncService: ensureMobileSyncService, + runWithIpcWindow: (event, fn) => + ipcWindowScope.run(BrowserWindow.fromWebContents(event.sender)?.id ?? null, fn), + getWindowSession, + createWindow: openAdeWindow, + closeWindow: closeAdeWindow, switchProjectFromDialog, closeCurrentProject, closeProjectByPath, @@ -5003,24 +5371,22 @@ app.whenReady().then(async () => { try { await switchProjectFromDialog(initialCandidate); } catch { - setActiveProject(null); + setForegroundProject(null); dormantContext = createDormantProjectContext(); } } + const initialWindowProjectRoot = startupUserSelected ? activeProjectRoot : null; const initialWindow = await createWindow({ logger: getActiveContext().logger, + onCreated: (createdWindow) => registerWindowSession(createdWindow, initialWindowProjectRoot), onCloseRequested: handleMainWindowCloseRequested, }); builtInBrowserService.attachToWindow(initialWindow); app.on("activate", async () => { if (BrowserWindow.getAllWindows().length === 0) { - const activatedWindow = await createWindow({ - logger: getActiveContext().logger, - onCloseRequested: handleMainWindowCloseRequested, - }); - builtInBrowserService.attachToWindow(activatedWindow); + await openAdeWindow(); } }); diff --git a/apps/desktop/src/main/services/adeActions/registry.ts b/apps/desktop/src/main/services/adeActions/registry.ts index 8673bbea..dd0a8656 100644 --- a/apps/desktop/src/main/services/adeActions/registry.ts +++ b/apps/desktop/src/main/services/adeActions/registry.ts @@ -210,13 +210,18 @@ export const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly stri chat: [ "createSession", "deleteSession", + "dispose", "getAvailableModels", + "getChatEventHistory", "getSessionSummary", "getSlashCommands", "interrupt", "listSessions", + "approveToolUse", + "respondToInput", "resumeSession", "sendMessage", + "updateSession", ], keybindings: ["get", "set"], onboarding: [ @@ -318,7 +323,15 @@ export const ADE_ACTION_ALLOWLIST: Partial<Record<AdeActionDomain, readonly stri "setToken", ], linear_dispatcher: ["dispatchIssue", "getDashboard", "listEmployees", "listQueue"], - linear_issue_tracker: ["getStatus", "listIssues"], + linear_issue_tracker: [ + "createComment", + "fetchIssueById", + "fetchIssuesByIds", + "getStatus", + "listIssues", + "listUsers", + "updateIssueAssignee", + ], linear_sync: ["getDashboard", "getRunDetail", "listQueue", "resolveQueueItem", "runSyncNow"], linear_ingress: ["ensureRelayWebhook", "getStatus", "listRecentEvents"], linear_routing: ["simulateRoute"], diff --git a/apps/desktop/src/main/services/ai/cliExecutableResolver.ts b/apps/desktop/src/main/services/ai/cliExecutableResolver.ts index 63215095..d26a2eed 100644 --- a/apps/desktop/src/main/services/ai/cliExecutableResolver.ts +++ b/apps/desktop/src/main/services/ai/cliExecutableResolver.ts @@ -277,6 +277,7 @@ function readShellPath( { encoding: "utf-8", env, + stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs, }, ); diff --git a/apps/desktop/src/main/services/ipc/registerIpc.ts b/apps/desktop/src/main/services/ipc/registerIpc.ts index c6069b09..75c5f5e0 100644 --- a/apps/desktop/src/main/services/ipc/registerIpc.ts +++ b/apps/desktop/src/main/services/ipc/registerIpc.ts @@ -1904,6 +1904,10 @@ export function registerIpc({ getCtx, getSyncService, resolveSyncService, + runWithIpcWindow, + getWindowSession, + createWindow, + closeWindow, switchProjectFromDialog, closeCurrentProject, closeProjectByPath, @@ -1913,6 +1917,10 @@ export function registerIpc({ getCtx: () => AppContext; getSyncService?: () => ReturnType<typeof createSyncService> | null | undefined; resolveSyncService?: () => Promise<ReturnType<typeof createSyncService> | null | undefined>; + runWithIpcWindow?: <T>(event: { sender: Electron.WebContents }, fn: () => T | Promise<T>) => T | Promise<T>; + getWindowSession?: (windowId: number | null) => { windowId: number | null; project: ProjectInfo | null }; + createWindow?: (args?: { projectRoot?: string | null }) => Promise<{ windowId: number | null; project: ProjectInfo | null }>; + closeWindow?: (windowId: number | null) => Promise<{ closed: boolean }>; switchProjectFromDialog: (selectedPath: string) => Promise<ProjectInfo>; closeCurrentProject: () => Promise<void>; closeProjectByPath: (projectRoot: string) => Promise<void>; @@ -2101,8 +2109,21 @@ export function registerIpc({ type TracedIpcMain = typeof ipcMain & { __adeTraceWrapped?: boolean; __adeOriginalHandle?: typeof ipcMain.handle; + __adeWindowScopeWrapped?: boolean; + __adeWindowScopeOriginalHandle?: typeof ipcMain.handle; }; + const tracedIpcMain = ipcMain as TracedIpcMain; + if (runWithIpcWindow && !tracedIpcMain.__adeWindowScopeWrapped) { + const originalHandle = tracedIpcMain.handle.bind(ipcMain); + tracedIpcMain.__adeWindowScopeOriginalHandle = originalHandle; + tracedIpcMain.handle = ((channel, listener) => + originalHandle(channel, (event, ...args) => + runWithIpcWindow(event, () => listener(event, ...args)) + )) as typeof ipcMain.handle; + tracedIpcMain.__adeWindowScopeWrapped = true; + } + type IpcInvokeAggregate = { channel: string; winId: number | null; @@ -2188,7 +2209,6 @@ export function registerIpc({ } }; - const tracedIpcMain = ipcMain as TracedIpcMain; if (traceIpcInvokes && !tracedIpcMain.__adeTraceWrapped) { const originalHandle = tracedIpcMain.handle.bind(ipcMain); tracedIpcMain.__adeOriginalHandle = originalHandle; @@ -3206,6 +3226,37 @@ export function registerIpc({ return ctx.hasUserSelectedProject ? ctx.project : null; }); + ipcMain.handle(IPC.appGetWindowSession, async (event) => { + const windowId = BrowserWindow.fromWebContents(event.sender)?.id ?? null; + if (getWindowSession) return getWindowSession(windowId); + const ctx = getCtx(); + return { + windowId, + project: ctx.hasUserSelectedProject ? ctx.project : null, + }; + }); + + ipcMain.handle(IPC.appNewWindow, async () => { + if (!createWindow) return { windowId: null }; + const result = await createWindow({ projectRoot: null }); + return { windowId: result.windowId }; + }); + + ipcMain.handle(IPC.appOpenProjectInNewWindow, async (_event, arg: { rootPath?: string }) => { + const rootPath = typeof arg?.rootPath === "string" ? arg.rootPath.trim() : ""; + if (!rootPath) throw new Error("rootPath is required"); + if (!createWindow) return { windowId: null, project: null }; + return createWindow({ projectRoot: rootPath }); + }); + + ipcMain.handle(IPC.appCloseWindow, async (event, arg: { windowId?: number | null } = {}) => { + const requestedWindowId = Number.isFinite(arg?.windowId) + ? Number(arg.windowId) + : BrowserWindow.fromWebContents(event.sender)?.id ?? null; + if (!closeWindow) return { closed: false }; + return closeWindow(requestedWindowId); + }); + ipcMain.handle(IPC.appOpenExternal, async (_event, arg: { url: string }): Promise<void> => { const urlRaw = typeof arg?.url === "string" ? arg.url.trim() : ""; if (!urlRaw) return; diff --git a/apps/desktop/src/preload/global.d.ts b/apps/desktop/src/preload/global.d.ts index 47957103..02682b9a 100644 --- a/apps/desktop/src/preload/global.d.ts +++ b/apps/desktop/src/preload/global.d.ts @@ -12,6 +12,7 @@ import type { AdoptAttachedLaneArgs, UnregisteredLaneCandidate, AppInfo, + AppNavigationRequest, AutoUpdateSnapshot, ClearLocalAdeDataArgs, ClearLocalAdeDataResult, @@ -716,9 +717,21 @@ declare global { ping: () => Promise<"pong">; getInfo: () => Promise<AppInfo>; getProject: () => Promise<ProjectInfo | null>; + getWindowSession: () => Promise<{ + windowId: number | null; + project: ProjectInfo | null; + }>; + newWindow: () => Promise<{ windowId: number | null }>; + openProjectInNewWindow: ( + rootPath: string, + ) => Promise<{ windowId: number | null; project: ProjectInfo | null }>; + closeWindow: (windowId?: number | null) => Promise<{ closed: boolean }>; onProjectChanged: ( cb: (project: ProjectInfo | null) => void, ) => () => void; + onNavigate: ( + cb: (request: AppNavigationRequest) => void, + ) => () => void; openExternal: (url: string) => Promise<void>; revealPath: (path: string) => Promise<void>; openPath: (path: string) => Promise<void>; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 2c029ecc..ca3b2344 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -16,6 +16,7 @@ import type { AdoptAttachedLaneArgs, UnregisteredLaneCandidate, AppInfo, + AppNavigationRequest, AutoUpdateSnapshot, ClearLocalAdeDataArgs, ClearLocalAdeDataResult, @@ -1060,6 +1061,16 @@ contextBridge.exposeInMainWorld("ade", { getInfo: async (): Promise<AppInfo> => ipcRenderer.invoke(IPC.appGetInfo), getProject: async (): Promise<ProjectInfo | null> => ipcRenderer.invoke(IPC.appGetProject), + getWindowSession: async (): Promise<{ windowId: number | null; project: ProjectInfo | null }> => + ipcRenderer.invoke(IPC.appGetWindowSession), + newWindow: async (): Promise<{ windowId: number | null }> => + ipcRenderer.invoke(IPC.appNewWindow), + openProjectInNewWindow: async ( + rootPath: string, + ): Promise<{ windowId: number | null; project: ProjectInfo | null }> => + ipcRenderer.invoke(IPC.appOpenProjectInNewWindow, { rootPath }), + closeWindow: async (windowId?: number | null): Promise<{ closed: boolean }> => + ipcRenderer.invoke(IPC.appCloseWindow, { windowId: windowId ?? null }), onProjectChanged: (cb: (project: ProjectInfo | null) => void) => { const listener = ( _event: Electron.IpcRendererEvent, @@ -1071,6 +1082,14 @@ contextBridge.exposeInMainWorld("ade", { ipcRenderer.on(IPC.appProjectChanged, listener); return () => ipcRenderer.removeListener(IPC.appProjectChanged, listener); }, + onNavigate: (cb: (request: AppNavigationRequest) => void) => { + const listener = ( + _event: Electron.IpcRendererEvent, + payload: AppNavigationRequest, + ) => cb(payload); + ipcRenderer.on(IPC.appNavigate, listener); + return () => ipcRenderer.removeListener(IPC.appNavigate, listener); + }, openExternal: async (url: string): Promise<void> => ipcRenderer.invoke(IPC.appOpenExternal, { url }), revealPath: async (path: string): Promise<void> => diff --git a/apps/desktop/src/renderer/browserMock.ts b/apps/desktop/src/renderer/browserMock.ts index c59751df..f2f991ac 100644 --- a/apps/desktop/src/renderer/browserMock.ts +++ b/apps/desktop/src/renderer/browserMock.ts @@ -2643,7 +2643,12 @@ if (typeof window !== "undefined" && shouldInstallBrowserMock(window)) { env: {}, }), getProject: resolved(MOCK_PROJECT), + getWindowSession: resolved({ windowId: 1, project: MOCK_PROJECT }), + newWindow: resolved({ windowId: 2 }), + openProjectInNewWindow: resolvedArg({ windowId: 2, project: MOCK_PROJECT }), + closeWindow: resolvedArg({ closed: false }), onProjectChanged: () => () => {}, + onNavigate: () => () => {}, openExternal: resolvedArg(undefined), revealPath: resolvedArg(undefined), writeClipboardText: resolvedArg(undefined), diff --git a/apps/desktop/src/renderer/components/app/App.tsx b/apps/desktop/src/renderer/components/app/App.tsx index b0e5e70f..005bc28a 100644 --- a/apps/desktop/src/renderer/components/app/App.tsx +++ b/apps/desktop/src/renderer/components/app/App.tsx @@ -68,6 +68,7 @@ import { useAppStore } from "../../state/appStore"; import { getDirtyFileTextForWindow } from "../../lib/dirtyWorkspaceBuffers"; import { getAiStatusCached } from "../../lib/aiDiscoveryCache"; import { dispatchWorkSurfaceRevealed } from "../terminals/workSurfaceVisibility"; +import type { AppNavigationRequest } from "../../../shared/types"; const StartupSplashScreen = ( <div className="flex h-full w-full flex-col items-center justify-center relative overflow-hidden" style={{ background: "var(--color-bg)" }}> @@ -290,6 +291,43 @@ function ShellLayout() { ); } +function AppNavigationBridge() { + const navigate = useNavigate(); + React.useEffect(() => { + const onNavigate = window.ade?.app?.onNavigate; + if (!onNavigate) return; + return onNavigate((request: AppNavigationRequest) => { + const target = request.target; + if (target.kind === "chat" || target.kind === "work") { + const params = new URLSearchParams(); + if (target.sessionId) params.set("sessionId", target.sessionId); + if (target.laneId) params.set("laneId", target.laneId); + navigate(`/work${params.toString() ? `?${params.toString()}` : ""}`); + return; + } + if (target.kind === "lane") { + const params = new URLSearchParams(); + params.set("laneId", target.laneId); + if (target.sessionId) params.set("sessionId", target.sessionId); + navigate(`/lanes?${params.toString()}`); + return; + } + if (target.kind === "pr") { + const params = new URLSearchParams(); + if (target.prId) params.set("prId", target.prId); + if (target.prNumber != null) params.set("pr", String(target.prNumber)); + if (target.laneId) params.set("laneId", target.laneId); + navigate(`/prs${params.toString() ? `?${params.toString()}` : ""}`); + return; + } + if (target.kind === "route") { + navigate(target.route.startsWith("/") ? target.route : `/${target.route}`); + } + }); + }, [navigate]); + return null; +} + export function App() { const theme = useAppStore((s) => s.theme); const projectRoot = useAppStore((s) => s.project?.rootPath ?? null); @@ -317,6 +355,7 @@ export function App() { <Router> <div data-theme={theme} className="h-full bg-bg text-fg font-sans antialiased selection:bg-accent/30"> <OnboardingBootstrap /> + <AppNavigationBridge /> <Routes> <Route path="/startup" element={<Navigate to="/work" replace />} /> <Route element={<ShellLayout />}> diff --git a/apps/desktop/src/renderer/components/app/TopBar.test.tsx b/apps/desktop/src/renderer/components/app/TopBar.test.tsx index 2cf12252..6375e317 100644 --- a/apps/desktop/src/renderer/components/app/TopBar.test.tsx +++ b/apps/desktop/src/renderer/components/app/TopBar.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, cleanup, createEvent, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { TopBar } from "./TopBar"; import { useAppStore } from "../../state/appStore"; @@ -101,12 +101,42 @@ function resetStore() { } as any); } +function makeDataTransfer(data: Record<string, string>, dropEffect = "move") { + return { + dropEffect, + effectAllowed: "move", + types: Object.keys(data), + getData: vi.fn((type: string) => data[type] ?? ""), + setData: vi.fn(), + }; +} + +function fireProjectTabDragEnd( + element: HTMLElement, + dataTransfer: ReturnType<typeof makeDataTransfer>, +) { + const event = createEvent.dragEnd(element, { dataTransfer }); + Object.defineProperty(event, "clientX", { value: -1 }); + Object.defineProperty(event, "clientY", { value: 12 }); + Object.defineProperty(event, "dataTransfer", { value: dataTransfer }); + fireEvent(element, event); +} + describe("TopBar", () => { const originalAde = globalThis.window.ade; beforeEach(() => { resetStore(); globalThis.window.ade = { + app: { + getWindowSession: vi.fn(async () => ({ windowId: 1, project: useAppStore.getState().project })), + newWindow: vi.fn(async () => ({ windowId: 2 })), + openProjectInNewWindow: vi.fn(async (rootPath: string) => ({ + windowId: 2, + project: { rootPath, name: rootPath.split("/").pop() ?? rootPath }, + })), + closeWindow: vi.fn(async () => ({ closed: true })), + }, project: { listRecent: vi.fn(async () => [ { @@ -161,17 +191,67 @@ describe("TopBar", () => { expect(globalThis.window.ade.sync.getStatus).not.toHaveBeenCalled(); }); - it("does not eagerly resolve icons for non-current recent projects", async () => { + it("does not render recent projects as tabs before a project is open", async () => { useAppStore.setState({ project: null } as any); render(<TopBar />); - expect(await screen.findByText("ADE")).toBeTruthy(); + await waitFor(() => { + expect(globalThis.window.ade.project.listRecent).toHaveBeenCalled(); + }); + expect(screen.queryByTitle("/Users/arul/ADE")).toBeNull(); await new Promise((resolve) => setTimeout(resolve, 850)); expect(globalThis.window.ade.project.resolveIcon).not.toHaveBeenCalled(); }); + it("opens a blank ADE window from the top bar", async () => { + render(<TopBar />); + + fireEvent.click(await screen.findByTitle("New window")); + + expect(globalThis.window.ade.app.newWindow).toHaveBeenCalledTimes(1); + }); + + it("consolidates a cross-window project tab dropped onto the same project", async () => { + render(<TopBar />); + + const tab = await screen.findByTitle("/Users/arul/ADE"); + await waitFor(() => { + expect(globalThis.window.ade.app.getWindowSession).toHaveBeenCalled(); + }); + + fireEvent.drop(tab, { + dataTransfer: makeDataTransfer({ + "application/x-ade-project-root": "/Users/arul/ADE", + "application/x-ade-window-id": "2", + }), + }); + + expect(globalThis.window.ade.app.closeWindow).toHaveBeenCalledWith(2); + expect(useAppStore.getState().switchProjectToPath).not.toHaveBeenCalled(); + }); + + it("does not detach again after a project tab is dropped onto an ADE target", async () => { + render(<TopBar />); + + const tab = await screen.findByTitle("/Users/arul/ADE"); + + fireProjectTabDragEnd(tab, makeDataTransfer({}, "move")); + + expect(globalThis.window.ade.app.openProjectInNewWindow).not.toHaveBeenCalled(); + }); + + it("detaches a project tab when it is dragged outside without an ADE drop target", async () => { + render(<TopBar />); + + const tab = await screen.findByTitle("/Users/arul/ADE"); + + fireProjectTabDragEnd(tab, makeDataTransfer({}, "none")); + + expect(globalThis.window.ade.app.openProjectInNewWindow).toHaveBeenCalledWith("/Users/arul/ADE"); + }); + it("opens the phone sync drawer from the host status control", async () => { render(<TopBar />); @@ -276,7 +356,7 @@ describe("TopBar", () => { expect((await screen.findByRole("alert")).textContent).toContain("Project icon must be 10 MB or smaller."); }); - it("confirms before removing a project tab", async () => { + it("confirms before closing a project tab", async () => { const confirm = vi.spyOn(window, "confirm").mockReturnValue(false); render(<TopBar />); @@ -284,11 +364,12 @@ describe("TopBar", () => { await screen.findByText("ADE"); fireEvent.click(screen.getByTitle("Remove project")); - expect(confirm).toHaveBeenCalledWith(expect.stringContaining("Close \"ADE\" and remove it from project tabs?")); + expect(confirm).toHaveBeenCalledWith(expect.stringContaining("Close \"ADE\" project tab?")); expect(globalThis.window.ade.project.forgetRecent).not.toHaveBeenCalled(); + expect(useAppStore.getState().closeProject).not.toHaveBeenCalled(); }); - it("removes the project tab after confirmation", async () => { + it("closes the active project tab after confirmation without removing it from recents", async () => { vi.spyOn(window, "confirm").mockReturnValue(true); render(<TopBar />); @@ -297,7 +378,8 @@ describe("TopBar", () => { fireEvent.click(screen.getByTitle("Remove project")); await waitFor(() => { - expect(globalThis.window.ade.project.forgetRecent).toHaveBeenCalledWith("/Users/arul/ADE"); + expect(useAppStore.getState().closeProject).toHaveBeenCalledTimes(1); }); + expect(globalThis.window.ade.project.forgetRecent).not.toHaveBeenCalled(); }); }); diff --git a/apps/desktop/src/renderer/components/app/TopBar.tsx b/apps/desktop/src/renderer/components/app/TopBar.tsx index 0bcc4137..b6925b17 100644 --- a/apps/desktop/src/renderer/components/app/TopBar.tsx +++ b/apps/desktop/src/renderer/components/app/TopBar.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ChatCircleDots, CircleNotch, DeviceMobile, Folder, FolderOpen, Plus, Minus, Trash, UploadSimple, X } from "@phosphor-icons/react"; +import { ArrowSquareOut, ChatCircleDots, CircleNotch, DeviceMobile, Folder, FolderOpen, Plus, Minus, Trash, UploadSimple, X } from "@phosphor-icons/react"; import * as Dialog from "@radix-ui/react-dialog"; import { useAppStore } from "../../state/appStore"; @@ -22,6 +22,8 @@ import { PublishToGitHubDialog } from "../projects/PublishToGitHubDialog"; import { SyncDevicesSection } from "../settings/SyncDevicesSection"; const RUNNING_LANE_PROCESS_STATES: ProcessRuntime["status"][] = ["starting", "running", "degraded"]; +const ADE_PROJECT_TAB_ROOT_MIME = "application/x-ade-project-root"; +const ADE_PROJECT_TAB_WINDOW_MIME = "application/x-ade-window-id"; // Bounded LRU so we don't accumulate icons for every project ever opened in // long-lived sessions. 24 entries keeps the working set hot for typical usage @@ -171,12 +173,16 @@ function projectIconErrorMessage(error: unknown): string { return cleaned || "Failed to update project icon."; } +function fallbackProjectName(rootPath: string): string { + return rootPath.split(/[\\/]/).filter(Boolean).pop() ?? rootPath; +} + function confirmProjectTabRemoval(projectName: string, isCurrent: boolean, isMissing: boolean): boolean { const label = projectName.trim() || "this project"; const action = isCurrent && !isMissing - ? `Close "${label}" and remove it from project tabs?` - : `Remove "${label}" from project tabs?`; - return window.confirm(`${action}\n\nThis does not delete any files on disk.`); + ? `Close "${label}" project tab?` + : `Close "${label}" project tab?`; + return window.confirm(`${action}\n\nThis does not remove it from Recent Projects or delete any files on disk.`); } function deriveSyncLabel(snapshot: SyncRoleSnapshot | null): string | null { @@ -467,8 +473,10 @@ export function TopBar() { const [phoneSyncOpen, setPhoneSyncOpen] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false); const [publishOpen, setPublishOpen] = useState(false); + const [openProjectTabRoots, setOpenProjectTabRoots] = useState<string[]>([]); const [dragIdx, setDragIdx] = useState<number | null>(null); const [dropIdx, setDropIdx] = useState<number | null>(null); + const [windowId, setWindowId] = useState<number | null>(null); const phoneSyncPanelRef = useRef<HTMLDivElement | null>(null); const dragCounterRef = useRef(0); const isProjectBusy = projectTransition != null || relocatingPath != null; @@ -517,6 +525,51 @@ export function TopBar() { fetchRecent(); }, [project?.rootPath, fetchRecent]); + useEffect(() => { + const rootPath = project?.rootPath ?? null; + if (!rootPath) { + setOpenProjectTabRoots([]); + return; + } + setOpenProjectTabRoots((prev) => + prev.includes(rootPath) ? prev : [...prev, rootPath] + ); + }, [project?.rootPath]); + + const projectTabs = useMemo<RecentProjectSummary[]>(() => + openProjectTabRoots.map((rootPath) => { + const recent = recentProjects.find((entry) => entry.rootPath === rootPath); + if (recent) return recent; + return { + rootPath, + displayName: + project?.rootPath === rootPath + ? project.displayName ?? fallbackProjectName(rootPath) + : fallbackProjectName(rootPath), + exists: true, + lastOpenedAt: "", + }; + }), + [openProjectTabRoots, project, recentProjects]); + + useEffect(() => { + let cancelled = false; + const getWindowSession = (window as unknown as { + ade?: { app?: { getWindowSession?: typeof window.ade.app.getWindowSession } }; + }).ade?.app?.getWindowSession; + if (typeof getWindowSession !== "function") return undefined; + getWindowSession() + .then((session) => { + if (!cancelled) setWindowId(session.windowId); + }) + .catch(() => { + if (!cancelled) setWindowId(null); + }); + return () => { + cancelled = true; + }; + }, []); + useEffect(() => { if (!phoneSyncOpen) return; const frame = window.requestAnimationFrame(() => { @@ -634,6 +687,11 @@ export function TopBar() { openNewTab(); }, [isProjectBusy, openNewTab]); + const handleOpenNewWindow = useCallback(() => { + if (isProjectBusy) return; + window.ade.app.newWindow().catch(() => {}); + }, [isProjectBusy]); + const handleSwitchProject = useCallback((rootPath: string) => { if (isProjectBusy) return; if (project?.rootPath === rootPath) { @@ -645,8 +703,8 @@ export function TopBar() { const handleRemoveTab = useCallback((rootPath: string) => { void (async () => { - const target = recentProjects.find((entry) => entry.rootPath === rootPath); - const fallbackName = rootPath.split(/[\\/]/).filter(Boolean).pop() ?? rootPath; + const target = projectTabs.find((entry) => entry.rootPath === rootPath); + const fallbackName = fallbackProjectName(rootPath); const confirmed = confirmProjectTabRemoval( target?.displayName ?? fallbackName, project?.rootPath === rootPath, @@ -657,21 +715,22 @@ export function TopBar() { const shouldClose = await checkForActiveWorkloads(rootPath); if (!shouldClose) return; - const rows = await window.ade.project.forgetRecent(rootPath).catch(() => null); - if (!rows) return; - - setRecentProjects(rows); - // If we just removed the active project, switch to the next available or show welcome. + const currentIndex = openProjectTabRoots.indexOf(rootPath); + const nextTabRoots = openProjectTabRoots.filter((entry) => entry !== rootPath); + setOpenProjectTabRoots(nextTabRoots); if (project?.rootPath === rootPath) { - const next = rows.find((r) => r.exists && r.rootPath !== rootPath); - if (next) { - switchProjectToPath(next.rootPath).catch(() => { }); + const nextRoot = + nextTabRoots[currentIndex] + ?? nextTabRoots[currentIndex - 1] + ?? null; + if (nextRoot) { + switchProjectToPath(nextRoot).catch(() => { }); } else { closeProject().catch(() => { }); } } })().catch(() => { }); - }, [checkForActiveWorkloads, project?.rootPath, recentProjects, closeProject, switchProjectToPath]); + }, [checkForActiveWorkloads, closeProject, openProjectTabRoots, project?.rootPath, projectTabs, switchProjectToPath]); const handleRelocate = useCallback((oldPath: string) => { setRelocatingPath(oldPath); @@ -683,12 +742,16 @@ export function TopBar() { })().catch(() => { }).finally(() => setRelocatingPath(null)); }, [openRepo]); - const handleDragStart = useCallback((e: React.DragEvent, idx: number) => { + const handleDragStart = useCallback((e: React.DragEvent, idx: number, rootPath: string) => { setDragIdx(idx); dragCounterRef.current = 0; e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(idx)); - }, []); + e.dataTransfer.setData(ADE_PROJECT_TAB_ROOT_MIME, rootPath); + if (windowId != null) { + e.dataTransfer.setData(ADE_PROJECT_TAB_WINDOW_MIME, String(windowId)); + } + }, [windowId]); const handleDragOver = useCallback((e: React.DragEvent, idx: number) => { e.preventDefault(); @@ -701,23 +764,65 @@ export function TopBar() { }, []); const handleDrop = useCallback((e: React.DragEvent, targetIdx: number) => { + if (dragIdx === null && Array.from(e.dataTransfer.types).includes(ADE_PROJECT_TAB_ROOT_MIME)) { + return; + } e.preventDefault(); + e.stopPropagation(); setDropIdx(null); if (dragIdx === null || dragIdx === targetIdx) { setDragIdx(null); return; } - const items = [...recentProjects]; + const items = [...openProjectTabRoots]; const [moved] = items.splice(dragIdx, 1); items.splice(targetIdx, 0, moved); - setRecentProjects(items); + setOpenProjectTabRoots(items); + setDragIdx(null); + }, [dragIdx, openProjectTabRoots]); + + const handleProjectTabDrop = useCallback((e: React.DragEvent) => { + const rootPath = e.dataTransfer.getData(ADE_PROJECT_TAB_ROOT_MIME); + if (!rootPath) return; + e.preventDefault(); + setDropIdx(null); setDragIdx(null); - window.ade.project.reorderRecent(items.map((r) => r.rootPath)).catch(() => {}); - }, [dragIdx, recentProjects]); - const handleDragEnd = useCallback(() => { + const sourceWindowIdRaw = e.dataTransfer.getData(ADE_PROJECT_TAB_WINDOW_MIME); + const parsedSourceWindowId = sourceWindowIdRaw ? Number(sourceWindowIdRaw) : null; + const sourceWindowId = parsedSourceWindowId != null && Number.isFinite(parsedSourceWindowId) + ? parsedSourceWindowId + : null; + if (sourceWindowId != null && sourceWindowId === windowId) return; + + if (project?.rootPath === rootPath) { + if (sourceWindowId != null) { + window.ade.app.closeWindow(sourceWindowId).catch(() => {}); + } + return; + } + switchProjectToPath(rootPath).catch(() => {}); + }, [project?.rootPath, switchProjectToPath, windowId]); + + const handleProjectTabDragOver = useCallback((e: React.DragEvent) => { + if (!Array.from(e.dataTransfer.types).includes(ADE_PROJECT_TAB_ROOT_MIME)) return; + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + }, []); + + const handleDragEnd = useCallback((e: React.DragEvent, rootPath?: string) => { + const draggedOutside = + rootPath && + (e.clientX < 0 || + e.clientY < 0 || + e.clientX > window.innerWidth || + e.clientY > window.innerHeight); + const droppedOnAdeTarget = e.dataTransfer.dropEffect && e.dataTransfer.dropEffect !== "none"; setDragIdx(null); setDropIdx(null); + if (draggedOutside && !droppedOnAdeTarget) { + window.ade.app.openProjectInNewWindow(rootPath).catch(() => {}); + } }, []); const handleProjectAccentColorChange = useCallback((rootPath: string, color: string | null) => { @@ -761,8 +866,9 @@ export function TopBar() { const syncLabel = deriveSyncLabel(syncSnapshot); const transitionTargetName = projectTransition?.rootPath - ? (recentProjects.find((entry) => entry.rootPath === projectTransition.rootPath)?.displayName - ?? projectTransition.rootPath.split(/[\\/]/).filter(Boolean).pop() + ? (projectTabs.find((entry) => entry.rootPath === projectTransition.rootPath)?.displayName + ?? recentProjects.find((entry) => entry.rootPath === projectTransition.rootPath)?.displayName + ?? fallbackProjectName(projectTransition.rootPath) ?? "project") : "project"; const projectTransitionLabel = @@ -794,23 +900,12 @@ export function TopBar() { {/* Project tabs — the container stays draggable, only interactive elements opt out */} <div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto scrollbar-none" + onDragOver={handleProjectTabDragOver} + onDrop={handleProjectTabDrop} > - {recentProjects.length === 0 && !project ? ( - <button - type="button" - className={cn( - "ade-shell-project-tab inline-flex items-center gap-2 px-3 py-0.5", - "transition-[background-color,color,border-color,box-shadow] duration-150" - )} - style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties} - onClick={handleOpenNew} - > - <Folder size={14} weight="regular" /> - Open a project - </button> - ) : ( + {projectTabs.length > 0 || isNewTabOpen ? ( <> - {recentProjects.map((rp, idx) => { + {projectTabs.map((rp, idx) => { const isCurrent = project?.rootPath === rp.rootPath; const isMissing = !rp.exists; const isRelocating = relocatingPath === rp.rootPath; @@ -840,11 +935,11 @@ export function TopBar() { aria-current={isCurrent ? "true" : undefined} aria-disabled={isRelocating || isProjectBusy ? true : undefined} draggable={!isMissing && !isRelocating && !isProjectBusy} - onDragStart={(e) => handleDragStart(e, idx)} + onDragStart={(e) => handleDragStart(e, idx, rp.rootPath)} onDragOver={(e) => handleDragOver(e, idx)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, idx)} - onDragEnd={handleDragEnd} + onDragEnd={(e) => handleDragEnd(e, rp.rootPath)} className={cn( "ade-shell-project-tab group inline-flex w-[clamp(128px,16vw,220px)] max-w-[220px] min-w-0 shrink-0 items-center gap-2 px-3 py-0.5", "transition-[background-color,color,border-color,box-shadow,opacity] duration-150", @@ -993,7 +1088,7 @@ export function TopBar() { </div> )} </> - )} + ) : null} {/* Add project button */} <button @@ -1011,6 +1106,20 @@ export function TopBar() { > <Plus size={12} weight="regular" /> </button> + <button + type="button" + className={cn( + "ade-shell-control inline-flex h-5.5 w-5.5 shrink-0 items-center justify-center", + "transition-[background-color,color,border-color,box-shadow] duration-150" + )} + data-variant="ghost" + onClick={handleOpenNewWindow} + disabled={isProjectBusy} + title="New window" + style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties} + > + <ArrowSquareOut size={12} weight="regular" /> + </button> </div> {showPublishPill ? ( diff --git a/apps/desktop/src/renderer/components/prs/PRsPage.tsx b/apps/desktop/src/renderer/components/prs/PRsPage.tsx index a449a138..62af7acb 100644 --- a/apps/desktop/src/renderer/components/prs/PRsPage.tsx +++ b/apps/desktop/src/renderer/components/prs/PRsPage.tsx @@ -139,7 +139,10 @@ function PRsPageInner() { setActiveTab(resolved.activeTab); if (!resolved.isWorkflowRoute) { - setSelectedPrId(routeState.prId ?? null); + const prNumberMatch = routeState.prNumber == null + ? null + : prs.find((pr) => pr.githubPrNumber === routeState.prNumber)?.id ?? null; + setSelectedPrId(routeState.prId ?? prNumberMatch); setSelectedDetailTab(routeState.detailTab); } if (resolved.effectiveWorkflow === "queue") { @@ -160,7 +163,7 @@ function PRsPageInner() { window.removeEventListener("popstate", syncFromLocation); window.removeEventListener("hashchange", syncFromLocation); }; - }, [location.search, rebaseNeeds, setActiveTab, setSelectedPrId, setSelectedQueueGroupId, setSelectedRebaseItemId]); + }, [location.search, prs, rebaseNeeds, setActiveTab, setSelectedPrId, setSelectedQueueGroupId, setSelectedRebaseItemId]); React.useEffect(() => { const current = parsePrsRouteState({ search: location.search }); diff --git a/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts b/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts index 670fc9ba..1b628e51 100644 --- a/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts +++ b/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts @@ -28,6 +28,7 @@ describe("prsRouteState", () => { workflowTab: "queue", laneId: null, prId: null, + prNumber: null, queueGroupId: "group-hash", eventId: null, threadId: null, @@ -47,6 +48,7 @@ describe("prsRouteState", () => { workflowTab: "rebase", laneId: "lane-456", prId: "pr-789", + prNumber: null, queueGroupId: "group-1", eventId: null, threadId: null, @@ -65,6 +67,7 @@ describe("prsRouteState", () => { workflowTab: null, laneId: null, prId: "pr-1", + prNumber: null, queueGroupId: null, eventId: "evt-99", threadId: "thr-12", @@ -88,6 +91,13 @@ describe("prsRouteState", () => { ).toBe("?tab=normal&prId=pr-1&eventId=evt-5&threadId=thr-3&commitSha=abc&detailTab=checks"); }); + it("parses PR number handoff routes", () => { + const parsed = parsePrsRouteState({ search: "?tab=normal&pr=123&laneId=lane-1" }); + expect(parsed.prNumber).toBe(123); + expect(parsed.prId).toBeNull(); + expect(parsed.laneId).toBe("lane-1"); + }); + it("builds normal and workflow route searches with the expected ids", () => { expect( buildPrsRouteSearch({ diff --git a/apps/desktop/src/renderer/components/prs/prsRouteState.ts b/apps/desktop/src/renderer/components/prs/prsRouteState.ts index 44fd7345..278d14d0 100644 --- a/apps/desktop/src/renderer/components/prs/prsRouteState.ts +++ b/apps/desktop/src/renderer/components/prs/prsRouteState.ts @@ -14,6 +14,7 @@ export type ParsedPrsRouteState = { workflowTab: PrWorkflowTab | null; laneId: string | null; prId: string | null; + prNumber: number | null; queueGroupId: string | null; eventId: string | null; threadId: string | null; @@ -56,6 +57,13 @@ function parseOptionalId(value: string | null): string | null { return trimmed.length > 0 ? trimmed : null; } +function parseOptionalNumber(value: string | null): number | null { + const trimmed = parseOptionalId(value); + if (!trimmed) return null; + const parsed = Number.parseInt(trimmed, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; +} + export function parsePrsRouteState(args: { search?: string | null; hash?: string | null }): ParsedPrsRouteState { const searchParams = parseSearch(args.search ?? ""); const hashParams = parseHashParams(args.hash ?? ""); @@ -75,6 +83,7 @@ export function parsePrsRouteState(args: { search?: string | null; hash?: string workflowTab, laneId: pick("laneId"), prId: pick("prId"), + prNumber: parseOptionalNumber(routeParams.get("pr")), queueGroupId: pick("queueGroupId"), eventId: pick("eventId"), threadId: pick("threadId"), diff --git a/apps/desktop/src/renderer/components/run/RunPage.advancedDrawer.test.tsx b/apps/desktop/src/renderer/components/run/RunPage.advancedDrawer.test.tsx index 28058a66..d3f7b451 100644 --- a/apps/desktop/src/renderer/components/run/RunPage.advancedDrawer.test.tsx +++ b/apps/desktop/src/renderer/components/run/RunPage.advancedDrawer.test.tsx @@ -3,6 +3,7 @@ import React from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; import { RunPage } from "./RunPage"; import { useAppStore } from "../../state/appStore"; import type { LaneSummary, ProjectInfo } from "../../../shared/types"; @@ -87,6 +88,7 @@ function installAdeStub() { }, project: { listRecent: vi.fn().mockResolvedValue([]), + resolveIcon: vi.fn().mockResolvedValue({ dataUrl: null, sourcePath: null, mimeType: null }), }, }; } @@ -112,6 +114,44 @@ afterEach(() => { }); describe("RunPage Advanced lane runtime drawer", () => { + it("renders saved project icons in the recent projects list", async () => { + const ade = (window as unknown as { + ade: { + project: { + listRecent: ReturnType<typeof vi.fn>; + resolveIcon: ReturnType<typeof vi.fn>; + }; + }; + }).ade; + ade.project.listRecent.mockResolvedValueOnce([ + { + rootPath: "/tmp/icon-project", + displayName: "Icon project", + exists: true, + lastOpenedAt: "2026-05-08T00:00:00.000Z", + laneCount: 1, + }, + ]); + ade.project.resolveIcon.mockResolvedValueOnce({ + dataUrl: "data:image/png;base64,icon", + sourcePath: "/tmp/icon-project/.ade/icon.png", + mimeType: "image/png", + }); + useAppStore.setState({ showWelcome: true, project: null }); + + const { container } = render( + <MemoryRouter> + <RunPage /> + </MemoryRouter>, + ); + + expect(await screen.findByText("Icon project")).toBeTruthy(); + await waitFor(() => { + expect(ade.project.resolveIcon).toHaveBeenCalledWith("/tmp/icon-project"); + expect(container.querySelector('img[src="data:image/png;base64,icon"]')).toBeTruthy(); + }); + }); + it("keeps LaneRuntimeBar collapsed by default with aria-expanded on the toggle", async () => { render(<RunPage />); const toggle = screen.getByRole("button", { name: /^advanced$/i }); diff --git a/apps/desktop/src/renderer/components/run/RunPage.tsx b/apps/desktop/src/renderer/components/run/RunPage.tsx index 635c7d7f..10575b00 100644 --- a/apps/desktop/src/renderer/components/run/RunPage.tsx +++ b/apps/desktop/src/renderer/components/run/RunPage.tsx @@ -21,6 +21,7 @@ import type { ProcessRuntime, ProjectConfigSnapshot, ConfigProcessGroupDefinition, + ProjectIcon, } from "../../../shared/types"; function generateId(): string { @@ -236,6 +237,44 @@ function runPageLaneStateEqual(left: PersistedRunPageLaneState, right: Persisted return leftEntries.every(([processId, laneId]) => right.commandLaneIds[processId] === laneId); } +function RecentProjectIcon({ rootPath }: { rootPath: string }) { + const [icon, setIcon] = useState<ProjectIcon | null>(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let cancelled = false; + setIcon(null); + setFailed(false); + window.ade.project.resolveIcon(rootPath).then((nextIcon) => { + if (!cancelled) setIcon(nextIcon); + }).catch(() => { + if (!cancelled) setIcon(null); + }); + return () => { + cancelled = true; + }; + }, [rootPath]); + + if (icon?.dataUrl && !failed) { + return ( + <img + src={icon.dataUrl} + alt="" + draggable={false} + onError={() => setFailed(true)} + style={{ + width: 22, + height: 22, + borderRadius: 6, + objectFit: "contain", + }} + /> + ); + } + + return <Folder size={16} weight="regular" />; +} + function WelcomeScreen() { const switchProjectToPath = useAppStore((s) => s.switchProjectToPath); const project = useAppStore((s) => s.project); @@ -346,7 +385,7 @@ function WelcomeScreen() { flexShrink: 0, }} > - <Folder size={16} weight="regular" /> + <RecentProjectIcon rootPath={rp.rootPath} /> </div> <div style={{ overflow: "hidden", flex: 1 }}> <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 2 }}>{rp.displayName}</div> diff --git a/apps/desktop/src/shared/ipc.ts b/apps/desktop/src/shared/ipc.ts index def3a332..85af6de2 100644 --- a/apps/desktop/src/shared/ipc.ts +++ b/apps/desktop/src/shared/ipc.ts @@ -2,6 +2,11 @@ export const IPC = { appPing: "ade.app.ping", appGetInfo: "ade.app.getInfo", appGetProject: "ade.app.getProject", + appGetWindowSession: "ade.app.getWindowSession", + appNewWindow: "ade.app.newWindow", + appOpenProjectInNewWindow: "ade.app.openProjectInNewWindow", + appCloseWindow: "ade.app.closeWindow", + appNavigate: "ade.app.navigate", appProjectChanged: "ade.app.projectChanged", appOpenExternal: "ade.app.openExternal", appRevealPath: "ade.app.revealPath", diff --git a/apps/desktop/src/shared/types/core.ts b/apps/desktop/src/shared/types/core.ts index 160a2acc..a728ad60 100644 --- a/apps/desktop/src/shared/types/core.ts +++ b/apps/desktop/src/shared/types/core.ts @@ -45,6 +45,41 @@ export type ProjectInfo = { baseRef: string; }; +export type AppNavigationTarget = + | { + kind: "work" | "chat"; + sessionId?: string | null; + laneId?: string | null; + } + | { + kind: "lane"; + laneId: string; + sessionId?: string | null; + } + | { + kind: "pr"; + prId?: string | null; + prNumber?: number | null; + laneId?: string | null; + } + | { + kind: "route"; + route: string; + }; + +export type AppNavigationRequest = { + target: AppNavigationTarget; + source?: "ade-code" | "desktop" | "cli" | string; +}; + +export type AppNavigationResult = { + ok: boolean; + mode: "desktop" | "unavailable"; + windowId?: number | null; + route?: string; + message?: string; +}; + export type ProjectBrowseInput = { partialPath?: string; cwd?: string | null; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e33f5977..5d976066 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -8,7 +8,7 @@ Consolidated technical reference for the ADE (Agentic Development Environment) s ADE is a local-first development control plane that orchestrates AI-assisted software engineering across parallel worktrees. It combines worktree-per-lane git isolation, a multi-provider AI runtime, a deterministic orchestrator for multi-step missions, a Linear-integrated CTO agent acting as a team lead, a pipeline builder for visual automations, stacked pull requests with conflict simulation, computer-use proofs, a SQLite-backed memory system, and multi-device sync via cr-sqlite CRDTs. Nothing leaves the user's machine by default: AI work runs through user-authenticated CLIs (Claude Code, Codex), local API-key routes (OpenCode server), or local model endpoints (Ollama, LM Studio, vLLM). -ADE ships as four coordinated apps: +ADE ships as five coordinated apps: ``` ┌─────────────────────────┐ @@ -31,10 +31,15 @@ ADE ships as four coordinated apps: │ │─── spawns ─────────────────────┐ │ │ │ │ ▼ │ │ │ │ ┌──────────────────────┐ │ -│ │ │ apps/ade-cli │ │ -│ │ │ (JSON-RPC over stdio │◀──── headless mode ──────┤ +│ │ │ apps/ade-cli │◀──── headless mode ──────┤ +│ │ │ (JSON-RPC over stdio │ │ │ │ │ or .ade/ade.sock) │ │ │ │ └──────────────────────┘ │ +│ │ ┌──────────────────────┐ │ +│ │ │ apps/ade-code │◀──── terminal Work chat ─┤ +│ │ │ (Ink TUI, same RPC │ │ +│ │ │ socket or embedded) │ │ +│ │ └──────────────────────┘ │ │ │ │ │ └── spawns CLI runtimes: │ │ claude (Claude Agent SDK) · codex CLI · opencode server │ @@ -134,6 +139,7 @@ bridge. `app_control` (every public method on `AppControlService`) and `terminal` (`list`, `read`, `write`, `signal`, `activeForChat` against `ptyService`). +- **`ade code`** — launches the terminal-native Work chat client (`apps/ade-code`, Ink + React). Uses the desktop JSON-RPC socket when `--socket` is set on the parent `ade` invocation (same path as other socket-backed commands); otherwise the TUI runs embedded against the headless runtime (`--embedded`) without implying the global auto-socket discovery used by `executePlan`. Override the binary with `ADE_CODE_EXECUTABLE` or a sibling `apps/ade-code/dist/cli.js` after `npm run build` in that package. - **Proof subcommands** — `ade proof capture` (alias of `screenshot`), `ade proof attach <path>`, `ade proof record`, `ade proof launch`, `ade proof interact`, `ade proof list/status/environment/ingest`. @@ -145,11 +151,15 @@ bridge. `--owner-id` (with `chat` and `pr` aliases) to layer an explicit owner on top of the inferred session identity. -### 2.3 Web app (`apps/web/`) +### 2.3 ADE Code (`apps/ade-code/`) + +Terminal-native **Work** chat client (Ink + React) for agents and power users who live in a shell. It speaks the same ADE JSON-RPC surface as the desktop app and `ade-cli`: **attached** mode connects to `.ade/ade.sock` (or the Windows named pipe from `adeMcpIpc`) when a socket is present; **embedded** mode loads `createAdeRuntime` / `createAdeRpcRequestHandler` from `apps/ade-cli` at runtime so headless services run in-process without Electron. Shared chat DTOs are imported from `apps/desktop/src/shared/types/*` (never the renderer barrel) so `npm run typecheck` in this package stays isolated. Entry: `src/cli.tsx` → `dist/cli.js` (`ade-code` bin). Launched from the desktop shell via `ade code` (see §2.2). Multi-window navigation from the TUI uses the `app/navigate` JSON-RPC method when a desktop socket is attached. + +### 2.4 Web app (`apps/web/`) A Vite/React SPA that serves the public marketing site and download page. Four pages: `HomePage`, `DownloadPage`, `PrivacyPage`, `TermsPage`. Independent package (`ade-web`), deployed via Vercel (`apps/web/vercel.json`). Not a runtime dependency of the desktop app. Shared-origin with the Mintlify docs site (`docs.json` at repo root). -### 2.4 iOS companion (`apps/ios/`) +### 2.5 iOS companion (`apps/ios/`) Native SwiftUI app acting as a controller for an ADE host. It reads live desktop state from a local cr-sqlite-backed SQLite database and sends commands to the host for execution. The phone never runs agents. @@ -407,6 +417,7 @@ ade.updates.* - Every handler is wrapped with a **30-second timeout** — if it does not resolve, the call rejects with a timeout error rather than hanging the renderer. - Every handler emits structured tracing: `ipc.invoke.begin`, `ipc.invoke.done`, `ipc.invoke.failed` with call ID, channel, window ID, duration, and summarized args/results. - `AppContext` indirection: handlers close over a context pointer that swaps atomically on project switch, so IPC channels remain registered across project transitions. +- **Multi-window shell** — the app can host multiple `BrowserWindow` instances (for example when opening another project in a dedicated window). Handler tracing already carries **window ID** so logs and diagnostics distinguish which renderer surface invoked a channel; `main.ts` ties each window to its project context before routing into services. ### 5.4 Event subscriptions (push, not poll) @@ -906,7 +917,8 @@ Full detail: [`docs/architecture/MULTI_DEVICE_SYNC.md`](../docs/architecture/MUL ADE/ ├── apps/ │ ├── desktop/ # Electron main/preload/renderer (primary product) -│ ├── ade-cli/ # Headless ADE CLI (Node, JSON-RPC over stdio) +│ ├── ade-cli/ # Headless ADE CLI (Node, JSON-RPC over stdio) +│ ├── ade-code/ # Terminal Ink TUI for Work chat (socket or embedded headless) │ ├── web/ # Marketing + download landing (Vite + React) │ └── ios/ # Native SwiftUI controller ├── docs/ @@ -914,7 +926,6 @@ ADE/ │ ├── architecture/ # Deep subsystem docs (source for this file) │ ├── features/ │ └── final-plan/ -├── new-docs/ # This file + feature docs ├── scripts/ # Release, validate, notarize, after-pack (per-platform) │ # Platform-specific: validate-mac-artifacts.mjs, │ # validate-win-artifacts.mjs, ade-cli-windows-wrapper.cmd, etc. @@ -939,6 +950,7 @@ Per-app scripts: |-----|-------------| | `apps/desktop` | `dev`, `build` (tsup + vite), `typecheck`, `test` (vitest), `lint` (ESLint), `dist:mac`, `dist:mac:universal:signed:zip`, `notarize:mac:dmg`, `validate:mac:artifacts`, `rebuild:native`, `version:ci`, `version:release`, `ade:dev`, `ade:build`, `ade:test`. | | `apps/ade-cli` | `dev`, `build`, `typecheck`, `test`. | +| `apps/ade-code` | `dev`, `build`, `typecheck`, `test` (Ink TUI; uses granular imports from `apps/desktop/src/shared/types/*`). | | `apps/web` | `dev`, `build`, `preview`, `typecheck`. | | `apps/ios` | Xcode project; tests via `xcodebuild test` / Xcode. | @@ -946,16 +958,18 @@ Per-app scripts: Stages: -1. **Install** (`install` job) — checkout, setup Node 22, parallel `npm ci` across desktop/ade-cli/web with shared cache keyed on all three lockfiles. +1. **Install** (`install` job) — checkout, setup Node 22, parallel `npm ci` across desktop, ade-cli, web, and ade-code with a shared cache keyed on all four lockfiles. 2. **Parallel checks**: - `secret-scan` — gitleaks on full history. - `typecheck-desktop` — `cd apps/desktop && npm run typecheck`. - `typecheck-ade-cli` — `cd apps/ade-cli && npm run typecheck`. - `typecheck-web` — `cd apps/web && npm run typecheck`. + - `typecheck-ade-code` — `cd apps/ade-code && npm run typecheck`. - `lint-desktop` — ESLint on `src/**/*.{ts,tsx}`. - `test-desktop` — **8-way shard matrix**: `npx vitest run --shard=${{ matrix.shard }}/8` across shards 1–8. - `test-ade-cli` — full ade-cli vitest. - - `build` — all three apps built sequentially after install. + - `test-ade-code` — ade-code vitest. + - `build` — all four apps built sequentially after install. - `validate-docs` — `node scripts/validate-docs.mjs`. 3. **Gate** (`ci-pass`) — all required jobs must pass (`if: always()` with failure/cancelled detection). @@ -1061,5 +1075,5 @@ Post-packaging hardening (`apps/desktop/scripts/`): - UI framework · [`docs/architecture/UI_FRAMEWORK.md`](../docs/architecture/UI_FRAMEWORK.md) - Multi-device sync · [`docs/architecture/MULTI_DEVICE_SYNC.md`](../docs/architecture/MULTI_DEVICE_SYNC.md) - iOS app · [`docs/architecture/IOS_APP.md`](../docs/architecture/IOS_APP.md) -- Feature docs (this directory) · [`new-docs/features/`](./features/) +- Feature docs · [`docs/features/`](./features/) - Product spec · [`docs/PRD.md`](../docs/PRD.md) diff --git a/docs/PRD.md b/docs/PRD.md index 5902ea3f..e67877c7 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -50,7 +50,7 @@ ADE is the control plane. It does not execute browser automation or computer-use ### Agents and chat - [**Agents**](./features/agents/README.md) — Three surfaces: chat, CTO operator, workers. Identity, capability modes, tool tiers, heartbeats. -- [**Chat**](./features/chat/README.md) — Multi-provider, streaming, tool-aware. Transcript and turns, tool system (universal/workflow/coordinator), agent routing, composer + derived panels, and parallel multi-model lane launch. +- [**Chat**](./features/chat/README.md) — Multi-provider, streaming, tool-aware. Transcript and turns, tool system (universal/workflow/coordinator), agent routing, composer + derived panels, and parallel multi-model lane launch. Terminal client: [ADE Code](./features/ade-code/README.md). - [**Memory**](./features/memory/README.md) — Unified SQLite + FTS + embeddings. Write gate, compaction, procedural learning, daily sweep, hybrid retrieval (BM25+cosine+MMR). - [**History**](./features/history/README.md) — Operations timeline + chat transcripts + exports. Every service follows the same `runTrackedOperation` recording pattern. @@ -82,7 +82,7 @@ For the system-wide picture — apps, processes, data plane, IPC, security, buil Quick pointers: -- **Apps**: `apps/desktop/` (Electron main + preload + renderer), `apps/ade-cli/` (headless ADE CLI action server), `apps/web/` (marketing), `apps/ios/` (companion). +- **Apps**: `apps/desktop/` (Electron main + preload + renderer), `apps/ade-cli/` (headless ADE CLI action server), `apps/ade-code/` (terminal Ink client for Work chat), `apps/web/` (marketing), `apps/ios/` (companion). - **Main-process services**: `apps/desktop/src/main/services/<domain>/` — one directory per capability. - **Renderer components**: `apps/desktop/src/renderer/components/<feature>/`. - **Shared types + IPC contract**: `apps/desktop/src/shared/`. diff --git a/docs/README.md b/docs/README.md index 5831ffc1..62c24173 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ Navigation map for the internal docs. **Start with [PRD.md](./PRD.md).** ## Layout ``` -new-docs/ +docs/ ├── README.md # this file ├── PRD.md # product entry point ├── ARCHITECTURE.md # system architecture @@ -21,6 +21,7 @@ new-docs/ │ └── ship-lane.md # autonomous PR-to-merge driver └── features/ ├── agents/ # agent identity, tools, personas + ├── ade-code/ # terminal Ink Work chat client (ade-code) ├── automations/ # rule triggers + actions + guardrails ├── chat/ # multi-provider agent chat ├── computer-use/ # proof control plane, backends, broker @@ -53,4 +54,4 @@ new-docs/ `docs.json` at the repo root configures the public-facing Mintlify docs site (`.mdx` files under `./chat/`, `./tools/`, `./missions/`, etc.). That site is user-facing and separate. -**This folder (`new-docs/`) is internal-only** — for engineers and AI agents working on ADE itself. +**This folder (`docs/`) is internal-only** — for engineers and AI agents working on ADE itself. diff --git a/docs/features/ade-code/README.md b/docs/features/ade-code/README.md new file mode 100644 index 00000000..1d3a77ba --- /dev/null +++ b/docs/features/ade-code/README.md @@ -0,0 +1,36 @@ +# ADE Code (terminal Work chat) + +ADE Code is a terminal-native client for the same **Work** agent chat surface the Electron app exposes in `AgentChatPane`. It targets agents and operators who prefer a shell-first workflow: Ink + React render the TUI, while chat transcripts, slash commands, and lane context flow through the same ADE action and JSON-RPC contracts as the desktop. + +## Source file map + +| Path | Role | +|------|------| +| `apps/ade-code/src/cli.tsx` | CLI entry: argv parsing, project discovery, connection bootstrap, Ink mount. | +| `apps/ade-code/src/app.tsx` | Primary Ink/React surface: navigation, composer, drawers, right pane, session lifecycle. | +| `apps/ade-code/src/connection.ts` | **Attached** path: JSON-RPC over `.ade/ade.sock` (or Windows named pipe). **Embedded** path: dynamic `import()` of `apps/ade-cli` `bootstrap` + `adeRpcServer` so headless services run in-process without pulling the whole dependency graph into `tsc` for this package. | +| `apps/ade-code/src/jsonRpcClient.ts` | Socket client: connect, request/response, `chat/event` notifications. | +| `apps/ade-code/src/adeApi.ts` | Typed wrappers over `AdeCodeConnection.action` / `actionList` for lanes, chat, models, navigation. | +| `apps/ade-code/src/commands.ts` / `linearCommands.ts` | Slash and command routing. | +| `apps/ade-code/src/format.ts` | Transcript rendering helpers for the TUI. | +| `apps/ade-code/src/types.ts` | Connection shape, launch context, navigation DTOs aligned with `apps/desktop/src/shared/types`. | +| `apps/desktop/src/shared/types/chat.ts` | Canonical chat DTOs (`AgentChatEventEnvelope`, sessions, pending input). Imported from **per-module** paths (not `types/index.ts`) so ade-code typecheck stays scoped. | +| `apps/desktop/src/shared/modelRegistry.ts` | Default model selection for new sessions (`getDefaultModelDescriptor`). | +| `apps/desktop/src/shared/adeLayout.ts` | Resolves `.ade` paths including socket location. | +| `apps/ade-cli/src/cli.ts` | `ade code` launcher: resolves `ade-code` binary (`ADE_CODE_EXECUTABLE`, sibling `dist/cli.js`, or `PATH`). | +| `apps/desktop/src/main/main.ts` | Multi-window shell: project windows, shared menu, JSON-RPC `app/navigate` for external controllers. | +| `apps/desktop/src/renderer/components/app/TopBar.tsx` | Window tab strip + project navigation when multiple windows are open. | + +## Runtime modes + +- **Attached** — `JsonRpcClient` connects to the desktop RPC socket. Initialization follows the same `ade/initialize` handshake as other socket clients. +- **Embedded** — no socket: `createAdeRuntime` + `createAdeRpcRequestHandler` from `apps/ade-cli` serve actions in-process. Used for headless/dev environments where Electron is not running. + +## Launch + +From a machine with the `ade` CLI on `PATH`: `ade code` (see `apps/ade-cli/README.md` for flags, `ADE_CODE_EXECUTABLE`, and how `--socket` on the parent `ade` process is forwarded). After local changes, run `npm run build` inside `apps/ade-code` so `dist/cli.js` exists for sibling resolution. + +## Related docs + +- [Chat feature](../chat/README.md) — in-app Work chat architecture (service + renderer). +- [ARCHITECTURE.md](../../ARCHITECTURE.md) §2.2–2.3 — CLI and ade-code placement in the system diagram. diff --git a/docs/features/chat/README.md b/docs/features/chat/README.md index 247eb4a6..aaf39d90 100644 --- a/docs/features/chat/README.md +++ b/docs/features/chat/README.md @@ -12,6 +12,7 @@ machinery layered on top. | Path | Role | |---|---| | `apps/desktop/src/main/services/chat/agentChatService.ts` | Main service: session lifecycle, turn dispatch, event emission, provider adapters, steer queue, handoff, auto-title, and prompt-derived lane-name suggestions for parallel launch. Tracks Codex Fast Mode (`codexFastMode: boolean`) per session and forwards it as `serviceTier: "fast" \| null` on every Codex `thread/start` and `turn/start` JSON-RPC call (see [Agent Routing](agent-routing.md#codex-service-tiers-fast-mode)). Spawns Claude/Codex agent runtimes with `buildAgentRuntimeEnv(managed)` so every agent process inherits `ADE_CHAT_SESSION_ID`, `ADE_LANE_ID`, `ADE_PROJECT_ROOT`, and `ADE_WORKSPACE_ROOT` (used by the agent guidance to call `ade --socket app-control logs` / `terminal read --chat-session "$ADE_CHAT_SESSION_ID"` without resolving the chat ID itself). Large orchestrator file. | +| `apps/ade-code/` | Terminal **Work** chat TUI (Ink + React): same action/RPC contracts as desktop, **attached** (socket) or **embedded** (headless runtime via `ade-cli`). See [ADE Code](../ade-code/README.md). | | `apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts` | Main-process broker for the in-app web browser. Owns a single `persist:ade-browser` partition, multiple `WebContentsView` tabs (cap 10), bounds + visibility against the renderer-supplied frame, debugger-protocol attachment for inspect-mode hit tests, screenshot capture, and emission of `BuiltInBrowserContextItem`s for selected page elements. Spoofs a desktop Chrome `User-Agent` and the matching `Sec-CH-UA*` client hints on every request through `webRequest.onBeforeSendHeaders` so external sign-in flows (Google, etc.) treat the embedded view as a normal desktop Chrome instead of refusing to load — the previous "open Google sign-in in the system browser" branch was removed because the spoofed UA stops Google from blocking the page in the first place. Window-open requests are forwarded into a fresh tab with `openPanel: true` so the Work sidebar Browser tab pops automatically. Backs the `ade.builtInBrowser.*` IPC surface and is consumed by both `ChatBuiltInBrowserPanel` (sidebar Browser tab) and `openExternal.ts` (links inside the renderer route through the built-in browser when the protocol is `http`/`https`/`about:blank`). | | `apps/desktop/src/shared/types/builtInBrowser.ts` | Cross-process types for the built-in browser: `BuiltInBrowserStatus`, `BuiltInBrowserTab`, `BuiltInBrowserContextItem` (`kind: "built_in_browser_element" | "built_in_browser_capture"`), `BuiltInBrowserSelectResult`, `BuiltInBrowserScreenshot`, `BuiltInBrowserOpenPanelArgs`, and the `BuiltInBrowserEventPayload` union (`status`, `open-request`, `selection`, `selection-cleared`, `error`). Navigate / create-tab / switch-tab args carry an optional `openPanel: boolean` so callers can ask for the Work sidebar Browser tab to flip open atomically with the navigation. | | `apps/desktop/src/main/services/chat/buildClaudeV2Message.ts` | Builds the message payload the Claude Agent SDK V2 session consumes. Handles base64 image content blocks and MIME inference. |