Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/coderabbit-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ jobs:
script: |
const pr = context.payload.pull_request;
const body = [
"@coderabbitai review",
"",
"If I directly merge them, will it cause regression anomalies? Note that this includes whether it will introduce bugs in the debugger. Please perform a comprehensive check.",
"@coderabbitai If the current suggestion is not adopted, will it lead to disruptive changes? Is there any user experience regression?",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
].join("\n");

await github.rest.issues.createComment({
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(html, /provider-fast-switch-select/);
assert.match(html, /forceCompactLayout/);
assert.match(html, /quickSwitchProvider\(\$event\.target\.value\)/);
assert.match(html, /onMainTabPointerDown\('sessions', \$event\)/);
assert.match(html, /onConfigTabPointerDown\('codex', \$event\)/);
assert.match(html, /onMainTabClick\('sessions', \$event\)/);
assert.match(html, /onConfigTabClick\('codex', \$event\)/);
assert.match(html, /data-main-tab=\"sessions\"/);
assert.match(html, /data-config-mode=\"codex\"/);
assert.match(html, /isMainTabNavActive\('settings'\)/);
assert.match(html, /isConfigModeNavActive\('codex'\)/);
assert.match(html, /v-memo="\[activeSessionExportKey === getSessionExportKey\(session\)/);
assert.match(html, /v-memo="\[msg\.text,\s*msg\.timestamp,\s*msg\.roleLabel,\s*msg\.normalizedRole\]"/);
assert.match(html, /v-memo="\[sessionTimelineActiveKey === node\.key,\s*node\.safePercent,\s*node\.title\]"/);
assert.match(html, /<button class="card-action-btn"[^>]*@click="copyClaudeShareCommand\(name\)"[^>]*disabled[^>]*>/);
});

Expand All @@ -38,6 +49,44 @@ test('web ui script defines provider mode metadata for codex only', () => {
assert.match(appScript, /\.\.\.createConfigModeComputed\(\)/);
assert.match(appScript, /switchConfigMode\(mode\)/);
assert.match(appScript, /mode\.trim\(\)\.toLowerCase\(\)/);
assert.match(appScript, /this\.switchMainTab\('config'\);/);
assert.match(appScript, /if \(this\.mainTab === 'config'\) {/);
assert.match(appScript, /this\.clearMainTabSwitchIntent\('config'\);/);
assert.match(appScript, /setMainTabSwitchIntent\(tab\)/);
assert.match(appScript, /ensureMainTabSwitchState\(\)/);
assert.match(appScript, /ensureImmediateNavDomState\(\)/);
assert.match(appScript, /applyImmediateNavIntent\(tab,\s*configMode = ''\)/);
assert.match(appScript, /clearImmediateNavIntent\(\)/);
assert.match(appScript, /setSessionPanelFastHidden\(hidden\)/);
assert.match(appScript, /isSessionPanelFastHidden\(\)/);
assert.match(appScript, /recordPointerNavCommit\(kind,\s*value\)/);
assert.match(appScript, /consumePointerNavCommit\(kind,\s*value\)/);
assert.match(appScript, /onMainTabPointerDown\(tab\)/);
assert.match(appScript, /onConfigTabPointerDown\(mode\)/);
assert.match(appScript, /onMainTabClick\(tab\)/);
assert.match(appScript, /onConfigTabClick\(mode\)/);
assert.match(appScript, /node\.classList\.toggle\('nav-intent-active'/);
assert.match(appScript, /node\.classList\.toggle\('nav-intent-inactive'/);
assert.match(appScript, /node\.classList\.remove\('nav-intent-active'\)/);
assert.match(appScript, /node\.classList\.remove\('nav-intent-inactive'\)/);
assert.match(appScript, /isMainTabNavActive\(tab\)/);
assert.match(appScript, /isConfigModeNavActive\(mode\)/);
assert.match(appScript, /const isLeavingSessions = previousTab === 'sessions' && targetTab !== 'sessions';/);
assert.match(appScript, /if \(targetTab === previousTab\) {/);
assert.match(appScript, /const shouldDeferApply = isLeavingSessions;/);
assert.match(appScript, /if \(isLeavingSessions && !this\.isSessionPanelFastHidden\(\)\) {/);
assert.match(appScript, /switchState\.pendingTarget = targetTab;/);
assert.match(appScript, /if \(ticket !== liveState\.ticket\) return;/);
assert.match(appScript, /activeSessionExportKey\(\)/);
assert.match(appScript, /this\.getMainTabForNav\(\) !== 'sessions'/);
assert.match(appScript, /scheduleIdleTask\(task,\s*timeoutMs = 160\)/);
assert.match(appScript, /scheduleSessionTabDeferredTeardown\(task\)/);
assert.match(appScript, /cancelScheduledSessionTabDeferredTeardown\(\)/);
assert.match(appScript, /suspendSessionTabRender\(\)/);
assert.match(appScript, /finalizeSessionTabTeardown\(\)/);
assert.match(appScript, /ensureSessionTimelineMeasurementCache\(\)/);
assert.match(appScript, /invalidateSessionTimelineMeasurementCache\(resetOffset = false\)/);
assert.match(appScript, /getCachedSessionTimelineMeasuredNodes\(nodes\)/);
assert.match(appScript, /quickSwitchProvider\(name\)/);
assert.match(appScript, /performProviderSwitch\(name\)/);
assert.match(appScript, /waitForCodexApplyIdle\(maxWaitMs = 20000\)/);
Expand Down
151 changes: 151 additions & 0 deletions tests/unit/session-tab-switch-performance.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..', '..');

const helpers = await import(pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'session-helpers.mjs')));
const {
Expand All @@ -13,6 +15,10 @@ const {
loadMoreSessionMessages
} = helpers;

function readProjectFile(relativePath) {
return fs.readFileSync(path.join(projectRoot, relativePath), 'utf8');
}

test('switchMainTab tears down session heavy render state when leaving sessions tab', () => {
const calls = {
teardown: 0,
Expand Down Expand Up @@ -98,6 +104,145 @@ test('switchMainTab keeps claude model context refresh behavior', () => {
assert.strictEqual(refreshCount, 1);
});

test('switchMainTab defers session teardown when scheduler exists to keep tab selection responsive', () => {
let deferredTask = null;
let teardownCount = 0;
const vm = {
mainTab: 'sessions',
configMode: 'codex',
sessionsLoadedOnce: true,
scheduleAfterFrame(task) {
deferredTask = task;
},
teardownSessionTabRender() {
teardownCount += 1;
},
prepareSessionTabRender() {},
loadSessions() {},
refreshClaudeModelContext() {}
};

switchMainTab.call(vm, 'settings');

assert.strictEqual(vm.mainTab, 'settings');
assert.strictEqual(typeof deferredTask, 'function');
assert.strictEqual(teardownCount, 0);

deferredTask();
assert.strictEqual(teardownCount, 1);
});

test('switchMainTab prefers idle teardown scheduler when available', () => {
let idleTask = null;
let frameTask = null;
let teardownCount = 0;
const vm = {
mainTab: 'sessions',
configMode: 'codex',
sessionsLoadedOnce: true,
scheduleSessionTabDeferredTeardown(task) {
idleTask = task;
},
scheduleAfterFrame(task) {
frameTask = task;
},
teardownSessionTabRender() {
teardownCount += 1;
},
prepareSessionTabRender() {},
loadSessions() {},
refreshClaudeModelContext() {}
};

switchMainTab.call(vm, 'settings');

assert.strictEqual(vm.mainTab, 'settings');
assert.strictEqual(typeof idleTask, 'function');
assert.strictEqual(frameTask, null);
assert.strictEqual(teardownCount, 0);

idleTask();
assert.strictEqual(teardownCount, 1);
});

test('switchMainTab suspends session render only when deferred finalize executes', () => {
let idleTask = null;
let suspendCount = 0;
let finalizeCount = 0;
const vm = {
mainTab: 'sessions',
configMode: 'codex',
sessionsLoadedOnce: true,
scheduleSessionTabDeferredTeardown(task) {
idleTask = task;
},
suspendSessionTabRender() {
suspendCount += 1;
},
finalizeSessionTabTeardown() {
finalizeCount += 1;
},
teardownSessionTabRender() {
throw new Error('fallback teardown should not run when finalize method exists');
},
prepareSessionTabRender() {},
loadSessions() {},
refreshClaudeModelContext() {}
};

switchMainTab.call(vm, 'settings');

assert.strictEqual(vm.mainTab, 'settings');
assert.strictEqual(suspendCount, 0);
assert.strictEqual(finalizeCount, 0);
assert.strictEqual(typeof idleTask, 'function');

idleTask();
assert.strictEqual(suspendCount, 1);
assert.strictEqual(finalizeCount, 1);
});

test('deferred teardown is skipped when user quickly switches back to sessions', () => {
let deferredTask = null;
let teardownCount = 0;
const vm = {
mainTab: 'sessions',
configMode: 'codex',
sessionsLoadedOnce: true,
scheduleAfterFrame(task) {
deferredTask = task;
},
teardownSessionTabRender() {
teardownCount += 1;
},
prepareSessionTabRender() {},
loadSessions() {},
refreshClaudeModelContext() {}
};

switchMainTab.call(vm, 'settings');
assert.strictEqual(vm.mainTab, 'settings');
assert.strictEqual(typeof deferredTask, 'function');

vm.mainTab = 'sessions';
deferredTask();
assert.strictEqual(teardownCount, 0);
});

test('session timeline stays always-on and no longer exposes toggle handler', () => {
const appScript = readProjectFile('web-ui/app.js');
assert.match(appScript, /sessionTimelineEnabled:\s*true,/);
assert.doesNotMatch(appScript, /toggleSessionTimeline\(\)/);
});

test('session template removes timeline switch button and binds refs by timeline node keys', () => {
const template = readProjectFile('web-ui/index.html');
assert.doesNotMatch(template, /@click="toggleSessionTimeline"/);
assert.doesNotMatch(template, /开启时间轴|关闭时间轴/);
assert.match(template, /:ref="getSessionMessageRefBinder\(getRecordRenderKey\(msg,\s*idx\)\)"/);
assert.match(template, /<aside v-if="sessionPreviewRenderEnabled && sessionTimelineNodes.length" class="session-timeline"/);
});

test('loadActiveSessionDetail primes visible messages even when timeline is disabled', async () => {
const vm = {
activeSession: {
Expand Down Expand Up @@ -138,6 +283,10 @@ test('loadActiveSessionDetail primes visible messages even when timeline is disa
scheduleSessionTimelineSync() {
this._syncCount = (this._syncCount || 0) + 1;
},
invalidateSessionTimelineMeasurementCache(resetOffset = false) {
this._invalidateCount = (this._invalidateCount || 0) + 1;
this._invalidateReset = !!resetOffset;
},
cancelSessionTimelineSync() {},
resetSessionPreviewMessageRender() {},
resetSessionDetailPagination() {},
Expand All @@ -157,6 +306,8 @@ test('loadActiveSessionDetail primes visible messages even when timeline is disa
assert.strictEqual(vm._primeCount, 1);
assert.strictEqual(vm._offsetCount, 1);
assert.strictEqual(vm._syncCount || 0, 0);
assert.strictEqual(vm._invalidateCount, 1);
assert.strictEqual(vm._invalidateReset, true);
assert.strictEqual(vm.sessionDetailLoading, false);
assert.strictEqual(vm.activeSessionDetailError, '');
});
Expand Down
Loading
Loading