Skip to content

Add sidebar project pinning#151

Open
friuns2 wants to merge 5 commits intomainfrom
codex/sidebar-project-pinning
Open

Add sidebar project pinning#151
friuns2 wants to merge 5 commits intomainfrom
codex/sidebar-project-pinning

Conversation

@friuns2
Copy link
Copy Markdown
Owner

@friuns2 friuns2 commented May 9, 2026

Summary

  • add Codex.app-compatible pinned project state via pinned-project-ids
  • expose Pin project / Unpin project in the sidebar project menu
  • keep pinned projects above normal project-order without rewriting project-order
  • document manual testing and llm-wiki behavior notes

Verification

  • pnpm exec vitest run src/composables/useDesktopState.test.ts
  • pnpm exec vue-tsc --noEmit
  • pnpm run build:frontend
  • Chrome parity checks against http://127.0.0.1:4173 with light and dark screenshots under output/playwright/

Performance audit

  • Built-in profile helper was blocked by missing local Playwright browser binary.
  • Fallback Chrome startup/request capture showed 1 /codex-api/workspace-roots-state request, 25 total /codex-api/ requests, DOMContentLoaded about 90 ms.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add sidebar project pinning with Codex.app state compatibility

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add Codex.app-compatible pinned project state via pinnedProjectIds
• Expose Pin/Unpin project actions in sidebar project menu
• Render pinned projects above regular projects while preserving project order
• Persist pinned state through workspace roots state and global configuration
Diagram
flowchart LR
  A["Project Menu"] -->|Pin/Unpin| B["setProjectPinned"]
  B -->|Update State| C["pinnedProjectIds"]
  C -->|Persist| D["Workspace Roots State"]
  D -->|Save| E["Global State File"]
  C -->|Reorder| F["orderGroupsWithPinnedProjects"]
  F -->|Render| G["Sidebar Display"]
Loading

Grey Divider

File Changes

1. src/api/codexGateway.ts ✨ Enhancement +3/-0

Add pinnedProjectIds to WorkspaceRootsState type

src/api/codexGateway.ts


2. src/composables/useDesktopState.test.ts 🧪 Tests +30/-0

Add test for pinned projects ordering

src/composables/useDesktopState.test.ts


3. src/composables/useDesktopState.ts ✨ Enhancement +104/-3

Implement pinned project state and ordering logic

src/composables/useDesktopState.ts


View more (8)
4. src/server/codexAppServerBridge.ts ✨ Enhancement +7/-0

Add pinnedProjectIds persistence in workspace state

src/server/codexAppServerBridge.ts


5. src/App.vue ✨ Enhancement +12/-2

Wire pinned project names and pin/unpin handler

src/App.vue


6. src/components/sidebar/SidebarThreadTree.vue ✨ Enhancement +17/-0

Add Pin/Unpin project menu items and toggle logic

src/components/sidebar/SidebarThreadTree.vue


7. llm-wiki/raw/features/sidebar-project-pinning.md 📝 Documentation +11/-0

Document sidebar project pinning implementation details

llm-wiki/raw/features/sidebar-project-pinning.md


8. llm-wiki/wiki/concepts/sidebar-project-pinning.md 📝 Documentation +25/-0

Create concept documentation for project pinning

llm-wiki/wiki/concepts/sidebar-project-pinning.md


9. llm-wiki/wiki/index.md 📝 Documentation +2/-0

Add sidebar project pinning to wiki index

llm-wiki/wiki/index.md


10. llm-wiki/wiki/log.md 📝 Documentation +6/-0

Log sidebar project pinning feature addition

llm-wiki/wiki/log.md


11. tests.md 🧪 Tests +32/-0

Add manual verification steps for project pinning

tests.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 9, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Remote projects cache wiped ✓ Resolved 🐞 Bug ≡ Correctness
Description
useDesktopState.setProjectPinned() writes workspace-roots-state without remoteProjects, and
codexGateway.setWorkspaceRootsState() replaces the cached state with exactly what was passed. After
pin/unpin, subsequent getWorkspaceRootsState() reads from cache and can behave as if there are no
remote projects until a refetch/reload.
Code

src/composables/useDesktopState.ts[R5166-5172]

+      await setWorkspaceRootsState({
+        order: rootsState.order,
+        labels: rootsState.labels,
+        active: rootsState.active,
+        projectOrder: rootsState.projectOrder,
+        pinnedProjectIds: nextPinnedProjectIds,
+      })
Evidence
The new pin/unpin path updates workspace-roots-state but omits remoteProjects, while the gateway
caches the provided state by cloning it; cloneWorkspaceRootsState defaults remoteProjects to [] when
missing, which drops remote projects from cache.

src/composables/useDesktopState.ts[5151-5183]
src/api/codexGateway.ts[2733-2743]
src/api/codexGateway.ts[2350-2358]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`setProjectPinned()` calls `setWorkspaceRootsState()` without `remoteProjects`. `codexGateway.setWorkspaceRootsState()` then overwrites `cachedWorkspaceRootsState` with a clone of the partial payload, causing `remoteProjects` to become `[]` in cache after pin/unpin.
### Issue Context
The server bridge preserves `remoteProjects` on PUT, but the client cache does not.
### Fix Focus Areas
- src/composables/useDesktopState.ts[5166-5172]
- src/api/codexGateway.ts[2733-2743]
### Implementation notes
Choose one:
1) In `setProjectPinned()` (and ideally other callers), include `remoteProjects: rootsState.remoteProjects ?? []` in the `setWorkspaceRootsState` payload.
2) Make `codexGateway.setWorkspaceRootsState()` merge missing fields from the current cached state (or invalidate cache after PUT and force the next `getWorkspaceRootsState()` to refetch).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Projectless order breaks on pin ✓ Resolved 🐞 Bug ≡ Correctness
Description
orderGroupsWithPinnedProjects() re-sorts the entire group list without the projectless guard used by
the workspace-order sort, so pinned projects can shift projectless groups’ position. This changes
sidebar ordering unexpectedly whenever pinned projects exist alongside projectless groups.
Code

src/composables/useDesktopState.ts[R1087-1102]

+function orderGroupsWithPinnedProjects(
+  groups: UiProjectGroup[],
+  pinnedProjectNames: string[],
+): UiProjectGroup[] {
+  if (pinnedProjectNames.length === 0) return groups
+  const pinnedIndexByName = new Map(pinnedProjectNames.map((name, index) => [name, index]))
+  return [...groups].sort((first, second) => {
+    const firstPinnedIndex = pinnedIndexByName.get(first.projectName)
+    const secondPinnedIndex = pinnedIndexByName.get(second.projectName)
+    if (firstPinnedIndex !== undefined && secondPinnedIndex !== undefined) {
+      return firstPinnedIndex - secondPinnedIndex
+    }
+    if (firstPinnedIndex !== undefined) return -1
+    if (secondPinnedIndex !== undefined) return 1
+    return 0
+  })
Evidence
The workspace ordering step explicitly keeps projectless groups stable (return 0 when either side
is projectless), but the subsequent pinned-project sort does not, effectively undoing that
constraint.

src/composables/useDesktopState.ts[1087-1103]
src/composables/useDesktopState.ts[1192-1209]
src/composables/useDesktopState.ts[1355-1357]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`orderGroupsWithPinnedProjects()` sorts all groups (including projectless). When called after `orderGroupsByWorkspaceProjectOrder()`, it can move projectless groups relative to projects.
### Issue Context
`orderGroupsByWorkspaceProjectOrder()` already treats projectless groups specially to keep their relative ordering stable.
### Fix Focus Areas
- src/composables/useDesktopState.ts[1087-1103]
- src/composables/useDesktopState.ts[1192-1209]
### Implementation notes
Add an early comparator guard in `orderGroupsWithPinnedProjects()` similar to the workspace-order sort:
- If `isProjectlessGroup(first) || isProjectlessGroup(second)`, return `0`.
This preserves projectless placement while still lifting pinned non-projectless groups.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Pins non-durable IDs ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolveProjectPinnedId() falls back to pinning a thread cwd or even the project display name when it
can’t match a workspace root or remote id. This can persist values like empty-cwd group names (e.g.
"Projectless") or arbitrary cwds into pinnedProjectIds, diverging from the intended durable
identifiers.
Code

src/composables/useDesktopState.ts[R5137-5140]

+    const group = sourceGroups.value.find((item) => item.projectName === normalizedName)
+    const cwd = group?.threads[0]?.cwd?.trim() ?? ''
+    return cwd || normalizedName
+  }
Evidence
The pin-id resolution explicitly returns cwd || normalizedName as a last resort, while the UI
always exposes Pin/Unpin for every project row; additionally, the project naming helper can produce
the literal 'Projectless' for empty paths, making a non-path pinned ID plausible.

src/composables/useDesktopState.ts[5130-5140]
src/components/sidebar/SidebarThreadTree.vue[316-321]
src/components/sidebar/SidebarThreadTree.vue[1106-1112]
src/pathUtils.ts[47-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolveProjectPinnedId()` can return a non-root/non-remote identifier (`cwd` or even `projectName`), which is then persisted into `pinnedProjectIds`. This can write values that Codex global state likely won’t treat as a stable project identifier.
### Issue Context
The sidebar always shows Pin/Unpin, so users can attempt to pin groups that don’t map cleanly to `rootsState.order` or `remoteProjects`.
### Fix Focus Areas
- src/composables/useDesktopState.ts[5130-5173]
- src/components/sidebar/SidebarThreadTree.vue[316-321]
### Implementation notes
- Change `resolveProjectPinnedId()` to return `''` unless it can resolve to:
- a `rootsState.remoteProjects[].id`, or
- a `rootsState.order` rootPath that matches `matchesWorkspaceRootProject()`.
- In `setProjectPinned()`, abort if the resolved ID is empty.
- Optionally hide/disable the Pin/Unpin menu item for groups that cannot be resolved to a durable pinned id.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/composables/useDesktopState.ts
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.

2 participants