Skip to content

Slice 013: LiveView chat UI with streaming - #24

Merged
HackTuah merged 19 commits into
mainfrom
slice/013-liveview-chat-streaming
Sep 20, 2026
Merged

HackTuah merged 19 commits into
mainfrom
slice/013-liveview-chat-streaming

Conversation

@HackTuah

Copy link
Copy Markdown
Member

Slice 013 per slices/013-liveview-chat-streaming/SLICE.md. Evidence in its PROOF.md; decisions, findings and follow-ups in its NOTES.md.

  • Renderer measured at G1: mdex ~> 0.13 (earmark is retired on hex with an open XSS advisory, phoenix_streamdown is still a beta). The first NIF in the bundle; the package workflow runs on this branch's mix.lock change.
  • Two LiveViews (/, /s/:id), TrinityWeb.ChatComponents, TrinityWeb.Markdown (the one raw/1), design tokens (dark by default), a Content-Security-Policy with a per-request nonce (the slice-000 debt).
  • Two changes in 012's modules, each needed by an AC: the Session re-reads its row per turn (red committed first), state/1 carries the draft text.
  • Gate exit 0, 176 tests, coverage 64.41% (from 60.82%). AC7: 6 renders for 1,000 deltas.
  • Manual queue for the owner (AC1, AC3, AC4) in PROOF.md, with screenshots already under proof/.

ROADMAP row 013 to in_progress. NOTES.md carries the renderer measurement
SLICE.md asks for, each row with the command that produced it, from
phoenix_streamdown 1.0.0-beta.4, earmark 1.4.49, mdex 0.13.5 and
mdex_native 0.2.8 read in a scratch directory: the beta is still the
latest release and unpinnable, earmark is retired on hex with an open
XSS advisory so the gate refuses it, and mdex completes fragments on its
own. The outcome is mdex, open to the owner's veto at G1, as is the font
decision (a stack, no bundled file). Then the fifteen-line build plan,
the three-item manual queue, and four deviations stated before any code.
plan_check: PASS.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
The chat's markdown renderer, chosen by the measurement in the slice's
NOTES.md: phoenix_streamdown is still a beta, earmark is retired on hex
with an open XSS advisory, and mdex completes streaming fragments on its
own. It brings mdex_native 0.2.8, a Rust NIF precompiled through
rustler_precompiled 0.9.0: the first NIF in the bundle, which the
package workflow proves on the mix.lock change. VERSIONS.md regenerated:
the phoenix_streamdown row reads not used and says why; the mdex row is
new. versions.verify: OK, 83 locked packages; hex.audit and deps.audit
clean.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Slice 013 line 4, committed failing by name: "set_model/2 (AC6's core)
the next turn of a running session uses the model set between turns".
Sessions.set_model/2 writes sessions.model and refuses an id the
registry does not know; the Session still reads the row it loaded at
init, so the fake sees model nil on the second turn:

  left:  nil
  right: "fake:embed"

Four of the five tests in the file pass: default_persona/0 creates the
row named default once; an unknown id and a missing session are
refused; state/1 now carries the in-progress text and is empty idle.
The fake provider moves from test/support into lib/trinity/llm/providers
so development can run it (TRINITY_FAKE_PROVIDER=1 in config/runtime.exs
puts it in the dev registry with a markdown demo script), and records
the last request it saw, which this test reads.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
The fix for the red committed just before it ("the next turn of a
running session uses the model set between turns"). start_model_call
re-reads the session row, so a model written between turns by
Sessions.set_model/2 is the next turn's model rather than the next
incarnation's. Slice 013 needs it for AC6; the change is one read in
012's module and every 012 test still passes (22 in test/trinity/sessions).

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…nderer, the policy

Lines 2 to 12 of the G1 plan. TrinityWeb.Markdown is the one path from
model output to the DOM (mdex, raw HTML omitted, the sanitizer on top,
fragments completed while streaming; five tests, and the tree's only
raw/1 call carries a scoped sobelow skip). The design tokens: two daisyUI
themes, dark by default, and a @theme block for fonts, text sizes and
radii, with a UI section in docs/03 naming them and the component
vocabulary later surfaces take. TrinityWeb.ChatComponents: message,
tool_card, draft, composer, model_picker, status_pill, banner,
local_time. SessionLive.Index (the list, New session on the default
persona) and SessionLive.Show (subscribe, ensure_started, history into a
stream, the turn in flight from state/1 with the already-queued deltas
drained, one assign for the in-progress text, catch-up of tool rows past
the last seen seq). Four hooks in assets/js/hooks.js: Composer,
Shortcuts (Ctrl/Cmd+K, Esc), ScrollToBottom, LocalTime.
TrinityWeb.Plugs.ContentSecurityPolicy on the browser pipeline with a
nonce per request; the root layout's theme script and the LiveDashboard
carry it; the slice-000 skip rekeyed with the new reason. The scaffold
home page goes; / is the index. Sessions.set_title/2 for the first line.
The Trinity boundary exports Sessions.Message and Sessions.SessionRow.

Tests, 21 in test/trinity_web: AC2 (send, deltas, the final message once),
AC3's test half (cancel, banner, Retry), AC4's test half (Process.exit
:kill under an open page), AC5 (a second page mounted mid-stream: four
rows on both pages, none twice), AC6 (the picker writes sessions.model
and the fake sees it on the next turn), AC7 (six renders for 1,000
deltas, counted on [:phoenix, :live_view, :render, :stop]), the CSP
header and nonce, the index and the two hook events. Screenshots under
proof/ from the dev server on the fake provider driven by a headless
chromium: streaming, final, cancelled, killed, the next message after a
kill, and the light theme. mix gate exit 0, 176 tests; credo --strict
--all clean; sobelow clean; docs/01, docs/03 and docs/06 (R6 closed, R24
opened for the NIF) updated; REUSE.toml covers proof/.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
The formatter had broken the pre-wrap div across three lines, and the
newline and indent rendered as a blank line above every user message
(seen in the first screenshots). phx-no-format on that element.
Screenshots refreshed, and AC1's real-provider run added: openrouter:ling
through the same page, 43/276 tokens, streaming and final.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
A new incarnation broadcasts {:state, :idle} on entering idle and then
{:turn_interrupted, m} from its rehydrate; waiting for the second and
then the first waited five seconds for a message already passed (NOTES
finding 5).

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…cer reads it

test/sobelow_skips_test.exs wants a "# sobelow_skip reason:" line in the
comment block above every inline @sobelow_skip; the block above
TrinityWeb.Markdown.to_html/2 said the same thing without the marker.
Found by the gate once markdown.ex was tracked (the test reads git
ls-files, so the first gate run, on an untracked file, could not see it).

Signed-off-by: Ayla Croft <aylacroft@proton.me>
PROOF.md with every criterion's evidence: AC2, AC5, AC6, AC7 and AC8
from the tests and the gate (6 renders for 1,000 deltas), AC1, AC3 and
AC4 from the test halves plus screenshots under proof/ taken from the
dev server on the fake provider and on openrouter:ling, with the owner's
three-step manual queue. NOTES.md carries the eleven findings, the
deviations and the follow-ups. coverage.tsv: 013 at 64.41% on 080c543,
from 60.82% at 012. ROADMAP row 013 to done. Gate exit 0 on the code
tree, 176 tests.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
… not 200

Run 35518054546 (the package workflow dispatched for slice 013) printed
"HTTP 500 on port 36821" on linux and nothing else; the exception was in
serve.log, which the step only printed when there was no response at all.
The locally built binary answers 200 from its first request, so the log
is the only way to see what differs on the runner.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Red: package run 35519205973 (all three operating systems) and run
35518054546 before it: GET / answered 500 with "no such table: sessions"
from the packaged binary, and the same binary here on a fresh install
(rm -rf ~/.local/share/.burrito, a fresh DATABASE_PATH): 500, no
migration line in the log, a 4 KB database. skip_migrations?/0 keyed on
RELEASE_NAME, which the release's bin/desktop exports and the Burrito
wrapper does not (it starts erlexec directly), so the packaged app never
migrated; nothing at / read a table before slice 013, which is why it
took until now to show. The check is now "running under Mix", where
mix ecto.migrate and the test alias own migrations. Measured after the
change on the same fresh-install path: four migrations logged, GET /
200 three times in a row.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…er decision

Findings 12 to 14 in NOTES.md: the migration fix, the mdex NIF refusing
to load in Burrito's musl ERTS (both precompiled artifacts), and the
Zig-linked source build that does load and render (screenshot added).
The decision between keeping mdex with a source-built NIF and a
pure-Elixir renderer is the owner's; the recommendation is written.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…he smoke path says whether it rendered

Owner decision 2026-09-20 (NOTES.md, the open decision after finding 14):
keep mdex and build its NIF from source where the precompiled artifact
does not load. rustler ~> 0.38 joins the tree at build time only
(VERSIONS row; versions.verify 84 locked, 48 pins). Three compile-time
settings, named together in config/config.exs and set by the package
workflow's linux job only: MDEX_NATIVE_BUILD=1, TRINITY_NIF_TARGET
(given to Rustler as the cargo target) and the linker
scripts/zig-cc-musl (zig cc -target x86_64-linux-musl; Rust's own
self-contained musl link refuses a cdylib without crt-static).
rust-toolchain.toml names the musl target so rustup installs it.

Trinity.Smoke prints a second line, TRINITY_SMOKE_MARKDOWN=ok, after
rendering one line through the NIF, and exits 3 otherwise; the package
workflow's smoke step reads it on all three targets, so a bundle whose
NIF does not load can no longer pass on a boot. The line is computed
inside Application.start/2 rather than in the Task: Kernel.CLI treats
the plain argument --smoke as a file once the application is up ("No
file named --smoke", exit 1), a race the Task only wins by halting at
once, lost on the first packaged run with the render inside it.
TrinityWeb.Markdown logs at :error, once, when it falls back to escaped
text.

Measured here on a fresh install of the binary built exactly as the
workflow builds it: the NIF needs libc.so only (readelf -d); --smoke
prints ok and exits 0 three runs in a row; with the gnu artifact swapped
into the payload it prints failed:%MDEx.DecodeError{} and exits 3; the
packaged chat renders the real provider's answer as markdown with no
on_load warning. mix gate exit 0, 177 tests; credo --strict --all clean.
docs/packaging.md gains the native-code section; R24 lifted in docs/06.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…d binary

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Three package runs (slice/011 and slice/012 tags, and 35521749862 on
the 013 branch) failed this step on macOS and Windows with an empty log:
the port loop timed out and `test -n` exited without a word. The binary's
own log goes to stderr before the step fails, the same as the non-200
path already does.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…d Windows

Red: package runs 35512430836 (slice/011), 35515556177 (slice/012),
35521749862 and 35522642934 (this branch), the last one printing the
reason at last: on macOS and Windows the serve step's launch refused to
start with "is held by OS pid ... in desktop mode" after the smoke run's
clean exit. Slice 010 read liveness through /proc on Linux and treated
any file as held elsewhere ("the safe direction"), and the smoke path
halts without running terminate/2, so the file it left made every
second launch of the packaged app refuse on both platforms: a user
whose app crashed once would be locked out until they found the file.
Liveness now comes from kill -0 on the other Unixes and tasklist on
Windows; a check that cannot run still says held. Linux unchanged (the
/proc test in lock_test.exs); the package run is the measurement for
the other two. The four lock.ex sobelow skips rekeyed (the moduledoc
grew three lines), reasons carried over. Gate exit 0, 177 tests.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
… after the closing commit

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit 3db7a5f into main Sep 20, 2026
4 checks passed
@HackTuah
HackTuah deleted the slice/013-liveview-chat-streaming branch September 20, 2026 17:03
@HackTuah HackTuah mentioned this pull request Sep 20, 2026
HackTuah added a commit that referenced this pull request Sep 20, 2026
…mit as its one exemption

The merge of pull request #24 was made with a subject and no body, so
GitHub's merge commit 3db7a5f carries no Signed-off-by, and it sits
under the protected tag slice/013 where nothing can rewrite it. Rule 8
reads the whole history and failed every gate from that commit on. The
sha is listed in scripts/plan_check.sh by full value with the reason,
docs/03 says the merge commit's body carries the sign-off from now on,
and NOTES.md finding 17 records the mistake. plan_check: PASS.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant