Git integration: branch picker + worktrees#3

merged32
main
juliusmarminge/git-integration
openedFeb 9, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago

Closes #7 (Does not implement multiple threads per worktree)

Summary

Users can now right-click on "+ New thread" to create threads from specific git branches, with optional worktree creation for isolated agent work.

Features

  • Branch context menu showing local branches sorted by current/default/alphabetical
  • Option to create git worktree for each branch before starting a thread
  • Graceful handling of non-git repositories with "Initialize git repo" button
  • Thread model extended with branch/worktreePath fields for persistence
  • Agent sessions use worktree path as cwd when available
  • Updated persistence schema to store branch and worktree info

Test Plan

  • Right-click "+ New thread" on a git-tracked project to see branch list
  • Select a branch to create thread with that branch in metadata
  • Click "worktree" button to create isolated worktree before starting thread
  • On non-git project, click to see "Not a git repository" with init option
  • Verify agent runs in worktree cwd when applicable
  • Verify branch/worktree badge displays in chat header

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

  • New Features

    • Branch toolbar beneath chat input to choose branch and environment (Local vs Worktree); environment locks after the first message.
    • Branch picker auto-populates from the repository; toolbar hidden for non-git projects.
    • First message in Worktree mode can create a new worktree and associate it with the thread; branch/worktree badges appear in header and sidebar.
  • Tests

    • Extensive unit and integration tests for branch, worktree, checkout, and end-to-end git flows.
Synced from GitHub

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds end-to-end Git integration: new contracts and WS API, server & desktop git utilities, renderer IPC and React Query wiring, per-thread branch/worktree state and persistence, BranchToolbar UI, ChatView worktree creation on first message, tests, and build/tooling/script updates.

Changes

Cohort / File(s)Summary
Contracts & WS API
packages/contracts/src/git.ts, packages/contracts/src/ipc.ts, packages/contracts/src/ws.ts, packages/contracts/src/index.ts
New zod schemas/types and result types for git operations; added WS method names; extended NativeApi.git with listBranches/createWorktree/removeWorktree/createBranch/checkout/init; re-exported git.
Server git module & WS handlers
apps/server/src/git.ts, apps/server/src/wsServer.ts, apps/server/src/git.test.ts, apps/server/src/wsServer.test.ts
Introduced server-side git utilities (runTerminalCommand, list/create/remove worktree, branch ops, init) and wired them into WS router; added FS-backed integration tests covering branches, worktrees, checkouts, and WS git RPCs.
Desktop git helpers & tests
apps/desktop/src/git.ts, apps/desktop/src/git.test.ts, apps/desktop/src/main.ts, apps/desktop/src/preload.ts
Extracted desktop git helpers into module; main imports module; preload exposes IPC for git operations; added desktop git tests.
Renderer native bridge
apps/web/src/wsNativeApi.ts, apps/desktop/src/preload.ts
Renderer-facing native API gains git.* methods that route to WS / preload IPC.
Frontend UI & flows
apps/web/src/components/BranchToolbar.tsx, apps/web/src/components/ChatView.tsx, apps/web/src/components/Sidebar.tsx, apps/web/src/App.tsx
Added BranchToolbar and integrated it into ChatView; ChatView manages envMode (local
State, types & persistence
apps/web/src/types.ts, apps/web/src/store.ts, apps/web/src/persistenceSchema.ts, apps/web/src/persistenceSchema.test.ts, apps/web/src/store.test.ts
Extended Thread with branch and worktreePath; added SET_THREAD_BRANCH action and reducer handling (clears session when worktreePath changes); persistence schema migrated to include nullable/optional fields; tests updated.
Web wiring & deps
apps/web/src/App.tsx, apps/web/package.json
Added @tanstack/react-query dependency, created QueryClient and wrapped app in QueryClientProvider; thread creation payload extended with new fields.
Tests & test plan
.plans/*, apps/desktop/src/git.test.ts, apps/server/src/git.test.ts, apps/web/src/git-flows.test.ts
Added integration/unit tests and a test plan for Git flows, worktrees, store reducer, and edge cases; factory helpers and comprehensive scenarios included.
Build/tooling & scripts
apps/*/tsdown.config.ts, removed tsup.config.ts, package.json, apps/*/package.json, *.turbo.jsonc, vitest.config.ts
Switched build tooling from tsup→tsdown, updated package scripts to turbo orchestration, adjusted outputs to .mjs/ESM, added turbo configs and vitest alias changes.
Misc UI/docs/formatting
README.md, AGENTS.md, various apps/web/*, apps/server/*, packages/contracts/*
Docs and plan files added/updated; assorted formatting and small non-functional refactors across CSS, tests, and TS files.

Sequence Diagram

mermaid
1sequenceDiagram
2 participant User as User
3 participant ChatView as ChatView (UI)
4 participant BranchToolbar as BranchToolbar (UI)
5 participant Store as Renderer Store
6 participant NativeAPI as Native API (preload/WS)
7 participant WebSocket as WebSocket Transport
8 participant Server as Server (git module)
9
10 User->>ChatView: open chat / branch controls
11 ChatView->>BranchToolbar: render (envMode, envLocked)
12 BranchToolbar->>NativeAPI: git.listBranches({ cwd })
13 NativeAPI->>WebSocket: invoke "git:list-branches"
14 WebSocket->>Server: WS_METHODS.gitListBranches
15 Server-->>WebSocket: return branches
16 WebSocket-->>NativeAPI: branches
17 NativeAPI-->>BranchToolbar: branches list
18
19 User->>ChatView: send first message
20 alt envMode == "worktree" && no worktreePath
21 ChatView->>NativeAPI: git.createWorktree({ branch, cwd })
22 NativeAPI->>WebSocket: invoke "git:create-worktree"
23 WebSocket->>Server: WS_METHODS.gitCreateWorktree
24 Server->>Server: runTerminalCommand("git worktree add ...")
25 Server-->>WebSocket: { worktreePath, branch }
26 WebSocket-->>NativeAPI: result
27 NativeAPI-->>ChatView: worktreePath
28 ChatView->>Store: dispatch SET_THREAD_BRANCH(threadId, branch, worktreePath)
29 else
30 ChatView->>Store: dispatch SET_THREAD_BRANCH(threadId, branch, null)
31 end
32
33 Store->>Store: update thread.branch & thread.worktreePath
34 ChatView->>NativeAPI: ensure/start session(cwd = worktreePath ?? project.cwd)
35 NativeAPI->>WebSocket: start session
36 WebSocket->>Server: establish session in selected cwd
37 Server-->>WebSocket: session ready
38 WebSocket-->>NativeAPI: session active
39 NativeAPI-->>ChatView: session active

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningThe PR includes tooling/infrastructure changes (tsup→tsdown migration, Turbo config, Vitest config, package.json restructuring) unrelated to the Git integration feature objectives stated in issue #7.Extract tooling/build changes into a separate PR. Keep this PR focused on Git feature implementation only—branch picker UI, worktree support, persistence schema updates, and related tests.
Docstring Coverage⚠️ WarningDocstring coverage is 12.20% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'Git integration: branch picker + worktrees' accurately summarizes the main changes—adding Git branch selection UI and worktree support for thread management.
Linked Issues check✅ PassedThe PR implements all primary objectives from issue #7: threads associated with local Git branches (current, existing, new), Git worktree-based workspaces, and worktree reuse patterns. Multiple-threads-per-worktree and 'best of N' selection noted as out-of-scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch juliusmarminge/git-integration

Comment @coderabbitai help to get the list of available commands and usage tips.

Synced from GitHub

Add branch picker and worktree flow to ChatView and expose NativeApi.git.* methods backed by WebSocket handlers in wsServer.ts

Introduce a BranchToolbar in ChatView for branch selection and worktree creation, wire NativeApi.git.listBranches/createWorktree/removeWorktree/createBranch/checkout/init to WebSocket routes, and update thread state to track branch and worktreePath with session reset on cwd changes. Switch builds to tsdown for desktop/server/contracts and add React Query at the web app root.

📍Where to Start

Start with WebSocket routing for git operations in createServer in wsServer.ts, then review the UI integration in BranchToolbar in BranchToolbar.tsx and thread state changes in store.ts.


Macroscope summarized 9442019.

apps/desktop/src/main.tsline 333Outdated · 8ee3bf5Open thread
……@@ -250,6 +289,95 @@ async function runTerminalCommand(
330+async function createGitWorktree(
331+ input: GitCreateWorktreeInput,
332+): Promise<GitCreateWorktreeResult> {
333+ const sanitizedBranch = input.branch.replace(/\//g, "-");
MmacroscopeappBoton line 333Feb 9, 2026

src/main.ts:333 Replacing / with - can cause collisions (e.g., feature/test and feature-test both become feature-test). Consider using a more unique transformation or appending a hash to avoid conflicts.

No longer relevant as of 79fdda9deea9abd21c3036f6a1d1f5004db4a8b2

apps/desktop/src/main.tsline 359Outdated · 8ee3bf5Open thread
……@@ -250,6 +289,95 @@ async function runTerminalCommand(
356+
357+async function removeGitWorktree(input: GitRemoveWorktreeInput): Promise<void> {
358+ const result = await runTerminalCommand({
359+ command: `git worktree remove '${input.path.replace(/'/g, "'\\''")}'`,
MmacroscopeappBoton line 359Feb 9, 2026

src/main.ts:359 Shell quoting uses POSIX single quotes, but runTerminalCommand invokes cmd.exe on Windows, so paths with spaces will break. Consider platform-aware quoting/escaping (e.g., double quotes on Windows) or a cross‑platform command builder.

No longer relevant as of 79fdda9deea9abd21c3036f6a1d1f5004db4a8b2

apps/renderer/src/components/ChatView.tsxline 162Outdated · eefd4e6Open thread
……@@ -154,7 +159,7 @@ export default function ChatView() {
155160 const session = await api.providers.startSession({
156161 provider: "codex",
157- cwd: activeProject.cwd || undefined,
162+ cwd: activeThread.worktreePath ?? activeProject.cwd,
MmacroscopeappBoton line 162Feb 9, 2026

components/ChatView.tsx:162 After dispatching SET_THREAD_BRANCH, ensureSession still reads the stale activeThread from the closure, so worktreePath is null and the session starts in the wrong directory. Consider passing the new worktreePath directly to ensureSession (or to startSession) instead of relying on the updated state.

✅ Addressed in 79fdda9deea9abd21c3036f6a1d1f5004db4a8b2

apps/renderer/src/components/ChatView.tsxlines 427–428Outdated · aa02b11Open thread
……@@ -416,10 +421,37 @@ export default function ChatView() {
425+ return;
420426 const trimmed = prompt.trim();
421427 if (!trimmed) return;
422428
MmacroscopeappBoton lines 427–428Feb 9, 2026

components/ChatView.tsx:427 Consider setting isSending = true at the start of onSend (before the worktree creation block) to prevent race conditions from rapid submissions.

No longer relevant as of bc70cc4b74e5e0258d35ab90a80aedd3f20f75e2

apps/server/src/git.tsline 45Outdated · da9421aOpen thread
……@@ -0,0 +1,129 @@
42+ .filter((line) => line.length > 0)
43+ .map((line) => ({
44+ name: line.replace(/^[*+]\s+/, ""),
45+ current: line.startsWith("* "),
MmacroscopeappBoton line 45Feb 9, 2026

🟡 Medium

src/git.ts:45 The child.killed flag becomes true immediately after child.kill("SIGTERM") is called, so the SIGKILL fallback is unreachable. Consider tracking process exit separately (e.g., with a flag set in the close event) instead of relying on child.killed.

🚀 Want me to fix this? Reply ex: "fix it for me".

apps/server/src/git.tsline 45Outdated · da9421aOpen thread
……@@ -0,0 +1,129 @@
42+ .filter((line) => line.length > 0)
43+ .map((line) => ({
44+ name: line.replace(/^[*+]\s+/, ""),
45+ current: line.startsWith("* "),
MmacroscopeappBoton line 45Feb 9, 2026

src/git.ts:45 The child.killed flag becomes true immediately after child.kill("SIGTERM") is called, so the SIGKILL fallback is unreachable. Consider tracking process exit separately (e.g., with a flag set in the close event) instead of relying on child.killed.

No longer relevant as of 1d68c4abfadb8ae1dfb50e667622df56872ce227

apps/desktop/src/terminal.tsline 37Outdated · 1d68c4aOpen thread
……@@ -0,0 +1,60 @@
34+ }, input.timeoutMs ?? 30_000);
35+
36+ child.stdout?.on("data", (chunk: Buffer) => {
37+ stdout += chunk.toString();
MmacroscopeappBoton line 37Feb 9, 2026

src/terminal.ts:37 Consider adding a maximum buffer size for stdout and stderr to prevent OOM if commands produce excessive output. Truncate or reject once a limit is reached.

No longer relevant as of bc70cc4b74e5e0258d35ab90a80aedd3f20f75e2

apps/renderer/src/components/BranchToolbar.tsxline 150Outdated · adf6a26Open thread
……@@ -0,0 +1,294 @@
147+ : "text-foreground/90 hover:bg-accent/50"
148+ }`}
149+ onClick={async () => {
150+ if (api && activeProject) {
MmacroscopeappBoton line 150Feb 9, 2026

components/BranchToolbar.tsx:150 The SET_THREAD_BRANCH dispatch (lines 169-179) runs even when the git checkout is skipped due to !api || !activeProject. Consider moving these dispatch calls inside the conditional block so UI state only updates after a successful checkout.

No longer relevant as of bc70cc4b74e5e0258d35ab90a80aedd3f20f75e2

apps/server/src/git.tsline 55Outdated · 6465e69Open thread
……@@ -0,0 +1,195 @@
52+ }, input.timeoutMs ?? 30_000);
53+
54+ child.stdout?.on("data", (chunk: Buffer) => {
55+ stdout += chunk.toString();

🟢 Low

src/git.ts:55 Multi-byte UTF-8 characters split across chunks will corrupt to �. Consider using StringDecoder from node:string_decoder to properly handle partial characters.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
text
1In file apps/server/src/git.ts around line 55:
2
3Multi-byte UTF-8 characters split across chunks will corrupt to `�`. Consider using `StringDecoder` from `node:string_decoder` to properly handle partial characters.
apps/server/src/git.tsline 151Outdated · 6465e69Open thread
……@@ -0,0 +1,195 @@
148+
149+export async function removeGitWorktree(input: GitRemoveWorktreeInput): Promise<void> {
150+ const result = await runTerminalCommand({
151+ command: `git worktree remove '${escapeSingleQuotes(input.path)}'`,
MmacroscopeappBoton line 151Feb 10, 2026

src/git.ts:151 On Windows, cmd.exe doesn't handle single-quoted arguments correctly. Consider using double quotes on Windows or escaping appropriately for cross-platform compatibility.

✅ Addressed in 807e3d18dec858c9694a595deaea4f9ac588db5f

Added 17 commits

  • kewl2a610f9
  • Add branch/env picker toolbar and create branch flow3047974
  • Add git checkout support and clear errors on success5944859
  • Add test for listGitBranches from worktree cwd6474490
  • Fix typecheck errors for branch/worktree fields69e38a2
  • Fix branch query cwd for worktree threads and error banner light mode6ce773f
  • eh79ac59f
  • resource management7e20148
  • Add branch/worktreePath to persistence test expectation8225352
  • plans9fc5ad2
  • Restore git toolbar via websocket backendc13e9f8
  • Fix "Open in editor" to respect worktree pathc5cbe15
  • Polish git worktree helpers after rebased1ecacd
  • Add git integration: branch picker + worktreesd64f78a
  • Add light mode support for git branch picker and default branch labele9cfe37
  • Extract git functions and add integration testsf47dc3c
  • Move toolbar below chat input, fix electron preload bundling, and update build configsd6437a7
turbo.jsonlines 20–24Outdated · d6437a7Open thread
……@@ -18,9 +18,9 @@
2222 "cache": false,
23- "persistent": true
23+ "persistent": false
2424 },
MmacroscopeappBoton lines 20–24Feb 11, 2026

turbo.json:20 The dev task with "dependsOn": ["^dev"] and "persistent": false will hang indefinitely. Since vite dev servers never complete, dependent tasks wait forever. Consider reverting to the previous approach: depend on build tasks that complete (e.g., @t3tools/contracts#build) and set persistent: true.

No longer relevant as of 7e5cd01d26b21093947cb9b988f5963b8ed6ce26

Added 8 commits

  • Fix CI typecheck: point contracts types to source and remove dead code74f6ce8
  • Fix CI typecheck: build contracts before typechecking dependents4ab095f
  • Fix dev:desktop task graph to match main's env var patternse2052b8
  • fixceceba2
  • Fix server build: add zod to noExternal to prevent tsdown errorca356d0
  • Suppress tsdown inlineOnly warning for server build01c3e82
  • Polish turbo task graph and dev scripts7e5cd01
  • Merge remote-tracking branch 'origin/main' into juliusmarminge/git-integration6e3726e
CcoderabbitaiBotreviewed · 5 inline threadsFeb 11, 2026
Synced from GitHub

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/scripts/smoke-test.mjs (1)

12-56: ⚠️ Potential issue | 🟠 Major

Handle spawn errors and non‑zero exits to avoid false positives.
Uncaught error events (e.g., missing Electron binary) and non‑zero exit codes can slip through, leading to flaky or misleading smoke-test results.

🛠️ Suggested fix
diff
1 const child = spawn(electronBin, [mainJs], {
2 stdio: ["pipe", "pipe", "pipe"],
3 env: {
4 ...process.env,
5 VITE_DEV_SERVER_URL: "",
6 ELECTRON_ENABLE_LOGGING: "1",
7 },
8 });
9
10+child.on("error", (err) => {
11+ clearTimeout(timeout);
12+ console.error("\nDesktop smoke test failed (spawn error):");
13+ console.error(err);
14+ process.exit(1);
15+});
16+
17 let output = "";
18 child.stdout.on("data", (chunk) => {
19 output += chunk.toString();
20 });
21 child.stderr.on("data", (chunk) => {
22 output += chunk.toString();
23 });
24
25 const timeout = setTimeout(() => {
26 child.kill();
27 }, 8_000);
28
29-child.on("exit", () => {
30+child.on("exit", (code, signal) => {
31 clearTimeout(timeout);
32
33 const fatalPatterns = [
34 "Cannot find module",
35 "MODULE_NOT_FOUND",
36 "Refused to execute",
37 "Uncaught Error",
38 "Uncaught TypeError",
39 "Uncaught ReferenceError",
40 ];
41 const failures = fatalPatterns.filter((pattern) => output.includes(pattern));
42
43- if (failures.length > 0) {
44+ if (failures.length > 0 || code !== 0 || signal) {
45 console.error("\nDesktop smoke test failed:");
46+ if (code !== 0) console.error(` - exit code: ${code}`);
47+ if (signal) console.error(` - signal: ${signal}`);
48 for (const failure of failures) {
49 console.error(` - ${failure}`);
50 }
51 console.error("\nFull output:\n" + output);
52 process.exit(1);
53 }
🤖 Fix all issues with AI agents
1In @.plans/git-flows-integration-tests.md:
2- Around line 93-99: The documentation uses "npx vitest run
3apps/desktop/src/git.test.ts" which contradicts the Bun-centric tooling
4elsewhere; update the verification snippet to use Bun's runner by replacing the
5"npx" invocation with "bunx" (so the second command uses bunx vitest run
6apps/desktop/src/git.test.ts) and keep the existing "cd apps/desktop && bun run
7test" line unchanged; ensure both examples consistently recommend Bun tooling.
8
9In `@apps/desktop/package.json`:
10- Around line 22-24: Update the package.json dev dependency for "vitest" from
11"^4.0.0" to "^4.0.18" to pick up recent patch fixes, and optionally add a
12comment or package.json note near "tsdown" to clarify it's already at the latest
13stable (0.20.3) if you want to signal stability; also review the test script
14that passes "--passWithNoTests" (in package.json scripts) and decide whether to
15remove or tighten that flag after updating vitest to encourage real test
16coverage.
17
18In `@apps/server/src/git.ts`:
19- Around line 122-183: The git commands in createGitWorktree, removeGitWorktree,
20createGitBranch, and checkoutGitBranch are built as single shell strings with
21single quotes (e.g., using escapeSingleQuotes) which breaks on Windows cmd.exe;
22change each to spawn git directly with an args array instead of a shell command
23string (e.g., call runTerminalCommand or child_process.spawn with ['git',
24'worktree', 'add', '-b', input.newBranch, worktreePath, input.branch'] and
25shell: false), remove the single-quote escaping (no escapeSingleQuotes
26wrapping), keep cwd and timeoutMs, and ensure failures still throw the same
27errors based on result.code/result.stderr.
28
29In `@apps/web/package.json`:
30- Around line 28-32: The package.json currently pins "vite" to a beta
31pre-release ("^8.0.0-beta.12"); change the "vite" dependency entry to a stable
327.x release (for example "^7.21.0" or the latest 7.x), update the lockfile by
33running your package manager (npm/yarn/pnpm install), and run the dev/build/test
34commands to verify nothing breaks; edit the "vite" key in package.json to
35replace the beta version, commit the lockfile changes, and ensure CI passes
36(reference: the "vite" dependency entry in package.json).
37
38In `@apps/web/src/components/BranchToolbar.tsx`:
39- Around line 61-73: The auto-sync useEffect in BranchToolbar (the effect that
40checks branches.find and dispatches SET_THREAD_BRANCH) is overwriting a
41user-selected worktree base branch; change the guard so the effect only runs for
42true local threads (e.g., when activeThread exists AND activeThread.worktreePath
43is falsy AND the thread is in local mode) or only when activeThread.branch is
44null/undefined, then proceed to find the current branch and dispatch
45SET_THREAD_BRANCH; update the useEffect condition that currently checks
46(!activeThread || activeThread.worktreePath) to include the extra guard (or
47replace it with a check for activeThread.branch == null) so selecting a
48different base branch for a pending worktree is not clobbered.
🧹 Nitpick comments (3)
.plans/git-flows-test-plan.md (2)

28-32: Consider adding guidance on mock cleanup between tests.

The factory pattern is solid, but the plan should mention resetting mocks between tests to ensure isolation. Consider adding a note about using beforeEach(() => vi.clearAllMocks()) or similar to reset the mocked Git API functions between test cases.

📋 Suggested addition to the Factories section

Add after line 32:

markdown
1**Setup/Teardown**
2
3Use `beforeEach(() => vi.clearAllMocks())` to reset mocked functions between tests, ensuring each test starts with clean mock state.

Based on learnings: For integration tests: Prefer deterministic inputs and explicit state checks; avoid relying on logs or timing assumptions. Clean up external resources in finally blocks.


94-102: Clarify working directory assumptions for verification commands.

The verification commands mix two approaches: line 96 changes directory before running tests, while lines 99-102 use full paths from the repo root. This inconsistency could confuse developers.

🔧 Suggested clarification

Consider adding a comment about the working directory:

diff
1 ## Verification
2
3+All commands assume you're at the repository root unless otherwise noted.
4+
5 ```bash
6 # Run all renderer tests
7 cd apps/renderer && bun run test
8
9 # Run just the new test file
10 npx vitest run apps/renderer/src/git-flows.test.ts
11
12 # Run just the store tests
13 npx vitest run apps/renderer/src/store.test.ts
1
2Alternatively, make all commands consistent:
3
4```bash
5# Run all renderer tests
6npx vitest run apps/renderer
7
8# Run just the new test file
9npx vitest run apps/renderer/src/git-flows.test.ts
10
11# Run just the store tests
12npx vitest run apps/renderer/src/store.test.ts
apps/web/src/components/BranchToolbar.tsx (1)

169-309: Consider splitting the toolbar UI into smaller subcomponents.
The render block is large and mixes toggle, menu, and creation form logic; extracting EnvModeToggle and BranchMenu subcomponents (kept in the same file) would reduce complexity and keep logic closer to the JSX it serves.

Based on learnings: Applies to **/*.tsx : Extract large React components into multiple subcomponents with granular functionality. Co-locate subcomponents in the same file as the main component. Avoid hoisting callbacks too high up the component tree; prefer colocating logic close to JSX.

apps/desktop/package.jsonlines 22–24Outdated · 6e3726eOpen thread
……@@ -18,11 +18,10 @@
22+ "tsdown": "^0.20.3",
2423 "typescript": "^5.7.3",
25- "vitest": "^3.0.0",
24+ "vitest": "^4.0.0",
CcoderabbitaiBoton lines 22–24Feb 11, 2026

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

shell
1#!/bin/bash
2
3# Check latest versions from npm registry
4echo "=== tsdown latest version ==="
5npm view tsdown@latest version 2>/dev/null || echo "Failed to fetch"
6
7echo ""
8echo "=== vitest latest version ==="
9npm view vitest@latest version 2>/dev/null || echo "Failed to fetch"
10
11echo ""
12echo "=== tsdown 0.20.3 details ==="
13npm view tsdown@0.20.3 2>/dev/null | head -30 || echo "Failed to fetch"
14
15echo ""
16echo "=== vitest 4.0.0 details ==="
17npm view vitest@4.0.0 2>/dev/null | head -30 || echo "Failed to fetch"

Repository: pingdotgg/codething-mvp

Length of output: 1591


🏁 Script executed:

shell
1#!/bin/bash
2
3# Check the full package.json context in apps/desktop
4echo "=== apps/desktop/package.json content ==="
5if [ -f "apps/desktop/package.json" ]; then
6 cat apps/desktop/package.json
7else
8 echo "File not found"
9fi

Repository: pingdotgg/codething-mvp

Length of output: 821


🏁 Script executed:

shell
1#!/bin/bash
2
3# Search for vitest and tsdown configuration in the repository
4echo "=== vitest configuration files ==="
5fd -e "vitest.config" -e "vitest.config.ts" -e "vitest.config.js" 2>/dev/null
6
7echo ""
8echo "=== tsdown usage in the codebase ==="
9rg "tsdown" --type json --type ts --type tsx -l 2>/dev/null | head -20
10
11echo ""
12echo "=== vitest usage pattern ==="
13rg "vitest" --type json --type ts --type tsx -l 2>/dev/null | head -20

Repository: pingdotgg/codething-mvp

Length of output: 169


Consider updating vitest to 4.0.18 and clarify tsdown stability.

tsdown@0.20.3 is already the latest stable release and does not represent a major update. However, vitest@4.0.0 is outdated—the current stable patch is 4.0.18. Both versions are compatible with TypeScript 5.7.3, but updating vitest to the latest patch is recommended for bug fixes and stability improvements. Note that the test script currently uses --passWithNoTests, suggesting minimal active test coverage; consider whether vitest configuration and test suite expansion are needed for this Electron desktop application.

🤖 Prompt for AI Agents
1In `@apps/desktop/package.json` around lines 22 - 24, Update the package.json dev
2dependency for "vitest" from "^4.0.0" to "^4.0.18" to pick up recent patch
3fixes, and optionally add a comment or package.json note near "tsdown" to
4clarify it's already at the latest stable (0.20.3) if you want to signal
5stability; also review the test script that passes "--passWithNoTests" (in
6package.json scripts) and decide whether to remove or tighten that flag after
7updating vitest to encourage real test coverage.
.plans/git-flows-integration-tests.mdlines 93–99Outdated · 6e3726eOpen thread
……@@ -0,0 +1,99 @@
96+
97+# Or just the git test file
98+npx vitest run apps/desktop/src/git.test.ts
99+```
CcoderabbitaiBoton lines 93–99Feb 11, 2026

⚠️ Potential issue | 🟡 Minor

Use bunx instead of npx in the verification snippet.
This keeps docs consistent with the Bun-only tooling standard.

✏️ Proposed doc fix
diff
1-# Or just the git test file
2-npx vitest run apps/desktop/src/git.test.ts
3+# Or just the git test file
4+bunx vitest run apps/desktop/src/git.test.ts

Based on learnings: Use Bun as the package manager. Use bunx <BIN> when executing scripts from npm packages not installed as a project dependency.

🤖 Prompt for AI Agents
1In @.plans/git-flows-integration-tests.md around lines 93 - 99, The
2documentation uses "npx vitest run apps/desktop/src/git.test.ts" which
3contradicts the Bun-centric tooling elsewhere; update the verification snippet
4to use Bun's runner by replacing the "npx" invocation with "bunx" (so the second
5command uses bunx vitest run apps/desktop/src/git.test.ts) and keep the existing
6"cd apps/desktop && bun run test" line unchanged; ensure both examples
7consistently recommend Bun tooling.
apps/web/package.jsonlines 28–32Outdated · 6e3726eOpen thread
……@@ -25,10 +25,10 @@
3030 "tailwindcss": "^4.0.0",
3131 "typescript": "^5.7.3",
32- "vite": "^6.0.5"
32+ "vite": "^8.0.0-beta.12"
CcoderabbitaiBoton lines 28–32Feb 11, 2026

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Vite 8 stable release date 2026

💡 Result:

As of February 11, 2026, Vite 8 does not have a stable (non-beta) release date announced or published yet—the project is still shipping 8.0.0 beta pre-releases. The Vite 8 beta was announced on December 3, 2025 and GitHub shows ongoing v8.0.0-beta.x releases (e.g., v8.0.0-beta.13 on February 5, 2026). [1][2]

Sources:
[1] https://vite.dev/blog/announcing-vite8-beta
[2] https://github.com/vitejs/vite/releases


Replace Vite 8 beta with a stable release version.

Vite 8 remains in beta as of February 2026 with no announced stable release date. Using pre-release versions in production code introduces significant risk of breaking changes and instability. Update to Vite 7.x or wait for Vite 8 stable release before merging.

🤖 Prompt for AI Agents
1In `@apps/web/package.json` around lines 28 - 32, The package.json currently pins
2"vite" to a beta pre-release ("^8.0.0-beta.12"); change the "vite" dependency
3entry to a stable 7.x release (for example "^7.21.0" or the latest 7.x), update
4the lockfile by running your package manager (npm/yarn/pnpm install), and run
5the dev/build/test commands to verify nothing breaks; edit the "vite" key in
6package.json to replace the beta version, commit the lockfile changes, and
7ensure CI passes (reference: the "vite" dependency entry in package.json).
apps/server/src/git.tslines 122–183Outdated · 6e3726eOpen thread
……@@ -0,0 +1,195 @@
180+ if (result.code !== 0) {
181+ throw new Error(result.stderr.trim() || "git checkout failed");
182+ }
183+}
CcoderabbitaiBoton lines 122–183Feb 11, 2026

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

nodejs child_process shell true windows cmd.exe quoting

💡 Result:

On Windows, child_process.spawn/exec with { shell: true } runs your command through the system shell (process.env.ComSpec, typically cmd.exe). Node expects that shell to understand Unix -c or Windows cmd.exe /d /s /c-style invocation. [1]

Why quoting “breaks” with shell: true on Windows

You’re now dealing with two parsers:

  1. Node serializes your command/args into a single command line for the shell. On Windows, Node has special behavior around argument quoting; windowsVerbatimArguments controls whether Node does any quoting/escaping, and it’s set to true automatically when the shell is CMD. [1]

  2. cmd.exe then applies its own quote rules to the string after /c (or /k). With /s, cmd uses special rules that strip the first/last quote around the command string while leaving the rest unchanged. [2] Those rules are quirky enough that the common workaround is the “double-quote wrapper” pattern: cmd /s /c ""C:\Path With Spaces\app.exe" "arg with spaces"". (Examples and rule details are documented widely.) [3]

Practical guidance

  • Prefer shell: false (the default) and pass an args array whenever you can:

    js
    1spawn('git', ['commit', '-m', 'message with spaces'], { shell: false });

    This avoids cmd.exe quoting entirely.

  • If you must use CMD via { shell: true }, build a single CMD command string and use the outer-double-quote form so paths-with-spaces work:

    js
    1spawn('cmd.exe', ['/d', '/s', '/c', '""C:\\Program Files\\Tool\\tool.exe" "arg 1" "arg&2""']);

    If an argument contains CMD metacharacters like &|<>^(), you generally must escape them for CMD (often with ^) because shell: true means they’re meaningful to the shell.

Sources: Node.js child_process docs (shell + windowsVerbatimArguments). [1] Microsoft cmd docs for /c and /s quote-stripping behavior. [2] CMD quoting behavior details and examples (including the cmd /c ""..."" pattern). [3]


Shell-quoted git commands will break on Windows cmd.exe.

Lines 132, 151, 163, and 175 wrap git arguments in single quotes; cmd.exe does not recognize single quotes as delimiters, so paths and branch names will be passed to git with quotes included as literal characters. This breaks on Windows, especially with spaces in paths or branch names.

To fix, spawn git directly with an arguments array instead of a shell command string:

js
1spawn('git', ['worktree', 'add', '-b', input.newBranch, worktreePath, input.branch], { shell: false })

This avoids cmd.exe's quoting rules entirely and works consistently across Windows, macOS, and Linux.

🤖 Prompt for AI Agents
1In `@apps/server/src/git.ts` around lines 122 - 183, The git commands in
2createGitWorktree, removeGitWorktree, createGitBranch, and checkoutGitBranch are
3built as single shell strings with single quotes (e.g., using
4escapeSingleQuotes) which breaks on Windows cmd.exe; change each to spawn git
5directly with an args array instead of a shell command string (e.g., call
6runTerminalCommand or child_process.spawn with ['git', 'worktree', 'add', '-b',
7input.newBranch, worktreePath, input.branch'] and shell: false), remove the
8single-quote escaping (no escapeSingleQuotes wrapping), keep cwd and timeoutMs,
9and ensure failures still throw the same errors based on
10result.code/result.stderr.

✅ Addressed in commit 198cc58

apps/web/src/components/BranchToolbar.tsxlines 61–73Outdated · 6e3726eOpen thread
……@@ -0,0 +1,312 @@
70+ branch: current.name,
71+ worktreePath: null,
72+ });
73+ }, [activeThread, branches, dispatch]);
CcoderabbitaiBoton lines 61–73Feb 11, 2026

⚠️ Potential issue | 🟠 Major

Worktree base-branch selection gets overwritten by auto-sync.
In worktree mode (before a worktree exists), the sync effect resets activeThread.branch to the current checked-out branch, which defeats selecting a different base branch for the worktree. Guard the sync to run only for local mode (or only when activeThread.branch is null).

🔧 Suggested fix
diff
1 useEffect(() => {
2 if (!activeThread || activeThread.worktreePath) return;
3+ if (envMode !== "local") return;
4 const current = branches.find((branch) => branch.current);
5 if (!current) return;
6 if (current.name === activeThread.branch) return;
7 dispatch({
8 type: "SET_THREAD_BRANCH",
9 threadId: activeThread.id,
10 branch: current.name,
11 worktreePath: null,
12 });
13- }, [activeThread, branches, dispatch]);
14+ }, [activeThread, branches, dispatch, envMode]);
🤖 Prompt for AI Agents
1In `@apps/web/src/components/BranchToolbar.tsx` around lines 61 - 73, The
2auto-sync useEffect in BranchToolbar (the effect that checks branches.find and
3dispatches SET_THREAD_BRANCH) is overwriting a user-selected worktree base
4branch; change the guard so the effect only runs for true local threads (e.g.,
5when activeThread exists AND activeThread.worktreePath is falsy AND the thread
6is in local mode) or only when activeThread.branch is null/undefined, then
7proceed to find the current branch and dispatch SET_THREAD_BRANCH; update the
8useEffect condition that currently checks (!activeThread ||
9activeThread.worktreePath) to include the extra guard (or replace it with a
10check for activeThread.branch == null) so selecting a different base branch for
11a pending worktree is not clobbered.

✅ Addressed in commit 807e3d1

Synced from GitHub

Greptile Overview

Greptile Summary

This PR adds git branch/worktree integration across the web UI and websocket server: threads can store branch/worktreePath, the web UI can list/checkout/create branches and create a worktree on the first send, and the server exposes git methods via new WS_METHODS routes.

Key issues to address before merge:

  • The new server git implementation executes git via a shell with interpolated user-controlled values (branch/path), which is unsafe and should be switched to argv-based spawn('git', [...]).
  • Deleting a thread does not remove its associated worktree, so worktree-backed threads will leak directories on disk.
  • The repo test suite is currently broken in this PR state due to the Vitest config + contracts aliasing (Zod import resolves to undefined), so CI/local bun run test will fail until resolved.

Confidence Score: 2/5

  • This PR should not be merged until test failures and the git command execution safety issues are resolved.
  • The PR introduces new server-side git execution that currently shells out with interpolated user inputs (high impact security/robustness issue), introduces a worktree lifecycle leak when threads are deleted, and leaves the repository’s Vitest test suite failing due to module resolution (zod import undefined).
  • apps/server/src/git.ts, apps/web/src/components/Sidebar.tsx, vitest.config.ts

Important Files Changed

FilenameOverview
apps/server/src/git.tsAdds git command helpers invoked over websocket; currently builds shell commands by interpolating branch/path which is unsafe and should avoid shell invocation.
apps/web/src/components/Sidebar.tsxAdds thread context menu delete action but does not clean up associated worktrees, leaving orphaned worktree directories for worktree-backed threads.
vitest.config.tsTest config aliasing contracts to source causes vitest runtime failures (zod import undefined); repo tests fail in this PR state and need fixing.
apps/server/src/wsServer.tsWires new git websocket methods to server-side git helpers; routing looks consistent with existing WS_METHODS switch.
apps/web/src/components/ChatView.tsxAdds envMode + worktree creation on first send and uses worktreePath for cwd; main risk is dependent on worktree lifecycle management elsewhere.

Sequence Diagram

mermaid
1sequenceDiagram
2 participant User as User
3 participant Web as Web app (ChatView/BranchToolbar)
4 participant WS as WS transport
5 participant Server as Server (wsServer)
6 participant Git as git CLI
7
8 User->>Web: Select envMode + branch
9 Web->>WS: git.listBranches(cwd)
10 WS->>Server: WS_METHODS.gitListBranches
11 Server->>Git: git branch --no-color
12 Git-->>Server: branches
13 Server-->>WS: {isRepo, branches}
14 WS-->>Web: branches
15
16 User->>Web: Send first message (worktree mode)
17 Web->>WS: git.createWorktree({cwd, branch, newBranch})
18 WS->>Server: WS_METHODS.gitCreateWorktree
19 Server->>Git: git worktree add -b newBranch path branch
20 Git-->>Server: worktree created
21 Server-->>WS: {worktree:{path, branch}}
22 WS-->>Web: worktree info
23 Web->>WS: providers.startSession({cwd: worktreePath})
24 WS->>Server: WS_METHODS.providersStartSession
25 Server-->>WS: session
26 WS-->>Web: session
27
28 User->>Web: Right-click thread -> Delete
29 Web->>WS: providers.stopSession(sessionId)
30 WS->>Server: WS_METHODS.providersStopSession
31 Server-->>WS: ok
32 WS-->>Web: ok
33 Note over Web: Current code deletes thread
34 Note over Web: Missing: git.removeWorktree({cwd, path})
vitest.config.tslines 4–13Outdated · 6e3726eOpen thread
……@@ -1,3 +1,13 @@
10+ },
11+ ],
12+ },
13+});

Vitest run broken This config aliases @t3tools/contracts to the TypeScript source entry (packages/contracts/src/index.ts). With the PR’s build changes, tests now fail at runtime with TypeError: undefined is not an object (evaluating '__vite_ssr_import_0__.z.object'), which indicates the Zod import in those aliased sources isn’t being resolved correctly under Vitest’s SSR transform.

Since bun run test is part of the repo workflow, this needs to be fixed before merge (either by adjusting the alias/optimizeDeps/ssr settings or changing how contracts are imported in tests).

Synced from GitHub
Additional Comments (2)

apps/server/src/git.ts Shell command injection risk runTerminalCommand executes input.command via a shell (/bin/sh -lc on *nix, cmd.exe /c on Windows). In this PR, git commands interpolate user-controlled values (e.g. branch, newBranch, path) into a single-quoted shell string via escapeSingleQuotes. That escaping is not a safe substitute for avoiding the shell, and this becomes exploitable if any caller can pass arbitrary branch/path values (e.g. via websocket).

Consider switching these git helpers to spawn('git', [...args], { cwd }) (no shell) and pass untrusted values as argv, not string interpolation.


apps/web/src/components/Sidebar.tsx Worktree leak on delete When deleting a thread, the code stops the provider session and dispatches DELETE_THREAD, but never removes an associated git worktree (thread.worktreePath). Since api.git.removeWorktree exists, deleting a worktree-backed thread currently leaves an orphaned worktree directory on disk.

This only affects threads that have worktreePath set (created via worktree mode). Those worktrees will accumulate over time unless cleanup is added here (or elsewhere as part of deletion).

Added 1 commit

  • Fix envMode reset bug and use shell-free git spawn198cc58
apps/server/src/git.tslines 191–191Outdated · 198cc58Open thread
……@@ -0,0 +1,212 @@
188+}
189+
190+export async function createGitBranch(input: GitCreateBranchInput): Promise<void> {
191+ const result = await runGit(["branch", input.branch], input.cwd, 10_000);
MmacroscopeappBoton lines 191–191Feb 11, 2026

🟢 Low

src/git.ts:191 Suggestion: user-supplied branch names can be misparsed as options or pathspecs. Add -- before branch arguments across git calls (e.g., branch, checkout), or prefer git switch for switching branches to avoid ambiguity.

suggestion
1 const result = await runGit(["branch", "--", input.branch], input.cwd, 10_000);

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
text
1In file apps/server/src/git.ts around line 191:
2
3Suggestion: user-supplied branch names can be misparsed as options or pathspecs. Add `--` before branch arguments across `git` calls (e.g., `branch`, `checkout`), or prefer `git switch` for switching branches to avoid ambiguity.

Added 1 commit

  • Add react-query, refactor BranchToolbar async state, and fix stale deps807e3d1
apps/web/src/components/BranchToolbar.tsxlines 75–76Outdated · 807e3d1Open thread
……@@ -0,0 +1,293 @@
73+ // Keep thread branch synced to git current branch for local threads.
74+ const queryBranches = branchesQuery.data?.branches;
75+ useEffect(() => {
76+ if (!activeThreadId || activeWorktreePath) return;
MmacroscopeappBoton lines 75–76Feb 11, 2026

🟡 Medium

components/BranchToolbar.tsx:75 The useEffect at line 75 reverts the thread branch to match Git HEAD when activeWorktreePath is null. However, the new worktree mode (lines 118-123) also has null activeWorktreePath but intentionally sets a different branch without checkout. Consider adding envMode !== "worktree" to the effect's early return to prevent it from overwriting the user's base branch selection.

diff
1- useEffect(() => {
2- if (!activeThreadId || activeWorktreePath) return;
3+ useEffect(() => {
4+ if (!activeThreadId || activeWorktreePath || envMode === "worktree") return;

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
text
1In file apps/web/src/components/BranchToolbar.tsx around lines 75-76:
2
3The `useEffect` at line 75 reverts the thread branch to match Git HEAD when `activeWorktreePath` is null. However, the new worktree mode (lines 118-123) also has null `activeWorktreePath` but intentionally sets a different branch without checkout. Consider adding `envMode !== "worktree"` to the effect's early return to prevent it from overwriting the user's base branch selection.
apps/web/src/components/BranchToolbar.tsxlines 57–58Outdated · 807e3d1Open thread
……@@ -0,0 +1,293 @@
55+ .then(() => api!.git.checkout({ cwd: branchCwd!, branch })),
56+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),
57+ onError: (error) =>
58+ setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
MmacroscopeappBoton lines 57–58Feb 11, 2026

🟢 Low

components/BranchToolbar.tsx:57 The chained createBranch().then(checkout) in createBranchMutation can leave git state out of sync with UI if checkout fails after branch creation succeeds. Consider separating these into distinct operations or refreshing branch state in onError to stay in sync.

diff
1- onError: (error) =>
2- setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
3+ onError: (error) => {
4+ queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] });
5+ setThreadError(error instanceof Error ? error.message : "Failed to create branch.");
6+ },

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
text
1In file apps/web/src/components/BranchToolbar.tsx around lines 57-58:
2
3The chained `createBranch().then(checkout)` in `createBranchMutation` can leave git state out of sync with UI if checkout fails after branch creation succeeds. Consider separating these into distinct operations or refreshing branch state in `onError` to stay in sync.
CcoderabbitaiBotreviewed · 3 inline threadsFeb 11, 2026
Synced from GitHub

Actionable comments posted: 3

🤖 Fix all issues with AI agents
1In `@apps/desktop/turbo.jsonc`:
2- Around line 5-23: The JSONC contains trailing commas after object entries
3which cause parse errors; remove the trailing commas in the "build", "dev",
4"start", and "smoke-test" task objects (specifically the commas following the
5"outputs", "persistent", "persistent"/"cache", and "outputs" lines) so each
6object ends with a closing brace without a trailing comma and the file parses
7correctly.
8
9In `@apps/server/turbo.jsonc`:
10- Around line 5-10: The JSONC has trailing commas after the "persistent" and
11closing object entries which causes parse errors; edit the "start" task object
12in turbo.jsonc (the "start" key and its properties "dependsOn", "cache",
13"persistent") and remove the trailing commas after "persistent": true and after
14the inner object so the JSONC object closes without extra commas, then re-run
15lint/CI.
16
17In `@apps/web/turbo.jsonc`:
18- Around line 5-8: The JSONC parse errors are caused by trailing commas in the
19"dev" object; remove the trailing comma after "persistent": true and any
20trailing comma before the closing brace of the outer object so the "dev" object
21and enclosing object are valid JSONC (i.e., edit the "dev" block to eliminate
22the comma after the last property and the comma before the closing brace).
🧹 Nitpick comments (1)
apps/web/src/components/BranchToolbar.tsx (1)

44-69: Consider adding error recovery for partial branch creation failure.

In createBranchMutation, if createBranch succeeds but the chained checkout fails, the branch exists but the UI shows an error. This is acceptable UX since the user can manually select the branch, but consider whether the onError message should indicate the branch was created.

💡 Optional: More specific error message
diff
1 const createBranchMutation = useMutation({
2 mutationFn: (branch: string) =>
3 api!.git
4 .createBranch({ cwd: branchCwd!, branch })
5 .then(() => api!.git.checkout({ cwd: branchCwd!, branch })),
6 onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),
7 onError: (error) =>
8- setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
9+ setThreadError(error instanceof Error ? error.message : "Failed to create or checkout branch."),
10 });
apps/server/turbo.jsonclines 5–10Outdated · 807e3d1Open thread
……@@ -0,0 +1,11 @@
7+ "cache": false,
8+ "persistent": true,
9+ },
10+ },
CcoderabbitaiBoton lines 5–10Feb 11, 2026

⚠️ Potential issue | 🟠 Major

Remove trailing commas to fix JSONC parse errors.

Biome reports parse errors at the closing braces; the trailing commas before } are the likely cause and will break lint/CI.

✅ Suggested fix
diff
1 "tasks": {
2 "start": {
3 "dependsOn": ["build"],
4 "cache": false,
5- "persistent": true,
6- },
7- },
8+ "persistent": true
9+ }
10+ }
11 }
📝 Committable suggestion

‼️ IMPORTANT Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

suggestion
1 "start": {
2 "dependsOn": ["build"],
3 "cache": false,
4 "persistent": true
5 }
6 }
🧰 Tools
🪛 Biome (2.3.14)

[error] 9-9: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 10-10: Expected a property but instead found '}'.

Expected a property here.

(parse)

🤖 Prompt for AI Agents
1In `@apps/server/turbo.jsonc` around lines 5 - 10, The JSONC has trailing commas
2after the "persistent" and closing object entries which causes parse errors;
3edit the "start" task object in turbo.jsonc (the "start" key and its properties
4"dependsOn", "cache", "persistent") and remove the trailing commas after
5"persistent": true and after the inner object so the JSONC object closes without
6extra commas, then re-run lint/CI.
apps/desktop/turbo.jsonclines 5–23Outdated · 807e3d1Open thread
……@@ -0,0 +1,24 @@
20+ "cache": false,
21+ "outputs": [],
22+ },
23+ },
CcoderabbitaiBoton lines 5–23Feb 11, 2026

⚠️ Potential issue | 🟠 Major

Remove trailing commas to fix JSONC parse errors.

Biome reports parse errors at the closing braces; the trailing commas before } are the likely cause and will break lint/CI.

✅ Suggested fix
diff
1 "tasks": {
2 "build": {
3 "dependsOn": ["^build"],
4- "outputs": ["dist-electron/**"],
5+ "outputs": ["dist-electron/**"]
6 },
7 "dev": {
8 "dependsOn": ["t3#build"],
9- "persistent": true,
10+ "persistent": true
11 },
12 "start": {
13 "dependsOn": ["build", "@t3tools/web#build", "t3#build"],
14 "cache": false,
15- "persistent": true,
16+ "persistent": true
17 },
18 "smoke-test": {
19 "dependsOn": ["build", "@t3tools/web#build", "t3#build"],
20 "cache": false,
21- "outputs": [],
22- },
23- },
24+ "outputs": []
25+ }
26+ }
27 }
📝 Committable suggestion

‼️ IMPORTANT Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

suggestion
1 "build": {
2 "dependsOn": ["^build"],
3 "outputs": ["dist-electron/**"]
4 },
5 "dev": {
6 "dependsOn": ["t3#build"],
7 "persistent": true
8 },
9 "start": {
10 "dependsOn": ["build", "@t3tools/web#build", "t3#build"],
11 "cache": false,
12 "persistent": true
13 },
14 "smoke-test": {
15 "dependsOn": ["build", "@t3tools/web#build", "t3#build"],
16 "cache": false,
17 "outputs": []
18 }
19 }
🧰 Tools
🪛 Biome (2.3.14)

[error] 8-8: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 12-12: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 17-17: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 22-22: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 23-23: Expected a property but instead found '}'.

Expected a property here.

(parse)

🤖 Prompt for AI Agents
1In `@apps/desktop/turbo.jsonc` around lines 5 - 23, The JSONC contains trailing
2commas after object entries which cause parse errors; remove the trailing commas
3in the "build", "dev", "start", and "smoke-test" task objects (specifically the
4commas following the "outputs", "persistent", "persistent"/"cache", and
5"outputs" lines) so each object ends with a closing brace without a trailing
6comma and the file parses correctly.
apps/web/turbo.jsonclines 5–8Outdated · 807e3d1Open thread
……@@ -0,0 +1,9 @@
5+ "dev": {
6+ "persistent": true,
7+ },
8+ },
CcoderabbitaiBoton lines 5–8Feb 11, 2026

⚠️ Potential issue | 🟠 Major

Remove trailing commas to fix JSONC parse errors.

Biome reports parse errors at the closing braces; the trailing commas before } are the likely cause and will break lint/CI.

✅ Suggested fix
diff
1 "tasks": {
2 "dev": {
3- "persistent": true,
4- },
5- },
6+ "persistent": true
7+ }
8+ }
9 }
📝 Committable suggestion

‼️ IMPORTANT Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

suggestion
1 "dev": {
2 "persistent": true
3 }
4 }
🧰 Tools
🪛 Biome (2.3.14)

[error] 7-7: Expected a property but instead found '}'.

Expected a property here.

(parse)


[error] 8-8: Expected a property but instead found '}'.

Expected a property here.

(parse)

🤖 Prompt for AI Agents
1In `@apps/web/turbo.jsonc` around lines 5 - 8, The JSONC parse errors are caused
2by trailing commas in the "dev" object; remove the trailing comma after
3"persistent": true and any trailing comma before the closing brace of the outer
4object so the "dev" object and enclosing object are valid JSONC (i.e., edit the
5"dev" block to eliminate the comma after the last property and the comma before
6the closing brace).

Added 1 commit

  • Enrich branches with worktree paths, fix toolbar display, and improve HMR config8324949
CcoderabbitaiBotreviewed · 2 inline threadsFeb 11, 2026
Synced from GitHub

Actionable comments posted: 2

🤖 Fix all issues with AI agents
1In `@apps/web/src/components/BranchToolbar.tsx`:
2- Around line 51-59: The chained mutation in createBranchMutation (mutationFn
3calling api.git.createBranch then api.git.checkout) can yield partial success
4where createBranch succeeds but checkout fails, causing an incorrect "Failed to
5create branch" error and no cache refresh; change the flow so createBranch is a
6standalone operation that calls queryClient.invalidateQueries({ queryKey:
7["git", "branches", branchCwd] }) on its onSuccess, then perform checkout as a
8separate step (either a follow-up mutation or a try/catch after createBranch)
9and handle checkout errors distinctly by calling setThreadError with a message
10like "Branch created but checkout failed: <checkout error>". Make sure to
11reference the existing createBranchMutation, api!.git.createBranch,
12api!.git.checkout, onSuccess, onError, queryClient.invalidateQueries, and
13setThreadError when making these changes.
14
15In `@apps/web/vite.config.ts`:
16- Line 5: The dev port is read from process.env.PORT (const port) while the
17desktop dev script expects ELECTRON_RENDERER_PORT, causing mismatches; update
18the port resolution in the vite config (where const port is defined) to prefer
19ELECTRON_RENDERER_PORT first and then fall back to PORT and then 5173 (or switch
20both web and desktop scripts to a single agreed env name), and ensure any
21related npm scripts or docs reference the chosen env variable so Electron can
22always connect to the web dev server.
🧹 Nitpick comments (1)
apps/web/src/components/BranchToolbar.tsx (1)

194-223: Consider adding ARIA attributes for dropdown accessibility.

The branch picker dropdown button lacks aria-expanded and aria-haspopup attributes, which screen readers rely on to announce dropdown state. This is a minor accessibility improvement.

♿ Suggested ARIA attributes
diff
1 <button
2 type="button"
3 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"
4 onClick={() => setIsBranchMenuOpen((open) => !open)}
5 disabled={branchesQuery.isLoading}
6+ aria-haspopup="listbox"
7+ aria-expanded={isBranchMenuOpen}
8 >
apps/web/src/components/BranchToolbar.tsxlines 51–59Outdated · 8324949Open thread
……@@ -0,0 +1,306 @@
56+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),
57+ onError: (error) =>
58+ setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
59+ });
CcoderabbitaiBoton lines 51–59Feb 11, 2026

⚠️ Potential issue | 🟡 Minor

Chained mutation may leave inconsistent state on partial failure.

If createBranch succeeds but checkout fails (e.g., due to uncommitted changes), the error message incorrectly says "Failed to create branch" even though the branch was created. Additionally, since onSuccess won't fire, the branch list won't refresh, hiding the newly created branch from the user.

Consider separating into two distinct operations or providing accurate error messaging for partial success scenarios.

🔧 Suggested fix: separate operations or improve error handling
diff
1 const createBranchMutation = useMutation({
2- mutationFn: (branch: string) =>
3- api!.git
4- .createBranch({ cwd: branchCwd!, branch })
5- .then(() => api!.git.checkout({ cwd: branchCwd!, branch })),
6- onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),
7- onError: (error) =>
8- setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
9+ mutationFn: async (branch: string) => {
10+ await api!.git.createBranch({ cwd: branchCwd!, branch });
11+ try {
12+ await api!.git.checkout({ cwd: branchCwd!, branch });
13+ } catch (checkoutError) {
14+ // Branch was created but checkout failed - still invalidate to show new branch
15+ await queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] });
16+ throw new Error(
17+ `Branch created but checkout failed: ${checkoutError instanceof Error ? checkoutError.message : "Unknown error"}`
18+ );
19+ }
20+ },
21+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),
22+ onError: (error) =>
23+ setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
24 });
📝 Committable suggestion

‼️ IMPORTANT Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

suggestion
1 const createBranchMutation = useMutation({
2 mutationFn: async (branch: string) => {
3 await api!.git.createBranch({ cwd: branchCwd!, branch });
4 try {
5 await api!.git.checkout({ cwd: branchCwd!, branch });
6 } catch (checkoutError) {
7 // Branch was created but checkout failed - still invalidate to show new branch
8 await queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] });
9 throw new Error(
10 `Branch created but checkout failed: ${checkoutError instanceof Error ? checkoutError.message : "Unknown error"}`
11 );
12 }
13 },
14 onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),
15 onError: (error) =>
16 setThreadError(error instanceof Error ? error.message : "Failed to create branch."),
17 });
🤖 Prompt for AI Agents
1In `@apps/web/src/components/BranchToolbar.tsx` around lines 51 - 59, The chained
2mutation in createBranchMutation (mutationFn calling api.git.createBranch then
3api.git.checkout) can yield partial success where createBranch succeeds but
4checkout fails, causing an incorrect "Failed to create branch" error and no
5cache refresh; change the flow so createBranch is a standalone operation that
6calls queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd]
7}) on its onSuccess, then perform checkout as a separate step (either a
8follow-up mutation or a try/catch after createBranch) and handle checkout errors
9distinctly by calling setThreadError with a message like "Branch created but
10checkout failed: <checkout error>". Make sure to reference the existing
11createBranchMutation, api!.git.createBranch, api!.git.checkout, onSuccess,
12onError, queryClient.invalidateQueries, and setThreadError when making these
13changes.
apps/web/vite.config.tsline 5Outdated · 8324949Open thread
……@@ -2,6 +2,8 @@ import tailwindcss from "@tailwindcss/vite";
22import react from "@vitejs/plugin-react";
33import { defineConfig } from "vite";
44
5+const port = Number(process.env.PORT ?? 5173);

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

shell
1#!/bin/bash
2# Search for PORT and ELECTRON_RENDERER_PORT usage patterns
3
4echo "=== Web app vite.config.ts port usage ==="
5grep -n "PORT\|port" apps/web/vite.config.ts | head -20
6
7echo ""
8echo "=== Desktop dev script ELECTRON_RENDERER_PORT usage ==="
9grep -n "ELECTRON_RENDERER_PORT\|port" apps/desktop/scripts/dev-electron.mjs | head -20
10
11echo ""
12echo "=== Search all usages of PORT env var ==="
13rg -n "process\.env\.PORT" --type ts --type js -A 2 -B 2
14
15echo ""
16echo "=== Search all usages of ELECTRON_RENDERER_PORT ==="
17rg -n "ELECTRON_RENDERER_PORT" --type ts --type js --type mjs -A 2 -B 2
18
19echo ""
20echo "=== Check for any .env files or examples ==="
21fd "\.env" . -type f

Repository: pingdotgg/codething-mvp

Length of output: 1296


🏁 Script executed:

shell
1#!/bin/bash
2# Check for coordinated dev scripts and documentation
3
4echo "=== Root package.json dev scripts ==="
5cat package.json | grep -A 20 '"scripts"' | head -30
6
7echo ""
8echo "=== Check for development documentation ==="
9find . -maxdepth 2 -name "*.md" -type f | xargs grep -l "PORT\|port\|dev" 2>/dev/null | head -5
10
11echo ""
12echo "=== List all dev-related scripts in monorepo ==="
13fd "package.json" | xargs grep -l "\"dev\"" 2>/dev/null
14
15echo ""
16echo "=== Check apps/web package.json ==="
17cat apps/web/package.json | grep -A 5 '"scripts"' 2>/dev/null || echo "Not found"
18
19echo ""
20echo "=== Check apps/desktop package.json ==="
21cat apps/desktop/package.json | grep -A 5 '"scripts"' 2>/dev/null || echo "Not found"

Repository: pingdotgg/codething-mvp

Length of output: 2056


Align port environment variable names between web and desktop dev scripts.

The web app uses PORT while the desktop dev script uses ELECTRON_RENDERER_PORT. Both default to 5173, so they match in typical usage, but if someone sets only one to a custom value, the Electron app will fail to connect to the dev server. Either use a single env var name across both or document the relationship clearly.

🤖 Prompt for AI Agents
1In `@apps/web/vite.config.ts` at line 5, The dev port is read from
2process.env.PORT (const port) while the desktop dev script expects
3ELECTRON_RENDERER_PORT, causing mismatches; update the port resolution in the
4vite config (where const port is defined) to prefer ELECTRON_RENDERER_PORT first
5and then fall back to PORT and then 5173 (or switch both web and desktop scripts
6to a single agreed env name), and ensure any related npm scripts or docs
7reference the chosen env variable so Electron can always connect to the web dev
8server.

Added 4 commits

  • Fix session not restarting when switching to a worktree branch5b7af7b
  • Fix worktree→local branch switch causing 'already used by worktree' errord6ecf15
  • Fix flash of 'Initialize git' when switching branch/worktreecf66f53
  • Filter out stale worktree paths from branch listing9442019
Ppatrozareviewed · 7 inline threadsJul 13, 2026
Synced from GitHub

Review: Git integration — branch picker + worktrees

Solid feature with strong test coverage (423 lines of real-git integration tests) and thoughtful edge-case handling — stale worktree filtering, worktree redirects, session restart on cwd change. A few issues worth addressing:

High

  • as never bypasses all input validation — the zod schemas in packages/contracts/src/git.ts are well-defined but every git WS handler casts request.params as never, skipping validation entirely. Unvalidated branch names with git metacharacters (e.g. leading -, --) could cause argument injection. Should use the schema .parse() methods.

Medium

  • Worktree path collision — path.basename(input.cwd) produces colliding paths for repos with the same basename (e.g. ~/work/t3code and ~/play/t3code).
  • Orphaned worktrees on failure — worktree is created and dispatched to store, but if ensureSession/sendTurn then fails, the worktree directory is left on disk with no cleanup.
  • isRepo logic inverted from stated intent — undefined ?? !isLoading evaluates to false on first load, contradicting the comment that says it defaults to true.

Low / Hygiene

  • codething/ hardcoded prefix for generated worktree branch names.
  • branchCwd! non-null assertions in mutations — query is gated by enabled: !!branchCwd but mutations don't re-check.
  • runTerminalCommand (general-purpose shell executor) left in git.ts — only used by test helper now, latent footgun if reused.
  • 4 .plans/ markdown files (391 lines) are working artifacts — should be gitignored.
  • Build/tooling churn (tsup→tsdown, vitest 3→4, turbo task graph iterations) mixed into feature PR — would be cleaner as separate PRs.

Strengths

  • Shell-free runGit (argv spawn) — eliminates quoting/injection surface.
  • SET_THREAD_BRANCH nulls session on cwd change + stops running session first.
  • Stale worktree filtering via existsSync.
  • Persistence schema uses .optional() with ?? null defaults — backward-compatible migration.
  • Theme-aware semantic tokens replace hardcoded dark-mode colors.
……@@ -0,0 +1,315 @@
39+ const branches = branchesQuery.data?.branches ?? [];
40+ // Default to true while loading — showing "Initialize git" during a fetch is wrong,
41+ // and worktrees are inherently git repos.
42+ const isRepo = branchesQuery.data?.isRepo ?? !branchesQuery.isLoading;
Ppatrozaon lines 40–42Jul 13, 2026

Medium: isRepo logic is inverted from the stated intent

The comment says "Default to true while loading," but the expression doesn't do that:

ts
1const isRepo = branchesQuery.data?.isRepo ?? !branchesQuery.isLoading;

On a cold first load, data is undefined and isLoading is true, so undefined ?? false = false — the "Initialize git" button flashes even for valid repos.

The intent (per the comment) should be:

ts
1const isRepo = branchesQuery.data?.isRepo ?? true;

Additionally, the "Initialize git" button is not disabled during query loading (disabled={!branchCwd || initMutation.isPending}), so a user could click git init on an already-initialized repo during the brief loading window.

apps/server/src/wsServer.tslines 315–331Open thread
……@@ -304,6 +312,24 @@ export function createServer(options: ServerOptions) {
328+ return checkoutGitBranch(request.params as never);
329+
330+ case WS_METHODS.gitInit:
331+ return initGitRepo(request.params as never);
Ppatrozaon lines 315–331Jul 13, 2026

High: Schemas defined but never enforced

The zod schemas in packages/contracts/src/git.ts (gitListBranchesInputSchema, gitCreateWorktreeInputSchema, etc.) are well-defined but purely decorative — every git handler casts request.params as never, skipping validation entirely.

While the shell-free runGit prevents shell injection, unvalidated branch names with git metacharacters (e.g. --, leading -, ..) could cause argument injection into git commands. For example, a branch name like --upload-pack passed to git checkout could trigger unexpected behavior.

Should be:

ts
1case WS_METHODS.gitListBranches:
2 return listGitBranches(gitListBranchesInputSchema.parse(request.params));
3case WS_METHODS.gitCreateWorktree:
4 return createGitWorktree(gitCreateWorktreeInputSchema.parse(request.params));
5// ... etc.

The contracts package exists specifically for this — the schemas should be the enforcement boundary.

apps/web/src/components/ChatView.tsxlines 448–470Open thread
……@@ -446,6 +435,39 @@ export default function ChatView() {
467+ });
468+ return;
469+ }
470+ }
Ppatrozaon lines 448–470Jul 13, 2026

Medium: Orphaned worktree on downstream failure

The worktree is created and dispatched to the store here, but if ensureSession (line 496) or sendTurn subsequently fails, the worktree directory remains on disk with no cleanup. There's a removeGitWorktree IPC method but it's never called on any error path.

Consider wrapping the entire send flow (worktree creation → ensureSession → sendTurn) in a try/catch that rolls back the worktree:

ts
1} catch (err) {
2 if (sessionCwd) {
3 await api.git.removeWorktree({ cwd: activeProject.cwd, path: sessionCwd }).catch(() => {});
4 }
5 dispatch({ type: 'SET_ERROR', ... });
6}

Also — this ~30 lines of branch-naming + worktree creation + dispatch + error handling is embedded directly in handleSend. Per AGENTS.md's maintainability guidance, it should be extracted into a prepareWorktreeSession() helper.

apps/server/src/git.tslines 177–179Open thread
……@@ -0,0 +1,229 @@
176+ const sanitizedBranch = input.newBranch.replace(/\//g, "-");
177+ const repoName = path.basename(input.cwd);
178+ const worktreePath =
179+ input.path ?? path.join(os.homedir(), ".t3", "worktrees", repoName, sanitizedBranch);
Ppatrozaon lines 177–179Jul 13, 2026

Medium: Worktree path collision on identical repo basenames

path.basename(input.cwd) extracts only the final directory component. Two repos with the same basename (e.g. ~/work/t3code and ~/projects/t3code) produce colliding worktree paths:

1~/.t3/worktrees/t3code/<branch> // both repos write here

The second repo's git worktree add would fail with "already exists" or silently reuse the first repo's worktree.

Consider incorporating a short hash of the full cwd:

ts
1const repoHash = crypto.createHash('sha1').update(input.cwd).digest('hex').slice(0, 8);
2const worktreePath = input.path ?? path.join(os.homedir(), '.t3', 'worktrees', `${repoName}-${repoHash}`, sanitizedBranch);
……@@ -0,0 +1,315 @@
61+ });
62+
63+ const initMutation = useMutation({
64+ mutationFn: () => api!.git.init({ cwd: branchCwd! }),
Ppatrozaon lines 46–64Jul 13, 2026

Low: branchCwd! non-null assertions in mutations

The query is gated with enabled: !!api && !!branchCwd, but the mutations all use branchCwd! without re-checking. If activeProject?.cwd is undefined (e.g. project not yet loaded), the mutation sends cwd: undefined to the server, which would fail or behave unexpectedly.

Consider guarding each mutation with an early return:

ts
1mutationFn: (branch: string) => {
2 if (!branchCwd) throw new Error('No cwd');
3 return api!.git.checkout({ cwd: branchCwd, branch });
4},
……@@ -446,6 +435,39 @@ export default function ChatView() {
446+ !activeThread.worktreePath
447+ ) {
448+ try {
449+ const newBranch = `codething/${crypto.randomUUID().slice(0, 8)}`;
Ppatrozaon line 449Jul 13, 2026

Low: Hardcoded codething/ prefix

This is the legacy project name. It should be a named constant or configurable, especially since the rest of the codebase uses "t3" branding (~/.t3/worktrees/...).

apps/server/src/git.tsline 57Open thread
……@@ -0,0 +1,229 @@
54+ });
55+}
56+
57+export async function runTerminalCommand(
Ppatrozaon line 57Jul 13, 2026

Low: runTerminalCommand left in git.ts

This general-purpose shell executor (spawn with -lc / /d /s /c) sits alongside the shell-free runGit. It's only used by the test helper now (git.test.ts). Shipping a shell-execution function in the git module is a latent footgun — it could easily be reused for user-facing operations, reintroducing the shell quoting/injection surface that runGit was specifically created to eliminate.

Consider moving it to a separate shell.ts module or making it test-only (not exported from the production module).