diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 6948df2fdf..fa7bdc7c47 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -116,6 +116,7 @@ Settings are organized into categories. Most settings should be placed within th | `ui.renderMode` | string | Default Markdown display mode. Use `"render"` for rich visual previews or `"raw"` to show source-oriented Markdown by default. Toggle during a session with `Alt/Option+M`; on macOS the terminal must send Option as Meta. See [Markdown Rendering](../features/markdown-rendering). | `"render"` | | `ui.showCitations` | boolean | Show citations for generated text in the chat. | `false` | | `ui.history.collapseOnResume` | boolean | Whether to collapse history by default when resuming a session. Can be toggled via `/history collapse-on-resume` and `/history expand-on-resume`. | `false` | +| `ui.history.collapsePreviewCount` | number | Number of most recent user turns to keep visible when `ui.history.collapseOnResume` is enabled. `0` collapses all restored history by default; `-1` shows all restored history. | `0` | | `ui.compactMode` | boolean | Hide tool output and thinking for a cleaner view. Toggle with `Ctrl+O` during a session or via the Settings dialog. Tool approval prompts are never hidden, even in compact mode. The setting persists across sessions. | `false` | | `ui.shellOutputMaxLines` | number | Max number of shell output lines shown inline. Set to `0` to disable the cap and show full output. Hidden lines are surfaced via the `+N lines` indicator. Errors, `!`-prefix user-initiated commands, confirming tools, and focused embedded shells always show full output. | `5` | | `ui.enableWelcomeBack` | boolean | Show welcome back dialog when returning to a project with conversation history. When enabled, Qwen Code will automatically detect if you're returning to a project with a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`) and show a dialog allowing you to continue your previous conversation or start fresh. If you choose **Start new chat session**, that choice is remembered for the current project until the project summary changes. This feature integrates with the `/summary` command and quit confirmation dialog. | `true` | diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index f3cb9d04ef..f0c4030d33 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -842,6 +842,16 @@ const SETTINGS_SCHEMA = { 'Whether to collapse history by default when resuming a session.', showInDialog: false, }, + collapsePreviewCount: { + type: 'number', + label: 'Collapse Preview Count', + category: 'UI', + requiresRestart: false, + default: 0, + description: + 'Number of most recent user turns to keep visible when collapsing history on resume. 0 collapses all restored history by default; -1 shows all restored history.', + showInDialog: false, + }, }, }, showLineNumbers: { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 177f9508b5..b468eac339 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -629,10 +629,13 @@ export const AppContainer = (props: AppContainerProps) => { const rawItems = buildResumedHistoryItems(resumedSessionData, config); const collapseOnResume = settings.merged.ui?.history?.collapseOnResume ?? false; + const collapsePreviewCount = + settings.merged.ui?.history?.collapsePreviewCount ?? 0; const historyItems = applyCollapsePolicyAndSummary( rawItems, collapseOnResume, + collapsePreviewCount, ); historyManager.loadHistory(historyItems); diff --git a/packages/cli/src/ui/hooks/useBranchCommand.ts b/packages/cli/src/ui/hooks/useBranchCommand.ts index 034ca69f75..3f83c9e460 100644 --- a/packages/cli/src/ui/hooks/useBranchCommand.ts +++ b/packages/cli/src/ui/hooks/useBranchCommand.ts @@ -157,9 +157,12 @@ export function useBranchCommand( const rawItems = buildResumedHistoryItems(resumed, config); const collapseOnResume = options.settings.merged.ui?.history?.collapseOnResume ?? false; + const collapsePreviewCount = + options.settings.merged.ui?.history?.collapsePreviewCount ?? 0; const uiHistoryItems = applyCollapsePolicyAndSummary( rawItems, collapseOnResume, + collapsePreviewCount, ); startNewSession(newSessionId); historyManager.clearItems(); @@ -276,6 +279,7 @@ export function useBranchCommand( setSessionName, remount, options.settings.merged.ui?.history?.collapseOnResume, + options.settings.merged.ui?.history?.collapsePreviewCount, ], ); diff --git a/packages/cli/src/ui/hooks/useResumeCommand.ts b/packages/cli/src/ui/hooks/useResumeCommand.ts index 6230a75fab..03e63feb83 100644 --- a/packages/cli/src/ui/hooks/useResumeCommand.ts +++ b/packages/cli/src/ui/hooks/useResumeCommand.ts @@ -123,10 +123,13 @@ export function useResumeCommand( const rawItems = buildResumedHistoryItems(sessionData, config); const collapseOnResume = settings.merged.ui?.history?.collapseOnResume ?? false; + const collapsePreviewCount = + settings.merged.ui?.history?.collapsePreviewCount ?? 0; const uiHistoryItems = applyCollapsePolicyAndSummary( rawItems, collapseOnResume, + collapsePreviewCount, ); // 1. Swap core first. Matches useBranchCommand's core-before-UI @@ -218,6 +221,7 @@ export function useResumeCommand( setSessionName, remount, settings.merged.ui?.history?.collapseOnResume, + settings.merged.ui?.history?.collapsePreviewCount, ], ); diff --git a/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts b/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts index 7ea9978e92..02ea815ce3 100644 --- a/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts +++ b/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts @@ -6,11 +6,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { + applyCollapsePolicyAndSummary, buildResumedHistoryItems, stripSuppressOnRestore, expandCollapsedHistory, } from './resumeHistoryUtils.js'; -import { ToolCallStatus } from '../types.js'; +import { MessageType, ToolCallStatus } from '../types.js'; import type { AnyDeclarativeTool, Config, @@ -496,6 +497,88 @@ describe('resumeHistoryUtils', () => { }); }); +describe('applyCollapsePolicyAndSummary', () => { + const makeItems = (): HistoryItem[] => + [ + { id: 1, type: MessageType.USER, text: 'first' }, + { id: 2, type: MessageType.GEMINI, text: 'first response' }, + { id: 3, type: MessageType.USER, text: 'second' }, + { id: 4, type: MessageType.GEMINI, text: 'second response' }, + { id: 5, type: MessageType.USER, text: 'third' }, + { id: 6, type: MessageType.GEMINI, text: 'third response' }, + ] as HistoryItem[]; + + const expectSuppressed = (item: HistoryItem) => { + expect(item.display).toEqual( + expect.objectContaining({ suppressOnRestore: true }), + ); + }; + + const expectVisible = (item: HistoryItem) => { + expect(item.display?.suppressOnRestore).toBeUndefined(); + }; + + it('suppresses all items and shows the full summary count by default', () => { + const result = applyCollapsePolicyAndSummary(makeItems(), true); + + expect(result).toHaveLength(7); + result.slice(0, 6).forEach(expectSuppressed); + expect(result[6]).toEqual( + expect.objectContaining({ + id: 7, + type: MessageType.INFO, + text: expect.stringContaining('6 messages hidden'), + display: { kind: 'collapse-summary' }, + }), + ); + }); + + it('keeps the most recent N user turns visible and summarizes only hidden items', () => { + const result = applyCollapsePolicyAndSummary(makeItems(), true, 2); + + expect(result).toHaveLength(7); + result.slice(0, 2).forEach(expectSuppressed); + result.slice(2, 6).forEach(expectVisible); + expect(result[6]).toEqual( + expect.objectContaining({ + id: 7, + type: MessageType.INFO, + text: expect.stringContaining('2 messages hidden'), + display: { kind: 'collapse-summary' }, + }), + ); + }); + + it('shows all items without a summary when preview count covers all user turns', () => { + const rawItems = makeItems(); + const result = applyCollapsePolicyAndSummary(rawItems, true, 3); + + expect(result).toEqual(rawItems); + expect( + result.some((item) => item.display?.kind === 'collapse-summary'), + ).toBe(false); + result.forEach(expectVisible); + }); + + it('shows all items without a summary when preview count is -1', () => { + const rawItems = makeItems(); + const result = applyCollapsePolicyAndSummary(rawItems, true, -1); + + expect(result).toBe(rawItems); + }); + + it('returns raw items unchanged when collapseOnResume is false', () => { + const rawItems = makeItems(); + const result = applyCollapsePolicyAndSummary(rawItems, false, 1); + + expect(result).toBe(rawItems); + }); + + it('returns empty history without a summary', () => { + expect(applyCollapsePolicyAndSummary([], true)).toEqual([]); + }); +}); + describe('stripSuppressOnRestore', () => { it('returns item unchanged when display is undefined', () => { const item = { id: 1, type: 'user', text: 'hello' } as HistoryItem; diff --git a/packages/cli/src/ui/utils/resumeHistoryUtils.ts b/packages/cli/src/ui/utils/resumeHistoryUtils.ts index 08c82a26ef..733746f649 100644 --- a/packages/cli/src/ui/utils/resumeHistoryUtils.ts +++ b/packages/cli/src/ui/utils/resumeHistoryUtils.ts @@ -568,16 +568,37 @@ export function expandCollapsedHistory(items: HistoryItem[]): HistoryItem[] { export function applyCollapsePolicyAndSummary( rawItems: HistoryItem[], collapseOnResume: boolean, + collapsePreviewCount: number = 0, ): HistoryItem[] { if (!collapseOnResume) return rawItems; + if (collapsePreviewCount === -1) return rawItems; - const uiHistoryItems = applyResumeDisplayPolicy(rawItems); + let boundary = rawItems.length; + if (collapsePreviewCount > 0) { + let userTurnCount = 0; + for (let i = rawItems.length - 1; i >= 0; i--) { + if (rawItems[i].type === MessageType.USER) { + userTurnCount++; + if (userTurnCount === collapsePreviewCount) { + boundary = i; + break; + } + } + } + if (userTurnCount < collapsePreviewCount) { + boundary = 0; + } + } - if (rawItems.length > 0) { + const hiddenItems = applyResumeDisplayPolicy(rawItems.slice(0, boundary)); + const visibleItems = rawItems.slice(boundary); + const uiHistoryItems = [...hiddenItems, ...visibleItems]; + + if (boundary > 0) { const nextId = rawItems[rawItems.length - 1].id + 1; return [ ...uiHistoryItems, - { id: nextId, ...createHistoryCollapseSummaryItem(rawItems.length) }, + { id: nextId, ...createHistoryCollapseSummaryItem(boundary) }, ]; } diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index 294692ab94..2ae943ad46 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -278,6 +278,11 @@ "description": "Whether to collapse history by default when resuming a session.", "type": "boolean", "default": false + }, + "collapsePreviewCount": { + "description": "Number of most recent user turns to keep visible when collapsing history on resume. 0 collapses all restored history by default; -1 shows all restored history.", + "type": "number", + "default": 0 } } },