Skip to content

feat(miot-chat): real answer streaming, inline loading, no truncation, markdown#847

Merged
korutx merged 8 commits into
trunkfrom
worktree-feat+miot-chat-tui-streaming
Jul 2, 2026
Merged

feat(miot-chat): real answer streaming, inline loading, no truncation, markdown#847
korutx merged 8 commits into
trunkfrom
worktree-feat+miot-chat-tui-streaming

Conversation

@odtorres

@odtorres odtorres commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What & why

Addresses three TUI complaints from the dev team about @microboxlabs/miot-chat:

"Streaming no está funcionando del todo bien, los loading fuera de la conversa (arriba de todo); respuestas muy largas truncadas, no las pinta completa, ponen ... cuando son mucho texto."

Each traced to a concrete root cause (diagnosis below), fixed across the harness, the TS client, and the TUI.

Root causes & fixes

Symptom Root cause Fix
Streaming doesn't really work Every answer.completed event carried only {length}, never the answer text — so the TUI showed nothing until a separate, sometimes-hanging GET /runs/{id} after the stream ended. Harness now emits answer.delta events (the answer text the model already streams), mirroring thinking.delta. The TUI accumulates them so the bubble grows token-by-token; the GET is now reconciliation-only.
Loading indicator detached at top TopLine rendered a spinner pinned to the top of the screen. Removed it — loading stays inline at the active turn / AssistantTurn spinner.
Long responses truncated with Transcript had removed Ink's <Static> and re-painted the whole transcript in the live region every render; content taller than the terminal gets truncated by Ink. Reintroduced <Static> for finished turns so they flush to native scrollback and escape viewport-height truncation.
Markdown not rendered (bonus) The Markdown component existed but was orphaned; AssistantTurn printed raw text. Completed assistant turns now render through Markdown (plain text while streaming, formatted on finish).

How <Static> is done safely

committed (terminal turns) and live (in-flight tail) are derived by a pure per-render filter on isTerminalItem, so they're always disjoint — no double-paint / ghost line. The filter returns a fresh array each render, which is required for Ink's <Static> to emit newly-committed items (it only re-renders on a new items reference — a same-reference mutation is silently dropped; this was caught and fixed during development via a frame probe).

Stack touched

  • miot-harness (Python): answer.delta literal + emission in stream_llm_with_thinking
  • @microboxlabs/miot-harness-client (TS): literal + AnswerDeltaData
  • @microboxlabs/miot-chat (TS): projector, Transcript, TopLine, AssistantTurn

The event literal stays in lockstep across events.py, event_types.json, and the TS HARNESS_EVENT_TYPES (parity-tested both sides).

Tests

All green, TDD throughout (failing test → fix → pass per task):

  • Harness: 14 passed (new test_llm_streaming.py + event parity + existing streaming suites)
  • miot-harness-client: 24 passed
  • miot-chat: 408 passed; check-types + lint clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Streaming assistant responses now update incrementally, producing “delta” events as text arrives.
    • Completed assistant messages now render as markdown (including proper list formatting).
  • Bug Fixes
    • Empty streaming deltas are ignored, preventing blank/stray transcript updates.
    • Top status line no longer shows a spinner while streaming; transcript rendering now preserves correct ordering during active streaming.
  • Tests
    • Added coverage for incremental assistant delta emission and transcript projector behavior.

odtorres and others added 6 commits June 30, 2026 14:42
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion

Render terminal turns via Ink <Static> so they flush to native
scrollback and escape the viewport-height '…' truncation on long
answers. committed/live are derived by a pure per-render filter so they
stay disjoint (no ghost line) and <Static> gets a fresh array each
render (required for it to emit newly-committed items).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e89a24c8-1c9d-4d6f-ab1c-c41cd46b0d15

📥 Commits

Reviewing files that changed from the base of the PR and between 9191ee4 and 563e501.

⛔ Files ignored due to path filters (1)
  • turbo-repo/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • turbo-repo/packages/miot-chat/package.json
  • turbo-repo/packages/miot-chat/src/tui/__tests__/components/Transcript.test.tsx
  • turbo-repo/packages/miot-chat/src/tui/transcript/Transcript.tsx
✅ Files skipped from review due to trivial changes (1)
  • turbo-repo/packages/miot-chat/package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • turbo-repo/packages/miot-chat/src/tui/tests/components/Transcript.test.tsx
  • turbo-repo/packages/miot-chat/src/tui/transcript/Transcript.tsx

📝 Walkthrough

Walkthrough

Adds a new answer.delta event across harness streaming, shared event types, transcript projection, and TUI rendering. Assistant output now streams into transcript state incrementally, then renders as plain text while streaming and markdown when complete.

Changes

Answer delta streaming feature

Layer / File(s) Summary
Register answer.delta event type
miot-harness/src/miot_harness/runtime/events.py, miot-harness/src/miot_harness/runtime/event_types.json, miot-harness/tests/test_events.py, turbo-repo/packages/miot-harness-client/src/types.ts
Adds answer.delta to the Python HarnessEventType, mirrored JSON event list, pinned type test, and shared TypeScript event/type payload definitions.
Emit answer.delta from LLM streaming
miot-harness/src/miot_harness/agents/llm_streaming.py, miot-harness/tests/test_llm_streaming.py
stream_llm_with_thinking emits answer.delta events for plain-string and structured text chunks with per-delta indices, and tests cover both stream shapes.
Project answer.delta into transcript state
turbo-repo/packages/miot-chat/src/tui/transcript/project.ts, turbo-repo/packages/miot-chat/src/tui/__tests__/transcript.project.test.ts
applyHarnessEvent handles answer.delta and accumulates it into a streaming assistant item or starts a new one; tests cover accumulation and empty deltas.
Split committed vs live transcript rendering
turbo-repo/packages/miot-chat/src/tui/transcript/Transcript.tsx, turbo-repo/packages/miot-chat/src/tui/transcript/AssistantTurn.tsx, turbo-repo/packages/miot-chat/src/tui/chrome/TopLine.tsx, turbo-repo/packages/miot-chat/src/tui/__tests__/components/*, turbo-repo/packages/miot-chat/package.json
Transcript rendering splits committed and live items, assistant turns render markdown only when complete, TopLine no longer renders a streaming spinner, related tests were updated, and the chat package version was bumped.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Model as Streaming model
  participant Harness as stream_llm_with_thinking
  participant Projector as applyHarnessEvent
  participant Transcript as Transcript UI
  participant AssistantTurn as AssistantTurn

  Model->>Harness: stream text chunk
  Harness->>Harness: emit answer.delta(agent, delta, index)
  Harness->>Projector: answer.delta event
  Projector->>Projector: appendAnswerDelta updates assistant item
  Projector->>Transcript: updated transcript items
  Transcript->>AssistantTurn: render live item
  AssistantTurn-->>Transcript: plain text while streaming
  Transcript->>AssistantTurn: render completed item
  AssistantTurn-->>Transcript: markdown when complete
Loading

Possibly related PRs

  • microboxlabs/modulariot#504: Adds the earlier transcript projector logic in turbo-repo/packages/miot-chat/src/tui/transcript/project.ts that this PR extends with answer.delta.
  • microboxlabs/modulariot#516: Updates the same stream_llm_with_thinking path in miot-harness/src/miot_harness/agents/llm_streaming.py that now emits answer.delta.

Suggested reviewers: korutx

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main changes: streamed answers, inline loading, non-truncated transcript rendering, and Markdown for completed messages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-feat+miot-chat-tui-streaming

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
miot-harness/src/miot_harness/agents/llm_streaming.py (1)

49-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated answer.delta emission logic.

The plain-string branch (lines 62-74) and the structured text-block branch (lines 104-116) build an almost identical HarnessEvent(type="answer.delta", ...) payload. Extracting a small helper would reduce duplication and the risk of the two paths drifting (e.g., one path forgetting to increment answer_index or include a field).

♻️ Proposed refactor
+def _emit_answer_delta(
+    progress: Progress,
+    run_id: str,
+    agent_name: str,
+    delta: str,
+    index: int,
+) -> None:
+    progress(
+        HarnessEvent(
+            run_id=run_id,
+            type="answer.delta",
+            message="",
+            data={"agent": agent_name, "delta": delta, "index": index},
+        )
+    )
+
+
 async def stream_llm_with_thinking(
     ...
             if isinstance(content, str):
                 if content:
                     text_parts.append(content)
-                    progress(
-                        HarnessEvent(
-                            run_id=run_id,
-                            type="answer.delta",
-                            message="",
-                            data={
-                                "agent": agent_name,
-                                "delta": content,
-                                "index": answer_index,
-                            },
-                        )
-                    )
+                    _emit_answer_delta(progress, run_id, agent_name, content, answer_index)
                     answer_index += 1
                 continue
             ...
                 elif btype == "text":
                     delta = block.get("text") or ""
                     if delta:
                         text_parts.append(delta)
-                        progress(
-                            HarnessEvent(
-                                run_id=run_id,
-                                type="answer.delta",
-                                message="",
-                                data={
-                                    "agent": agent_name,
-                                    "delta": delta,
-                                    "index": answer_index,
-                                },
-                            )
-                        )
+                        _emit_answer_delta(progress, run_id, agent_name, delta, answer_index)
                         answer_index += 1

Also applies to: 62-74, 104-116

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@miot-harness/src/miot_harness/agents/llm_streaming.py` at line 49, The
`answer.delta` payload construction is duplicated in `llm_streaming.py` across
the plain-string branch and the structured text-block branch inside the
streaming logic that uses `answer_index`. Extract the shared
`HarnessEvent(type="answer.delta", ...)` creation into a small helper and have
both branches call it so the fields stay consistent and `answer_index` is
incremented in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@turbo-repo/packages/miot-chat/src/tui/transcript/Transcript.tsx`:
- Around line 61-67: The Transcript component is splitting items into two
independent buckets, which can reorder entries instead of preserving the
original sequence; update Transcript so committed remains the leading prefix and
live remains the trailing tail in the same relative order they appear in
props.items. In Transcript and its render logic, avoid rendering all committed
rows before all live rows; instead, derive the split point from the first live
item and keep the committed prefix intact while appending the live tail
unchanged.

---

Nitpick comments:
In `@miot-harness/src/miot_harness/agents/llm_streaming.py`:
- Line 49: The `answer.delta` payload construction is duplicated in
`llm_streaming.py` across the plain-string branch and the structured text-block
branch inside the streaming logic that uses `answer_index`. Extract the shared
`HarnessEvent(type="answer.delta", ...)` creation into a small helper and have
both branches call it so the fields stay consistent and `answer_index` is
incremented in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48d8988e-9266-4a8d-9133-9f73ee07d453

📥 Commits

Reviewing files that changed from the base of the PR and between 1792d1a and 9191ee4.

📒 Files selected for processing (14)
  • miot-harness/src/miot_harness/agents/llm_streaming.py
  • miot-harness/src/miot_harness/runtime/event_types.json
  • miot-harness/src/miot_harness/runtime/events.py
  • miot-harness/tests/test_events.py
  • miot-harness/tests/test_llm_streaming.py
  • turbo-repo/packages/miot-chat/src/tui/__tests__/components/AssistantTurn.test.tsx
  • turbo-repo/packages/miot-chat/src/tui/__tests__/components/TopLine.test.tsx
  • turbo-repo/packages/miot-chat/src/tui/__tests__/components/Transcript.test.tsx
  • turbo-repo/packages/miot-chat/src/tui/__tests__/transcript.project.test.ts
  • turbo-repo/packages/miot-chat/src/tui/chrome/TopLine.tsx
  • turbo-repo/packages/miot-chat/src/tui/transcript/AssistantTurn.tsx
  • turbo-repo/packages/miot-chat/src/tui/transcript/Transcript.tsx
  • turbo-repo/packages/miot-chat/src/tui/transcript/project.ts
  • turbo-repo/packages/miot-harness-client/src/types.ts

Comment thread turbo-repo/packages/miot-chat/src/tui/transcript/Transcript.tsx
Split at the first in-flight item instead of two independent filters, so
a completed item (e.g. a finished tool) no longer floats above an earlier
still-active chain row. committed stays the leading prefix, live the
trailing tail, in original order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@odtorres odtorres self-assigned this Jun 30, 2026
Streaming (answer.delta), markdown rendering, inline loading, and
Static-based truncation fix. Backward-compatible; patch release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

App preview image

The latest app preview image for this PR is ready.

  • Immutable image: ghcr.io/microboxlabs/miot-app@sha256:48dc42b533cb96bb92362e11d7812cafbca29c59af8e01f5ee62bcd7757e160c
  • Moving tag: ghcr.io/microboxlabs/miot-app:pr-847
  • SHA tag: ghcr.io/microboxlabs/miot-app:pr-847-sha-563e501

@korutx korutx merged commit e45f321 into trunk Jul 2, 2026
20 checks passed
@korutx korutx deleted the worktree-feat+miot-chat-tui-streaming branch July 2, 2026 20:52
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.

feat(harness): live steering channel — inject guidance mid-run & gracefully interrupt

2 participants