feat(miner-ui): useStreamingText hook + StreamingText renderer - #6583
Conversation
The first streaming-text primitive for the miner-ui chat rail (JSONbored#6516): progressively reveal a chat response's text as chunks arrive, instead of popping the whole message in at once. Unwired plumbing only — no backend, no route, exercised solely against mock chunk sources in tests. - lib/use-streaming-text.ts — useStreamingText(source): consumes a ChunkSource (a () => AsyncIterable<string> factory, exported by name), accumulating each chunk into text with an idle/streaming/done/error/ cancelled status and a cancel(). Mirrors usePolledFetch's cancelled-flag discipline: a chunk resolving after a new source starts, after cancel(), or after unmount never touches state; a mid-stream throw/reject surfaces via status/error, never as an unhandled rejection. - components/streaming-text.tsx — thin <StreamingText> renderer showing the accumulated text with a caret while streaming, suppressed under prefers-reduced-motion (detected via window.matchMedia + a change listener, the use-mobile.tsx technique — no motion dependency added). Tests (co-located flat in src/, mirroring use-polled-fetch.test.ts) cover every status transition — incremental accumulation, cancel-on-new-source, cancel-on-unmount (no late write), and the error path — plus the renderer's full-motion caret vs reduced-motion suppression. Local @loopover/ui-miner gate green: typecheck + 192 tests (coverage 88/86/81/90, above the 85/85/75/85 threshold) + eslint (0 errors). Closes JSONbored#6516
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-16 15:35:11 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
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 first streaming-text primitive for the miner-ui chat rail (#6516): progressively reveal a chat response's text as chunks arrive. Unwired plumbing only — no backend, no route, no live
fetch/EventSource; exercised solely against mock chunk sources in tests.Deliverables
lib/use-streaming-text.ts—useStreamingText(source)consumes aChunkSource(a() => AsyncIterable<string>factory, exported by name), accumulating each chunk intotextwith anidle | streaming | done | error | cancelledstatus and acancel(). All state writes run inside the effect's async worker (guarded by a per-runcancelledflag), so a chunk resolving after a new source starts, aftercancel(), or after unmount never touches state, and a mid-stream throw/reject surfaces viastatus/errorrather than as an unhandled rejection.components/streaming-text.tsx— thin<StreamingText>renderer showing the accumulated text with a caret while streaming, suppressed underprefers-reduced-motion(viawindow.matchMedia+ achangelistener — theuse-mobile.tsxtechnique; nomotiondependency).use-streaming-text.test.ts+streaming-text.test.tsx— co-located flat insrc/.Tests (every branch)
Incremental accumulation; cancel-on-new-source; cancel-on-unmount (no late write); mid-stream error →
status: error; renderer full-motion caret vs reduced-motion suppression.apps/**is Codecov-ignored, so the operative gate is the local vitest coverage threshold (85/85/75/85) — this suite lands at 89/88/83/91. Local@loopover/ui-minertypecheck + 221 tests + the fullnpm run ui:lint(0 errors, incl.react-hooks/set-state-in-effect) all green.Third submission of #6516 (after #6573/#6579): the earlier ones were auto-closed on the CI "UI lint" step's
react-hooks/set-state-in-effectrule (miner-ui pins react-hooks 7.1.1), which my local install had been resolving at 5.2.0 and so missed. State transitions are now written from the effect's async worker, not synchronously in the effect body — verified against 7.1.1 locally.Closes #6516