python-xli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_xli-0.2.0.dist-info/METADATA +397 -0
- python_xli-0.2.0.dist-info/RECORD +22 -0
- python_xli-0.2.0.dist-info/WHEEL +4 -0
- python_xli-0.2.0.dist-info/licenses/LICENSE +21 -0
- xli/__init__.py +38 -0
- xli/approval.py +14 -0
- xli/cells.py +389 -0
- xli/engine.py +868 -0
- xli/images.py +90 -0
- xli/pets.py +27 -0
- xli/render/__init__.py +21 -0
- xli/render/diff.py +37 -0
- xli/render/message.py +115 -0
- xli/render/plan.py +70 -0
- xli/render/reasoning.py +20 -0
- xli/render/tool.py +128 -0
- xli/render_bridge.py +36 -0
- xli/slash.py +154 -0
- xli/status.py +59 -0
- xli/theme.py +153 -0
- xli/ui.py +346 -0
- xli/wizard.py +46 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-xli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Build polished, transcript-style terminal interfaces for chat / agent / REPL-ish apps in 20 lines of Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/vitalops/xli
|
|
6
|
+
Project-URL: Issues, https://github.com/vitalops/xli/issues
|
|
7
|
+
Author-email: Fariz Rahman <farizrahman4u@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,chat,cli,prompt-toolkit,rich,terminal,transcript,tui
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
19
|
+
Classifier: Topic :: Terminals
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: prompt-toolkit>=3.0.40
|
|
22
|
+
Requires-Dist: rich>=13.7
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
28
|
+
Provides-Extra: images
|
|
29
|
+
Requires-Dist: pillow>=10.0; extra == 'images'
|
|
30
|
+
Provides-Extra: markdown
|
|
31
|
+
Requires-Dist: pygments>=2.17; extra == 'markdown'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
<div align="center">
|
|
35
|
+
|
|
36
|
+
# xli
|
|
37
|
+
|
|
38
|
+
**Build polished, transcript-style terminal UIs for chat, agent, and REPL apps β in ~20 lines of Python.** β¨
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/xli/)
|
|
41
|
+
[](https://pypi.org/project/xli/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](#-credits)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import xli
|
|
49
|
+
|
|
50
|
+
ui = xli.UI(title="echo")
|
|
51
|
+
|
|
52
|
+
@ui.on_prompt
|
|
53
|
+
async def reply(prompt: str) -> None:
|
|
54
|
+
with ui.streaming("assistant") as out:
|
|
55
|
+
for token in f"You said: {prompt}".split():
|
|
56
|
+
out.write(token + " ")
|
|
57
|
+
|
|
58
|
+
ui.run()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That little snippet gets you: markdown-rendered **streaming** responses, slash-command
|
|
62
|
+
autocomplete, `@file` mentions, multi-line input (Enter sends, Alt+Enter for newlines),
|
|
63
|
+
persistent history, a themed status bar, arrow-selectable prompts, inline approvals β and a
|
|
64
|
+
terminal that *feels right*. πͺ
|
|
65
|
+
|
|
66
|
+
And the best part: the transcript flows into your terminal's **normal scrollback**, so text stays
|
|
67
|
+
**selectable, scrollable, and searchable** with your terminal's own tools. xli doesn't take over
|
|
68
|
+
the screen.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## β¨ Highlights
|
|
73
|
+
|
|
74
|
+
- π **Real scrollback, not a screen takeover.** Finalized output is printed into your terminal's
|
|
75
|
+
native scrollback β select, scroll, and find all work the way they always do.
|
|
76
|
+
- π **Streaming markdown** that appears token-by-token, then settles into properly-rendered text.
|
|
77
|
+
- π **Mutable cards.** A tool call flips from `running` β `done` *in place*; a plan's checkboxes
|
|
78
|
+
tick off live β via a handle you hold and update from anywhere.
|
|
79
|
+
- β¨οΈ **A real composer.** Multi-line input, slash-command autocomplete, `@file` mentions, history,
|
|
80
|
+
and paste handling out of the box.
|
|
81
|
+
- β
**Inline approvals, pickers & wizards** β arrow-selectable, no modal screen-takeover, and they
|
|
82
|
+
block your agent until the user answers.
|
|
83
|
+
- βΈοΈ **Type-ahead & ESC-to-interrupt** while the agent is working, with cooperative `asyncio`
|
|
84
|
+
cancellation.
|
|
85
|
+
- π¨ **Themeable** via plain dataclasses β minimal/glyph-driven by default, boxed if you insist.
|
|
86
|
+
- πͺΆ **Tiny surface, two deps.** Wraps [rich](https://github.com/Textualize/rich) for rendering and
|
|
87
|
+
[prompt_toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) for input. No build steps.
|
|
88
|
+
|
|
89
|
+
## π€ What it is
|
|
90
|
+
|
|
91
|
+
A small, opinionated library for **one specific job**: interactive agent / chat-style terminal apps
|
|
92
|
+
where output is a *flowing transcript*, not an app screen.
|
|
93
|
+
|
|
94
|
+
The pattern xli handles:
|
|
95
|
+
|
|
96
|
+
- A scrolling transcript of structured cards β user messages, assistant messages, tool calls,
|
|
97
|
+
diffs, plans, reasoning, images β committed to real scrollback.
|
|
98
|
+
- Streaming markdown that appears as it arrives, then settles into rendered scrollback.
|
|
99
|
+
- A persistent multi-line composer at the bottom with autocomplete, `@file` mentions, and history.
|
|
100
|
+
- **Mutable cards** β a tool card that flips `running` β `done` in place, a plan whose checkboxes
|
|
101
|
+
update β via a handle you hold.
|
|
102
|
+
- Inline, arrow-selectable approvals / pickers / wizards that block until resolved.
|
|
103
|
+
- A subtle status bar, an animated "working" spinner, type-ahead, and ESC-to-interrupt.
|
|
104
|
+
|
|
105
|
+
## π« What it is NOT
|
|
106
|
+
|
|
107
|
+
| Not⦠| Because |
|
|
108
|
+
|---|---|
|
|
109
|
+
| A TUI framework | No widget tree, no CSS, no focus model, no mouse panes. Reach for [Textual](https://github.com/Textualize/textual) when you want a full app screen. |
|
|
110
|
+
| A "richer `print`" | xli is interactive β it owns the event loop. For one-shot static rendering, use [rich](https://github.com/Textualize/rich) directly. |
|
|
111
|
+
| A full-screen app | It renders **inline** so your terminal keeps native scroll / select / find. That's the whole point. |
|
|
112
|
+
| Tied to any LLM / agent framework | It knows nothing about providers. It renders the events *you* emit β OpenAI, Anthropic, LangChain, your own loop, whatever. |
|
|
113
|
+
| A REPL builder | It runs *your* code in response to prompts. For a Python REPL, use [ptpython](https://github.com/prompt-toolkit/ptpython). |
|
|
114
|
+
|
|
115
|
+
## βοΈ How it compares
|
|
116
|
+
|
|
117
|
+
xli lives in the gap between "low-level rendering toolkit" and "full-screen app framework." If your
|
|
118
|
+
output is a **conversation that scrolls**, that gap is exactly where you want to be.
|
|
119
|
+
|
|
120
|
+
| | xli | [Textual](https://github.com/Textualize/textual) | [rich](https://github.com/Textualize/rich) | [prompt_toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) | [questionary](https://github.com/tmbo/questionary) |
|
|
121
|
+
|---|---|---|---|---|---|
|
|
122
|
+
| **Shape** | Flowing transcript | Full-screen app | Print / render | Input / REPL | Prompts only |
|
|
123
|
+
| **Native scrollback** | β
keeps it | β takes over screen | β
(it just prints) | β οΈ partial | β
|
|
|
124
|
+
| **Streaming + mutable cards** | β
built-in | β
(you wire widgets) | β DIY | β DIY | β |
|
|
125
|
+
| **Composer (multiline, history, `@`, `/`)** | β
built-in | π§ build from widgets | β | π§ primitives | β |
|
|
126
|
+
| **Inline approvals / pickers / wizards** | β
built-in | π§ build from widgets | β | π§ primitives | β
(pickers) |
|
|
127
|
+
| **Best for** | Chat / agent transcripts | Dashboards, IDEs, full apps | Static output | Custom REPLs & input | One-off questionnaires |
|
|
128
|
+
|
|
129
|
+
**In short:** Textual gives you a canvas to build *any* app and asks you to design the whole screen.
|
|
130
|
+
xli gives you *one* app shape β the agent/chat transcript β already assembled, and hands the
|
|
131
|
+
scrollback back to your terminal. If you've been gluing rich + prompt_toolkit together to make a
|
|
132
|
+
chat loop, xli *is* that glue, done well. π
|
|
133
|
+
|
|
134
|
+
## π¦ Install
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
pip install xli
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Optional extras:
|
|
141
|
+
|
|
142
|
+
- `xli[markdown]` β `pygments` for code-block syntax highlighting in messages (recommended).
|
|
143
|
+
- `xli[images]` β `pillow`, for inline images (`ui.image(...)`).
|
|
144
|
+
|
|
145
|
+
Two core dependencies (rich, prompt_toolkit). No build steps. Requires Python **3.11+**.
|
|
146
|
+
|
|
147
|
+
## π Quickstart
|
|
148
|
+
|
|
149
|
+
A fuller echo agent β streaming, a status field, a tool card that updates in place, and a slash
|
|
150
|
+
command:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import asyncio
|
|
154
|
+
import xli
|
|
155
|
+
|
|
156
|
+
ui = xli.UI(title="echo", status_fields=["turn"], pet="cat")
|
|
157
|
+
turn = 0
|
|
158
|
+
|
|
159
|
+
@ui.command("clear", description="clear the screen")
|
|
160
|
+
async def clear(ui, args):
|
|
161
|
+
ui.clear_transcript()
|
|
162
|
+
|
|
163
|
+
@ui.on_prompt
|
|
164
|
+
async def handle(prompt: str) -> None:
|
|
165
|
+
global turn
|
|
166
|
+
turn += 1
|
|
167
|
+
ui.status.set(turn=turn)
|
|
168
|
+
|
|
169
|
+
card = ui.tool("think", status="running") # live, mutable card
|
|
170
|
+
with ui.working("thinking"):
|
|
171
|
+
await asyncio.sleep(0.5)
|
|
172
|
+
card.update(status="done", output="ok") # commits to scrollback
|
|
173
|
+
|
|
174
|
+
with ui.streaming("assistant") as out:
|
|
175
|
+
for token in f"You said: **{prompt}**".split():
|
|
176
|
+
out.write(token + " ")
|
|
177
|
+
await asyncio.sleep(0.04)
|
|
178
|
+
|
|
179
|
+
ui.run()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
> π‘ Want to see everything at once? `examples/demo.py` exercises every feature in one file.
|
|
183
|
+
|
|
184
|
+
## π The vocabulary
|
|
185
|
+
|
|
186
|
+
`xli.UI` exposes a small set of methods you call from any handler. Transcript methods return a
|
|
187
|
+
**cell handle** you can mutate.
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# --- streaming + cards (return a Cell handle) ---
|
|
191
|
+
with ui.streaming(role) as out: # streamed text; out.write(chunk); out.text
|
|
192
|
+
...
|
|
193
|
+
card = ui.tool(name, args=, output=, status="running") # status="running" -> live + mutable
|
|
194
|
+
card.update(status="done", output=...) # mutate in place; commits when final
|
|
195
|
+
card.remove() # drop a live cell
|
|
196
|
+
|
|
197
|
+
ui.message(role, text) # one-shot message
|
|
198
|
+
ui.diff(diff, path=) # syntax-colored unified diff
|
|
199
|
+
ui.plan([("step", "status"), β¦]) # checklist
|
|
200
|
+
ui.reasoning(summary) # muted thought rail
|
|
201
|
+
ui.image("plot.png") # inline image (kitty / iTerm2 / half-block fallback)
|
|
202
|
+
ui.link("label", "https://β¦") # OSC 8 hyperlink
|
|
203
|
+
ui.note("β¦") / ui.header("β¦") # muted status lines
|
|
204
|
+
ui.print(any_rich_renderable) # escape hatch for custom rendering
|
|
205
|
+
|
|
206
|
+
# --- a spinner while you work ---
|
|
207
|
+
with ui.working("running tests"): ... # animated, with an elapsed timer
|
|
208
|
+
|
|
209
|
+
# --- blocking prompts (await) ---
|
|
210
|
+
decision = await ui.approve(title=, body=, reason=) # arrow-select Yes / Always / No
|
|
211
|
+
choice = await ui.pick("Model", ["gpt-5", "claude-opus"]) # β/β Β· 1-9 Β· enter
|
|
212
|
+
yes = await ui.confirm("Delete?")
|
|
213
|
+
name = await ui.input("Name?", default="")
|
|
214
|
+
answers = await ui.wizard([ # multi-step flow -> dict
|
|
215
|
+
ui.step.pick("Model", ["opus", "sonnet"]),
|
|
216
|
+
ui.step.confirm("Stream responses?"),
|
|
217
|
+
ui.step.text("Project name", default="app"),
|
|
218
|
+
])
|
|
219
|
+
|
|
220
|
+
# --- chrome + lifecycle ---
|
|
221
|
+
ui.status.set(model="gpt-5", tokens="3.2k/400k") # bottom bar (declare fields up front)
|
|
222
|
+
ui.notify("response ready") # desktop notification (OSC 9)
|
|
223
|
+
ui.clear_transcript()
|
|
224
|
+
ui.exit()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## π§ The one design decision
|
|
228
|
+
|
|
229
|
+
xli renders **inline**, not full-screen. Finalized cells are *printed into your terminal's normal
|
|
230
|
+
scrollback* β so selection, scrolling, and find all come from the terminal itself. Only a small
|
|
231
|
+
**live region** at the bottom (the composer, status bar, an in-progress stream, a running tool
|
|
232
|
+
card, a spinner, a picker) is redrawn.
|
|
233
|
+
|
|
234
|
+
A cell is **mutable while it's live** at the bottom; once it finalizes it **commits to scrollback**
|
|
235
|
+
and becomes immutable (but selectable). That two-tier model is what lets xli have both editable,
|
|
236
|
+
animated cards *and* native, selectable scrollback β the thing full-screen TUIs give up.
|
|
237
|
+
|
|
238
|
+
## π Mutable cards
|
|
239
|
+
|
|
240
|
+
The transcript methods return a handle. Hold it, mutate it from anywhere (including across `await`s
|
|
241
|
+
and from other tasks) β it re-renders in place while live, then commits to scrollback once it's
|
|
242
|
+
finalized:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
card = ui.tool("shell", status="running", args={"command": ["pytest", "-q"]})
|
|
246
|
+
result = await run_shell(...)
|
|
247
|
+
card.update(status="done", output=result) # β shell β¦ and the output, committed
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
A `ui.tool(...)` **without** a `status` is a one-shot card (committed immediately). With
|
|
251
|
+
`status="running"` it stays live and mutable until you update it to `done` / `error` / `cancelled`.
|
|
252
|
+
|
|
253
|
+
## β¨οΈ Slash commands & @file mentions
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
@ui.command("model", description="switch model")
|
|
257
|
+
async def cmd_model(ui, args):
|
|
258
|
+
sel = await ui.pick("Model", ["gpt-5", "claude-opus"])
|
|
259
|
+
if sel is not None:
|
|
260
|
+
ui.status.set(model=sel)
|
|
261
|
+
|
|
262
|
+
@ui.command("quit", aliases=["q", "exit"])
|
|
263
|
+
async def cmd_quit(ui, args):
|
|
264
|
+
ui.exit()
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Typing `/` opens a command list **below** the composer (arrow to navigate, Tab to fill, Enter to
|
|
268
|
+
run). `/help`, `/quit`, and `/clear` are built in (override freely). Typing `@` opens a **file
|
|
269
|
+
picker** from the working directory; Tab/Enter inserts the path β handy for letting users reference
|
|
270
|
+
files for your agent.
|
|
271
|
+
|
|
272
|
+
## β
Approvals, pickers, wizards
|
|
273
|
+
|
|
274
|
+
All are inline and arrow-selectable (no modal screen-takeover). The request commits to scrollback so
|
|
275
|
+
it scrolls into view and persists; the choices appear in the live region; the outcome is recorded
|
|
276
|
+
below.
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
decision = await ui.approve(
|
|
280
|
+
title="apply patch to README.md",
|
|
281
|
+
body="add a one-liner about xli",
|
|
282
|
+
reason="writes outside the workspace root",
|
|
283
|
+
) # -> "approved" | "approved_for_session" | "denied" | "aborted"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`β/β` move the highlight, `1`-`9` quick-select, `Enter` confirms, `Esc` cancels.
|
|
287
|
+
|
|
288
|
+
## βΈοΈ Interrupts
|
|
289
|
+
|
|
290
|
+
The composer stays live while your handler runs β users can **type ahead** (queued prompts show as
|
|
291
|
+
muted `β―` lines) and press **ESC to interrupt** the current turn. Interrupt is cooperative `asyncio`
|
|
292
|
+
cancellation; register cleanup with `@ui.on_interrupt`:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
@ui.on_interrupt
|
|
296
|
+
async def cleanup():
|
|
297
|
+
await release_resources() # don't write to the transcript here
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
A running tool card left behind by an interrupted turn is automatically marked `cancelled`.
|
|
301
|
+
|
|
302
|
+
## π¨ Themes
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
ui = xli.UI(theme="codex") # default β minimal, glyph-driven, no borders, no solid bg
|
|
306
|
+
ui = xli.UI(theme="minimal") # even more austere
|
|
307
|
+
ui = xli.UI(theme="boxed") # rounded borders if you really want them
|
|
308
|
+
ui = xli.UI(theme=xli.Theme( # custom β it's a dataclass; override fields
|
|
309
|
+
user_color="cyan",
|
|
310
|
+
tool_glyph="β",
|
|
311
|
+
code_theme="monokai",
|
|
312
|
+
))
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Themes are dataclasses β override fields, don't subclass. The default leans "light": chrome is font
|
|
316
|
+
color + thin rules, never solid background blocks. See [`docs/theme.md`](docs/theme.md) for the
|
|
317
|
+
design guide you can hand to a coding agent.
|
|
318
|
+
|
|
319
|
+
## π Plugging into an agent
|
|
320
|
+
|
|
321
|
+
xli renders the event types you give it. It doesn't care if those come from OpenAI, Anthropic,
|
|
322
|
+
LangChain, or your own framework:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
@ui.on_prompt
|
|
326
|
+
async def handle(prompt: str) -> None:
|
|
327
|
+
cards = {} # tool-call id -> live card handle
|
|
328
|
+
async for event in my_agent.stream(prompt):
|
|
329
|
+
if event.kind == "message":
|
|
330
|
+
ui.message("assistant", event.text)
|
|
331
|
+
elif event.kind == "tool_call":
|
|
332
|
+
cards[event.id] = ui.tool(event.name, args=event.args, status="running")
|
|
333
|
+
elif event.kind == "tool_result":
|
|
334
|
+
cards[event.id].update(status="done", output=event.output) # flips in place
|
|
335
|
+
elif event.kind == "approval_request":
|
|
336
|
+
decision = await ui.approve(title=event.title, body=event.body)
|
|
337
|
+
my_agent.respond(event.id, decision)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
For token-by-token output, wrap a `with ui.streaming("assistant") as out:` block and call
|
|
341
|
+
`out.write(delta)` as deltas arrive. Have your own event stream? Register a custom renderer:
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
@ui.renderer("benchmark")
|
|
345
|
+
def render_benchmark(ui, event):
|
|
346
|
+
ui.print(make_bar_chart(event["data"]))
|
|
347
|
+
|
|
348
|
+
ui.dispatch({"type": "benchmark", "data": [...]})
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## β¨οΈ Keys
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
enter send (or run highlighted command / select picker option / accept input)
|
|
355
|
+
alt+enter β
ctrl+j newline
|
|
356
|
+
β / β history Β· navigate the command/file list Β· move picker selection
|
|
357
|
+
tab accept the highlighted completion / insert @file path
|
|
358
|
+
1β9 quick-select a picker option
|
|
359
|
+
esc cancel a picker/modal or close the list β otherwise interrupt the turn
|
|
360
|
+
ctrl+c interrupt ctrl+d quit
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## π οΈ Contributing
|
|
364
|
+
|
|
365
|
+
Contributions, bug reports, and ideas are very welcome! π
|
|
366
|
+
|
|
367
|
+
```sh
|
|
368
|
+
git clone https://github.com/fariz/xli
|
|
369
|
+
cd xli
|
|
370
|
+
pip install -e ".[dev,markdown,images]"
|
|
371
|
+
pytest # run the tests
|
|
372
|
+
ruff check . # lint
|
|
373
|
+
mypy xli # type-check
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Found a rough edge or have a use case xli doesn't cover cleanly? Open an
|
|
377
|
+
[issue](https://github.com/fariz/xli/issues) β the API is small on purpose, so design
|
|
378
|
+
conversations matter.
|
|
379
|
+
|
|
380
|
+
## π Status
|
|
381
|
+
|
|
382
|
+
**Pre-1.0** (currently `0.2.0`). The API is stable enough to build on; versions are bumped
|
|
383
|
+
thoughtfully if anything user-facing changes.
|
|
384
|
+
|
|
385
|
+
## π Credits
|
|
386
|
+
|
|
387
|
+
xli stands on the shoulders of two excellent libraries:
|
|
388
|
+
|
|
389
|
+
- [**rich**](https://github.com/Textualize/rich) β all the rendering.
|
|
390
|
+
- [**prompt_toolkit**](https://github.com/prompt-toolkit/python-prompt-toolkit) β all the input.
|
|
391
|
+
|
|
392
|
+
xli's contribution is the *composition*: the API you'd actually want to write a chat/agent terminal
|
|
393
|
+
app in.
|
|
394
|
+
|
|
395
|
+
## π License
|
|
396
|
+
|
|
397
|
+
[MIT](LICENSE) Β© xli contributors
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
xli/__init__.py,sha256=gNFFbOR4JOFbN2-iff3VWE-hedtIvX0zFty7Pq4ZsgY,768
|
|
2
|
+
xli/approval.py,sha256=7NmOxnEDgvjN7i8_9p_-M5-8uILPSMT8FmmHvtETH6o,588
|
|
3
|
+
xli/cells.py,sha256=Hm_LWWgWdzoRRwBmGlPJCWI-G0eiEYlWGVI-a6pOkxk,13984
|
|
4
|
+
xli/engine.py,sha256=6FIdZz1-8SQ_GiVF8nNx1GC-fQnpJ9s6M0CSvxYxwFE,35386
|
|
5
|
+
xli/images.py,sha256=j5pygWopovC7FO-0K-SMPfXXnfFXHnW1Y3xxw2dxw3E,3336
|
|
6
|
+
xli/pets.py,sha256=sdWX8fDommhmD2tx9xTOPNLEH7BfD45b1l6NPDzJIYI,1056
|
|
7
|
+
xli/render_bridge.py,sha256=WjMKMbTSH6h-xTBQZTgSmpadaREAHyzEZEpHK19VzTI,1245
|
|
8
|
+
xli/slash.py,sha256=FxzkszW_K__o0G6BYKk-KR3piYXYTrQ_Xv8ajArpjB8,4993
|
|
9
|
+
xli/status.py,sha256=V-F3a0Xq4-4xOIiDiA0B_hoFyZ692WtpnWfbK2WO1qE,1836
|
|
10
|
+
xli/theme.py,sha256=P589MBrVeLhZA5JOUEJtEVK3Avk4DJbFE7LSuPBhp-Y,4688
|
|
11
|
+
xli/ui.py,sha256=hNRv-8QI0twn9rOuOb5-v4p6pjvexvfKvSkfbmtBCfU,14099
|
|
12
|
+
xli/wizard.py,sha256=Aawf08WWL9RgoS1BZg7N1tXfLwF0B-fhn5bF8dIr9gU,1584
|
|
13
|
+
xli/render/__init__.py,sha256=dGGrGufWp3zNtEGxdW-SKm68pKvYR2RpK2wh_bTaHtA,607
|
|
14
|
+
xli/render/diff.py,sha256=otcr7KbgzNH-W4jdhm8g4yg9Un1MyTz9q4prbwpw-IU,1106
|
|
15
|
+
xli/render/message.py,sha256=lPqJFCPPFBm67reVy91JmvG7KdNGslESdCzyTOwhY7U,3673
|
|
16
|
+
xli/render/plan.py,sha256=N_n4hAEApitu2mEsvnp04YV8ZZGhOLuoLQrD5bSpeCI,2202
|
|
17
|
+
xli/render/reasoning.py,sha256=ea8nQ60yo6qeHETlwVuKIjbc5tPnF5h53l6MhEtv-r8,565
|
|
18
|
+
xli/render/tool.py,sha256=Gbr1TLlkdT2xItXFyOaf8C3B3irmrAvemdGpH5XE6eQ,4153
|
|
19
|
+
python_xli-0.2.0.dist-info/METADATA,sha256=L0XsAZPljCeyNJGa0pdQUyYnbtnGiKcDVmDnG-DKV38,16656
|
|
20
|
+
python_xli-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
21
|
+
python_xli-0.2.0.dist-info/licenses/LICENSE,sha256=wQFeRNo0meQ2e05iZET1PNN3l9spnFsh3pZ62NmjzFc,1073
|
|
22
|
+
python_xli-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xli contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
xli/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""xli β terminal interfaces for transcript-style agent / chat apps.
|
|
2
|
+
|
|
3
|
+
The public surface is intentionally tiny. Most users only need::
|
|
4
|
+
|
|
5
|
+
import xli
|
|
6
|
+
|
|
7
|
+
ui = xli.UI()
|
|
8
|
+
|
|
9
|
+
@ui.on_prompt
|
|
10
|
+
async def reply(prompt: str) -> None:
|
|
11
|
+
with ui.streaming("assistant") as out:
|
|
12
|
+
out.write(f"You said: {prompt}")
|
|
13
|
+
|
|
14
|
+
ui.run()
|
|
15
|
+
|
|
16
|
+
See ``README.md`` for the full cookbook and ``docs/`` for design + theming guides.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .cells import Cell
|
|
22
|
+
from .slash import SlashCommand
|
|
23
|
+
from .theme import BOXED, CODEX, MINIMAL, Theme
|
|
24
|
+
from .ui import UI, Streaming
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BOXED",
|
|
28
|
+
"CODEX",
|
|
29
|
+
"Cell",
|
|
30
|
+
"MINIMAL",
|
|
31
|
+
"SlashCommand",
|
|
32
|
+
"Streaming",
|
|
33
|
+
"Theme",
|
|
34
|
+
"UI",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
__version__ = "0.2.0"
|
xli/approval.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""The approval decision type.
|
|
2
|
+
|
|
3
|
+
The inline approval *flow* (rendering the prompt + capturing y / a / n / esc) lives in
|
|
4
|
+
:mod:`xli.engine`, since it has to happen inside the running app's key loop. This module
|
|
5
|
+
just defines the four-value result so it can be imported without pulling in the engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
#: ``"approved"`` Β· ``"approved_for_session"`` (the "always" choice) Β· ``"denied"`` Β·
|
|
13
|
+
#: ``"aborted"`` (esc / ctrl-c while the prompt is open).
|
|
14
|
+
Decision = Literal["approved", "approved_for_session", "denied", "aborted"]
|