From 1d7f3f96412bbdcb917fb15b2fa024de6c121f6f Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 9 May 2026 09:22:01 +0700 Subject: [PATCH 1/5] Add sidebar project pinning --- .../raw/features/sidebar-project-pinning.md | 11 ++ .../wiki/concepts/sidebar-project-pinning.md | 25 ++++ llm-wiki/wiki/index.md | 2 + llm-wiki/wiki/log.md | 6 + src/App.vue | 14 ++- src/api/codexGateway.ts | 3 + src/components/sidebar/SidebarThreadTree.vue | 17 +++ src/composables/useDesktopState.test.ts | 30 +++++ src/composables/useDesktopState.ts | 107 +++++++++++++++++- src/server/codexAppServerBridge.ts | 7 ++ tests.md | 32 ++++++ 11 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 llm-wiki/raw/features/sidebar-project-pinning.md create mode 100644 llm-wiki/wiki/concepts/sidebar-project-pinning.md diff --git a/llm-wiki/raw/features/sidebar-project-pinning.md b/llm-wiki/raw/features/sidebar-project-pinning.md new file mode 100644 index 000000000..dd0ba1b31 --- /dev/null +++ b/llm-wiki/raw/features/sidebar-project-pinning.md @@ -0,0 +1,11 @@ +# Source: Sidebar Project Pinning + +Date: 2026-05-09 + +The sidebar now mirrors Codex.app project pinning. Codex.app exposes `Pin project` from each project row action menu and persists the selection in `~/.codex/.codex-global-state.json` under `pinned-project-ids`. + +The web bridge reads and writes this key through `/codex-api/workspace-roots-state` as `pinnedProjectIds`. Existing workspace root fields remain preserved when pinning changes: `electron-saved-workspace-roots`, `electron-workspace-root-labels`, `active-workspace-roots`, `project-order`, and `remote-projects`. + +Pinned projects are rendered before regular projects while preserving the pinned order. Non-pinned projects continue to follow Codex `project-order`. Duplicate leaf-name projects are resolved through the same full-path disambiguation used for workspace roots, and remote projects keep their remote project id as the pinned id. + +The project action menu now shows `Pin project` or `Unpin project` depending on the current pinned state. Pinning does not rewrite manual project order; it only updates `pinned-project-ids`. diff --git a/llm-wiki/wiki/concepts/sidebar-project-pinning.md b/llm-wiki/wiki/concepts/sidebar-project-pinning.md new file mode 100644 index 000000000..8bc249c40 --- /dev/null +++ b/llm-wiki/wiki/concepts/sidebar-project-pinning.md @@ -0,0 +1,25 @@ +# Sidebar Project Pinning + +Sidebar project pinning follows Codex.app global state instead of treating pinning as a local-only reorder. + +## Behavior + +- Project row actions include `Pin project` for unpinned projects and `Unpin project` for pinned projects. +- Pinned projects render before regular projects. +- Pinned project order follows `pinned-project-ids`. +- Regular project order continues to follow `project-order`. +- Pinning preserves the existing workspace-root state fields and only changes `pinned-project-ids`. + +## State + +Codex.app stores pinned project ids in `~/.codex/.codex-global-state.json` under `pinned-project-ids`. The web bridge exposes that key as `pinnedProjectIds` in `/codex-api/workspace-roots-state`. + +Local projects use the workspace root path as the durable pinned id. Remote projects use the remote project id. Duplicate folder names keep using the existing full-path project disambiguation before matching pinned rows. + +## Verification Notes + +Manual verification should check both light and dark themes because this feature changes the project row action menu. A focused unit test should assert that a pinned project appears before the rest of the Codex `project-order`. + +## Sources + +- [Sidebar project pinning source](../../raw/features/sidebar-project-pinning.md) diff --git a/llm-wiki/wiki/index.md b/llm-wiki/wiki/index.md index 94b762bd3..142e545ca 100644 --- a/llm-wiki/wiki/index.md +++ b/llm-wiki/wiki/index.md @@ -12,6 +12,7 @@ - [concepts/merge-to-main-workflow.md](./concepts/merge-to-main-workflow.md): branch integration and conflict-resolution workflow. - [concepts/opencode-zen-big-pickle.md](./concepts/opencode-zen-big-pickle.md): OpenCode Zen Big Pickle model configuration for Codex CLI and OpenCode CLI. - [concepts/realtime-chat-rendering.md](./concepts/realtime-chat-rendering.md): realtime chat rendering, sync-churn reduction, and inline media sanitization. +- [concepts/sidebar-project-pinning.md](./concepts/sidebar-project-pinning.md): Codex.app-style project pinning state, ordering, and sidebar menu behavior. - [concepts/skills-route-ui.md](./concepts/skills-route-ui.md): Skills route naming, first-launch Plugins card persistence, dark-theme fixes, and verification lessons. - [concepts/thread-heartbeat-automations.md](./concepts/thread-heartbeat-automations.md): thread-scoped heartbeat automation storage, multi-automation management, and manual run behavior. @@ -19,6 +20,7 @@ - [../raw/features/integrated-terminal.md](../raw/features/integrated-terminal.md): source facts for the integrated terminal implementation and follow-up tests. - [../raw/features/directory-hub-composio-skills-search.md](../raw/features/directory-hub-composio-skills-search.md): source facts for Directory Hub, Composio connectors, Skills search/install, and edge-case tests. - [../raw/features/realtime-chat-rendering-inline-media.md](../raw/features/realtime-chat-rendering-inline-media.md): source facts for realtime chat rendering and inline media sanitization. +- [../raw/features/sidebar-project-pinning.md](../raw/features/sidebar-project-pinning.md): source facts for Codex.app-style sidebar project pinning. - [../raw/features/skills-route-ui-and-first-launch-card.md](../raw/features/skills-route-ui-and-first-launch-card.md): source facts for the Skills route rename, first-launch Plugins card, dark-theme fix, and dev-server workflow adjustment. - [../raw/features/thread-heartbeat-automations.md](../raw/features/thread-heartbeat-automations.md): source facts for thread heartbeat automations, multiple automations per thread, and Run now queue behavior. - [../raw/projects/codex-web-local.md](../raw/projects/codex-web-local.md): immutable source snapshot for project facts. diff --git a/llm-wiki/wiki/log.md b/llm-wiki/wiki/log.md index 6f1bc3d43..687ed2cbb 100644 --- a/llm-wiki/wiki/log.md +++ b/llm-wiki/wiki/log.md @@ -46,3 +46,9 @@ - Updated wiki page: `concepts/opencode-zen-big-pickle.md`. - Documents: DeepSeek thinking-mode `reasoning_content` round-trip requirement, Chat-shaped Zen proxy endpoint selection, streaming reasoning preservation, Docker validation, and the `/tmp/app.tar` restart gotcha. - Updated `index.md`. + +## [2026-05-09] ingest | sidebar project pinning +- Added source: `raw/features/sidebar-project-pinning.md`. +- Created wiki page: `concepts/sidebar-project-pinning.md`. +- Documents: Codex.app `pinned-project-ids` state, project menu pin/unpin behavior, pinned ordering before `project-order`, and workspace-root preservation. +- Updated `index.md`. diff --git a/src/App.vue b/src/App.vue index 8106e4068..fbf9c3d42 100644 --- a/src/App.vue +++ b/src/App.vue @@ -62,6 +62,7 @@ = { const { projectGroups, + pinnedProjectNames, projectDisplayNameById, selectedThread, selectedThreadTokenUsage, @@ -1230,6 +1233,7 @@ const { removeProject, reorderProject, pinProjectToTop, + setProjectPinned, startPolling, stopPolling, primeSelectedThread, @@ -1263,10 +1267,11 @@ const gitRepoStatusRequestByCwd = new Map>() const newWorktreeBaseBranch = ref('') const worktreeBranchOptions = ref([]) const isLoadingWorktreeBranches = ref(false) -const workspaceRootOptionsState = ref<{ order: string[]; labels: Record; projectOrder: string[] }>({ +const workspaceRootOptionsState = ref<{ order: string[]; labels: Record; projectOrder: string[]; pinnedProjectIds: string[] }>({ order: [], labels: {}, projectOrder: [], + pinnedProjectIds: [], }) const worktreeInitStatus = ref<{ phase: 'idle' | 'running' | 'error'; title: string; message: string }>({ phase: 'idle', @@ -2438,6 +2443,10 @@ function onReorderProject(payload: { projectName: string; toIndex: number }): vo reorderProject(payload.projectName, payload.toIndex) } +function onSetProjectPinned(payload: { projectName: string; pinned: boolean }): void { + void setProjectPinned(payload.projectName, payload.pinned) +} + function onRequestProjectGitStatus(projectName: string): void { const group = projectGroups.value.find((entry) => entry.projectName === projectName) const cwd = resolvePreferredLocalCwd(projectName, group?.threads[0]?.cwd?.trim() ?? '') @@ -3283,9 +3292,10 @@ async function loadWorkspaceRootOptionsState(): Promise { order: [...state.order], labels: { ...state.labels }, projectOrder: [...state.projectOrder], + pinnedProjectIds: [...(state.pinnedProjectIds ?? [])], } } catch { - workspaceRootOptionsState.value = { order: [], labels: {}, projectOrder: [] } + workspaceRootOptionsState.value = { order: [], labels: {}, projectOrder: [], pinnedProjectIds: [] } } } diff --git a/src/api/codexGateway.ts b/src/api/codexGateway.ts index 6f64305a7..e4dfea5ab 100644 --- a/src/api/codexGateway.ts +++ b/src/api/codexGateway.ts @@ -268,6 +268,7 @@ export type WorkspaceRootsState = { labels: Record active: string[] projectOrder: string[] + pinnedProjectIds?: string[] remoteProjects?: Array<{ id: string hostId: string @@ -2242,6 +2243,7 @@ function normalizeWorkspaceRootsState(payload: unknown): WorkspaceRootsState { labels, active: normalizeArray(record.active).map((value) => normalizePathForUi(value)), projectOrder: normalizeArray(record.projectOrder).map((value) => normalizePathForUi(value)), + pinnedProjectIds: normalizeArray(record.pinnedProjectIds).map((value) => normalizePathForUi(value)), remoteProjects: Array.isArray(record.remoteProjects) ? record.remoteProjects.flatMap((item) => { if (!item || typeof item !== 'object' || Array.isArray(item)) return [] @@ -2351,6 +2353,7 @@ function cloneWorkspaceRootsState(state: WorkspaceRootsState): WorkspaceRootsSta labels: { ...state.labels }, active: [...state.active], projectOrder: [...state.projectOrder], + pinnedProjectIds: [...(state.pinnedProjectIds ?? [])], remoteProjects: state.remoteProjects?.map((item) => ({ ...item })) ?? [], } } diff --git a/src/components/sidebar/SidebarThreadTree.vue b/src/components/sidebar/SidebarThreadTree.vue index 9c92ca139..dc510b496 100644 --- a/src/components/sidebar/SidebarThreadTree.vue +++ b/src/components/sidebar/SidebarThreadTree.vue @@ -316,6 +316,9 @@ +