Skip to content

Commit d455df5

Browse files
authored
Merge pull request #21 from shoom1/docs/sync-handlers-and-changelog
docs: sync-handler event-loop warning; changelog for pending fixes
2 parents 68f0fa3 + 5cf639b commit d455df5

3 files changed

Lines changed: 38 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Fixed
11+
12+
- First Ctrl+C at an idle prompt now exits the session. Previously the live pending-input future was treated as in-flight work: the binding cancelled it (killing the input loop) but never exited the app, leaving a zombie session that echoed typed input without delivering it; only a second Ctrl+C exited. Behavior change for apps driving `prompt_async()` directly with their own loop: Ctrl+C at an idle prompt now also calls `app.exit()` in addition to raising `KeyboardInterrupt`, so a caller that swallowed the exception to keep the session alive will now observe the application exiting.
13+
- `DialogManager.show()` now raises `RuntimeError` when a dialog is already open instead of overwriting it — a second concurrent dialog used to orphan the first dialog's result future, hanging its awaiter forever.
14+
- `show_settings_dialog(can_cancel=False)` no longer returns the string `"close"` when Escape is pressed (violating the documented `dict | None` contract). Escape is now disabled in that mode; the Done button is the only way out.
15+
- Custom `AppInfo.expand_key` is now reflected in the thinking-box truncation hint. The hint previously always read `ctrl-t to expand` even when the actual binding was different.
16+
- `StreamingContent.set_line()` with an out-of-range negative index now raises a clear `IndexError` naming the index the caller passed, and leaves content untouched.
17+
- The deprecated `finish_thinking()` now truncates each box to its own `max_lines` instead of the session-wide `max_thinking_height`, matching `ThinkingContext.finish()`. (`ThinkingBoxManager.finish_all()` result tuples gained a fifth element, `max_collapsed_lines`.)
18+
- `clear()` now clears the screen through `app.renderer.clear()` while the app is running. The previous raw `\033[2J\033[H` write bypassed the renderer and left its screen state stale, corrupting the next repaint.
19+
- Thinking-box height estimation strips ANSI styling escapes before measuring line wrap, so heavily styled `content_format="ansi"` content no longer renders an oversized box.
20+
- The thinking header separator is sized to the terminal width instead of a hardcoded 80 columns.
21+
22+
### Changed
23+
24+
- Importing `thinking_prompt` no longer monkey-patches `rich.markdown.Markdown` globally. The left-aligned heading style is applied through an internal `Markdown` subclass used only by this library's own markdown rendering; host applications' Rich output is unaffected.
25+
- Rich/Pygments helpers moved from `display` to a new `rich_utils` module (`display` re-exports the old names for backward compatibility). The public `rich_to_ansi` export is unchanged.
26+
- Removed dead `ThinkingBoxControl.get_key_bindings()` and `get_console_output()` — neither had a production caller; expand/collapse is owned by the session-level binding and console truncation by `Display.thinking()`.
27+
- `DEFAULT_SPINNER_FRAMES` now lives in `types` as the single source of truth (still importable from `layout`); `AppInfo.thinking_animation` defaults to it.
28+
- README and docstrings now state explicitly that sync input handlers block the event loop (frozen UI, no Ctrl+C) and recommend async handlers with `asyncio.to_thread` for blocking work.
29+
830
## [0.3.2] - 2026-05-01
931

1032
### Fixed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ if __name__ == "__main__":
5555
asyncio.run(main())
5656
```
5757

58+
> **Note — input handlers run on the event loop.** A synchronous handler
59+
> blocks the UI for its entire duration: the screen freezes, the spinner
60+
> stops, and Ctrl+C is not processed until it returns. Use an `async`
61+
> handler for anything that takes time, and wrap blocking calls with
62+
> `await asyncio.to_thread(...)` so the UI stays responsive.
63+
5864
## Key Bindings
5965

6066
| Key | Action |

thinking_prompt/session.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,13 @@ def on_input(self, func: Callable[[str], Any]) -> Callable[[str], Any]:
882882
input text as a string and can be sync or async. The handler decides
883883
whether to use thinking mode by calling start_thinking().
884884
885+
Warning:
886+
Handlers run on the event loop. A synchronous handler blocks
887+
the UI for its entire duration — the screen freezes, spinners
888+
stop, and Ctrl+C is not processed until it returns. Use an
889+
async handler for anything that takes time, and wrap blocking
890+
calls with ``await asyncio.to_thread(...)``.
891+
885892
Example:
886893
session = ThinkingPromptSession(header="MyApp")
887894
@@ -944,6 +951,9 @@ async def run_async(
944951
handler: Callback for each input. If not provided, uses handler
945952
registered with @on_input decorator. The handler decides
946953
whether to use thinking mode by calling start_thinking().
954+
Sync handlers block the event loop (and the UI) until
955+
they return — prefer async handlers for slow work; see
956+
on_input() for details.
947957
948958
Raises:
949959
ValueError: If no handler is provided and none was registered.

0 commit comments

Comments
 (0)