Slice 013: LiveView chat UI with streaming - #24
Merged
Merged
Conversation
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>
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice 013 per
slices/013-liveview-chat-streaming/SLICE.md. Evidence in itsPROOF.md; decisions, findings and follow-ups in itsNOTES.md.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; thepackageworkflow runs on this branch'smix.lockchange./,/s/:id),TrinityWeb.ChatComponents,TrinityWeb.Markdown(the oneraw/1), design tokens (dark by default), a Content-Security-Policy with a per-request nonce (the slice-000 debt).state/1carries the draft text.proof/.