Skip to content

feat: add --worktree flag toggle for Claude Code#578

Open
ashalliants wants to merge 1 commit intositeboon:mainfrom
ashalliants:feat/worktree-toggle
Open

feat: add --worktree flag toggle for Claude Code#578
ashalliants wants to merge 1 commit intositeboon:mainfrom
ashalliants:feat/worktree-toggle

Conversation

@ashalliants
Copy link
Copy Markdown

@ashalliants ashalliants commented Mar 24, 2026

Summary

  • Adds a toggleable worktree isolation option for Claude Code sessions (--worktree flag and SDK settings.worktree)
  • Detects and groups git worktrees as related projects in the sidebar with branch badges, expandable repo headers, and stale worktree detection
  • Passes CLI flags (--worktree, --dangerously-skip-permissions) through to the shell/PTY provider, with safety guards to avoid creating duplicate worktrees on resume
  • Adds worktree cleanup on project delete with a confirmation warning
  • Adds a resizable sidebar, visible new-session button, and simpler loading state
  • Fixes a bug where the SDK crashes with "nested session" error when the server is started from within a Claude Code session or PM2 inherits the CLAUDECODE env var

Changes

Worktree Toggle (Settings & SDK):

  • Added useWorktree: boolean to ClaudePermissionsState and ClaudeSettings interfaces
  • Updated getClaudeSettings() defaults and parsing to include useWorktree
  • Maps useWorktree to SDK settings: { worktree: {} } in mapCliOptionsToSDK()
  • UI toggle in Claude permissions panel with GitBranch icon (blue info styling)

Sidebar Worktree Grouping:

  • Detects worktree paths (/.claude/worktrees/) and groups them under their parent repo
  • SidebarRepoGroup component with expandable headers, branch badges, and group actions
  • Stale worktree detection and visual hierarchy improvements
  • New sidebar i18n keys for all 6 locales

Shell/PTY CLI Flags:

  • Builds claude --worktree --dangerously-skip-permissions flag string from claudeSettings
  • Only passes --worktree for new sessions and when not already inside a worktree directory
  • Worktree cleanup via git worktree remove when deleting a worktree project, with confirmation dialog

Resizable Sidebar & UX:

  • Sidebar width is draggable and persisted
  • New-session button always visible
  • Simpler loading state

SDK Nested-Session Fix:

  • Clears CLAUDECODE env var in server/load-env.js at startup
  • Prevents the Claude Agent SDK from refusing to spawn CLI subprocesses when the server inherits this variable from a parent Claude Code session or PM2

i18n:

  • Added permissions.useWorktree.label and permissions.useWorktree.description keys for all 6 locales (en, de, ja, ko, ru, zh-CN)
  • Added sidebar worktree grouping keys for all locales

Testing:

  • Set up vitest as the testing framework
  • Tests for mapCliOptionsToSDK (worktree mapping + existing SDK option behavior)
  • Tests for getClaudeSettings (localStorage parsing including useWorktree defaults)
  • Tests for worktree grouping logic
  • i18n locale completeness tests

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Git worktree isolation option for Claude Code execution across all settings.
    • Redesigned sidebar with repository grouping, linked worktree support, and improved session aggregation.
    • Made desktop sidebar resizable with persistent width settings.
    • Added per-session worktree configuration in chat composer.
  • Improvements

    • Enhanced project discovery with automatic worktree detection and organization.
    • Improved branch switching with automatic UI updates.
    • Added worktree awareness to search and filtering.
  • Internationalization

    • Added localization support for worktree features across six languages (English, German, Japanese, Korean, Russian, Simplified Chinese).
  • Tests

    • Added comprehensive test coverage for worktree functionality and localization validation.

Test plan

  • Toggle worktree on, start a new session - verify it creates a worktree
  • Resume an existing worktree session - verify no duplicate worktree created
  • Delete a worktree project - verify cleanup dialog and git worktree remove
  • Sidebar groups worktree projects under parent repo correctly
  • Stale worktree indicator appears for orphaned worktrees
  • Resize sidebar, refresh - width persists
  • Start server via PM2 from inside a Claude Code session - SDK chat works (no nested-session crash)
  • npm test passes all vitest suites

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 24, 2026

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

Walkthrough

This PR adds git worktree isolation support with a major sidebar redesign. It introduces worktree detection and project grouping by repository, new UI components for displaying branches and worktrees, a useWorktree boolean setting across the Claude configuration, improved project discovery with worktree metadata, and comprehensive multi-language i18n updates.

Changes

Cohort / File(s) Summary
Settings & Types
src/components/chat/types/types.ts, src/components/settings/types/types.ts, src/components/shell/types/types.ts, src/types/app.ts, src/types/global.d.ts
Added useWorktree: boolean to ClaudeSettings and ClaudePermissionsState; extended ShellInitMessage with optional claudeSettings object; introduced new WorktreeInfo and RepoGroup types; added worktreeInfo, repoGroup, repoGroupSize, isMainWorktree, and isStale optional properties to Project; added updateProjectBranch optional function to Window interface.
Chat Settings & Storage
src/components/chat/utils/chatStorage.ts, src/components/settings/hooks/useSettingsController.ts, src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx, src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx
Updated getClaudeSettings() to include useWorktree field with default/fallback handling; extended settings persistence to track useWorktree; added UI checkbox and text input in permissions section for worktree isolation and name configuration.
Server Worktree & Project Discovery
server/projects.js, server/routes/git.js
Added worktree awareness during project discovery: getWorktreeInfo() helper detects linked worktrees; synthesizes worktree info from path convention; groups projects by mainRepoRoot; adds repoGroup, repoGroupSize, isMainWorktree, isStale annotations; normalizes display names via fixWorktreeDisplayNames; updates deletion to clean up git worktrees; exports worktree synthesis and cleanup helpers. Added getWorktreeInfo() export to validate checkout targets and reject linked worktrees.
SDK & CLI Mapping
server/claude-sdk.js
Extended mapCliOptionsToSDK to conditionally inject SDK settings.worktree when useWorktree is truthy, constructing { name: String(worktreeName) } when provided; exported mapCliOptionsToSDK function.
Server Index & Command Construction
server/index.js, server/load-env.js
Updated Claude command construction to derive claudeFlags from claudeSettings including --worktree with optional name for new sessions; injected flags into both new and resumed session commands (including PowerShell fallback); prevented CLAUDECODE environment variable from persisting via .env.
Shell & Session Initialization
src/components/shell/hooks/useShellConnection.ts
Extended WebSocket init message to include claudeSettings object with skipPermissions and useWorktree fields derived from getClaudeSettings().
Chat Composer & UI Wiring
src/components/chat/hooks/useChatComposerState.ts, src/components/chat/view/ChatInterface.tsx, src/components/chat/view/subcomponents/ChatMessagesPane.tsx, src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
Added per-session worktree state (useWorktreeForSession, worktreeName) to composer state; reset worktree controls when project changes; apply worktree settings only for new sessions; exposed worktree fields/setters in hook return and threaded through component hierarchy; added UI section in provider selection with checkbox and optional name input.
Sidebar Type & Utility System
src/components/sidebar/types/types.ts, src/components/sidebar/utils/utils.ts, src/components/sidebar/utils/repoAggregates.ts
Introduced RepoGroup type with discriminant __type: 'repo-group'; added groupProjectsByRepo() function to organize projects and groups; added isRepoGroup() type guard; added repo-level session aggregation (getRepoSessions, getRepoSessionTotal) and deterministic branch color mapping (branchChipColorIndex); extended search in filterProjects to match branch names.
Sidebar New Components
src/components/sidebar/view/subcomponents/BranchChip.tsx, src/components/sidebar/view/subcomponents/NewSessionRow.tsx, src/components/sidebar/view/subcomponents/RecentSessions.tsx, src/components/sidebar/view/subcomponents/RepoCard.tsx, src/components/sidebar/view/subcomponents/WorktreeRow.tsx
Added BranchChip component rendering branch with deterministic color and icon; added NewSessionRow button component; added RecentSessions collapsible section showing recent cross-repo sessions sorted by activity; added RepoCard collapsible repo section with main project, session rows, and worktree subsection; added WorktreeRow component for individual worktree display with activity label and actions.
Sidebar Removed Components
src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx, src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx, src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx
Deleted legacy SidebarProjectItem, SidebarProjectSessions, and SidebarSessionItem components; replaced with new repo-card-based rendering.
Sidebar Updated Components
src/components/sidebar/view/Sidebar.tsx, src/components/sidebar/view/subcomponents/SidebarContent.tsx, src/components/sidebar/view/subcomponents/SidebarProjectList.tsx, src/components/sidebar/view/subcomponents/SidebarModals.tsx, src/components/sidebar/view/subcomponents/SidebarProjectsState.tsx
Updated SidebarProjectList to use groupProjectsByRepo() and render RepoCard instead of SidebarProjectItem; simplified props by removing editing/session/star handlers; forwarded additionalSessions into RepoCard; added worktree deletion warning in confirmation modal; removed numeric progress display in loading UI; removed fixed width styling; threaded additionalSessions through component tree.
App Layout & Project State
src/components/app/AppContent.tsx, src/hooks/useProjectsState.ts, src/components/git-panel/hooks/useGitPanelController.ts
Added resizable sidebar width persistence via localStorage with mouse drag and double-click reset; added updateProjectBranch() callback to sync worktree branch name changes; updated branch switch error handling and added fetchBranches() call after successful checkout; wired updateProjectBranch to global window function.
Internationalization - Settings
src/i18n/locales/{en,de,ja,ko,ru,zh-CN}/settings.json
Added permissions.useWorktree translation entries with label and description across all supported locales describing git worktree isolation and --worktree flag equivalence.
Internationalization - Sidebar
src/i18n/locales/{en,de,ja,ko,ru,zh-CN}/sidebar.json
Added extensive translation keys for worktree/repo grouping UI: branch, repo groups, worktree sections, archived/stale/empty states, session counts (pluralized), pagination, and tooltips for new session in worktree and delete worktree actions; added deleteConfirmation.worktreeWarning key.
Internationalization - Chat
src/i18n/locales/en/chat.json
Added providerSelection.worktree localization group with checkbox label, description, and name input placeholder for session-level worktree options.
Testing - SDK & Storage
server/__tests__/claude-sdk.test.js, src/components/chat/utils/__tests__/chatStorage.test.ts
Added comprehensive Vitest test suite for mapCliOptionsToSDK function covering default behavior, model overrides, permission modes, and conditional worktree settings injection; added test suite for getClaudeSettings() covering default values, JSON parsing, useWorktree coercion, and fallback handling.
Testing - Worktree & Repo
server/__tests__/worktree-grouping.test.js, src/components/sidebar/utils/__tests__/repoAggregates.test.ts
Added test suite for synthesizeWorktreeInfoFromPath and fixWorktreeDisplayNames helpers; added test suite for getRepoSessions, getRepoSessionTotal, and branchChipColorIndex utilities validating session aggregation, total computation, and deterministic color mapping.
Testing - i18n Validation
src/i18n/__tests__/locales.test.ts
Added Vitest test suite validating locale file completeness and structure for all supported languages, ensuring required permission keys exist and contain valid label/description strings.
Build & Configuration
package.json, vitest.config.js
Added test and test:watch scripts via Vitest; added vitest ^4.1.1 to devDependencies; configured Vitest with global test APIs enabled.
Documentation
docs/superpowers/plans/2026-04-25-sidebar-worktree-redesign.md, docs/superpowers/specs/2026-04-25-sidebar-worktree-redesign-design.md
Added comprehensive implementation plan documenting new utilities, components, and migration steps for sidebar redesign; added design specification detailing RepoCard layout, RECENT/WORKTREES sections, branch chips, stale worktree styling, and acceptance criteria.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Chat/Settings UI
    participant ComposerState as useChatComposerState
    participant Shell as useShellConnection
    participant Server as server/index.js
    participant SDK as server/claude-sdk.js
    participant Git as git commands

    User->>UI: Enable worktree option & enter worktree name
    UI->>ComposerState: updateUseWorktreeForSession(), setWorktreeName()
    User->>UI: Submit new session
    ComposerState->>ComposerState: Build claudeToolsSettings with useWorktree flag
    ComposerState->>Shell: Send message with claudeSettings
    Shell->>Server: WebSocket init with claudeSettings {useWorktree, skipPermissions}
    Server->>SDK: mapCliOptionsToSDK(cliOptions, claudeSettings)
    SDK->>SDK: Build sdkOptions.settings.worktree = {name: worktreeName}
    Server->>Git: git worktree add || git checkout
    Git-->>Server: Success
    Server->>User: Session initialized in isolated git worktree
Loading
sequenceDiagram
    participant Server as server/projects.js
    participant Git as git commands
    participant FS as File System
    participant Sidebar as SidebarProjectList

    Server->>Git: Discover projects in workspace
    Server->>Git: getWorktreeInfo(projectPath)
    Git-->>Server: {isWorktree, mainRepoRoot, branchName, worktreeRoot}
    Server->>FS: fs.access() to check if stale
    Server->>Server: Group by mainRepoRoot
    Server->>Server: Synthesize parent "main" project if needed
    Server->>Server: Annotate with repoGroup, repoGroupSize, isMainWorktree
    Server-->>Sidebar: Projects with worktreeInfo & repo grouping
    Sidebar->>Sidebar: groupProjectsByRepo(projects)
    Sidebar->>Sidebar: Render RepoCard per group
    Sidebar->>Sidebar: Show branch chips & worktree rows
Loading

Possibly Related PRs

Suggested Reviewers

  • blackmammoth
  • viper151

Poem

🐰 Worktrees bloom, branches split and branch again,
Repos grouped in harmony, no more tangled den!
Sidebars dance with fresh cards and sessions bright,
The warren's redesigned—a beautiful sight! ✨🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature: adding a --worktree flag toggle for Claude Code. It directly reflects the central objective of this large changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/i18n/locales/ru/settings.json (1)

335-338: Localize the new RU strings to avoid mixed-language UI.

The key structure is correct, but this introduces English copy in the Russian locale and causes inconsistent UX in the settings panel.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/ru/settings.json` around lines 335 - 338, The Russian locale
contains English text for the "useWorktree" setting; update the
"useWorktree.label" and "useWorktree.description" values in
src/i18n/locales/ru/settings.json to proper Russian translations (replace the
current English strings for the keys "useWorktree.label" and
"useWorktree.description") so the settings panel no longer mixes languages and
displays fully localized RU text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/i18n/locales/ko/settings.json`:
- Around line 356-359: The Korean locale currently contains English placeholders
for the new useWorktree keys; update the useWorktree object by translating the
"label" and "description" into Korean (i.e., replace the English strings for
useWorktree.label and useWorktree.description with appropriate Korean
translations) so the UI is fully localized.

---

Nitpick comments:
In `@src/i18n/locales/ru/settings.json`:
- Around line 335-338: The Russian locale contains English text for the
"useWorktree" setting; update the "useWorktree.label" and
"useWorktree.description" values in src/i18n/locales/ru/settings.json to proper
Russian translations (replace the current English strings for the keys
"useWorktree.label" and "useWorktree.description") so the settings panel no
longer mixes languages and displays fully localized RU text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 97ed8830-1a0f-448f-beb3-5dacd26d6b9e

📥 Commits

Reviewing files that changed from the base of the PR and between b54cdf8 and 162902f.

📒 Files selected for processing (13)
  • server/claude-sdk.js
  • src/components/chat/types/types.ts
  • src/components/chat/utils/chatStorage.ts
  • src/components/settings/hooks/useSettingsController.ts
  • src/components/settings/types/types.ts
  • src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx
  • src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx
  • src/i18n/locales/de/settings.json
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/ja/settings.json
  • src/i18n/locales/ko/settings.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/zh-CN/settings.json

Comment thread src/i18n/locales/ko/settings.json
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/components/chat/utils/__tests__/chatStorage.test.ts (1)

4-12: Consider making clear consistent with other mock methods.

The clear method is a plain function while getItem, setItem, and removeItem use vi.fn(). This doesn't affect test functionality but could be made consistent if you want to assert on clear calls in future tests.

🔧 Optional: Make clear a vi.fn() for consistency
 const localStorageMock = (() => {
   let store: Record<string, string> = {};
   return {
     getItem: vi.fn((key: string) => store[key] ?? null),
     setItem: vi.fn((key: string, value: string) => { store[key] = value; }),
     removeItem: vi.fn((key: string) => { delete store[key]; }),
-    clear: () => { store = {}; },
+    clear: vi.fn(() => { store = {}; }),
   };
 })();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/chat/utils/__tests__/chatStorage.test.ts` around lines 4 - 12,
The localStorageMock's clear method is a plain function while
getItem/setItem/removeItem use vi.fn(); make clear consistent by replacing its
implementation with a spy: change clear to vi.fn(() => { store = {}; }) while
keeping the same closure over store so tests can now assert calls on
localStorageMock.clear just like they do for localStorageMock.getItem, setItem,
and removeItem.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/claude-sdk.js`:
- Around line 174-177: The current code sets sdkOptions.worktree = {} which is
not a valid ClaudeAgentOptions field; instead, when settings.useWorktree is
true, remove the invalid assignment and configure the agent to use isolation and
the worktree tools/events: set the agent option isolation: "worktree" (e.g.,
when constructing the agent options object), and register/use the built-in tools
EnterWorktree and ExitWorktree plus hooks/events WorktreeCreate and
WorktreeRemove for any custom VCS setup, so replace references to
sdkOptions.worktree with these correct mechanisms (check where sdkOptions is
assembled and where the agent is created to add isolation:"worktree" and
hook/tool registration).

In `@src/i18n/__tests__/locales.test.ts`:
- Line 5: The test uses the LOCALES_DIR constant which relies on __dirname but
__dirname isn't defined in ES modules; add the standard ES-module definition at
the top of this test file by importing fileURLToPath from 'url' and dirname from
'path', then define const __dirname = dirname(fileURLToPath(import.meta.url));
so LOCALES_DIR = path.resolve(__dirname, '../locales') works as expected; place
these imports/definition before the LOCALES_DIR declaration to mirror the
pattern used in other files.

---

Nitpick comments:
In `@src/components/chat/utils/__tests__/chatStorage.test.ts`:
- Around line 4-12: The localStorageMock's clear method is a plain function
while getItem/setItem/removeItem use vi.fn(); make clear consistent by replacing
its implementation with a spy: change clear to vi.fn(() => { store = {}; })
while keeping the same closure over store so tests can now assert calls on
localStorageMock.clear just like they do for localStorageMock.getItem, setItem,
and removeItem.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 18456a1e-3581-4510-bbe4-f932fcf4040c

📥 Commits

Reviewing files that changed from the base of the PR and between 162902f and 3b8775a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • package.json
  • server/__tests__/claude-sdk.test.js
  • server/claude-sdk.js
  • src/components/chat/utils/__tests__/chatStorage.test.ts
  • src/i18n/__tests__/locales.test.ts
  • vitest.config.js

Comment thread server/claude-sdk.js Outdated
Comment thread src/i18n/__tests__/locales.test.ts
@ashalliants ashalliants force-pushed the feat/worktree-toggle branch from 08278ed to 8707776 Compare April 8, 2026 20:46
@ashalliants
Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/i18n/__tests__/locales.test.ts (1)

14-19: Expand locale key checks to cover nested fields used by PermissionsContent.

Current assertions only validate object presence for skipPermissions, allowedTools, and blockedTools. The UI reads deeper keys (e.g., labels/descriptions/placeholders), so missing translations can still ship undetected.

Also applies to: 43-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/__tests__/locales.test.ts` around lines 14 - 19, The test currently
only asserts presence of top-level keys in REQUIRED_PERMISSION_KEYS but must
also verify nested translation fields that PermissionsContent reads (e.g.,
labels, descriptions, placeholders, and any enum/item labels under
'allowedTools'/'blockedTools'); update the test in
src/i18n/__tests__/locales.test.ts to iterate each locale and for each key in
REQUIRED_PERMISSION_KEYS assert the presence and non-empty string values for the
deeper properties referenced by PermissionsContent (explicitly check label,
description, placeholder and any per-tool display names), and apply the same
deeper checks for the other assertions around lines where the current permission
keys are validated (the block covering the other assertions referenced in the
comment).
server/index.js (1)

1800-1801: Consider using a more robust worktree path detection.

The string check projectPath.includes('/.claude/worktrees/') works for the standard Claude worktree location but could miss edge cases on Windows (backslashes) or non-standard configurations.

🔧 Optional: Use path-based check for cross-platform support
-const isAlreadyInWorktree = projectPath.includes('/.claude/worktrees/');
+const isAlreadyInWorktree = projectPath.includes(`${path.sep}.claude${path.sep}worktrees${path.sep}`) ||
+                            projectPath.includes('/.claude/worktrees/');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/index.js` around lines 1800 - 1801, The current detection uses a raw
string include on projectPath (isAlreadyInWorktree) which fails on
Windows/backslashes or non-standard locations; update the check to normalize and
inspect path segments instead: require('path'), use path.normalize(projectPath)
or split path by path.sep and verify that the segment sequence
['.claude','worktrees'] appears, then derive isAlreadyInWorktree from that
normalized/segment-based test and keep shouldCreateWorktree as
claudeSettings.useWorktree && !hasSession && !isAlreadyInWorktree so the logic
uses cross-platform path checks (referencing projectPath, isAlreadyInWorktree,
shouldCreateWorktree, claudeSettings.useWorktree, hasSession).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/load-env.js`:
- Around line 11-16: The deletion of CLAUDECODE (delete process.env.CLAUDECODE)
is being undone by the .env loader; move the removal to after the environment is
loaded (or remove CLAUDECODE from the parsed .env object before merging) so the
variable cannot be reintroduced. Locate the delete process.env.CLAUDECODE
statement and ensure it runs after dotenv/config or whatever env-loading call is
used (also apply the same change for the other occurrence around the second
block at lines 23-25), or strip CLAUDECODE from the result of the env loader
before assigning to process.env.

In `@server/projects.js`:
- Around line 1271-1302: The cleanupWorktreeDirectory function currently calls
execFileAsync('git', ['worktree', 'remove', worktreePath, '--force'], ...) which
can discard uncommitted/untracked changes and swallows errors; update it to
first verify mainRepoRoot is a git repo (e.g. run execFileAsync('git',
['rev-parse', '--show-toplevel'], {cwd: mainRepoRoot}) and bail if that fails),
then attempt git worktree remove without '--force' and only fall back to
'--force' if the first remove fails and you log the specific error;
additionally, in both the outer catch (around execFileAsync removal) and the
inner catch (around fs.rm and prune), log detailed errors including worktreePath
and mainRepoRoot via console.error or processLogger (referencing
cleanupWorktreeDirectory, execFileAsync, fs.rm, and git worktree prune) instead
of silently ignoring them so failures are visible for debugging.
- Around line 698-705: When creating manual project entries, add the same
fallback worktree synthesis used for Claude-discovered projects whenever
getWorktreeInfo(actualProjectDir) returns null (i.e., after the try/catch that
sets project.worktreeInfo: if null, call the same synthesize/fallback routine
used for Claude-discovered projects to populate project.worktreeInfo so deleted
worktrees still group with their parent). Also initialize project.isStale on the
manual project object (set project.isStale = false or the same initial value
used for Claude-discovered projects) before pushing the project to projects to
ensure consistent behavior with Claude-discovered entries.

In `@src/i18n/__tests__/locales.test.ts`:
- Around line 24-29: The current loop that reads and JSON.parses locale files at
module load time (using SUPPORTED_LOCALES, LOCALES_DIR, and populating
localeData) causes the test suite to crash before assertions if a settings.json
is missing/invalid; move the file I/O into test runtime (e.g., within each test
case or a beforeEach) and wrap reads/parses in existence checks and try/catch so
the tests can assert presence/validity (for example, check
fs.existsSync(filePath) and then JSON.parse inside a try block) instead of
parsing during suite definition.
- Around line 45-47: The test dereferences nested properties directly
(localeData[locale].permissions and permissions[key]) which can throw TypeError
if a parent key is missing; before asserting about permissions[key] in the test
(variables: localeData, locale, permissions, key), first assert that
localeData[locale] is defined and that permissions (e.g., const permissions =
(localeData[locale] as ...).permissions) is defined and is an object, then
perform the existing assertions about permissions[key]; apply the same guarding
pattern to the similar block referenced around lines 52-58 so the tests fail
with clear assertion messages rather than throwing.

---

Nitpick comments:
In `@server/index.js`:
- Around line 1800-1801: The current detection uses a raw string include on
projectPath (isAlreadyInWorktree) which fails on Windows/backslashes or
non-standard locations; update the check to normalize and inspect path segments
instead: require('path'), use path.normalize(projectPath) or split path by
path.sep and verify that the segment sequence ['.claude','worktrees'] appears,
then derive isAlreadyInWorktree from that normalized/segment-based test and keep
shouldCreateWorktree as claudeSettings.useWorktree && !hasSession &&
!isAlreadyInWorktree so the logic uses cross-platform path checks (referencing
projectPath, isAlreadyInWorktree, shouldCreateWorktree,
claudeSettings.useWorktree, hasSession).

In `@src/i18n/__tests__/locales.test.ts`:
- Around line 14-19: The test currently only asserts presence of top-level keys
in REQUIRED_PERMISSION_KEYS but must also verify nested translation fields that
PermissionsContent reads (e.g., labels, descriptions, placeholders, and any
enum/item labels under 'allowedTools'/'blockedTools'); update the test in
src/i18n/__tests__/locales.test.ts to iterate each locale and for each key in
REQUIRED_PERMISSION_KEYS assert the presence and non-empty string values for the
deeper properties referenced by PermissionsContent (explicitly check label,
description, placeholder and any per-tool display names), and apply the same
deeper checks for the other assertions around lines where the current permission
keys are validated (the block covering the other assertions referenced in the
comment).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2ffc08c0-1c67-4361-bfdd-a11361cc96fd

📥 Commits

Reviewing files that changed from the base of the PR and between 6ade4cf and 8707776.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (44)
  • package.json
  • server/__tests__/claude-sdk.test.js
  • server/__tests__/worktree-grouping.test.js
  • server/claude-sdk.js
  • server/index.js
  • server/load-env.js
  • server/projects.js
  • server/routes/git.js
  • src/components/app/AppContent.tsx
  • src/components/chat/types/types.ts
  • src/components/chat/utils/__tests__/chatStorage.test.ts
  • src/components/chat/utils/chatStorage.ts
  • src/components/git-panel/hooks/useGitPanelController.ts
  • src/components/settings/hooks/useSettingsController.ts
  • src/components/settings/types/types.ts
  • src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx
  • src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx
  • src/components/shell/hooks/useShellConnection.ts
  • src/components/shell/types/types.ts
  • src/components/sidebar/types/types.ts
  • src/components/sidebar/utils/utils.ts
  • src/components/sidebar/view/subcomponents/SidebarContent.tsx
  • src/components/sidebar/view/subcomponents/SidebarModals.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectList.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectsState.tsx
  • src/components/sidebar/view/subcomponents/SidebarRepoGroup.tsx
  • src/hooks/useProjectsState.ts
  • src/i18n/__tests__/locales.test.ts
  • src/i18n/locales/de/settings.json
  • src/i18n/locales/de/sidebar.json
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/en/sidebar.json
  • src/i18n/locales/ja/settings.json
  • src/i18n/locales/ja/sidebar.json
  • src/i18n/locales/ko/settings.json
  • src/i18n/locales/ko/sidebar.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/ru/sidebar.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-CN/sidebar.json
  • src/types/app.ts
  • src/types/global.d.ts
  • vitest.config.js
✅ Files skipped from review due to trivial changes (18)
  • src/types/global.d.ts
  • package.json
  • src/i18n/locales/de/settings.json
  • src/i18n/locales/en/settings.json
  • vitest.config.js
  • src/components/shell/types/types.ts
  • src/i18n/locales/de/sidebar.json
  • src/components/settings/hooks/useSettingsController.ts
  • src/i18n/locales/zh-CN/settings.json
  • src/components/sidebar/types/types.ts
  • src/i18n/locales/en/sidebar.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/ru/sidebar.json
  • src/i18n/locales/ja/sidebar.json
  • src/i18n/locales/ko/settings.json
  • src/components/settings/view/tabs/agents-settings/sections/content/PermissionsContent.tsx
  • src/i18n/locales/ja/settings.json
  • src/components/chat/utils/chatStorage.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/components/chat/types/types.ts
  • src/components/settings/types/types.ts
  • server/claude-sdk.js
  • src/components/sidebar/view/subcomponents/SidebarProjectList.tsx
  • src/i18n/locales/zh-CN/sidebar.json
  • src/types/app.ts
  • src/components/chat/utils/tests/chatStorage.test.ts
  • src/i18n/locales/ko/sidebar.json
  • server/tests/claude-sdk.test.js

Comment thread server/load-env.js
Comment thread server/projects.js Outdated
Comment thread server/projects.js
Comment thread src/i18n/__tests__/locales.test.ts Outdated
Comment thread src/i18n/__tests__/locales.test.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
server/projects.js (1)

1297-1308: ⚠️ Potential issue | 🟠 Major

Avoid force-removing the worktree on the first delete attempt.

git worktree remove refuses dirty worktrees unless --force is used. Calling it with --force immediately here, then falling back to fs.rm(), means deleting a project can discard uncommitted or untracked work without a second destructive opt-in. Please try a plain remove first and only escalate to a forced delete when the caller explicitly confirms that data loss is acceptable. (git-scm.com)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/projects.js` around lines 1297 - 1308, The current removal always
calls execFileAsync('git', ['worktree', 'remove', worktreePath, '--force'], ...)
which can discard local changes; change the logic in the block that uses
execFileAsync, fs.rm, worktreePath and mainRepoRoot so it first attempts
execFileAsync('git', ['worktree', 'remove', worktreePath'], ...) without
--force, and only if that call fails with the specific “worktree dirty”/refusal
error (or an explicit force flag passed into the surrounding function, e.g., a
forceDelete parameter) retry with --force; preserve the existing fallback to
fs.rm only when forced or when git is unavailable, and update the
console.warn/process logs to clearly indicate when a forced destructive delete
was used versus a safe non-forced removal.
🧹 Nitpick comments (1)
src/i18n/__tests__/locales.test.ts (1)

21-45: Consider using beforeAll for cleaner test isolation.

The current approach populates localeData[locale] as a side effect within the second test (line 35). Subsequent tests depend on this test having run and succeeded. While the guards (expect(root).toBeDefined()) ensure clear failures, using a beforeAll hook would make tests more independent and allow selective test execution (e.g., it.only) without breaking dependent tests.

♻️ Optional refactor for test isolation
   for (const locale of SUPPORTED_LOCALES) {
     describe(`locale: ${locale}`, () => {
       const filePath = path.join(LOCALES_DIR, locale, 'settings.json');
+      let localeContent: Record<string, unknown> | undefined;
+
+      beforeAll(() => {
+        if (fs.existsSync(filePath)) {
+          try {
+            const raw = fs.readFileSync(filePath, 'utf-8');
+            localeContent = JSON.parse(raw);
+            localeData[locale] = localeContent;
+          } catch {
+            // Let tests assert the failure
+          }
+        }
+      });
 
       it('has a settings.json file', () => {
         expect(fs.existsSync(filePath)).toBe(true);
       });
 
       it('has valid settings.json content', () => {
-        expect(() => {
-          const raw = fs.readFileSync(filePath, 'utf-8');
-          localeData[locale] = JSON.parse(raw);
-        }).not.toThrow();
+        expect(localeContent).toBeDefined();
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/__tests__/locales.test.ts` around lines 21 - 45, The tests populate
localeData[locale] as a side-effect inside the `'has valid settings.json
content'` it block, causing later tests (like the permissions check) to depend
on that test having run; move the file read + JSON.parse for each locale into a
beforeAll hook inside the describe(`locale: ${locale}`) block so
localeData[locale] is populated before any it tests run (use the same filePath
and JSON parsing logic currently in the validity test), keep the existing expect
guards in the it blocks but read/parse in beforeAll to allow running individual
it tests (e.g., it.only) without breaking them, and retain references to
SUPPORTED_LOCALES, LOCALES_DIR, and settings.json when locating the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/projects.js`:
- Around line 394-407: The path parser is Windows-unsafe because it only looks
for '/.claude/worktrees/'; create a small helper (e.g., normalizePathForParsing)
that returns the input path with backslashes converted to forward slashes, use
that in synthesizeWorktreeInfoFromPath to find the marker '/.claude/worktrees/'
and compute markerIdx, afterMarker and worktreeName from the normalized path,
then map results back to the original string when building
worktreeRoot/mainRepoRoot/branchName if you need original separators; also
replace the same logic in the other helper that uses the same marker (the
cleanupWorktreeDirectory-related function around the other occurrence) so both
are separator-agnostic.
- Around line 417-430: The function fixWorktreeDisplayNames is incorrectly
treating an arbitrary grouped[0] (mainProject) that may itself be a worktree as
the repo's real main checkout, causing siblings with displayName === branchName
to be rewritten to that child's name; change the logic to detect whether
mainProject is actually a worktree (e.g. const mainIsWorktree =
!!mainProject?.worktreeInfo) and only perform the
branch-name-to-mainProject.displayName rewrite when mainIsWorktree is false
(i.e., when you have a real main checkout). Apply the same guard to the
equivalent branch-name rewrite at the other occurrence referenced in the
comment.

---

Duplicate comments:
In `@server/projects.js`:
- Around line 1297-1308: The current removal always calls execFileAsync('git',
['worktree', 'remove', worktreePath, '--force'], ...) which can discard local
changes; change the logic in the block that uses execFileAsync, fs.rm,
worktreePath and mainRepoRoot so it first attempts execFileAsync('git',
['worktree', 'remove', worktreePath'], ...) without --force, and only if that
call fails with the specific “worktree dirty”/refusal error (or an explicit
force flag passed into the surrounding function, e.g., a forceDelete parameter)
retry with --force; preserve the existing fallback to fs.rm only when forced or
when git is unavailable, and update the console.warn/process logs to clearly
indicate when a forced destructive delete was used versus a safe non-forced
removal.

---

Nitpick comments:
In `@src/i18n/__tests__/locales.test.ts`:
- Around line 21-45: The tests populate localeData[locale] as a side-effect
inside the `'has valid settings.json content'` it block, causing later tests
(like the permissions check) to depend on that test having run; move the file
read + JSON.parse for each locale into a beforeAll hook inside the
describe(`locale: ${locale}`) block so localeData[locale] is populated before
any it tests run (use the same filePath and JSON parsing logic currently in the
validity test), keep the existing expect guards in the it blocks but read/parse
in beforeAll to allow running individual it tests (e.g., it.only) without
breaking them, and retain references to SUPPORTED_LOCALES, LOCALES_DIR, and
settings.json when locating the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3a1671e0-06b7-4f12-8197-8695fe264a3e

📥 Commits

Reviewing files that changed from the base of the PR and between 8707776 and 4984040.

📒 Files selected for processing (3)
  • server/load-env.js
  • server/projects.js
  • src/i18n/__tests__/locales.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/load-env.js

Comment thread server/projects.js
Comment thread server/projects.js
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/projects.js`:
- Around line 731-739: Normalize the repo-group key before bucketing: compute a
normalizedRepoRoot from project.worktreeInfo?.mainRepoRoot (use Node's
path.normalize to unify separators and, on Windows, lowercase the path for
case-insensitive matching) and use that normalizedRepoRoot when checking/setting
repoGroups instead of the raw mainRepoRoot; this ensures outputs of
getWorktreeInfo() and synthesizeWorktreeInfoFromPath() map to the same bucket.
Reference repoGroups and project.worktreeInfo?.mainRepoRoot when implementing
the change and add a path import if not present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 012c3687-c77c-4cf8-83c8-b716a9a801a1

📥 Commits

Reviewing files that changed from the base of the PR and between 4984040 and 4c7fe74.

📒 Files selected for processing (1)
  • server/projects.js

Comment thread server/projects.js
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (7)
server/projects.js (1)

299-310: Use path.join for the synthesized worktree path.

${repoRoot}/.claude/worktrees/${worktreeName} hardcodes forward slashes. On Windows, repoRoot (resolved from the parent project) typically contains backslashes, so the constructed path mixes separators. Node FS tolerates this, but path.join is more idiomatic and consistent with the rest of the file (e.g., line 312, 79).

♻️ Proposed fix
-      const repoRoot = await extractProjectDirectory(parentProjectName);
-      const candidatePath = `${repoRoot}/.claude/worktrees/${worktreeName}`;
+      const repoRoot = await extractProjectDirectory(parentProjectName);
+      const candidatePath = path.join(repoRoot, '.claude', 'worktrees', worktreeName);
       projectDirectoryCache.set(projectName, candidatePath);
       return candidatePath;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/projects.js` around lines 299 - 310, The synthesized worktree path
currently concatenates strings with forward slashes; update the construction to
use path.join to produce a platform-safe path. In the block handling
worktreeMarker (variables: worktreeMarker, markerIdx, parentProjectName,
worktreeName) where repoRoot is obtained from
extractProjectDirectory(parentProjectName), replace the literal
`${repoRoot}/.claude/worktrees/${worktreeName}` with a path.join(repoRoot,
'.claude', 'worktrees', worktreeName) result before setting
projectDirectoryCache.set(projectName, ...) and returning it so paths are
correct on Windows and consistent with other uses in this file.
docs/superpowers/specs/2026-04-25-sidebar-worktree-redesign-design.md (1)

29-29: Add a language tag to the fenced code blocks (markdownlint MD040).

Both ASCII-art blocks are tagged as plain fences. Adding text (or plaintext) silences the linter and is a common convention for non-language content.

📝 Proposed fix
-```
+```text
 ┌──────────────────────────────────────────────┐
-```
+```text
 SidebarProjectList.tsx

Also applies to: 123-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-25-sidebar-worktree-redesign-design.md` at
line 29, The fenced code blocks containing ASCII-art and the
"SidebarProjectList.tsx" snippet are currently untagged and trigger markdownlint
MD040; update those fence openings to include a plaintext tag (e.g., ```text or
```plaintext) for the two ASCII-art blocks shown and any other similar plain
fences (the other occurrence noted at the document). Locate the untagged
triple-backtick blocks in the sidebar-worktree-redesign-design.md file and add
the language tag to each opening fence so the linter recognizes them as
non-code/plaintext.
src/components/sidebar/view/subcomponents/BranchChip.tsx (1)

5-30: Tie palette length to the hash modulus to avoid silent drift.

branchChipColorIndex uses PALETTE_SIZE = 5 in repoAggregates.ts, and this file independently defines a 5-entry PALETTE. If one is changed, the chip will silently fall back to NEUTRAL for some branches. Either export the palette length from repoAggregates (and use it there) or assert the relationship at module load.

Optional refactor
-import { branchChipColorIndex } from '../../utils/repoAggregates';
+import { branchChipColorIndex } from '../../utils/repoAggregates';
+
+// Keep in sync with PALETTE_SIZE in repoAggregates.ts
 const PALETTE = [
   ...
 ];
+if (process.env.NODE_ENV !== 'production') {
+  // Sanity: hash modulus and palette length must match.
+  // (Or export PALETTE_SIZE and derive both from it.)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/BranchChip.tsx` around lines 5 -
30, PALETTE in BranchChip.tsx is independently sized while branchChipColorIndex
in repoAggregates.ts uses a hardcoded PALETTE_SIZE=5, which can cause silent
fallback to NEUTRAL if they diverge; fix by either importing an exported palette
size (e.g., export PALETTE_SIZE or getPaletteLength) from repoAggregates and use
that to derive the palette or by adding a runtime assertion at module load in
BranchChip.tsx that checks PALETTE.length === branchChipColorIndex's modulus
constant (referencing PALETTE and branchChipColorIndex) and throw/log a clear
error if they differ so the relationship cannot silently drift.
src/components/sidebar/view/subcomponents/RepoCard.tsx (2)

158-177: Performance: per-worktree getRepoSessions/getRepoSessionTotal runs on every render.

For each WorktreeRow, both getRepoSessionTotal([wt]) (Line 163) and lastActivityLabelFor(wt, additionalSessions, currentTime, t) (Line 164) are computed inline on every render, and lastActivityLabelFor itself calls getRepoSessions([project], additionalSessions) which clones, annotates, and sorts that worktree's sessions. With many worktrees and a currentTime ticking each second (typical for relative-time labels), this is wasted work on the hot path.

Consider a single memo that produces Map<name, { sessionCount, lastActivityLabel }> keyed off [projects, additionalSessions, currentTime]:

Suggested refactor
+  const worktreeStats = useMemo(() => {
+    const map = new Map<string, { sessionCount: number; lastActivityLabel: string | null }>();
+    for (const wt of linkedWorktrees) {
+      const sessions = getRepoSessions([wt], additionalSessions);
+      map.set(wt.name, {
+        sessionCount: getRepoSessionTotal([wt]),
+        lastActivityLabel: sessions.length === 0
+          ? null
+          : labelForDate(new Date(sessions[0].lastActivity || sessions[0].createdAt || 0), currentTime, t),
+      });
+    }
+    return map;
+  }, [linkedWorktrees, additionalSessions, currentTime, t]);

…and consume worktreeStats.get(wt.name) in both the active and archived WorktreeRow mappings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/RepoCard.tsx` around lines 158 -
177, Rendering calls compute getRepoSessionTotal and lastActivityLabelFor (which
itself calls getRepoSessions) for every WorktreeRow on each render, causing
wasted work when currentTime updates; fix by computing a memoized Map keyed by
[activeWorktrees, additionalSessions, currentTime] (e.g., worktreeStats:
Map<name, { sessionCount, lastActivityLabel }>) using useMemo, populate it by
iterating activeWorktrees and calling
getRepoSessions/getRepoSessionTotal/lastActivityLabelFor once per worktree, then
replace inline calls in the WorktreeRow props (sessionCount, lastActivityLabel)
with lookups like worktreeStats.get(wt.name) so re-renders reuse the precomputed
values.

112-122: Conditional defaultValue for plurals is ineffective.

Same pattern as in WorktreeRow.tsx: t('projects.worktrees', { defaultValue: linkedWorktrees.length === 1 ? 'worktree' : 'worktrees', count: linkedWorktrees.length })defaultValue is only used when the key is missing; with count, i18next selects worktrees_one / worktrees_other from the bundle. Either define plural keys (worktrees_one/worktrees_other) and drop the conditional, or use distinct t() calls. The same applies to projects.sessionsShort on Line 112.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/RepoCard.tsx` around lines 112 -
122, The current conditional defaultValue on t() is ineffective for plurals;
update the code to use i18next pluralization properly by adding plural keys
(projects.worktrees_one / projects.worktrees_other and
projects.sessionsShort_one / projects.sessionsShort_other) in your locale bundle
and then call t('projects.worktrees', { count: linkedWorktrees.length }) and
t('projects.sessionsShort', { count: sessionTotal }) in RepoCard (same pattern
applies in WorktreeRow.tsx) so you can remove the linkedWorktrees.length === 1
conditional and let i18next pick the correct plural form.
src/components/sidebar/view/subcomponents/RecentSessions.tsx (1)

122-134: formatRelative returns "0m ago" for the 0–59 s window when lastActivity is slightly in the past but rounds down.

Line 128 already short-circuits minutes < 1 to "just now", so this is fine for the common path. But note the "1 minute ago" case will render as "1m ago" rather than the singular time.oneMinuteAgo key that exists in the locale files (e.g., de/sidebar.json has "oneMinuteAgo": "vor 1 Min."). Consider routing 1m and 1h through the singular keys for higher-quality localized output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/RecentSessions.tsx` around lines
122 - 134, formatRelative currently returns pluralized minute/hour translations
(e.g., "1m ago") because it only short-circuits for <1 minute; update
formatRelative to detect the singular cases and call the singular i18n keys:
when minutes === 1 use t('time.oneMinuteAgo', { defaultValue: '1m ago' }) and
when hours === 1 use t('time.oneHourAgo', { defaultValue: '1h ago' }), keeping
the existing branches for <1 minute, minutes <60, hours <24 and days; apply
these checks using the same computed values (date from session.lastActivity ||
session.createdAt, minutes, hours) so localization uses the dedicated singular
keys.
src/components/sidebar/view/subcomponents/WorktreeRow.tsx (1)

32-39: Conditional defaultValue in i18next is ignored when the translation key exists.

The bundles define sessionsShort_one and sessionsShort_other across all locales, so i18next uses count to select the correct plural form from the bundle rather than from the conditional defaultValue. The ternary expression doesn't control pluralization and only adds confusion.

Suggested fix
-      : `${sessionCount} ${t('projects.sessionsShort', {
-          defaultValue: sessionCount === 1 ? 'session' : 'sessions',
-          count: sessionCount,
-        })}${lastActivityLabel ? ` · ${lastActivityLabel}` : ''}`;
+      : `${sessionCount} ${t('projects.sessionsShort', {
+          defaultValue: 'sessions',
+          count: sessionCount,
+        })}${lastActivityLabel ? ` · ${lastActivityLabel}` : ''}`;

This aligns with how the same pattern is already implemented in RepoCard.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/WorktreeRow.tsx` around lines 32 -
39, The metaText uses a ternary defaultValue to pick singular/plural for
t('projects.sessionsShort') but i18next will ignore that when plural keys exist;
replace the ternary + conditional defaultValue with the same pattern used in
RepoCard.tsx: always call t('projects.sessionsShort', { count: sessionCount,
defaultValue: sessionCount === 1 ? 'session' : 'sessions' }) (keeping the
`${sessionCount} ${...}${lastActivityLabel ? \` · ${lastActivityLabel}\` : ''}`
composition) so pluralization is driven by the count param and the code is
simplified; update the metaText expression in WorktreeRow.tsx accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/projects.js`:
- Around line 800-805: The synthetic worktreeInfo sets branchName to null which
violates the WorktreeInfo.branchName: string contract; fix by changing the
object literal to use branchName: '' (empty string) instead of null in the
worktreeInfo construction (the object containing isWorktree, worktreeRoot,
mainRepoRoot, branchName), or alternatively update the WorktreeInfo type
declaration to accept string | null (i.e., change the branchName type to allow
null) so the runtime value matches the declared type.

In `@src/components/sidebar/utils/repoAggregates.ts`:
- Around line 33-46: getRepoSessionTotal currently skips adding
cursor/codex/gemini sessions when project.sessionMeta?.total is a number,
causing non-Claude sessions to be undercounted; fix by removing the early
continue and always summing Claude count plus other provider counts.
Specifically, in getRepoSessionTotal compute per-projectTotal =
(project.sessionMeta?.total ?? 0) + (project.sessions?.length ?? 0) +
(project.cursorSessions?.length ?? 0) + (project.codexSessions?.length ?? 0) +
(project.geminiSessions?.length ?? 0) and add that to total (replace the
existing if/continue branch with this combined sum).

In `@src/components/sidebar/view/subcomponents/RecentSessions.tsx`:
- Around line 33-117: The "Show all" button is one-way because showAll is only
set to true; change the toggle to flip showAll (use setShowAll(prev => !prev))
and update the button label to show either t('projects.showAll') with
sessions.length when showAll is false or a "Show less" label (e.g.,
t('projects.showLess', { defaultValue: 'Show less' })) when showAll is true;
keep the button rendered whenever expanded && sessions.length > DEFAULT_VISIBLE
(instead of hidden > 0) so users can collapse back to the compact view; adjust
any text/count shown to reflect the current state and keep visible/hidden
calculation based on showAll and DEFAULT_VISIBLE as before.

In `@src/components/sidebar/view/subcomponents/RepoCard.tsx`:
- Around line 228-245: The lastActivityLabelFor function currently returns "0m
ago" for events under one minute; update lastActivityLabelFor to short-circuit
when minutes < 1 and return t('time.justNow', { defaultValue: 'just now' })
(mirror the behavior in RecentSessions.tsx's formatRelative) before the minutes
< 60 branch—use the existing minutes calculation and keep the rest of the
branches intact.

In `@src/components/sidebar/view/subcomponents/WorktreeRow.tsx`:
- Line 60: The BranchChip fallback currently uses project.displayName which
mislabels non-branch worktrees; update the rendering in WorktreeRow so that if
project.worktreeInfo?.branchName exists you render <BranchChip
branchName={project.worktreeInfo.branchName} /> and otherwise render a plain
<span> (e.g., <span className="worktree-name">{project.displayName}</span>)
instead of BranchChip so the directory/display name is shown without the
Git/Home branch styling or icon; locate the conditional around BranchChip in
WorktreeRow and replace the fallback with a simple span.

In `@src/i18n/locales/de/sidebar.json`:
- Around line 66-67: The German locale contains untranslated tooltip strings:
the keys "newSessionInWorktree" and "deleteWorktree" currently have English
values; update their values to proper German translations (e.g., "Neue Sitzung
in diesem Worktree" for newSessionInWorktree and "Worktree löschen" for
deleteWorktree) so the de sidebar bundle is fully localized.
- Around line 24-35: Several keys in the German locale file are still English
and one key duplicates another with inconsistent casing; update the values for
"recent", "showAll", "emptyWorktree", "staleWorktree", "messages",
"sessionsShort_one", "sessionsShort_other", and "older" to their German
translations (e.g. "recent" -> "Zuletzt"/"Letzte", "showAll" -> "Alle anzeigen",
"emptyWorktree" -> "leer · zum Starten klicken", "staleWorktree" ->
"archiviert", "messages" -> "Nachrichten", "sessionsShort_one" -> "Sitzung",
"sessionsShort_other" -> "Sitzungen", "older" -> "Älter") and remove or
consolidate the duplicate "archived" vs "staleWorktree" so there's a single,
consistently cased source of truth; then audit the same keys in the
corresponding locale files for ja, ko, ru, and zh-CN and provide proper
translations or mark for translator review if missing.

In `@src/i18n/locales/ja/sidebar.json`:
- Around line 27-35: Several sidebar JSON keys remain in English; update the
Japanese translations for the keys "recent", "worktreesUpper", "showAll",
"emptyWorktree", "staleWorktree", "messages", "sessionsShort_one",
"sessionsShort_other", "older", plus "newSessionInWorktree" and "deleteWorktree"
referenced later, replacing the English values with appropriate Japanese strings
so the ja locale is fully localized (you can ignore "sessionsShort_one" for
plural behavior since i18next uses only "other" for ja). Locate those keys in
src/i18n/locales/ja/sidebar.json and provide natural Japanese translations for
each value.

In `@src/i18n/locales/ko/sidebar.json`:
- Around line 27-35: Several sidebar i18n keys in ko/sidebar.json remain in
English (e.g., "recent", "worktreesUpper", "showAll", "emptyWorktree",
"staleWorktree", "messages", "sessionsShort_one", "sessionsShort_other",
"older", plus "newSessionInWorktree" and "deleteWorktree") causing a
mixed-language UI; replace the English values with appropriate Korean
translations for those keys (keep "sessionsShort_one" if you prefer even though
i18next for ko only uses the "other" plural form) so the sidebar is fully
localized—update the values for the listed keys in the JSON (matching the exact
key names) to their Korean equivalents and ensure JSON syntax remains valid.

---

Nitpick comments:
In `@docs/superpowers/specs/2026-04-25-sidebar-worktree-redesign-design.md`:
- Line 29: The fenced code blocks containing ASCII-art and the
"SidebarProjectList.tsx" snippet are currently untagged and trigger markdownlint
MD040; update those fence openings to include a plaintext tag (e.g., ```text or
```plaintext) for the two ASCII-art blocks shown and any other similar plain
fences (the other occurrence noted at the document). Locate the untagged
triple-backtick blocks in the sidebar-worktree-redesign-design.md file and add
the language tag to each opening fence so the linter recognizes them as
non-code/plaintext.

In `@server/projects.js`:
- Around line 299-310: The synthesized worktree path currently concatenates
strings with forward slashes; update the construction to use path.join to
produce a platform-safe path. In the block handling worktreeMarker (variables:
worktreeMarker, markerIdx, parentProjectName, worktreeName) where repoRoot is
obtained from extractProjectDirectory(parentProjectName), replace the literal
`${repoRoot}/.claude/worktrees/${worktreeName}` with a path.join(repoRoot,
'.claude', 'worktrees', worktreeName) result before setting
projectDirectoryCache.set(projectName, ...) and returning it so paths are
correct on Windows and consistent with other uses in this file.

In `@src/components/sidebar/view/subcomponents/BranchChip.tsx`:
- Around line 5-30: PALETTE in BranchChip.tsx is independently sized while
branchChipColorIndex in repoAggregates.ts uses a hardcoded PALETTE_SIZE=5, which
can cause silent fallback to NEUTRAL if they diverge; fix by either importing an
exported palette size (e.g., export PALETTE_SIZE or getPaletteLength) from
repoAggregates and use that to derive the palette or by adding a runtime
assertion at module load in BranchChip.tsx that checks PALETTE.length ===
branchChipColorIndex's modulus constant (referencing PALETTE and
branchChipColorIndex) and throw/log a clear error if they differ so the
relationship cannot silently drift.

In `@src/components/sidebar/view/subcomponents/RecentSessions.tsx`:
- Around line 122-134: formatRelative currently returns pluralized minute/hour
translations (e.g., "1m ago") because it only short-circuits for <1 minute;
update formatRelative to detect the singular cases and call the singular i18n
keys: when minutes === 1 use t('time.oneMinuteAgo', { defaultValue: '1m ago' })
and when hours === 1 use t('time.oneHourAgo', { defaultValue: '1h ago' }),
keeping the existing branches for <1 minute, minutes <60, hours <24 and days;
apply these checks using the same computed values (date from
session.lastActivity || session.createdAt, minutes, hours) so localization uses
the dedicated singular keys.

In `@src/components/sidebar/view/subcomponents/RepoCard.tsx`:
- Around line 158-177: Rendering calls compute getRepoSessionTotal and
lastActivityLabelFor (which itself calls getRepoSessions) for every WorktreeRow
on each render, causing wasted work when currentTime updates; fix by computing a
memoized Map keyed by [activeWorktrees, additionalSessions, currentTime] (e.g.,
worktreeStats: Map<name, { sessionCount, lastActivityLabel }>) using useMemo,
populate it by iterating activeWorktrees and calling
getRepoSessions/getRepoSessionTotal/lastActivityLabelFor once per worktree, then
replace inline calls in the WorktreeRow props (sessionCount, lastActivityLabel)
with lookups like worktreeStats.get(wt.name) so re-renders reuse the precomputed
values.
- Around line 112-122: The current conditional defaultValue on t() is
ineffective for plurals; update the code to use i18next pluralization properly
by adding plural keys (projects.worktrees_one / projects.worktrees_other and
projects.sessionsShort_one / projects.sessionsShort_other) in your locale bundle
and then call t('projects.worktrees', { count: linkedWorktrees.length }) and
t('projects.sessionsShort', { count: sessionTotal }) in RepoCard (same pattern
applies in WorktreeRow.tsx) so you can remove the linkedWorktrees.length === 1
conditional and let i18next pick the correct plural form.

In `@src/components/sidebar/view/subcomponents/WorktreeRow.tsx`:
- Around line 32-39: The metaText uses a ternary defaultValue to pick
singular/plural for t('projects.sessionsShort') but i18next will ignore that
when plural keys exist; replace the ternary + conditional defaultValue with the
same pattern used in RepoCard.tsx: always call t('projects.sessionsShort', {
count: sessionCount, defaultValue: sessionCount === 1 ? 'session' : 'sessions'
}) (keeping the `${sessionCount} ${...}${lastActivityLabel ? \` ·
${lastActivityLabel}\` : ''}` composition) so pluralization is driven by the
count param and the code is simplified; update the metaText expression in
WorktreeRow.tsx accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ae73fb08-2f74-4cef-a118-2fb935f5c2f0

📥 Commits

Reviewing files that changed from the base of the PR and between 8cf916b and d097804.

📒 Files selected for processing (22)
  • docs/superpowers/plans/2026-04-25-sidebar-worktree-redesign.md
  • docs/superpowers/specs/2026-04-25-sidebar-worktree-redesign-design.md
  • server/projects.js
  • src/components/sidebar/utils/__tests__/repoAggregates.test.ts
  • src/components/sidebar/utils/repoAggregates.ts
  • src/components/sidebar/view/Sidebar.tsx
  • src/components/sidebar/view/subcomponents/BranchChip.tsx
  • src/components/sidebar/view/subcomponents/NewSessionRow.tsx
  • src/components/sidebar/view/subcomponents/RecentSessions.tsx
  • src/components/sidebar/view/subcomponents/RepoCard.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectList.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx
  • src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx
  • src/components/sidebar/view/subcomponents/WorktreeRow.tsx
  • src/hooks/useProjectsState.ts
  • src/i18n/locales/de/sidebar.json
  • src/i18n/locales/en/sidebar.json
  • src/i18n/locales/ja/sidebar.json
  • src/i18n/locales/ko/sidebar.json
  • src/i18n/locales/ru/sidebar.json
  • src/i18n/locales/zh-CN/sidebar.json
💤 Files with no reviewable changes (3)
  • src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectSessions.tsx
  • src/components/sidebar/view/subcomponents/SidebarProjectItem.tsx
✅ Files skipped from review due to trivial changes (2)
  • src/i18n/locales/ru/sidebar.json
  • docs/superpowers/plans/2026-04-25-sidebar-worktree-redesign.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/i18n/locales/zh-CN/sidebar.json
  • src/components/sidebar/view/subcomponents/SidebarProjectList.tsx

Comment thread server/projects.js
Comment on lines +800 to +805
worktreeInfo: {
isWorktree: false,
worktreeRoot: repoRoot,
mainRepoRoot: repoRoot,
branchName: null,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Synthetic worktreeInfo.branchName: null violates the typed contract.

WorktreeInfo.branchName is declared as string in src/types/app.ts (lines 33-38). Setting it to null here works at runtime (frontend uses ?? null in places like RepoCard.tsx), but other consumers that trust the type signature will silently get null where they expect a string. Either widen the type to string | null or use '' here.

🛡️ Proposed fix (use empty string)
         worktreeInfo: {
           isWorktree: false,
           worktreeRoot: repoRoot,
           mainRepoRoot: repoRoot,
-          branchName: null,
+          branchName: '',
         },

Alternatively, update WorktreeInfo.branchName in src/types/app.ts to string | null to make the synthetic case explicit at the type level.

📝 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.

Suggested change
worktreeInfo: {
isWorktree: false,
worktreeRoot: repoRoot,
mainRepoRoot: repoRoot,
branchName: null,
},
worktreeInfo: {
isWorktree: false,
worktreeRoot: repoRoot,
mainRepoRoot: repoRoot,
branchName: '',
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/projects.js` around lines 800 - 805, The synthetic worktreeInfo sets
branchName to null which violates the WorktreeInfo.branchName: string contract;
fix by changing the object literal to use branchName: '' (empty string) instead
of null in the worktreeInfo construction (the object containing isWorktree,
worktreeRoot, mainRepoRoot, branchName), or alternatively update the
WorktreeInfo type declaration to accept string | null (i.e., change the
branchName type to allow null) so the runtime value matches the declared type.

Comment on lines +33 to +46
export const getRepoSessionTotal = (projects: Project[]): number => {
let total = 0;
for (const project of projects) {
if (typeof project.sessionMeta?.total === 'number') {
total += project.sessionMeta.total;
continue;
}
total += (project.sessions?.length ?? 0)
+ (project.cursorSessions?.length ?? 0)
+ (project.codexSessions?.length ?? 0)
+ (project.geminiSessions?.length ?? 0);
}
return total;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

getRepoSessionTotal undercounts projects with non-Claude sessions.

sessionMeta.total in server/projects.js (around lines 530-534) is populated from getSessions(...), which reads only Claude .jsonl files. The early continue then skips the cursor/codex/gemini accumulation entirely. Since the server always initializes sessionMeta on every project, the typeof check is essentially always true, so the fallback branch is rarely (if ever) executed.

Result: a project with, say, 0 Claude sessions but 3 Cursor sessions reports a total of 0 in the repo header subtitle.

🔧 Proposed fix — always include the other providers
 export const getRepoSessionTotal = (projects: Project[]): number => {
   let total = 0;
   for (const project of projects) {
-    if (typeof project.sessionMeta?.total === 'number') {
-      total += project.sessionMeta.total;
-      continue;
-    }
-    total += (project.sessions?.length ?? 0)
-      + (project.cursorSessions?.length ?? 0)
+    // sessionMeta.total covers Claude sessions (server may report > sessions.length).
+    // Fall back to sessions.length when sessionMeta isn't populated.
+    total += typeof project.sessionMeta?.total === 'number'
+      ? project.sessionMeta.total
+      : (project.sessions?.length ?? 0);
+    total += (project.cursorSessions?.length ?? 0)
       + (project.codexSessions?.length ?? 0)
       + (project.geminiSessions?.length ?? 0);
   }
   return total;
 };
📝 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.

Suggested change
export const getRepoSessionTotal = (projects: Project[]): number => {
let total = 0;
for (const project of projects) {
if (typeof project.sessionMeta?.total === 'number') {
total += project.sessionMeta.total;
continue;
}
total += (project.sessions?.length ?? 0)
+ (project.cursorSessions?.length ?? 0)
+ (project.codexSessions?.length ?? 0)
+ (project.geminiSessions?.length ?? 0);
}
return total;
};
export const getRepoSessionTotal = (projects: Project[]): number => {
let total = 0;
for (const project of projects) {
// sessionMeta.total covers Claude sessions (server may report > sessions.length).
// Fall back to sessions.length when sessionMeta isn't populated.
total += typeof project.sessionMeta?.total === 'number'
? project.sessionMeta.total
: (project.sessions?.length ?? 0);
total += (project.cursorSessions?.length ?? 0)
(project.codexSessions?.length ?? 0)
(project.geminiSessions?.length ?? 0);
}
return total;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/utils/repoAggregates.ts` around lines 33 - 46,
getRepoSessionTotal currently skips adding cursor/codex/gemini sessions when
project.sessionMeta?.total is a number, causing non-Claude sessions to be
undercounted; fix by removing the early continue and always summing Claude count
plus other provider counts. Specifically, in getRepoSessionTotal compute
per-projectTotal = (project.sessionMeta?.total ?? 0) + (project.sessions?.length
?? 0) + (project.cursorSessions?.length ?? 0) + (project.codexSessions?.length
?? 0) + (project.geminiSessions?.length ?? 0) and add that to total (replace the
existing if/continue branch with this combined sum).

Comment on lines +33 to +117
const [showAll, setShowAll] = useState(false);

if (sessions.length === 0) {
return null;
}

const visible = showAll ? sessions : sessions.slice(0, DEFAULT_VISIBLE);
const hidden = sessions.length - visible.length;

const renderSessionRow = (session: RepoSession) => {
const project = projectsByName[session.__projectName];
const branchName = project?.worktreeInfo?.branchName ?? null;
const origin: 'main' | 'worktree' = project?.isMainWorktree ? 'main' : 'worktree';
const view = createSessionViewModel(session, currentTime, t);
const isSelected = selectedSession?.id === session.id;

return (
<button
key={`${session.__projectName}-${session.id}`}
type="button"
onClick={() => onSessionClick(session, session.__projectName)}
className={cn(
'flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
'hover:bg-accent/50',
isSelected && 'bg-accent text-accent-foreground',
)}
>
<span
className={cn(
'mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full',
view.isActive
? 'bg-green-500 shadow-[0_0_0_3px_rgba(34,197,94,0.15)]'
: 'bg-muted-foreground/30',
)}
aria-hidden
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-xs font-medium text-foreground">
{view.sessionName}
</span>
<span className="block truncate text-[10px] text-muted-foreground">
{formatRelative(session, currentTime, t)}
{view.messageCount > 0 && (
<>
{' · '}
{view.messageCount} {t('projects.messages', { defaultValue: 'messages' })}
</>
)}
</span>
</span>
<BranchChip branchName={branchName} origin={origin} />
</button>
);
};

return (
<div className="md:space-y-0.5">
<div className="flex items-center gap-1.5 px-2 py-1">
<button
type="button"
onClick={() => setExpanded((prev) => !prev)}
className="flex flex-1 items-center gap-1.5 rounded-md text-[10px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
>
{expanded ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<span>{t('projects.recent', { defaultValue: 'Recent' })}</span>
<span className="text-muted-foreground/60">· {sessions.length}</span>
</button>
{expanded && hidden > 0 && (
<button
type="button"
onClick={() => setShowAll(true)}
className="rounded px-1 text-[10px] font-medium normal-case tracking-normal text-muted-foreground hover:text-foreground"
>
{t('projects.showAll', { defaultValue: 'Show all' })} {sessions.length}
</button>
)}
</div>

{expanded && (
<div className="space-y-0.5">{visible.map(renderSessionRow)}</div>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor UX: "Show all" is one-way — no way to collapse back to the top 5.

Once setShowAll(true) fires (Line 107), hidden becomes 0 so the button on Lines 104-112 disappears, and there is no inverse "Show less" affordance. For repos with many sessions, users who expanded the list cannot return to the compact view without remounting (e.g., switching repos).

Suggested fix
-        {expanded && hidden > 0 && (
+        {expanded && sessions.length > DEFAULT_VISIBLE && (
           <button
             type="button"
-            onClick={() => setShowAll(true)}
+            onClick={() => setShowAll((prev) => !prev)}
             className="rounded px-1 text-[10px] font-medium normal-case tracking-normal text-muted-foreground hover:text-foreground"
           >
-            {t('projects.showAll', { defaultValue: 'Show all' })} {sessions.length}
+            {showAll
+              ? t('projects.showLess', { defaultValue: 'Show less' })
+              : `${t('projects.showAll', { defaultValue: 'Show all' })} ${sessions.length}`}
           </button>
         )}
📝 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.

Suggested change
const [showAll, setShowAll] = useState(false);
if (sessions.length === 0) {
return null;
}
const visible = showAll ? sessions : sessions.slice(0, DEFAULT_VISIBLE);
const hidden = sessions.length - visible.length;
const renderSessionRow = (session: RepoSession) => {
const project = projectsByName[session.__projectName];
const branchName = project?.worktreeInfo?.branchName ?? null;
const origin: 'main' | 'worktree' = project?.isMainWorktree ? 'main' : 'worktree';
const view = createSessionViewModel(session, currentTime, t);
const isSelected = selectedSession?.id === session.id;
return (
<button
key={`${session.__projectName}-${session.id}`}
type="button"
onClick={() => onSessionClick(session, session.__projectName)}
className={cn(
'flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
'hover:bg-accent/50',
isSelected && 'bg-accent text-accent-foreground',
)}
>
<span
className={cn(
'mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full',
view.isActive
? 'bg-green-500 shadow-[0_0_0_3px_rgba(34,197,94,0.15)]'
: 'bg-muted-foreground/30',
)}
aria-hidden
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-xs font-medium text-foreground">
{view.sessionName}
</span>
<span className="block truncate text-[10px] text-muted-foreground">
{formatRelative(session, currentTime, t)}
{view.messageCount > 0 && (
<>
{' · '}
{view.messageCount} {t('projects.messages', { defaultValue: 'messages' })}
</>
)}
</span>
</span>
<BranchChip branchName={branchName} origin={origin} />
</button>
);
};
return (
<div className="md:space-y-0.5">
<div className="flex items-center gap-1.5 px-2 py-1">
<button
type="button"
onClick={() => setExpanded((prev) => !prev)}
className="flex flex-1 items-center gap-1.5 rounded-md text-[10px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
>
{expanded ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<span>{t('projects.recent', { defaultValue: 'Recent' })}</span>
<span className="text-muted-foreground/60">· {sessions.length}</span>
</button>
{expanded && hidden > 0 && (
<button
type="button"
onClick={() => setShowAll(true)}
className="rounded px-1 text-[10px] font-medium normal-case tracking-normal text-muted-foreground hover:text-foreground"
>
{t('projects.showAll', { defaultValue: 'Show all' })} {sessions.length}
</button>
)}
</div>
{expanded && (
<div className="space-y-0.5">{visible.map(renderSessionRow)}</div>
)}
const [showAll, setShowAll] = useState(false);
if (sessions.length === 0) {
return null;
}
const visible = showAll ? sessions : sessions.slice(0, DEFAULT_VISIBLE);
const hidden = sessions.length - visible.length;
const renderSessionRow = (session: RepoSession) => {
const project = projectsByName[session.__projectName];
const branchName = project?.worktreeInfo?.branchName ?? null;
const origin: 'main' | 'worktree' = project?.isMainWorktree ? 'main' : 'worktree';
const view = createSessionViewModel(session, currentTime, t);
const isSelected = selectedSession?.id === session.id;
return (
<button
key={`${session.__projectName}-${session.id}`}
type="button"
onClick={() => onSessionClick(session, session.__projectName)}
className={cn(
'flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
'hover:bg-accent/50',
isSelected && 'bg-accent text-accent-foreground',
)}
>
<span
className={cn(
'mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full',
view.isActive
? 'bg-green-500 shadow-[0_0_0_3px_rgba(34,197,94,0.15)]'
: 'bg-muted-foreground/30',
)}
aria-hidden
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-xs font-medium text-foreground">
{view.sessionName}
</span>
<span className="block truncate text-[10px] text-muted-foreground">
{formatRelative(session, currentTime, t)}
{view.messageCount > 0 && (
<>
{' · '}
{view.messageCount} {t('projects.messages', { defaultValue: 'messages' })}
</>
)}
</span>
</span>
<BranchChip branchName={branchName} origin={origin} />
</button>
);
};
return (
<div className="md:space-y-0.5">
<div className="flex items-center gap-1.5 px-2 py-1">
<button
type="button"
onClick={() => setExpanded((prev) => !prev)}
className="flex flex-1 items-center gap-1.5 rounded-md text-[10px] font-semibold uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground"
>
{expanded ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<span>{t('projects.recent', { defaultValue: 'Recent' })}</span>
<span className="text-muted-foreground/60">· {sessions.length}</span>
</button>
{expanded && sessions.length > DEFAULT_VISIBLE && (
<button
type="button"
onClick={() => setShowAll((prev) => !prev)}
className="rounded px-1 text-[10px] font-medium normal-case tracking-normal text-muted-foreground hover:text-foreground"
>
{showAll
? t('projects.showLess', { defaultValue: 'Show less' })
: `${t('projects.showAll', { defaultValue: 'Show all' })} ${sessions.length}`}
</button>
)}
</div>
{expanded && (
<div className="space-y-0.5">{visible.map(renderSessionRow)}</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/RecentSessions.tsx` around lines 33
- 117, The "Show all" button is one-way because showAll is only set to true;
change the toggle to flip showAll (use setShowAll(prev => !prev)) and update the
button label to show either t('projects.showAll') with sessions.length when
showAll is false or a "Show less" label (e.g., t('projects.showLess', {
defaultValue: 'Show less' })) when showAll is true; keep the button rendered
whenever expanded && sessions.length > DEFAULT_VISIBLE (instead of hidden > 0)
so users can collapse back to the compact view; adjust any text/count shown to
reflect the current state and keep visible/hidden calculation based on showAll
and DEFAULT_VISIBLE as before.

Comment on lines +228 to +245
function lastActivityLabelFor(
project: Project,
additionalSessions: AdditionalSessionsByProject,
now: Date,
t: TFunction,
): string | null {
const sessions = getRepoSessions([project], additionalSessions);
if (sessions.length === 0) return null;
const latest = sessions[0];
const date = new Date(latest.lastActivity || latest.createdAt || 0);
const diffMs = now.getTime() - date.getTime();
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 60) return t('time.minutesAgo', { defaultValue: '{{count}}m ago', count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t('time.hoursAgo', { defaultValue: '{{count}}h ago', count: hours });
const days = Math.floor(hours / 24);
return t('time.daysAgo', { defaultValue: '{{count}}d ago', count: days });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

lastActivityLabelFor will render "0m ago" for events under one minute old.

Unlike formatRelative in RecentSessions.tsx (which short-circuits minutes < 1 to time.justNow), this helper goes straight into time.minutesAgo and produces "0m ago". Add the same just-now branch for parity:

   const minutes = Math.floor(diffMs / 60_000);
+  if (minutes < 1) return t('time.justNow', { defaultValue: 'just now' });
   if (minutes < 60) return t('time.minutesAgo', { defaultValue: '{{count}}m ago', count: minutes });
📝 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.

Suggested change
function lastActivityLabelFor(
project: Project,
additionalSessions: AdditionalSessionsByProject,
now: Date,
t: TFunction,
): string | null {
const sessions = getRepoSessions([project], additionalSessions);
if (sessions.length === 0) return null;
const latest = sessions[0];
const date = new Date(latest.lastActivity || latest.createdAt || 0);
const diffMs = now.getTime() - date.getTime();
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 60) return t('time.minutesAgo', { defaultValue: '{{count}}m ago', count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t('time.hoursAgo', { defaultValue: '{{count}}h ago', count: hours });
const days = Math.floor(hours / 24);
return t('time.daysAgo', { defaultValue: '{{count}}d ago', count: days });
}
function lastActivityLabelFor(
project: Project,
additionalSessions: AdditionalSessionsByProject,
now: Date,
t: TFunction,
): string | null {
const sessions = getRepoSessions([project], additionalSessions);
if (sessions.length === 0) return null;
const latest = sessions[0];
const date = new Date(latest.lastActivity || latest.createdAt || 0);
const diffMs = now.getTime() - date.getTime();
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 1) return t('time.justNow', { defaultValue: 'just now' });
if (minutes < 60) return t('time.minutesAgo', { defaultValue: '{{count}}m ago', count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t('time.hoursAgo', { defaultValue: '{{count}}h ago', count: hours });
const days = Math.floor(hours / 24);
return t('time.daysAgo', { defaultValue: '{{count}}d ago', count: days });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/RepoCard.tsx` around lines 228 -
245, The lastActivityLabelFor function currently returns "0m ago" for events
under one minute; update lastActivityLabelFor to short-circuit when minutes < 1
and return t('time.justNow', { defaultValue: 'just now' }) (mirror the behavior
in RecentSessions.tsx's formatRelative) before the minutes < 60 branch—use the
existing minutes calculation and keep the rest of the branches intact.

)}
title={project.fullPath}
>
<BranchChip branchName={project.worktreeInfo?.branchName ?? project.displayName} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

BranchChip falls back to project.displayName, which is not a branch name.

When project.worktreeInfo?.branchName is missing (e.g., for a stale worktree where worktree info failed to resolve), this renders the project's display name inside a chip with a Git/Home icon, presenting it to the user as a branch. This is misleading and will look especially odd for stale entries.

Suggested fix
-      <BranchChip branchName={project.worktreeInfo?.branchName ?? project.displayName} />
+      <BranchChip branchName={project.worktreeInfo?.branchName ?? null} />

If a label is still desired when no branch is known, render a plain <span> with the directory name rather than a BranchChip.

📝 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.

Suggested change
<BranchChip branchName={project.worktreeInfo?.branchName ?? project.displayName} />
<BranchChip branchName={project.worktreeInfo?.branchName ?? null} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/view/subcomponents/WorktreeRow.tsx` at line 60, The
BranchChip fallback currently uses project.displayName which mislabels
non-branch worktrees; update the rendering in WorktreeRow so that if
project.worktreeInfo?.branchName exists you render <BranchChip
branchName={project.worktreeInfo.branchName} /> and otherwise render a plain
<span> (e.g., <span className="worktree-name">{project.displayName}</span>)
instead of BranchChip so the directory/display name is shown without the
Git/Home branch styling or icon; locate the conditional around BranchChip in
WorktreeRow and replace the fallback with a simple span.

Comment on lines +24 to +35
"archived": "archiviert",
"showMoreWorktrees": "{{count}} weitere anzeigen...",
"newGroupSession": "Neue Sitzung in {{name}}",
"recent": "Recent",
"worktreesUpper": "Worktrees",
"showAll": "Show all",
"emptyWorktree": "empty · click to start",
"staleWorktree": "archived",
"messages": "messages",
"sessionsShort_one": "session",
"sessionsShort_other": "sessions",
"older": "Older"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Several newly added German strings are still in English — this regresses the de locale.

The following entries are in the German bundle but are not translated. They will render as English when the user has selected German:

  • Line 27 "recent": "Recent" → e.g. "Letzte" / "Zuletzt"
  • Line 29 "showAll": "Show all""Alle anzeigen"
  • Line 30 "emptyWorktree": "empty · click to start" → e.g. "leer · zum Starten klicken"
  • Line 31 "staleWorktree": "archived""archiviert" (also: this duplicates archived on Line 24 with inconsistent casing; pick one source of truth)
  • Line 32 "messages": "messages""Nachrichten"
  • Line 33 "sessionsShort_one": "session""Sitzung"
  • Line 34 "sessionsShort_other": "sessions""Sitzungen"
  • Line 35 "older": "Older""Älter"
Suggested diff
     "recent": "Recent",
+    "recent": "Zuletzt",
     "worktreesUpper": "Worktrees",
-    "showAll": "Show all",
-    "emptyWorktree": "empty · click to start",
-    "staleWorktree": "archived",
-    "messages": "messages",
-    "sessionsShort_one": "session",
-    "sessionsShort_other": "sessions",
-    "older": "Older"
+    "showAll": "Alle anzeigen",
+    "emptyWorktree": "leer · zum Starten klicken",
+    "staleWorktree": "archiviert",
+    "messages": "Nachrichten",
+    "sessionsShort_one": "Sitzung",
+    "sessionsShort_other": "Sitzungen",
+    "older": "Älter"

Please also audit ja, ko, ru, and zh-CN for the same untranslated keys, since the PR adds the same set across all six locales.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/de/sidebar.json` around lines 24 - 35, Several keys in the
German locale file are still English and one key duplicates another with
inconsistent casing; update the values for "recent", "showAll", "emptyWorktree",
"staleWorktree", "messages", "sessionsShort_one", "sessionsShort_other", and
"older" to their German translations (e.g. "recent" -> "Zuletzt"/"Letzte",
"showAll" -> "Alle anzeigen", "emptyWorktree" -> "leer · zum Starten klicken",
"staleWorktree" -> "archiviert", "messages" -> "Nachrichten",
"sessionsShort_one" -> "Sitzung", "sessionsShort_other" -> "Sitzungen", "older"
-> "Älter") and remove or consolidate the duplicate "archived" vs
"staleWorktree" so there's a single, consistently cased source of truth; then
audit the same keys in the corresponding locale files for ja, ko, ru, and zh-CN
and provide proper translations or mark for translator review if missing.

Comment on lines +66 to +67
"newSessionInWorktree": "New session in this worktree",
"deleteWorktree": "Delete worktree"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tooltip strings are also untranslated in the German bundle.

newSessionInWorktree and deleteWorktree are in English on Lines 66–67. Suggested:

-    "newSessionInWorktree": "New session in this worktree",
-    "deleteWorktree": "Delete worktree"
+    "newSessionInWorktree": "Neue Sitzung in diesem Worktree",
+    "deleteWorktree": "Worktree löschen"
📝 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.

Suggested change
"newSessionInWorktree": "New session in this worktree",
"deleteWorktree": "Delete worktree"
"newSessionInWorktree": "Neue Sitzung in diesem Worktree",
"deleteWorktree": "Worktree löschen"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/de/sidebar.json` around lines 66 - 67, The German locale
contains untranslated tooltip strings: the keys "newSessionInWorktree" and
"deleteWorktree" currently have English values; update their values to proper
German translations (e.g., "Neue Sitzung in diesem Worktree" for
newSessionInWorktree and "Worktree löschen" for deleteWorktree) so the de
sidebar bundle is fully localized.

Comment on lines +27 to +35
"recent": "Recent",
"worktreesUpper": "Worktrees",
"showAll": "Show all",
"emptyWorktree": "empty · click to start",
"staleWorktree": "archived",
"messages": "messages",
"sessionsShort_one": "session",
"sessionsShort_other": "sessions",
"older": "Older"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Multiple Japanese strings left untranslated.

Same issue as the Korean file: ~10 newly added keys still hold English values (recent, worktreesUpper, showAll, emptyWorktree, staleWorktree, messages, sessionsShort_one/_other, older, newSessionInWorktree, deleteWorktree). The rest of the Japanese sidebar is fully translated, so this will be visibly inconsistent for ja users.

Note: i18next treats ja as having only the other plural form, so sessionsShort_one is unused but harmless.

🌐 Suggested translations
-    "recent": "Recent",
-    "worktreesUpper": "Worktrees",
-    "showAll": "Show all",
-    "emptyWorktree": "empty · click to start",
-    "staleWorktree": "archived",
-    "messages": "messages",
-    "sessionsShort_one": "session",
-    "sessionsShort_other": "sessions",
-    "older": "Older"
+    "recent": "最近",
+    "worktreesUpper": "ワークツリー",
+    "showAll": "すべて表示",
+    "emptyWorktree": "空 · クリックで開始",
+    "staleWorktree": "アーカイブ済み",
+    "messages": "メッセージ",
+    "sessionsShort_one": "セッション",
+    "sessionsShort_other": "セッション",
+    "older": "以前"
   },
   ...
-    "newSessionInWorktree": "New session in this worktree",
-    "deleteWorktree": "Delete worktree"
+    "newSessionInWorktree": "このワークツリーで新しいセッション",
+    "deleteWorktree": "ワークツリーを削除"

Also applies to: 65-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/ja/sidebar.json` around lines 27 - 35, Several sidebar JSON
keys remain in English; update the Japanese translations for the keys "recent",
"worktreesUpper", "showAll", "emptyWorktree", "staleWorktree", "messages",
"sessionsShort_one", "sessionsShort_other", "older", plus "newSessionInWorktree"
and "deleteWorktree" referenced later, replacing the English values with
appropriate Japanese strings so the ja locale is fully localized (you can ignore
"sessionsShort_one" for plural behavior since i18next uses only "other" for ja).
Locate those keys in src/i18n/locales/ja/sidebar.json and provide natural
Japanese translations for each value.

Comment on lines +27 to +35
"recent": "Recent",
"worktreesUpper": "Worktrees",
"showAll": "Show all",
"emptyWorktree": "empty · click to start",
"staleWorktree": "archived",
"messages": "messages",
"sessionsShort_one": "session",
"sessionsShort_other": "sessions",
"older": "Older"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Multiple Korean strings left untranslated.

The new sidebar UI introduces ~10 keys whose values are still English in ko/sidebar.json (e.g., recent, worktreesUpper, showAll, emptyWorktree, staleWorktree, messages, sessionsShort_one/_other, older, newSessionInWorktree, deleteWorktree). Existing Korean translations were already present for older keys, so this looks like an oversight rather than intentional. Korean users will see a mixed-language sidebar.

Note: i18next treats ko as having only the other plural form, so sessionsShort_one is unused but harmless to keep.

🌐 Suggested translations
-    "recent": "Recent",
-    "worktreesUpper": "Worktrees",
-    "showAll": "Show all",
-    "emptyWorktree": "empty · click to start",
-    "staleWorktree": "archived",
-    "messages": "messages",
-    "sessionsShort_one": "session",
-    "sessionsShort_other": "sessions",
-    "older": "Older"
+    "recent": "최근",
+    "worktreesUpper": "워크트리",
+    "showAll": "모두 보기",
+    "emptyWorktree": "비어 있음 · 클릭하여 시작",
+    "staleWorktree": "보관됨",
+    "messages": "메시지",
+    "sessionsShort_one": "세션",
+    "sessionsShort_other": "세션",
+    "older": "이전"
   },
   ...
-    "newSessionInWorktree": "New session in this worktree",
-    "deleteWorktree": "Delete worktree"
+    "newSessionInWorktree": "이 워크트리에서 새 세션",
+    "deleteWorktree": "워크트리 삭제"

Also applies to: 65-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/ko/sidebar.json` around lines 27 - 35, Several sidebar i18n
keys in ko/sidebar.json remain in English (e.g., "recent", "worktreesUpper",
"showAll", "emptyWorktree", "staleWorktree", "messages", "sessionsShort_one",
"sessionsShort_other", "older", plus "newSessionInWorktree" and
"deleteWorktree") causing a mixed-language UI; replace the English values with
appropriate Korean translations for those keys (keep "sessionsShort_one" if you
prefer even though i18next for ko only uses the "other" plural form) so the
sidebar is fully localized—update the values for the listed keys in the JSON
(matching the exact key names) to their Korean equivalents and ensure JSON
syntax remains valid.

Introduces per-session git worktree control and a redesigned sidebar
that surfaces worktrees as first-class citizens of their parent repo.

Worktrees:
- Per-session worktree toggle in the new-session flow
- Server-side worktree detection via git rev-parse, falling back to
  the .claude/worktrees/<name> path convention
- Repo-grouping logic that synthesizes a parent project for repos
  that have only worktrees discovered so far
- Cleanup hook removes the git worktree directory on project delete
- Fast-path resolves a worktree's directory from its encoded project
  name (preserving dashes in repo names like "developer-tracker"),
  bypassing cwd-vote heuristics that subagent cwds would otherwise
  poison

Sidebar redesign (docs/superpowers/specs/2026-04-25-sidebar-worktree-
redesign-design.md, /plans/2026-04-25-sidebar-worktree-redesign.md):
- Repo header (RepoCard) with current-branch chip, replacing the
  three-level RepoGroup → ProjectItem → Sessions nesting
- "+ New session" full-width primary CTA at top of expanded body
- RECENT section: recency-sorted sessions across main + worktrees
  with branch chips; "Show all N" inline expansion
- WORKTREES section: collapsible, with active worktrees plus a
  separate "Archived (N)" sub-toggle for stale ones
- BranchChip primitive with deterministic per-branch color and
  Home/GitBranch icon variants for main vs worktree origin
- Hover-revealed "+" (new session here) and trash (delete worktree)
  on each worktree row
- Header click toggles expansion only — no implicit project select,
  so mobile users can browse without losing the sidebar
- Single-worktree projects render with no WORKTREES section

Tests:
- src/components/sidebar/utils/__tests__/repoAggregates.test.ts
- server/__tests__/worktree-grouping.test.js
- server/__tests__/claude-sdk.test.js
- src/components/chat/utils/__tests__/chatStorage.test.ts
@ashalliants ashalliants force-pushed the feat/worktree-toggle branch from d097804 to a64785c Compare April 26, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant