feat(miner-ui): add the chat rail's message composer - #6549
Conversation
The chat rail needs a message input and there wasn't one to reuse: the JSONbored#6244 audit found no submit-on-Enter composer anywhere, and the ui-kit's Textarea/Button are deliberately behavior-less primitives with no submit/auto-grow logic of their own. Add ChatComposer, built from those primitives, with the behavior living app-side. Enter submits the trimmed draft and clears; Shift+Enter inserts a newline and doesn't. Clicking Send goes through the same path, so the two can never disagree about what counts as empty -- whitespace-only is trimmed first and blocked exactly like "". A modifier chord (Ctrl/Cmd/Alt+Enter) doesn't submit either: those are submit chords in plenty of chat UIs, and treating one as a bare Enter would send a half-written message. The textarea auto-grows to fit typed or pasted content up to a cap, then stops and scrolls internally. Height is re-measured on every value change rather than on keystrokes, so a paste and the post-submit clear settle too, and the box is collapsed before measuring -- scrollHeight can't shrink back while the element still holds the taller height from the previous change, so deleting a line would otherwise never shrink it. useLayoutEffect, so the grown box paints in the same frame as the text instead of the caret outrunning it. Ships unwired and self-contained per the issue: it owns its own draft state, takes only onSubmit plus presentational props, and never calls an API, MCP tool, or action endpoint. No file under packages/loopover-ui-kit/** or apps/loopover-miner-ui/src/routes/** is touched. Tests are fixture-driven off a mock onSubmit and cover both sides of every branch: Enter vs Shift+Enter, non-empty vs whitespace-only through both the keyboard and the button, clear-after-submit, and auto-grow below vs at the cap. The auto-grow tests stub scrollHeight, since jsdom computes no layout and would otherwise report 0 for every box. Closes JSONbored#6514
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-16 13:19:46 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|

Summary
The chat rail needs a message input and there wasn't one to reuse — the #6244 audit found no submit-on-Enter composer anywhere, and the ui-kit's
Textarea/Buttonare deliberately behavior-less primitives with no submit/auto-grow logic of their own. This addsChatComposer, built from those primitives, with all the behavior living app-side.The three "will be closed" conditions — explicitly honored
The issue names three things that would disqualify a PR. Verified before pushing:
packages/loopover-ui-kit/**— that package stays a behavior-less primitives layer; the submit/auto-grow logic is entirely in the new app-level file.apps/loopover-miner-ui/src/routes/**(or__root.tsx) — this ships unwired, for a later issue to mount.fetch/API/MCP call — grepped; the component has zero network or data access. It owns its own draft state and takes onlyonSubmitplus presentational props.The diff is exactly two new files.
Behavior
Entersubmits the trimmed draft and clears;Shift+Enterinserts a newline and doesn't submit.submit()as the keyboard path, so the two can never disagree about what counts as empty."", on both paths — and a blocked submit doesn't silently eat the draft.Ctrl/Cmd/Alt+Enterare submit chords in plenty of chat UIs; treating one as a bareEnterwould fire off a half-written message, so only an unmodifiedEntersubmits.Two details worth recording:
autobefore measuring.scrollHeightcan't shrink back while the element still holds the taller inline height from the previous change, so deleting a line would otherwise never shrink the box.useLayoutEffectso the grown box paints in the same frame as the text rather than the caret outrunning it for a frame on a fast paste.Tests — 12/12, fixture-driven off a mock
onSubmitBoth sides of every branch the issue enumerates:
Entersubmits vsShift+EnternewlineEnterand the button pathPlus: shrink-back on delete, height settling after a submit clears the box, non-
Enterkeys, and the modifier chords.The jsdom gotcha the issue flagged is handled as directed: jsdom computes no layout, so a rendered textarea's
scrollHeightis always0. The auto-grow tests stub it (Object.defineProperty(textarea, "scrollHeight", …)) before firing the change event and assert the component reacted — the inline height andoverflowY— rather than asserting a real measured pixel height, which jsdom will never produce.Queries follow this app's accessible-query style (
getByRole("textbox"),getByRole("button", { name: /send/i })), per thegrafana-footer-link.test.tsxprecedent, not class names or test-ids.Validation
npm run ui:test— all three UI workspaces green (294 + 149 + 22), with no coverage-threshold failure. That's the gate this file is actually measured against (apps/loopover-miner-ui/vitest.config.ts's local floor), since Codecov ignoresapps/**. I did not addapps/**coverage wiring to the root config, per the issue.npm run ui:typecheck— clean (both workspaces).npm run ui:lint— 0 non-prettier violations; prettier reports my files unchanged (the repo-wideprettier/prettiererrors are Windows CRLF noise, absent on CI's LF checkout).git diff --checkclean; tree contains only the two intended files (no generatedrouteTree.gen.tschurn). Rebased on latestmain— no base conflict.Scope
apps/loopover-miner-ui/). Maintainer-authored issue → approved work.src/components/*.tsx+ named export, test colocated atsrc/root,@loopover/ui-kit/components/*import path — all per thegrafana-footer-link/portfolio.tsxprecedents the issue cites.site/,CNAME, orlovablechanges.Safety
Closes #6514