spike: cc-chat as a Scala.js custom element — core split, ADR 0002, measured bundles #60

Merged
mph merged 8 commits from spike/cc-chat-scalajs into main 2026-08-12 14:47:55 +00:00
Owner

Summary

Validates the agent-chat UI direction decided with ADR 0002: the durable asset is a versioned session-over-HTTP contract, and its reference client is a self-contained Scala.js custom element (<cc-chat>) sharing the SDK's model and fold logic through a cross-built core. This branch carries the enabling core split, the ADR, the spike that proved the approach, and the bundle-size CI gate.

What lands

  • core split for cross-building: platform-free sources (model, parsing, CLIError, conversation-log model/parser) move to core/shared/src, compiled by both the JVM core and the new core.js Scala.js module (claude-code-query-core_sjs1_3). JVM-bound code stays in core/src.
  • Timestamps on Moment (released iw-support-time:0.2.0, resolved from the forge registry): removes scala-java-time at the root — java.time is unreachable from cross-built sources, cutting ~158 KB gzip from browser bundles. ConversationLogEntry.timestamp: Option[Moment], parsed with Moment.parseIsoOption (timestamps stay best-effort on vendor JSONL).
  • ADR 0002: the contract-first design, Scala.js decision and its accepted costs, asset-first delivery from the jar, Laminar adoption (27 KB delta, under the agreed 40 KB rule), deferred decisions with triggers.
  • CI bundle-size gate: 250 KB gzip, just above the measured 217.9 KB — tight enough that a scala-java-time-class regression breaks the build instead of eating the 400 KB design ceiling silently.
  • Spike code (uispike/*, throwaway by declaration): bare/rawdom/laminar element variants and a toy host proving hashed classpath-resource delivery, history + SSE wiring with shared circe decoders, and correct fold semantics (verified in Chrome, incl. a real 1,128-line transcript → 122 rows).

Measured (fullLinkJS, gzip; ceiling 400 KB)

target size
bare element 7.9 KB
+ shared model, circe, parsers 190 KB
+ Laminar 217 KB

Not in scope

The real ui module (transcript viewer + Tapir endpoints, built test-first) is the next slice per ADR 0002 and supersedes uispike/*. The spike deliberately skipped TDD, as recorded in the ADR.

🤖 Generated with Claude Code

## Summary Validates the agent-chat UI direction decided with ADR 0002: the durable asset is a versioned session-over-HTTP contract, and its reference client is a self-contained Scala.js custom element (`<cc-chat>`) sharing the SDK's model and fold logic through a cross-built core. This branch carries the enabling core split, the ADR, the spike that proved the approach, and the bundle-size CI gate. ## What lands - **core split for cross-building**: platform-free sources (model, parsing, `CLIError`, conversation-log model/parser) move to `core/shared/src`, compiled by both the JVM `core` and the new `core.js` Scala.js module (`claude-code-query-core_sjs1_3`). JVM-bound code stays in `core/src`. - **Timestamps on `Moment`** (released `iw-support-time:0.2.0`, resolved from the forge registry): removes `scala-java-time` at the root — `java.time` is unreachable from cross-built sources, cutting ~158 KB gzip from browser bundles. `ConversationLogEntry.timestamp: Option[Moment]`, parsed with `Moment.parseIsoOption` (timestamps stay best-effort on vendor JSONL). - **ADR 0002**: the contract-first design, Scala.js decision and its accepted costs, asset-first delivery from the jar, Laminar adoption (27 KB delta, under the agreed 40 KB rule), deferred decisions with triggers. - **CI bundle-size gate**: 250 KB gzip, just above the measured 217.9 KB — tight enough that a scala-java-time-class regression breaks the build instead of eating the 400 KB design ceiling silently. - **Spike code (`uispike/*`, throwaway by declaration)**: bare/rawdom/laminar element variants and a toy host proving hashed classpath-resource delivery, history + SSE wiring with shared circe decoders, and correct fold semantics (verified in Chrome, incl. a real 1,128-line transcript → 122 rows). ## Measured (fullLinkJS, gzip; ceiling 400 KB) | target | size | |---|---| | bare element | 7.9 KB | | + shared model, circe, parsers | 190 KB | | + Laminar | 217 KB | ## Not in scope The real `ui` module (transcript viewer + Tapir endpoints, built test-first) is the next slice per ADR 0002 and supersedes `uispike/*`. The spike deliberately skipped TDD, as recorded in the ADR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The message model, parsers, CLIError, and the conversation log entry
model/parser have no JVM dependency. They move to core/shared/src,
compiled by both the JVM core module and a new core.js Scala.js module,
so browser consumers can share the wire model and codecs with the
server. JVM-bound code (os-lib, file IO) stays in core/src.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bare (element floor) 7.9 KB gzip; rawdom (+ shared model, circe, both
parsers) 347 KB; laminar 375 KB — Laminar delta 27 KB. scala-java-time,
dragged in by Instant in ConversationLogEntry, accounts for ~178 KB:
serving the browser normalized history instead of raw vendor JSONL
would nearly halve the bundle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Serves the linked element bundle from the jar-shaped resource layout
under a content-hashed immutable path, a history page (real transcript
via CC_CHAT_TRANSCRIPT or a bundled sample), and canned SSE frames
exercising the fold: tool status, assistant reply, non-duplicating
result, and a fallback-only result. Verified in Chrome: correct
rendering on canned and real data, clean console, element survives DOM
moves without re-wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the two design sessions and the spike results: the versioned
contract as the durable asset, the self-contained cc-chat element as
its reference client, Scala.js sharing the core model, Laminar (27 KB
delta), asset-first delivery from the jar, and the normalized-history
decision that removes scala-java-time (~178 KB) from the browser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ConversationLogEntry.timestamp is works.iterative.core.Moment (from the
dependency-free iw-support-time module) instead of java.time.Instant,
and the parser goes through Moment.parseIso. java.time is no longer
reachable from the cross-built sources at all, so the creep risk is
gone, not avoided: the JS classpath has no scala-java-time to pull in.
Spike bundles drop ~158 KB gzip: rawdom 347→190 KB, laminar 375→217 KB.

BREAKING: consumers reading ConversationLogEntry.timestamp get a Moment;
on the JVM, works.iterative.core.MomentInterop.toInstant converts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gate sits just above the measured 217 KB rather than at the 400 KB
design ceiling, so a scala-java-time-class regression (~158 KB) breaks
the build instead of silently consuming headroom. ADR records the root
fix, the re-measured numbers, the iw-support-time dependency and its
release sequencing, and moves the normalized-history question to open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iw-support-time's parseIso now returns Either[Moment.ParseError, Moment];
the log parser keeps Option semantics via parseIsoOption. Laminar bundle:
217,852 B gzip (gate 256,000).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build: depend on released iw-support-time 0.2.0 from the forge registry
Some checks failed
CI / Check Formatting (pull_request) Successful in 54s
CI / Compile (pull_request) Successful in 1m10s
CI / Check Linting (pull_request) Successful in 51s
CI / Run Tests (pull_request) Successful in 1m5s
CI / Check Bundle Size (pull_request) Successful in 52s
CI / Run Integration Tests (pull_request) Failing after 1m37s
a381de915d
Adds the iterative-works Forgejo Maven registry to every module that
reaches iw-support-time (directly or through core.js), so CI resolves
the release without a local ivy copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mph force-pushed spike/cc-chat-scalajs from a381de915d
Some checks failed
CI / Check Formatting (pull_request) Successful in 54s
CI / Compile (pull_request) Successful in 1m10s
CI / Check Linting (pull_request) Successful in 51s
CI / Run Tests (pull_request) Successful in 1m5s
CI / Check Bundle Size (pull_request) Successful in 52s
CI / Run Integration Tests (pull_request) Failing after 1m37s
to e86bc729b6
All checks were successful
CI / Check Formatting (pull_request) Successful in 50s
CI / Compile (pull_request) Successful in 1m42s
CI / Check Linting (pull_request) Successful in 1m19s
CI / Run Tests (pull_request) Successful in 1m10s
CI / Check Bundle Size (pull_request) Successful in 46s
CI / Run Integration Tests (pull_request) Successful in 1m39s
2026-08-12 14:32:33 +00:00
Compare
mph merged commit e1d69b38c9 into main 2026-08-12 14:47:55 +00:00
mph deleted branch spike/cc-chat-scalajs 2026-08-12 14:47:55 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/claude-code-query!60
No description provided.