Skip to content

Print chat cells as a flat event log (textql_sdk.cell_render) - #27

Merged
rodnnnney merged 5 commits into
mainfrom
rod/watch-chat
Aug 5, 2026
Merged

Print chat cells as a flat event log (textql_sdk.cell_render)#27
rodnnnney merged 5 commits into
mainfrom
rod/watch-chat

Conversation

@rodnnnney

Copy link
Copy Markdown
Collaborator

What

Adds textql_sdk.cell_render, a handwritten module that prints a chat Cell the way the v2 SSE stream (POST /v2/chats/stream) presents it: a flat event log where an execution step appears twice — once when it starts running, carrying the query or code the model generated, and once when it finishes, carrying the result.

cell  sql 4f2a91c8-…  running  connector_id=3
      SELECT customer, sum(amount) AS revenue
      FROM orders
cell  sql 4f2a91c8-…  done  842ms
      dataframe: 12 rows × 2 cols
      preview:
      | customer | revenue |
text  Acme led at $12,000.
done  completed

watch_chat re-sends a full snapshot on every update, so CellPrinter collapses those to those two events, keyed on (cell.id, done).

A Cell is a oneof over ~50 payload types. CELL_INPUTS names the input fields per type; everything else the server set is printed directly off the protobuf descriptors, so an unfamiliar cell type still shows its contents instead of a placeholder. Nothing is truncated. The registries are plain module-level dicts, so callers can reassign them.

Also

  • examples/watch_chat.py now uses CellPrinter and is back to streaming concerns only (create, watch, resume, reconnect).
  • Custom TLS support in streaming.py (http_client=), so a private CA can be handed to the Connect stack as well as httpx.
  • STREAMING.md gains a "Printing cells" section.

Testing

Exercised against synthetic cell snapshots: SQL (running / done / failed / connector auth hold), Python with images, charts and dataframes, web search, MCP, prose (md/ans/summary — note SummaryCell keeps its text in summary, not content), thinking cells (hidden when the payload is empty, matching isHiddenThinkingCell), empty oneof, and a 60-line Python cell to confirm nothing is elided. pylint 10.00/10 against pylintrc; pyright clean on the new module.

🤖 Generated with Claude Code

The example rendered every cell snapshot identically, so a cell mid-execution
was indistinguishable from a finished one, per-cell failures were invisible, and
the incremental-print logic assumed cell text only ever grows.

- Branch on `cell.complete` rather than comparing `lifecycle` to
  LIFECYCLE_EXECUTED. `Complete` is set from a polymorphic `IsComplete()`:
  non-executable cells (markdown, text) are complete at LIFECYCLE_CREATED,
  executable ones at EXECUTED or HALTED. Comparing lifecycle directly marks
  every markdown cell as never finishing.
- Surface LIFECYCLE_EXECUTING and LIFECYCLE_HANDOFF_PENDING so long-running
  cells read as in-flight.
- Report `exec_error` per cell. A run reaches run_complete even when individual
  cells failed, so run_error alone misses them; run_complete now summarises
  failed and non-terminal cells.
- Treat each event as a full snapshot: print a delta only when text grew
  monotonically, otherwise reprint. Slicing a delta out of replaced content
  (a SQL cell swapping query for query + results) printed garbage.

Document the same rules in STREAMING.md.
The streaming bridge builds its own Connect transport (pyqwest) and never sees
the httpx client on the Textql SDK, so there was no way to give streaming a
private CA — `verify=` on the SDK only covered unary REST calls.

- Thread `http_client` through create_streaming_client, create_connect_client
  and their sync variants, so callers can supply a pyqwest client. Defaults to
  None, which keeps the shared default transport and today's behaviour.
- watch_chat example reads TEXTQL_CA_BUNDLE and applies it to both stacks.
- Document the sharp edge: an HTTPTransport you construct yourself starts with
  an empty trust store, so a CA bundle without tls_include_system_certs=True
  fails every TLS handshake rather than just the private-CA ones. Verified
  empirically — bare HTTPTransport() cannot reach app.textql.com, while the
  shared default transport can.

Also switch the example to MODEL_SONNET_4_6.

Verified: pyright clean on streaming.py (the CI gate) and pylint 10.00/10 on
both files; example imports and the render path re-checked against real
protobuf messages. The ~18.5k pyright errors under src/_connect are
pre-existing generated-code noise and untouched here.
fe/src/lib/clients/WatchChatClient.ts is the reference consumer of this stream.
Compared against it, the example was missing resume and reconnect entirely: a
dropped connection ended the run, and a retry would have replayed the chat from
the beginning.

- Track `event.cursor` on every event and `cell.id` on cells reporting
  complete, then replay both as resume_cursor / latest_complete_cell_id. Same
  checkpointing the frontend does (WatchChatClient.ts:248-260).
- Reconnect with exponential backoff, 7 attempts from 500ms, matching
  MAX_RECONNECT_ATTEMPTS / BASE_RECONNECT_DELAY_MS. Treat a dropped transport
  as routine; run_error stays terminal and does not retry.
- Handle the handoff_pending event instead of silently dropping it.

Already at parity and unchanged: snapshot-replace by cell id (the store's
updateMessageInPlace does the same), branching on `complete` rather than
lifecycle (WatchChatClient uses it for the resume checkpoint, and
hasMidExecutionCell tests `generated && !complete`), and per-cell exec_error
rather than only run_error.

Verified with a fake stream that drops mid-run: the retry carries
latest_complete_cell_id=cell-A and resume_cursor=c2, then completes.
pyright clean on streaming.py, pylint 10.00/10 on both files.
@rodnnnney
rodnnnney merged commit a0ef661 into main Aug 5, 2026
8 checks passed
@rodnnnney
rodnnnney deleted the rod/watch-chat branch August 5, 2026 14:32
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.

1 participant