Skip to content

feat(miner-ui): add the chat rail's message composer - #6549

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
luciferlive112116:feat/miner-ui-chat-composer
Jul 16, 2026
Merged

feat(miner-ui): add the chat rail's message composer#6549
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
luciferlive112116:feat/miner-ui-chat-composer

Conversation

@luciferlive112116

Copy link
Copy Markdown
Contributor

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/Button are deliberately behavior-less primitives with no submit/auto-grow logic of their own. This adds ChatComposer, 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:

  • No changes under 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.
  • No changes under apps/loopover-miner-ui/src/routes/** (or __root.tsx) — this ships unwired, for a later issue to mount.
  • No fetch/API/MCP call — grepped; the component has zero network or data access. It owns its own draft state and takes only onSubmit plus presentational props.

The diff is exactly two new files.

Behavior

  • Enter submits the trimmed draft and clears; Shift+Enter inserts a newline and doesn't submit.
  • Clicking Send routes through the same submit() as the keyboard path, so the two can never disagree about what counts as empty.
  • Whitespace-only is trimmed first and blocked exactly like "", on both paths — and a blocked submit doesn't silently eat the draft.
  • Modifier chords don't submit. Ctrl/Cmd/Alt+Enter are submit chords in plenty of chat UIs; treating one as a bare Enter would fire off a half-written message, so only an unmodified Enter submits.
  • Auto-grow to a cap (160px ≈ six lines), then it stops and scrolls internally.

Two details worth recording:

  1. Height is re-measured on every value change, not on keystrokes — a paste, and the reset after submit, have to settle the height too.
  2. The box is collapsed to auto before measuring. scrollHeight can'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. useLayoutEffect so 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 onSubmit

Both sides of every branch the issue enumerates:

Branch Covered
Enter submits vs Shift+Enter newline ✅ both
Non-empty vs whitespace-only blocked ✅ both, via both the Enter and the button path
Textarea clears after submit
Auto-grow below the cap vs stops/scrolls at the cap ✅ both

Plus: shrink-back on delete, height settling after a submit clears the box, non-Enter keys, and the modifier chords.

The jsdom gotcha the issue flagged is handled as directed: jsdom computes no layout, so a rendered textarea's scrollHeight is always 0. The auto-grow tests stub it (Object.defineProperty(textarea, "scrollHeight", …)) before firing the change event and assert the component reacted — the inline height and overflowY — 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 the grafana-footer-link.test.tsx precedent, not class names or test-ids.

Validation

  • New suite — 12/12 pass.
  • 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 ignores apps/**. I did not add apps/** coverage wiring to the root config, per the issue.
  • npm run ui:typecheck — clean (both workspaces).
  • npm run ui:lint0 non-prettier violations; prettier reports my files unchanged (the repo-wide prettier/prettier errors are Windows CRLF noise, absent on CI's LF checkout).
  • git diff --check clean; tree contains only the two intended files (no generated routeTree.gen.ts churn). Rebased on latest main — no base conflict.

Scope

  • Two new files, one coherent component, in a wanted path (apps/loopover-miner-ui/). Maintainer-authored issue → approved work.
  • Follows the app's conventions: flat src/components/*.tsx + named export, test colocated at src/ root, @loopover/ui-kit/components/* import path — all per the grafana-footer-link / portfolio.tsx precedents the issue cites.
  • No secrets; no changelog, site/, CNAME, or lovable changes.

Safety

  • Additive and inert: the component is not mounted anywhere yet, so it changes no existing route's behaviour. It has no action surface — no API, no MCP tool, no governor/portfolio-queue endpoint — it just hands a trimmed string to its caller.

Closes #6514

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-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@loopover-orb loopover-orb Bot added the gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. label Jul 16, 2026
@loopover-orb

loopover-orb Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-16 13:19:46 UTC

2 files · 1 AI reviewer · no blockers · readiness 82/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
Adds a self-contained ChatComposer built from ui-kit primitives with submit-on-Enter, Shift+Enter newline, modifier-chord blocking, trim-before-empty-check, and a capped auto-grow textarea, plus a thorough test suite covering each behavior. The logic is correct and traceable: the collapse-then-measure pattern correctly avoids the scrollHeight-can't-shrink trap, and both submit paths share one `submit()` function so they can't diverge. It ships unwired (no route/app-kit changes), matching the stated scope, and is backed by a real linked issue (#6514) building on the #6244 audit's finding that no such composer existed.

Nits — 5 non-blocking
  • chat-composer.tsx:59 — the `Textarea` has no associated `<label>`/`aria-label`; only a `placeholder` (which isn't an accessible name for screen readers) — consider adding `aria-label="Message"` or similar.
  • The auto-grow effect (chat-composer.tsx:36-48) directly mutates `textarea.style` outside React's render cycle; fine for this narrow, well-tested case, but worth a one-line note if a future maintainer is tempted to move this logic into the shared ui-kit Textarea.
  • The `className="max-h-[160px] resize-none"` on the Textarea duplicates the `MAX_COMPOSER_HEIGHT_PX` constant as a hardcoded literal — if the constant ever changes, this class won't update with it.
  • Consider deriving the Tailwind `max-h-[160px]` class from `MAX_COMPOSER_HEIGHT_PX` via inline style instead of a separate literal, to avoid the two ever drifting apart.
  • Add an `aria-label` or wrap the Textarea in a labeled control for screen-reader users, per the flagged accessibility note.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #6514
Related work ⚠️ 3 scoped overlaps Top overlaps are listed below; lower-confidence bulk is hidden.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 156 registered-repo PR(s), 94 merged, 31 issue(s).
Contributor context ✅ Confirmed Gittensor contributor luciferlive112116; Gittensor profile; 156 PR(s), 31 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The PR adds exactly the two required new files, builds the composer from the ui-kit's raw Textarea/Button primitives with all submit-on-Enter, Shift+Enter-newline, empty/whitespace guard, and auto-grow-with-cap logic implemented app-side, and does not touch packages/loopover-ui-kit or any routes file, matching the issue's scope and explicit exclusions.

Review context
  • Author: luciferlive112116
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 156 PR(s), 31 issue(s).
  • Related work: Titles/paths share 7 meaningful terms. (issue #6515, issue #6518)
  • Related work: Titles/paths share 7 meaningful terms. (issue #6516, issue #6518)
  • Related work: Titles/paths share 7 meaningful terms. (issue #6513, issue #6518)
Contributor next steps
  • Start here: Review top overlaps.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask 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.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

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.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit d0c6d65 into JSONbored:main Jul 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chat UI: composer component (submit-on-Enter, Shift+Enter newline, auto-grow)

1 participant