chore(ui): finalize timeline selection audit fixes

Complete re-review of PR #188 (commits 224cab6 feature + 2c27fc5 perf/i18n follow-up). Gatekeeper focus: standards, correctness, perf/complexity, and translation completeness.

What this changes (pre -> post)

Pre: timeline primarily navigation/hover preview; bulk delete selection message-level and token metrics tied to backend assistant output tokens (missing tool payload weight).

Post: segment-level timeline selection + range (Shift) + toggle (Ctrl/Meta) + mobile long-press; histogram ribs overlay showing relative + absolute (~10k cap) token weight; assistant-turn grouping to avoid adjacency bugs; bulk-delete toolbar shows Before / Selection / After token pills.

Code standards / correctness

OK: Solid signal/memo/effect patterns with cleanup; no obvious lifecycle leaks. Grouping avoids adjacency overlap by mapping messageId to turns.

Fix: selection-id stability is mitigated by pruning stale ids after segment rebuilds; long term stable ids from part ids/toolPartIds remain recommended.

Fix: token counts now share getPartCharCount in both x-ray overlay and bulk-delete toolbar, keeping estimates consistent with live store updates.

Performance / complexity

OK: O(n^2) hotspots removed for liveSegmentChars and selectedTokenTotal. groupRole + deleteUpTo hover checks now memoize messageId sets/maps.

Note: getPartCharCount can be heavy for large tool payloads but remains gated behind selection mode.

CSS / UI integration

Fix: x-ray token label now uses theme tokens instead of hard-coded colors. Delete toolbar now uses menu-based controls with selection-mode toggle.

i18n

Fix: selection hint now renders Cmd/Ctrl via localized modifier placeholder; all locales updated.
This commit is contained in:
VooDisss
2026-03-03 03:49:51 +02:00
parent 2c27fc53ad
commit ed322a16bf
11 changed files with 436 additions and 213 deletions

View File

@@ -0,0 +1,70 @@
import type { ClientPart } from "../types/message"
/**
* Count the total character content of a message part.
*
* Used by both the xray histogram overlay (message-timeline) and the
* bulk-delete toolbar token pills (message-section) so both surfaces
* derive token estimates from the same logic.
*
* Skips `filediff` metadata — it contains full before/after file content
* and would inflate the character count by 10-100x for large files.
*/
export function getPartCharCount(part: ClientPart): number {
if (!part) return 0
let count = 0
if (typeof (part as any).text === "string") {
count += (part as any).text.length
}
if (part.type === "tool") {
const state = (part as any).state
if (state) {
if (state.input) {
try {
count += JSON.stringify(state.input).length
} catch {}
}
if (state.output) {
if (typeof state.output === "string") {
count += state.output.length
} else {
try {
count += JSON.stringify(state.output).length
} catch {}
}
}
if (state.metadata) {
for (const [key, val] of Object.entries(state.metadata)) {
if (key === "filediff") continue
if (typeof val === "string") {
count += val.length
} else if (val && typeof val === "object") {
try {
count += JSON.stringify(val).length
} catch {}
}
}
}
}
}
if (Array.isArray((part as any).content)) {
count += (part as any).content.reduce((acc: number, entry: unknown) => {
if (typeof entry === "string") return acc + entry.length
if (entry && typeof entry === "object") {
let entryCount = (String((entry as any).text || "")).length + (String((entry as any).value || "")).length
if (Array.isArray((entry as any).content)) {
entryCount += (entry as any).content.reduce((innerAcc: number, sub: unknown) => {
if (typeof sub === "string") return innerAcc + sub.length
return innerAcc + (String((sub as any)?.text || "")).length
}, 0)
}
return acc + entryCount
}
return acc
}, 0)
}
return count
}