Standalone prompt_toolkit TUI feasibility spike (Phase TUI-1).
Purpose
-------
Prove — before committing to a full TUI rewrite of OC Chat — that the
risky parts of a prompt_toolkit-based three-pane terminal application
actually work on Windows Terminal + PowerShell, the project's
declared host. The risky parts (Codex review):
1. Rich-ANSI rendering for Markdown headings, fenced code with syntax
highlighting, GitHub-style tables, and wide unicode (CJK / combining
marks) inside a prompt_toolkit pane.
2. Independent pane scrolling via PgUp/PgDn/Home/End on the
conversation pane only — header and input stay pinned.
3. SIGINT-during-stream behavior. A worker thread appends fake
streaming chunks; Ctrl+C must cancel the stream and leave the app
alive (no terminal corruption, no hang).
4. Resize tolerance. SIGWINCH on POSIX; prompt_toolkit's internal
terminal-size polling on Windows.
5. Alt-screen vs non-alt-screen exit semantics. We use the default
(non-alt-screen + full_screen=True style with explicit redraw) and
verify the terminal restores cleanly after Ctrl+Q.
The spike is **deliberately not wired into the OC Chat runtime**. It
is a standalone runnable: ``python -m oc_chat.scripts.tui_spike``. The
companion ``README_TUI_SPIKE.md`` documents how to run it, the exit
criteria that gate Round 2, and a results table for manual smoke.
Responsibilities
----------------
- :func:`main` — bootstrap the prompt_toolkit Application and run it.
- :func:`build_application` — assemble the three-pane layout, key
bindings, and styled controls. Returns a ready-to-run
:class:`Application`.
- :class:`_StreamSimulator` — background thread that appends fake
Rich-ANSI streaming chunks to the conversation buffer at a bounded
rate. Stop event for SIGINT cancellation.
- :data:`SAMPLE_RICH_BLOCKS` — representative Rich-ANSI samples
(Markdown heading, fenced code, table, long paragraph, wide
unicode) used to populate the conversation pane on launch.
Diagnostics
-----------
Domain: OC_CHAT.TUI_SPIKE
Levels:
L2 — application lifecycle (start / exit)
L3 — stream simulation events (start, cancel, chunk rate cap)
L4 — per-chunk trace
Contracts
---------
- The spike must not import any OC Chat runtime modules other than
:mod:`oc_diagnostics`. It does not touch sessions, providers, the
agent, or the registry. Deletable later without touching anything
else.
- Rendering uses ``FormattedTextControl(text=ANSI(...))`` so Rich's
ANSI output is passed through verbatim — prompt_toolkit does not
re-parse the escape codes; it just hands them to the terminal.
- Worker-thread → UI handoff goes via
:meth:`Application.create_background_task` /
``app.loop.call_soon_threadsafe``. Direct buffer mutation from the
worker thread would race with the event loop.
- The simulated chunk rate is capped at 80 chunks/sec, matching the
observed max from real DeepSeek-v4-pro generations. Above this,
prompt_toolkit's redraw cadence struggles and the test isn't
representative.
- SIGINT during streaming sets a ``threading.Event`` that the worker
polls between chunks; the app stays alive. Ctrl+Q exits via the
normal key-binding path.
Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- prompt_toolkit (lazy; 3.x Application, layout and key bindings, assumed installed for any developer running this spike)
- rich (lazy; optional ANSI rendering of the sample blocks, falls back to a plain-text sample when missing)