diff --git a/AGENTS.md b/AGENTS.md index 57c2cd407..4d62a70c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,9 +62,20 @@ - Reproduce the issue with a focused test when feasible; if direct reproduction is impractical, document the exact reasoning and code evidence used to accept or reject the finding. - Prefer adding or updating a regression test for every accepted review-bot bug before or alongside the fix. - Do not patch purely to satisfy a bot comment if the behavior is correct, stale, already fixed, or the proposed change would make the implementation worse. -- After pushing any commit to an open PR, wait and poll for Qodo/review-bot comments and PR review status for about 30 seconds before reporting the push workflow as complete. -- After fixing an accepted review-bot finding, run the narrow regression test plus the relevant build/typecheck command, push the commit, and re-check the PR comments/status. -- In the completion report, distinguish confirmed fixes from stale or rejected bot comments. +- After creating any PR, always wait for Qodo to finish before reporting the PR workflow as complete: + 1. Poll the PR with `gh pr view --json comments,reviews,reviewDecision,mergeStateStatus,statusCheckRollup`. + 2. If the Qodo code-review comment says `Check back in a few minutes`, `is analyzing this pull request`, or otherwise looks like a placeholder, sleep 30 seconds and poll again. + 3. Continue polling until Qodo posts a completed code-review comment with active counts such as `Bugs (N)`, `Rule violations (N)`, or `Requirement gaps (N)`. + 4. Do not treat the separate Qodo summary/walkthrough comment as the completed code review. +- When Qodo reports findings: + 1. Inspect each finding and classify it as accepted, stale/already fixed, rejected/incorrect, or needs follow-up. + 2. For each accepted bug, inspect the code path and add or update a focused regression test where feasible. + 3. Implement the fix, then run the narrow regression test plus the relevant build/typecheck command. + 4. Commit the fix as its own discrete commit and push it to the PR branch. + 5. Poll Qodo again using the same 30-second loop until the updated review is complete. + 6. Repeat until Qodo reports `Bugs (0)`, `Rule violations (0)`, and `Requirement gaps (0)`, or until all remaining findings are explicitly documented as stale/rejected with code evidence. +- After pushing any commit to an open PR, run the same Qodo wait-and-fix loop before reporting the push workflow as complete. +- In the completion report, include the PR URL, latest commit, commands run, Qodo final counts, and which bot findings were fixed versus stale/rejected. ## Performance Audit Rule (MANDATORY) 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..88ad3686b 100644 --- a/src/api/codexGateway.ts +++ b/src/api/codexGateway.ts @@ -50,6 +50,7 @@ import type { UiReviewWorkspaceView, UiRateLimitSnapshot, UiRateLimitWindow, + UiThread, UiThreadAutomation, UiThreadAutomationStatus, } from '../types/codex' @@ -268,6 +269,7 @@ export type WorkspaceRootsState = { labels: Record active: string[] projectOrder: string[] + pinnedProjectIds?: string[] remoteProjects?: Array<{ id: string hostId: string @@ -753,6 +755,31 @@ export async function getThreadGroupsPage( } } +export async function getThreadSummariesByIds(threadIds: string[]): Promise { + const normalizedThreadIds = threadIds + .map((threadId) => threadId.trim()) + .filter((threadId, index, rows) => threadId.length > 0 && rows.indexOf(threadId) === index) + + if (normalizedThreadIds.length === 0) return [] + + const summaries = await Promise.all( + normalizedThreadIds.map(async (threadId) => { + try { + const payload = await callRpc('thread/read', { + threadId, + includeTurns: false, + }) + const groups = normalizeThreadGroupsV2({ data: [payload.thread] } as ThreadListResponse) + return groups.flatMap((group) => group.threads)[0] ?? null + } catch { + return null + } + }), + ) + + return summaries.filter((thread): thread is UiThread => thread !== null) +} + export function getBackgroundThreadListLimit(): number { return BACKGROUND_THREAD_LIST_LIMIT } @@ -2242,6 +2269,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 +2379,7 @@ function cloneWorkspaceRootsState(state: WorkspaceRootsState): WorkspaceRootsSta labels: { ...state.labels }, active: [...state.active], projectOrder: [...state.projectOrder], + pinnedProjectIds: [...(state.pinnedProjectIds ?? [])], remoteProjects: state.remoteProjects?.map((item) => ({ ...item })) ?? [], } } @@ -2728,6 +2757,7 @@ async function readJsonResponse(response: Response): Promise { } export async function setWorkspaceRootsState(nextState: WorkspaceRootsState): Promise { + const previousState = cachedWorkspaceRootsState const response = await fetch('/codex-api/workspace-roots-state', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -2736,7 +2766,10 @@ export async function setWorkspaceRootsState(nextState: WorkspaceRootsState): Pr if (!response.ok) { throw new Error('Failed to save workspace roots state') } - cachedWorkspaceRootsState = cloneWorkspaceRootsState(nextState) + cachedWorkspaceRootsState = cloneWorkspaceRootsState({ + ...nextState, + remoteProjects: nextState.remoteProjects ?? previousState?.remoteProjects ?? [], + }) } export async function openProjectRoot(path: string, options?: { createIfMissing?: boolean; label?: string }): Promise { diff --git a/src/components/sidebar/SidebarThreadTree.vue b/src/components/sidebar/SidebarThreadTree.vue index 9c92ca139..025977fab 100644 --- a/src/components/sidebar/SidebarThreadTree.vue +++ b/src/components/sidebar/SidebarThreadTree.vue @@ -316,6 +316,9 @@ +