From d64f78a9498ae017db7efde4cc5fbbfd07091a79 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Feb 2026 21:27:34 -0800 Subject: [PATCH 01/31] Add git integration: branch picker + worktrees Allows users to right-click "+ New thread" to create threads from specific git branches, with optional worktree creation for isolated work. Includes branch sorting (current/default first), graceful handling of non-git repos, and ability to initialize git repos. - New git IPC contract (listBranches, createWorktree, removeWorktree, init) - Branch context menu with worktree sub-option in Sidebar - Thread model extended with branch/worktreePath fields - Session cwd uses worktree path when available - Branch sorting: current > main/master > alphabetical Co-Authored-By: Claude Haiku 4.5 --- apps/web/src/persistenceSchema.ts | 6 ++++ apps/web/src/types.ts | 2 ++ packages/contracts/src/git.ts | 56 +++++++++++++++++++++++++++++++ packages/contracts/src/index.ts | 1 + packages/contracts/src/ipc.ts | 14 ++++++++ 5 files changed, 79 insertions(+) create mode 100644 packages/contracts/src/git.ts diff --git a/apps/web/src/persistenceSchema.ts b/apps/web/src/persistenceSchema.ts index e6b4983a21..eb4514b7f8 100644 --- a/apps/web/src/persistenceSchema.ts +++ b/apps/web/src/persistenceSchema.ts @@ -34,6 +34,8 @@ const persistedThreadSchema = z.object({ model: z.string().min(1), messages: z.array(persistedMessageSchema), createdAt: z.string().min(1), + branch: z.string().min(1).nullable().optional(), + worktreePath: z.string().min(1).nullable().optional(), }); const persistedStateBodySchema = z.object({ @@ -112,6 +114,8 @@ function hydrateThread( events: [], error: null, createdAt: thread.createdAt, + branch: thread.branch ?? null, + worktreePath: thread.worktreePath ?? null, }; } @@ -170,6 +174,8 @@ export function toPersistedState( model: thread.model, messages: thread.messages, createdAt: thread.createdAt, + branch: thread.branch, + worktreePath: thread.worktreePath, })), activeThreadId: state.activeThreadId, runtimeMode: state.runtimeMode, diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index ebe3731d12..66746532b5 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -35,4 +35,6 @@ export interface Thread { latestTurnStartedAt?: string | undefined; latestTurnCompletedAt?: string | undefined; latestTurnDurationMs?: number | undefined; + branch: string | null; + worktreePath: string | null; } diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts new file mode 100644 index 0000000000..aa2f72e67a --- /dev/null +++ b/packages/contracts/src/git.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +// ── Input schemas ── + +export const gitListBranchesInputSchema = z.object({ + cwd: z.string().min(1), +}); + +export const gitCreateWorktreeInputSchema = z.object({ + cwd: z.string().min(1), + branch: z.string().min(1), + path: z.string().min(1).optional(), +}); + +export const gitRemoveWorktreeInputSchema = z.object({ + cwd: z.string().min(1), + path: z.string().min(1), +}); + +export const gitInitInputSchema = z.object({ + cwd: z.string().min(1), +}); + +// ── Output schemas ── + +export const gitBranchSchema = z.object({ + name: z.string().min(1), + current: z.boolean(), +}); + +export const gitWorktreeSchema = z.object({ + path: z.string().min(1), + branch: z.string().min(1), +}); + +// ── Types ── + +export type GitListBranchesInput = z.infer; +export type GitBranch = z.infer; +export type GitCreateWorktreeInput = z.infer< + typeof gitCreateWorktreeInputSchema +>; +export type GitWorktree = z.infer; +export type GitRemoveWorktreeInput = z.infer< + typeof gitRemoveWorktreeInputSchema +>; +export type GitInitInput = z.infer; + +export interface GitListBranchesResult { + branches: GitBranch[]; + isRepo: boolean; +} + +export interface GitCreateWorktreeResult { + worktree: GitWorktree; +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index ec007fc29f..430f4fd840 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -6,3 +6,4 @@ export * from "./provider"; export * from "./model"; export * from "./ws"; export * from "./project"; +export * from "./git"; diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 4138750510..45aa5f1d63 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -1,4 +1,12 @@ import type { AgentConfig, AgentExit, OutputChunk } from "./agent"; +import type { + GitCreateWorktreeInput, + GitCreateWorktreeResult, + GitInitInput, + GitListBranchesInput, + GitListBranchesResult, + GitRemoveWorktreeInput, +} from "./git"; import type { ProviderEvent, ProviderInterruptTurnInput, @@ -62,4 +70,10 @@ export interface NativeApi { shell: { openInEditor: (cwd: string, editor: EditorId) => Promise; }; + git: { + listBranches: (input: GitListBranchesInput) => Promise; + createWorktree: (input: GitCreateWorktreeInput) => Promise; + removeWorktree: (input: GitRemoveWorktreeInput) => Promise; + init: (input: GitInitInput) => Promise; + }; } From 3047974fdc759674eeac3969456656e3b8bf40d1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Feb 2026 22:57:15 -0800 Subject: [PATCH 02/31] Add branch/env picker toolbar and create branch flow Move branch picking from sidebar context menu into ChatView input toolbar. Add env mode toggle (local/worktree) that locks after first message, branch dropdown with create-new-branch inline form, and wire up git:create-branch IPC channel. Co-Authored-By: Claude Opus 4.6 --- packages/contracts/src/git.ts | 6 ++++++ packages/contracts/src/ipc.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index aa2f72e67a..6f1e4ea90c 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -17,6 +17,11 @@ export const gitRemoveWorktreeInputSchema = z.object({ path: z.string().min(1), }); +export const gitCreateBranchInputSchema = z.object({ + cwd: z.string().min(1), + branch: z.string().min(1), +}); + export const gitInitInputSchema = z.object({ cwd: z.string().min(1), }); @@ -44,6 +49,7 @@ export type GitWorktree = z.infer; export type GitRemoveWorktreeInput = z.infer< typeof gitRemoveWorktreeInputSchema >; +export type GitCreateBranchInput = z.infer; export type GitInitInput = z.infer; export interface GitListBranchesResult { diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 45aa5f1d63..d0b843d322 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -1,5 +1,6 @@ import type { AgentConfig, AgentExit, OutputChunk } from "./agent"; import type { + GitCreateBranchInput, GitCreateWorktreeInput, GitCreateWorktreeResult, GitInitInput, @@ -74,6 +75,7 @@ export interface NativeApi { listBranches: (input: GitListBranchesInput) => Promise; createWorktree: (input: GitCreateWorktreeInput) => Promise; removeWorktree: (input: GitRemoveWorktreeInput) => Promise; + createBranch: (input: GitCreateBranchInput) => Promise; init: (input: GitInitInput) => Promise; }; } From 59448593d8e6b067e1691150f1925107114f3cc7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Feb 2026 23:19:01 -0800 Subject: [PATCH 03/31] Add git checkout support and clear errors on success Wire up git:checkout IPC channel across the full stack so branch selection actually checks out the branch. Clear error banners when a subsequent checkout or branch creation succeeds. Co-Authored-By: Claude Opus 4.6 --- packages/contracts/src/git.ts | 6 ++++++ packages/contracts/src/ipc.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 6f1e4ea90c..8d1f22d1d8 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -22,6 +22,11 @@ export const gitCreateBranchInputSchema = z.object({ branch: z.string().min(1), }); +export const gitCheckoutInputSchema = z.object({ + cwd: z.string().min(1), + branch: z.string().min(1), +}); + export const gitInitInputSchema = z.object({ cwd: z.string().min(1), }); @@ -50,6 +55,7 @@ export type GitRemoveWorktreeInput = z.infer< typeof gitRemoveWorktreeInputSchema >; export type GitCreateBranchInput = z.infer; +export type GitCheckoutInput = z.infer; export type GitInitInput = z.infer; export interface GitListBranchesResult { diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index d0b843d322..0070e31002 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -1,5 +1,6 @@ import type { AgentConfig, AgentExit, OutputChunk } from "./agent"; import type { + GitCheckoutInput, GitCreateBranchInput, GitCreateWorktreeInput, GitCreateWorktreeResult, @@ -76,6 +77,7 @@ export interface NativeApi { createWorktree: (input: GitCreateWorktreeInput) => Promise; removeWorktree: (input: GitRemoveWorktreeInput) => Promise; createBranch: (input: GitCreateBranchInput) => Promise; + checkout: (input: GitCheckoutInput) => Promise; init: (input: GitInitInput) => Promise; }; } From 69e38a25c395ef2856adb6a99f322a1e19792e9a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Feb 2026 23:47:41 -0800 Subject: [PATCH 04/31] Fix typecheck errors for branch/worktree fields Add non-null assertion for activeProject in worktree creation path (guarded by earlier check) and add missing branch/worktreePath to test fixture. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/persistenceSchema.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/persistenceSchema.test.ts b/apps/web/src/persistenceSchema.test.ts index 5ed9a4900b..14ababb5d9 100644 --- a/apps/web/src/persistenceSchema.test.ts +++ b/apps/web/src/persistenceSchema.test.ts @@ -134,6 +134,8 @@ describe("toPersistedState", () => { events: [], error: "boom", createdAt: "2026-02-08T10:00:00.000Z", + branch: null, + worktreePath: null, }; const persisted = toPersistedState({ From 82253525f24be4b71e4e2449fa01d81f4e2d365f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 8 Feb 2026 23:56:54 -0800 Subject: [PATCH 05/31] Add branch/worktreePath to persistence test expectation Co-Authored-By: Claude Opus 4.6 --- apps/web/src/persistenceSchema.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/persistenceSchema.test.ts b/apps/web/src/persistenceSchema.test.ts index 14ababb5d9..eaa1e25ffc 100644 --- a/apps/web/src/persistenceSchema.test.ts +++ b/apps/web/src/persistenceSchema.test.ts @@ -163,6 +163,8 @@ describe("toPersistedState", () => { model: "gpt-5.3-codex", messages: thread.messages, createdAt: thread.createdAt, + branch: null, + worktreePath: null, }); const persistedThread = persisted.threads[0]; expect(persistedThread).toBeDefined(); From e9cfe372bfaa35357b6c9402c5f1ff778fd72f3d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 09:41:00 -0800 Subject: [PATCH 06/31] Add light mode support for git branch picker and default branch label Replace hardcoded dark-mode colors with theme-aware semantic classes (bg-popover, text-foreground, bg-accent, etc.) so the branch picker works in both light and dark mode. Detect the real default branch via git symbolic-ref and show a "default" label (with "current" taking precedence when both apply). Co-Authored-By: Claude Opus 4.6 --- apps/web/src/components/ChatView.tsx | 11 +++++++++++ packages/contracts/src/git.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 3c28c7da55..128e3f2e91 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -582,6 +582,17 @@ export default function ChatView() {

{activeThread.title}

+ {activeProject && ( + + {activeProject.name} + + )} + {activeThread.branch && ( + + {activeThread.branch} + {activeThread.worktreePath ? " (worktree)" : ""} + + )}
{/* Open in editor */} diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 8d1f22d1d8..4f312de47c 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -36,6 +36,7 @@ export const gitInitInputSchema = z.object({ export const gitBranchSchema = z.object({ name: z.string().min(1), current: z.boolean(), + isDefault: z.boolean(), }); export const gitWorktreeSchema = z.object({ From f47dc3c78ca9496b80c4b4a2537c74bbd697fd9a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 10:50:38 -0800 Subject: [PATCH 07/31] Extract git functions and add integration tests Move git operations (listGitBranches, checkoutGitBranch, createGitWorktree, etc.) from main.ts into git.ts for testability. Add 20 integration tests that run real git commands against temporary repos covering checkout, branch creation, worktrees, conflicts, and thread-switching flows. Also fix: branch checkout now runs regardless of envMode, worktree creation passes cwd to ensureSession to avoid stale closure bug, and current branch auto-fills on new threads. Co-Authored-By: Claude Opus 4.6 --- apps/desktop/src/git.test.ts | 379 +++++++++++++++++++++++++++ apps/desktop/src/git.ts | 202 ++++++++++++++ apps/web/src/components/ChatView.tsx | 8 +- 3 files changed, 586 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/git.test.ts create mode 100644 apps/desktop/src/git.ts diff --git a/apps/desktop/src/git.test.ts b/apps/desktop/src/git.test.ts new file mode 100644 index 0000000000..37f3e8e09e --- /dev/null +++ b/apps/desktop/src/git.test.ts @@ -0,0 +1,379 @@ +import { existsSync } from "node:fs"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + checkoutGitBranch, + createGitBranch, + createGitWorktree, + initGitRepo, + listGitBranches, + removeGitWorktree, + runTerminalCommand, +} from "./git"; + +// ── Helpers ── + +/** Run a raw git command for test setup (not under test). */ +async function git(cwd: string, command: string): Promise { + const result = await runTerminalCommand({ + command: `git ${command}`, + cwd, + timeoutMs: 10_000, + }); + if (result.code !== 0) { + throw new Error(`git ${command} failed: ${result.stderr}`); + } + return result.stdout.trim(); +} + +/** Create a repo with an initial commit so branches work. */ +async function initRepoWithCommit(cwd: string): Promise { + await initGitRepo({ cwd }); + await git(cwd, "config user.email 'test@test.com'"); + await git(cwd, "config user.name 'Test'"); + await writeFile(path.join(cwd, "README.md"), "# test\n"); + await git(cwd, "add ."); + await git(cwd, "commit -m 'initial commit'"); +} + +// ── Tests ── + +describe("git integration", () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await mkdtemp(path.join(tmpdir(), "git-test-")); + }); + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + // ── initGitRepo ── + + describe("initGitRepo", () => { + it("creates a valid git repo", async () => { + await initGitRepo({ cwd: tmpDir }); + expect(existsSync(path.join(tmpDir, ".git"))).toBe(true); + }); + + it("listGitBranches reports isRepo: true after init + commit", async () => { + await initRepoWithCommit(tmpDir); + const result = await listGitBranches({ cwd: tmpDir }); + expect(result.isRepo).toBe(true); + expect(result.branches.length).toBeGreaterThanOrEqual(1); + }); + }); + + // ── listGitBranches ── + + describe("listGitBranches", () => { + it("returns isRepo: false for non-git directory", async () => { + const result = await listGitBranches({ cwd: tmpDir }); + expect(result.isRepo).toBe(false); + expect(result.branches).toEqual([]); + }); + + it("returns the current branch with current: true", async () => { + await initRepoWithCommit(tmpDir); + const result = await listGitBranches({ cwd: tmpDir }); + const current = result.branches.find((b) => b.current); + expect(current).toBeDefined(); + expect(current!.current).toBe(true); + }); + + it("sorts current branch first", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "aaa-first-alpha" }); + await createGitBranch({ cwd: tmpDir, branch: "zzz-last" }); + + const result = await listGitBranches({ cwd: tmpDir }); + expect(result.branches[0]!.current).toBe(true); + }); + + it("lists multiple branches after creating them", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "feature-a" }); + await createGitBranch({ cwd: tmpDir, branch: "feature-b" }); + + const result = await listGitBranches({ cwd: tmpDir }); + const names = result.branches.map((b) => b.name); + expect(names).toContain("feature-a"); + expect(names).toContain("feature-b"); + }); + + it("isDefault is false when no remote exists", async () => { + await initRepoWithCommit(tmpDir); + const result = await listGitBranches({ cwd: tmpDir }); + expect(result.branches.every((b) => b.isDefault === false)).toBe(true); + }); + }); + + // ── checkoutGitBranch ── + + describe("checkoutGitBranch", () => { + it("checks out an existing branch", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "feature" }); + + await checkoutGitBranch({ cwd: tmpDir, branch: "feature" }); + + const result = await listGitBranches({ cwd: tmpDir }); + const current = result.branches.find((b) => b.current); + expect(current!.name).toBe("feature"); + }); + + it("throws when branch does not exist", async () => { + await initRepoWithCommit(tmpDir); + await expect( + checkoutGitBranch({ cwd: tmpDir, branch: "nonexistent" }), + ).rejects.toThrow(); + }); + + it("throws when checkout would overwrite uncommitted changes", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "other" }); + + // Create a conflicting change: modify README on current branch + await writeFile(path.join(tmpDir, "README.md"), "modified\n"); + await git(tmpDir, "add README.md"); + + // On the other branch, README has original content — checkout should conflict + // Actually git checkout with staged changes can sometimes work, so let's + // create a real conflict: commit different content on other branch first. + // Simpler: write to a tracked file without committing, then checkout a branch + // where that file differs. + + // First, checkout other branch cleanly + await git(tmpDir, "stash"); + await checkoutGitBranch({ cwd: tmpDir, branch: "other" }); + await writeFile(path.join(tmpDir, "README.md"), "other content\n"); + await git(tmpDir, "add ."); + await git(tmpDir, "commit -m 'other change'"); + + // Go back to default branch + const defaultBranch = ( + await listGitBranches({ cwd: tmpDir }) + ).branches.find((b) => !b.current)!.name; + await checkoutGitBranch({ cwd: tmpDir, branch: defaultBranch }); + + // Make uncommitted changes to the same file + await writeFile(path.join(tmpDir, "README.md"), "conflicting local\n"); + + // Checkout should fail due to uncommitted changes + await expect( + checkoutGitBranch({ cwd: tmpDir, branch: "other" }), + ).rejects.toThrow(); + }); + }); + + // ── createGitBranch ── + + describe("createGitBranch", () => { + it("creates a new branch visible in listGitBranches", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "new-feature" }); + + const result = await listGitBranches({ cwd: tmpDir }); + expect(result.branches.some((b) => b.name === "new-feature")).toBe(true); + }); + + it("throws when branch already exists", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "dupe" }); + await expect( + createGitBranch({ cwd: tmpDir, branch: "dupe" }), + ).rejects.toThrow(); + }); + }); + + // ── createGitWorktree + removeGitWorktree ── + + describe("createGitWorktree", () => { + it("creates a worktree directory at the expected path", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "wt-branch" }); + + const wtPath = path.join(tmpDir, "worktree-out"); + const result = await createGitWorktree({ + cwd: tmpDir, + branch: "wt-branch", + path: wtPath, + }); + + expect(result.worktree.path).toBe(wtPath); + expect(result.worktree.branch).toBe("wt-branch"); + expect(existsSync(wtPath)).toBe(true); + expect(existsSync(path.join(wtPath, "README.md"))).toBe(true); + + // Clean up worktree + await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + }); + + it("worktree has the correct branch checked out", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "wt-check" }); + + const wtPath = path.join(tmpDir, "wt-check-dir"); + await createGitWorktree({ + cwd: tmpDir, + branch: "wt-check", + path: wtPath, + }); + + // Verify the worktree is on the right branch + const branchOutput = await git(wtPath, "branch --show-current"); + expect(branchOutput).toBe("wt-check"); + + await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + }); + + it("throws when branch is already checked out in main worktree", async () => { + await initRepoWithCommit(tmpDir); + // Try to create a worktree for the current branch (already checked out) + const branches = await listGitBranches({ cwd: tmpDir }); + const currentBranch = branches.branches.find((b) => b.current)!.name; + + const wtPath = path.join(tmpDir, "wt-conflict"); + await expect( + createGitWorktree({ + cwd: tmpDir, + branch: currentBranch, + path: wtPath, + }), + ).rejects.toThrow(); + }); + + it("removeGitWorktree cleans up the worktree", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "wt-remove" }); + + const wtPath = path.join(tmpDir, "wt-remove-dir"); + await createGitWorktree({ + cwd: tmpDir, + branch: "wt-remove", + path: wtPath, + }); + expect(existsSync(wtPath)).toBe(true); + + await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + expect(existsSync(wtPath)).toBe(false); + }); + }); + + // ── Full flow: local branch checkout ── + + describe("full flow: local branch checkout", () => { + it("init → commit → create branch → checkout → verify current", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "feature-login" }); + await checkoutGitBranch({ cwd: tmpDir, branch: "feature-login" }); + + const result = await listGitBranches({ cwd: tmpDir }); + const current = result.branches.find((b) => b.current); + expect(current!.name).toBe("feature-login"); + }); + }); + + // ── Full flow: worktree creation from selected branch ── + + describe("full flow: worktree creation", () => { + it("creates worktree from a non-current branch", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "feature-wt" }); + + const wtPath = path.join(tmpDir, "my-worktree"); + const result = await createGitWorktree({ + cwd: tmpDir, + branch: "feature-wt", + path: wtPath, + }); + + // Worktree exists + expect(existsSync(result.worktree.path)).toBe(true); + + // Main repo still on original branch + const mainBranches = await listGitBranches({ cwd: tmpDir }); + const mainCurrent = mainBranches.branches.find((b) => b.current); + expect(mainCurrent!.name).not.toBe("feature-wt"); + + // Worktree is on feature-wt + const wtBranch = await git(wtPath, "branch --show-current"); + expect(wtBranch).toBe("feature-wt"); + + await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + }); + }); + + // ── Full flow: thread switching simulation ── + + describe("full flow: thread switching (checkout toggling)", () => { + it("checkout a → checkout b → checkout a → current matches", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "branch-a" }); + await createGitBranch({ cwd: tmpDir, branch: "branch-b" }); + + // Simulate switching to thread A's branch + await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); + let branches = await listGitBranches({ cwd: tmpDir }); + expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); + + // Simulate switching to thread B's branch + await checkoutGitBranch({ cwd: tmpDir, branch: "branch-b" }); + branches = await listGitBranches({ cwd: tmpDir }); + expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b"); + + // Switch back to thread A + await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); + branches = await listGitBranches({ cwd: tmpDir }); + expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); + }); + }); + + // ── Full flow: checkout conflict ── + + describe("full flow: checkout conflict", () => { + it("uncommitted changes prevent checkout to a diverged branch", async () => { + await initRepoWithCommit(tmpDir); + await createGitBranch({ cwd: tmpDir, branch: "diverged" }); + + // Make diverged branch have different file content + await checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }); + await writeFile(path.join(tmpDir, "README.md"), "diverged content\n"); + await git(tmpDir, "add ."); + await git(tmpDir, "commit -m 'diverge'"); + + // Go back to default branch + const defaultBranch = ( + await git(tmpDir, "rev-parse --abbrev-ref HEAD") + ).includes("diverged") + ? // we're on diverged, need to find the other one + (await listGitBranches({ cwd: tmpDir })).branches.find( + (b) => !b.current && b.name !== "diverged", + )!.name + : (await git(tmpDir, "rev-parse --abbrev-ref HEAD")); + + // Actually, let's just get back to the initial branch explicitly + const allBranches = await listGitBranches({ cwd: tmpDir }); + const initialBranch = allBranches.branches.find( + (b) => b.name !== "diverged", + )!.name; + await checkoutGitBranch({ cwd: tmpDir, branch: initialBranch }); + + // Make local uncommitted changes to the same file + await writeFile(path.join(tmpDir, "README.md"), "local uncommitted\n"); + + // Attempt checkout should fail + await expect( + checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }), + ).rejects.toThrow(); + + // Current branch should still be the initial one + const result = await listGitBranches({ cwd: tmpDir }); + expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch); + }); + }); +}); diff --git a/apps/desktop/src/git.ts b/apps/desktop/src/git.ts new file mode 100644 index 0000000000..6f253b4d57 --- /dev/null +++ b/apps/desktop/src/git.ts @@ -0,0 +1,202 @@ +import { spawn } from "node:child_process"; +import path from "node:path"; + +import type { + GitCheckoutInput, + GitCreateBranchInput, + GitCreateWorktreeInput, + GitCreateWorktreeResult, + GitInitInput, + GitListBranchesInput, + GitListBranchesResult, + GitRemoveWorktreeInput, + TerminalCommandInput, + TerminalCommandResult, +} from "@t3tools/contracts"; + +export async function runTerminalCommand( + input: TerminalCommandInput, +): Promise { + const shellPath = + process.platform === "win32" + ? (process.env.ComSpec ?? "cmd.exe") + : (process.env.SHELL ?? "/bin/sh"); + + const args = + process.platform === "win32" + ? ["/d", "/s", "/c", input.command] + : ["-lc", input.command]; + + return new Promise((resolve, reject) => { + const child = spawn(shellPath, args, { + cwd: input.cwd, + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + + const timeout = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + setTimeout(() => { + if (!child.killed) { + child.kill("SIGKILL"); + } + }, 1_000).unref(); + }, input.timeoutMs ?? 30_000); + + child.stdout?.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + + child.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); + + child.on("error", (error) => { + clearTimeout(timeout); + reject(error); + }); + + child.on("close", (code, signal) => { + clearTimeout(timeout); + resolve({ + stdout, + stderr, + code: code ?? null, + signal: signal ?? null, + timedOut, + }); + }); + }); +} + +export async function listGitBranches( + input: GitListBranchesInput, +): Promise { + const result = await runTerminalCommand({ + command: "git branch --no-color", + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + const stderr = result.stderr.trim(); + if (stderr.includes("not a git repository")) { + return { branches: [], isRepo: false }; + } + throw new Error(stderr || "git branch failed"); + } + + // Resolve the real default branch from the remote + const defaultRef = await runTerminalCommand({ + command: "git symbolic-ref refs/remotes/origin/HEAD", + cwd: input.cwd, + timeoutMs: 5_000, + }); + const defaultBranch = + defaultRef.code === 0 + ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") + : null; + + const branches = result.stdout + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => ({ + name: line.replace(/^[*+]\s+/, ""), + current: line.startsWith("* "), + isDefault: line.replace(/^[*+]\s+/, "") === defaultBranch, + })) + .sort((a, b) => { + if (a.current !== b.current) return a.current ? -1 : 1; + if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; + return a.name.localeCompare(b.name); + }); + + return { branches, isRepo: true }; +} + +export async function createGitWorktree( + input: GitCreateWorktreeInput, +): Promise { + const sanitizedBranch = input.branch.replace(/\//g, "-"); + const repoName = path.basename(input.cwd); + const worktreePath = + input.path ?? + path.join(input.cwd, "..", `${repoName}-worktrees`, sanitizedBranch); + + const result = await runTerminalCommand({ + command: `git worktree add '${worktreePath.replace(/'/g, "'\\''")}' '${input.branch.replace(/'/g, "'\\''")}'`, + cwd: input.cwd, + timeoutMs: 30_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git worktree add failed"); + } + + return { + worktree: { + path: worktreePath, + branch: input.branch, + }, + }; +} + +export async function removeGitWorktree( + input: GitRemoveWorktreeInput, +): Promise { + const result = await runTerminalCommand({ + command: `git worktree remove '${input.path.replace(/'/g, "'\\''")}'`, + cwd: input.cwd, + timeoutMs: 15_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git worktree remove failed"); + } +} + +export async function createGitBranch( + input: GitCreateBranchInput, +): Promise { + const result = await runTerminalCommand({ + command: `git branch '${input.branch.replace(/'/g, "'\\''")}'`, + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git branch create failed"); + } +} + +export async function checkoutGitBranch( + input: GitCheckoutInput, +): Promise { + const result = await runTerminalCommand({ + command: `git checkout '${input.branch.replace(/'/g, "'\\''")}'`, + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git checkout failed"); + } +} + +export async function initGitRepo(input: GitInitInput): Promise { + const result = await runTerminalCommand({ + command: "git init", + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git init failed"); + } +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 128e3f2e91..0635064eab 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -384,7 +384,9 @@ export default function ChatView() { setIsEditorMenuOpen(false); }; - const ensureSession = async (): Promise => { + const ensureSession = async ( + cwdOverride?: string, + ): Promise => { if (!api || !activeThread || !activeProject) return null; if (activeThread.session && activeThread.session.status !== "closed") { const sessionThreadId = activeThread.session.threadId ?? null; @@ -406,7 +408,7 @@ export default function ChatView() { try { const session = await api.providers.startSession({ provider: "codex", - cwd: activeProject.cwd || undefined, + cwd: cwdOverride ?? activeThread.worktreePath ?? activeProject.cwd, model: selectedModel || undefined, resumeThreadId: priorCodexThreadId ?? undefined, approvalPolicy: runtimeSessionConfig.approvalPolicy, @@ -471,7 +473,7 @@ export default function ChatView() { const previousMessages = activeThread.messages; setPrompt(""); - const sessionInfo = await ensureSession(); + const sessionInfo = await ensureSession(activeThread.worktreePath ?? undefined); if (!sessionInfo) return; setIsSending(true); From 7e201486f15575d587890dbe1fd637a5f552da91 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 11:00:24 -0800 Subject: [PATCH 08/31] resource management --- apps/desktop/src/git.test.ts | 257 ++++++++++++++++++----------------- apps/desktop/tsconfig.json | 2 +- 2 files changed, 131 insertions(+), 128 deletions(-) diff --git a/apps/desktop/src/git.test.ts b/apps/desktop/src/git.test.ts index 37f3e8e09e..ed55f02c0f 100644 --- a/apps/desktop/src/git.test.ts +++ b/apps/desktop/src/git.test.ts @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { checkoutGitBranch, @@ -29,6 +29,17 @@ async function git(cwd: string, command: string): Promise { return result.stdout.trim(); } +/** Create a disposable temp directory that cleans up automatically. */ +async function makeTmpDir() { + const dir = await mkdtemp(path.join(tmpdir(), "git-test-")); + return { + path: dir, + [Symbol.asyncDispose]: async () => { + await rm(dir, { recursive: true, force: true }); + }, + }; +} + /** Create a repo with an initial commit so branches work. */ async function initRepoWithCommit(cwd: string): Promise { await initGitRepo({ cwd }); @@ -42,27 +53,19 @@ async function initRepoWithCommit(cwd: string): Promise { // ── Tests ── describe("git integration", () => { - let tmpDir: string; - - beforeEach(async () => { - tmpDir = await mkdtemp(path.join(tmpdir(), "git-test-")); - }); - - afterEach(async () => { - await rm(tmpDir, { recursive: true, force: true }); - }); - // ── initGitRepo ── describe("initGitRepo", () => { it("creates a valid git repo", async () => { - await initGitRepo({ cwd: tmpDir }); - expect(existsSync(path.join(tmpDir, ".git"))).toBe(true); + await using tmp = await makeTmpDir(); + await initGitRepo({ cwd: tmp.path }); + expect(existsSync(path.join(tmp.path, ".git"))).toBe(true); }); it("listGitBranches reports isRepo: true after init + commit", async () => { - await initRepoWithCommit(tmpDir); - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.isRepo).toBe(true); expect(result.branches.length).toBeGreaterThanOrEqual(1); }); @@ -72,42 +75,47 @@ describe("git integration", () => { describe("listGitBranches", () => { it("returns isRepo: false for non-git directory", async () => { - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.isRepo).toBe(false); expect(result.branches).toEqual([]); }); it("returns the current branch with current: true", async () => { - await initRepoWithCommit(tmpDir); - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); const current = result.branches.find((b) => b.current); expect(current).toBeDefined(); expect(current!.current).toBe(true); }); it("sorts current branch first", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "aaa-first-alpha" }); - await createGitBranch({ cwd: tmpDir, branch: "zzz-last" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" }); + await createGitBranch({ cwd: tmp.path, branch: "zzz-last" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches[0]!.current).toBe(true); }); it("lists multiple branches after creating them", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature-a" }); - await createGitBranch({ cwd: tmpDir, branch: "feature-b" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-a" }); + await createGitBranch({ cwd: tmp.path, branch: "feature-b" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); const names = result.branches.map((b) => b.name); expect(names).toContain("feature-a"); expect(names).toContain("feature-b"); }); it("isDefault is false when no remote exists", async () => { - await initRepoWithCommit(tmpDir); - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches.every((b) => b.isDefault === false)).toBe(true); }); }); @@ -116,57 +124,50 @@ describe("git integration", () => { describe("checkoutGitBranch", () => { it("checks out an existing branch", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature" }); - await checkoutGitBranch({ cwd: tmpDir, branch: "feature" }); + await checkoutGitBranch({ cwd: tmp.path, branch: "feature" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); const current = result.branches.find((b) => b.current); expect(current!.name).toBe("feature"); }); it("throws when branch does not exist", async () => { - await initRepoWithCommit(tmpDir); - await expect( - checkoutGitBranch({ cwd: tmpDir, branch: "nonexistent" }), - ).rejects.toThrow(); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "nonexistent" })).rejects.toThrow(); }); it("throws when checkout would overwrite uncommitted changes", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "other" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "other" }); // Create a conflicting change: modify README on current branch - await writeFile(path.join(tmpDir, "README.md"), "modified\n"); - await git(tmpDir, "add README.md"); - - // On the other branch, README has original content — checkout should conflict - // Actually git checkout with staged changes can sometimes work, so let's - // create a real conflict: commit different content on other branch first. - // Simpler: write to a tracked file without committing, then checkout a branch - // where that file differs. + await writeFile(path.join(tmp.path, "README.md"), "modified\n"); + await git(tmp.path, "add README.md"); // First, checkout other branch cleanly - await git(tmpDir, "stash"); - await checkoutGitBranch({ cwd: tmpDir, branch: "other" }); - await writeFile(path.join(tmpDir, "README.md"), "other content\n"); - await git(tmpDir, "add ."); - await git(tmpDir, "commit -m 'other change'"); + await git(tmp.path, "stash"); + await checkoutGitBranch({ cwd: tmp.path, branch: "other" }); + await writeFile(path.join(tmp.path, "README.md"), "other content\n"); + await git(tmp.path, "add ."); + await git(tmp.path, "commit -m 'other change'"); // Go back to default branch - const defaultBranch = ( - await listGitBranches({ cwd: tmpDir }) - ).branches.find((b) => !b.current)!.name; - await checkoutGitBranch({ cwd: tmpDir, branch: defaultBranch }); + const defaultBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => !b.current, + )!.name; + await checkoutGitBranch({ cwd: tmp.path, branch: defaultBranch }); // Make uncommitted changes to the same file - await writeFile(path.join(tmpDir, "README.md"), "conflicting local\n"); + await writeFile(path.join(tmp.path, "README.md"), "conflicting local\n"); // Checkout should fail due to uncommitted changes - await expect( - checkoutGitBranch({ cwd: tmpDir, branch: "other" }), - ).rejects.toThrow(); + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "other" })).rejects.toThrow(); }); }); @@ -174,19 +175,19 @@ describe("git integration", () => { describe("createGitBranch", () => { it("creates a new branch visible in listGitBranches", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "new-feature" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "new-feature" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches.some((b) => b.name === "new-feature")).toBe(true); }); it("throws when branch already exists", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "dupe" }); - await expect( - createGitBranch({ cwd: tmpDir, branch: "dupe" }), - ).rejects.toThrow(); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "dupe" }); + await expect(createGitBranch({ cwd: tmp.path, branch: "dupe" })).rejects.toThrow(); }); }); @@ -194,12 +195,13 @@ describe("git integration", () => { describe("createGitWorktree", () => { it("creates a worktree directory at the expected path", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "wt-branch" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "wt-branch" }); - const wtPath = path.join(tmpDir, "worktree-out"); + const wtPath = path.join(tmp.path, "worktree-out"); const result = await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "wt-branch", path: wtPath, }); @@ -209,17 +211,18 @@ describe("git integration", () => { expect(existsSync(wtPath)).toBe(true); expect(existsSync(path.join(wtPath, "README.md"))).toBe(true); - // Clean up worktree - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + // Clean up worktree before tmp dir disposal + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); it("worktree has the correct branch checked out", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "wt-check" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "wt-check" }); - const wtPath = path.join(tmpDir, "wt-check-dir"); + const wtPath = path.join(tmp.path, "wt-check-dir"); await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "wt-check", path: wtPath, }); @@ -228,19 +231,20 @@ describe("git integration", () => { const branchOutput = await git(wtPath, "branch --show-current"); expect(branchOutput).toBe("wt-check"); - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); it("throws when branch is already checked out in main worktree", async () => { - await initRepoWithCommit(tmpDir); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); // Try to create a worktree for the current branch (already checked out) - const branches = await listGitBranches({ cwd: tmpDir }); + const branches = await listGitBranches({ cwd: tmp.path }); const currentBranch = branches.branches.find((b) => b.current)!.name; - const wtPath = path.join(tmpDir, "wt-conflict"); + const wtPath = path.join(tmp.path, "wt-conflict"); await expect( createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: currentBranch, path: wtPath, }), @@ -248,18 +252,19 @@ describe("git integration", () => { }); it("removeGitWorktree cleans up the worktree", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "wt-remove" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "wt-remove" }); - const wtPath = path.join(tmpDir, "wt-remove-dir"); + const wtPath = path.join(tmp.path, "wt-remove-dir"); await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "wt-remove", path: wtPath, }); expect(existsSync(wtPath)).toBe(true); - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); expect(existsSync(wtPath)).toBe(false); }); }); @@ -268,11 +273,12 @@ describe("git integration", () => { describe("full flow: local branch checkout", () => { it("init → commit → create branch → checkout → verify current", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature-login" }); - await checkoutGitBranch({ cwd: tmpDir, branch: "feature-login" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-login" }); + await checkoutGitBranch({ cwd: tmp.path, branch: "feature-login" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); const current = result.branches.find((b) => b.current); expect(current!.name).toBe("feature-login"); }); @@ -282,12 +288,13 @@ describe("git integration", () => { describe("full flow: worktree creation", () => { it("creates worktree from a non-current branch", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature-wt" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-wt" }); - const wtPath = path.join(tmpDir, "my-worktree"); + const wtPath = path.join(tmp.path, "my-worktree"); const result = await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "feature-wt", path: wtPath, }); @@ -296,7 +303,7 @@ describe("git integration", () => { expect(existsSync(result.worktree.path)).toBe(true); // Main repo still on original branch - const mainBranches = await listGitBranches({ cwd: tmpDir }); + const mainBranches = await listGitBranches({ cwd: tmp.path }); const mainCurrent = mainBranches.branches.find((b) => b.current); expect(mainCurrent!.name).not.toBe("feature-wt"); @@ -304,7 +311,7 @@ describe("git integration", () => { const wtBranch = await git(wtPath, "branch --show-current"); expect(wtBranch).toBe("feature-wt"); - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); }); @@ -312,23 +319,24 @@ describe("git integration", () => { describe("full flow: thread switching (checkout toggling)", () => { it("checkout a → checkout b → checkout a → current matches", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "branch-a" }); - await createGitBranch({ cwd: tmpDir, branch: "branch-b" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "branch-a" }); + await createGitBranch({ cwd: tmp.path, branch: "branch-b" }); // Simulate switching to thread A's branch - await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); - let branches = await listGitBranches({ cwd: tmpDir }); + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); + let branches = await listGitBranches({ cwd: tmp.path }); expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); // Simulate switching to thread B's branch - await checkoutGitBranch({ cwd: tmpDir, branch: "branch-b" }); - branches = await listGitBranches({ cwd: tmpDir }); + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-b" }); + branches = await listGitBranches({ cwd: tmp.path }); expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b"); // Switch back to thread A - await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); - branches = await listGitBranches({ cwd: tmpDir }); + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); + branches = await listGitBranches({ cwd: tmp.path }); expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); }); }); @@ -337,42 +345,37 @@ describe("git integration", () => { describe("full flow: checkout conflict", () => { it("uncommitted changes prevent checkout to a diverged branch", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "diverged" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "diverged" }); // Make diverged branch have different file content - await checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }); - await writeFile(path.join(tmpDir, "README.md"), "diverged content\n"); - await git(tmpDir, "add ."); - await git(tmpDir, "commit -m 'diverge'"); + await checkoutGitBranch({ cwd: tmp.path, branch: "diverged" }); + await writeFile(path.join(tmp.path, "README.md"), "diverged content\n"); + await git(tmp.path, "add ."); + await git(tmp.path, "commit -m 'diverge'"); // Go back to default branch - const defaultBranch = ( - await git(tmpDir, "rev-parse --abbrev-ref HEAD") - ).includes("diverged") + const defaultBranch = (await git(tmp.path, "rev-parse --abbrev-ref HEAD")).includes("diverged") ? // we're on diverged, need to find the other one - (await listGitBranches({ cwd: tmpDir })).branches.find( + (await listGitBranches({ cwd: tmp.path })).branches.find( (b) => !b.current && b.name !== "diverged", )!.name - : (await git(tmpDir, "rev-parse --abbrev-ref HEAD")); + : await git(tmp.path, "rev-parse --abbrev-ref HEAD"); // Actually, let's just get back to the initial branch explicitly - const allBranches = await listGitBranches({ cwd: tmpDir }); - const initialBranch = allBranches.branches.find( - (b) => b.name !== "diverged", - )!.name; - await checkoutGitBranch({ cwd: tmpDir, branch: initialBranch }); + const allBranches = await listGitBranches({ cwd: tmp.path }); + const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name; + await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch }); // Make local uncommitted changes to the same file - await writeFile(path.join(tmpDir, "README.md"), "local uncommitted\n"); + await writeFile(path.join(tmp.path, "README.md"), "local uncommitted\n"); // Attempt checkout should fail - await expect( - checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }), - ).rejects.toThrow(); + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "diverged" })).rejects.toThrow(); // Current branch should still be the initial one - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch); }); }); diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 893a95a419..c914820301 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "composite": true, "types": ["node", "electron"], - "lib": ["ES2023", "DOM"] + "lib": ["ES2023", "DOM", "esnext.disposable"] }, "include": ["src", "tsup.config.ts"] } From 2a610f92b9c6254119d13bbc83b50c9ca7d288b4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 13:28:10 -0800 Subject: [PATCH 09/31] kewl --- apps/desktop/src/git.test.ts | 61 +++++++++++++++++++++---------- apps/desktop/src/git.ts | 8 ++-- apps/web/src/hooks/useGitState.ts | 12 ++++++ packages/contracts/src/git.ts | 3 ++ 4 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 apps/web/src/hooks/useGitState.ts diff --git a/apps/desktop/src/git.test.ts b/apps/desktop/src/git.test.ts index ed55f02c0f..104111b6cd 100644 --- a/apps/desktop/src/git.test.ts +++ b/apps/desktop/src/git.test.ts @@ -194,15 +194,19 @@ describe("git integration", () => { // ── createGitWorktree + removeGitWorktree ── describe("createGitWorktree", () => { - it("creates a worktree directory at the expected path", async () => { + it("creates a worktree with a new branch from the base branch", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "wt-branch" }); const wtPath = path.join(tmp.path, "worktree-out"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + const result = await createGitWorktree({ cwd: tmp.path, - branch: "wt-branch", + branch: currentBranch, + newBranch: "wt-branch", path: wtPath, }); @@ -215,37 +219,44 @@ describe("git integration", () => { await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); - it("worktree has the correct branch checked out", async () => { + it("worktree has the new branch checked out", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "wt-check" }); const wtPath = path.join(tmp.path, "wt-check-dir"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + await createGitWorktree({ cwd: tmp.path, - branch: "wt-check", + branch: currentBranch, + newBranch: "wt-check", path: wtPath, }); - // Verify the worktree is on the right branch + // Verify the worktree is on the new branch const branchOutput = await git(wtPath, "branch --show-current"); expect(branchOutput).toBe("wt-check"); await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); - it("throws when branch is already checked out in main worktree", async () => { + it("throws when new branch name already exists", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); - // Try to create a worktree for the current branch (already checked out) - const branches = await listGitBranches({ cwd: tmp.path }); - const currentBranch = branches.branches.find((b) => b.current)!.name; + await createGitBranch({ cwd: tmp.path, branch: "existing" }); const wtPath = path.join(tmp.path, "wt-conflict"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + await expect( createGitWorktree({ cwd: tmp.path, branch: currentBranch, + newBranch: "existing", path: wtPath, }), ).rejects.toThrow(); @@ -254,12 +265,16 @@ describe("git integration", () => { it("removeGitWorktree cleans up the worktree", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "wt-remove" }); const wtPath = path.join(tmp.path, "wt-remove-dir"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + await createGitWorktree({ cwd: tmp.path, - branch: "wt-remove", + branch: currentBranch, + newBranch: "wt-remove", path: wtPath, }); expect(existsSync(wtPath)).toBe(true); @@ -284,18 +299,22 @@ describe("git integration", () => { }); }); - // ── Full flow: worktree creation from selected branch ── + // ── Full flow: worktree creation from base branch ── describe("full flow: worktree creation", () => { - it("creates worktree from a non-current branch", async () => { + it("creates worktree with new branch from current branch", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "feature-wt" }); + + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; const wtPath = path.join(tmp.path, "my-worktree"); const result = await createGitWorktree({ cwd: tmp.path, - branch: "feature-wt", + branch: currentBranch, + newBranch: "feature-wt", path: wtPath, }); @@ -305,9 +324,9 @@ describe("git integration", () => { // Main repo still on original branch const mainBranches = await listGitBranches({ cwd: tmp.path }); const mainCurrent = mainBranches.branches.find((b) => b.current); - expect(mainCurrent!.name).not.toBe("feature-wt"); + expect(mainCurrent!.name).toBe(currentBranch); - // Worktree is on feature-wt + // Worktree is on the new branch const wtBranch = await git(wtPath, "branch --show-current"); expect(wtBranch).toBe("feature-wt"); @@ -356,7 +375,9 @@ describe("git integration", () => { await git(tmp.path, "commit -m 'diverge'"); // Go back to default branch - const defaultBranch = (await git(tmp.path, "rev-parse --abbrev-ref HEAD")).includes("diverged") + const defaultBranch = (await git(tmp.path, "rev-parse --abbrev-ref HEAD")).includes( + "diverged", + ) ? // we're on diverged, need to find the other one (await listGitBranches({ cwd: tmp.path })).branches.find( (b) => !b.current && b.name !== "diverged", diff --git a/apps/desktop/src/git.ts b/apps/desktop/src/git.ts index 6f253b4d57..072fc37e4c 100644 --- a/apps/desktop/src/git.ts +++ b/apps/desktop/src/git.ts @@ -123,14 +123,16 @@ export async function listGitBranches( export async function createGitWorktree( input: GitCreateWorktreeInput, ): Promise { - const sanitizedBranch = input.branch.replace(/\//g, "-"); + const esc = (s: string) => s.replace(/'/g, "'\\''"); + const sanitizedBranch = input.newBranch.replace(/\//g, "-"); const repoName = path.basename(input.cwd); const worktreePath = input.path ?? path.join(input.cwd, "..", `${repoName}-worktrees`, sanitizedBranch); + // Create a new branch from the base branch in a new worktree const result = await runTerminalCommand({ - command: `git worktree add '${worktreePath.replace(/'/g, "'\\''")}' '${input.branch.replace(/'/g, "'\\''")}'`, + command: `git worktree add -b '${esc(input.newBranch)}' '${esc(worktreePath)}' '${esc(input.branch)}'`, cwd: input.cwd, timeoutMs: 30_000, }); @@ -142,7 +144,7 @@ export async function createGitWorktree( return { worktree: { path: worktreePath, - branch: input.branch, + branch: input.newBranch, }, }; } diff --git a/apps/web/src/hooks/useGitState.ts b/apps/web/src/hooks/useGitState.ts new file mode 100644 index 0000000000..fc4ac6b453 --- /dev/null +++ b/apps/web/src/hooks/useGitState.ts @@ -0,0 +1,12 @@ +import { queryOptions, skipToken } from "@tanstack/react-query"; +import { readNativeApi } from "../session-logic"; + +export const listBranchesQuery = (cwd: string | undefined) => { + const api = readNativeApi(); + + return queryOptions({ + queryKey: ["git-branches", cwd], + queryFn: api && cwd ? () => api.git.listBranches({ cwd }) : skipToken, + refetchOnWindowFocus: true, + }); +}; diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 4f312de47c..a4284f2860 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -8,7 +8,10 @@ export const gitListBranchesInputSchema = z.object({ export const gitCreateWorktreeInputSchema = z.object({ cwd: z.string().min(1), + /** Base branch to create the worktree from. */ branch: z.string().min(1), + /** New branch name to create in the worktree. */ + newBranch: z.string().min(1), path: z.string().min(1).optional(), }); From c5cbe159935680f201e90eee73619d79b7eb0ad1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 13:39:13 -0800 Subject: [PATCH 10/31] Fix "Open in editor" to respect worktree path Both openInEditor and the Cmd+O shortcut now use the thread's worktreePath when available instead of always using activeProject.cwd. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/components/ChatView.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0635064eab..0df78d8275 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -368,17 +368,19 @@ export default function ChatView() { if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { if (api && activeProject) { e.preventDefault(); - void api.shell.openInEditor(activeProject.cwd, lastEditor); + const cwd = activeThread?.worktreePath ?? activeProject.cwd; + void api.shell.openInEditor(cwd, lastEditor); } } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); - }, [api, activeProject, lastEditor]); + }, [api, activeProject, activeThread, lastEditor]); const openInEditor = (editorId: EditorId) => { if (!api || !activeProject) return; - void api.shell.openInEditor(activeProject.cwd, editorId); + const cwd = activeThread?.worktreePath ?? activeProject.cwd; + void api.shell.openInEditor(cwd, editorId); setLastEditor(editorId); localStorage.setItem(LAST_EDITOR_KEY, editorId); setIsEditorMenuOpen(false); From 6ce773ff1e865de0f1eb8f790f840ac0351d94ef Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 14:15:39 -0800 Subject: [PATCH 11/31] Fix branch query cwd for worktree threads and error banner light mode Use worktree path instead of project cwd for git branch queries so worktree threads correctly report their branch as current. Also update error banner to use semantic color tokens for light mode support. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/components/ChatView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0df78d8275..55acbbcc7f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -649,7 +649,7 @@ export default function ChatView() { {/* Error banner */} {activeThread.error && ( -
+
{activeThread.error}
)} From 647449069dc5348b1dffe406b71210d7c988b881 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 14:24:22 -0800 Subject: [PATCH 12/31] Add test for listGitBranches from worktree cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies that querying branches from a worktree directory correctly reports the worktree's branch as current, while the main repo still shows its own branch — the assumption the BranchToolbar fix relies on. Co-Authored-By: Claude Opus 4.6 --- apps/desktop/src/git.test.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/apps/desktop/src/git.test.ts b/apps/desktop/src/git.test.ts index 104111b6cd..0683069558 100644 --- a/apps/desktop/src/git.test.ts +++ b/apps/desktop/src/git.test.ts @@ -262,6 +262,36 @@ describe("git integration", () => { ).rejects.toThrow(); }); + it("listGitBranches from worktree cwd reports worktree branch as current", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + + const wtPath = path.join(tmp.path, "wt-list-dir"); + const mainBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + + await createGitWorktree({ + cwd: tmp.path, + branch: mainBranch, + newBranch: "wt-list", + path: wtPath, + }); + + // listGitBranches from the worktree should show wt-list as current + const wtBranches = await listGitBranches({ cwd: wtPath }); + expect(wtBranches.isRepo).toBe(true); + const wtCurrent = wtBranches.branches.find((b) => b.current); + expect(wtCurrent!.name).toBe("wt-list"); + + // Main repo should still show the original branch as current + const mainBranches = await listGitBranches({ cwd: tmp.path }); + const mainCurrent = mainBranches.branches.find((b) => b.current); + expect(mainCurrent!.name).toBe(mainBranch); + + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); + }); + it("removeGitWorktree cleans up the worktree", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); From d1ecacda43718b0a96e9fca1cd7d30f82a64a574 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 23:38:06 -0800 Subject: [PATCH 13/31] Polish git worktree helpers after rebase Co-authored-by: codex --- apps/desktop/src/git.test.ts | 10 ---------- apps/desktop/src/git.ts | 15 +++++++++------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/git.test.ts b/apps/desktop/src/git.test.ts index 0683069558..dd73725c03 100644 --- a/apps/desktop/src/git.test.ts +++ b/apps/desktop/src/git.test.ts @@ -404,16 +404,6 @@ describe("git integration", () => { await git(tmp.path, "add ."); await git(tmp.path, "commit -m 'diverge'"); - // Go back to default branch - const defaultBranch = (await git(tmp.path, "rev-parse --abbrev-ref HEAD")).includes( - "diverged", - ) - ? // we're on diverged, need to find the other one - (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => !b.current && b.name !== "diverged", - )!.name - : await git(tmp.path, "rev-parse --abbrev-ref HEAD"); - // Actually, let's just get back to the initial branch explicitly const allBranches = await listGitBranches({ cwd: tmp.path }); const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name; diff --git a/apps/desktop/src/git.ts b/apps/desktop/src/git.ts index 072fc37e4c..9273672834 100644 --- a/apps/desktop/src/git.ts +++ b/apps/desktop/src/git.ts @@ -14,6 +14,10 @@ import type { TerminalCommandResult, } from "@t3tools/contracts"; +function escapeSingleQuotes(value: string): string { + return value.replace(/'/g, "'\\''"); +} + export async function runTerminalCommand( input: TerminalCommandInput, ): Promise { @@ -111,7 +115,7 @@ export async function listGitBranches( current: line.startsWith("* "), isDefault: line.replace(/^[*+]\s+/, "") === defaultBranch, })) - .sort((a, b) => { + .toSorted((a, b) => { if (a.current !== b.current) return a.current ? -1 : 1; if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; return a.name.localeCompare(b.name); @@ -123,7 +127,6 @@ export async function listGitBranches( export async function createGitWorktree( input: GitCreateWorktreeInput, ): Promise { - const esc = (s: string) => s.replace(/'/g, "'\\''"); const sanitizedBranch = input.newBranch.replace(/\//g, "-"); const repoName = path.basename(input.cwd); const worktreePath = @@ -132,7 +135,7 @@ export async function createGitWorktree( // Create a new branch from the base branch in a new worktree const result = await runTerminalCommand({ - command: `git worktree add -b '${esc(input.newBranch)}' '${esc(worktreePath)}' '${esc(input.branch)}'`, + command: `git worktree add -b '${escapeSingleQuotes(input.newBranch)}' '${escapeSingleQuotes(worktreePath)}' '${escapeSingleQuotes(input.branch)}'`, cwd: input.cwd, timeoutMs: 30_000, }); @@ -153,7 +156,7 @@ export async function removeGitWorktree( input: GitRemoveWorktreeInput, ): Promise { const result = await runTerminalCommand({ - command: `git worktree remove '${input.path.replace(/'/g, "'\\''")}'`, + command: `git worktree remove '${escapeSingleQuotes(input.path)}'`, cwd: input.cwd, timeoutMs: 15_000, }); @@ -167,7 +170,7 @@ export async function createGitBranch( input: GitCreateBranchInput, ): Promise { const result = await runTerminalCommand({ - command: `git branch '${input.branch.replace(/'/g, "'\\''")}'`, + command: `git branch '${escapeSingleQuotes(input.branch)}'`, cwd: input.cwd, timeoutMs: 10_000, }); @@ -181,7 +184,7 @@ export async function checkoutGitBranch( input: GitCheckoutInput, ): Promise { const result = await runTerminalCommand({ - command: `git checkout '${input.branch.replace(/'/g, "'\\''")}'`, + command: `git checkout '${escapeSingleQuotes(input.branch)}'`, cwd: input.cwd, timeoutMs: 10_000, }); From c13e9f89a977518a07426a68df48603127fc2ec7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 23:58:27 -0800 Subject: [PATCH 14/31] Restore git toolbar via websocket backend Co-authored-by: codex --- apps/server/src/git.test.ts | 423 ++++++++++++++++++++++ apps/server/src/git.ts | 207 +++++++++++ apps/server/src/wsServer.test.ts | 26 ++ apps/server/src/wsServer.ts | 26 ++ apps/server/tsconfig.json | 2 +- apps/web/src/App.tsx | 2 + apps/web/src/components/BranchToolbar.tsx | 312 ++++++++++++++++ apps/web/src/components/ChatView.tsx | 53 ++- apps/web/src/components/Sidebar.tsx | 2 + apps/web/src/store.test.ts | 2 + apps/web/src/store.ts | 16 + apps/web/src/wsNativeApi.ts | 10 + packages/contracts/src/ws.ts | 8 + 13 files changed, 1087 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/git.test.ts create mode 100644 apps/server/src/git.ts create mode 100644 apps/web/src/components/BranchToolbar.tsx diff --git a/apps/server/src/git.test.ts b/apps/server/src/git.test.ts new file mode 100644 index 0000000000..dd73725c03 --- /dev/null +++ b/apps/server/src/git.test.ts @@ -0,0 +1,423 @@ +import { existsSync } from "node:fs"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +import { + checkoutGitBranch, + createGitBranch, + createGitWorktree, + initGitRepo, + listGitBranches, + removeGitWorktree, + runTerminalCommand, +} from "./git"; + +// ── Helpers ── + +/** Run a raw git command for test setup (not under test). */ +async function git(cwd: string, command: string): Promise { + const result = await runTerminalCommand({ + command: `git ${command}`, + cwd, + timeoutMs: 10_000, + }); + if (result.code !== 0) { + throw new Error(`git ${command} failed: ${result.stderr}`); + } + return result.stdout.trim(); +} + +/** Create a disposable temp directory that cleans up automatically. */ +async function makeTmpDir() { + const dir = await mkdtemp(path.join(tmpdir(), "git-test-")); + return { + path: dir, + [Symbol.asyncDispose]: async () => { + await rm(dir, { recursive: true, force: true }); + }, + }; +} + +/** Create a repo with an initial commit so branches work. */ +async function initRepoWithCommit(cwd: string): Promise { + await initGitRepo({ cwd }); + await git(cwd, "config user.email 'test@test.com'"); + await git(cwd, "config user.name 'Test'"); + await writeFile(path.join(cwd, "README.md"), "# test\n"); + await git(cwd, "add ."); + await git(cwd, "commit -m 'initial commit'"); +} + +// ── Tests ── + +describe("git integration", () => { + // ── initGitRepo ── + + describe("initGitRepo", () => { + it("creates a valid git repo", async () => { + await using tmp = await makeTmpDir(); + await initGitRepo({ cwd: tmp.path }); + expect(existsSync(path.join(tmp.path, ".git"))).toBe(true); + }); + + it("listGitBranches reports isRepo: true after init + commit", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); + expect(result.isRepo).toBe(true); + expect(result.branches.length).toBeGreaterThanOrEqual(1); + }); + }); + + // ── listGitBranches ── + + describe("listGitBranches", () => { + it("returns isRepo: false for non-git directory", async () => { + await using tmp = await makeTmpDir(); + const result = await listGitBranches({ cwd: tmp.path }); + expect(result.isRepo).toBe(false); + expect(result.branches).toEqual([]); + }); + + it("returns the current branch with current: true", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); + const current = result.branches.find((b) => b.current); + expect(current).toBeDefined(); + expect(current!.current).toBe(true); + }); + + it("sorts current branch first", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" }); + await createGitBranch({ cwd: tmp.path, branch: "zzz-last" }); + + const result = await listGitBranches({ cwd: tmp.path }); + expect(result.branches[0]!.current).toBe(true); + }); + + it("lists multiple branches after creating them", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-a" }); + await createGitBranch({ cwd: tmp.path, branch: "feature-b" }); + + const result = await listGitBranches({ cwd: tmp.path }); + const names = result.branches.map((b) => b.name); + expect(names).toContain("feature-a"); + expect(names).toContain("feature-b"); + }); + + it("isDefault is false when no remote exists", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); + expect(result.branches.every((b) => b.isDefault === false)).toBe(true); + }); + }); + + // ── checkoutGitBranch ── + + describe("checkoutGitBranch", () => { + it("checks out an existing branch", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature" }); + + await checkoutGitBranch({ cwd: tmp.path, branch: "feature" }); + + const result = await listGitBranches({ cwd: tmp.path }); + const current = result.branches.find((b) => b.current); + expect(current!.name).toBe("feature"); + }); + + it("throws when branch does not exist", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "nonexistent" })).rejects.toThrow(); + }); + + it("throws when checkout would overwrite uncommitted changes", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "other" }); + + // Create a conflicting change: modify README on current branch + await writeFile(path.join(tmp.path, "README.md"), "modified\n"); + await git(tmp.path, "add README.md"); + + // First, checkout other branch cleanly + await git(tmp.path, "stash"); + await checkoutGitBranch({ cwd: tmp.path, branch: "other" }); + await writeFile(path.join(tmp.path, "README.md"), "other content\n"); + await git(tmp.path, "add ."); + await git(tmp.path, "commit -m 'other change'"); + + // Go back to default branch + const defaultBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => !b.current, + )!.name; + await checkoutGitBranch({ cwd: tmp.path, branch: defaultBranch }); + + // Make uncommitted changes to the same file + await writeFile(path.join(tmp.path, "README.md"), "conflicting local\n"); + + // Checkout should fail due to uncommitted changes + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "other" })).rejects.toThrow(); + }); + }); + + // ── createGitBranch ── + + describe("createGitBranch", () => { + it("creates a new branch visible in listGitBranches", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "new-feature" }); + + const result = await listGitBranches({ cwd: tmp.path }); + expect(result.branches.some((b) => b.name === "new-feature")).toBe(true); + }); + + it("throws when branch already exists", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "dupe" }); + await expect(createGitBranch({ cwd: tmp.path, branch: "dupe" })).rejects.toThrow(); + }); + }); + + // ── createGitWorktree + removeGitWorktree ── + + describe("createGitWorktree", () => { + it("creates a worktree with a new branch from the base branch", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + + const wtPath = path.join(tmp.path, "worktree-out"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + + const result = await createGitWorktree({ + cwd: tmp.path, + branch: currentBranch, + newBranch: "wt-branch", + path: wtPath, + }); + + expect(result.worktree.path).toBe(wtPath); + expect(result.worktree.branch).toBe("wt-branch"); + expect(existsSync(wtPath)).toBe(true); + expect(existsSync(path.join(wtPath, "README.md"))).toBe(true); + + // Clean up worktree before tmp dir disposal + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); + }); + + it("worktree has the new branch checked out", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + + const wtPath = path.join(tmp.path, "wt-check-dir"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + + await createGitWorktree({ + cwd: tmp.path, + branch: currentBranch, + newBranch: "wt-check", + path: wtPath, + }); + + // Verify the worktree is on the new branch + const branchOutput = await git(wtPath, "branch --show-current"); + expect(branchOutput).toBe("wt-check"); + + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); + }); + + it("throws when new branch name already exists", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "existing" }); + + const wtPath = path.join(tmp.path, "wt-conflict"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + + await expect( + createGitWorktree({ + cwd: tmp.path, + branch: currentBranch, + newBranch: "existing", + path: wtPath, + }), + ).rejects.toThrow(); + }); + + it("listGitBranches from worktree cwd reports worktree branch as current", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + + const wtPath = path.join(tmp.path, "wt-list-dir"); + const mainBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + + await createGitWorktree({ + cwd: tmp.path, + branch: mainBranch, + newBranch: "wt-list", + path: wtPath, + }); + + // listGitBranches from the worktree should show wt-list as current + const wtBranches = await listGitBranches({ cwd: wtPath }); + expect(wtBranches.isRepo).toBe(true); + const wtCurrent = wtBranches.branches.find((b) => b.current); + expect(wtCurrent!.name).toBe("wt-list"); + + // Main repo should still show the original branch as current + const mainBranches = await listGitBranches({ cwd: tmp.path }); + const mainCurrent = mainBranches.branches.find((b) => b.current); + expect(mainCurrent!.name).toBe(mainBranch); + + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); + }); + + it("removeGitWorktree cleans up the worktree", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + + const wtPath = path.join(tmp.path, "wt-remove-dir"); + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + + await createGitWorktree({ + cwd: tmp.path, + branch: currentBranch, + newBranch: "wt-remove", + path: wtPath, + }); + expect(existsSync(wtPath)).toBe(true); + + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); + expect(existsSync(wtPath)).toBe(false); + }); + }); + + // ── Full flow: local branch checkout ── + + describe("full flow: local branch checkout", () => { + it("init → commit → create branch → checkout → verify current", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-login" }); + await checkoutGitBranch({ cwd: tmp.path, branch: "feature-login" }); + + const result = await listGitBranches({ cwd: tmp.path }); + const current = result.branches.find((b) => b.current); + expect(current!.name).toBe("feature-login"); + }); + }); + + // ── Full flow: worktree creation from base branch ── + + describe("full flow: worktree creation", () => { + it("creates worktree with new branch from current branch", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + + const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => b.current, + )!.name; + + const wtPath = path.join(tmp.path, "my-worktree"); + const result = await createGitWorktree({ + cwd: tmp.path, + branch: currentBranch, + newBranch: "feature-wt", + path: wtPath, + }); + + // Worktree exists + expect(existsSync(result.worktree.path)).toBe(true); + + // Main repo still on original branch + const mainBranches = await listGitBranches({ cwd: tmp.path }); + const mainCurrent = mainBranches.branches.find((b) => b.current); + expect(mainCurrent!.name).toBe(currentBranch); + + // Worktree is on the new branch + const wtBranch = await git(wtPath, "branch --show-current"); + expect(wtBranch).toBe("feature-wt"); + + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); + }); + }); + + // ── Full flow: thread switching simulation ── + + describe("full flow: thread switching (checkout toggling)", () => { + it("checkout a → checkout b → checkout a → current matches", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "branch-a" }); + await createGitBranch({ cwd: tmp.path, branch: "branch-b" }); + + // Simulate switching to thread A's branch + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); + let branches = await listGitBranches({ cwd: tmp.path }); + expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); + + // Simulate switching to thread B's branch + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-b" }); + branches = await listGitBranches({ cwd: tmp.path }); + expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b"); + + // Switch back to thread A + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); + branches = await listGitBranches({ cwd: tmp.path }); + expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); + }); + }); + + // ── Full flow: checkout conflict ── + + describe("full flow: checkout conflict", () => { + it("uncommitted changes prevent checkout to a diverged branch", async () => { + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "diverged" }); + + // Make diverged branch have different file content + await checkoutGitBranch({ cwd: tmp.path, branch: "diverged" }); + await writeFile(path.join(tmp.path, "README.md"), "diverged content\n"); + await git(tmp.path, "add ."); + await git(tmp.path, "commit -m 'diverge'"); + + // Actually, let's just get back to the initial branch explicitly + const allBranches = await listGitBranches({ cwd: tmp.path }); + const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name; + await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch }); + + // Make local uncommitted changes to the same file + await writeFile(path.join(tmp.path, "README.md"), "local uncommitted\n"); + + // Attempt checkout should fail + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "diverged" })).rejects.toThrow(); + + // Current branch should still be the initial one + const result = await listGitBranches({ cwd: tmp.path }); + expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch); + }); + }); +}); diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts new file mode 100644 index 0000000000..9273672834 --- /dev/null +++ b/apps/server/src/git.ts @@ -0,0 +1,207 @@ +import { spawn } from "node:child_process"; +import path from "node:path"; + +import type { + GitCheckoutInput, + GitCreateBranchInput, + GitCreateWorktreeInput, + GitCreateWorktreeResult, + GitInitInput, + GitListBranchesInput, + GitListBranchesResult, + GitRemoveWorktreeInput, + TerminalCommandInput, + TerminalCommandResult, +} from "@t3tools/contracts"; + +function escapeSingleQuotes(value: string): string { + return value.replace(/'/g, "'\\''"); +} + +export async function runTerminalCommand( + input: TerminalCommandInput, +): Promise { + const shellPath = + process.platform === "win32" + ? (process.env.ComSpec ?? "cmd.exe") + : (process.env.SHELL ?? "/bin/sh"); + + const args = + process.platform === "win32" + ? ["/d", "/s", "/c", input.command] + : ["-lc", input.command]; + + return new Promise((resolve, reject) => { + const child = spawn(shellPath, args, { + cwd: input.cwd, + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + + const timeout = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + setTimeout(() => { + if (!child.killed) { + child.kill("SIGKILL"); + } + }, 1_000).unref(); + }, input.timeoutMs ?? 30_000); + + child.stdout?.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + + child.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); + + child.on("error", (error) => { + clearTimeout(timeout); + reject(error); + }); + + child.on("close", (code, signal) => { + clearTimeout(timeout); + resolve({ + stdout, + stderr, + code: code ?? null, + signal: signal ?? null, + timedOut, + }); + }); + }); +} + +export async function listGitBranches( + input: GitListBranchesInput, +): Promise { + const result = await runTerminalCommand({ + command: "git branch --no-color", + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + const stderr = result.stderr.trim(); + if (stderr.includes("not a git repository")) { + return { branches: [], isRepo: false }; + } + throw new Error(stderr || "git branch failed"); + } + + // Resolve the real default branch from the remote + const defaultRef = await runTerminalCommand({ + command: "git symbolic-ref refs/remotes/origin/HEAD", + cwd: input.cwd, + timeoutMs: 5_000, + }); + const defaultBranch = + defaultRef.code === 0 + ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") + : null; + + const branches = result.stdout + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => ({ + name: line.replace(/^[*+]\s+/, ""), + current: line.startsWith("* "), + isDefault: line.replace(/^[*+]\s+/, "") === defaultBranch, + })) + .toSorted((a, b) => { + if (a.current !== b.current) return a.current ? -1 : 1; + if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; + return a.name.localeCompare(b.name); + }); + + return { branches, isRepo: true }; +} + +export async function createGitWorktree( + input: GitCreateWorktreeInput, +): Promise { + const sanitizedBranch = input.newBranch.replace(/\//g, "-"); + const repoName = path.basename(input.cwd); + const worktreePath = + input.path ?? + path.join(input.cwd, "..", `${repoName}-worktrees`, sanitizedBranch); + + // Create a new branch from the base branch in a new worktree + const result = await runTerminalCommand({ + command: `git worktree add -b '${escapeSingleQuotes(input.newBranch)}' '${escapeSingleQuotes(worktreePath)}' '${escapeSingleQuotes(input.branch)}'`, + cwd: input.cwd, + timeoutMs: 30_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git worktree add failed"); + } + + return { + worktree: { + path: worktreePath, + branch: input.newBranch, + }, + }; +} + +export async function removeGitWorktree( + input: GitRemoveWorktreeInput, +): Promise { + const result = await runTerminalCommand({ + command: `git worktree remove '${escapeSingleQuotes(input.path)}'`, + cwd: input.cwd, + timeoutMs: 15_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git worktree remove failed"); + } +} + +export async function createGitBranch( + input: GitCreateBranchInput, +): Promise { + const result = await runTerminalCommand({ + command: `git branch '${escapeSingleQuotes(input.branch)}'`, + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git branch create failed"); + } +} + +export async function checkoutGitBranch( + input: GitCheckoutInput, +): Promise { + const result = await runTerminalCommand({ + command: `git checkout '${escapeSingleQuotes(input.branch)}'`, + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git checkout failed"); + } +} + +export async function initGitRepo(input: GitInitInput): Promise { + const result = await runTerminalCommand({ + command: "git init", + cwd: input.cwd, + timeoutMs: 10_000, + }); + + if (result.code !== 0) { + throw new Error(result.stderr.trim() || "git init failed"); + } +} diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index ef5cb1b963..770d1ccdb9 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -281,6 +281,32 @@ describe("WebSocket Server", () => { expect(afterRemove.result).toEqual([]); }); + it("supports git methods over websocket", async () => { + const repoCwd = makeTempDir("t3code-ws-git-project-"); + + server = createTestServer({ cwd: "/test" }); + await server.start(); + const addr = server.httpServer.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + + const ws = await connectWs(port); + connections.push(ws); + await waitForMessage(ws); + + const beforeInit = await sendRequest(ws, WS_METHODS.gitListBranches, { cwd: repoCwd }); + expect(beforeInit.error).toBeUndefined(); + expect(beforeInit.result).toEqual({ branches: [], isRepo: false }); + + const initResponse = await sendRequest(ws, WS_METHODS.gitInit, { cwd: repoCwd }); + expect(initResponse.error).toBeUndefined(); + + const afterInit = await sendRequest(ws, WS_METHODS.gitListBranches, { + cwd: repoCwd, + }); + expect(afterInit.error).toBeUndefined(); + expect((afterInit.result as { isRepo: boolean }).isRepo).toBe(true); + }); + it("prunes missing projects on startup", async () => { const stateDir = makeTempDir("t3code-ws-prune-state-"); const existing = makeTempDir("t3code-ws-existing-project-"); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index ae9dc7ca68..97c35f1660 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -19,6 +19,14 @@ import { WebSocketServer, type WebSocket } from "ws"; import { createLogger } from "./logger"; import { ProjectRegistry } from "./projectRegistry"; import { ProviderManager } from "./providerManager"; +import { + checkoutGitBranch, + createGitBranch, + createGitWorktree, + initGitRepo, + listGitBranches, + removeGitWorktree, +} from "./git"; const MIME_TYPES: Record = { ".html": "text/html; charset=utf-8", @@ -304,6 +312,24 @@ export function createServer(options: ServerOptions) { return undefined; } + case WS_METHODS.gitListBranches: + return listGitBranches(request.params as never); + + case WS_METHODS.gitCreateWorktree: + return createGitWorktree(request.params as never); + + case WS_METHODS.gitRemoveWorktree: + return removeGitWorktree(request.params as never); + + case WS_METHODS.gitCreateBranch: + return createGitBranch(request.params as never); + + case WS_METHODS.gitCheckout: + return checkoutGitBranch(request.params as never); + + case WS_METHODS.gitInit: + return initGitRepo(request.params as never); + case WS_METHODS.serverGetConfig: return { cwd }; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index d6c715e1c1..629a48bfc7 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "composite": true, "types": ["node"], - "lib": ["ES2023"] + "lib": ["ES2023", "esnext.disposable"] }, "include": ["src", "tsup.config.ts"] } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e12d563ab9..969f55a04c 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -82,6 +82,8 @@ function AutoProjectBootstrap() { events: [], error: null, createdAt: new Date().toISOString(), + branch: null, + worktreePath: null, }, }); }); diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx new file mode 100644 index 0000000000..c7cd2e25a4 --- /dev/null +++ b/apps/web/src/components/BranchToolbar.tsx @@ -0,0 +1,312 @@ +import type { GitBranch } from "@t3tools/contracts"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { readNativeApi } from "../session-logic"; +import { useStore } from "../store"; + +interface BranchToolbarProps { + envMode: "local" | "worktree"; + onEnvModeChange: (mode: "local" | "worktree") => void; + envLocked: boolean; +} + +export default function BranchToolbar({ + envMode, + onEnvModeChange, + envLocked, +}: BranchToolbarProps) { + const { state, dispatch } = useStore(); + const api = useMemo(() => readNativeApi(), []); + + const [branches, setBranches] = useState([]); + const [isRepo, setIsRepo] = useState(false); + const [isLoadingBranches, setIsLoadingBranches] = useState(false); + const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); + const [isCreatingBranch, setIsCreatingBranch] = useState(false); + const [newBranchName, setNewBranchName] = useState(""); + const [isInitializingRepo, setIsInitializingRepo] = useState(false); + const branchMenuRef = useRef(null); + + const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); + const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); + const branchCwd = activeThread?.worktreePath ?? activeProject?.cwd; + + const loadBranches = useCallback(async () => { + if (!api || !branchCwd) return; + setIsLoadingBranches(true); + try { + const result = await api.git.listBranches({ cwd: branchCwd }); + setIsRepo(result.isRepo); + setBranches(result.branches); + } catch (error) { + setIsRepo(false); + setBranches([]); + if (activeThread) { + dispatch({ + type: "SET_ERROR", + threadId: activeThread.id, + error: + error instanceof Error ? error.message : "Failed to load git branches.", + }); + } + } finally { + setIsLoadingBranches(false); + } + }, [activeThread, api, branchCwd, dispatch]); + + useEffect(() => { + void loadBranches(); + }, [loadBranches]); + + // Keep thread branch synced to git current branch for local threads. + useEffect(() => { + if (!activeThread || activeThread.worktreePath) return; + const current = branches.find((branch) => branch.current); + if (!current) return; + if (current.name === activeThread.branch) return; + dispatch({ + type: "SET_THREAD_BRANCH", + threadId: activeThread.id, + branch: current.name, + worktreePath: null, + }); + }, [activeThread, branches, dispatch]); + + useEffect(() => { + if (!isBranchMenuOpen) return; + const handleClickOutside = (event: MouseEvent) => { + if (!branchMenuRef.current) return; + if ( + event.target instanceof Node && + !branchMenuRef.current.contains(event.target) + ) { + setIsBranchMenuOpen(false); + } + }; + window.addEventListener("mousedown", handleClickOutside); + return () => { + window.removeEventListener("mousedown", handleClickOutside); + }; + }, [isBranchMenuOpen]); + + const setThreadError = (error: string | null) => { + if (!activeThread) return; + dispatch({ + type: "SET_ERROR", + threadId: activeThread.id, + error, + }); + }; + + const setThreadBranch = (branch: string | null, worktreePath: string | null) => { + if (!activeThread) return; + dispatch({ + type: "SET_THREAD_BRANCH", + threadId: activeThread.id, + branch, + worktreePath, + }); + }; + + const initializeRepo = async () => { + if (!api || !branchCwd || !activeThread || isInitializingRepo) return; + setIsInitializingRepo(true); + try { + await api.git.init({ cwd: branchCwd }); + setThreadError(null); + await loadBranches(); + } catch (error) { + setThreadError(error instanceof Error ? error.message : "Failed to initialize git repo."); + } finally { + setIsInitializingRepo(false); + } + }; + + const selectBranch = async (branch: GitBranch) => { + if (!api || !activeThread || !branchCwd) return; + + // For new worktree mode, selecting a branch picks the base branch. + if (envMode === "worktree" && !envLocked && !activeThread.worktreePath) { + setThreadError(null); + setThreadBranch(branch.name, null); + setIsBranchMenuOpen(false); + return; + } + + try { + await api.git.checkout({ + cwd: branchCwd, + branch: branch.name, + }); + setThreadError(null); + setThreadBranch(branch.name, activeThread.worktreePath); + setIsBranchMenuOpen(false); + await loadBranches(); + } catch (error) { + setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."); + } + }; + + const createBranch = async () => { + const name = newBranchName.trim(); + if (!api || !activeThread || !branchCwd || !name) return; + try { + await api.git.createBranch({ cwd: branchCwd, branch: name }); + await api.git.checkout({ cwd: branchCwd, branch: name }); + setThreadError(null); + setThreadBranch(name, activeThread.worktreePath); + setNewBranchName(""); + setIsCreatingBranch(false); + setIsBranchMenuOpen(false); + await loadBranches(); + } catch (error) { + setThreadError(error instanceof Error ? error.message : "Failed to create branch."); + } + }; + + if (!activeThread || !activeProject) return null; + + return ( +
+
+ {envLocked ? ( + + {activeThread.worktreePath ? "Worktree" : "Local"} + + ) : ( + + )} +
+ + {!isRepo ? ( + + ) : ( +
+ + {isBranchMenuOpen && ( +
+

+ Branch +

+
+ {branches.map((branch) => { + const isSelected = branch.name === activeThread.branch; + return ( + + ); + })} +
+ {envMode === "local" && ( + <> +
+ {isCreatingBranch ? ( +
{ + event.preventDefault(); + void createBranch(); + }} + > + setNewBranchName(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Escape") { + setIsCreatingBranch(false); + setNewBranchName(""); + } + }} + // biome-ignore lint/a11y/noAutofocus: branch name input should focus when shown + autoFocus + /> + +
+ ) : ( + + )} + + )} +
+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 55acbbcc7f..fe530e1e3f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -33,6 +33,7 @@ import { readNativeApi, } from "../session-logic"; import { useStore } from "../store"; +import BranchToolbar from "./BranchToolbar"; import ChatMarkdown from "./ChatMarkdown"; function formatMessageMeta(createdAt: string, duration: string | null): string { @@ -148,6 +149,7 @@ export default function ChatView() { }); const [selectedEffort, setSelectedEffort] = useState(DEFAULT_REASONING); + const [envMode, setEnvMode] = useState<"local" | "worktree">("local"); const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); const [respondingRequestIds, setRespondingRequestIds] = useState( [], @@ -265,6 +267,11 @@ export default function ChatView() { approvalPolicy: "on-request", sandboxMode: "workspace-write", } as const); + const envLocked = Boolean( + activeThread && + (activeThread.messages.length > 0 || + (activeThread.session !== null && activeThread.session.status !== "closed")), + ); const handleRuntimeModeChange = async ( mode: "approval-required" | "full-access", @@ -309,6 +316,11 @@ export default function ChatView() { setExpandedWorkGroups({}); }, [activeThread?.id]); + useEffect(() => { + if (!activeThread) return; + setEnvMode(activeThread.worktreePath ? "worktree" : "local"); + }, [activeThread]); + // Auto-resize textarea useEffect(() => { const ta = textareaRef.current; @@ -450,6 +462,39 @@ export default function ChatView() { if (!api || !activeThread || isSending || isConnecting) return; const trimmed = prompt.trim(); if (!trimmed) return; + if (!activeProject) return; + + // On first message: lock in branch + create worktree if needed. + let sessionCwd: string | undefined; + if ( + activeThread.messages.length === 0 && + activeThread.branch && + envMode === "worktree" && + !activeThread.worktreePath + ) { + try { + const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`; + const result = await api.git.createWorktree({ + cwd: activeProject.cwd, + branch: activeThread.branch, + newBranch, + }); + sessionCwd = result.worktree.path; + dispatch({ + type: "SET_THREAD_BRANCH", + threadId: activeThread.id, + branch: result.worktree.branch, + worktreePath: result.worktree.path, + }); + } catch (err) { + dispatch({ + type: "SET_ERROR", + threadId: activeThread.id, + error: err instanceof Error ? err.message : "Failed to create worktree", + }); + return; + } + } // Auto-title from first message if (activeThread.messages.length === 0) { @@ -475,7 +520,7 @@ export default function ChatView() { const previousMessages = activeThread.messages; setPrompt(""); - const sessionInfo = await ensureSession(activeThread.worktreePath ?? undefined); + const sessionInfo = await ensureSession(sessionCwd); if (!sessionInfo) return; setIsSending(true); @@ -910,6 +955,12 @@ export default function ChatView() { )}
+ + {/* Input bar */}
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 82568b4f9f..d3789c6667 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -57,6 +57,8 @@ export default function Sidebar() { events: [], error: null, createdAt: new Date().toISOString(), + branch: null, + worktreePath: null, }, }); }, diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts index c10bfe8e79..4b3e036781 100644 --- a/apps/web/src/store.test.ts +++ b/apps/web/src/store.test.ts @@ -39,6 +39,8 @@ function makeThread(overrides: Partial = {}): Thread { events: [], error: null, createdAt: "2026-02-09T00:00:00.000Z", + branch: null, + worktreePath: null, ...overrides, }; } diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index f9b075b89e..2ee930fbb9 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -33,6 +33,12 @@ type Action = | { type: "SET_ERROR"; threadId: string; error: string | null } | { type: "SET_THREAD_TITLE"; threadId: string; title: string } | { type: "SET_THREAD_MODEL"; threadId: string; model: string } + | { + type: "SET_THREAD_BRANCH"; + threadId: string; + branch: string | null; + worktreePath: string | null; + } | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode }; // ── State ──────────────────────────────────────────────────────────── @@ -369,6 +375,16 @@ export function reducer(state: AppState, action: Action): AppState { })), }; + case "SET_THREAD_BRANCH": + return { + ...state, + threads: updateThread(state.threads, action.threadId, (t) => ({ + ...t, + branch: action.branch, + worktreePath: action.worktreePath, + })), + }; + case "SET_RUNTIME_MODE": return { ...state, diff --git a/apps/web/src/wsNativeApi.ts b/apps/web/src/wsNativeApi.ts index 1ed5d5d2a1..7e8b3ad028 100644 --- a/apps/web/src/wsNativeApi.ts +++ b/apps/web/src/wsNativeApi.ts @@ -95,6 +95,16 @@ export function createWsNativeApi(): NativeApi { openInEditor: (cwd, editor) => transport.request(WS_METHODS.shellOpenInEditor, { cwd, editor }), }, + git: { + listBranches: (input) => transport.request(WS_METHODS.gitListBranches, input), + createWorktree: (input) => + transport.request(WS_METHODS.gitCreateWorktree, input), + removeWorktree: (input) => + transport.request(WS_METHODS.gitRemoveWorktree, input), + createBranch: (input) => transport.request(WS_METHODS.gitCreateBranch, input), + checkout: (input) => transport.request(WS_METHODS.gitCheckout, input), + init: (input) => transport.request(WS_METHODS.gitInit, input), + }, }; instance = { api, transport }; diff --git a/packages/contracts/src/ws.ts b/packages/contracts/src/ws.ts index 3264ca0ebe..641227500d 100644 --- a/packages/contracts/src/ws.ts +++ b/packages/contracts/src/ws.ts @@ -19,6 +19,14 @@ export const WS_METHODS = { // Shell methods shellOpenInEditor: "shell.openInEditor", + // Git methods + gitListBranches: "git.listBranches", + gitCreateWorktree: "git.createWorktree", + gitRemoveWorktree: "git.removeWorktree", + gitCreateBranch: "git.createBranch", + gitCheckout: "git.checkout", + gitInit: "git.init", + // Server meta serverGetConfig: "server.getConfig", } as const; From 79ac59f04e8f02b113dfdf5de9e9a5305c748f33 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Feb 2026 00:30:01 -0800 Subject: [PATCH 15/31] eh --- apps/desktop/package.json | 13 +- apps/desktop/src/git.test.ts | 423 ---------------- apps/desktop/src/git.ts | 207 -------- apps/desktop/tsconfig.json | 2 +- .../{tsup.config.ts => tsdown.config.ts} | 0 apps/desktop/turbo.jsonc | 13 + apps/server/package.json | 6 +- apps/server/src/git.ts | 28 +- apps/server/tsconfig.json | 2 +- .../{tsup.config.ts => tsdown.config.ts} | 0 apps/web/package.json | 4 +- apps/web/src/components/BranchToolbar.tsx | 2 +- apps/web/src/components/ChatView.tsx | 14 +- bun.lock | 477 ++++++++++++------ package.json | 4 +- packages/contracts/package.json | 21 +- 16 files changed, 366 insertions(+), 850 deletions(-) delete mode 100644 apps/desktop/src/git.test.ts delete mode 100644 apps/desktop/src/git.ts rename apps/desktop/{tsup.config.ts => tsdown.config.ts} (100%) create mode 100644 apps/desktop/turbo.jsonc rename apps/server/{tsup.config.ts => tsdown.config.ts} (100%) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9fc5d44ac8..3b4fd30f9c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -4,11 +4,11 @@ "private": true, "main": "dist-electron/main.js", "scripts": { - "dev": "bun run --cwd ../server build && concurrently -k -n BUNDLE,WEB,ELECTRON \"bun run dev:bundle\" \"bun run --cwd ../web dev\" \"bun run dev:electron\"", - "dev:bundle": "tsup --watch", + "dev": "bun run --parallel dev:bundle dev:electron", + "dev:bundle": "tsdown --watch", "dev:electron": "bun run scripts/dev-electron.mjs", - "build": "tsup", - "start": "electron dist-electron/main.js", + "build": "tsdown", + "start": "electron dist-electron/main.cjs", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests", "smoke-test": "node scripts/smoke-test.mjs" @@ -18,11 +18,10 @@ }, "devDependencies": { "@types/node": "^22.10.2", - "concurrently": "^9.1.2", "electronmon": "^2.0.2", - "tsup": "^8.3.5", + "tsdown": "^0.20.3", "typescript": "^5.7.3", - "vitest": "^3.0.0", + "vitest": "^4.0.0", "wait-on": "^8.0.2" } } diff --git a/apps/desktop/src/git.test.ts b/apps/desktop/src/git.test.ts deleted file mode 100644 index dd73725c03..0000000000 --- a/apps/desktop/src/git.test.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { existsSync } from "node:fs"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; - -import { - checkoutGitBranch, - createGitBranch, - createGitWorktree, - initGitRepo, - listGitBranches, - removeGitWorktree, - runTerminalCommand, -} from "./git"; - -// ── Helpers ── - -/** Run a raw git command for test setup (not under test). */ -async function git(cwd: string, command: string): Promise { - const result = await runTerminalCommand({ - command: `git ${command}`, - cwd, - timeoutMs: 10_000, - }); - if (result.code !== 0) { - throw new Error(`git ${command} failed: ${result.stderr}`); - } - return result.stdout.trim(); -} - -/** Create a disposable temp directory that cleans up automatically. */ -async function makeTmpDir() { - const dir = await mkdtemp(path.join(tmpdir(), "git-test-")); - return { - path: dir, - [Symbol.asyncDispose]: async () => { - await rm(dir, { recursive: true, force: true }); - }, - }; -} - -/** Create a repo with an initial commit so branches work. */ -async function initRepoWithCommit(cwd: string): Promise { - await initGitRepo({ cwd }); - await git(cwd, "config user.email 'test@test.com'"); - await git(cwd, "config user.name 'Test'"); - await writeFile(path.join(cwd, "README.md"), "# test\n"); - await git(cwd, "add ."); - await git(cwd, "commit -m 'initial commit'"); -} - -// ── Tests ── - -describe("git integration", () => { - // ── initGitRepo ── - - describe("initGitRepo", () => { - it("creates a valid git repo", async () => { - await using tmp = await makeTmpDir(); - await initGitRepo({ cwd: tmp.path }); - expect(existsSync(path.join(tmp.path, ".git"))).toBe(true); - }); - - it("listGitBranches reports isRepo: true after init + commit", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - const result = await listGitBranches({ cwd: tmp.path }); - expect(result.isRepo).toBe(true); - expect(result.branches.length).toBeGreaterThanOrEqual(1); - }); - }); - - // ── listGitBranches ── - - describe("listGitBranches", () => { - it("returns isRepo: false for non-git directory", async () => { - await using tmp = await makeTmpDir(); - const result = await listGitBranches({ cwd: tmp.path }); - expect(result.isRepo).toBe(false); - expect(result.branches).toEqual([]); - }); - - it("returns the current branch with current: true", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - const result = await listGitBranches({ cwd: tmp.path }); - const current = result.branches.find((b) => b.current); - expect(current).toBeDefined(); - expect(current!.current).toBe(true); - }); - - it("sorts current branch first", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" }); - await createGitBranch({ cwd: tmp.path, branch: "zzz-last" }); - - const result = await listGitBranches({ cwd: tmp.path }); - expect(result.branches[0]!.current).toBe(true); - }); - - it("lists multiple branches after creating them", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "feature-a" }); - await createGitBranch({ cwd: tmp.path, branch: "feature-b" }); - - const result = await listGitBranches({ cwd: tmp.path }); - const names = result.branches.map((b) => b.name); - expect(names).toContain("feature-a"); - expect(names).toContain("feature-b"); - }); - - it("isDefault is false when no remote exists", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - const result = await listGitBranches({ cwd: tmp.path }); - expect(result.branches.every((b) => b.isDefault === false)).toBe(true); - }); - }); - - // ── checkoutGitBranch ── - - describe("checkoutGitBranch", () => { - it("checks out an existing branch", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "feature" }); - - await checkoutGitBranch({ cwd: tmp.path, branch: "feature" }); - - const result = await listGitBranches({ cwd: tmp.path }); - const current = result.branches.find((b) => b.current); - expect(current!.name).toBe("feature"); - }); - - it("throws when branch does not exist", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await expect(checkoutGitBranch({ cwd: tmp.path, branch: "nonexistent" })).rejects.toThrow(); - }); - - it("throws when checkout would overwrite uncommitted changes", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "other" }); - - // Create a conflicting change: modify README on current branch - await writeFile(path.join(tmp.path, "README.md"), "modified\n"); - await git(tmp.path, "add README.md"); - - // First, checkout other branch cleanly - await git(tmp.path, "stash"); - await checkoutGitBranch({ cwd: tmp.path, branch: "other" }); - await writeFile(path.join(tmp.path, "README.md"), "other content\n"); - await git(tmp.path, "add ."); - await git(tmp.path, "commit -m 'other change'"); - - // Go back to default branch - const defaultBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => !b.current, - )!.name; - await checkoutGitBranch({ cwd: tmp.path, branch: defaultBranch }); - - // Make uncommitted changes to the same file - await writeFile(path.join(tmp.path, "README.md"), "conflicting local\n"); - - // Checkout should fail due to uncommitted changes - await expect(checkoutGitBranch({ cwd: tmp.path, branch: "other" })).rejects.toThrow(); - }); - }); - - // ── createGitBranch ── - - describe("createGitBranch", () => { - it("creates a new branch visible in listGitBranches", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "new-feature" }); - - const result = await listGitBranches({ cwd: tmp.path }); - expect(result.branches.some((b) => b.name === "new-feature")).toBe(true); - }); - - it("throws when branch already exists", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "dupe" }); - await expect(createGitBranch({ cwd: tmp.path, branch: "dupe" })).rejects.toThrow(); - }); - }); - - // ── createGitWorktree + removeGitWorktree ── - - describe("createGitWorktree", () => { - it("creates a worktree with a new branch from the base branch", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - - const wtPath = path.join(tmp.path, "worktree-out"); - const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => b.current, - )!.name; - - const result = await createGitWorktree({ - cwd: tmp.path, - branch: currentBranch, - newBranch: "wt-branch", - path: wtPath, - }); - - expect(result.worktree.path).toBe(wtPath); - expect(result.worktree.branch).toBe("wt-branch"); - expect(existsSync(wtPath)).toBe(true); - expect(existsSync(path.join(wtPath, "README.md"))).toBe(true); - - // Clean up worktree before tmp dir disposal - await removeGitWorktree({ cwd: tmp.path, path: wtPath }); - }); - - it("worktree has the new branch checked out", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - - const wtPath = path.join(tmp.path, "wt-check-dir"); - const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => b.current, - )!.name; - - await createGitWorktree({ - cwd: tmp.path, - branch: currentBranch, - newBranch: "wt-check", - path: wtPath, - }); - - // Verify the worktree is on the new branch - const branchOutput = await git(wtPath, "branch --show-current"); - expect(branchOutput).toBe("wt-check"); - - await removeGitWorktree({ cwd: tmp.path, path: wtPath }); - }); - - it("throws when new branch name already exists", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "existing" }); - - const wtPath = path.join(tmp.path, "wt-conflict"); - const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => b.current, - )!.name; - - await expect( - createGitWorktree({ - cwd: tmp.path, - branch: currentBranch, - newBranch: "existing", - path: wtPath, - }), - ).rejects.toThrow(); - }); - - it("listGitBranches from worktree cwd reports worktree branch as current", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - - const wtPath = path.join(tmp.path, "wt-list-dir"); - const mainBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => b.current, - )!.name; - - await createGitWorktree({ - cwd: tmp.path, - branch: mainBranch, - newBranch: "wt-list", - path: wtPath, - }); - - // listGitBranches from the worktree should show wt-list as current - const wtBranches = await listGitBranches({ cwd: wtPath }); - expect(wtBranches.isRepo).toBe(true); - const wtCurrent = wtBranches.branches.find((b) => b.current); - expect(wtCurrent!.name).toBe("wt-list"); - - // Main repo should still show the original branch as current - const mainBranches = await listGitBranches({ cwd: tmp.path }); - const mainCurrent = mainBranches.branches.find((b) => b.current); - expect(mainCurrent!.name).toBe(mainBranch); - - await removeGitWorktree({ cwd: tmp.path, path: wtPath }); - }); - - it("removeGitWorktree cleans up the worktree", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - - const wtPath = path.join(tmp.path, "wt-remove-dir"); - const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => b.current, - )!.name; - - await createGitWorktree({ - cwd: tmp.path, - branch: currentBranch, - newBranch: "wt-remove", - path: wtPath, - }); - expect(existsSync(wtPath)).toBe(true); - - await removeGitWorktree({ cwd: tmp.path, path: wtPath }); - expect(existsSync(wtPath)).toBe(false); - }); - }); - - // ── Full flow: local branch checkout ── - - describe("full flow: local branch checkout", () => { - it("init → commit → create branch → checkout → verify current", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "feature-login" }); - await checkoutGitBranch({ cwd: tmp.path, branch: "feature-login" }); - - const result = await listGitBranches({ cwd: tmp.path }); - const current = result.branches.find((b) => b.current); - expect(current!.name).toBe("feature-login"); - }); - }); - - // ── Full flow: worktree creation from base branch ── - - describe("full flow: worktree creation", () => { - it("creates worktree with new branch from current branch", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - - const currentBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( - (b) => b.current, - )!.name; - - const wtPath = path.join(tmp.path, "my-worktree"); - const result = await createGitWorktree({ - cwd: tmp.path, - branch: currentBranch, - newBranch: "feature-wt", - path: wtPath, - }); - - // Worktree exists - expect(existsSync(result.worktree.path)).toBe(true); - - // Main repo still on original branch - const mainBranches = await listGitBranches({ cwd: tmp.path }); - const mainCurrent = mainBranches.branches.find((b) => b.current); - expect(mainCurrent!.name).toBe(currentBranch); - - // Worktree is on the new branch - const wtBranch = await git(wtPath, "branch --show-current"); - expect(wtBranch).toBe("feature-wt"); - - await removeGitWorktree({ cwd: tmp.path, path: wtPath }); - }); - }); - - // ── Full flow: thread switching simulation ── - - describe("full flow: thread switching (checkout toggling)", () => { - it("checkout a → checkout b → checkout a → current matches", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "branch-a" }); - await createGitBranch({ cwd: tmp.path, branch: "branch-b" }); - - // Simulate switching to thread A's branch - await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); - let branches = await listGitBranches({ cwd: tmp.path }); - expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); - - // Simulate switching to thread B's branch - await checkoutGitBranch({ cwd: tmp.path, branch: "branch-b" }); - branches = await listGitBranches({ cwd: tmp.path }); - expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b"); - - // Switch back to thread A - await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); - branches = await listGitBranches({ cwd: tmp.path }); - expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); - }); - }); - - // ── Full flow: checkout conflict ── - - describe("full flow: checkout conflict", () => { - it("uncommitted changes prevent checkout to a diverged branch", async () => { - await using tmp = await makeTmpDir(); - await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "diverged" }); - - // Make diverged branch have different file content - await checkoutGitBranch({ cwd: tmp.path, branch: "diverged" }); - await writeFile(path.join(tmp.path, "README.md"), "diverged content\n"); - await git(tmp.path, "add ."); - await git(tmp.path, "commit -m 'diverge'"); - - // Actually, let's just get back to the initial branch explicitly - const allBranches = await listGitBranches({ cwd: tmp.path }); - const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name; - await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch }); - - // Make local uncommitted changes to the same file - await writeFile(path.join(tmp.path, "README.md"), "local uncommitted\n"); - - // Attempt checkout should fail - await expect(checkoutGitBranch({ cwd: tmp.path, branch: "diverged" })).rejects.toThrow(); - - // Current branch should still be the initial one - const result = await listGitBranches({ cwd: tmp.path }); - expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch); - }); - }); -}); diff --git a/apps/desktop/src/git.ts b/apps/desktop/src/git.ts deleted file mode 100644 index 9273672834..0000000000 --- a/apps/desktop/src/git.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { spawn } from "node:child_process"; -import path from "node:path"; - -import type { - GitCheckoutInput, - GitCreateBranchInput, - GitCreateWorktreeInput, - GitCreateWorktreeResult, - GitInitInput, - GitListBranchesInput, - GitListBranchesResult, - GitRemoveWorktreeInput, - TerminalCommandInput, - TerminalCommandResult, -} from "@t3tools/contracts"; - -function escapeSingleQuotes(value: string): string { - return value.replace(/'/g, "'\\''"); -} - -export async function runTerminalCommand( - input: TerminalCommandInput, -): Promise { - const shellPath = - process.platform === "win32" - ? (process.env.ComSpec ?? "cmd.exe") - : (process.env.SHELL ?? "/bin/sh"); - - const args = - process.platform === "win32" - ? ["/d", "/s", "/c", input.command] - : ["-lc", input.command]; - - return new Promise((resolve, reject) => { - const child = spawn(shellPath, args, { - cwd: input.cwd, - env: process.env, - stdio: ["ignore", "pipe", "pipe"], - }); - - let stdout = ""; - let stderr = ""; - let timedOut = false; - - const timeout = setTimeout(() => { - timedOut = true; - child.kill("SIGTERM"); - setTimeout(() => { - if (!child.killed) { - child.kill("SIGKILL"); - } - }, 1_000).unref(); - }, input.timeoutMs ?? 30_000); - - child.stdout?.on("data", (chunk: Buffer) => { - stdout += chunk.toString(); - }); - - child.stderr?.on("data", (chunk: Buffer) => { - stderr += chunk.toString(); - }); - - child.on("error", (error) => { - clearTimeout(timeout); - reject(error); - }); - - child.on("close", (code, signal) => { - clearTimeout(timeout); - resolve({ - stdout, - stderr, - code: code ?? null, - signal: signal ?? null, - timedOut, - }); - }); - }); -} - -export async function listGitBranches( - input: GitListBranchesInput, -): Promise { - const result = await runTerminalCommand({ - command: "git branch --no-color", - cwd: input.cwd, - timeoutMs: 10_000, - }); - - if (result.code !== 0) { - const stderr = result.stderr.trim(); - if (stderr.includes("not a git repository")) { - return { branches: [], isRepo: false }; - } - throw new Error(stderr || "git branch failed"); - } - - // Resolve the real default branch from the remote - const defaultRef = await runTerminalCommand({ - command: "git symbolic-ref refs/remotes/origin/HEAD", - cwd: input.cwd, - timeoutMs: 5_000, - }); - const defaultBranch = - defaultRef.code === 0 - ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") - : null; - - const branches = result.stdout - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .map((line) => ({ - name: line.replace(/^[*+]\s+/, ""), - current: line.startsWith("* "), - isDefault: line.replace(/^[*+]\s+/, "") === defaultBranch, - })) - .toSorted((a, b) => { - if (a.current !== b.current) return a.current ? -1 : 1; - if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; - return a.name.localeCompare(b.name); - }); - - return { branches, isRepo: true }; -} - -export async function createGitWorktree( - input: GitCreateWorktreeInput, -): Promise { - const sanitizedBranch = input.newBranch.replace(/\//g, "-"); - const repoName = path.basename(input.cwd); - const worktreePath = - input.path ?? - path.join(input.cwd, "..", `${repoName}-worktrees`, sanitizedBranch); - - // Create a new branch from the base branch in a new worktree - const result = await runTerminalCommand({ - command: `git worktree add -b '${escapeSingleQuotes(input.newBranch)}' '${escapeSingleQuotes(worktreePath)}' '${escapeSingleQuotes(input.branch)}'`, - cwd: input.cwd, - timeoutMs: 30_000, - }); - - if (result.code !== 0) { - throw new Error(result.stderr.trim() || "git worktree add failed"); - } - - return { - worktree: { - path: worktreePath, - branch: input.newBranch, - }, - }; -} - -export async function removeGitWorktree( - input: GitRemoveWorktreeInput, -): Promise { - const result = await runTerminalCommand({ - command: `git worktree remove '${escapeSingleQuotes(input.path)}'`, - cwd: input.cwd, - timeoutMs: 15_000, - }); - - if (result.code !== 0) { - throw new Error(result.stderr.trim() || "git worktree remove failed"); - } -} - -export async function createGitBranch( - input: GitCreateBranchInput, -): Promise { - const result = await runTerminalCommand({ - command: `git branch '${escapeSingleQuotes(input.branch)}'`, - cwd: input.cwd, - timeoutMs: 10_000, - }); - - if (result.code !== 0) { - throw new Error(result.stderr.trim() || "git branch create failed"); - } -} - -export async function checkoutGitBranch( - input: GitCheckoutInput, -): Promise { - const result = await runTerminalCommand({ - command: `git checkout '${escapeSingleQuotes(input.branch)}'`, - cwd: input.cwd, - timeoutMs: 10_000, - }); - - if (result.code !== 0) { - throw new Error(result.stderr.trim() || "git checkout failed"); - } -} - -export async function initGitRepo(input: GitInitInput): Promise { - const result = await runTerminalCommand({ - command: "git init", - cwd: input.cwd, - timeoutMs: 10_000, - }); - - if (result.code !== 0) { - throw new Error(result.stderr.trim() || "git init failed"); - } -} diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index c914820301..0ca5bcaa76 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -5,5 +5,5 @@ "types": ["node", "electron"], "lib": ["ES2023", "DOM", "esnext.disposable"] }, - "include": ["src", "tsup.config.ts"] + "include": ["src", "tsdown.config.ts"] } diff --git a/apps/desktop/tsup.config.ts b/apps/desktop/tsdown.config.ts similarity index 100% rename from apps/desktop/tsup.config.ts rename to apps/desktop/tsdown.config.ts diff --git a/apps/desktop/turbo.jsonc b/apps/desktop/turbo.jsonc new file mode 100644 index 0000000000..843ebb0b2d --- /dev/null +++ b/apps/desktop/turbo.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist-electron/**"], + }, + "dev": { + "persistent": true, + }, + }, +} diff --git a/apps/server/package.json b/apps/server/package.json index d9b3af34a7..f26426cff2 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -11,10 +11,8 @@ "main": "./dist/index.js", "scripts": { "dev": "VITE_DEV_SERVER_URL=http://localhost:5173 tsx src/index.ts", - "dev:desktop": "T3CODE_MODE=desktop T3CODE_NO_BROWSER=1 tsx src/index.ts", - "build": "tsup && node scripts/bundle-client.mjs", + "build": "tsdown && node scripts/bundle-client.mjs", "start": "node dist/index.js", - "start:desktop": "T3CODE_MODE=desktop T3CODE_NO_BROWSER=1 node dist/index.js", "typecheck": "tsc --noEmit", "test": "vitest run" }, @@ -27,7 +25,7 @@ "@t3tools/web": "workspace:*", "@types/node": "^22.10.2", "@types/ws": "^8.5.13", - "tsup": "^8.3.5", + "tsdown": "^0.20.3", "tsx": "^4.19.0", "typescript": "^5.7.3" } diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index 9273672834..bdb5d1b608 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -1,4 +1,5 @@ import { spawn } from "node:child_process"; +import os from "node:os"; import path from "node:path"; import type { @@ -27,9 +28,7 @@ export async function runTerminalCommand( : (process.env.SHELL ?? "/bin/sh"); const args = - process.platform === "win32" - ? ["/d", "/s", "/c", input.command] - : ["-lc", input.command]; + process.platform === "win32" ? ["/d", "/s", "/c", input.command] : ["-lc", input.command]; return new Promise((resolve, reject) => { const child = spawn(shellPath, args, { @@ -78,9 +77,7 @@ export async function runTerminalCommand( }); } -export async function listGitBranches( - input: GitListBranchesInput, -): Promise { +export async function listGitBranches(input: GitListBranchesInput): Promise { const result = await runTerminalCommand({ command: "git branch --no-color", cwd: input.cwd, @@ -102,9 +99,7 @@ export async function listGitBranches( timeoutMs: 5_000, }); const defaultBranch = - defaultRef.code === 0 - ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") - : null; + defaultRef.code === 0 ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "") : null; const branches = result.stdout .split("\n") @@ -130,8 +125,7 @@ export async function createGitWorktree( const sanitizedBranch = input.newBranch.replace(/\//g, "-"); const repoName = path.basename(input.cwd); const worktreePath = - input.path ?? - path.join(input.cwd, "..", `${repoName}-worktrees`, sanitizedBranch); + input.path ?? path.join(os.homedir(), ".t3", "worktrees", repoName, sanitizedBranch); // Create a new branch from the base branch in a new worktree const result = await runTerminalCommand({ @@ -152,9 +146,7 @@ export async function createGitWorktree( }; } -export async function removeGitWorktree( - input: GitRemoveWorktreeInput, -): Promise { +export async function removeGitWorktree(input: GitRemoveWorktreeInput): Promise { const result = await runTerminalCommand({ command: `git worktree remove '${escapeSingleQuotes(input.path)}'`, cwd: input.cwd, @@ -166,9 +158,7 @@ export async function removeGitWorktree( } } -export async function createGitBranch( - input: GitCreateBranchInput, -): Promise { +export async function createGitBranch(input: GitCreateBranchInput): Promise { const result = await runTerminalCommand({ command: `git branch '${escapeSingleQuotes(input.branch)}'`, cwd: input.cwd, @@ -180,9 +170,7 @@ export async function createGitBranch( } } -export async function checkoutGitBranch( - input: GitCheckoutInput, -): Promise { +export async function checkoutGitBranch(input: GitCheckoutInput): Promise { const result = await runTerminalCommand({ command: `git checkout '${escapeSingleQuotes(input.branch)}'`, cwd: input.cwd, diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 629a48bfc7..bf7111184a 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -5,5 +5,5 @@ "types": ["node"], "lib": ["ES2023", "esnext.disposable"] }, - "include": ["src", "tsup.config.ts"] + "include": ["src", "tsdown.config.ts"] } diff --git a/apps/server/tsup.config.ts b/apps/server/tsdown.config.ts similarity index 100% rename from apps/server/tsup.config.ts rename to apps/server/tsdown.config.ts diff --git a/apps/web/package.json b/apps/web/package.json index 57f79caa4f..2594309c1e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -25,10 +25,10 @@ "@tailwindcss/vite": "^4.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.1.4", "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", "tailwindcss": "^4.0.0", "typescript": "^5.7.3", - "vite": "^6.0.5" + "vite": "^8.0.0-beta.12" } } diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index c7cd2e25a4..1ba9c0cec1 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -167,7 +167,7 @@ export default function BranchToolbar({ if (!activeThread || !activeProject) return null; return ( -
+
{envLocked ? ( diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index fe530e1e3f..edb24386c8 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -955,14 +955,8 @@ export default function ChatView() { )}
- - {/* Input bar */} -
+
{/* Textarea area */} @@ -1231,6 +1225,12 @@ export default function ChatView() {
+ +
); } diff --git a/bun.lock b/bun.lock index 6732579846..5785a3f147 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "oxlint": "^1.43.0", "turbo": "^2.3.3", "typescript": "^5.7.3", - "vitest": "^3.0.0", + "vitest": "^4.0.0", }, }, "apps/desktop": { @@ -20,11 +20,10 @@ }, "devDependencies": { "@types/node": "^22.10.2", - "concurrently": "^9.1.2", "electronmon": "^2.0.2", - "tsup": "^8.3.5", + "tsdown": "^0.20.3", "typescript": "^5.7.3", - "vitest": "^3.0.0", + "vitest": "^4.0.0", "wait-on": "^8.0.2", }, }, @@ -43,7 +42,7 @@ "@t3tools/web": "workspace:*", "@types/node": "^22.10.2", "@types/ws": "^8.5.13", - "tsup": "^8.3.5", + "tsdown": "^0.20.3", "tsx": "^4.19.0", "typescript": "^5.7.3", }, @@ -66,11 +65,11 @@ "@tailwindcss/vite": "^4.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.1.4", "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", "tailwindcss": "^4.0.0", "typescript": "^5.7.3", - "vite": "^6.0.5", + "vite": "^8.0.0-beta.12", }, }, "packages/contracts": { @@ -80,7 +79,7 @@ "zod": "^3.24.1", }, "devDependencies": { - "tsup": "^8.3.5", + "tsdown": "^0.20.3", "typescript": "^5.7.3", }, }, @@ -92,7 +91,7 @@ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@8.0.0-rc.1", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.1", "@babel/types": "^8.0.0-rc.1", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-3ypWOOiC4AYHKr8vYRVtWtWmyvcoItHtVqF8paFax+ydpmUdPsJpLBkBBs5ItmhdrwC3a0ZSqqFAdzls4ODP3w=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], @@ -106,13 +105,13 @@ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.1", "", {}, "sha512-I4YnARytXC2RzkLNVnf5qFNFMzp679qZpmtw/V3Jt2uGnWiIxyJtaukjG7R8pSx8nG2NamICpGfljQsogj+FbQ=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/parser": ["@babel/parser@8.0.0-rc.1", "", { "dependencies": { "@babel/types": "^8.0.0-rc.1" }, "bin": "./bin/babel-parser.js" }, "sha512-6HyyU5l1yK/7h9Ki52i5h6mDAx4qJdiLQO4FdCyJNoB/gy3T3GGJdhQzzbZgvgZCugYBvwtQiWRt94QKedHnkA=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], @@ -126,6 +125,12 @@ "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -200,6 +205,12 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.112.0", "", {}, "sha512-4vYtWXMnXM6EaweCxbJ6bISAhkNHeN33SihvuX3wrpqaSJA4ZEoW35i9mSvE74+GDf1yTeVE+aEHA+WBpjDk/g=="], + + "@oxc-project/types": ["@oxc-project/types@0.112.0", "", {}, "sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ=="], + "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="], "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="], @@ -232,7 +243,35 @@ "@oxlint/win32-x64": ["@oxlint/win32-x64@1.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bSuItSU8mTSDsvmmLTepTdCL2FkJI6dwt9tot/k0EmiYF+ArRzmsl4lXVLssJNRV5lJEc5IViyTrh7oiwrjUqA=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.3", "", { "os": "android", "cpu": "arm64" }, "sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JWWLzvcmc/3pe7qdJqPpuPk91SoE/N+f3PcWx/6ZwuyDVyungAEJPvKm/eEldiDdwTmaEzWfIR+HORxYWrCi1A=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-MTakBxfx3tde5WSmbHxuqlDsIW0EzQym+PJYGF4P6lG2NmKzi128OGynoFUqoD5ryCySEY85dug4v+LWGBElIw=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jje3oopyOLs7IwfvXoS6Lxnmie5JJO7vW29fdGFu5YGY1EDbVDhD+P9vDihqS5X6fFiqL3ZQZCMBg6jyHkSVww=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.3", "", { "os": "linux", "cpu": "arm" }, "sha512-A0n8P3hdLAaqzSFrQoA42p23ZKBYQOw+8EH5r15Sa9X1kD9/JXe0YT2gph2QTWvdr0CVK2BOXiK6ENfy6DXOag=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kWXkoxxarYISBJ4bLNf5vFkEbb4JvccOwxWDxuK9yee8lg5XA7OpvlTptfRuwEvYcOZf+7VS69Uenpmpyo5Bjw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.3", "", { "os": "linux", "cpu": "x64" }, "sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.3", "", { "os": "linux", "cpu": "x64" }, "sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.3", "", { "os": "none", "cpu": "arm64" }, "sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.3", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-gekrQ3Q2HiC1T5njGyuUJoGpK/l6B/TNXKed3fZXNf9YRTJn3L5MOZsFBn4bN2+UX+8+7hgdlTcEsexX988G4g=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-85y5JifyMgs8m5K2XzR/VDsapKbiFiohl7s5lEj7nmNGO0pkTXE7q6TQScei96BNAsoK7JC3pA7ukA8WRHVJpg=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.3", "", { "os": "win32", "cpu": "x64" }, "sha512-a4VUQZH7LxGbUJ3qJ/TzQG8HxdHvf+jOnqf7B7oFx1TEBm+j2KNL2zr5SQ7wHkNAcaPevF6gf9tQnVBnC4mD+A=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], @@ -326,6 +365,8 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -350,6 +391,8 @@ "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="], + "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -372,32 +415,30 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-kit": ["ast-kit@3.0.0-beta.1", "", { "dependencies": { "@babel/parser": "^8.0.0-beta.4", "estree-walker": "^3.0.3", "pathe": "^2.0.3" } }, "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], @@ -408,6 +449,8 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], + "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -418,8 +461,6 @@ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], @@ -432,9 +473,9 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -444,12 +485,6 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], - - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -460,14 +495,6 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], - - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -478,8 +505,6 @@ "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -492,6 +517,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -502,6 +529,8 @@ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "electron": ["electron@33.4.11", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg=="], @@ -510,7 +539,7 @@ "electronmon": ["electronmon@2.0.4", "", { "dependencies": { "chalk": "^3.0.0", "import-from": "^3.0.0", "runtime-required": "^1.1.0", "watchboy": "^0.4.3" }, "bin": { "electronmon": "bin/cli.js" } }, "sha512-u6eDrvUbqa+wsnMrhG2vHmo5neL1owLg2e5i1avGWcOb4rHsUf9lSfbs0FvfPsBNpLxxlPO98nrMhAGV+zw/fQ=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -552,8 +581,6 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], @@ -566,8 +593,6 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -606,6 +631,8 @@ "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], @@ -614,6 +641,8 @@ "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="], + "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], @@ -624,8 +653,6 @@ "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], @@ -640,9 +667,7 @@ "joi": ["joi@18.0.2", "", { "dependencies": { "@hapi/address": "^5.1.1", "@hapi/formula": "^3.0.2", "@hapi/hoek": "^11.0.7", "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", "@standard-schema/spec": "^1.0.0" } }, "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA=="], - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -656,35 +681,29 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], - "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], @@ -692,8 +711,6 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], @@ -806,12 +823,8 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -820,10 +833,10 @@ "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -838,8 +851,6 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -848,14 +859,8 @@ "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], @@ -864,6 +869,8 @@ "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], @@ -872,9 +879,7 @@ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], - - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], @@ -888,8 +893,6 @@ "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -900,6 +903,10 @@ "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "rolldown": ["rolldown@1.0.0-rc.3", "", { "dependencies": { "@oxc-project/types": "=0.112.0", "@rolldown/pluginutils": "1.0.0-rc.3" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-x64": "1.0.0-rc.3", "@rolldown/binding-freebsd-x64": "1.0.0-rc.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.3", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.3", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.3", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.3", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.3", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.3" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Po/YZECDOqVXjIXrtC5h++a5NLvKAQNrd9ggrIG3sbDfGO5BqTUsrI6l8zdniKRp3r5Tp/2JTrXqx4GIguFCMw=="], + + "rolldown-plugin-dts": ["rolldown-plugin-dts@0.22.1", "", { "dependencies": { "@babel/generator": "8.0.0-rc.1", "@babel/helper-validator-identifier": "8.0.0-rc.1", "@babel/parser": "8.0.0-rc.1", "@babel/types": "8.0.0-rc.1", "ast-kit": "^3.0.0-beta.1", "birpc": "^4.0.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.1", "obug": "^2.1.1" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-rc.3", "typescript": "^5.0.0", "vue-tsc": "~3.2.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-5E0AiM5RSQhU6cjtkDFWH6laW4IrMu0j1Mo8x04Xo1ALHmaRMs9/7zej7P3RrryVHW/DdZAp85MA7Be55p0iUw=="], + "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], @@ -910,18 +917,14 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], @@ -932,23 +935,15 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "sucrase": ["sucrase@3.35.1", "", { "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" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], - "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "t3": ["t3@workspace:apps/server"], @@ -956,21 +951,15 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -980,12 +969,10 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "tsdown": ["tsdown@0.20.3", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "defu": "^6.1.4", "empathic": "^2.0.0", "hookable": "^6.0.1", "import-without-cache": "^0.2.5", "obug": "^2.1.1", "picomatch": "^4.0.3", "rolldown": "1.0.0-rc.3", "rolldown-plugin-dts": "^0.22.1", "semver": "^7.7.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig-core": "^7.4.2", "unrun": "^0.2.27" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@vitejs/devtools": "*", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "@vitejs/devtools", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-qWOUXSbe4jN8JZEgrkc/uhJpC8VN2QpNu3eZkBWwNuTEjc/Ik1kcc54ycfcQ5QPRHeu9OQXaLfCI3o7pEJgB2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsup": ["tsup@8.5.1", "", { "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" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="], @@ -1006,7 +993,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "unconfig-core": ["unconfig-core@7.4.2", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1028,17 +1015,17 @@ "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="], + "unrun": ["unrun@0.2.27", "", { "dependencies": { "rolldown": "1.0.0-rc.3" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-Mmur1UJpIbfxasLOhPRvox/QS4xBiDii71hMP7smfRthGcwFL2OAmYRgduLANOAU4LUkvVamuP+02U+c90jlrw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vite": ["vite@8.0.0-beta.13", "", { "dependencies": { "@oxc-project/runtime": "0.112.0", "fdir": "^6.5.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.3", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.24", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7s/rfpYOAo7WUHh9irzaGjhhKb12hGv0BpDegAMV5A391wdyvM45WtX6VMV7hvEtZF2j/QtpDpR6ldXI3GgARQ=="], - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], "wait-on": ["wait-on@8.0.5", "", { "dependencies": { "axios": "^1.12.1", "joi": "^18.0.1", "lodash": "^4.17.21", "minimist": "^1.2.8", "rxjs": "^7.8.2" }, "bin": { "wait-on": "bin/wait-on" } }, "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag=="], @@ -1046,32 +1033,50 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/core/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/generator/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="], + + "@babel/template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -1084,15 +1089,17 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@tailwindcss/vite/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "@types/babel__core/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + "@types/babel__template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "electronmon/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "@vitejs/plugin-react/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], - "global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + + "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -1100,62 +1107,198 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="], + + "vitest/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + + "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], + + "@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], + + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "@tailwindcss/vite/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "@vitejs/plugin-react/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="], + + "vitest/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@tailwindcss/vite/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@vitejs/plugin-react/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + "@vitejs/plugin-react/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "electronmon/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@vitejs/plugin-react/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "vitest/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], } } diff --git a/package.json b/package.json index 27a799018f..c592c96211 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo run dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3 --parallel", "dev:server": "bun run build:contracts && bun run --cwd apps/server dev", "dev:web": "VITE_WS_URL=ws://localhost:3773 bun run --cwd apps/web dev", - "dev:desktop": "bun run --cwd apps/desktop dev", + "dev:desktop": "turbo watch dev --filter=@t3tools/desktop --filter=t3 --filter=@t3tools/web", "start": "node apps/server/dist/index.js", "start:desktop": "bun run --cwd apps/desktop start", "build": "turbo run build", @@ -26,7 +26,7 @@ "oxlint": "^1.43.0", "turbo": "^2.3.3", "typescript": "^5.7.3", - "vitest": "^3.0.0" + "vitest": "^4.0.0" }, "engines": { "bun": ">=1.3.9" diff --git a/packages/contracts/package.json b/packages/contracts/package.json index e78207dca9..d4216fd398 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -7,18 +7,23 @@ ], "type": "module", "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./src/index.ts", + "module": "./dist/index.mjs", + "types": "./src/index.d.cts", "exports": { ".": { - "types": "./src/index.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" + "import": { + "types": "./src/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./src/index.d.cts", + "default": "./dist/index.cjs" + } } }, "scripts": { - "dev": "tsup src/index.ts --format esm,cjs --dts --watch --clean", - "build": "tsup src/index.ts --format esm,cjs --dts --clean", + "dev": "tsdown src/index.ts --format esm,cjs --dts", + "build": "tsdown src/index.ts --format esm,cjs --dts --clean", "typecheck": "tsc --noEmit", "test": "vitest run" }, @@ -26,7 +31,7 @@ "zod": "^3.24.1" }, "devDependencies": { - "tsup": "^8.3.5", + "tsdown": "^0.20.3", "typescript": "^5.7.3" } } From 9fc5ad20830a148ae36abf2381efef8283dc3699 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Feb 2026 00:30:21 -0800 Subject: [PATCH 16/31] plans --- ...ch-environment-picker-in-chatview-input.md | 74 +++++++++++ .plans/git-flows-integration-tests.md | 99 +++++++++++++++ .plans/git-flows-test-plan.md | 103 ++++++++++++++++ ...git-integration-branch-picker-worktrees.md | 115 ++++++++++++++++++ 4 files changed, 391 insertions(+) create mode 100644 .plans/branch-environment-picker-in-chatview-input.md create mode 100644 .plans/git-flows-integration-tests.md create mode 100644 .plans/git-flows-test-plan.md create mode 100644 .plans/git-integration-branch-picker-worktrees.md diff --git a/.plans/branch-environment-picker-in-chatview-input.md b/.plans/branch-environment-picker-in-chatview-input.md new file mode 100644 index 0000000000..2c1994d2c8 --- /dev/null +++ b/.plans/branch-environment-picker-in-chatview-input.md @@ -0,0 +1,74 @@ +# Branch/Environment Picker in ChatView Input + +## Summary + +Add a secondary toolbar below the ChatView input area (similar to Codex UI) that lets users select the target branch and environment mode (Local vs New worktree) before sending their first message. + +## UX + +- A toolbar appears **below** the input form (always visible when it's a git repo) +- Two controls: + 1. **Environment mode** (left side): toggles between "Local" and "New worktree" — **locked after first message** (no longer clickable, just shows current mode as label) + 2. **Branch picker** (right side): dropdown showing local branches — **always changeable**, even after messages are sent +- If not a git repo, the toolbar is hidden entirely (thread uses project cwd as-is) + +## Changes + +### 0. Install `@tanstack/react-query` in `apps/renderer` + +Add dependency + wrap app in `QueryClientProvider`. + +### 1. `apps/renderer/src/store.ts` — MODIFY + +Add a new action to the reducer: + +```ts +| { type: "SET_THREAD_BRANCH"; threadId: string; branch: string | null; worktreePath: string | null } +``` + +Reducer case updates `branch` and `worktreePath` on the thread. + +### 2. `apps/renderer/src/components/ChatView.tsx` — MODIFY + +**Fetch branches** via `useQuery`: + +```ts +const branchQuery = useQuery({ + queryKey: ["git-branches", activeProject?.cwd], + queryFn: () => api.git.listBranches({ cwd: activeProject!.cwd }), + enabled: !!activeProject, +}); +``` + +**Local state:** + +- `envMode: "local" | "worktree"` — environment mode (local component state) + +**UI:** Below the `
`, render a toolbar bar (hidden if `!branchQuery.data?.isRepo`): + +- Left side: env mode button ("Local" / "New worktree") — disabled after first message (locked in) +- Right side: branch dropdown from `branchQuery.data.branches` +- Both styled like existing model picker (small text, chevron, dropdown menus) + +**Behavior:** + +- Branch picker is always active — changing branch dispatches `SET_THREAD_BRANCH` immediately +- Env mode is only clickable when `activeThread.messages.length === 0`. After first message, it becomes a static label showing the locked-in mode +- On first send (`onSend`): if `envMode === "worktree"` and a branch is selected, call `api.git.createWorktree` before starting the session, then dispatch `SET_THREAD_BRANCH` with the worktreePath +- `ensureSession` already uses `activeThread.worktreePath ?? activeProject.cwd` + +### Files to modify + +1. `apps/renderer/package.json` — add `@tanstack/react-query` +2. `apps/renderer/src/main.tsx` (or App entry) — wrap in `QueryClientProvider` +3. `apps/renderer/src/store.ts` — add `SET_THREAD_BRANCH` action +4. `apps/renderer/src/components/ChatView.tsx` — branch/env picker UI with `useQuery` + +## Verification + +1. `turbo build` — compiles +2. Create a new thread → branch bar appears below input with "Local" + current branch +3. Change branch in dropdown → branch updates on thread +4. Toggle "New worktree" → send message → worktree created, session uses worktree cwd +5. After first message: env mode label locks to "Worktree" (not clickable), branch picker still works +6. Non-git project → no branch bar shown diff --git a/.plans/git-flows-integration-tests.md b/.plans/git-flows-integration-tests.md new file mode 100644 index 0000000000..70e233a008 --- /dev/null +++ b/.plans/git-flows-integration-tests.md @@ -0,0 +1,99 @@ +# Git Flows Integration Tests + +## Overview + +Real integration tests that run actual git commands against temporary repos. No mocking. + +## Step 1: Extract git functions into `apps/desktop/src/git.ts` + +The git functions (`listGitBranches`, `createGitWorktree`, `removeGitWorktree`, `createGitBranch`, `checkoutGitBranch`, `initGitRepo`) and their helper `runTerminalCommand` are currently private in `main.ts`. Extract them into a new `apps/desktop/src/git.ts` module with named exports. + +`main.ts` will import and re-use them — no behavior change, just moving code. + +**Files modified:** + +- `apps/desktop/src/git.ts` — new file with all git functions exported +- `apps/desktop/src/main.ts` — import from `./git` instead of defining inline + +## Step 2: Create `apps/desktop/src/git.test.ts` + +Integration tests using real temp git repos. Each test group creates a fresh temp directory with `git init`, makes commits, creates branches as needed, and cleans up after. + +### Setup/teardown pattern + +```ts +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + listGitBranches, + createGitBranch, + checkoutGitBranch, + createGitWorktree, + removeGitWorktree, + initGitRepo, +} from "./git"; + +// Helper: run a raw git command in a dir (for test setup, not under test) +// Helper: create an initial commit (git needs at least one commit for branches) +``` + +### Test groups + +**1. initGitRepo** + +- Creates a valid git repo in a temp dir +- listGitBranches reports `isRepo: true` after init + +**2. listGitBranches** + +- Returns `isRepo: false` for non-git directory +- Returns the current branch with `current: true` +- Sorts current branch first +- Lists multiple branches after creating them +- `isDefault` is false when no remote (no origin/HEAD) + +**3. checkoutGitBranch** + +- Checks out an existing branch (current flag moves) +- Throws when branch doesn't exist +- Throws when checkout would overwrite uncommitted changes (dirty working tree) + +**4. createGitBranch** + +- Creates a new branch (appears in listGitBranches) +- Throws when branch already exists + +**5. createGitWorktree + removeGitWorktree** + +- Creates a worktree directory at the expected path +- Worktree has the correct branch checked out +- Throws when branch is already checked out in another worktree +- removeGitWorktree cleans up the worktree + +**6. Full flow: local branch checkout** + +- init → commit → create branch → checkout → verify current + +**7. Full flow: worktree creation from selected branch** + +- init → commit → create branch → create worktree → verify worktree dir exists and has correct branch + +**8. Full flow: thread switching simulation** + +- init → commit → create branch-a, branch-b → checkout a → checkout b → checkout a → verify current matches + +**9. Full flow: checkout conflict** + +- init → commit → create branch → modify file (unstaged) → checkout other branch → expect error + +## Verification + +```bash +# Run the git integration tests +cd apps/desktop && bun run test + +# Or just the git test file +npx vitest run apps/desktop/src/git.test.ts +``` diff --git a/.plans/git-flows-test-plan.md b/.plans/git-flows-test-plan.md new file mode 100644 index 0000000000..45b86b622b --- /dev/null +++ b/.plans/git-flows-test-plan.md @@ -0,0 +1,103 @@ +# Git Flows Test Plan + +## Overview + +Add tests for git branch/worktree flows. Two files: + +1. **Extend** `apps/renderer/src/store.test.ts` — reducer tests for `SET_THREAD_BRANCH` +2. **Create** `apps/renderer/src/git-flows.test.ts` — flow logic tests + +All tests are pure Vitest unit tests (no React rendering). They test the reducer directly and simulate handler logic via sequential reducer dispatches + mocked API calls. + +## File 1: `apps/renderer/src/store.test.ts` (extend) + +Add `describe("SET_THREAD_BRANCH reducer")` with 6 tests: + +- Sets branch + worktreePath atomically +- Clears both to null +- Updates branch while preserving worktreePath +- Does not affect other threads (multi-thread state) +- No-op for nonexistent thread id +- Does not mutate messages, error, or session fields + +Uses existing `makeThread`, `makeState` factories. + +## File 2: `apps/renderer/src/git-flows.test.ts` (new) + +### Factories + +- `makeThread()`, `makeState()`, `makeSession()` — same pattern as store.test.ts +- `makeBranch()` — creates `GitBranch` objects +- `makeMessage()` — creates `ChatMessage` objects +- `makeGitApi()` — returns `{ checkout, createWorktree, createBranch, listBranches }` with `vi.fn()` mocks + +### Test groups (~30 tests total) + +**1. Local branch checkout flow** (2 tests) + +- Successful checkout → SET_THREAD_BRANCH updates branch +- Checkout failure → SET_ERROR, branch unchanged + +**2. Thread branch conflict on send** (3 tests) + +- Two threads maintain independent branch state after SET_ACTIVE_THREAD +- Branch state preserved through multiple thread switches + updates +- Checkout failure on thread switch sets error only on target thread + +**3. Worktree creation on send** (5 tests) + +- First message in worktree mode → createWorktree → SET_THREAD_BRANCH with worktreePath +- No worktree when messages already exist +- No worktree in local envMode +- No worktree when worktreePath already set +- createWorktree failure → SET_ERROR, send aborted, no messages pushed + +**4. Env mode locking** (4 tests) + +- envLocked=false when no messages +- envLocked=true with messages +- Transitions false→true after PUSH_USER_MESSAGE +- Remains true after SET_ERROR and UPDATE_SESSION + +**5. Auto-fill current branch** (3 tests) + +- Dispatches SET_THREAD_BRANCH when thread has no branch and current branch exists +- Does not overwrite existing branch +- No-op when no branch is marked current + +**6. Default branch detection** (2 tests) + +- isDefault flag on branch objects +- current and isDefault can be on different branches + +**7. Branch creation + checkout** (3 tests) + +- Successful create + checkout updates branch +- createBranch failure → error, branch unchanged +- checkout failure after successful create → error, branch unchanged + +**8. Session CWD resolution** (3 tests) + +- Uses worktreePath when available +- cwdOverride takes precedence over worktreePath +- Falls back to project cwd when no worktree + +**9. Error handling patterns** (4 tests) + +- SET_ERROR sets error on correct thread +- SET_ERROR with null clears error +- Error on one thread doesn't affect others +- Error cleared before successful branch operations + +## Verification + +```bash +# Run all renderer tests +cd apps/renderer && bun run test + +# Run just the new test file +npx vitest run apps/renderer/src/git-flows.test.ts + +# Run just the store tests +npx vitest run apps/renderer/src/store.test.ts +``` diff --git a/.plans/git-integration-branch-picker-worktrees.md b/.plans/git-integration-branch-picker-worktrees.md new file mode 100644 index 0000000000..b5b5e82e32 --- /dev/null +++ b/.plans/git-integration-branch-picker-worktrees.md @@ -0,0 +1,115 @@ +# Git Integration: Branch Picker + Worktrees + +## Summary + +Add git integration to let users start new threads from a specific branch, optionally creating a git worktree for isolated agent work. + +## UX Flow + +- **Left click** "+ New thread" → immediately creates a thread (current behavior, unchanged) +- **Right click** "+ New thread" → opens a context menu with git options: + - List of local branches → clicking one creates a thread on that branch (uses project cwd) + - Each branch has a "worktree" sub-option → creates a worktree, then creates thread with worktree as cwd +- When thread has a worktree, the agent session uses the worktree path as its cwd +- If git fails (not a repo), context menu shows "Not a git repository" disabled item + +## Changes + +### 1. `packages/contracts/src/git.ts` — CREATE + +New Zod schemas and types: + +- `gitListBranchesInputSchema` — `{ cwd: string }` +- `gitCreateWorktreeInputSchema` — `{ cwd: string, branch: string, path?: string }` +- `gitRemoveWorktreeInputSchema` — `{ cwd: string, path: string }` +- `gitBranchSchema` — `{ name: string, current: boolean }` +- Result types for each + +### 2. `packages/contracts/src/ipc.ts` — MODIFY + +- Add 3 IPC channels: `git:list-branches`, `git:create-worktree`, `git:remove-worktree` +- Add `git` namespace to `NativeApi` with `listBranches`, `createWorktree`, `removeWorktree` + +### 3. `packages/contracts/src/index.ts` — MODIFY + +- Add `export * from "./git"` + +### 4. `apps/desktop/src/main.ts` — MODIFY + +Add 3 IPC handlers + helper functions: + +- `listGitBranches()` — runs `git branch --no-color`, parses output into `{ name, current }[]` +- `createGitWorktree()` — runs `git worktree add `, defaults path to `../{repo}-worktrees/{branch}` +- `removeGitWorktree()` — runs `git worktree remove ` + +Reuses existing `runTerminalCommand()`. + +### 5. `apps/desktop/src/preload.ts` — MODIFY + +Add `git` namespace with 3 `ipcRenderer.invoke` calls. + +### 6. `apps/renderer/src/types.ts` — MODIFY + +Add to `Thread`: + +``` +branch: string | null +worktreePath: string | null +``` + +### 7. `apps/renderer/src/persistenceSchema.ts` — MODIFY + +- Add optional `branch`/`worktreePath` to persisted thread schema (`.nullable().optional()` for backwards compat) +- Add V3 schema, update union +- Update `hydrateThread` to default new fields to `null` +- Update `toPersistedState` to serialize new fields + +### 8. `apps/renderer/src/store.ts` — MODIFY + +- Update persisted state key to v3, keep v2 as legacy fallback + +### 9. `apps/renderer/src/components/Sidebar.tsx` — MODIFY (main UI work) + +- Keep existing left-click `handleNewThread` unchanged (immediate thread creation) +- Add `onContextMenu` handler to "+ New thread" buttons (both global and per-project) +- On right-click: fetch branches via `api.git.listBranches`, show a custom context menu +- Context menu items: branch names, each with a nested option to create with worktree +- Clicking a branch → creates thread with `branch` set, title = branch name +- Clicking "with worktree" → calls `api.git.createWorktree` first, then creates thread with `worktreePath` +- Show branch badge on thread list items +- If not a git repo, show "Not a git repository" as disabled menu item + +Context menu component: a positioned `
` with `position: fixed` anchored to the click position, dismissed on click-outside or Escape. Follows the existing dropdown pattern from ChatView's model picker. + +### 10. `apps/renderer/src/components/ChatView.tsx` — MODIFY + +- Line 157: use `activeThread.worktreePath ?? activeProject.cwd` as session cwd +- Show branch/worktree badge in header bar + +## Implementation Order + +1. `packages/contracts/src/git.ts` (new schemas) +2. `packages/contracts/src/ipc.ts` + `index.ts` (wire up channels) +3. `apps/desktop/src/main.ts` (git command handlers) +4. `apps/desktop/src/preload.ts` (bridge methods) +5. `apps/renderer/src/types.ts` (Thread type update) +6. `apps/renderer/src/persistenceSchema.ts` + `store.ts` (persistence migration) +7. `apps/renderer/src/components/Sidebar.tsx` (branch picker UI) +8. `apps/renderer/src/components/ChatView.tsx` (worktree cwd + badge) + +## Edge Cases + +- **Not a git repo**: `git branch` fails → context menu shows "Not a git repository" disabled item +- **Branch has slashes**: `feature/foo` → worktree dir becomes `feature-foo` +- **Worktree exists**: git error surfaces to user via inline error message in context menu +- **No persistence breakage**: `.nullable().optional()` fields parse fine with old data + +## Verification + +1. `turbo build` — confirm contracts/desktop/renderer all compile +2. Launch app, add a project pointing to a git repo +3. Click "+ New thread" → verify branch list loads +4. Select a branch, click Start → thread created with branch in title +5. Enable worktree checkbox, pick branch, Start → verify worktree directory created on disk +6. Send a message in worktree thread → verify agent runs in worktree cwd +7. Add a non-git project → verify graceful error, can still create thread From d6437a7e131ab98706efe31263cddb4ae957557a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 11:44:24 -0800 Subject: [PATCH 17/31] Move toolbar below chat input, fix electron preload bundling, and update build configs - Move BranchToolbar below the chat input box to match Codex app layout - Change worktree path to ~/.t3code/worktrees/{repo}/{branch} - Split desktop tsdown config into separate main/preload builds to prevent code-splitting from leaking node:os into the sandboxed preload script - Migrate turbo scripts to turbo watch, add desktop start/smoke-test tasks - Update server output to .mjs, add CJS format, remove tsx dependency Co-Authored-By: Claude Opus 4.6 --- README.md | 4 ++-- apps/desktop/package.json | 2 +- apps/desktop/scripts/dev-electron.mjs | 2 +- apps/desktop/scripts/smoke-test.mjs | 6 +----- apps/desktop/src/main.ts | 2 +- apps/desktop/tsdown.config.ts | 25 ++++++++++++++++++------- apps/desktop/turbo.jsonc | 10 ++++++++++ apps/server/package.json | 9 ++++----- apps/server/tsdown.config.ts | 4 ++-- apps/server/turbo.jsonc | 16 ++++++++++++++++ apps/web/turbo.jsonc | 9 +++++++++ bun.lock | 1 - package.json | 16 ++++++++-------- turbo.json | 4 ++-- vitest.config.ts | 12 +++++++++++- 15 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 apps/server/turbo.jsonc create mode 100644 apps/web/turbo.jsonc diff --git a/README.md b/README.md index 1f18c9d300..93f955361e 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ npx t3 ## Scripts -- `bun run dev` — Starts contracts, server, and web dev tasks via Turborepo's parallel task runner. -- `bun run dev:server` — Starts just the WebSocket server (uses tsx for TS execution). +- `bun run dev` — Starts contracts, server, and web in `turbo watch` mode. +- `bun run dev:server` — Starts just the WebSocket server (uses Bun TypeScript execution). - `bun run dev:web` — Starts just the Vite dev server for the web app. - `bun run start` — Runs the production server (serves built web app as static files). - `bun run build` — Builds contracts, web app, and server through Turbo. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3b4fd30f9c..d6afd21ce9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -8,7 +8,7 @@ "dev:bundle": "tsdown --watch", "dev:electron": "bun run scripts/dev-electron.mjs", "build": "tsdown", - "start": "electron dist-electron/main.cjs", + "start": "electron dist-electron/main.js", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests", "smoke-test": "node scripts/smoke-test.mjs" diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs index 0c49d1e586..2a6153a7da 100644 --- a/apps/desktop/scripts/dev-electron.mjs +++ b/apps/desktop/scripts/dev-electron.mjs @@ -10,7 +10,7 @@ await waitOn({ `tcp:${port}`, "file:dist-electron/main.js", "file:dist-electron/preload.js", - "file:../server/dist/index.js", + // "file:../server/dist/index.cjs", ], }); diff --git a/apps/desktop/scripts/smoke-test.mjs b/apps/desktop/scripts/smoke-test.mjs index 895d18b8b6..883da7203a 100644 --- a/apps/desktop/scripts/smoke-test.mjs +++ b/apps/desktop/scripts/smoke-test.mjs @@ -1,16 +1,12 @@ -import { execSync, spawn } from "node:child_process"; +import { spawn } from "node:child_process"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const desktopDir = resolve(__dirname, ".."); -const rootDir = resolve(__dirname, "../../.."); const electronBin = resolve(desktopDir, "node_modules/.bin/electron"); const mainJs = resolve(desktopDir, "dist-electron/main.js"); -console.log("Building contracts + web + server + desktop..."); -execSync("bun run build", { cwd: rootDir, stdio: "inherit" }); - console.log("\nLaunching Electron smoke test..."); const child = spawn(electronBin, [mainJs], { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 3161a2e9dc..66e4b8a330 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -13,7 +13,7 @@ fixPath(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; const ROOT_DIR = path.resolve(__dirname, "../../.."); -const BACKEND_ENTRY = path.join(ROOT_DIR, "apps/server/dist/index.js"); +const BACKEND_ENTRY = path.join(ROOT_DIR, "apps/server/dist/index.mjs"); const WEB_ENTRY = path.join(ROOT_DIR, "apps/web/dist/index.html"); const STATE_DIR = path.join(os.homedir(), ".t3", "userdata"); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); diff --git a/apps/desktop/tsdown.config.ts b/apps/desktop/tsdown.config.ts index f87ac594df..4cffbb2587 100644 --- a/apps/desktop/tsdown.config.ts +++ b/apps/desktop/tsdown.config.ts @@ -1,10 +1,21 @@ -import { defineConfig } from "tsup"; +import { defineConfig } from "tsdown"; -export default defineConfig({ - entry: ["src/main.ts", "src/preload.ts"], - format: "cjs", +const shared = { + format: "cjs" as const, outDir: "dist-electron", sourcemap: true, - clean: true, - noExternal: ["@t3tools/contracts"], -}); + outExtensions: () => ({ js: ".js" }), +}; + +export default defineConfig([ + { + ...shared, + entry: ["src/main.ts"], + clean: true, + noExternal: ["@t3tools/contracts"], + }, + { + ...shared, + entry: ["src/preload.ts"], + }, +]); diff --git a/apps/desktop/turbo.jsonc b/apps/desktop/turbo.jsonc index 843ebb0b2d..0f8f5b4b8e 100644 --- a/apps/desktop/turbo.jsonc +++ b/apps/desktop/turbo.jsonc @@ -9,5 +9,15 @@ "dev": { "persistent": true, }, + "start": { + "dependsOn": ["build", "@t3tools/web#build", "t3#build"], + "cache": false, + "persistent": true, + }, + "smoke-test": { + "dependsOn": ["build", "@t3tools/web#build", "t3#build"], + "cache": false, + "outputs": [], + }, }, } diff --git a/apps/server/package.json b/apps/server/package.json index f26426cff2..7ed3d16144 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -2,17 +2,17 @@ "name": "t3", "version": "0.1.0", "bin": { - "t3": "./dist/index.js" + "t3": "./dist/index.mjs" }, "files": [ "dist" ], "type": "module", - "main": "./dist/index.js", + "main": "./dist/index.mjs", "scripts": { - "dev": "VITE_DEV_SERVER_URL=http://localhost:5173 tsx src/index.ts", + "dev": "VITE_DEV_SERVER_URL=http://localhost:5173 bun run src/index.ts", "build": "tsdown && node scripts/bundle-client.mjs", - "start": "node dist/index.js", + "start": "node dist/index.mjs", "typecheck": "tsc --noEmit", "test": "vitest run" }, @@ -26,7 +26,6 @@ "@types/node": "^22.10.2", "@types/ws": "^8.5.13", "tsdown": "^0.20.3", - "tsx": "^4.19.0", "typescript": "^5.7.3" } } diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts index 6708d8e27c..e5cd33b059 100644 --- a/apps/server/tsdown.config.ts +++ b/apps/server/tsdown.config.ts @@ -2,12 +2,12 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], - format: "esm", + format: ["esm", "cjs"], outDir: "dist", sourcemap: true, clean: true, noExternal: ["@t3tools/contracts"], banner: { - js: '#!/usr/bin/env node\n', + js: "#!/usr/bin/env node\n", }, }); diff --git a/apps/server/turbo.jsonc b/apps/server/turbo.jsonc new file mode 100644 index 0000000000..348412533d --- /dev/null +++ b/apps/server/turbo.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "dev": { + "dependsOn": ["@t3tools/contracts#dev"], + "persistent": true, + "interruptible": true + }, + "start": { + "dependsOn": ["build"], + "cache": false, + "persistent": true + } + } +} diff --git a/apps/web/turbo.jsonc b/apps/web/turbo.jsonc new file mode 100644 index 0000000000..619cc63b36 --- /dev/null +++ b/apps/web/turbo.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "dev": { + "persistent": true + } + } +} diff --git a/bun.lock b/bun.lock index 5785a3f147..49f27c4212 100644 --- a/bun.lock +++ b/bun.lock @@ -43,7 +43,6 @@ "@types/node": "^22.10.2", "@types/ws": "^8.5.13", "tsdown": "^0.20.3", - "tsx": "^4.19.0", "typescript": "^5.7.3", }, }, diff --git a/package.json b/package.json index c592c96211..e41205a186 100644 --- a/package.json +++ b/package.json @@ -6,20 +6,20 @@ "packages/*" ], "scripts": { - "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo run dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3 --parallel", - "dev:server": "bun run build:contracts && bun run --cwd apps/server dev", - "dev:web": "VITE_WS_URL=ws://localhost:3773 bun run --cwd apps/web dev", + "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo watch dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3", + "dev:server": "turbo watch dev --filter=t3", + "dev:web": "VITE_WS_URL=ws://localhost:3773 turbo watch dev --filter=@t3tools/web", "dev:desktop": "turbo watch dev --filter=@t3tools/desktop --filter=t3 --filter=@t3tools/web", - "start": "node apps/server/dist/index.js", - "start:desktop": "bun run --cwd apps/desktop start", + "start": "turbo run start --filter=t3", + "start:desktop": "turbo run start --filter=@t3tools/desktop", "build": "turbo run build", - "build:desktop": "bun run build:contracts && bun run --cwd apps/web build && bun run --cwd apps/server build && bun run --cwd apps/desktop build", + "build:desktop": "turbo run build --filter=@t3tools/desktop --filter=t3", "typecheck": "turbo run typecheck", "lint": "oxlint --report-unused-disable-directives", "test": "turbo run test", - "test:desktop-smoke": "bun run --cwd apps/desktop smoke-test", + "test:desktop-smoke": "turbo run smoke-test --filter=@t3tools/desktop", "fmt": "oxfmt", - "build:contracts": "bun run --cwd packages/contracts build" + "build:contracts": "turbo run build --filter=@t3tools/contracts" }, "devDependencies": { "oxfmt": "^0.28.0", diff --git a/turbo.json b/turbo.json index 6d0970bf95..d797982de9 100644 --- a/turbo.json +++ b/turbo.json @@ -18,9 +18,9 @@ "outputs": ["dist/**", "dist-electron/**"] }, "dev": { - "dependsOn": ["@t3tools/contracts#build"], + "dependsOn": ["^dev"], "cache": false, - "persistent": true + "persistent": false }, "typecheck": { "dependsOn": ["^typecheck"], diff --git a/vitest.config.ts b/vitest.config.ts index 8fb6f2dcff..b8c4e89a2b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,13 @@ +import * as path from "node:path"; import { defineConfig } from "vitest/config"; -export default defineConfig({}); +export default defineConfig({ + resolve: { + alias: [ + { + find: /^@t3tools\/contracts$/, + replacement: path.resolve(import.meta.dirname, "./packages/contracts/src/index.ts"), + }, + ], + }, +}); From 74f6ce86004a39ffa07d59d468c78ae677e93c3d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 12:06:58 -0800 Subject: [PATCH 18/31] Fix CI typecheck: point contracts types to source and remove dead code - Point @t3tools/contracts type exports to ./src/index.ts instead of generated .d.cts/.d.mts files that only exist after build - Remove unused useGitState.ts that imported missing @tanstack/react-query Co-Authored-By: Claude Opus 4.6 --- apps/web/src/hooks/useGitState.ts | 12 ------------ packages/contracts/package.json | 6 +++--- 2 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 apps/web/src/hooks/useGitState.ts diff --git a/apps/web/src/hooks/useGitState.ts b/apps/web/src/hooks/useGitState.ts deleted file mode 100644 index fc4ac6b453..0000000000 --- a/apps/web/src/hooks/useGitState.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { queryOptions, skipToken } from "@tanstack/react-query"; -import { readNativeApi } from "../session-logic"; - -export const listBranchesQuery = (cwd: string | undefined) => { - const api = readNativeApi(); - - return queryOptions({ - queryKey: ["git-branches", cwd], - queryFn: api && cwd ? () => api.git.listBranches({ cwd }) : skipToken, - refetchOnWindowFocus: true, - }); -}; diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d4216fd398..9184477776 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -8,15 +8,15 @@ "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.mjs", - "types": "./src/index.d.cts", + "types": "./src/index.ts", "exports": { ".": { "import": { - "types": "./src/index.d.mts", + "types": "./src/index.ts", "default": "./dist/index.mjs" }, "require": { - "types": "./src/index.d.cts", + "types": "./src/index.ts", "default": "./dist/index.cjs" } } From 4ab095fd9167c2201db2f2833d93d3db044391df Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 12:08:24 -0800 Subject: [PATCH 19/31] Fix CI typecheck: build contracts before typechecking dependents - Change turbo typecheck task to dependsOn: ["^build"] so the contracts package generates .d.cts/.d.mts declaration files before downstream packages run tsc - Revert contracts types back to generated declaration files - Remove unused useGitState.ts that imported missing @tanstack/react-query Co-Authored-By: Claude Opus 4.6 --- packages/contracts/package.json | 6 +++--- turbo.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 9184477776..d4216fd398 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -8,15 +8,15 @@ "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.mjs", - "types": "./src/index.ts", + "types": "./src/index.d.cts", "exports": { ".": { "import": { - "types": "./src/index.ts", + "types": "./src/index.d.mts", "default": "./dist/index.mjs" }, "require": { - "types": "./src/index.ts", + "types": "./src/index.d.cts", "default": "./dist/index.cjs" } } diff --git a/turbo.json b/turbo.json index d797982de9..fd290124b8 100644 --- a/turbo.json +++ b/turbo.json @@ -23,7 +23,7 @@ "persistent": false }, "typecheck": { - "dependsOn": ["^typecheck"], + "dependsOn": ["^build"], "outputs": [] }, "test": { From e2052b8ae24dd20bee0b9ac1625b253c9566f238 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 12:15:59 -0800 Subject: [PATCH 20/31] Fix dev:desktop task graph to match main's env var patterns - Remove --filter=t3 from dev:desktop since electron spawns its own server from dist/index.mjs (running server via turbo would conflict) - Add t3#build and contracts#build as dependencies of desktop dev task so the server is built before electron starts Co-Authored-By: Claude Opus 4.6 --- apps/desktop/turbo.jsonc | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/turbo.jsonc b/apps/desktop/turbo.jsonc index 0f8f5b4b8e..c99c47cbfc 100644 --- a/apps/desktop/turbo.jsonc +++ b/apps/desktop/turbo.jsonc @@ -7,6 +7,7 @@ "outputs": ["dist-electron/**"], }, "dev": { + "dependsOn": ["@t3tools/contracts#build", "t3#build"], "persistent": true, }, "start": { diff --git a/package.json b/package.json index e41205a186..2d5c5da546 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo watch dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3", "dev:server": "turbo watch dev --filter=t3", "dev:web": "VITE_WS_URL=ws://localhost:3773 turbo watch dev --filter=@t3tools/web", - "dev:desktop": "turbo watch dev --filter=@t3tools/desktop --filter=t3 --filter=@t3tools/web", + "dev:desktop": "turbo watch dev --filter=@t3tools/desktop --filter=@t3tools/web", "start": "turbo run start --filter=t3", "start:desktop": "turbo run start --filter=@t3tools/desktop", "build": "turbo run build", From ceceba279fe9412f19121ea939d74572249d5020 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 12:28:51 -0800 Subject: [PATCH 21/31] fix --- apps/server/tsdown.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts index e5cd33b059..b7506f6a0b 100644 --- a/apps/server/tsdown.config.ts +++ b/apps/server/tsdown.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from "tsup"; +import { defineConfig } from "tsdown"; export default defineConfig({ entry: ["src/index.ts"], From ca356d0edb7860ef0db4bc531fa37a90a5e9e7f6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 12:33:27 -0800 Subject: [PATCH 22/31] Fix server build: add zod to noExternal to prevent tsdown error tsdown v0.20.3 errors when it detects undeclared dependencies in the bundle. Since @t3tools/contracts (already in noExternal) transitively pulls in zod, explicitly declare it to avoid the build failure. Co-Authored-By: Claude Opus 4.6 --- apps/server/tsdown.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts index b7506f6a0b..ffd0a1ed62 100644 --- a/apps/server/tsdown.config.ts +++ b/apps/server/tsdown.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ outDir: "dist", sourcemap: true, clean: true, - noExternal: ["@t3tools/contracts"], + noExternal: ["@t3tools/contracts", "zod"], banner: { js: "#!/usr/bin/env node\n", }, From 01c3e8266b1cf6b008972c86c8a0fb4b35220935 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 12:36:52 -0800 Subject: [PATCH 23/31] Suppress tsdown inlineOnly warning for server build tsdown escalates warnings to errors when CI=true. Adding inlineOnly: false suppresses the bundled-dependency warning since we intentionally inline contracts and zod. Co-Authored-By: Claude Opus 4.6 --- apps/server/tsdown.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts index ffd0a1ed62..1a67bab626 100644 --- a/apps/server/tsdown.config.ts +++ b/apps/server/tsdown.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ sourcemap: true, clean: true, noExternal: ["@t3tools/contracts", "zod"], + inlineOnly: false, banner: { js: "#!/usr/bin/env node\n", }, From 7e5cd01d26b21093947cb9b988f5963b8ed6ce26 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 12:54:19 -0800 Subject: [PATCH 24/31] Polish turbo task graph and dev scripts - Switch from turbo watch to turbo run --parallel for dev scripts - Simplify contracts exports to use ./src/index.ts for types directly - Add --watch --clean to contracts dev script - Root dev task depends on contracts#build, is persistent - Revert typecheck to depend on ^typecheck (contracts types from source) - Remove redundant server dev task override - Desktop dev depends only on t3#build (contracts implied) - Uncomment server dist wait in dev-electron.mjs Co-Authored-By: Claude Opus 4.6 --- apps/desktop/scripts/dev-electron.mjs | 2 +- apps/desktop/turbo.jsonc | 16 ++++++++-------- apps/server/turbo.jsonc | 5 ----- package.json | 8 ++++---- packages/contracts/package.json | 15 +++++---------- turbo.json | 6 +++--- 6 files changed, 21 insertions(+), 31 deletions(-) diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs index 2a6153a7da..dacbb99c66 100644 --- a/apps/desktop/scripts/dev-electron.mjs +++ b/apps/desktop/scripts/dev-electron.mjs @@ -10,7 +10,7 @@ await waitOn({ `tcp:${port}`, "file:dist-electron/main.js", "file:dist-electron/preload.js", - // "file:../server/dist/index.cjs", + "file:../server/dist/index.mjs", ], }); diff --git a/apps/desktop/turbo.jsonc b/apps/desktop/turbo.jsonc index c99c47cbfc..8994c78062 100644 --- a/apps/desktop/turbo.jsonc +++ b/apps/desktop/turbo.jsonc @@ -1,24 +1,24 @@ { - "$schema": "https://turborepo.com/schema.json", + "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["dist-electron/**"], + "outputs": ["dist-electron/**"] }, "dev": { - "dependsOn": ["@t3tools/contracts#build", "t3#build"], - "persistent": true, + "dependsOn": ["t3#build"], + "persistent": true }, "start": { "dependsOn": ["build", "@t3tools/web#build", "t3#build"], "cache": false, - "persistent": true, + "persistent": true }, "smoke-test": { "dependsOn": ["build", "@t3tools/web#build", "t3#build"], "cache": false, - "outputs": [], - }, - }, + "outputs": [] + } + } } diff --git a/apps/server/turbo.jsonc b/apps/server/turbo.jsonc index 348412533d..c466a3a025 100644 --- a/apps/server/turbo.jsonc +++ b/apps/server/turbo.jsonc @@ -2,11 +2,6 @@ "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tasks": { - "dev": { - "dependsOn": ["@t3tools/contracts#dev"], - "persistent": true, - "interruptible": true - }, "start": { "dependsOn": ["build"], "cache": false, diff --git a/package.json b/package.json index 2d5c5da546..106d67ba23 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "packages/*" ], "scripts": { - "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo watch dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3", - "dev:server": "turbo watch dev --filter=t3", - "dev:web": "VITE_WS_URL=ws://localhost:3773 turbo watch dev --filter=@t3tools/web", - "dev:desktop": "turbo watch dev --filter=@t3tools/desktop --filter=@t3tools/web", + "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo run dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3 --parallel", + "dev:server": "turbo run dev --filter=t3", + "dev:web": "VITE_WS_URL=ws://localhost:3773 turbo run dev --filter=@t3tools/web", + "dev:desktop": "turbo run dev --filter=@t3tools/desktop --filter=@t3tools/web --parallel", "start": "turbo run start --filter=t3", "start:desktop": "turbo run start --filter=@t3tools/desktop", "build": "turbo run build", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d4216fd398..a2e4a1f71b 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -8,21 +8,16 @@ "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.mjs", - "types": "./src/index.d.cts", + "types": "./src/index.ts", "exports": { ".": { - "import": { - "types": "./src/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./src/index.d.cts", - "default": "./dist/index.cjs" - } + "types": "./src/index.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" } }, "scripts": { - "dev": "tsdown src/index.ts --format esm,cjs --dts", + "dev": "tsdown src/index.ts --format esm,cjs --dts --watch --clean", "build": "tsdown src/index.ts --format esm,cjs --dts --clean", "typecheck": "tsc --noEmit", "test": "vitest run" diff --git a/turbo.json b/turbo.json index fd290124b8..6d0970bf95 100644 --- a/turbo.json +++ b/turbo.json @@ -18,12 +18,12 @@ "outputs": ["dist/**", "dist-electron/**"] }, "dev": { - "dependsOn": ["^dev"], + "dependsOn": ["@t3tools/contracts#build"], "cache": false, - "persistent": false + "persistent": true }, "typecheck": { - "dependsOn": ["^build"], + "dependsOn": ["^typecheck"], "outputs": [] }, "test": { From 198cc58377fd87ff39b8a37464dd8cb1a77beb0b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 14:15:30 -0800 Subject: [PATCH 25/31] Fix envMode reset bug and use shell-free git spawn - ChatView: change useEffect dependency from [activeThread] (new object ref every dispatch) to [activeThread?.id, activeThread?.worktreePath] so envMode is no longer silently reset to "local" on unrelated store dispatches, making the worktree code path reachable. - git.ts: replace shell command strings (runTerminalCommand + single-quote escaping) with a runGit helper that spawns git directly via argv array. Eliminates Windows quoting issues and the command injection surface. Co-Authored-By: Claude Opus 4.6 --- apps/server/src/git.ts | 91 +++++++++++++++++----------- apps/web/src/components/ChatView.tsx | 2 +- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index bdb5d1b608..e6e06cb214 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -15,8 +15,46 @@ import type { TerminalCommandResult, } from "@t3tools/contracts"; -function escapeSingleQuotes(value: string): string { - return value.replace(/'/g, "'\\''"); +/** Spawn git directly with an argv array — no shell, no quoting needed. */ +function runGit( + args: string[], + cwd: string, + timeoutMs = 30_000, +): Promise { + return new Promise((resolve, reject) => { + const child = spawn("git", args, { + cwd, + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + + const timeout = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + setTimeout(() => { + if (!child.killed) child.kill("SIGKILL"); + }, 1_000).unref(); + }, timeoutMs); + + child.stdout?.on("data", (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); + child.on("error", (error) => { + clearTimeout(timeout); + reject(error); + }); + child.on("close", (code, signal) => { + clearTimeout(timeout); + resolve({ stdout, stderr, code: code ?? null, signal: signal ?? null, timedOut }); + }); + }); } export async function runTerminalCommand( @@ -78,11 +116,7 @@ export async function runTerminalCommand( } export async function listGitBranches(input: GitListBranchesInput): Promise { - const result = await runTerminalCommand({ - command: "git branch --no-color", - cwd: input.cwd, - timeoutMs: 10_000, - }); + const result = await runGit(["branch", "--no-color"], input.cwd, 10_000); if (result.code !== 0) { const stderr = result.stderr.trim(); @@ -93,11 +127,11 @@ export async function listGitBranches(input: GitListBranchesInput): Promise { - const result = await runTerminalCommand({ - command: `git worktree remove '${escapeSingleQuotes(input.path)}'`, - cwd: input.cwd, - timeoutMs: 15_000, - }); + const result = await runGit(["worktree", "remove", input.path], input.cwd, 15_000); if (result.code !== 0) { throw new Error(result.stderr.trim() || "git worktree remove failed"); @@ -159,11 +188,7 @@ export async function removeGitWorktree(input: GitRemoveWorktreeInput): Promise< } export async function createGitBranch(input: GitCreateBranchInput): Promise { - const result = await runTerminalCommand({ - command: `git branch '${escapeSingleQuotes(input.branch)}'`, - cwd: input.cwd, - timeoutMs: 10_000, - }); + const result = await runGit(["branch", input.branch], input.cwd, 10_000); if (result.code !== 0) { throw new Error(result.stderr.trim() || "git branch create failed"); @@ -171,11 +196,7 @@ export async function createGitBranch(input: GitCreateBranchInput): Promise { - const result = await runTerminalCommand({ - command: `git checkout '${escapeSingleQuotes(input.branch)}'`, - cwd: input.cwd, - timeoutMs: 10_000, - }); + const result = await runGit(["checkout", input.branch], input.cwd, 10_000); if (result.code !== 0) { throw new Error(result.stderr.trim() || "git checkout failed"); @@ -183,11 +204,7 @@ export async function checkoutGitBranch(input: GitCheckoutInput): Promise } export async function initGitRepo(input: GitInitInput): Promise { - const result = await runTerminalCommand({ - command: "git init", - cwd: input.cwd, - timeoutMs: 10_000, - }); + const result = await runGit(["init"], input.cwd, 10_000); if (result.code !== 0) { throw new Error(result.stderr.trim() || "git init failed"); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index edb24386c8..6a36885f19 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -319,7 +319,7 @@ export default function ChatView() { useEffect(() => { if (!activeThread) return; setEnvMode(activeThread.worktreePath ? "worktree" : "local"); - }, [activeThread]); + }, [activeThread?.id, activeThread?.worktreePath]); // Auto-resize textarea useEffect(() => { From 807e3d18dec858c9694a595deaea4f9ac588db5f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 14:26:24 -0800 Subject: [PATCH 26/31] Add react-query, refactor BranchToolbar async state, and fix stale deps - Add @tanstack/react-query and wire up QueryClientProvider in App.tsx - Refactor BranchToolbar to use useQuery for branch listing and useMutation for checkout, create branch, and git init, replacing manual useState/useCallback/useEffect async patterns - Fix branch sync effect and ChatView envMode effect to use stable primitive deps instead of full object references, preventing unnecessary re-runs on unrelated store dispatches Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 7 + apps/desktop/turbo.jsonc | 12 +- apps/server/src/codexAppServerManager.test.ts | 8 +- apps/server/src/codexAppServerManager.ts | 44 +--- apps/server/src/git.ts | 12 +- apps/server/src/providerManager.test.ts | 5 +- apps/server/src/providerManager.ts | 11 +- apps/server/src/wsServer.ts | 5 +- apps/server/turbo.jsonc | 6 +- apps/web/index.html | 5 +- apps/web/package.json | 1 + apps/web/src/App.tsx | 11 +- apps/web/src/components/BranchToolbar.tsx | 197 ++++++++---------- apps/web/src/components/ChatView.tsx | 188 +++++------------ apps/web/src/components/DiffPanel.tsx | 4 +- apps/web/src/historyBootstrap.ts | 15 +- apps/web/src/index.css | 37 +--- apps/web/src/persistenceSchema.ts | 15 +- apps/web/src/session-logic.ts | 25 +-- apps/web/src/store.ts | 4 +- apps/web/src/wsNativeApi.ts | 6 +- apps/web/turbo.jsonc | 6 +- apps/web/vite.config.ts | 4 +- bun.lock | 5 + packages/contracts/src/git.ts | 8 +- packages/contracts/src/provider.ts | 27 +-- 26 files changed, 235 insertions(+), 433 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7f5148fce0..affea58d01 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,11 +1,13 @@ # CLAUDE.md ## Project Snapshot + T3 Code is a minimal web GUI for using code agents like Codex and Claude Code (coming soon). This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged. ## Core Priorities + 1. Performance first. 2. Reliability first. 3. Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams). @@ -13,23 +15,28 @@ This repository is a VERY EARLY WIP. Proposing sweeping changes that improve lon If a tradeoff is required, choose correctness and robustness over short-term convenience. ## Package Roles + - `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions. - `apps/web`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket. - `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. ## Codex App Server (Important) + T3 Code is currently Codex-first. The server starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events to the browser through WebSocket push messages. How we use it in this codebase: + - Session startup/resume and turn lifecycle are brokered in `apps/server/src/codexAppServerManager.ts`. - Provider dispatch and thread event logging are coordinated in `apps/server/src/providerManager.ts`. - WebSocket server routes NativeApi methods in `apps/server/src/wsServer.ts`. - Web app consumes provider event streams via WebSocket push on channel `providers.event`. Docs: + - Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server ## Reference Repos + - Open-source Codex repo: https://github.com/openai/codex - Codex-Monitor (Tauri, feature-complete, strong reference implementation): https://github.com/Dimillian/CodexMonitor diff --git a/apps/desktop/turbo.jsonc b/apps/desktop/turbo.jsonc index 8994c78062..2ff803229e 100644 --- a/apps/desktop/turbo.jsonc +++ b/apps/desktop/turbo.jsonc @@ -4,21 +4,21 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["dist-electron/**"] + "outputs": ["dist-electron/**"], }, "dev": { "dependsOn": ["t3#build"], - "persistent": true + "persistent": true, }, "start": { "dependsOn": ["build", "@t3tools/web#build", "t3#build"], "cache": false, - "persistent": true + "persistent": true, }, "smoke-test": { "dependsOn": ["build", "@t3tools/web#build", "t3#build"], "cache": false, - "outputs": [] - } - } + "outputs": [], + }, + }, } diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index b611e4d4de..7c87e4442c 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -57,17 +57,13 @@ describe("normalizeCodexModelSlug", () => { describe("isRecoverableThreadResumeError", () => { it("matches not-found resume errors", () => { expect( - isRecoverableThreadResumeError( - new Error("thread/resume failed: thread not found"), - ), + isRecoverableThreadResumeError(new Error("thread/resume failed: thread not found")), ).toBe(true); }); it("ignores non-resume errors", () => { expect( - isRecoverableThreadResumeError( - new Error("thread/start failed: permission denied"), - ), + isRecoverableThreadResumeError(new Error("thread/start failed: permission denied")), ).toBe(false); }); diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index af1e67e36b..f4ed54ab15 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -26,9 +26,7 @@ interface PendingRequest { interface PendingApprovalRequest { requestId: string; jsonRpcId: string | number; - method: - | "item/commandExecution/requestApproval" - | "item/fileChange/requestApproval"; + method: "item/commandExecution/requestApproval" | "item/fileChange/requestApproval"; requestKind: ProviderRequestKind; threadId?: string; turnId?: string; @@ -122,16 +120,12 @@ export function classifyCodexStderrLine(rawLine: string): { message: string } | } export function isRecoverableThreadResumeError(error: unknown): boolean { - const message = ( - error instanceof Error ? error.message : String(error) - ).toLowerCase(); + const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); if (!message.includes("thread/resume")) { return false; } - return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => - message.includes(snippet), - ); + return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => message.includes(snippet)); } export interface CodexAppServerManagerEvents { @@ -209,14 +203,10 @@ export class CodexAppServerManager extends EventEmitter { +function runGit(args: string[], cwd: string, timeoutMs = 30_000): Promise { return new Promise((resolve, reject) => { const child = spawn("git", args, { cwd, @@ -127,11 +123,7 @@ export async function listGitBranches(input: GitListBranchesInput): Promise { method: string; threadId: string; }) => void; - threadLogStreams: Map< - string, - { writableEnded: boolean; destroyed: boolean } - >; + threadLogStreams: Map; }; internals.onCodexEvent({ id: "evt-1", diff --git a/apps/server/src/providerManager.ts b/apps/server/src/providerManager.ts index be17babc35..52fb1b6484 100644 --- a/apps/server/src/providerManager.ts +++ b/apps/server/src/providerManager.ts @@ -87,11 +87,7 @@ export class ProviderManager extends EventEmitter { throw new Error(`Unknown provider session: ${input.sessionId}`); } - await this.codex.respondToRequest( - input.sessionId, - input.requestId, - input.decision, - ); + await this.codex.respondToRequest(input.sessionId, input.requestId, input.decision); } stopSession(raw: ProviderStopSessionInput): void { @@ -151,10 +147,7 @@ export class ProviderManager extends EventEmitter { private resolveThreadId(event: ProviderEvent): string | undefined { const fromPayload = this.readThreadIdFromPayload(event.payload); - const threadId = - event.threadId ?? - fromPayload ?? - this.sessionThreadIds.get(event.sessionId); + const threadId = event.threadId ?? fromPayload ?? this.sessionThreadIds.get(event.sessionId); if (threadId) { this.sessionThreadIds.set(event.sessionId, threadId); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 97c35f1660..353535ae32 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -369,7 +369,10 @@ export function createServer(options: ServerOptions) { const isServerNotRunningError = (error: unknown): boolean => { if (!(error instanceof Error)) return false; const maybeCode = (error as NodeJS.ErrnoException).code; - return maybeCode === "ERR_SERVER_NOT_RUNNING" || error.message.toLowerCase().includes("not running"); + return ( + maybeCode === "ERR_SERVER_NOT_RUNNING" || + error.message.toLowerCase().includes("not running") + ); }; const closeWebSocketServer = new Promise((resolve, reject) => { diff --git a/apps/server/turbo.jsonc b/apps/server/turbo.jsonc index c466a3a025..34f8874f8e 100644 --- a/apps/server/turbo.jsonc +++ b/apps/server/turbo.jsonc @@ -5,7 +5,7 @@ "start": { "dependsOn": ["build"], "cache": false, - "persistent": true - } - } + "persistent": true, + }, + }, } diff --git a/apps/web/index.html b/apps/web/index.html index fe848c5287..25aace82a3 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,7 +5,10 @@ - + T3 Code diff --git a/apps/web/package.json b/apps/web/package.json index 2594309c1e..d995f4819b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@t3tools/contracts": "workspace:*", + "@tanstack/react-query": "^5.90.0", "highlight.js": "^11.11.1", "lucide-react": "^0.563.0", "react": "^19.0.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 969f55a04c..4619f6c933 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useEffect, useMemo, useRef } from "react"; import ChatView from "./components/ChatView"; @@ -168,10 +169,14 @@ function Layout() { ); } +const queryClient = new QueryClient(); + export default function App() { return ( - - - + + + + + ); } diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index 1ba9c0cec1..aed876f203 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -1,5 +1,6 @@ import type { GitBranch } from "@t3tools/contracts"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useMemo, useRef, useState } from "react"; import { readNativeApi } from "../session-logic"; import { useStore } from "../store"; @@ -10,76 +11,85 @@ interface BranchToolbarProps { envLocked: boolean; } -export default function BranchToolbar({ - envMode, - onEnvModeChange, - envLocked, -}: BranchToolbarProps) { +export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) { const { state, dispatch } = useStore(); const api = useMemo(() => readNativeApi(), []); + const queryClient = useQueryClient(); - const [branches, setBranches] = useState([]); - const [isRepo, setIsRepo] = useState(false); - const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); const [isCreatingBranch, setIsCreatingBranch] = useState(false); const [newBranchName, setNewBranchName] = useState(""); - const [isInitializingRepo, setIsInitializingRepo] = useState(false); const branchMenuRef = useRef(null); const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); - const branchCwd = activeThread?.worktreePath ?? activeProject?.cwd; + const activeThreadId = activeThread?.id; + const activeThreadBranch = activeThread?.branch ?? null; + const activeWorktreePath = activeThread?.worktreePath ?? null; + const branchCwd = activeWorktreePath ?? activeProject?.cwd; - const loadBranches = useCallback(async () => { - if (!api || !branchCwd) return; - setIsLoadingBranches(true); - try { - const result = await api.git.listBranches({ cwd: branchCwd }); - setIsRepo(result.isRepo); - setBranches(result.branches); - } catch (error) { - setIsRepo(false); - setBranches([]); - if (activeThread) { - dispatch({ - type: "SET_ERROR", - threadId: activeThread.id, - error: - error instanceof Error ? error.message : "Failed to load git branches.", - }); - } - } finally { - setIsLoadingBranches(false); - } - }, [activeThread, api, branchCwd, dispatch]); + // ── Queries ─────────────────────────────────────────────────────────── - useEffect(() => { - void loadBranches(); - }, [loadBranches]); + const branchesQuery = useQuery({ + queryKey: ["git", "branches", branchCwd], + queryFn: () => api!.git.listBranches({ cwd: branchCwd! }), + enabled: !!api && !!branchCwd, + }); + + const branches = branchesQuery.data?.branches ?? []; + const isRepo = branchesQuery.data?.isRepo ?? false; + + // ── Mutations ───────────────────────────────────────────────────────── + + const checkoutMutation = useMutation({ + mutationFn: (branch: string) => api!.git.checkout({ cwd: branchCwd!, branch }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }), + onError: (error) => + setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."), + }); + + const createBranchMutation = useMutation({ + mutationFn: (branch: string) => + api!.git + .createBranch({ cwd: branchCwd!, branch }) + .then(() => api!.git.checkout({ cwd: branchCwd!, branch })), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }), + onError: (error) => + setThreadError(error instanceof Error ? error.message : "Failed to create branch."), + }); + + const initMutation = useMutation({ + mutationFn: () => api!.git.init({ cwd: branchCwd! }), + onSuccess: () => { + setThreadError(null); + queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }); + }, + onError: (error) => + setThreadError(error instanceof Error ? error.message : "Failed to initialize git repo."), + }); + + // ── Effects ─────────────────────────────────────────────────────────── // Keep thread branch synced to git current branch for local threads. + const queryBranches = branchesQuery.data?.branches; useEffect(() => { - if (!activeThread || activeThread.worktreePath) return; - const current = branches.find((branch) => branch.current); + if (!activeThreadId || activeWorktreePath) return; + const current = queryBranches?.find((branch) => branch.current); if (!current) return; - if (current.name === activeThread.branch) return; + if (current.name === activeThreadBranch) return; dispatch({ type: "SET_THREAD_BRANCH", - threadId: activeThread.id, + threadId: activeThreadId, branch: current.name, worktreePath: null, }); - }, [activeThread, branches, dispatch]); + }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]); useEffect(() => { if (!isBranchMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { if (!branchMenuRef.current) return; - if ( - event.target instanceof Node && - !branchMenuRef.current.contains(event.target) - ) { + if (event.target instanceof Node && !branchMenuRef.current.contains(event.target)) { setIsBranchMenuOpen(false); } }; @@ -89,79 +99,50 @@ export default function BranchToolbar({ }; }, [isBranchMenuOpen]); + // ── Helpers ─────────────────────────────────────────────────────────── + const setThreadError = (error: string | null) => { - if (!activeThread) return; - dispatch({ - type: "SET_ERROR", - threadId: activeThread.id, - error, - }); + if (!activeThreadId) return; + dispatch({ type: "SET_ERROR", threadId: activeThreadId, error }); }; const setThreadBranch = (branch: string | null, worktreePath: string | null) => { - if (!activeThread) return; - dispatch({ - type: "SET_THREAD_BRANCH", - threadId: activeThread.id, - branch, - worktreePath, - }); - }; - - const initializeRepo = async () => { - if (!api || !branchCwd || !activeThread || isInitializingRepo) return; - setIsInitializingRepo(true); - try { - await api.git.init({ cwd: branchCwd }); - setThreadError(null); - await loadBranches(); - } catch (error) { - setThreadError(error instanceof Error ? error.message : "Failed to initialize git repo."); - } finally { - setIsInitializingRepo(false); - } + if (!activeThreadId) return; + dispatch({ type: "SET_THREAD_BRANCH", threadId: activeThreadId, branch, worktreePath }); }; - const selectBranch = async (branch: GitBranch) => { - if (!api || !activeThread || !branchCwd) return; + const selectBranch = (branch: GitBranch) => { + if (!api || !activeThreadId || !branchCwd) return; // For new worktree mode, selecting a branch picks the base branch. - if (envMode === "worktree" && !envLocked && !activeThread.worktreePath) { + if (envMode === "worktree" && !envLocked && !activeWorktreePath) { setThreadError(null); setThreadBranch(branch.name, null); setIsBranchMenuOpen(false); return; } - try { - await api.git.checkout({ - cwd: branchCwd, - branch: branch.name, - }); - setThreadError(null); - setThreadBranch(branch.name, activeThread.worktreePath); - setIsBranchMenuOpen(false); - await loadBranches(); - } catch (error) { - setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."); - } + checkoutMutation.mutate(branch.name, { + onSuccess: () => { + setThreadError(null); + setThreadBranch(branch.name, activeWorktreePath); + setIsBranchMenuOpen(false); + }, + }); }; - const createBranch = async () => { + const createBranch = () => { const name = newBranchName.trim(); - if (!api || !activeThread || !branchCwd || !name) return; - try { - await api.git.createBranch({ cwd: branchCwd, branch: name }); - await api.git.checkout({ cwd: branchCwd, branch: name }); - setThreadError(null); - setThreadBranch(name, activeThread.worktreePath); - setNewBranchName(""); - setIsCreatingBranch(false); - setIsBranchMenuOpen(false); - await loadBranches(); - } catch (error) { - setThreadError(error instanceof Error ? error.message : "Failed to create branch."); - } + if (!api || !activeThreadId || !branchCwd || !name) return; + createBranchMutation.mutate(name, { + onSuccess: () => { + setThreadError(null); + setThreadBranch(name, activeWorktreePath); + setNewBranchName(""); + setIsCreatingBranch(false); + setIsBranchMenuOpen(false); + }, + }); }; if (!activeThread || !activeProject) return null; @@ -192,10 +173,10 @@ export default function BranchToolbar({ ) : (
@@ -203,7 +184,7 @@ export default function BranchToolbar({ type="button" className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[12px] text-muted-foreground/60 transition-colors duration-150 hover:bg-accent/50 hover:text-muted-foreground/80" onClick={() => setIsBranchMenuOpen((open) => !open)} - disabled={isLoadingBranches} + disabled={branchesQuery.isLoading} > {activeThread.branch @@ -246,7 +227,7 @@ export default function BranchToolbar({ ? "bg-accent text-foreground" : "text-foreground/90 hover:bg-accent/50" }`} - onClick={() => void selectBranch(branch)} + onClick={() => selectBranch(branch)} > {branch.name} {(branch.current || branch.isDefault) && ( @@ -266,7 +247,7 @@ export default function BranchToolbar({ className="flex items-center gap-1 px-1" onSubmit={(event) => { event.preventDefault(); - void createBranch(); + createBranch(); }} > @@ -702,9 +660,7 @@ export default function ChatView() { {pendingApprovals.length > 0 && (
{pendingApprovals.map((approval) => { - const isResponding = respondingRequestIds.includes( - approval.requestId, - ); + const isResponding = respondingRequestIds.includes(approval.requestId); return (
- void onRespondToApproval(approval.requestId, "accept") - } + onClick={() => void onRespondToApproval(approval.requestId, "accept")} > Approve once @@ -738,12 +692,7 @@ export default function ChatView() { type="button" className="rounded-md border border-sky-300/30 bg-sky-500/[0.15] px-2 py-1 text-[11px] text-sky-100 transition-colors duration-150 hover:bg-sky-500/[0.22] disabled:cursor-not-allowed disabled:opacity-50" disabled={isResponding} - onClick={() => - void onRespondToApproval( - approval.requestId, - "acceptForSession", - ) - } + onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")} > Always allow this session @@ -751,9 +700,7 @@ export default function ChatView() { type="button" className="rounded-md border border-border px-2 py-1 text-[11px] text-foreground/90 transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50" disabled={isResponding} - onClick={() => - void onRespondToApproval(approval.requestId, "decline") - } + onClick={() => void onRespondToApproval(approval.requestId, "decline")} > Decline @@ -761,9 +708,7 @@ export default function ChatView() { type="button" className="rounded-md border border-rose-300/30 bg-rose-500/[0.12] px-2 py-1 text-[11px] text-rose-100 transition-colors duration-150 hover:bg-rose-500/[0.2] disabled:cursor-not-allowed disabled:opacity-50" disabled={isResponding} - onClick={() => - void onRespondToApproval(approval.requestId, "cancel") - } + onClick={() => void onRespondToApproval(approval.requestId, "cancel")} > Cancel turn @@ -778,15 +723,14 @@ export default function ChatView() {
{activeThread.messages.length === 0 && !isWorking ? (
-

Send a message to start the conversation.

+

+ Send a message to start the conversation. +

) : (
{timelineEntries.map((timelineEntry, index) => { - if ( - timelineEntry.kind === "work" && - timelineEntries[index - 1]?.kind === "work" - ) { + if (timelineEntry.kind === "work" && timelineEntries[index - 1]?.kind === "work") { return null; } @@ -807,16 +751,13 @@ export default function ChatView() { const groupId = timelineEntry.id; const isExpanded = expandedWorkGroups[groupId] ?? false; - const hasOverflow = - groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES; + const hasOverflow = groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES; const visibleEntries = hasOverflow && !isExpanded ? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES) : groupedEntries; const hiddenCount = groupedEntries.length - visibleEntries.length; - const onlyToolEntries = groupedEntries.every( - (entry) => entry.tone === "tool", - ); + const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool"); const groupLabel = onlyToolEntries ? groupedEntries.length === 1 ? "Tool call" @@ -902,9 +843,7 @@ export default function ChatView() {
- {completionSummary - ? `Response • ${completionSummary}` - : "Response"} + {completionSummary ? `Response • ${completionSummary}` : "Response"}
@@ -913,24 +852,17 @@ export default function ChatView() {

{formatMessageMeta( timelineEntry.message.createdAt, timelineEntry.message.streaming - ? formatElapsed( - timelineEntry.message.createdAt, - nowIso, - ) + ? formatElapsed(timelineEntry.message.createdAt, nowIso) : formatElapsed( timelineEntry.message.createdAt, - assistantCompletionByItemId.get( - timelineEntry.message.id, - ), + assistantCompletionByItemId.get(timelineEntry.message.id), ), )}

@@ -1086,9 +1018,7 @@ export default function ChatView() { disabled={isSwitchingRuntimeMode} onClick={() => void handleRuntimeModeChange( - state.runtimeMode === "full-access" - ? "approval-required" - : "full-access", + state.runtimeMode === "full-access" ? "approval-required" : "full-access", ) } title={ @@ -1098,13 +1028,7 @@ export default function ChatView() { } > {state.runtimeMode === "full-access" ? ( - - {state.runtimeMode === "full-access" - ? "Full access" - : "Supervised"} - + {state.runtimeMode === "full-access" ? "Full access" : "Supervised"}
@@ -1226,11 +1140,7 @@ export default function ChatView() {
- +
); } diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 6a546c848f..72e11aa4a5 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -7,7 +7,9 @@ export default function DiffPanel() { return (