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
103 changes: 88 additions & 15 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -5775,11 +5775,21 @@ async function listSessionUsage(params = {}) {
const limit = Number.isFinite(rawLimit)
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
: 200;
return await listAllSessionsData({
const sessions = await listAllSessions({
source,
limit,
forceRefresh: !!params.forceRefresh
});
return Array.isArray(sessions)
? sessions.map((item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) {
return item;
}
const normalized = { ...item };
delete normalized.__messageCountExact;
return normalized;
})
: [];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

function listSessionPaths(params = {}) {
Expand Down Expand Up @@ -10765,6 +10775,82 @@ const PUBLIC_WEB_UI_STATIC_ASSETS = new Set([

function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
const connections = new Set();
const probeWebUiReadiness = (callback) => {
const payload = JSON.stringify({ action: 'status', params: {} });
const requestOptions = {
hostname: openHost,
port,
path: '/api',
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(payload, 'utf-8')
}
};
let settled = false;
const finish = (ready) => {
if (settled) return;
settled = true;
callback(ready);
};
const req = http.request(requestOptions, (probeRes) => {
if (typeof probeRes.resume === 'function') {
probeRes.resume();
}
probeRes.on('end', () => {
finish(probeRes.statusCode === 200);
});
});
req.on('error', () => finish(false));
req.setTimeout(1000, () => {
try { req.destroy(); } catch (_) {}
finish(false);
});
req.end(payload, 'utf-8');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};
const openBrowserAfterReady = (url) => {
const maxAttempts = 40;
const retryDelayMs = 150;
let finished = false;

const finish = (ready) => {
if (finished) return;
finished = true;
if (!ready) {
console.warn('! Web UI 就绪探测超时,未自动打开浏览器,请手动访问:', url);
return;
}

const platform = process.platform;
let command;
if (platform === 'win32') {
command = `start \"\" \"${url}\"`;
} else if (platform === 'darwin') {
command = `open \"${url}\"`;
} else {
command = `xdg-open \"${url}\"`;
}

exec(command, (error) => {
if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};
const scheduleProbe = (attempt) => {
probeWebUiReadiness((ready) => {
if (ready) {
finish(true);
return;
}
if (attempt >= maxAttempts) {
finish(false);
return;
}
setTimeout(() => scheduleProbe(attempt + 1), retryDelayMs);
});
};

scheduleProbe(1);
};
const writeWebUiAssetError = (res, requestPath, error) => {
const message = error && error.message ? error.message : String(error);
console.error(`! Web UI 资源读取失败 [${requestPath}]:`, message);
Expand Down Expand Up @@ -11373,21 +11459,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
}

if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) {
const platform = process.platform;
let command;
const url = openUrl;

if (platform === 'win32') {
command = `start \"\" \"${url}\"`;
} else if (platform === 'darwin') {
command = `open \"${url}\"`;
} else {
command = `xdg-open \"${url}\"`;
}

exec(command, (error) => {
if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
});
openBrowserAfterReady(url);
}
});

Expand Down
5 changes: 4 additions & 1 deletion tests/unit/compact-layout-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ test('styles keep desktop layout wide and session history readable on large scre
assert.match(styles, /\.main-panel-topbar\s*\{[\s\S]*position:\s*sticky;[\s\S]*top:\s*0;/);
assert.match(styles, /\.side-item-meta\s*\{[\s\S]*display:\s*flex;[\s\S]*opacity:\s*1;/);
assert.match(styles, /\.brand-logo\s*\{[\s\S]*width:\s*38px;[\s\S]*height:\s*38px;/);
assert.match(styles, /\.content-wrapper\s*\{[\s\S]*max-width:\s*1280px;/);
assert.match(styles, /\.content-wrapper\s*\{[\s\S]*width:\s*min\(100%,\s*1480px\);[\s\S]*max-width:\s*none;/);
assert.match(styles, /\.mode-content\s*\{[\s\S]*width:\s*100%;/);
assert.match(styles, /\.trash-item-actions\s*\{[\s\S]*grid-template-columns:\s*repeat\(2,\s*minmax\(116px,\s*116px\)\);/);
assert.match(styles, /\.trash-item-actions\s+\.btn-mini\s*\{[\s\S]*height:\s*38px;[\s\S]*min-height:\s*38px;[\s\S]*white-space:\s*nowrap;/);
assert.match(styles, /\.session-layout\s*\{[\s\S]*grid-template-columns:\s*minmax\(260px,\s*360px\)\s*minmax\(0,\s*1fr\);/);
assert.match(styles, /\.session-preview-scroll\s*\{[\s\S]*padding-right:\s*52px;/);
assert.match(styles, /\.session-timeline\s*\{[\s\S]*right:\s*4px;[\s\S]*width:\s*44px;/);
assert.match(styles, /\.session-item\s*\{[\s\S]*min-height:\s*102px;/);

const html = readBundledWebUiHtml();
Expand Down
6 changes: 5 additions & 1 deletion tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(html, /isProviderConfigMode/);
assert.match(html, /provider-fast-switch-select/);
assert.match(html, /forceCompactLayout/);
assert.match(html, /<script src="\/res\/vue\.global\.js"><\/script>/);
assert.match(html, /<script src="\/res\/vue\.global\.prod\.js"><\/script>/);
assert.doesNotMatch(html, /<script src="\/res\/vue\.global\.js"><\/script>/);
assert.match(html, /quickSwitchProvider\(\$event\.target\.value\)/);
assert.match(html, /onMainTabPointerDown\('sessions', \$event\)/);
assert.match(html, /onConfigTabPointerDown\('codex', \$event\)/);
Expand Down Expand Up @@ -69,6 +70,9 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(html, /:aria-selected="mainTab === 'usage'"/);
assert.match(html, /id="panel-usage"/);
assert.match(html, /v-show="mainTab === 'usage'"/);
assert.match(usagePanel, /sessionsUsageLoading && !sessionsUsageList\.length" class="session-empty">正在加载 Usage 统计\.\.\.</);
assert.match(usagePanel, /sessionsUsageError && !sessionsUsageList\.length" class="usage-empty">/);
assert.match(usagePanel, /v-else-if="!sessionsUsageList\.length" class="usage-empty">暂无可用于统计的会话数据/);
assert.match(html, /data-main-tab="market"/);
assert.match(html, /onMainTabPointerDown\('market', \$event\)/);
assert.match(html, /onMainTabClick\('market', \$event\)/);
Expand Down
1 change: 1 addition & 0 deletions tests/unit/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ await import(pathToFileURL(path.join(__dirname, 'agents-modal-guards.test.mjs'))
await import(pathToFileURL(path.join(__dirname, 'session-actions-standalone.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'session-browser-timeline-regression.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'session-usage.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'session-usage-backend.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'agents-diff-ui.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'text-diff.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'claude-settings-sync.test.mjs')));
Expand Down
115 changes: 115 additions & 0 deletions tests/unit/session-usage-backend.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import assert from 'assert';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const fs = require('fs');

const cliPath = path.join(__dirname, '..', '..', 'cli.js');
const cliContent = fs.readFileSync(cliPath, 'utf-8');

function extractFunction(content, funcName) {
const regex = new RegExp(`async function ${funcName}\\([^)]*\\)\\s*\\{[\\s\\S]*?^\\}`, 'm');
const match = content.match(regex);
if (!match) {
throw new Error(`Function ${funcName} not found`);
}
return match[0];
}

const listSessionUsageSrc = extractFunction(cliContent, 'listSessionUsage');

function instantiateListSessionUsage(bindings = {}) {
const bindingNames = Object.keys(bindings);
const bindingValues = Object.values(bindings);
return Function(...bindingNames, `${listSessionUsageSrc}\nreturn listSessionUsage;`)(...bindingValues);
}

test('listSessionUsage uses lightweight session listing without exact hydration', async () => {
const calls = [];
const listAllSessions = async (params) => {
calls.push({ type: 'listAllSessions', params });
return [
{
source: 'codex',
sessionId: 'sess-1',
messageCount: 12,
__messageCountExact: false
}
];
};
const listAllSessionsData = async () => {
calls.push({ type: 'listAllSessionsData' });
throw new Error('should not call listAllSessionsData');
};
const listSessionUsage = instantiateListSessionUsage({
MAX_SESSION_LIST_SIZE: 300,
listAllSessions,
listAllSessionsData
});

const result = await listSessionUsage({
source: 'all',
limit: 200,
forceRefresh: true
});

assert.deepStrictEqual(
calls,
[
{
type: 'listAllSessions',
params: {
source: 'all',
limit: 200,
forceRefresh: true
}
}
]
);
assert.deepStrictEqual(result, [
{
source: 'codex',
sessionId: 'sess-1',
messageCount: 12
}
]);
});

test('listSessionUsage normalizes source and default limit for lightweight usage aggregation', async () => {
const calls = [];
const listAllSessions = async (params) => {
calls.push(params);
return [];
};
const listSessionUsage = instantiateListSessionUsage({
MAX_SESSION_LIST_SIZE: 300,
listAllSessions,
listAllSessionsData: async () => {
throw new Error('should not call listAllSessionsData');
}
});

await listSessionUsage({
source: 'invalid',
limit: '9999',
forceRefresh: 1
});
await listSessionUsage({});

assert.deepStrictEqual(calls, [
{
source: 'all',
limit: 300,
forceRefresh: true
},
{
source: 'all',
limit: 200,
forceRefresh: false
}
]);
});
Loading
Loading