Skip to content

feat(sidebar): add workspace management - #2138

Merged
zerob13 merged 2 commits into
devfrom
codex/issue-2115-workspace-sidebar-spec
Aug 11, 2026
Merged

feat(sidebar): add workspace management#2138
zerob13 merged 2 commits into
devfrom
codex/issue-2115-workspace-sidebar-spec

Conversation

@zerob13

@zerob13 zerob13 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Problem

The sidebar treated Session groups as the source of workspace existence. Selecting a directory could persist a valid active environment, but a zero-Session workspace was filtered out and appeared not to have been added.

The same projection caused several related UX problems:

  • users could not add a workspace from the Workspace header;
  • a newly active directory could land behind explicitly ordered workspaces;
  • empty, filtered, paginated, missing, and historical workspaces were conflated;
  • the workspace ellipsis menu was coupled to reorder availability, so it disappeared for a single workspace or during search and offered only movement actions.

UX

Before

Workspace                                      [Group]
  project-a                                      [+] [...]
    Session A
  project-b                                      [+] [...]
    Session B

A selected directory with no Session did not appear.

After

Workspace                               [Add] [Group]
  new-project                         Empty      [+] [...]
  project-a                                      [+] [...]
    Session A
  project-b                                      [+] [...]
    Session B

  [...]
    Move to Top
    Move Up
    Move Down
    Move to Bottom
    ----------------
    Archive

Archive reuses the existing reversible environment-hide flow. It does not introduce a new lifecycle, delete Sessions, delete messages, or touch the real folder.

Solution

  • Add an accessible, guarded Add workspace action to the sidebar header.
  • Project active environments into the sidebar independently of loaded Session groups.
  • Render zero-Session workspaces as Empty and start the first correctly scoped draft through the existing one-shot project intent.
  • Keep Project as the owner of identity, lifecycle, durable counts, and ordering; keep Session as the owner of visible children.
  • Persist newly selected or reactivated directories at the top while leaving duplicate active selection order-stable.
  • Preserve search, agent filters, pinning, pagination, path-keyed collapse state, missing paths, historical groups, and the built-in Chat section.
  • Keep the ellipsis menu available for every active workspace. Movement actions retain their existing gates; Archive calls the existing projectStore.archiveEnvironment(path) action behind confirmation and failure feedback.
  • Document the UX, business rules, implementation boundary, rollback, and validation in SDD artifacts.

Business behavior

  • Adding a workspace registers it without replacing the current New Thread project selection or automatically creating a draft.
  • A committed Project snapshot is required before a workspace row appears.
  • New or reactivated workspaces are persisted first; selecting an already-active workspace does not move it.
  • Empty workspace activation starts a draft with the exact projectDir.
  • Archived zero-Session workspaces disappear from the active list. Existing Session history remains navigable as a historical group without active affordances.
  • Missing, archived, and removed workspaces cannot start new conversations.
  • The built-in Chat workspace is never duplicated under Workspace.

Validation

  • pnpm run format
  • pnpm run i18n
  • pnpm run lint
  • pnpm run typecheck
  • Sidebar component: 62/62 passed
  • Project store: 13/13 passed
  • Session store: 80/80 passed with an 8 GB Node heap
  • New Thread suites: 43/43 passed
  • Focused Project service environment/selection tests: 9/9 passed

Validation notes

  • Native Windows and POSIX manual validation remains pending while this PR is Draft.
  • The optional native SQLite preference-table suite is unavailable in this workspace; its focused regression remains committed for CI.
  • The full Project service suite has two pre-existing Windows path expectation failures (/mock/... versus resolved C:\mock\...); the focused changed contracts pass.
  • Local Node is 24.15.0 while the repository requests Node 24.18 or newer.

Closes #2115

Summary by CodeRabbit

  • New Features

    • Add and manage workspaces directly from the sidebar.
    • View empty, unavailable, archived, and historical workspaces clearly.
    • Start a new chat from an empty workspace.
    • Reorder active workspaces while preserving existing selections.
    • Archive workspaces with confirmation and accessible keyboard/menu actions.
    • Restore archived workspaces through Settings.
  • Bug Fixes

    • Improved folder-picker error handling and workspace registration.
    • Preserved workspace visibility when refreshes fail.
    • Prevented duplicate activation and preserved workspace ordering.
    • Cleared stale selections after workspace removal or archival.
  • Localization

    • Added sidebar workspace translations across supported languages.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds sidebar workspace registration and lifecycle handling. It merges managed environments with session groups, supports empty and archived workspaces, adds versioned project mutations, updates directory ordering, and expands picker, grouping, localization, and regression coverage.

Changes

Workspace registration and sidebar lifecycle

Layer / File(s) Summary
Workspace contracts and implementation plan
docs/features/complete-directory-management/spec.md, docs/features/sidebar-workspace-registration/*
The documentation defines registration, ordering, empty-workspace, archive, lifecycle, accessibility, and validation behavior.
Transactional activation and project contracts
src/main/project/*, src/shared/contracts/routes/project.routes.ts, src/shared/utils/filesystem.ts, src/renderer/api/ProjectClient.ts, test/main/project/*, test/main/routes/*, test/main/shared/*, test/renderer/api/clients.test.ts
Directory selection activates new or archived paths at the top of active order. Project mutations and directory selection return committed revision versions. Snapshots expose the default chat workspace path. Workspace paths use shared normalization.
Project and session store coordination
src/renderer/src/stores/ui/project.ts, src/renderer/src/stores/ui/session.ts, src/renderer/src/pages/NewThreadPage.vue, test/renderer/stores/*
The project store coordinates committed snapshots, picker selection, archive validation, lifecycle cleanup, and error recovery. The session store serializes group-mode persistence and restores the persisted mode after failures.
Sidebar projection and actions
src/renderer/src/components/WindowSideBar.vue, src/renderer/src/i18n/*/chat.json
The sidebar merges environment metadata with session groups, shows empty and unavailable workspaces, supports workspace creation and archive confirmation, and adds localized labels.
Sidebar regression coverage
test/renderer/components/WindowSideBar.test.ts
Tests cover registration, empty-workspace conversion, lifecycle visibility, filtering, ordering, picker behavior, archive success and failure, and Chat handling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WindowSideBar
  participant projectStore
  participant ProjectService
  participant ProjectSnapshot
  User->>WindowSideBar: choose Add Workspace
  WindowSideBar->>projectStore: openFolderPicker({ select: false })
  projectStore->>ProjectService: selectDirectory()
  ProjectService->>ProjectSnapshot: activate directory and commit version
  ProjectSnapshot-->>ProjectService: return path and version
  ProjectService-->>projectStore: return path and version
  projectStore-->>WindowSideBar: apply committed snapshot
  WindowSideBar-->>User: reveal workspace
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: zhangmo8, yyhhyyyyyy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 summarizes the primary change: adding workspace management to the sidebar.
Linked Issues check ✅ Passed The implementation satisfies the coding objectives in [#2115], including workspace creation, empty workspaces, lifecycle handling, ordering, and regression coverage.
Out of Scope Changes check ✅ Passed The code, tests, localization, and documentation changes directly support workspace management and the requirements in [#2115].
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-2115-workspace-sidebar-spec

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.

@zerob13
zerob13 marked this pull request as ready for review August 11, 2026 01:56
@zerob13
zerob13 requested a review from zhangmo8 August 11, 2026 01:58

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/src/stores/ui/session.ts (1)

1311-1314: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the last persisted mode after a failed queued write.

Two failed queued requests can leave groupMode at a mode that was never persisted. If a project write fails and a later time write also fails, the later rollback restores previousMode as project, although the durable setting remains time.

Track the last successfully persisted mode. Use that value for rollback. Add a regression test for two queued failed writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/stores/ui/session.ts` around lines 1311 - 1314, Update the
group-mode persistence flow around the persistError catch to track the last
successfully persisted mode and restore that value on any failed queued write,
rather than using the request-local previousMode. Ensure successful writes
update the tracked persisted mode, and add a regression test covering two queued
failures where rollback must return to the durable mode.
🧹 Nitpick comments (7)
test/renderer/components/WindowSideBar.test.ts (3)

1073-1103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The concurrency test calls an internal method instead of the rendered control.

Lines 1084-1085 invoke (wrapper.vm as any).handleAddWorkspace() twice. The user-visible guard is the disabled add-workspace button. Trigger the button twice so the test covers the observable contract and does not break when the handler is renamed.

♻️ Proposed refactor
-    const firstRequest = (wrapper.vm as any).handleAddWorkspace()
-    const secondRequest = (wrapper.vm as any).handleAddWorkspace()
-    await nextTick()
+    const addButton = wrapper.get('[data-testid="window-sidebar-add-workspace-button"]')
+    await addButton.trigger('click')
+    await nextTick()
+    await addButton.trigger('click')
+    await nextTick()

As per coding guidelines: "Keep committed tests lean and focused on project reliability, stability, and observable contracts; remove temporary checks that only test implementation internals before handoff."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/renderer/components/WindowSideBar.test.ts` around lines 1073 - 1103,
Update the concurrency test to trigger the rendered add-workspace button twice
instead of calling the internal handleAddWorkspace method directly. Use the
existing button selector and preserve the assertions covering a single picker
invocation, disabled state, cancellation side effects, and concurrent-request
behavior.

Source: Coding guidelines


60-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

createEnvironment produces archived and removed fixtures without timestamps.

fallbackStatus sets status, but archivedAt and removedAt still default to null. An archived fixture therefore has status: 'archived' and archivedAt: null, which the main process never produces. If the component ever reads archivedAt or removedAt instead of status, the test passes for the wrong reason. The equivalent helper in test/renderer/stores/projectStore.test.ts derives the timestamps from the status.

♻️ Proposed refactor
   status: environment.status ?? fallbackStatus,
   sortOrder: environment.sortOrder ?? 0,
-  archivedAt: environment.archivedAt ?? null,
-  removedAt: environment.removedAt ?? null
+  archivedAt:
+    environment.archivedAt ?? ((environment.status ?? fallbackStatus) === 'archived' ? 100 : null),
+  removedAt:
+    environment.removedAt ?? ((environment.status ?? fallbackStatus) === 'removed' ? 100 : null)
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/renderer/components/WindowSideBar.test.ts` around lines 60 - 74, Update
createEnvironment so fallbackStatus-derived fixtures also receive the
corresponding archivedAt or removedAt timestamp, matching the
projectStore.test.ts helper; preserve explicitly supplied timestamps and leave
unrelated status handling unchanged.

1045-1048: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the focus target, not that some element received focus.

expect(focusSpy).toHaveBeenCalled() passes when any element calls focus(), including the search input or the add button. The test claims the registered row receives focus. Assert the receiver instead.

♻️ Proposed refactor
-      expect(focusSpy).toHaveBeenCalled()
+      expect(focusSpy.mock.instances).toContain(
+        wrapper.get('[data-group-id="/work/new"]').element
+      )

As per coding guidelines: "Add the smallest regression test for user-visible behavior or a documented contract."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/renderer/components/WindowSideBar.test.ts` around lines 1045 - 1048,
Update the focus assertion in the test around the group ordering expectations to
verify that the registered row element is the receiver of focus, rather than
only checking that focusSpy was called. Preserve the existing ordering
assertions and use the row’s focus target or registered-row selector already
established in the test.

Source: Coding guidelines

src/renderer/src/stores/ui/project.ts (1)

288-291: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The catch block re-wraps errors it produced itself.

When refreshProjectSnapshot fails, error.value becomes Failed to load project snapshot: .... Line 282 throws that string, and this catch overwrites error.value with Failed to open folder picker: Error: Failed to load project snapshot: .... The stored message nests twice. Preserve the original snapshot error instead.

♻️ Proposed refactor
-      await refreshProjectSnapshot()
-      if (error.value) {
-        throw new Error(error.value)
-      }
+      await refreshProjectSnapshot()
+      if (error.value) {
+        // Keep the snapshot failure message; do not re-wrap it below.
+        throw new SnapshotRefreshError(error.value)
+      }
       if (!environments.value.some((environment) => environment.path === selectedPath)) {
         throw new Error('Selected workspace is missing from the project snapshot')
       }
       return selectedPath
     } catch (cause) {
-      error.value = `Failed to open folder picker: ${cause}`
+      if (!(cause instanceof SnapshotRefreshError)) {
+        error.value = `Failed to open folder picker: ${cause}`
+      }
       throw cause
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/stores/ui/project.ts` around lines 288 - 291, Update the
folder-picker error handling around refreshProjectSnapshot so errors already
produced by that snapshot flow are not rewrapped as “Failed to open folder
picker.” Preserve the original snapshot error in error.value while retaining the
existing behavior for genuine folder-picker failures and rethrowing the cause.
src/renderer/src/components/WindowSideBar.vue (2)

477-480: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the redundant inner v-if.

DropdownMenu now renders only when isActiveProjectDirectoryGroup(group) is true. That predicate already implies isProjectDirectoryGroup(group). The v-if on the trigger button is always true here. An empty DropdownMenuTrigger as-child would also break Reka UI if the condition were ever false.

♻️ Proposed change
                     <DropdownMenuTrigger as-child>
                       <DcButton
-                        v-if="isProjectDirectoryGroup(group)"
                         type="button"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/WindowSideBar.vue` around lines 477 - 480, Remove
the redundant v-if="isProjectDirectoryGroup(group)" from the DcButton inside
DropdownMenuTrigger in the isActiveProjectDirectoryGroup(group) branch, leaving
the trigger and button rendered directly whenever the active directory group
condition passes.

438-449: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The title attribute on an SVG does not create a native tooltip.

Icon renders an <svg> element. Browsers show tooltips for SVG only when a <title> child element exists, not from a title attribute. The sr-only span at Lines 450-456 keeps the text available to screen readers, so this is visual only. Wrap the icon in a <span :title="..."> to restore the hover tooltip.

💡 Proposed change
-                    <Icon
-                      v-if="isWorkspaceUnavailable(group)"
-                      icon="lucide:circle-alert"
-                      data-testid="window-sidebar-workspace-unavailable"
-                      aria-hidden="true"
-                      class="ml-auto size-3.5 shrink-0 text-amber-500"
-                      :title="
-                        t('chat.input.workspaceUnavailableTooltip', {
-                          path: getGroupIdentifier(group)
-                        })
-                      "
-                    />
+                    <span
+                      v-if="isWorkspaceUnavailable(group)"
+                      class="ml-auto flex shrink-0 items-center"
+                      :title="
+                        t('chat.input.workspaceUnavailableTooltip', {
+                          path: getGroupIdentifier(group)
+                        })
+                      "
+                    >
+                      <Icon
+                        icon="lucide:circle-alert"
+                        data-testid="window-sidebar-workspace-unavailable"
+                        aria-hidden="true"
+                        class="size-3.5 text-amber-500"
+                      />
+                    </span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/WindowSideBar.vue` around lines 438 - 449, Move
the workspace-unavailable tooltip binding from the Icon component to a wrapping
span around it, keeping the existing translated message and preserving the
Icon’s accessibility and styling attributes. Use the existing
isWorkspaceUnavailable, getGroupIdentifier, and translation expression
unchanged.
src/renderer/src/i18n/fa-IR/chat.json (1)

353-355: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Check the RTL layout of the new sidebar indicators.

The translations are correct. fa-IR and he-IL are RTL locales. WindowSideBar.vue positions the Empty label (Line 434) and the unavailable icon (Line 443) with ml-auto. That class does not flip under dir="rtl". Use the logical ms-auto if the app renders these locales with dir="rtl".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/i18n/fa-IR/chat.json` around lines 353 - 355, Update the
sidebar indicator positioning in WindowSideBar.vue by replacing the physical
ml-auto utility on the Empty label and unavailable icon with the logical ms-auto
utility, preserving correct alignment for both LTR and RTL locales.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/project/index.ts`:
- Around line 253-265: Serialize the post-picker active-environment read and
update sequence around getEnvironments(), markActive(), and reorderActive() so
concurrent selections cannot compute ordering from the same stale list. Ensure
each selection re-reads and reorders the complete active list atomically,
preserving selection order and preventing duplicate sort_order values. Add a
regression test covering concurrent selections.

In `@src/renderer/src/components/WindowSideBar.vue`:
- Around line 1588-1615: Update handleAddWorkspace to await the project
environment refresh after openFolderPicker resolves, and assign
projectEnvironmentMetadataReady from the refresh result instead of setting it
unconditionally; then ensure revealWorkspaceGroup performs its [data-group-id]
lookup after the refresh or retries across several animation frames before
giving up. Apply these changes at src/renderer/src/components/WindowSideBar.vue
lines 1588-1615 and 1559-1575.
- Around line 930-943: Normalize project paths with the same separator-free path
normalization used by groupByProject before creating
activeProjectEnvironmentByPath, historicalProjectEnvironmentByPath,
selectableProjectPathSet, and projectOrderIndex. Apply that normalization to
every key and lookup in sessionGroupByPath as well, preserving lookups for paths
with trailing separators.

In `@src/renderer/src/stores/ui/project.ts`:
- Around line 280-286: Update openFolderPicker and the selectDirectory flow to
return the bumped snapshot version together with the selected path, then pass
that version as minVersion to refreshProjectSnapshot so validation waits for the
correct refresh. If post-selectProject validation fails, restore the previous
workspace selection before propagating the error.

---

Outside diff comments:
In `@src/renderer/src/stores/ui/session.ts`:
- Around line 1311-1314: Update the group-mode persistence flow around the
persistError catch to track the last successfully persisted mode and restore
that value on any failed queued write, rather than using the request-local
previousMode. Ensure successful writes update the tracked persisted mode, and
add a regression test covering two queued failures where rollback must return to
the durable mode.

---

Nitpick comments:
In `@src/renderer/src/components/WindowSideBar.vue`:
- Around line 477-480: Remove the redundant
v-if="isProjectDirectoryGroup(group)" from the DcButton inside
DropdownMenuTrigger in the isActiveProjectDirectoryGroup(group) branch, leaving
the trigger and button rendered directly whenever the active directory group
condition passes.
- Around line 438-449: Move the workspace-unavailable tooltip binding from the
Icon component to a wrapping span around it, keeping the existing translated
message and preserving the Icon’s accessibility and styling attributes. Use the
existing isWorkspaceUnavailable, getGroupIdentifier, and translation expression
unchanged.

In `@src/renderer/src/i18n/fa-IR/chat.json`:
- Around line 353-355: Update the sidebar indicator positioning in
WindowSideBar.vue by replacing the physical ml-auto utility on the Empty label
and unavailable icon with the logical ms-auto utility, preserving correct
alignment for both LTR and RTL locales.

In `@src/renderer/src/stores/ui/project.ts`:
- Around line 288-291: Update the folder-picker error handling around
refreshProjectSnapshot so errors already produced by that snapshot flow are not
rewrapped as “Failed to open folder picker.” Preserve the original snapshot
error in error.value while retaining the existing behavior for genuine
folder-picker failures and rethrowing the cause.

In `@test/renderer/components/WindowSideBar.test.ts`:
- Around line 1073-1103: Update the concurrency test to trigger the rendered
add-workspace button twice instead of calling the internal handleAddWorkspace
method directly. Use the existing button selector and preserve the assertions
covering a single picker invocation, disabled state, cancellation side effects,
and concurrent-request behavior.
- Around line 60-74: Update createEnvironment so fallbackStatus-derived fixtures
also receive the corresponding archivedAt or removedAt timestamp, matching the
projectStore.test.ts helper; preserve explicitly supplied timestamps and leave
unrelated status handling unchanged.
- Around line 1045-1048: Update the focus assertion in the test around the group
ordering expectations to verify that the registered row element is the receiver
of focus, rather than only checking that focusSpy was called. Preserve the
existing ordering assertions and use the row’s focus target or registered-row
selector already established in the test.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 455d099f-dfab-43d2-921f-48de66bbff58

📥 Commits

Reviewing files that changed from the base of the PR and between 4d02c92 and 61c510f.

📒 Files selected for processing (34)
  • docs/features/complete-directory-management/spec.md
  • docs/features/sidebar-workspace-registration/plan.md
  • docs/features/sidebar-workspace-registration/spec.md
  • docs/features/sidebar-workspace-registration/tasks.md
  • src/main/project/index.ts
  • src/renderer/src/components/WindowSideBar.vue
  • src/renderer/src/i18n/da-DK/chat.json
  • src/renderer/src/i18n/de-DE/chat.json
  • src/renderer/src/i18n/en-US/chat.json
  • src/renderer/src/i18n/es-ES/chat.json
  • src/renderer/src/i18n/fa-IR/chat.json
  • src/renderer/src/i18n/fr-FR/chat.json
  • src/renderer/src/i18n/he-IL/chat.json
  • src/renderer/src/i18n/id-ID/chat.json
  • src/renderer/src/i18n/it-IT/chat.json
  • src/renderer/src/i18n/ja-JP/chat.json
  • src/renderer/src/i18n/ko-KR/chat.json
  • src/renderer/src/i18n/ms-MY/chat.json
  • src/renderer/src/i18n/pl-PL/chat.json
  • src/renderer/src/i18n/pt-BR/chat.json
  • src/renderer/src/i18n/ru-RU/chat.json
  • src/renderer/src/i18n/tr-TR/chat.json
  • src/renderer/src/i18n/vi-VN/chat.json
  • src/renderer/src/i18n/zh-CN/chat.json
  • src/renderer/src/i18n/zh-HK/chat.json
  • src/renderer/src/i18n/zh-TW/chat.json
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/stores/ui/project.ts
  • src/renderer/src/stores/ui/session.ts
  • test/main/project/data/tables/newEnvironmentPreferencesTable.test.ts
  • test/main/project/projectService.test.ts
  • test/renderer/components/WindowSideBar.test.ts
  • test/renderer/stores/projectStore.test.ts
  • test/renderer/stores/sessionStore.test.ts

Comment thread src/main/project/index.ts Outdated
Comment thread src/renderer/src/components/WindowSideBar.vue
Comment on lines +1588 to +1615
const handleAddWorkspace = async () => {
if (isAddingWorkspace.value) {
return
}

isAddingWorkspace.value = true
try {
const selectedPath = await projectStore.openFolderPicker({ select: false })
if (!selectedPath) {
return
}

projectEnvironmentMetadataReady.value = true
sessionSearchQuery.value = ''
await sessionStore.setGroupMode('project')
await revealWorkspaceGroup(selectedPath)
} catch (error) {
console.warn('[WindowSideBar] Failed to add workspace:', error)
notifyRenderer({
kind: 'error',
code: 'chat.workspace.registrationFailed',
title: t('common.error.operationFailed'),
description: t('chat.sidebar.addWorkspaceFailed')
})
} finally {
isAddingWorkspace.value = false
}
}

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

handleAddWorkspace does not await an environment refresh after the folder pick. The flow marks the environment metadata ready and then reveals the group, but nothing guarantees that projectStore.environments already contains the new path. Both sites fail in the same way when the store snapshot is still in flight: the merged projection reads stale environments, and the reveal query finds no [data-group-id] element.

  • src/renderer/src/components/WindowSideBar.vue#L1588-L1615: await an environment refresh after openFolderPicker resolves, and set projectEnvironmentMetadataReady from that result instead of setting it unconditionally at Line 1600.
  • src/renderer/src/components/WindowSideBar.vue#L1559-L1575: run the [data-group-id] lookup after the refresh resolves, or retry the lookup across a few animation frames before you give up.
📍 Affects 1 file
  • src/renderer/src/components/WindowSideBar.vue#L1588-L1615 (this comment)
  • src/renderer/src/components/WindowSideBar.vue#L1559-L1575
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/WindowSideBar.vue` around lines 1588 - 1615,
Update handleAddWorkspace to await the project environment refresh after
openFolderPicker resolves, and assign projectEnvironmentMetadataReady from the
refresh result instead of setting it unconditionally; then ensure
revealWorkspaceGroup performs its [data-group-id] lookup after the refresh or
retries across several animation frames before giving up. Apply these changes at
src/renderer/src/components/WindowSideBar.vue lines 1588-1615 and 1559-1575.

Comment thread src/renderer/src/stores/ui/project.ts Outdated

@zhangmo8 zhangmo8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM 👍

Reviewed the projection change and verified locally on macOS:

  • Sidebar now projects projectStore.environments as the source of truth and merges Session groups as children — exactly the direction #2115 called for. Empty workspaces render with the Empty label and clicking them starts a correctly scoped draft.
  • reorderActive's conditional upsert (WHERE status = 'active') cleanly prevents a reorder from resurrecting archived environments, and the wasActive guard in selectDirectory keeps duplicate selections order-stable as described.
  • setGroupMode persisting the captured mode (instead of re-reading groupMode.value) plus the rollback-and-rethrow chain is a nice concurrency fix along the way.
  • Both callers of the now-throwing openFolderPicker handle the rejection with user-facing feedback.
  • All 20 locale files contain the three new chat.sidebar.* keys.
  • Ran locally: WindowSideBar 62/62, projectStore 13/13, projectService 31/31 (the 4 native SQLite table tests skip as documented), and pnpm run typecheck passes.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
test/main/routes/dispatcher.test.ts (1)

5617-5626: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Assert the published mutation version.

These assertions validate only the route response. The publishEnvironmentsChanged fixture discards its version argument and publishes Date.now(). A regression in archive or selection event-version propagation would pass.

Capture the callback arguments. Assert that both mutations publish version 1.

As per coding guidelines, “Add the smallest regression test for user-visible behavior or a documented contract.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/routes/dispatcher.test.ts` around lines 5617 - 5626, Update the
test covering archive and directory selection mutations to capture the arguments
passed to the publishEnvironmentsChanged fixture instead of discarding its
version value. Assert that both mutation events publish version 1, while
preserving the existing route-response assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/renderer/src/stores/ui/project.ts`:
- Around line 228-231: Update setDefaultProject, reorderEnvironments,
restoreEnvironment, and removeEnvironment to capture the mutation result
returned by their configClient calls, with each route exposing its committed
version. Pass result.version to requireProjectSnapshot instead of 0 so the store
waits for the exact mutation snapshot, and add regression tests covering an
older in-flight refresh for each mutation.

In `@test/renderer/api/clients.test.ts`:
- Line 2925: Update the `bridge.invoke` assertion for `tools.listDefinitions` to
expect invocation 13 instead of 12, while leaving the asserted event and
arguments unchanged.

---

Outside diff comments:
In `@test/main/routes/dispatcher.test.ts`:
- Around line 5617-5626: Update the test covering archive and directory
selection mutations to capture the arguments passed to the
publishEnvironmentsChanged fixture instead of discarding its version value.
Assert that both mutation events publish version 1, while preserving the
existing route-response assertions.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c064c610-1808-4b20-bd22-c5153542bd70

📥 Commits

Reviewing files that changed from the base of the PR and between 61c510f and 04c0808.

📒 Files selected for processing (23)
  • docs/features/complete-directory-management/spec.md
  • docs/features/sidebar-workspace-registration/plan.md
  • docs/features/sidebar-workspace-registration/spec.md
  • docs/features/sidebar-workspace-registration/tasks.md
  • src/main/project/data/tables/newEnvironmentPreferences.ts
  • src/main/project/index.ts
  • src/main/project/routes.ts
  • src/renderer/api/ProjectClient.ts
  • src/renderer/src/components/WindowSideBar.vue
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/stores/ui/project.ts
  • src/renderer/src/stores/ui/session.ts
  • src/shared/contracts/routes/project.routes.ts
  • src/shared/utils/filesystem.ts
  • test/main/project/data/tables/newEnvironmentPreferencesTable.test.ts
  • test/main/project/projectService.test.ts
  • test/main/routes/contracts.test.ts
  • test/main/routes/dispatcher.test.ts
  • test/main/shared/filesystem.test.ts
  • test/renderer/api/clients.test.ts
  • test/renderer/components/WindowSideBar.test.ts
  • test/renderer/stores/projectStore.test.ts
  • test/renderer/stores/sessionStore.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/features/complete-directory-management/spec.md
  • src/renderer/src/components/WindowSideBar.vue

Comment on lines 228 to +231
async function setDefaultProject(path: string | null): Promise<void> {
try {
await configClient.setDefaultProjectPath(normalizePath(path))
await refreshProjectSnapshot()
await requireProjectSnapshot()

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Return and require the exact mutation snapshot version.

These mutations call requireProjectSnapshot() with version 0. Their routes return no version, although they publish versioned environment or default-project events. If an older refresh is in flight, this code can accept that stale snapshot and report mutation success before the committed projection includes the change.

Return version from config.setDefaultProjectPath, project.reorderEnvironments, project.restoreEnvironment, and project.removeEnvironment. Then pass each returned version to requireProjectSnapshot(result.version). Add stale-refresh regression tests for these mutations.

Proposed store-side change
-      await projectClient.reorderEnvironments(orderedPaths)
-      await requireProjectSnapshot()
+      const result = await projectClient.reorderEnvironments(orderedPaths)
+      await requireProjectSnapshot(result.version)

-      await projectClient.restoreEnvironment(path)
-      await requireProjectSnapshot()
+      const result = await projectClient.restoreEnvironment(path)
+      await requireProjectSnapshot(result.version)

       const result = await projectClient.removeEnvironment(path)
-      await requireProjectSnapshot()
+      await requireProjectSnapshot(result.version)

Also applies to: 247-248, 279-280, 289-290

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/stores/ui/project.ts` around lines 228 - 231, Update
setDefaultProject, reorderEnvironments, restoreEnvironment, and
removeEnvironment to capture the mutation result returned by their configClient
calls, with each route exposing its committed version. Pass result.version to
requireProjectSnapshot instead of 0 so the store waits for the exact mutation
snapshot, and add regression tests covering an older in-flight refresh for each
mutation.

expect(bridge.on).toHaveBeenCalledWith('project:environments-changed', expect.any(Function))
expect(unsubscribe).toEqual(expect.any(Function))
expect(bridge.invoke).toHaveBeenNthCalledWith(11, 'tools.listDefinitions', {
expect(bridge.invoke).toHaveBeenNthCalledWith(12, 'tools.listDefinitions', {

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Correct the invocation ordinal.

Line 2921 verifies invocation 11. Line 2887 performs another project.selectDirectory invocation. tools.listDefinitions is invocation 13, not invocation 12. This assertion fails.

Proposed fix
-    expect(bridge.invoke).toHaveBeenNthCalledWith(12, 'tools.listDefinitions', {
+    expect(bridge.invoke).toHaveBeenNthCalledWith(13, 'tools.listDefinitions', {
📝 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
expect(bridge.invoke).toHaveBeenNthCalledWith(12, 'tools.listDefinitions', {
expect(bridge.invoke).toHaveBeenNthCalledWith(13, 'tools.listDefinitions', {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/renderer/api/clients.test.ts` at line 2925, Update the `bridge.invoke`
assertion for `tools.listDefinitions` to expect invocation 13 instead of 12,
while leaving the asserted event and arguments unchanged.

@zerob13
zerob13 merged commit 439af91 into dev Aug 11, 2026
10 of 12 checks passed
@zhangmo8
zhangmo8 deleted the codex/issue-2115-workspace-sidebar-spec branch August 11, 2026 03:25
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.

[feature] Add workspaces from the sidebar and show empty workspaces

2 participants