Skip to content

feat(ui): run the turn on a worker thread, marshal the UI to the loop#9

Merged
lavindeep merged 1 commit into
ui-reworkfrom
feat/ui-worker-turn
Jun 28, 2026
Merged

feat(ui): run the turn on a worker thread, marshal the UI to the loop#9
lavindeep merged 1 commit into
ui-reworkfrom
feat/ui-worker-turn

Conversation

@lavindeep

Copy link
Copy Markdown
Owner

Branch 4/9 of the UI v2 full-screen TUI rework — the threading keystone, and the first live-test milestone.

What

In the full-screen app the prompt_toolkit event loop is the main thread, so driving a model turn synchronously there would freeze the UI for the whole turn. Run the one synchronous turn on a worker thread and marshal every UI callback back onto the loop thread, so AppUI state + pane repaint only ever touch the loop thread.

  • ThreadedUI — wraps AppUI + an injected schedule; each fire-and-forget call is enqueued via functools.partial. Blocking approvals return a value, so they delegate to the inner (which still raises until branch 7, surfaced as a pane error).
  • TurnRunner — one worker per turn; busy flag touched only on the loop thread (set in start, cleared via a scheduled _mark_done) — no lock. The worker only calls run_turn + schedule; any failure surfaces as a pane error and the finally clears busy, so a crashed turn never wedges the app. schedule reads app.loop lazily and call_soon_threadsafe's a callback that runs the call then invalidate().
  • build_app gains on_submit; the opt-in run_app (SHELLPILOT_UI=app) wires it and app.run()s it. The default REPL is byte-identical — app mode is off unless the env opt-in is set.

Verification

  • Phase gate green: ruff + mypy --strict clean, 1334 passed (20 new test_app_turn.py tests).
  • Marshaling/threading proven via a real worker thread + FakeLLM; every load-bearing test proven red-first (marshal-not-inline, busy-guard, exception-surfaced, busy-cleared, on_submit).
  • Independent skeptical review (SHIP) + second-model cross-thread review against the prompt_toolkit source (concerns only) — call_soon_threadsafe + lazy app.loop + invalidate-from-callback all confirmed safe; the conversation-guard moved inside the worker try and the shutdown-drop documented per their feedback.
  • DESIGN §31.13 in the same commit.

Scope

No cancellation (branch 6), no approval focus-swap (branch 7), no queue (branch 9), no thinking indicator (branch 5). Live behaviors (real app.run() loop, chrome-persists-through-a-turn, resize/scroll) are live-test items, run by the maintainer — not CI.

In the full-screen app the prompt_toolkit event loop is the main thread,
so driving a model turn synchronously there would freeze the UI for the
whole turn — no repaint, no scroll. Run the one synchronous turn on a
worker thread and marshal every UI callback back onto the loop thread, so
AppUI state and the pane repaint are only ever touched on the loop thread.

- ThreadedUI: a RuntimeUI wrapping AppUI + an injected schedule; each
  fire-and-forget content call is enqueued via functools.partial (never a
  loop-variable closure). Blocking approvals return a value so they can't
  be fire-and-forget — they delegate to the inner, which still raises until
  the focus-swap lands, surfaced by the worker as a pane error.
- TurnRunner: owns one worker per turn and a busy flag touched only on the
  loop thread (set in start, cleared in a scheduled _mark_done) — no lock.
  The worker only calls run_turn + schedule; any failure (incl. a build-
  order violation) surfaces as a pane error and the finally clears busy, so
  a crashed turn never wedges the app. schedule reads app.loop lazily and
  call_soon_threadsafe's a callback that runs the call then invalidate().
- build_app gains on_submit; the opt-in run_app (SHELLPILOT_UI=app) wires
  it all and app.run()s it. The default REPL is byte-identical — app mode
  is off unless the env opt-in is set.

The threading model was independently reviewed against the prompt_toolkit
source (call_soon_threadsafe + lazy app.loop + invalidate-from-callback
all confirmed safe). Known dev-entry gaps (no session_end audit; in-flight
turn at /exit drops its final busy-clear in the dying app) are documented
for the promotion-to-default review.

DESIGN.md section 31.13 covers the worker turn, marshaling, and the entry.
@lavindeep lavindeep merged commit f00ac27 into ui-rework Jun 28, 2026
2 checks passed
@lavindeep lavindeep deleted the feat/ui-worker-turn branch June 28, 2026 00:12
lavindeep added a commit that referenced this pull request Jul 2, 2026
feat(ui): run the turn on a worker thread, marshal the UI to the loop
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