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:
@@ -8,7 +8,10 @@ import { persist } from './persist.ts';
|
||||
import { nodeSqliteTransactionAdapter } from './tx.ts';
|
||||
import { acquireWriterLease, writerLockPathFor } from './writer-lease.ts';
|
||||
import { runRetryableWriteTransaction, isBeginBusyFailure, hasUnusableTransaction } from './write-coordinator.ts';
|
||||
import { parse as claudeParse } from './providers/claude.ts';
|
||||
import {
|
||||
CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER,
|
||||
parse as claudeParse,
|
||||
} from './providers/claude.ts';
|
||||
import { parse as codexParse } from './providers/codex.ts';
|
||||
import type { Cursor, IndexRecord } from './providers/types.ts';
|
||||
import type { ClaudeJsonlFile } from './parsing.ts';
|
||||
@@ -210,7 +213,17 @@ function buildIndex({ force = false }: { force?: boolean } = {}) {
|
||||
|
||||
const db = openDb();
|
||||
const txDb = nodeSqliteTransactionAdapter(db);
|
||||
const claudeInputMarkerMissing = !db.prepare(
|
||||
'SELECT jsonl_path FROM index_state WHERE jsonl_path = ?',
|
||||
).get(CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER);
|
||||
const claudeInputSemanticsOutdated = claudeInputMarkerMissing && Boolean(db.prepare(`
|
||||
SELECT 1 FROM messages
|
||||
WHERE COALESCE(source, 'claude') = 'claude'
|
||||
AND (input_tokens IS NOT NULL OR output_tokens IS NOT NULL)
|
||||
LIMIT 1
|
||||
`).get());
|
||||
const skippedFiles: SkippedFile[] = [];
|
||||
let claudeInputMigrationFailed = false;
|
||||
try {
|
||||
try {
|
||||
if (force) {
|
||||
@@ -261,9 +274,10 @@ function buildIndex({ force = false }: { force?: boolean } = {}) {
|
||||
// (docs/adr/0001). needsReindex keeps the "skip unchanged file" fast path;
|
||||
// the cursor's line count drives incremental resume inside parse().
|
||||
const { needed, skip } = needsReindex(db, f.path);
|
||||
if (needed) {
|
||||
if (needed || claudeInputSemanticsOutdated) {
|
||||
const unit = { key: f.path, sessionId: f.sessionId, project: f.project, isSubagent: f.isSubagent, agentId: f.agentId };
|
||||
persist(db, unit, claudeParse(unit, skip > 0 ? `0:${skip}` : null));
|
||||
const cursor = !claudeInputSemanticsOutdated && skip > 0 ? `0:${skip}` : null;
|
||||
persist(db, unit, claudeParse(unit, cursor));
|
||||
}
|
||||
indexSubagentMeta(db, f);
|
||||
}
|
||||
@@ -273,6 +287,9 @@ function buildIndex({ force = false }: { force?: boolean } = {}) {
|
||||
return { skip: true, reason: 'database_busy', skipped: skippedFiles.length, skippedFiles };
|
||||
}
|
||||
if (hasUnusableTransaction(e)) throw e;
|
||||
if (claudeInputSemanticsOutdated && f.source !== 'codex') {
|
||||
claudeInputMigrationFailed = true;
|
||||
}
|
||||
// A per-file failure is skippable: log and move on.
|
||||
const error = e as { message?: unknown; obelisk?: unknown } | null;
|
||||
const message = errorMessage(e);
|
||||
@@ -291,6 +308,10 @@ function buildIndex({ force = false }: { force?: boolean } = {}) {
|
||||
db.exec("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')");
|
||||
rebuildMemoryFts(db);
|
||||
db.prepare("INSERT OR REPLACE INTO index_state (jsonl_path, mtime, lines_processed) VALUES ('__last_build__', ?, 0)").run(Date.now());
|
||||
if (!claudeInputSemanticsOutdated || !claudeInputMigrationFailed) {
|
||||
db.prepare('INSERT OR REPLACE INTO index_state (jsonl_path, mtime, lines_processed) VALUES (?, ?, 0)')
|
||||
.run(CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER, Date.now());
|
||||
}
|
||||
}, { label: 'finalize' });
|
||||
} catch (error) {
|
||||
if (isBeginBusyFailure(error)) {
|
||||
|
||||
Reference in New Issue
Block a user