External PRs here have consistently arrived lint-clean, typechecking, and
green on their own tests, then stalled on the same handful of causes: a
capability advertised in the description but unreachable in code, assertions
written against the implementation rather than the requirement, destructive
migrations that cannot resume after an interruption, concepts reinvented next
to an existing one, transcript content treated as trusted, and verification
run against a head that a later merge invalidated.
Generic style rules would not have caught any of those, so CONTRIBUTING.md
states them directly, alongside hard per-area constraints for the
renderer/Electron, provider adapter, schema migration, main process, and
indexing/daemon boundaries. The PR template mirrors those as collapsible
per-area checklists so a renderer change is not asked about tokenizer
migrations, and gives deliberately-out-of-scope work its own place above the
fold rather than leaving it as a caveat inside a shipped diff.
README points at the document and repeats the four rules worth knowing before
reading it.
A decoder sizes an image from its header, so the row grows as soon as the
first bytes are parsed -- measured here at 421ms against a load event at
2407ms. Marking the row only once load fired left that first and largest
growth outside the compensation window, and scrolling back through history
still moved the reader by the full image height.
Rows are now marked from the moment an image mounts until loading finishes,
counted so a message with several images stays marked until the last one is
done. The listener also moves to the document: rows announce their images
while mounting, before a ref-driven listener on the timeline element would
exist to hear the first one, which is why the mount signal was being lost.
The fixture serves a real PNG and holds the response open, so the row grows
long before load the way a large image does; an SVG served in one shot lays
out and fires load together and cannot reproduce this. A companion assertion
checks the fixture still has that gap, so it cannot quietly stop testing the
timing it exists to test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Electron suites were only reachable one npm script at a time, so a new
one was covered by whoever remembered it existed. test:electron:all builds
once and runs all five, reporting a single summary and a non-zero exit.
The runner lives under tests/ rather than app/scripts/, which .gitignore
excludes entirely.
The image row spacing assertion also stops hard-coding the current gap --
it had 0.59px of headroom -- and calibrates against the spacing the rest of
the timeline is using.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
renderMarkdown ran the marked configuration on every call and leaned on a
module-level flag to make that cheap. It belongs next to the custom element
registration, which already runs once, before anything renders.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A source the Markdown renderer refuses now goes through the same element
with no src rather than a separate light-DOM span, so a blocked source and
a source that fails to load are one piece of UI instead of two. That drops
the span's stylesheet rule and the only reason the renderer built elements
just to serialise them.
Decoding what marked escaped no longer runs untrusted markup through a
detached element's innerHTML; the entities marked actually emits are
decoded in one pass, and attributes are escaped on the way out. With the
DOM dependency gone the renderer is directly unit-testable, so the escaping
and the protocol allowlist now have coverage that does not need Electron.
The element's shadow styles also stop hard-coding values that already exist
as tokens -- custom properties cross the shadow boundary, so --muted and
--hairline-strong are now the single source of truth -- and the height cap
becomes --session-image-max-block, which compact Markdown surfaces (subagent
panes, memory rows, tool results) lower from a viewport fraction to 240px.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wide-image probe resolved only from a load handler that a regression
could keep from ever firing, and it was awaited bare, so a broken build
hung the run instead of reporting a failure. Every renderer probe now runs
against a deadline, the probe rejects on image error, and the suite as a
whole is bounded.
Adds a held image endpoint so an above-viewport image can be made to finish
at a moment the test controls, and asserts the reader does not move -- both
at rest and mid-gesture, which is where it regressed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
virtual-core skips scroll compensation when an already-measured row above
the viewport is re-measured during an upward scroll: that is normally an
estimate correction, and compensating it makes rows jump while the reader
scrolls back through history. An image finishing loading is not an estimate
correction -- the row really did get taller -- so skipping compensation
pushed everything on screen down by the full image height. Scrolling back
through a session with screenshots moved the reader by ~490px per image.
The element now announces load and error through a composed event, and the
timeline compensates size changes for rows that just settled media while
leaving virtual-core's guard in place for every other re-measurement.
loading="lazy" comes off the image at the same time. Rows are only mounted
within a few viewports, so the attribute bought almost nothing while making
load timing depend on Chromium's connection heuristics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
marked <= 14 calls renderer.image(href, title, text); marked >= 15 passes
the token instead. With only the positional form handled, an upgrade of the
pinned CDN build would turn every session image into fallback text without
any error, so normalise both shapes and cover them with a unit test.
The tag name moves into session-image-contract.js so the Markdown renderer
no longer reaches it through the module that imports the .vue component,
which is what kept it out of Node's test runner.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mark absolute Markdown links and inline-code relative paths as file
references, resolve them against the originating message cwd, and open
them in the configured editor. Local links no longer navigate the SPA
away from the running renderer.
The canonical transcript assembly projected messages down to seven
fields, so cwd and session_id never reached the renderer. Both are added
to SessionDetailMessage: cwd because the working directory can change
mid-session, session_id because it scopes which roots a reference may
resolve inside. The main process derives those roots from the database
rather than trusting anything the renderer sends.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Electron 33 bundles Node 20.18, which has no type stripping. Every
tests/electron-*.mjs harness imports session-detail-assembly.mjs, which
in turn imports packages/core/src/session-detail.ts, so all three failed
at load with ERR_UNKNOWN_FILE_EXTENSION. Electron 43 bundles Node 24.18
and runs them.
better-sqlite3 11 does not compile against the newer V8 (removed
Context::GetIsolate, PropertyCallbackInfo::This, changed
SetNativeDataProperty signature), so the native dependency moves to 13
to match the ABI. electron-builder moves to 26 for Electron 43 support.
Verified: npm test 275/275, typecheck, electron-vite build, electron-builder
pack, and all three harnesses (15 + 55 + 11 assertions).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Treat token_count events without usage as null canonical token values so node:sqlite never receives undefined. Covers issue #1 with a parse-to-persist regression test.
- normalize Kimi thinking parts and omit empty placeholders
- add standalone session-sharing and UI demo artifacts
- add regression coverage and refresh the Kimi index marker
- bump the Electron app and CLI packages to v0.2.1
- ignore generated app build assets
Publish real virtual-row measurements while suppressing momentum-breaking scroll writes, then reconcile the reader anchor once scrolling settles. Keep live patches out of the timeline during active gestures and add Electron coverage for tall-row overlap, residual motion, and existing-message updates.
Cache semantic timeline anchors and disclosures per session instead of relying on KeepAlive. Restore state after virtualized layout stabilization while preserving explicit focus and tail-follow behavior.
Require the bootstrap agent to ask whether the skill should be installed locally or globally, and suppress only SQLite’s ExperimentalWarning in CLI test processes.
Present the bootstrap SKILL.md as the recommended setup path, keep manual installation secondary, and add regression coverage for the documentation order.
Add @obelisk-apps/cli with the existing build, search, query, and attune contract plus official skill installation.
Separate the docs-only skill artifact, bootstrap installer, release layout, cross-platform CI, and package-level regression coverage.
- Add "Run locally" section: clone → cd app → npm ci → npm run dev, with a
note on first-run indexing, source directories, and WSL auto-detection.
- Add "Debug the app" section: HMR, DevTools shortcuts, main-process logs,
Node inspector attachment, and isolated HOME for testing.
- Note that better-sqlite3 may need platform build tools if no prebuilt binary
is available.
- Fix "macOS only" → mention prebuilt for macOS, runnable from source on all
platforms.
- Fix typo: "A surface for human" → "for humans".
- Fix manual install path wording.
Co-Authored-By: Codex (GPT-5) <noreply@openai.com>
Eliminate full-table scans from subagent tool queries and guardian retractions by indexing message_uuid on tool_calls and tool_results. Add query-plan regressions for both access paths.
Defer and coalesce global catalogue refreshes while SessionDetail is active, carry session metadata through incremental patches, and gate in-flight catalogue commits by route. Harden the virtual timeline cold-open and user-scroll lifecycle, and add Electron frame, continuity, metadata, and overlap regressions.
Discard virtualizer measurement corrections suppressed during an active user gesture instead of replaying stale offsets at scrollend. Preserve post-scroll live anchoring and add unit plus Electron coverage for ordinary scrolling without updates.
Track real user scrolling through scrollend with a quiet watchdog, defer timeline commits and virtualizer corrections until settlement, and make SessionDetail the sole tail-follow owner. Add unit and Electron regressions for scroll writes, reader anchoring, near-tail escape, explicit navigation, and flap timing.
Virtualize SessionDetail rows behind stable presentation boundaries and apply typed incremental patches only after visible commits. Preserve reader state across live updates, handle coalesced and reordered patches, and verify the 120Hz append path with Electron tracing.
Render SessionDetail through measured dynamic-height virtual rows while preserving disclosure state, UUID navigation, reader anchoring, and tail-follow across live updates.
Add focused state/reconciliation tests plus a production Electron harness covering long-session DOM bounds, scrolling performance, offscreen navigation, and live viewport stability.
Cache timeline DOM indexes and memoize the timeline so progress and flap updates avoid rescanning or rerendering old messages. Preserve scroll anchors and disclosure state across targeted live message replacements.
Preserve unchanged message identities and isolate Vue row rendering during live refreshes.
Use a tail-only scroll path so updates do not interrupt active readers or expanded tools. Add regression coverage for 908-message appends, changed snapshots, and scroll policy.
Keep message navigation and progress state synchronized, and preserve
sequential split-flap count updates with a bounded queue.
Publish the Obelisk skill under skills/obelisk for npx skills, sharing
the same staging layout between CI and local releases with regression coverage.
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>