Skip to content

Commit be39109

Browse files
committed
Merge pull request #83 from SakuraByteCore/fix/web-ui-startup-and-usage-performance
fix: stabilize web ui startup and reduce usage load
2 parents b6ade58 + 03336f6 commit be39109

37 files changed

Lines changed: 3296 additions & 155 deletions

cli.js

Lines changed: 634 additions & 47 deletions
Large diffs are not rendered by default.

tests/e2e/run.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const testMcp = require('./test-mcp');
2424
const testWorkflow = require('./test-workflow');
2525
const testInvalidConfig = require('./test-invalid-config');
2626
const testWebUiAssets = require('./test-web-ui-assets');
27+
const testWebUiSessionBrowser = require('./test-web-ui-session-browser');
2728

2829
async function main() {
2930
const realHome = os.homedir();
@@ -132,6 +133,7 @@ async function main() {
132133
await testMcp(ctx);
133134
await testWorkflow(ctx);
134135
await testWebUiAssets(ctx);
136+
await testWebUiSessionBrowser(ctx);
135137

136138
} finally {
137139
const waitForExit = new Promise((resolve) => {

tests/e2e/test-mcp.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ module.exports = async function testMcp(ctx) {
209209
assert(item.messageCount === httpItem.messageCount, `mcp session.list messageCount drifted for ${item.sessionId}`);
210210
}
211211
const mcpLongSession = sessionListPayload.sessions.find((item) => item && item.sessionId === longSessionId);
212-
assert(mcpLongSession && mcpLongSession.messageCount === longMessageCount, 'mcp session.list should expose exact long-session messageCount');
212+
assert(mcpLongSession && Number.isFinite(mcpLongSession.messageCount), 'mcp session.list should expose numeric long-session messageCount');
213+
assert(mcpLongSession && mcpLongSession.messageCount >= 0, 'mcp session.list should expose non-negative long-session messageCount');
213214

214215
const httpAllSessions = await api('list-sessions', { source: 'all', forceRefresh: true, limit: sessionResourcePayload.sessions.length || 120 });
215216
const httpAllByKey = new Map((httpAllSessions.sessions || []).map((item) => [
@@ -223,7 +224,8 @@ module.exports = async function testMcp(ctx) {
223224
assert(item.messageCount === httpItem.messageCount, `mcp sessions resource messageCount drifted for ${key}`);
224225
}
225226
const resourceLongSession = sessionResourcePayload.sessions.find((item) => item && item.sessionId === longSessionId);
226-
assert(resourceLongSession && resourceLongSession.messageCount === longMessageCount, 'mcp sessions resource should expose exact long-session messageCount');
227+
assert(resourceLongSession && Number.isFinite(resourceLongSession.messageCount), 'mcp sessions resource should expose numeric long-session messageCount');
228+
assert(resourceLongSession && resourceLongSession.messageCount >= 0, 'mcp sessions resource should expose non-negative long-session messageCount');
227229

228230
const claudeSettingsPayload = ((readOnlyById.get(5).result || {}).structuredContent) || {};
229231
assert(claudeSettingsPayload.redacted === true, 'mcp claude.settings.get should mark payload as redacted');

tests/e2e/test-sessions.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ module.exports = async function testSessions(ctx) {
4040
assert(usageSessions.sessions.some((item) => item.sessionId === sessionId), 'list-sessions-usage missing codex entry');
4141
assert(usageSessions.sessions.some((item) => item.sessionId === claudeSessionId), 'list-sessions-usage missing claude entry');
4242
assert(usageSessions.sessions.every((item) => !Object.prototype.hasOwnProperty.call(item, '__messageCountExact')), 'list-sessions-usage should not expose exact hydration markers');
43+
const usageCodexEntry = usageSessions.sessions.find((item) => item.sessionId === sessionId);
44+
assert(usageCodexEntry && usageCodexEntry.totalTokens === 120, 'list-sessions-usage missing codex totalTokens');
45+
assert(usageCodexEntry && usageCodexEntry.contextWindow === 128000, 'list-sessions-usage missing codex contextWindow');
4346
const defaultUsageSessions = await api('list-sessions-usage');
4447
assert(Array.isArray(defaultUsageSessions.sessions), 'list-sessions-usage without params should still return sessions');
4548
assert(defaultUsageSessions.source === 'all', 'list-sessions-usage without params should default source to all');
@@ -118,6 +121,8 @@ module.exports = async function testSessions(ctx) {
118121
const longSessionId = 'codex-long-trash-count-e2e';
119122
const longSessionPath = path.join(tmpHome, '.codex', 'sessions', `${longSessionId}.jsonl`);
120123
const longMessageCount = 1205;
124+
const hugeLineSessionId = 'codex-huge-line-preview-e2e';
125+
const hugeLineSessionPath = path.join(tmpHome, '.codex', 'sessions', `${hugeLineSessionId}.jsonl`);
121126
const trashRoot = path.join(tmpHome, '.codex', 'codexmate-session-trash');
122127
const trashFilesDir = path.join(trashRoot, 'files');
123128
const trashIndexPath = path.join(trashRoot, 'index.json');
@@ -264,7 +269,8 @@ module.exports = async function testSessions(ctx) {
264269
const restoredIndexlessClaudeSessions = await api('list-sessions', { source: 'claude', limit: 200, forceRefresh: true });
265270
const restoredIndexlessClaudeItem = restoredIndexlessClaudeSessions.sessions.find(item => item.sessionId === indexlessClaudeSessionId);
266271
assert(restoredIndexlessClaudeItem, 'restored indexless Claude session should be listed again');
267-
assert(restoredIndexlessClaudeItem.messageCount === indexlessClaudeMessageCount, 'restored indexless Claude session should keep exact list messageCount');
272+
assert(Number.isFinite(restoredIndexlessClaudeItem.messageCount), 'restored indexless Claude session should keep numeric list messageCount');
273+
assert(restoredIndexlessClaudeItem.messageCount >= 0, 'restored indexless Claude session should keep non-negative list messageCount');
268274

269275
const longSessionRecords = [{
270276
type: 'session_meta',
@@ -287,7 +293,14 @@ module.exports = async function testSessions(ctx) {
287293
const longSessionsBeforeDelete = await api('list-sessions', { source: 'codex', limit: 200, forceRefresh: true });
288294
const longSessionListItem = longSessionsBeforeDelete.sessions.find(item => item.sessionId === longSessionId);
289295
assert(longSessionListItem, 'long codex session should appear in list-sessions');
290-
assert(longSessionListItem.messageCount === longMessageCount, 'list-sessions should return exact long-session messageCount');
296+
assert(Number.isFinite(longSessionListItem.messageCount), 'list-sessions should return numeric long-session messageCount');
297+
assert(longSessionListItem.messageCount >= 0, 'list-sessions should return non-negative long-session messageCount');
298+
const longSessionPreview = await api('session-detail', { source: 'codex', sessionId: longSessionId, messageLimit: 80, preview: true });
299+
assert(Array.isArray(longSessionPreview.messages), 'session-detail preview should return messages');
300+
assert(longSessionPreview.messages.length > 0, 'session-detail preview should keep recent messages');
301+
assert(longSessionPreview.messages.length <= 80, 'session-detail preview should respect preview messageLimit');
302+
assert(longSessionPreview.clipped === true, 'session-detail preview should stay clipped for long sessions');
303+
assert(Number.isFinite(longSessionPreview.totalMessages) === false, 'session-detail preview should avoid exact totalMessages for long sessions');
291304
const longSessionDetail = await api('session-detail', { source: 'codex', sessionId: longSessionId });
292305
assert(longSessionDetail.totalMessages === longMessageCount, 'session-detail should return exact long-session totalMessages');
293306
assert(longSessionDetail.messageLimit === 300, 'session-detail should keep default detail window size');
@@ -296,6 +309,51 @@ module.exports = async function testSessions(ctx) {
296309
assert(longSessionDetail.messages[0].messageIndex === longMessageCount - longSessionDetail.messages.length, 'session-detail should keep the latest message indexes');
297310
assert(longSessionDetail.messages[longSessionDetail.messages.length - 1].messageIndex === longMessageCount - 1, 'session-detail should keep the latest tail message index');
298311

312+
const hugeLineRecords = [{
313+
type: 'session_meta',
314+
payload: { id: hugeLineSessionId, cwd: '/tmp/huge-line-preview' },
315+
timestamp: '2025-03-01T00:00:00.000Z'
316+
}];
317+
for (let i = 0; i < 3; i += 1) {
318+
hugeLineRecords.push({
319+
type: 'response_item',
320+
payload: {
321+
type: 'message',
322+
role: i % 2 === 0 ? 'user' : 'assistant',
323+
content: `huge-line-preview-${i}-` + 'q'.repeat(1300000)
324+
},
325+
timestamp: buildTimestamp('2025-03-07T00:00:00.000Z', i)
326+
});
327+
}
328+
fs.writeFileSync(hugeLineSessionPath, hugeLineRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');
329+
330+
const hugeLinePreview = await api('session-detail', {
331+
source: 'codex',
332+
sessionId: hugeLineSessionId,
333+
messageLimit: 80,
334+
preview: true
335+
});
336+
assert(Array.isArray(hugeLinePreview.messages), 'session-detail preview should return messages for huge-line sessions');
337+
assert(hugeLinePreview.messages.length > 0, 'session-detail preview should fall back when huge lines exceed the fast tail window');
338+
assert(hugeLinePreview.messages.length <= 3, 'session-detail preview should not duplicate huge-line messages');
339+
assert(hugeLinePreview.clipped === false, 'session-detail preview should report unclipped when fallback can read the whole huge-line session');
340+
assert(
341+
hugeLinePreview.messages.every((message) => typeof message.text === 'string' && message.text.length <= 4000),
342+
'session-detail preview should cap huge-line payload text before sending it to the web ui'
343+
);
344+
345+
const hugeLineDetail = await api('session-detail', {
346+
source: 'codex',
347+
sessionId: hugeLineSessionId,
348+
messageLimit: 80
349+
});
350+
assert(hugeLineDetail.totalMessages === 3, 'session-detail should keep exact totalMessages for huge-line sessions');
351+
assert(hugeLineDetail.messages.length === 3, 'session-detail should keep all huge-line messages when under limit');
352+
assert(
353+
hugeLineDetail.messages.some((message) => typeof message.text === 'string' && message.text.length > 1000000),
354+
'full session-detail should keep the original huge-line content outside preview mode'
355+
);
356+
299357
deleteLongResult = await api('trash-session', { source: 'codex', sessionId: longSessionId });
300358
assert(deleteLongResult.success === true, 'trash-session should trash long codex session');
301359
assert(deleteLongResult.messageCount === longMessageCount, 'trash-session should return exact long-session messageCount');

tests/e2e/test-setup.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ module.exports = async function testSetup(ctx) {
8787
type: 'response_item',
8888
payload: { type: 'message', role: 'assistant', content: 'world' },
8989
timestamp: '2025-01-01T00:00:02.000Z'
90+
},
91+
{
92+
type: 'event_msg',
93+
payload: {
94+
type: 'token_usage',
95+
info: {
96+
total_token_usage: {
97+
input_tokens: 80,
98+
output_tokens: 40,
99+
total_tokens: 120
100+
},
101+
model_context_window: 128000
102+
}
103+
},
104+
timestamp: '2025-01-01T00:00:03.000Z'
90105
}
91106
];
92107
fs.writeFileSync(sessionPath, sessionRecords.map(record => JSON.stringify(record)).join('\n') + '\n', 'utf-8');

tests/e2e/test-web-ui-assets.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,14 @@ module.exports = async function testWebUiAssets(ctx) {
5252
);
5353
assert(bundledIndex.body.includes('id="settings-panel-trash"'), '/web-ui/index.html should inline settings partials');
5454
assert(bundledIndex.body.includes('src="/web-ui/app.js"'), '/web-ui/index.html should point to the absolute app entry');
55-
assert(bundledIndex.body.includes('src="/res/vue.global.js"'), '/web-ui/index.html should use the compiler-enabled vue runtime');
56-
assert(!bundledIndex.body.includes('src="/res/vue.global.prod.js"'), '/web-ui/index.html should not use the prod-only vue runtime');
55+
assert(
56+
bundledIndex.body.includes('src="/res/vue.global.prod.js"'),
57+
'/web-ui/index.html should use the production Vue browser build'
58+
);
59+
assert(
60+
!bundledIndex.body.includes('src="/res/runtime.global.prod.js"'),
61+
'/web-ui/index.html should not use the runtime-only Vue build'
62+
);
5763
assert(!bundledIndex.body.includes('src="web-ui/app.js"'), '/web-ui/index.html should not use a relative app entry');
5864
assert(!/<!--\s*@include\s+/.test(bundledIndex.body), '/web-ui/index.html should not leak include directives');
5965

0 commit comments

Comments
 (0)