SessionDetail live update: - Extract session-view-state.mjs: capture scroll position, disclosure (open/skill-md-open) state, and visible-UUID anchor before refresh; reconcile messages by UUID (in-place update, append tail only); restore scroll and disclosure state after DOM patch. findLastMessageAtOrAbove uses binary search (O(log n)) instead of linear scan. - scrollRevision tracks user scrolls during refresh to avoid stale anchors overriding manual navigation. - Throttle onScroll to one rAF per frame. Tool renderer: - Extract tool-renderer.js: standalone module for rendering tool call cards (Read/Write/Edit diffs, Bash terminal output, search results, JS/TS syntax highlighting). Replaces inline rendering in SessionDetail. - tests/app-tool-renderer.test.mjs covers escaping, highlighting, and terminal formatting. Input tokens semantics migration: - Claude provider now sums input_tokens + cache_creation_input_tokens + cache_read_input_tokens into a single input_tokens value (was previously only the raw field, undercounting when cache tokens are present). - One-time index-wide re-parse triggered when the marker __claude_input_tokens_include_cache_v1__ is absent and the DB already has token data (self-healing on first build after upgrade). - App indexer.ts carries the same marker check for the app's build path. Also: - PRODUCT.md: product register (users, purpose, brand, design principles, accessibility targets). - README.md: minor wording updates. Co-Authored-By: Codex (GPT-5) <noreply@openai.com>
113 lines
5.1 KiB
JavaScript
113 lines
5.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { getArgPreview, getToolIcon, renderTerminalTool } from '../app/src/renderer/src/tool-renderer.js';
|
|
|
|
test('Codex exec renders source and decoded result instead of a Bash terminal', () => {
|
|
const output = JSON.stringify([
|
|
{ type: 'input_text', text: 'Script completed\nWall time 0.1 seconds\nOutput:\n' },
|
|
{ type: 'input_text', text: 'first line\nsecond line' },
|
|
]);
|
|
const html = renderTerminalTool('exec', 'const value = await tools.read();', output, false);
|
|
|
|
assert.match(html, /class="codeact-view is-complete"/);
|
|
assert.match(html, />Source</);
|
|
assert.match(html, /codeact-token keyword">const</);
|
|
assert.match(html, /codeact-token keyword">await</);
|
|
assert.match(html, /codeact-token global">tools</);
|
|
assert.doesNotMatch(html, />JavaScript</);
|
|
assert.match(html, />Result</);
|
|
assert.match(html, /first line\nsecond line/);
|
|
assert.doesNotMatch(html, />Completed</);
|
|
assert.doesNotMatch(html, /codeact-status-dot/);
|
|
assert.doesNotMatch(html, /0\.1 seconds/);
|
|
assert.doesNotMatch(html, /terminal-view|input_text|Script completed/);
|
|
});
|
|
|
|
test('Codex exec formats consecutive JSON result blocks independently', () => {
|
|
const output = JSON.stringify([
|
|
{ type: 'input_text', text: 'Script completed\nWall time 0.2 seconds\nOutput:\n' },
|
|
{ type: 'input_text', text: JSON.stringify({ exit_code: 0, output: 'first\nline' }) },
|
|
{ type: 'input_text', text: JSON.stringify({ exit_code: 1, output: 'second\nline' }) },
|
|
]);
|
|
const html = renderTerminalTool('exec', 'await task();', output, false);
|
|
const visibleText = html.replace(/<[^>]+>/g, '');
|
|
const resultBlockCount = html.match(/class="codeact-result-block/g)?.length || 0;
|
|
|
|
assert.equal(resultBlockCount, 2);
|
|
assert.match(visibleText, /\{\n {2}"exit_code": 0,\n {2}"output": "first\\nline"\n\}/);
|
|
assert.match(visibleText, /\{\n {2}"exit_code": 1,\n {2}"output": "second\\nline"\n\}/);
|
|
assert.match(html, /codeact-json-token key">"exit_code"</);
|
|
assert.match(html, /codeact-json-token number">0</);
|
|
assert.doesNotMatch(visibleText, /\{"exit_code":/);
|
|
});
|
|
|
|
test('Claude Bash continues to use terminal rendering', () => {
|
|
const html = renderTerminalTool('Bash', { command: 'echo hello' }, 'hello\n', false);
|
|
|
|
assert.match(html, /class="terminal-view"/);
|
|
assert.match(html, /echo hello/);
|
|
});
|
|
|
|
test('Codex exec decodes a truncated input_text envelope without inventing missing output', () => {
|
|
const truncated = '[{"type":"input_text","text":"Script completed\\nWall time 0.2 seconds\\nOutput:\\n"},{"type":"input_text","text":"line 1\\nline 2';
|
|
const html = renderTerminalTool('exec', 'text(result);', truncated, false);
|
|
|
|
assert.match(html, /line 1\nline 2/);
|
|
assert.match(html, /Indexed output truncated/);
|
|
assert.doesNotMatch(html, /\\n|input_text/);
|
|
});
|
|
|
|
test('Codex exec derives failed and running states from the captured result', () => {
|
|
const failed = JSON.stringify([
|
|
{ type: 'input_text', text: 'Script failed\nWall time 9.4 seconds\nOutput:\n' },
|
|
{ type: 'input_text', text: 'Script error:\nExit code: 1' },
|
|
]);
|
|
const running = 'Script running with cell ID 128\nWall time 10.0 seconds\nOutput:\n';
|
|
|
|
const failedHtml = renderTerminalTool('exec', 'await task();', failed, false);
|
|
const runningHtml = renderTerminalTool('exec', 'await task();', running, false);
|
|
|
|
assert.match(failedHtml, /class="codeact-view is-failed"/);
|
|
assert.match(failedHtml, />Failed</);
|
|
assert.match(runningHtml, /class="codeact-view is-running"/);
|
|
assert.match(runningHtml, />Running</);
|
|
});
|
|
|
|
test('Codex exec keeps large payloads in a bounded DOM structure', () => {
|
|
const source = 'const value = await tools.read();\n'.repeat(300);
|
|
const output = JSON.stringify([
|
|
{ type: 'input_text', text: 'Script completed\nWall time 0.3 seconds\nOutput:\n' },
|
|
{ type: 'input_text', text: 'x'.repeat(9_000) },
|
|
]);
|
|
const html = renderTerminalTool('exec', source, output, false);
|
|
const spanCount = html.match(/<span\b/g)?.length || 0;
|
|
const preCount = html.match(/<pre\b/g)?.length || 0;
|
|
|
|
assert.ok(spanCount < source.length / 4, `expected token-level markup, received ${spanCount} spans`);
|
|
assert.equal(preCount, 3);
|
|
assert.doesNotMatch(html, /code-char|ansi-char/);
|
|
});
|
|
|
|
test('Codex exec syntax highlighting escapes source content', () => {
|
|
const source = 'const value = "<script>"; // not markup';
|
|
const output = JSON.stringify([
|
|
{ type: 'input_text', text: 'Script completed\nWall time 0.1 seconds\nOutput:\n' },
|
|
{ type: 'input_text', text: JSON.stringify({ html: '<script>result</script>' }) },
|
|
]);
|
|
const html = renderTerminalTool('exec', source, output, false);
|
|
|
|
assert.match(html, /codeact-token string">"<script>"</);
|
|
assert.match(html, /codeact-token comment">\/\/ not markup</);
|
|
assert.match(html, /codeact-json-token string">"<script>result<\/script>"</);
|
|
assert.doesNotMatch(html, /<script>/);
|
|
});
|
|
|
|
test('Codex exec preview uses its string input', () => {
|
|
assert.equal(getArgPreview({ input_json: JSON.stringify('echo hello') }), 'echo hello');
|
|
});
|
|
|
|
test('Codex exec uses the Claude Bash terminal icon', () => {
|
|
assert.equal(getToolIcon('exec'), getToolIcon('Bash'));
|
|
});
|