Tighten conversation rhythm, flatten the tool list, and smooth streaming text
Conversation rhythm: - Single `--paragraph-gap` knob drives paragraph spacing both inside a markdown block and between consecutive prose parts, out-specifying Tailwind Typography's prose margins. Code cards carry the same gap themselves so it holds at any Streamdown nesting depth. - Two-tier vertical rhythm: `--turn-block-gap` separates scaffolding (tools / thinking) from the reply; `--tool-row-gap` keeps a tool run tight. - Drop the prose indent so prose, tools, todos, and thinking share one left edge. `---` renders as quiet spacing, not a heavy rule. Flat tool list: - Tools always render as a standalone-row stack, never a "Tool actions · N steps" group. assistant-ui slices the tool range unstably (interleaved live vs. reconstructed-consecutive when settled), so grouping reshuffled the whole turn the instant it settled. Flat rows are pixel-identical either way. - Inline approvals can no longer be buried in a collapsed group body. - Remove the now-dead grouping helpers from tool-fallback-model. Empty thinking: - Suppress reasoning disclosures with no visible text (encrypted / spinner- coerced reasoning) instead of leaving an empty "Thinking" header. - Tail stall indicator returns "thinking" when a running turn goes quiet. Streaming cadence: - Smooth character-reveal decouples visible cadence from bursty arrival. - Flush queued text deltas before applying tool events so a tool row can't jump ahead of its preceding text. - Disable Nagle on the GUI WebSocket so per-token frames aren't coalesced. Polish: clarify/patch/vision_analyze tool meta, queue-panel + diff-lines spacing, sticky human bubble expands on focus (not hover).
This commit is contained in:
@@ -26,6 +26,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any
|
||||
|
||||
from tui_gateway import server
|
||||
@@ -137,6 +138,24 @@ def _ws_peer_label(ws: Any) -> str:
|
||||
return f"{host}:{port}" if port is not None else host
|
||||
|
||||
|
||||
def _disable_nagle(ws: Any) -> None:
|
||||
"""Disable Nagle so streamed JSON-RPC frames go out individually.
|
||||
|
||||
Without it the kernel coalesces the small per-token frames, so a burst after
|
||||
the model's think-pause lands on the client in one tick and no client-side
|
||||
smoothing can recover the cadence. GUI/WS only; chat platforms don't hit
|
||||
this path. Best-effort — skip silently if the socket isn't reachable.
|
||||
"""
|
||||
try:
|
||||
scope = getattr(ws, "scope", None) or {}
|
||||
transport = (scope.get("extensions") or {}).get("transport") or getattr(ws, "transport", None)
|
||||
sock = transport.get_extra_info("socket") if transport is not None else None
|
||||
if sock is not None:
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
except Exception as exc: # pragma: no cover - best-effort tuning
|
||||
_log.debug("ws TCP_NODELAY skip: %s", exc)
|
||||
|
||||
|
||||
async def handle_ws(ws: Any) -> None:
|
||||
"""Run one WebSocket session. Wire-compatible with ``tui_gateway.entry``."""
|
||||
peer = _ws_peer_label(ws)
|
||||
@@ -150,6 +169,9 @@ async def handle_ws(ws: Any) -> None:
|
||||
try:
|
||||
await ws.accept()
|
||||
disconnect_reason = "connected"
|
||||
# Push small streamed frames out immediately instead of letting Nagle
|
||||
# batch them — keeps the live token cadence intact for GUI clients.
|
||||
_disable_nagle(ws)
|
||||
_log.info("ws accepted peer=%s", peer)
|
||||
|
||||
transport = WSTransport(ws, asyncio.get_running_loop(), peer=peer)
|
||||
|
||||
Reference in New Issue
Block a user