feat(app): live session update + tool renderer + input_tokens migration
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>
This commit is contained in:
@@ -7,6 +7,7 @@ import { join } from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
import { buildIndex } from '../app/src/main/indexer.ts';
|
||||
import { CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER } from '../packages/core/src/providers/claude.ts';
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
class TestDatabase {
|
||||
@@ -89,6 +90,61 @@ test('app indexer records build success without claiming daemon ownership', () =
|
||||
db2.close();
|
||||
});
|
||||
|
||||
test('app indexer refreshes unchanged Claude usage when input token semantics change', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'obelisk-app-indexer-token-semantics-'));
|
||||
const claudeDir = join(home, '.claude');
|
||||
const projectDir = join(claudeDir, 'projects', '-tmp-obelisk-app');
|
||||
mkdirSync(projectDir, { recursive: true });
|
||||
const sessionId = 'session-token-semantics-1';
|
||||
const jsonlPath = join(projectDir, `${sessionId}.jsonl`);
|
||||
writeFileSync(jsonlPath, [
|
||||
JSON.stringify({
|
||||
uuid: 'msg-token-semantics-1',
|
||||
type: 'assistant',
|
||||
timestamp: '2026-06-13T10:00:00Z',
|
||||
cwd: '/tmp/obelisk-app',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'cached response' }],
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
cache_creation_input_tokens: 20,
|
||||
cache_read_input_tokens: 30,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
const dbPath = join(claudeDir, 'obelisk.sqlite');
|
||||
buildIndex({ claudeDir, dbPath, DatabaseImpl: TestDatabase });
|
||||
|
||||
const stale = new TestDatabase(dbPath);
|
||||
stale.prepare('UPDATE messages SET input_tokens = 10 WHERE uuid = ?').run('msg-token-semantics-1');
|
||||
stale.prepare('DELETE FROM index_state WHERE jsonl_path = ?')
|
||||
.run(CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER);
|
||||
stale.close();
|
||||
|
||||
buildIndex({
|
||||
claudeDir,
|
||||
dbPath,
|
||||
DatabaseImpl: TestDatabase,
|
||||
changedPaths: [`-tmp-obelisk-app/${sessionId}.jsonl`],
|
||||
});
|
||||
|
||||
const refreshed = new TestDatabase(dbPath);
|
||||
assert.equal(
|
||||
refreshed.prepare('SELECT input_tokens FROM messages WHERE uuid = ?').get('msg-token-semantics-1').input_tokens,
|
||||
60,
|
||||
);
|
||||
assert.ok(
|
||||
refreshed.prepare('SELECT jsonl_path FROM index_state WHERE jsonl_path = ?')
|
||||
.get(CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER),
|
||||
);
|
||||
refreshed.close();
|
||||
});
|
||||
|
||||
test('force rebuild ignores stale JSONL index_state rows after session tables were cleared', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'obelisk-app-indexer-force-'));
|
||||
const claudeDir = join(home, '.claude');
|
||||
|
||||
Reference in New Issue
Block a user