feat: add an opt-in all-sessions time sort mode#659
feat: add an opt-in all-sessions time sort mode#659apple-ouyang wants to merge 2 commits intositeboon:mainfrom
Conversation
原因: - 项目侧边栏默认只取每个 provider 的少量会话,无法按真实最近时间展示完整的 Codex 历史 - 需要把这个行为做成可选配置,避免直接改默认值带来性能回退 改动: - 新增 showAllSessionsSortedByTime UI 偏好,并在快速设置里暴露开关与多语言文案 - /api/projects 支持 includeAllSessions 查询参数,按开关返回 Claude、Cursor、Codex、Gemini 的完整会话列表 - projects_updated 改为按当前本地偏好重新拉取项目数据,避免后台更新把 UI 退回到截断结果
📝 WalkthroughWalkthroughAdds a UI preference and request flags to return all sessions sorted by time. The flag is threaded from quick-settings through hooks and API calls to server routes, which now support timeline-style session aggregation, Codex index warming/dirtying, and timeline-based pagination. Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as QuickSettingsPanel
participant Pref as useUiPreferences
participant Hook as useProjectsState / SidebarController
participant API as src/utils/api.js
participant Server as server/index.js
participant Projects as server/projects.js
User->>UI: Toggle "showAllSessionsSortedByTime"
UI->>Pref: set preference
Pref-->>Hook: preference changed (effect)
Hook->>API: api.projects({ sortAllSessionsByTime: true })
API->>Server: GET /api/projects?sortAllSessionsByTime=true
Server->>Projects: getProjects(progressCallback, { sortAllSessionsByTime: true })
Projects->>Projects: aggregate timelineSessions across providers (Claude/Cursor/Codex/Gemini), sort, paginate
Projects-->>Server: projects payload with timelineSessions
Server-->>API: HTTP 200 with projects
API-->>Hook: resolved projects
Hook->>UI: update state -> render timeline sessions
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/projects.js (1)
489-493:⚠️ Potential issue | 🟠 MajorSort merged Gemini sessions before applying
sessionLimit.This concatenates in-memory Gemini sessions ahead of CLI sessions and then truncates, so the returned list is not actually the newest
Nsessions by time. That breaks the new time-sorted mode and can hide newer CLI sessions behind older UI sessions. Apply the same fix in both blocks.Proposed fix
- const mergedGemini = [...uiSessions, ...cliSessions.filter(s => !uiIds.has(s.id))]; - project.geminiSessions = sessionLimit > 0 ? mergedGemini.slice(0, sessionLimit) : mergedGemini; + const mergedGemini = [...uiSessions, ...cliSessions.filter(s => !uiIds.has(s.id))] + .sort((a, b) => new Date(b.lastActivity || 0) - new Date(a.lastActivity || 0)); + project.geminiSessions = sessionLimit > 0 ? mergedGemini.slice(0, sessionLimit) : mergedGemini;Also applies to: 609-613
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/projects.js` around lines 489 - 493, The mergedGemini list currently preserves UI-first ordering then slices by sessionLimit, hiding newer CLI sessions; after creating mergedGemini (from uiSessions and cliSessions filtered by uiIds) sort the merged array by the session timestamp (e.g., a createdAt or timestamp property on session objects) in descending (newest-first) order and only then apply sessionLimit and assign to project.geminiSessions; make this same change for the second merging occurrence (the other block that builds mergedGemini from uiSessions and cliSessions) so both places return the newest N sessions consistently.
🧹 Nitpick comments (1)
src/components/quick-settings-panel/constants.ts (1)
49-53: Consider using a distinct icon for this toggle.Both
autoScrollToBottom(line 47) andshowAllSessionsSortedByTimeuse theArrowDownicon. Since they appear in the same "View Options" section, this may cause visual confusion for users. Consider using a more semantically appropriate icon likeClock,List, orSortDescfrom lucide-react to better convey "all sessions sorted by time."♻️ Suggested change
import { ArrowDown, Brain, + Clock, Eye, Languages, Maximize2, } from 'lucide-react';{ key: 'showAllSessionsSortedByTime', labelKey: 'quickSettings.showAllSessionsSortedByTime', - icon: ArrowDown, + icon: Clock, },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/quick-settings-panel/constants.ts` around lines 49 - 53, The two view toggles use the same ArrowDown icon causing confusion; update the showAllSessionsSortedByTime entry in constants.ts (the object with key 'showAllSessionsSortedByTime') to use a more semantically appropriate lucide-react icon such as Clock, List, or SortDesc instead of ArrowDown so it visually represents "sorted by time" and differs from autoScrollToBottom.
🤖 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/index.js`:
- Around line 506-507: The watcher is calling getProjects(broadcastProgress)
without the includeAllSessions option, so after FS changes clients who requested
includeAllSessions see truncated lists; modify setupProjectsWatcher to accept an
includeAllSessions boolean (or options object) and have it call
getProjects(broadcastProgress, { includeAllSessions }) when building the
projects_updated payload; update the place where setupProjectsWatcher is invoked
so it forwards the request-scoped includeAllSessions value (the same value used
when initially calling getProjects) and ensure any broadcast or projects_updated
emission uses that option consistently.
---
Outside diff comments:
In `@server/projects.js`:
- Around line 489-493: The mergedGemini list currently preserves UI-first
ordering then slices by sessionLimit, hiding newer CLI sessions; after creating
mergedGemini (from uiSessions and cliSessions filtered by uiIds) sort the merged
array by the session timestamp (e.g., a createdAt or timestamp property on
session objects) in descending (newest-first) order and only then apply
sessionLimit and assign to project.geminiSessions; make this same change for the
second merging occurrence (the other block that builds mergedGemini from
uiSessions and cliSessions) so both places return the newest N sessions
consistently.
---
Nitpick comments:
In `@src/components/quick-settings-panel/constants.ts`:
- Around line 49-53: The two view toggles use the same ArrowDown icon causing
confusion; update the showAllSessionsSortedByTime entry in constants.ts (the
object with key 'showAllSessionsSortedByTime') to use a more semantically
appropriate lucide-react icon such as Clock, List, or SortDesc instead of
ArrowDown so it visually represents "sorted by time" and differs from
autoScrollToBottom.
🪄 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: 54248ae6-85d0-43d6-b0c3-10df185d2f93
📒 Files selected for processing (14)
server/index.jsserver/projects.jssrc/components/quick-settings-panel/constants.tssrc/components/quick-settings-panel/types.tssrc/components/quick-settings-panel/view/QuickSettingsPanelView.tsxsrc/hooks/useProjectsState.tssrc/hooks/useUiPreferences.tssrc/i18n/locales/de/settings.jsonsrc/i18n/locales/en/settings.jsonsrc/i18n/locales/ja/settings.jsonsrc/i18n/locales/ko/settings.jsonsrc/i18n/locales/ru/settings.jsonsrc/i18n/locales/zh-CN/settings.jsonsrc/utils/api.js
| const includeAllSessions = req.query.includeAllSessions === 'true'; | ||
| const projects = await getProjects(broadcastProgress, { includeAllSessions }); |
There was a problem hiding this comment.
The toggle only affects the initial fetch, not watcher-driven refreshes.
This route now honors includeAllSessions, but setupProjectsWatcher() still publishes projects_updated payloads from getProjects(broadcastProgress) with the default cap. After any filesystem change, opted-in clients get truncated session lists again until they do a full refetch.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/index.js` around lines 506 - 507, The watcher is calling
getProjects(broadcastProgress) without the includeAllSessions option, so after
FS changes clients who requested includeAllSessions see truncated lists; modify
setupProjectsWatcher to accept an includeAllSessions boolean (or options object)
and have it call getProjects(broadcastProgress, { includeAllSessions }) when
building the projects_updated payload; update the place where
setupProjectsWatcher is invoked so it forwards the request-scoped
includeAllSessions value (the same value used when initially calling
getProjects) and ensure any broadcast or projects_updated emission uses that
option consistently.
原因: - 全局按时间排序的会话列表在首屏会一次性加载大量历史,导致 loading projects 很慢 - Codex 全局索引首次构建成本高,需要缓存和预热来压低接口延迟 - review comment 指出的 Gemini 排序问题和设置项图标歧义需要补齐 - 用户希望 subagent 对话不要出现在主 session 列表里,避免噪音 改动: - 将按时间排序模式改成分页加载,每次只返回 5 条,并保留按时间线继续加载的行为 - 为 Codex session 索引增加缓存、并发共享构建、后台预热和 stale-while-revalidate 脏标记刷新 - 统一 Gemini 会话聚合逻辑,确保截断前先按时间排序,并把时间排序设置图标改为 Clock3 - 解析 Codex session_meta 的 subagent 标记,默认从列表中过滤 subagent,会话删除等内部流程可显式包含它们 影响: - 默认开启按时间排序后,首屏不会再全量拉历史记录 - Codex subagent 会话默认不再出现在 UI 列表中,但底层数据仍保留
|
Updated in 416c5ee. Addressed the remaining review feedback:
Local verification:
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 1663-1675: The branch that checks codexSessionsIndexDirty must not
return the stale codexSessionsIndexCache; instead, when codexSessionsIndexDirty
is true ensure you await the rebuild promise (codexSessionsIndexPromise) or
start buildCodexSessionsIndex() and return its result; update the logic around
codexSessionsIndexPromise, codexSessionsIndexDirty, buildCodexSessionsIndex(),
and codexSessionsIndexCache so the function returns the newly built index (or
the in-flight promise's resolved index) rather than the old cache.
In `@src/components/sidebar/hooks/useSidebarController.ts`:
- Around line 508-511: The code passes getProjectSessions(project).length to
api.sessions which counts all providers, but the backend paginates Claude-only
in legacy mode; compute a Claude-only count instead and pass that when timeline
mode is off: replace currentSessionCount with something like const
currentClaudeSessionCount = getProjectSessions(project).filter(s => /* detect
Claude by s.provider === 'claude' or s.model startsWith('claude') */).length and
use it in the api.sessions call that currently passes currentSessionCount (and
apply the same change to the other api.sessions call around the second usage).
In `@src/hooks/useProjectsState.ts`:
- Around line 156-157: The code enables the new timeline path by default because
showAllSessionsSortedByTime defaults to true in useUiPreferences; change the
default to false so the feature is opt-in: update the default value for
showAllSessionsSortedByTime in the useUiPreferences implementation (and any
other place where that preference default is set) to false, or explicitly set
const { showAllSessionsSortedByTime = false } = preferences in useProjectsState
(and the other referenced usage) to ensure first-load behavior remains unchanged
until users opt in.
🪄 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: 3001eca0-a7b9-4d8b-b928-579a9eee52fe
📒 Files selected for processing (10)
server/index.jsserver/projects.jssrc/components/quick-settings-panel/constants.tssrc/components/sidebar/hooks/useSidebarController.tssrc/components/sidebar/types/types.tssrc/components/sidebar/utils/utils.tssrc/hooks/useProjectsState.tssrc/hooks/useUiPreferences.tssrc/types/app.tssrc/utils/api.js
✅ Files skipped from review due to trivial changes (2)
- src/types/app.ts
- src/components/quick-settings-panel/constants.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/hooks/useUiPreferences.ts
| if (codexSessionsIndexCache && codexSessionsIndexDirty) { | ||
| if (!codexSessionsIndexPromise) { | ||
| codexSessionsIndexPromise = buildCodexSessionsIndex() | ||
| .then((index) => { | ||
| codexSessionsIndexCache = index; | ||
| codexSessionsIndexDirty = false; | ||
| return index; | ||
| }) | ||
| .finally(() => { | ||
| codexSessionsIndexPromise = null; | ||
| }); | ||
| } | ||
| return codexSessionsIndexCache; |
There was a problem hiding this comment.
Don’t serve the stale Codex index after invalidation.
After markCodexSessionsIndexDirty(), this branch starts a rebuild but still returns codexSessionsIndexCache. In server/index.js, the watcher dirties the index and immediately calls getProjects(), so the first refresh after a Codex file change can be built from stale data and completely miss that change.
💡 Proposed fix
if (codexSessionsIndexCache && codexSessionsIndexDirty) {
if (!codexSessionsIndexPromise) {
codexSessionsIndexPromise = buildCodexSessionsIndex()
.then((index) => {
codexSessionsIndexCache = index;
codexSessionsIndexDirty = false;
return index;
})
.finally(() => {
codexSessionsIndexPromise = null;
});
}
- return codexSessionsIndexCache;
+ return codexSessionsIndexPromise;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/projects.js` around lines 1663 - 1675, The branch that checks
codexSessionsIndexDirty must not return the stale codexSessionsIndexCache;
instead, when codexSessionsIndexDirty is true ensure you await the rebuild
promise (codexSessionsIndexPromise) or start buildCodexSessionsIndex() and
return its result; update the logic around codexSessionsIndexPromise,
codexSessionsIndexDirty, buildCodexSessionsIndex(), and codexSessionsIndexCache
so the function returns the newly built index (or the in-flight promise's
resolved index) rather than the old cache.
| const currentSessionCount = getProjectSessions(project).length; | ||
| const response = await api.sessions(project.name, 5, currentSessionCount, { | ||
| sortAllSessionsByTime: showAllSessionsSortedByTime, | ||
| }); |
There was a problem hiding this comment.
Use a Claude-only offset when timeline mode is off.
In legacy mode, this endpoint still paginates only Claude sessions (server/index.js Lines 533-535). getProjectSessions(project).length also counts Cursor/Codex/Gemini items, so projects with external sessions will skip Claude pages when the user clicks “load more”.
💡 Proposed fix
- const currentSessionCount = getProjectSessions(project).length;
+ const currentSessionCount = showAllSessionsSortedByTime
+ ? getProjectSessions(project).length
+ : (project.sessions?.length ?? 0) + (additionalSessions[project.name]?.length ?? 0);
const response = await api.sessions(project.name, 5, currentSessionCount, {
sortAllSessionsByTime: showAllSessionsSortedByTime,
});
@@
- [getProjectSessions, loadingSessions, projectHasMoreOverrides, showAllSessionsSortedByTime],
+ [additionalSessions, getProjectSessions, loadingSessions, projectHasMoreOverrides, showAllSessionsSortedByTime],Also applies to: 544-544
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/sidebar/hooks/useSidebarController.ts` around lines 508 - 511,
The code passes getProjectSessions(project).length to api.sessions which counts
all providers, but the backend paginates Claude-only in legacy mode; compute a
Claude-only count instead and pass that when timeline mode is off: replace
currentSessionCount with something like const currentClaudeSessionCount =
getProjectSessions(project).filter(s => /* detect Claude by s.provider ===
'claude' or s.model startsWith('claude') */).length and use it in the
api.sessions call that currently passes currentSessionCount (and apply the same
change to the other api.sessions call around the second usage).
| const { preferences } = useUiPreferences(); | ||
| const { showAllSessionsSortedByTime } = preferences; |
There was a problem hiding this comment.
This ships the new mode as default-on, not opt-in.
useUiPreferences() currently defaults showAllSessionsSortedByTime to true, so routing all project fetches through this flag enables the new timeline query path for users with no saved preference. That contradicts the PR contract and changes first-load behavior/perf unexpectedly. Please flip the default to false before merging.
Also applies to: 186-186
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/useProjectsState.ts` around lines 156 - 157, The code enables the
new timeline path by default because showAllSessionsSortedByTime defaults to
true in useUiPreferences; change the default to false so the feature is opt-in:
update the default value for showAllSessionsSortedByTime in the useUiPreferences
implementation (and any other place where that preference default is set) to
false, or explicitly set const { showAllSessionsSortedByTime = false } =
preferences in useProjectsState (and the other referenced usage) to ensure
first-load behavior remains unchanged until users opt in.
Summary by CodeRabbit
New Features
Improvements