thinking-prompt
A prompt_toolkit extension that adds a "thinking box" above the prompt for displaying AI thinking/processing content with real-time streaming updates.
Features
- Thinking Box: A collapsible area above the prompt that shows processing/thinking content
- Multiple Thinking Boxes: Run multiple independent boxes concurrently with ordering and per-box lifecycle control
- Real-time Streaming: Content updates in real-time as your callback returns new content
- Animated Header: Configurable animated indicator showing thinking is in progress
- Rich Output: Support for markdown rendering and syntax-highlighted code blocks
- Rich/ANSI in Thinking Box: Use Rich markup for styled, in-place-updating content inside the thinking box
- Customizable Styles: Full control over colors and styling
Installation
pip install thinking-prompt
For markdown and code highlighting support:
pip install thinking-prompt[all]
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).
Key Bindings
| Key | Action |
|---|---|
| Ctrl+T | Expand/collapse all thinking boxes (while one is open) |
| Ctrl+C | Cancel current operation or exit |
| 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. When a handler ends, the session finishes the boxes it opened (and any whose task has already ended); a box opened by a background task stays until that task finishes it. Ctrl+C at an idle prompt clears every remaining box.
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 lines under a +N earlier lines... ctrl-t to expand hint, so a stream stays visibly live; a one-row box has no room
for the hint and shows just the newest line. 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]")
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...")
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 loopclear_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)
License
MIT
Metadata
Release files for thinking-prompt 0.4.0
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.0.tar.gz | 1.4 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| thinking_prompt-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.6 MB
Release files / thinking_prompt-0.4.0.tar.gz
| Download URL | thinking_prompt-0.4.0.tar.gz |
|---|---|
| Size | 1.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0bd85c93ea9fafb5477484b868faf76a3bde710ce23b9bf766c59b6bf3a18509
|
|
BLAKE2b-256 checksum How to use checksums |
017433a02a2a289b2c87597b7ec054a26c68b6bc1e26b9016806417ea82da419
|
| 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.0-py3-none-any.whl
| Download URL | thinking_prompt-0.4.0-py3-none-any.whl |
|---|---|
| Size | 105.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
69c53c85b45d0945c234534e9fa9629273200e999c9f1a7687a73f6fe3985431
|
|
BLAKE2b-256 checksum How to use checksums |
2f0abda16592450d880b57d3d3bb27b98fef7905b091084d282acf0f8fc3c812
|
| 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.0
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.