thinking-prompt
A prompt_toolkit extension that adds "thinking boxes" above the prompt, for streaming what an AI model is thinking or doing as it happens. Use it for your own agent CLI, or any CLI that runs long tasks: thinking boxes, dialogs and form fields on a prompt that stays inline, in the terminal's scrollback.
Features
- Thinking boxes: stream text into a box above the prompt as it arrives. It keeps the newest lines in view, Ctrl+T shows it all, and its content stays in the transcript when it ends.
- Several at once: boxes run side by side, each finished on its own,
ordered by
order=. - Animated header: a spinner, and a title you can change while it runs.
- Dialogs: yes/no, choice, dropdown, checklist or your own, drawn as a box or inline under the prompt.
- Form fields: text, checkbox, select, dropdown, checklist, radio and choice fields on any dialog, so a settings dialog is a few lines.
- Rich output: markdown, syntax-highlighted code and Rich renderables, in the transcript or inside a box.
- Themes: dark, light, mono, the terminal's own colors, or auto;
switchable at runtime, and
NO_COLORis honored. - Light on the terminal: no full screen, and the screen is redrawn only when something changes, so an idle prompt costs no CPU.
- Typed, one dependency:
prompt_toolkit, plus optionalrichandpygments.
Installation
pip install thinking-prompt
For markdown and code highlighting support:
pip install thinking-prompt[all]
Requires Python 3.9–3.14. CI runs the tests on Linux, macOS and Windows; the UI is tested by hand in macOS terminals.
Quick Start
import asyncio
from thinking_prompt import ThinkingPromptSession, AppInfo
async def main():
app_info = AppInfo(name="MyApp", version="1.0.0")
session = ThinkingPromptSession(app_info=app_info, message=">>> ")
@session.on_input
async def handle(text: str):
if not text.strip():
return
# Use context manager for clean thinking management
async with session.thinking() as box:
box.append("Processing...\n")
await asyncio.sleep(0.5)
box.append("Done!\n")
session.add_response(f"You said: {text}")
await session.run_async()
if __name__ == "__main__":
asyncio.run(main())
Note — input handlers run on the event loop. A synchronous handler blocks the UI for its entire duration: the screen freezes, the spinner stops, and Ctrl+C is not processed until it returns. Use an
asynchandler for anything that takes time, and wrap blocking calls withawait asyncio.to_thread(...)so the UI stays responsive.
Note — handler errors. An exception in a handler is shown as
[ERROR] Handler error: KeyError: 'x'and logged, with its traceback, to thethinking_promptlogger. The library adds no log output of its own; configure logging to see tracebacks — to a file, since stderr would draw over the UI:logging.basicConfig(filename="app.log", level=logging.ERROR).
Use with a model
Stream a model's thinking into a box and print its answer when it's done.
With Claude (pip install anthropic), the Quick Start's handler becomes:
from anthropic import AsyncAnthropic
client = AsyncAnthropic() # reads ANTHROPIC_API_KEY
history = []
@session.on_input
async def handle(text: str):
turn = {"role": "user", "content": text}
async with session.thinking(title="Thinking") as box:
async with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=16000,
thinking={"type": "adaptive", "display": "summarized"},
messages=[*history, turn],
) as stream:
async for event in stream:
if event.type == "thinking":
box.append(event.thinking)
reply = await stream.get_final_message()
history.extend([turn, {"role": "assistant", "content": reply.content}])
answer = "".join(b.text for b in reply.content if b.type == "text")
session.add_response(answer, markdown=True)
When the reply is done the box closes, leaving the thinking (its last lines,
if it's long) in the transcript above the answer. Ctrl+C cancels the handler,
which closes the stream, and the unanswered turn stays out of history.
examples/claude_chat.py is the whole program.
Key Bindings
| Key | Action |
|---|---|
| Ctrl+T | Expand/collapse all thinking boxes (while one is open) |
| Ctrl+C | Cancel the running handler; at an idle prompt, end the boxes nobody else will (see When a Box Ends), or exit when no box is open |
| Ctrl+D | Exit application (empty input line only) |
Rebind Ctrl+T with expand_key= (see Session Options).
API Reference
ThinkingPromptSession
The main class for creating a thinking-enabled prompt session.
session = ThinkingPromptSession(
message=">>> ", # Prompt message
app_info=AppInfo(...), # Startup banner (none without it)
max_thinking_height=15, # Max lines when collapsed
enable_status_bar=True, # Show status bar
echo_input=True, # Echo user input to console
complete_while_typing=True, # Show completions as you type
completions_menu_height=6, # Max rows in completions popup
)
Every argument after message is keyword-only. Keys and the thinking
header are under Session Options.
Thinking API
Context Manager (recommended):
async with session.thinking(title="Processing") as box:
box.append("Step 1...\n")
await asyncio.sleep(0.5)
box.append("Step 2...\n")
# Automatically finishes when exiting context
Manual control:
box = session.start_thinking(title="Processing")
box.append("Processing...\n")
await asyncio.sleep(0.5)
# Finish this specific box and optionally echo to console
box.finish(add_to_history=True, echo_to_console=True)
To show text you already hold, pass a content callback instead. The box
re-reads it while it's drawn (every 0.1 s). You can read such a box
(box.text is what it shows), but writing to it raises TypeError:
status = {"line": "starting"}
box = session.start_thinking(lambda: status["line"], title="Watching")
status["line"] = "step 2" # shown at the next poll
box.finish()
When a Box Ends
A box's content is kept however it ends, echoed to the console and history as on a normal finish:
- Cancelled (Ctrl+C, or the task running a
thinking()block is cancelled): the content, then oneOperation cancelled...line after all the boxes that the cancel ended. - The handler raises: the content, then the
[ERROR] Handler error: ...line. - Left open by a handler that returns: finished as
box.finish()would.
A box belongs to the task that opened it. The session ends a box only when
nobody else will: its task has ended, it was opened outside asyncio (from a
thread), or the app's own task opened it (the task running run_async(),
e.g. for a startup status box). A box a live task is feeding is that task's
to end.
- A handler ends: the session finishes the boxes the handler opened, and those whose task has ended or that were opened outside asyncio.
- Ctrl+C at an idle prompt: the session ends the boxes nobody else
will. If boxes that live tasks are feeding remain, Ctrl+C doesn't exit:
the status bar says
Background work is running — Ctrl+D exitsuntil those boxes end or the next input is accepted. - Exit: the last frame leaves out the boxes and the status bar. The
boxes nobody else will end are echoed below it. A live task's box is
echoed when the task finishes it, or else when the task ends
(
asyncio.runcancels leftover tasks at shutdown).
box.is_finished tells whether a box is done. Calling finish() again
returns the same text without echoing it twice. Writes to a finished box are
dropped, so box.text stays what finish() returned; the first one logs a
warning to the thinking_prompt logger.
Ctrl+T expands or collapses all open boxes together, and boxes start collapsed again once the last one finishes.
Redraws
The screen is redrawn only when something changes, never on a timer, so an idle prompt costs no CPU:
- Writes to a box (
append,set_line,set_title, ...) redraw right away, from any thread; fast streams are capped at 30 redraws a second. - A spinner redraws at each frame (10 a second for the default) while its
box is shown;
ThinkingPromptSession(spinner=None)turns the spinner off. - Text that can't say when it changes is re-read every 0.1 s while it's
shown: a content callback passed to
start_thinking(), and a promptmessageorstatus_textgiven as a callable. - When the terminal gets narrower or shorter, the session repaints once
the size holds for 0.25 s: it clears the screen and scrollback and prints
the transcript again, as
set_theme(repaint=True)does. A narrower window re-wraps the UI's full-width rows and a shorter one pushes its top into scrollback, so the old frame can't be erased row by row. Shell output from before the session, anything printed outside it (plainprint()) and entrieshistory_limitdropped don't come back.
Long Content
A collapsed box shows at most max_lines rows (at least 1; session
default: max_thinking_height, at least 2). When content overflows, the
box keeps the newest rows under a +N earlier lines... ctrl-t to expand hint, so a stream stays visibly live, even mid-paragraph: a line
longer than the box shows its end, and the hint counts it. A box with no
row to spare for the hint (one row, or a narrow terminal where the hint
wraps) shows just its newest rows. Ctrl+T expands the box to fit its content, up to
the space the terminal has. When the box finishes, the console echo keeps
the same end the box showed.
For content whose first lines matter most — a task list, a plan — keep the head instead:
tasks = session.start_thinking(title="Tasks", max_lines=10, overflow="head")
# or: async with session.thinking(overflow="head") as box: ...
Multiple Thinking Boxes
Run multiple boxes concurrently with independent lifecycles. Use order to control positioning (higher = closer to prompt):
# Task list pinned near the prompt
tasks = session.start_thinking(title="Tasks", order=100, max_lines=10)
tasks.append_rich("[dim] ○ Download[/dim]\n")
tasks.append_rich("[dim] ○ Process[/dim]\n")
tasks.append_rich("[dim] ○ Report[/dim]\n")
# Step 1: separate detail box appears above the task list
tasks.set_line_rich(0, "[bold cyan] ⟳ Downloading…[/bold cyan]")
dl = session.start_thinking(title="Download", max_lines=2)
for pct in range(0, 101, 20):
dl.set_line(0, f" {pct}% complete")
await asyncio.sleep(0.3)
dl.finish(echo_to_console=False, add_to_history=False)
tasks.set_line_rich(0, "[green] ✓ Download[/green]")
# Step 2: another detail box
tasks.set_line_rich(1, "[bold cyan] ⟳ Processing…[/bold cyan]")
proc = session.start_thinking(title="Process", max_lines=2)
for i in range(5):
proc.set_line(0, f" Batch {i+1}/5...")
await asyncio.sleep(0.4)
proc.finish(echo_to_console=False, add_to_history=False)
tasks.set_line_rich(1, "[green] ✓ Process[/green]")
# Finish task list
tasks.finish(echo_to_console=False, add_to_history=False)
Rich/ANSI Content in Thinking Box
Use append_rich() and set_line_rich() for styled content with Rich markup:
async with session.thinking(title="Processing") as box:
box.append_rich("[dim] ○ Load data[/dim]\n")
box.append_rich("[dim] ○ Transform[/dim]\n")
box.set_line_rich(0, "[bold cyan] ⟳ Load data…[/bold cyan]")
box.set_title("Loading")
await asyncio.sleep(1)
box.set_line_rich(0, "[green] ✓ Load data[/green]")
box.set_line_rich(1, "[bold cyan] ⟳ Transform…[/bold cyan]")
box.set_title("Transforming")
await asyncio.sleep(1)
box.set_line_rich(1, "[green] ✓ Transform[/green]")
Markup keeps its whitespace, so the newline above ends each task's line, and
text wraps in the box like plain text. A Rich renderable such as a Panel or
Table is laid out at the terminal's width.
Dynamic Titles & In-Place Updates
Use set_title() to change the thinking header and set_line() to update lines in place:
async with session.thinking(title="Downloading") as box:
box.append("Progress: 0%\n")
for i in range(1, 101):
box.set_line(-1, f"Progress: {i}%")
box.set_title(f"Downloading {i}%")
await asyncio.sleep(0.02)
set_line() and set_line_rich() replace one line, so their text must be one
line: a newline in it, or markup that renders to several lines (a Panel, a
Table), raises ValueError. Indices past the end add empty lines up to it.
Lines are numbered as len(box) counts them, so -1 is the last line, and a
trailing newline ends a line rather than starting one. A negative index on an
empty box raises IndexError.
Output Methods
# Plain text response
session.add_response("Hello, world!")
# Markdown (requires rich)
session.add_response("# Title\n- Item 1\n- Item 2", markdown=True)
# Syntax-highlighted code (requires pygments)
session.add_code("def hello(): return 'world'", language="python")
# Status bar text
session.set_status("Ready") # Plain text
from rich.text import Text
session.set_status(Text.from_markup("[bold]Processing[/bold]")) # Rich renderable
# Status messages
session.add_success("Operation completed")
session.add_warning("Rate limit approaching")
session.add_error("Connection failed")
session.add_message("system", "Connecting to server...")
The session sometimes puts a hint in the status bar in place of your
status: Busy — press Ctrl+C to cancel when Enter is pressed while a
handler runs, and Background work is running — Ctrl+D exits (see
When a Box Ends). Your status comes back once the hint
no longer applies; a status you set meanwhile replaces the hint.
Dialogs
Dialogs open over a running session: call them from an input handler, or
from a task started while run()/run_async() is running. On a session
that isn't running they raise RuntimeError. If the session exits while a
dialog is open, awaiting it raises asyncio.CancelledError.
# Yes/No confirmation
result = await session.yes_no_dialog("Confirm", "Delete this item?")
if result:
# User clicked Yes
# Message dialog
await session.message_dialog("Info", "Operation completed!")
# Choice dialog (multiple buttons)
action = await session.choice_dialog("Action", "What to do?", ["Save", "Discard", "Cancel"])
# Dropdown selection
theme = await session.dropdown_dialog("Theme", "Choose:", ["Light", "Dark", "System"])
# Custom dialog
from thinking_prompt import Dialog, ButtonConfig
result = await session.show_dialog(Dialog(
title="Custom",
body="Choose an option:",
buttons=[
ButtonConfig("Option A", result="a"),
ButtonConfig("Option B", result="b"),
],
))
A button either returns a fixed result or runs a handler, which decides
what happens: set_result(...) or cancel() closes the dialog, doing
nothing keeps it open (e.g. while input is invalid). For a form, give the
dialog fields and read what they hold with dialog.values; for other
content, pass any prompt_toolkit container as body, or subclass Dialog
and override build_body() / get_buttons():
from thinking_prompt import ButtonConfig, Dialog, TextField
class LoginDialog(Dialog):
title = "Login"
fields = [TextField("user", "Username")]
def get_buttons(self):
return [
ButtonConfig("Login", handler=self.login),
ButtonConfig("Cancel", handler=self.cancel),
]
def login(self):
if self.values["user"]: # stays open while empty
self.set_result(self.values["user"])
name = await session.show_dialog(LoginDialog())
Escape closes any dialog and returns None (or the dialog's
escape_result). To require a button press instead, pass
escapable=False to Dialog(...), or set escapable = False on a
subclass.
A Dialog has no buttons unless you give it some, and it needs something
to take keyboard focus: at least one button, fields, or a focusable body (e.g. a
TextArea, closed with Escape). Otherwise show_dialog() raises
ValueError. Other options: escape_result, escapable, width (box
only; never narrower than the buttons), max_height, fields, align_values.
Where a box sits. Under the prompt, above the status bar: where an inline dialog would be. When the rest of the screen takes rows (a thinking box, a longer prompt), the box shares them as an inline dialog does: it takes the rows it gets and scrolls the rest. Only when those are fewer than its title, one row and its buttons need is it centered over the screen.
Long content scrolls. A dialog is never taller than the terminal (less
the prompt's rows), nor than max_height rows if you pass it — counting the
whole dialog: title, borders, buttons, hint. What doesn't fit scrolls: rows
with a cursor (fields, lists, actions) follow the cursor, and a text body
scrolls with PgUp/PgDn (and ↑↓ in a box). Every helper takes max_height=.
Small terminals. A box dialog that can't fit the terminal at all (narrower than its buttons, or shorter than its frame, buttons and one body row) shows a short notice in its place, with the size it needs. Keys still reach the hidden dialog: Esc cancels it and Enter presses its focused button. It comes back as soon as the terminal is big enough.
Upgrading from 0.3: DialogConfig and BaseDialog were removed.
| 0.3 | 0.4 |
|---|---|
DialogConfig(title, body, buttons, ...) |
Dialog(title, body, buttons, ...) |
class MyDialog(BaseDialog) |
class MyDialog(Dialog) |
get_buttons() returning ("OK", handler) |
ButtonConfig("OK", handler=handler) |
escape_result = _UNSET |
escapable = False |
BaseDialog subclass relying on its default OK button |
buttons = (ButtonConfig("OK"),) |
Dialog(height=N) |
max_height=N: the dialog sizes to its content, up to N rows |
top on a dialog subclass |
Drop it: a box sits under the prompt, where an inline dialog would (a subclass's top attribute is ignored) |
SettingsDialog(title, items, ...), show_settings_dialog(title, items, ...) |
Dialog(title, fields=items, buttons=..., width=60): see Form fields |
SettingsItem, InlineSelectItem, DropdownItem, CheckboxItem, TextItem |
Field, SelectField, DropdownField, CheckboxField, TextField (frozen; same attributes) |
| A settings dialog's changed-values result | Compare dialog.values with your config, or config.update(values) |
can_cancel=False |
One Done button and escapable=False |
| Ctrl+S in a settings dialog | Removed: Tab (or ↓ inline) to the Save button |
Inline dialogs
Any dialog can also be drawn inline: rows under the prompt, above the status bar, in the rows a box dialog would take, but laid out as plain rows instead of a bordered box. Its buttons become a numbered list:
Delete 3 files?
This can't be undone.
❯ 1. Delete
2. Keep
↑↓ navigate · Enter select · Esc cancel
Choose per session, per dialog, or per call:
from thinking_prompt import ButtonConfig, Dialog, ThinkingPromptSession
session = ThinkingPromptSession(dialog_placement="inline") # default: "box"
ok = await session.yes_no_dialog("Delete?", "3 files", placement="box") # this call only
buttons = [ButtonConfig("A", result="a"), ButtonConfig("B", result="b")]
await session.show_dialog(Dialog("Pick", "Which one?", buttons, placement="inline"))
One cursor walks every row: ↑↓ move (Tab / Shift+Tab too), Enter runs the highlighted action, 1–9 run an action directly, Esc closes an escapable dialog. Fields are edited in place: ←→ change a selector, Enter opens a dropdown's menu, Space toggles a checkbox, Enter edits a text field.
Check, radio and choice lists show one option per line, in both styles:
from thinking_prompt import ButtonConfig, ChecklistField, ChoiceField, Dialog, RadioField
tools = await session.checklist_dialog(
"Tools", "Enable any:", ["search", "code", "files"], defaults=["search"],
) # the checked options, or None if cancelled
await session.show_dialog(Dialog("Settings", fields=[
ChecklistField("tools", "Tools", options=["search", "code"]),
RadioField("mode", "Mode", options=["fast", "careful"], default="fast"),
ChoiceField("theme", "Theme", options=["dark", "light"]),
], buttons=[ButtonConfig("Done")]))
A long text body scrolls with PgUp/PgDn, and the hint line says so while it
overflows; rows and actions scroll within 12 rows, following the cursor,
unless max_height says otherwise. width applies to box dialogs only.
Dialogs need a running session; to ask something before
run_async() starts or after it ends, use prompt_toolkit's own choice()
(prompt_toolkit 3.0.52 or later).
Form fields
Any dialog can hold form fields, under its body. dialog.values holds what
they're set to, by key; a button handler returns it. values= gives the
fields their starting values, so a settings dialog is a few lines:
from thinking_prompt import (
ButtonConfig, CheckboxField, Dialog, DropdownField, TextField,
)
SETTINGS = [
DropdownField("model", "Model", options=["gpt-4", "o1"]),
CheckboxField("stream", "Stream responses"),
TextField("name", "Name", description="Shown to others"),
]
dialog = Dialog(
"Settings",
fields=SETTINGS,
values=config, # starting values by key; keys that aren't fields are ignored
buttons=[
ButtonConfig("Save", handler=lambda: dialog.set_result(dialog.values)),
ButtonConfig("Cancel"),
],
width=60,
)
values = await session.show_dialog(dialog) # every field's value, or None
if values is not None:
config.update(values)
A field missing from values= starts at its default. Each show starts
afresh from values= (read again then) or the defaults, so a dialog can be
kept and shown again: it picks up what Save put in config, and an edit the
user cancelled is gone. Fields are frozen, so a list of them can be a
subclass's fields class attribute (MySettings(values=config)). Keys must
be unique.
| Field | Shows | Keys |
|---|---|---|
DropdownField |
The value with ▼; Enter opens a menu of the options under it, in a box and inline |
Enter (or Space) to open, ↑↓ to choose, Enter to keep, Escape to put the old value back |
SelectField |
A ◀ value ▶ selector |
←→ (or Space) to step through the options |
TextField |
Text, edited in place (password=True shows dots) |
Enter to edit, Enter/Escape to keep/drop the edit |
CheckboxField |
true / false |
Space, Enter or ←→ to toggle |
ChecklistField |
One [x] option per line; value: the checked ones, in option order |
↑↓ between options, Space/Enter to toggle |
RadioField |
One (•) option per line; value: the picked one |
↑↓ between options, Space/Enter to pick |
ChoiceField |
A radio list drawn like prompt_toolkit's choice(): no marks, the pick in bold; value: the picked one |
↑↓ between options, Space/Enter to pick. As a dialog's only field (with nothing in the body taking focus), Enter also presses the first button, or with no buttons closes the dialog with the pick |
Every field takes key, label, description and default. A check,
radio or choice list with no label and no description shows just its options.
Keys: in a box, ↑↓ move a row at a time and Tab / Shift+Tab a field at a time (Tab on to the buttons after the last field, Shift+Tab from the buttons back to it; a radio or choice list is always entered at its picked option). Inline, one cursor walks the rows and the actions.
Aligning values: values start two columns after the longest label or
description (align_values="labels", the default), so descriptions don't run
under them; a description too long to leave room for the widest value still
does. Check, radio and choice lists, whose labels and descriptions sit on
rows of their own, don't count. align_values="right" puts values at the row's
right edge, and a number starts them at that column, cutting longer labels with
….
When a row is short of width, its label is cut first (down to 3 columns), then
a dropdown or text field gives up its padding, and only then is the value
itself cut with …. A box with no width is as wide as its fields' rows.
Session Options
from thinking_prompt import Spinner, ThinkingPromptSession
session = ThinkingPromptSession(
# Keys
expand_key="c-t", # Ctrl+T expands/collapses thinking boxes
# Thinking boxes
thinking_title="Thinking", # Header title of boxes started without one
spinner=Spinner.DOTS, # Spinner.LINE, Spinner.ELLIPSIS, your own, or None
spinner_position="before", # "before" or "after" the title
echo_thinking=True, # Echo a finished box to the console
)
A Spinner is its frames and the seconds each is shown:
Spinner(("◐", "◓", "◑", "◒"), interval=0.15). One frame makes a static mark.
Upgrading from 0.3: full screen (Ctrl+E with
AppInfo(fullscreen_enabled=True)) was removed: drop fullscreen_enabled=
and fullscreen_key=. switch_to_fullscreen(), switch_to_prompt() and
is_fullscreen are gone too. Ctrl+T shows thinking boxes in full, and the
terminal's own scrollback holds the transcript.
AppInfo
The banner printed when the session starts:
from thinking_prompt import AppInfo
app_info = AppInfo(
name="MyApp",
version="1.0.0",
welcome_message="Welcome to MyApp!", # Optional; replaces the name/version box
)
session = ThinkingPromptSession(app_info=app_info)
welcome_message can also be a callable returning a string or a Rich
renderable. Leave app_info out for no banner. An empty message ("", or a
callable returning "" or None) prints nothing.
Theming
Four built-in themes, selectable by name:
session = ThinkingPromptSession(theme="light") # or "dark", "mono", "terminal", "auto"
dark— the default (unchanged from previous versions)light— palette tuned for light terminal backgroundsmono— attributes only (bold/italic/reverse), rendered without colorterminal— named ANSI colors that inherit your terminal's own paletteauto— picks mono underNO_COLOR, light/dark fromCOLORFGBGwhen available, otherwiseterminal
Custom themes are ThinkingPromptStyles instances (theme= accepts them
too). All element styles derive from the color_* tokens, so overriding
tokens restyles the whole UI consistently. Derivation happens at render
time, so tweaking a built-in theme works too:
import dataclasses
from thinking_prompt import ThinkingPromptStyles
styles = dataclasses.replace(ThinkingPromptStyles.light(), color_accent="#c2410c")
session = ThinkingPromptSession(theme=styles)
Element fields you set explicitly (e.g. error_message="fg:red") win over
the tokens; styles.to_style_dict() shows the resolved result.
Code highlighting — fenced code in markdown and add_code() alike —
follows the theme's code_theme: a Pygments style name ("monokai" in
dark, "default" in light), or "ansi_dark"/"ansi_light" to use the
terminal's own 16 colors (the terminal theme's choice).
Switch at runtime:
session.set_theme("light") # live UI re-themes on next paint
session.set_theme("light", repaint=True) # also clears and re-prints the transcript
With repaint=True, markdown and code blocks re-render from source in the
new theme; output from add_rich() and raw ANSI keeps its original colors.
NO_COLOR (non-empty, checked at startup — see no-color.org) forces
colorless rendering regardless of theme, while keeping bold/italic.
Upgrading from 0.3: style settings that did nothing were removed.
| 0.3 | 0.4 |
|---|---|
ThinkingPromptSession(styles=s) |
ThinkingPromptSession(theme=s) |
SettingsDialog(..., styles=...) |
Removed with the settings dialog (see Form fields). |
setting_* style fields (setting_label, setting_value_true, ...) |
field_* (field_label, field_value_true, ...); the setting-* classes are field-* |
assistant_prefix, select_value, user_separator |
Removed: no component drew with them. |
checkbox_mark (styled nothing) |
Now styles the [x] / (•) marks of check and radio lists. |
Bound history growth for long sessions:
session = ThinkingPromptSession(history_limit=1000) # max transcript entries
Examples
See the examples/ directory for complete demos:
basic.py- Simple thinking box usagedemo.py- Interactive demo with simulated AI thinkingstreaming.py- Character-by-character streamingchat_demo.py- Chat-style conversation loopclaude_chat.py- Chat with Claude: its thinking streams into a box, its answer prints as markdown (needsanthropicand an API key)clear_demo.py- Clearing the screen and history withsession.clear()progress_demo.py- Progress bar with callbackdemo_progress_line.py- In-place progress updatesdemo_multi_box.py- Multiple concurrent thinking boxes with task listdemo_messages_during_thinking.py- Output messages during thinkingdemo_animated_separator.py- Different animation configurationsdemo_task_progress.py- Rich-styled task progress with in-place status updatesdialog_test.py- Dialog system demo (yes/no, message, choice, dropdown)settings_dialog_demo.py- A settings dialog built from Dialog fields, box and inlinedemo_showcase.py- Feature showcase for demos and screenshots; dialogs start inline,/inlineswitches to boxesdemo.tape- VHS script that records the demo GIF above fromdemo_showcase.py:vhs examples/demo.tapecompleter_demo.py- Slash-command autocompletion (like Claude Code)demo_themes.py- Built-in themes and runtime theme switchinginline_dialogs.py- Dialogs drawn inline under the prompt (and the same ones as boxes)shell_jobs.py- Shell commands as background jobs, each streaming into its own numbered box while you type the next;/stop Nstops one,rmasks first
License
MIT
Metadata
Release files for thinking-prompt 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| thinking_prompt-0.4.1.tar.gz | 1.3 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| thinking_prompt-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / thinking_prompt-0.4.1.tar.gz
| Download URL | thinking_prompt-0.4.1.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c95423cc9431b0e82beebd1f43269093ba55ad6d8c005c76ff7e9dc3113e05d
|
|
BLAKE2b-256 checksum How to use checksums |
ec448dc3e0d6e13c272cd4174483cc33a94034b91023cec60341b1a7af36e23e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Release files / thinking_prompt-0.4.1-py3-none-any.whl
| Download URL | thinking_prompt-0.4.1-py3-none-any.whl |
|---|---|
| Size | 111.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fe81874c23d851eda68e3c5044cf3a1789e1d29b6ee1feeb0d870ae68072858
|
|
BLAKE2b-256 checksum How to use checksums |
8500a42e76e7e7515ec57893e7a5cd7649a973c6b807aa5ce386189778e03ba0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Security for thinking-prompt 0.4.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.