diff --git a/src/adapter/webview/log-webview.ts b/src/adapter/webview/log-webview.ts
index 85c4891..dee8213 100644
--- a/src/adapter/webview/log-webview.ts
+++ b/src/adapter/webview/log-webview.ts
@@ -214,7 +214,8 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter
if (raws.length === 0) {
return { rows: [], maxLanes: 0, hasMore: false };
}
- // 客户端过滤(mergeMode / date / regex),message 近似取 subject。
+ // 客户端过滤(mergeMode / date / regex / checkpoint),message 近似取 subject。
+ // keepCheckpoint 由 scope 驱动:仅 Checkpointer 视图保留 checkpoint 自动提交,All/Current 剔除。
const filterable = raws.map((r) => ({
message: r.subject,
authorDate: r.authorDate ? new Date(r.authorDate) : undefined,
@@ -222,7 +223,7 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter
hash: r.hash,
raw: r,
}));
- const survived = applyClientFilters(filterable, toClientFilter(this.filter));
+ const survived = applyClientFilters(filterable, { ...toClientFilter(this.filter), keepCheckpoint: this.scope === 'checkpointer' });
const layout = computeGraphLayout(survived.map((s) => ({ hash: s.hash, parents: s.parents })));
const hashSet = new Set(survived.map((s) => s.hash));
const chips = await this.fetchChips(hashSet);
@@ -380,6 +381,7 @@ body { margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscod
+
@@ -393,9 +395,11 @@ body { margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscod
const vscode = acquireVsCodeApi();
const PALETTE = ${palette};
const ROW_H = 24, LANE_W = 14, NODE_R = 4, GUTTER = 10, OVERSCAN = 8, LOAD_THRESHOLD = 40;
+/** scope 白名单兜底:仅接受三态,否则回退默认 'all'(兼容未来废弃的持久化值)。 */
+function normalizeScope(v) { return v === 'all' || v === 'current' || v === 'checkpointer' ? v : 'all'; }
const persisted = vscode.getState() || {};
let selectedHash = persisted.selectedHash || null;
-let scope = persisted.scope || 'all';
+let scope = normalizeScope(persisted.scope);
let model = { rows: [], maxLanes: 0, hasMore: false, repoRoot: '' };
let renderedFirst = -1, renderedLast = -1, fetching = false;
const viewport = document.getElementById('viewport');
@@ -473,6 +477,7 @@ function render() {
emptyEl.style.display = total === 0 ? 'block' : 'none';
document.getElementById('scope-all').classList.toggle('active', scope === 'all');
document.getElementById('scope-current').classList.toggle('active', scope === 'current');
+ document.getElementById('scope-checkpointer').classList.toggle('active', scope === 'checkpointer');
if (model.hasMore && !fetching && l >= total - LOAD_THRESHOLD) {
fetching = true; spinnerEl.style.display = 'block';
vscode.postMessage({ type: 'log/loadMore', payload: { cursor: total } });
@@ -516,8 +521,10 @@ rowsEl.addEventListener('contextmenu', function (e) {
e.preventDefault();
vscode.postMessage({ type: 'log/commitAction', payload: { op: 'menu', hash: r.getAttribute('data-hash') } });
});
-document.getElementById('scope-all').addEventListener('click', function () { if (scope !== 'all') { scope = 'all'; vscode.setState({ selectedHash: selectedHash, scope: scope }); vscode.postMessage({ type: 'log/setScope', payload: { scope: 'all' } }); } });
-document.getElementById('scope-current').addEventListener('click', function () { if (scope !== 'current') { scope = 'current'; vscode.setState({ selectedHash: selectedHash, scope: scope }); vscode.postMessage({ type: 'log/setScope', payload: { scope: 'current' } }); } });
+function setScope(next) { if (scope !== next) { scope = next; vscode.setState({ selectedHash: selectedHash, scope: scope }); vscode.postMessage({ type: 'log/setScope', payload: { scope: next } }); } }
+document.getElementById('scope-all').addEventListener('click', function () { setScope('all'); });
+document.getElementById('scope-current').addEventListener('click', function () { setScope('current'); });
+document.getElementById('scope-checkpointer').addEventListener('click', function () { setScope('checkpointer'); });
viewport.addEventListener('scroll', scheduleRender, { passive: true });
viewport.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown') { e.preventDefault(); moveSel(1); }
diff --git a/src/engine/log/log-filter.ts b/src/engine/log/log-filter.ts
index 2734c73..5d6456d 100644
--- a/src/engine/log/log-filter.ts
+++ b/src/engine/log/log-filter.ts
@@ -16,6 +16,12 @@ export interface LogClientFilter {
/** 截止日期(含),按 authorDate 过滤。 */
readonly dateTo?: Date;
readonly messageRegex?: RegExp;
+ /**
+ * 是否保留 Conductor 等 Agent 工具自动创建的 checkpoint 提交(subject 以 `checkpoint:` 开头,见
+ * {@link isCheckpointSubject})。`false` 时剔除;缺省或 `true` 时保留。由 Log 视图 scope 驱动
+ * (Checkpointer 视图保留完整历史,All/Current 剔除以呈现干净的人类提交历史)。
+ */
+ readonly keepCheckpoint?: boolean;
}
/**
@@ -65,9 +71,27 @@ export function safeRegex(pattern: string): RegExp | undefined {
}
}
+/**
+ * Conductor 等 Agent 工具自动创建的「状态快照」提交前缀(LangGraph Checkpointer 语义)。
+ * 已知模式:`checkpoint:session--turn--start/end`、`checkpoint:conductor-archive-`、
+ * `checkpoint:conductor-getdiff`。锚定 subject 行首、大小写不敏感;集中于此为单一事实源,
+ * adapter / 测试一律引用本常量或谓词,杜绝识别规则分散。
+ */
+export const CHECKPOINT_SUBJECT_RE = /^checkpoint:/i;
+
+/** 判定提交 subject 是否为 checkpoint 自动提交({@link CHECKPOINT_SUBJECT_RE} 的谓词封装)。 */
+export function isCheckpointSubject(subject: string): boolean {
+ return CHECKPOINT_SUBJECT_RE.test(subject);
+}
+
/** 对已取回的提交施加客户端过滤(不可变,返回新数组)。 */
export function applyClientFilters(commits: readonly T[], filter: LogClientFilter): T[] {
let result: T[] = [...commits];
+ // checkpoint 剔除置链首:其高密度特性可减少后续 mergeMode/date/regex 的计算量;
+ // 与其他维度同为 AND 语义、可交换,顺序不影响结果正确性。
+ if (filter.keepCheckpoint === false) {
+ result = result.filter((c) => !isCheckpointSubject(c.message));
+ }
if (filter.mergeMode === 'merge-only') {
result = result.filter((c) => c.parents.length > 1);
} else if (filter.mergeMode === 'no-merge') {
diff --git a/src/engine/log/log-query.ts b/src/engine/log/log-query.ts
index 2175127..6134437 100644
--- a/src/engine/log/log-query.ts
+++ b/src/engine/log/log-query.ts
@@ -10,8 +10,15 @@
import type { LogFilter } from './log-filter';
import { LOG_GRAPH_FORMAT } from './log-line';
-/** 提交范围:`all` = 全部分支(`--all`,IDEA 默认);`current` = 当前分支(HEAD)。 */
-export type LogScope = 'all' | 'current';
+/**
+ * 提交范围(工具栏互斥单选):
+ * - `all` = 全分支(`--all`),剔除 checkpoint 自动提交(干净的人类历史,默认);
+ * - `current` = 当前分支(HEAD),剔除 checkpoint 自动提交;
+ * - `checkpointer` = 全分支(`--all`),保留 checkpoint 自动提交(原始完整视图)。
+ * checkpoint 的保留/剔除由 adapter 层据 scope 注入 `LogClientFilter.keepCheckpoint`,
+ * engine query 层仅据本类型决定 `--all` 分支范围,对 checkpoint 概念无感知(职责单一)。
+ */
+export type LogScope = 'all' | 'current' | 'checkpointer';
/** 分页参数。 */
export interface LogQueryOptions {
@@ -26,7 +33,7 @@ export interface LogQueryOptions {
*/
export function buildLogArgs(filter: LogFilter | undefined, scope: LogScope, opts: LogQueryOptions): string[] {
const args: string[] = ['--topo-order'];
- if (scope === 'all') {
+ if (scope === 'all' || scope === 'checkpointer') {
args.push('--all');
}
args.push(`--max-count=${opts.maxCount}`);
diff --git a/tests/unit/log-filter.test.ts b/tests/unit/log-filter.test.ts
index e48ed06..2952075 100644
--- a/tests/unit/log-filter.test.ts
+++ b/tests/unit/log-filter.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { applyClientFilters, safeRegex, type FilterableCommit, type LogClientFilter } from '../../src/engine/log/log-filter';
+import { CHECKPOINT_SUBJECT_RE, applyClientFilters, isCheckpointSubject, safeRegex, type FilterableCommit, type LogClientFilter } from '../../src/engine/log/log-filter';
const D = (iso: string): Date => new Date(iso);
const C = (message: string, parents: string[], authorDate?: Date): FilterableCommit => ({ message, parents, authorDate });
@@ -11,6 +11,13 @@ const COMMITS: FilterableCommit[] = [
C('docs: readme', ['p1', 'p2', 'p3'], D('2026-07-01')),
];
+/** 含 Conductor 自动 checkpoint 提交的样本(用于 keepCheckpoint 维度测试,与 COMMITS 隔离以保护既有断言)。 */
+const COMMITS_WITH_CKPT: FilterableCommit[] = [
+ ...COMMITS,
+ C('checkpoint:session-x-turn-y-start', ['p1'], D('2026-06-05')),
+ C('checkpoint:conductor-archive-uuid', ['p1'], D('2026-06-15')),
+];
+
describe('log-filter', () => {
it('merge-only 仅保留 parents>1', () => {
const f: LogClientFilter = { mergeMode: 'merge-only' };
@@ -58,6 +65,44 @@ describe('log-filter', () => {
});
});
+describe('isCheckpointSubject / CHECKPOINT_SUBJECT_RE', () => {
+ it('识别 session checkpoint 起/止', () => {
+ expect(isCheckpointSubject('checkpoint:session-abc-turn-def-start')).toBe(true);
+ expect(isCheckpointSubject('checkpoint:session-abc-turn-def-end')).toBe(true);
+ });
+ it('识别 conductor-archive / conductor-getdiff', () => {
+ expect(isCheckpointSubject('checkpoint:conductor-archive-uuid')).toBe(true);
+ expect(isCheckpointSubject('checkpoint:conductor-getdiff')).toBe(true);
+ });
+ it('不误伤非 checkpoint 前缀(含正文出现 checkpoint 字样)', () => {
+ expect(isCheckpointSubject('docs: checkpoint notes')).toBe(false);
+ expect(isCheckpointSubject('feat: add checkpoint api')).toBe(false);
+ expect(isCheckpointSubject('')).toBe(false);
+ });
+ it('大小写不敏感(容错 Agent 工具配置差异)', () => {
+ expect(isCheckpointSubject('Checkpoint:session-x')).toBe(true);
+ expect(CHECKPOINT_SUBJECT_RE.test('CHECKPOINT:foo')).toBe(true);
+ });
+});
+
+describe('applyClientFilters — keepCheckpoint 维度', () => {
+ it('keepCheckpoint=false 剔除 checkpoint 提交(保留正常提交)', () => {
+ const out = applyClientFilters(COMMITS_WITH_CKPT, { keepCheckpoint: false }).map((c) => c.message);
+ expect(out).toEqual(['feat: a', 'merge: branch', 'fix: bug [urgent]', 'docs: readme']);
+ });
+ it('keepCheckpoint=undefined 不过滤 checkpoint(向后兼容)', () => {
+ expect(applyClientFilters(COMMITS_WITH_CKPT, {})).toHaveLength(6);
+ });
+ it('keepCheckpoint=true 保留 checkpoint', () => {
+ expect(applyClientFilters(COMMITS_WITH_CKPT, { keepCheckpoint: true })).toHaveLength(6);
+ });
+ it('组合:keepCheckpoint=false + no-merge + dateFrom(顺序正交)', () => {
+ const f: LogClientFilter = { keepCheckpoint: false, mergeMode: 'no-merge', dateFrom: D('2026-06-15') };
+ // 剔 checkpoint(2 条) + 剔 merge(docs) + 剔早于 06-15(feat:a) → 仅 fix: bug [urgent]
+ expect(applyClientFilters(COMMITS_WITH_CKPT, f).map((c) => c.message)).toEqual(['fix: bug [urgent]']);
+ });
+});
+
describe('safeRegex', () => {
it('合法模式返回 RegExp', () => {
expect(safeRegex('feat')).toEqual(/feat/);
diff --git a/tests/unit/log-query.test.ts b/tests/unit/log-query.test.ts
index 90e02fc..5a8407b 100644
--- a/tests/unit/log-query.test.ts
+++ b/tests/unit/log-query.test.ts
@@ -14,6 +14,10 @@ describe('buildLogArgs — 顺序与必选项', () => {
expect(buildLogArgs(noFilter, 'current', { maxCount: 300 })).not.toContain('--all');
});
+ it('scope=checkpointer 与 all 同为全分支(含 --all)', () => {
+ expect(buildLogArgs(noFilter, 'checkpointer', { maxCount: 300 })).toContain('--all');
+ });
+
it('max-count 与 skip', () => {
expect(buildLogArgs(noFilter, 'all', { maxCount: 500 })).toContain('--max-count=500');
expect(buildLogArgs(noFilter, 'all', { maxCount: 500, skip: 500 })).toContain('--skip=500');