tau-tui 0.2.0__tar.gz

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.
Files changed (49) hide show
  1. tau_tui-0.2.0/.gitignore +25 -0
  2. tau_tui-0.2.0/LICENSE +21 -0
  3. tau_tui-0.2.0/PKG-INFO +274 -0
  4. tau_tui-0.2.0/README.md +247 -0
  5. tau_tui-0.2.0/pyproject.toml +36 -0
  6. tau_tui-0.2.0/src/tau_tui/__init__.py +35 -0
  7. tau_tui-0.2.0/src/tau_tui/app.py +947 -0
  8. tau_tui-0.2.0/src/tau_tui/approval.py +230 -0
  9. tau_tui-0.2.0/src/tau_tui/brand.py +247 -0
  10. tau_tui-0.2.0/src/tau_tui/cli.py +59 -0
  11. tau_tui-0.2.0/src/tau_tui/commands.py +182 -0
  12. tau_tui-0.2.0/src/tau_tui/composer.py +391 -0
  13. tau_tui-0.2.0/src/tau_tui/events.py +228 -0
  14. tau_tui-0.2.0/src/tau_tui/header.py +113 -0
  15. tau_tui-0.2.0/src/tau_tui/history.py +174 -0
  16. tau_tui-0.2.0/src/tau_tui/keys.py +112 -0
  17. tau_tui-0.2.0/src/tau_tui/sessions.py +261 -0
  18. tau_tui-0.2.0/src/tau_tui/strings.py +144 -0
  19. tau_tui-0.2.0/src/tau_tui/theme.py +252 -0
  20. tau_tui-0.2.0/src/tau_tui/tools.py +416 -0
  21. tau_tui-0.2.0/src/tau_tui/transcript.py +821 -0
  22. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_approval_80x24.raw +167 -0
  23. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_busy_80x24.raw +164 -0
  24. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_chat_80x24.raw +168 -0
  25. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_events_50x24.raw +164 -0
  26. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_menu_80x24.raw +163 -0
  27. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_picker_80x24.raw +164 -0
  28. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_short_80x18.raw +137 -0
  29. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_ultra_160x45.raw +256 -0
  30. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_welcome_50x24.raw +198 -0
  31. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_welcome_80x24.raw +222 -0
  32. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_welcome_tr_80x24.raw +222 -0
  33. tau_tui-0.2.0/tests/__snapshots__/test_snapshots/test_wide_100x30.raw +192 -0
  34. tau_tui-0.2.0/tests/conftest.py +8 -0
  35. tau_tui-0.2.0/tests/fixtures/mark_16.txt +8 -0
  36. tau_tui-0.2.0/tests/fixtures/mark_24.txt +12 -0
  37. tau_tui-0.2.0/tests/helpers.py +250 -0
  38. tau_tui-0.2.0/tests/test_app.py +469 -0
  39. tau_tui-0.2.0/tests/test_approval.py +221 -0
  40. tau_tui-0.2.0/tests/test_brand.py +107 -0
  41. tau_tui-0.2.0/tests/test_cancel.py +62 -0
  42. tau_tui-0.2.0/tests/test_cli.py +174 -0
  43. tau_tui-0.2.0/tests/test_commands.py +552 -0
  44. tau_tui-0.2.0/tests/test_composer.py +237 -0
  45. tau_tui-0.2.0/tests/test_events.py +73 -0
  46. tau_tui-0.2.0/tests/test_history.py +241 -0
  47. tau_tui-0.2.0/tests/test_sessions.py +329 -0
  48. tau_tui-0.2.0/tests/test_snapshots.py +242 -0
  49. tau_tui-0.2.0/tests/test_transcript.py +380 -0
@@ -0,0 +1,25 @@
1
+ .DS_Store
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ *.local.env
6
+ __pycache__/
7
+ *.py[cod]
8
+
9
+ # Python / uv
10
+ .venv/
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ dist/
14
+ build/
15
+ *.egg-info/
16
+
17
+ # Personal facts about the user stay local (persona/user.example.md is the tracked template)
18
+ persona/user.md
19
+
20
+ # Tools tau wrote itself, waiting for approval
21
+ tools/_pending/*
22
+ !tools/_pending/.gitkeep
23
+
24
+ # MkDocs build output
25
+ site/
tau_tui-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fport
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 all
13
+ 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 THE
21
+ SOFTWARE.
tau_tui-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.5
2
+ Name: tau-tui
3
+ Version: 0.2.0
4
+ Summary: Textual terminal UI for tau: streamed markdown replies, tool cards, live events and in-TUI approvals.
5
+ Project-URL: Homepage, https://github.com/fport/tau
6
+ Project-URL: Documentation, https://docs.tau.getporti.com
7
+ Project-URL: Issues, https://github.com/fport/tau/issues
8
+ Project-URL: Changelog, https://github.com/fport/tau/releases
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,assistant,cli,tau,terminal,textual,tui
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Home Automation
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: rich>=13
24
+ Requires-Dist: tau-core>=0.1.0
25
+ Requires-Dist: textual<9,>=8.2
26
+ Description-Content-Type: text/markdown
27
+
28
+ # tau-tui
29
+
30
+ The [Textual](https://textual.textualize.io/) terminal interface for tau: replies stream in as
31
+ markdown, tool calls are cards with their tier, approval and result, agent events show in a live
32
+ pane, tier-2 calls ask in a modal dialog, and the brand's τ mark opens every new session. One row
33
+ of chrome at the top, at most four at the bottom, so 80x24 over mosh on a phone stays usable.
34
+
35
+ It is its own distribution (`tau-tui`, import `tau_tui`), uses only the public API of `tau-core`
36
+ and plugs into the `tau` command through the `tau.commands` entry point (`tui`):
37
+
38
+ ```sh
39
+ pip install tau-tui # or, in this repo: uv sync --all-packages --group dev
40
+ tau tui # new session
41
+ tau tui --resume # continue the most recent earlier session
42
+ tau tui --resume ID # continue a given session
43
+ tau --home /path/to/tau --lang tr tui --profile travel
44
+ ```
45
+
46
+ Configuration comes from `tau.toml` and `.env` like every other channel (`TAU_HOME`,
47
+ `TAU_DATA_DIR`, `TAU_PROFILE`, `TAU_LANG`). Missing credentials, a broken config or an unknown
48
+ session end the command with a plain English message and exit code 2 before the interface opens.
49
+ The chrome (hints, labels, notices, the approval dialog) follows `[ui] language` (`en` or `tr`);
50
+ `/lang` switches it for one run.
51
+
52
+ ## Layout
53
+
54
+ Four bands by width (`-phone` < 60 cols, `-narrow` 60–99, `-wide` 100–139, `-ultra` ≥ 140) and
55
+ three by height (`-short` < 20 rows, `-mid`, `-tall` ≥ 30). The header is one row, the composer a
56
+ rounded box of one to five text rows, the hint bar one row.
57
+
58
+ 80x24 (`-narrow`), idle after a two-tool turn:
59
+
60
+ ```text
61
+ τ tau home · 20260929-120000-abcd brain · en
62
+ › Salon lambası açık mı? Açıksa kapat.
63
+ ● get_light_state(room="salon") tier 0 · 38ms
64
+ ⎿ {"on": true, "brightness": 80}
65
+ ● set_light(room="salon", on=false) tier 2 · approved · 412ms
66
+ ⎿ Light turned off
67
+ τ Salon lambası kapatıldı.
68
+ • Önceki durum: açık, %80 parlaklık
69
+ ✓ Worked for 4s · 2 tool calls · ctrl+o expand
70
+ ╭──────────────────────────────────────────────────────────────────────────────╮
71
+ │ ❯ Type a message… │
72
+ ╰──────────────────────────────────────────────────────────────────────────────╯
73
+ ? shortcuts · / commands · ctrl+j newline ctrl+q quit
74
+ ```
75
+
76
+ 100x30 (`-wide`): the events pane docks on the right (34 cols; 40 at `-ultra`), `ctrl+e` hides
77
+ it. 160x45 (`-ultra`): a read-only session sidebar (28 cols) on the left; clicking a row resumes.
78
+
79
+ ```text
80
+ τ tau home · 20260929-120000-abcd brain · 7 tools · en
81
+ › Salon lambası açık mı? Açıksa kapat. │ Events · 5
82
+ ● get_light_state(room="salon") tier 0 · 38ms │ 12:00:01 ● get_light_state
83
+ ⎿ {"on": true, "brightness": 80} │ tier 0 · ok · 38ms
84
+ τ Salon lambası kapatıldı. │ 12:00:02 ◆ approval set_light
85
+ │ tier 2 → approved (tui)
86
+ ```
87
+
88
+ 80x30 (`-narrow -tall`): an 8-row events strip under the transcript. Below 100 cols `ctrl+e` /
89
+ F4 swaps the transcript for the events log in the same body; the composer stays live and Esc
90
+ returns to the chat.
91
+
92
+ 50x24 (`-phone`, Termius over mosh): the header drops the session id (`/session` shows it),
93
+ tool badges move to the result line, the composer has no border, hints say `^j`, the welcome
94
+ card shows the 16-column mark, replies stream as plain text and settle into markdown when the
95
+ segment ends, and the approval dialog is full width with stacked buttons.
96
+
97
+ ```text
98
+ τ tau · home en
99
+ › Salon lambası açık mı? Açıksa kapat.
100
+ ○ set_light(room="salon", on=false)
101
+ ⎿ tier 2 · running
102
+ ✻ Running set_light…
103
+ ❯
104
+ esc cancel · ^j newline busy
105
+ ```
106
+
107
+ `-short` (< 20 rows): no hint row, the welcome mark is hidden, the composer grows to two rows.
108
+
109
+ ## Vocabulary
110
+
111
+ Every glyph is one cell wide and none is an emoji-presentation code point, so phone terminals
112
+ draw them at the right width: `›` you · `τ` tau · `●` finished tool call · `○` running or waiting
113
+ for approval · `⎿` result line · `✻` busy verb / reasoning · `✓ ◆ ✗` turn footer (finished /
114
+ limit, cancelled, interrupted / error) · `◆` needs you · `▸` notice · `❯` prompt.
115
+
116
+ State colours (deliberate; no green and no red in the palette, shapes double the colours):
117
+ **ok / success = Flare**, **error = Signal**, **rejected = Ember**, **running / pending = Core at
118
+ 55 %**, **attention = Flare**, notice = Ember. Rich renderables (the events pane) cannot take a TCSS
119
+ alpha, so they use the literal blends `theme.TEXT_MUTED` (Core over Void at 60 %, the live clock and
120
+ the running dot) and `theme.TEXT_DISABLED` (38 %, replayed clocks). The header `τ` breathes while the welcome card is
121
+ on screen, runs the comet while a turn is busy and is static Ember otherwise; nothing else in
122
+ the UI moves. The composer border is Ember, Signal while busy and Flare while an approval waits.
123
+
124
+ ## Keys and commands
125
+
126
+ | Key | Effect |
127
+ |---|---|
128
+ | Enter | send (or accept the highlighted menu row); while tau answers the text stays and a toast says so |
129
+ | ctrl+j | newline (shift+enter too where the terminal delivers it) |
130
+ | Esc | close the menu · cancel the running turn · leave the events view · scroll back to the bottom |
131
+ | ctrl+e / F4 | events: toggle the column (≥ 100 cols) or the view (< 100 cols) |
132
+ | ctrl+r / F2 | session picker (Enter resumes, `n` new, Esc closes) |
133
+ | ctrl+n | new session |
134
+ | ctrl+t / F3 | `/tools` |
135
+ | ctrl+o / F6 | expand or collapse the cards of the last turn: the arguments as JSON and the full result (a click toggles any card) |
136
+ | PageUp / PageDown, F7 / F8 | scroll the transcript; PageUp at the top of a folded history loads 20 older turns |
137
+ | F9 | back to the bottom |
138
+ | ↑ / ↓ | recall sent prompts (single-line composer); navigate the menu while it is open |
139
+ | `?` or F1 | help (only in an empty composer; `?x` is sent as a message) |
140
+ | ctrl+q | quit (ctrl+c copies a selection in the composer; without one it only says how to quit) |
141
+
142
+ Typing `/` into an empty composer opens a fuzzy menu of the slash commands (`↑↓` pick, Tab
143
+ complete, Enter run, Esc close). An exact name ranks first, and a command typed in full runs as
144
+ typed even when the menu has not caught up with a burst of keys (a paste, mosh), so `/session`
145
+ never runs `/sessions`. The commands are the shared spec of `tau_core.commands`, with
146
+ the same wording as `tau chat`:
147
+
148
+ | command | effect |
149
+ |---|---|
150
+ | `/help`, `/tools`, `/sessions`, `/session`, `/lang en\|tr` | as in `tau chat` |
151
+ | `/resume [n\|id]`, `/new` | switch sessions (see below) |
152
+ | `/compact [focus]` | summarize this session with its own model and continue in a new session seeded with the summary; the old session is kept |
153
+ | `/clear` | empty the transcript (turns, welcome card, info cards, the fold); the session and the agent go on, `/resume` or `--resume` show the history again |
154
+ | `/status` | an info card: profile, session, model, context, tools, language, versions, hub |
155
+ | `/model [role]` | list the roles of `tau.toml`, or switch this session to one; the header follows |
156
+ | `/quit` (alias `/exit`) | leave |
157
+
158
+ The Turkish aliases (`/devam`, `/çık`, `/özet`, `/temizle`, `/durum`, `/rol`, …) keep working
159
+ but are never listed; the menu shows the twelve canonical names and every row is on screen at
160
+ 80x24 (in the `-short` band it scrolls). `/new`, `/resume`, `/compact`, `/clear` and
161
+ `/model <role>` wait for the current turn (a toast says so); `/status` and `/model` without an
162
+ argument run any time.
163
+
164
+ ## Approvals
165
+
166
+ `TuiApprover` (`name = "tui"`) is the channel's `Approver`. A tier-2 call opens `ApprovalScreen`:
167
+ tool, effect, the arguments as pretty JSON and the buttons **Approve** / **Reject**.
168
+
169
+ - `y` or `e` approves, `n`, `h` or Esc rejects; focus starts on **Reject**, so Enter rejects, and
170
+ Tab only moves between the two buttons (PageUp/PageDown scroll the arguments). The keys work in
171
+ both languages whatever the UI setting.
172
+ - `y`/`e` and **Approve** only work 0.5 s after the dialog opens (the hint reads `(arming…)`
173
+ meanwhile), so keys typed for the chat cannot approve by accident.
174
+ - Anything other than an explicit approval rejects: a dialog closed another way, an approver
175
+ with no running app, or a cancelled wait (the dialog is taken down so it cannot approve later).
176
+ - Rejecting does not cancel the turn: the model reads the refusal and answers.
177
+
178
+ The dialog is translucent: the chat stays visible behind it. Behind it the tool card reads
179
+ `○ set_light(…)` + `⎿ ◆ waiting for your approval`, the composer and the header `τ` turn Flare
180
+ and the hint bar says what to press. Nothing in this package approves on its own.
181
+
182
+ ## Sessions and events
183
+
184
+ `/sessions` lists the ten most recent earlier sessions with messages (numbered for `/resume n`,
185
+ same order as `tau chat`); `/resume` alone resumes the most recent one. Resuming rebuilds the
186
+ agent on the stored session (the previous one is closed), renders the history from the stored
187
+ messages (user rows, markdown replies, tool cards with the recorded duration, approval and
188
+ result), replays the stored agent events into the events pane before live ones, and keeps the
189
+ current agent when the rebuild fails. The last 40 turns stay live; older ones fold into one
190
+ line and PageUp at the top pages them in. One `EventBus` serves the app for its lifetime; events
191
+ stamped with another session id are ignored.
192
+
193
+ `/compact [focus]` asks the agent's own model for a continuation summary, opens a new session
194
+ whose history starts with that summary, links the two sessions in their metadata
195
+ (`compacted_from` / `compacted_to`) and keeps the old one untouched; the transcript is reset
196
+ like `/new`, then shows the summary as an info card and the notice `Compacted N messages into a
197
+ summary. New session <id>; the previous session <old id> is kept.`, the header shows the new id
198
+ and the events pane starts empty. The summary is written in a worker, so the app stays
199
+ responsive: the chrome is busy (`✻ Summarizing…`, `esc cancel` in the hint bar), read-only
200
+ commands such as `/status` still answer, turns and session switches are refused, and Esc
201
+ cancels the summarizer (`Compaction cancelled; the session continues.`). A failure (nothing to
202
+ summarize, a model error, missing credentials) is a notice and the current session goes on.
203
+ The details are in tau-core's README under "Compaction".
204
+
205
+ `/model fast` rebuilds the agent on the same session with that role (the transcript stays,
206
+ the header and the welcome card follow) and remembers the role in the session's metadata, so
207
+ `/resume` and `tau tui --resume` come back on it; `build_app(role=...)` pins a role for the
208
+ whole run instead. An unknown role, missing credentials or an unavailable provider leave the
209
+ current agent working and say why.
210
+
211
+ ## Knobs
212
+
213
+ | Setting | Effect |
214
+ |---|---|
215
+ | `TAU_TUI_ASCII=1` | ASCII vocabulary (`> t * o L * + x ! - >`), `ascii` box borders on every screen (the screens carry an `-ascii` class that `theme.ASCII_CSS` keys on), an Ember-only mark |
216
+ | `TAU_TUI_PLAIN_STREAM=1` | stream every reply as plain text and parse the markdown once per segment |
217
+ | `TEXTUAL_ANIMATIONS=none` | no header motion at all (also the default under `motion=False`) |
218
+ | `TEXTUAL_COLOR_SYSTEM` / `--ansi` | the `tau-ansi` theme for 16-colour terminals |
219
+
220
+ Live markdown streaming needs ≥ 60 cols; two consecutive slow appends (a long open code fence)
221
+ switch the segment to plain text until it ends. Replies are coalesced to at most one update every
222
+ 80 ms.
223
+
224
+ ## Logs
225
+
226
+ While the interface runs the terminal belongs to Textual: details go to
227
+ `<TAU_DATA_DIR>/logs/tau.log` (for example `~/.local/share/tau/logs/tau.log`), the stderr
228
+ handler of the `tau` command is detached for the duration, and the chat shows a short notice
229
+ instead of a traceback.
230
+
231
+ ## Python API
232
+
233
+ ```python
234
+ from tau_tui import TauApp, build_app, resolve_resume, SessionNotFound
235
+ from tau_tui import TuiApprover, ApprovalScreen, Transcript, EventLog, rasterise
236
+
237
+ app = build_app(config, session_store=store, session_id=None) # builds the agent (channel "tui")
238
+ app.run()
239
+ ```
240
+
241
+ `build_app` creates one `EventBus` and one `TuiApprover`, builds the first agent with
242
+ `tau_core.build_agent(..., channel="tui")` and gives `TauApp` a factory
243
+ `agent_factory(session_id, *, role=None, seed_messages=None)` that builds every later agent
244
+ (`/new`, `/resume`, `/compact`, `/model`) on the same bus, store and approver. `role=None`
245
+ means `brain`, or the role a resumed `session_id` remembers from `/model`; an explicit `role`
246
+ is pinned for the run. Tests pass `model=FakeModel([...])`, `tools=[...]`, a fixed `clock`,
247
+ `tz=UTC`, `motion=False` and `show_durations=False`.
248
+ `resolve_resume(store, resume, language=...)` turns `--resume [ID]` into a session id and an
249
+ optional notice (`SessionNotFound` for an unknown id). `rasterise(cols)` is the τ mark as
250
+ half-block cells (`docs/brand/README.md`, "Terminal").
251
+
252
+ ## Development
253
+
254
+ ```sh
255
+ uv run pytest packages/tau-tui -q # pilot + snapshot tests, offline
256
+ uv run pytest packages/tau-tui --snapshot-update # after an intended visual change
257
+ ```
258
+
259
+ Snapshots live under `tests/__snapshots__/test_snapshots/` as `.raw` files: pytest-textual-snapshot
260
+ 1.0 with syrupy 6 writes SVG under that extension, and the extension is pinned in
261
+ `test_snapshots.py` (exactly one file per snapshot test, no glob). There are twelve: welcome at
262
+ 80x24, 50x24 and in Turkish, chat (a markdown list, a one-line fence and a rejected tier-2 card),
263
+ busy, approval, menu (all twelve rows) and picker at 80x24, wide 100x30 (busy chrome), ultra
264
+ 160x45 (a fence and the last turn's cards expanded), the events view at 50x24 and short 80x18. Every snapshot uses the fixed session id
265
+ `20260929-120000-abcd` (checked in every file), a fixed clock, UTC event times, no motion and no
266
+ durations, and is taken after the streams stopped; review a regenerated file before committing it
267
+ (it must never contain a local path, which the test also checks). The pilot tests read the widgets
268
+ through text oracles (`Transcript.text`, `EventLog.text`, `HintBar.text`, `HeaderBar.text`,
269
+ `ToolCard.text`) so behaviour changes do not churn the snapshots. The mark fixtures under
270
+ `tests/fixtures/` are the text previews of `rasterise(24)` and `rasterise(16)`; regenerate them
271
+ together with the pinned SHA-256 in `test_brand.py` when the geometry in `docs/brand/render.py`
272
+ changes.
273
+
274
+ License: MIT.
@@ -0,0 +1,247 @@
1
+ # tau-tui
2
+
3
+ The [Textual](https://textual.textualize.io/) terminal interface for tau: replies stream in as
4
+ markdown, tool calls are cards with their tier, approval and result, agent events show in a live
5
+ pane, tier-2 calls ask in a modal dialog, and the brand's τ mark opens every new session. One row
6
+ of chrome at the top, at most four at the bottom, so 80x24 over mosh on a phone stays usable.
7
+
8
+ It is its own distribution (`tau-tui`, import `tau_tui`), uses only the public API of `tau-core`
9
+ and plugs into the `tau` command through the `tau.commands` entry point (`tui`):
10
+
11
+ ```sh
12
+ pip install tau-tui # or, in this repo: uv sync --all-packages --group dev
13
+ tau tui # new session
14
+ tau tui --resume # continue the most recent earlier session
15
+ tau tui --resume ID # continue a given session
16
+ tau --home /path/to/tau --lang tr tui --profile travel
17
+ ```
18
+
19
+ Configuration comes from `tau.toml` and `.env` like every other channel (`TAU_HOME`,
20
+ `TAU_DATA_DIR`, `TAU_PROFILE`, `TAU_LANG`). Missing credentials, a broken config or an unknown
21
+ session end the command with a plain English message and exit code 2 before the interface opens.
22
+ The chrome (hints, labels, notices, the approval dialog) follows `[ui] language` (`en` or `tr`);
23
+ `/lang` switches it for one run.
24
+
25
+ ## Layout
26
+
27
+ Four bands by width (`-phone` < 60 cols, `-narrow` 60–99, `-wide` 100–139, `-ultra` ≥ 140) and
28
+ three by height (`-short` < 20 rows, `-mid`, `-tall` ≥ 30). The header is one row, the composer a
29
+ rounded box of one to five text rows, the hint bar one row.
30
+
31
+ 80x24 (`-narrow`), idle after a two-tool turn:
32
+
33
+ ```text
34
+ τ tau home · 20260929-120000-abcd brain · en
35
+ › Salon lambası açık mı? Açıksa kapat.
36
+ ● get_light_state(room="salon") tier 0 · 38ms
37
+ ⎿ {"on": true, "brightness": 80}
38
+ ● set_light(room="salon", on=false) tier 2 · approved · 412ms
39
+ ⎿ Light turned off
40
+ τ Salon lambası kapatıldı.
41
+ • Önceki durum: açık, %80 parlaklık
42
+ ✓ Worked for 4s · 2 tool calls · ctrl+o expand
43
+ ╭──────────────────────────────────────────────────────────────────────────────╮
44
+ │ ❯ Type a message… │
45
+ ╰──────────────────────────────────────────────────────────────────────────────╯
46
+ ? shortcuts · / commands · ctrl+j newline ctrl+q quit
47
+ ```
48
+
49
+ 100x30 (`-wide`): the events pane docks on the right (34 cols; 40 at `-ultra`), `ctrl+e` hides
50
+ it. 160x45 (`-ultra`): a read-only session sidebar (28 cols) on the left; clicking a row resumes.
51
+
52
+ ```text
53
+ τ tau home · 20260929-120000-abcd brain · 7 tools · en
54
+ › Salon lambası açık mı? Açıksa kapat. │ Events · 5
55
+ ● get_light_state(room="salon") tier 0 · 38ms │ 12:00:01 ● get_light_state
56
+ ⎿ {"on": true, "brightness": 80} │ tier 0 · ok · 38ms
57
+ τ Salon lambası kapatıldı. │ 12:00:02 ◆ approval set_light
58
+ │ tier 2 → approved (tui)
59
+ ```
60
+
61
+ 80x30 (`-narrow -tall`): an 8-row events strip under the transcript. Below 100 cols `ctrl+e` /
62
+ F4 swaps the transcript for the events log in the same body; the composer stays live and Esc
63
+ returns to the chat.
64
+
65
+ 50x24 (`-phone`, Termius over mosh): the header drops the session id (`/session` shows it),
66
+ tool badges move to the result line, the composer has no border, hints say `^j`, the welcome
67
+ card shows the 16-column mark, replies stream as plain text and settle into markdown when the
68
+ segment ends, and the approval dialog is full width with stacked buttons.
69
+
70
+ ```text
71
+ τ tau · home en
72
+ › Salon lambası açık mı? Açıksa kapat.
73
+ ○ set_light(room="salon", on=false)
74
+ ⎿ tier 2 · running
75
+ ✻ Running set_light…
76
+ ❯
77
+ esc cancel · ^j newline busy
78
+ ```
79
+
80
+ `-short` (< 20 rows): no hint row, the welcome mark is hidden, the composer grows to two rows.
81
+
82
+ ## Vocabulary
83
+
84
+ Every glyph is one cell wide and none is an emoji-presentation code point, so phone terminals
85
+ draw them at the right width: `›` you · `τ` tau · `●` finished tool call · `○` running or waiting
86
+ for approval · `⎿` result line · `✻` busy verb / reasoning · `✓ ◆ ✗` turn footer (finished /
87
+ limit, cancelled, interrupted / error) · `◆` needs you · `▸` notice · `❯` prompt.
88
+
89
+ State colours (deliberate; no green and no red in the palette, shapes double the colours):
90
+ **ok / success = Flare**, **error = Signal**, **rejected = Ember**, **running / pending = Core at
91
+ 55 %**, **attention = Flare**, notice = Ember. Rich renderables (the events pane) cannot take a TCSS
92
+ alpha, so they use the literal blends `theme.TEXT_MUTED` (Core over Void at 60 %, the live clock and
93
+ the running dot) and `theme.TEXT_DISABLED` (38 %, replayed clocks). The header `τ` breathes while the welcome card is
94
+ on screen, runs the comet while a turn is busy and is static Ember otherwise; nothing else in
95
+ the UI moves. The composer border is Ember, Signal while busy and Flare while an approval waits.
96
+
97
+ ## Keys and commands
98
+
99
+ | Key | Effect |
100
+ |---|---|
101
+ | Enter | send (or accept the highlighted menu row); while tau answers the text stays and a toast says so |
102
+ | ctrl+j | newline (shift+enter too where the terminal delivers it) |
103
+ | Esc | close the menu · cancel the running turn · leave the events view · scroll back to the bottom |
104
+ | ctrl+e / F4 | events: toggle the column (≥ 100 cols) or the view (< 100 cols) |
105
+ | ctrl+r / F2 | session picker (Enter resumes, `n` new, Esc closes) |
106
+ | ctrl+n | new session |
107
+ | ctrl+t / F3 | `/tools` |
108
+ | ctrl+o / F6 | expand or collapse the cards of the last turn: the arguments as JSON and the full result (a click toggles any card) |
109
+ | PageUp / PageDown, F7 / F8 | scroll the transcript; PageUp at the top of a folded history loads 20 older turns |
110
+ | F9 | back to the bottom |
111
+ | ↑ / ↓ | recall sent prompts (single-line composer); navigate the menu while it is open |
112
+ | `?` or F1 | help (only in an empty composer; `?x` is sent as a message) |
113
+ | ctrl+q | quit (ctrl+c copies a selection in the composer; without one it only says how to quit) |
114
+
115
+ Typing `/` into an empty composer opens a fuzzy menu of the slash commands (`↑↓` pick, Tab
116
+ complete, Enter run, Esc close). An exact name ranks first, and a command typed in full runs as
117
+ typed even when the menu has not caught up with a burst of keys (a paste, mosh), so `/session`
118
+ never runs `/sessions`. The commands are the shared spec of `tau_core.commands`, with
119
+ the same wording as `tau chat`:
120
+
121
+ | command | effect |
122
+ |---|---|
123
+ | `/help`, `/tools`, `/sessions`, `/session`, `/lang en\|tr` | as in `tau chat` |
124
+ | `/resume [n\|id]`, `/new` | switch sessions (see below) |
125
+ | `/compact [focus]` | summarize this session with its own model and continue in a new session seeded with the summary; the old session is kept |
126
+ | `/clear` | empty the transcript (turns, welcome card, info cards, the fold); the session and the agent go on, `/resume` or `--resume` show the history again |
127
+ | `/status` | an info card: profile, session, model, context, tools, language, versions, hub |
128
+ | `/model [role]` | list the roles of `tau.toml`, or switch this session to one; the header follows |
129
+ | `/quit` (alias `/exit`) | leave |
130
+
131
+ The Turkish aliases (`/devam`, `/çık`, `/özet`, `/temizle`, `/durum`, `/rol`, …) keep working
132
+ but are never listed; the menu shows the twelve canonical names and every row is on screen at
133
+ 80x24 (in the `-short` band it scrolls). `/new`, `/resume`, `/compact`, `/clear` and
134
+ `/model <role>` wait for the current turn (a toast says so); `/status` and `/model` without an
135
+ argument run any time.
136
+
137
+ ## Approvals
138
+
139
+ `TuiApprover` (`name = "tui"`) is the channel's `Approver`. A tier-2 call opens `ApprovalScreen`:
140
+ tool, effect, the arguments as pretty JSON and the buttons **Approve** / **Reject**.
141
+
142
+ - `y` or `e` approves, `n`, `h` or Esc rejects; focus starts on **Reject**, so Enter rejects, and
143
+ Tab only moves between the two buttons (PageUp/PageDown scroll the arguments). The keys work in
144
+ both languages whatever the UI setting.
145
+ - `y`/`e` and **Approve** only work 0.5 s after the dialog opens (the hint reads `(arming…)`
146
+ meanwhile), so keys typed for the chat cannot approve by accident.
147
+ - Anything other than an explicit approval rejects: a dialog closed another way, an approver
148
+ with no running app, or a cancelled wait (the dialog is taken down so it cannot approve later).
149
+ - Rejecting does not cancel the turn: the model reads the refusal and answers.
150
+
151
+ The dialog is translucent: the chat stays visible behind it. Behind it the tool card reads
152
+ `○ set_light(…)` + `⎿ ◆ waiting for your approval`, the composer and the header `τ` turn Flare
153
+ and the hint bar says what to press. Nothing in this package approves on its own.
154
+
155
+ ## Sessions and events
156
+
157
+ `/sessions` lists the ten most recent earlier sessions with messages (numbered for `/resume n`,
158
+ same order as `tau chat`); `/resume` alone resumes the most recent one. Resuming rebuilds the
159
+ agent on the stored session (the previous one is closed), renders the history from the stored
160
+ messages (user rows, markdown replies, tool cards with the recorded duration, approval and
161
+ result), replays the stored agent events into the events pane before live ones, and keeps the
162
+ current agent when the rebuild fails. The last 40 turns stay live; older ones fold into one
163
+ line and PageUp at the top pages them in. One `EventBus` serves the app for its lifetime; events
164
+ stamped with another session id are ignored.
165
+
166
+ `/compact [focus]` asks the agent's own model for a continuation summary, opens a new session
167
+ whose history starts with that summary, links the two sessions in their metadata
168
+ (`compacted_from` / `compacted_to`) and keeps the old one untouched; the transcript is reset
169
+ like `/new`, then shows the summary as an info card and the notice `Compacted N messages into a
170
+ summary. New session <id>; the previous session <old id> is kept.`, the header shows the new id
171
+ and the events pane starts empty. The summary is written in a worker, so the app stays
172
+ responsive: the chrome is busy (`✻ Summarizing…`, `esc cancel` in the hint bar), read-only
173
+ commands such as `/status` still answer, turns and session switches are refused, and Esc
174
+ cancels the summarizer (`Compaction cancelled; the session continues.`). A failure (nothing to
175
+ summarize, a model error, missing credentials) is a notice and the current session goes on.
176
+ The details are in tau-core's README under "Compaction".
177
+
178
+ `/model fast` rebuilds the agent on the same session with that role (the transcript stays,
179
+ the header and the welcome card follow) and remembers the role in the session's metadata, so
180
+ `/resume` and `tau tui --resume` come back on it; `build_app(role=...)` pins a role for the
181
+ whole run instead. An unknown role, missing credentials or an unavailable provider leave the
182
+ current agent working and say why.
183
+
184
+ ## Knobs
185
+
186
+ | Setting | Effect |
187
+ |---|---|
188
+ | `TAU_TUI_ASCII=1` | ASCII vocabulary (`> t * o L * + x ! - >`), `ascii` box borders on every screen (the screens carry an `-ascii` class that `theme.ASCII_CSS` keys on), an Ember-only mark |
189
+ | `TAU_TUI_PLAIN_STREAM=1` | stream every reply as plain text and parse the markdown once per segment |
190
+ | `TEXTUAL_ANIMATIONS=none` | no header motion at all (also the default under `motion=False`) |
191
+ | `TEXTUAL_COLOR_SYSTEM` / `--ansi` | the `tau-ansi` theme for 16-colour terminals |
192
+
193
+ Live markdown streaming needs ≥ 60 cols; two consecutive slow appends (a long open code fence)
194
+ switch the segment to plain text until it ends. Replies are coalesced to at most one update every
195
+ 80 ms.
196
+
197
+ ## Logs
198
+
199
+ While the interface runs the terminal belongs to Textual: details go to
200
+ `<TAU_DATA_DIR>/logs/tau.log` (for example `~/.local/share/tau/logs/tau.log`), the stderr
201
+ handler of the `tau` command is detached for the duration, and the chat shows a short notice
202
+ instead of a traceback.
203
+
204
+ ## Python API
205
+
206
+ ```python
207
+ from tau_tui import TauApp, build_app, resolve_resume, SessionNotFound
208
+ from tau_tui import TuiApprover, ApprovalScreen, Transcript, EventLog, rasterise
209
+
210
+ app = build_app(config, session_store=store, session_id=None) # builds the agent (channel "tui")
211
+ app.run()
212
+ ```
213
+
214
+ `build_app` creates one `EventBus` and one `TuiApprover`, builds the first agent with
215
+ `tau_core.build_agent(..., channel="tui")` and gives `TauApp` a factory
216
+ `agent_factory(session_id, *, role=None, seed_messages=None)` that builds every later agent
217
+ (`/new`, `/resume`, `/compact`, `/model`) on the same bus, store and approver. `role=None`
218
+ means `brain`, or the role a resumed `session_id` remembers from `/model`; an explicit `role`
219
+ is pinned for the run. Tests pass `model=FakeModel([...])`, `tools=[...]`, a fixed `clock`,
220
+ `tz=UTC`, `motion=False` and `show_durations=False`.
221
+ `resolve_resume(store, resume, language=...)` turns `--resume [ID]` into a session id and an
222
+ optional notice (`SessionNotFound` for an unknown id). `rasterise(cols)` is the τ mark as
223
+ half-block cells (`docs/brand/README.md`, "Terminal").
224
+
225
+ ## Development
226
+
227
+ ```sh
228
+ uv run pytest packages/tau-tui -q # pilot + snapshot tests, offline
229
+ uv run pytest packages/tau-tui --snapshot-update # after an intended visual change
230
+ ```
231
+
232
+ Snapshots live under `tests/__snapshots__/test_snapshots/` as `.raw` files: pytest-textual-snapshot
233
+ 1.0 with syrupy 6 writes SVG under that extension, and the extension is pinned in
234
+ `test_snapshots.py` (exactly one file per snapshot test, no glob). There are twelve: welcome at
235
+ 80x24, 50x24 and in Turkish, chat (a markdown list, a one-line fence and a rejected tier-2 card),
236
+ busy, approval, menu (all twelve rows) and picker at 80x24, wide 100x30 (busy chrome), ultra
237
+ 160x45 (a fence and the last turn's cards expanded), the events view at 50x24 and short 80x18. Every snapshot uses the fixed session id
238
+ `20260929-120000-abcd` (checked in every file), a fixed clock, UTC event times, no motion and no
239
+ durations, and is taken after the streams stopped; review a regenerated file before committing it
240
+ (it must never contain a local path, which the test also checks). The pilot tests read the widgets
241
+ through text oracles (`Transcript.text`, `EventLog.text`, `HintBar.text`, `HeaderBar.text`,
242
+ `ToolCard.text`) so behaviour changes do not churn the snapshots. The mark fixtures under
243
+ `tests/fixtures/` are the text previews of `rasterise(24)` and `rasterise(16)`; regenerate them
244
+ together with the pinned SHA-256 in `test_brand.py` when the geometry in `docs/brand/render.py`
245
+ changes.
246
+
247
+ License: MIT.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "tau-tui"
3
+ version = "0.2.0"
4
+ description = "Textual terminal UI for tau: streamed markdown replies, tool cards, live events and in-TUI approvals."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ dependencies = ["click>=8.1", "rich>=13", "tau-core>=0.1.0", "textual>=8.2,<9"]
9
+ keywords = ["tau", "agent", "assistant", "tui", "textual", "terminal", "cli"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Environment :: Console",
13
+ "Intended Audience :: End Users/Desktop",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: MacOS",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Home Automation",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/fport/tau"
24
+ Documentation = "https://docs.tau.getporti.com"
25
+ Issues = "https://github.com/fport/tau/issues"
26
+ Changelog = "https://github.com/fport/tau/releases"
27
+
28
+ [project.entry-points."tau.commands"]
29
+ tui = "tau_tui.cli:tui"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/tau_tui"]
@@ -0,0 +1,35 @@
1
+ """tau-tui: the Textual terminal interface for tau.
2
+
3
+ Public API::
4
+
5
+ TauApp, build_app, resolve_resume, SessionNotFound
6
+ TuiApprover, ApprovalScreen
7
+ Transcript, EventLog, rasterise
8
+ """
9
+
10
+ from importlib.metadata import PackageNotFoundError, version
11
+
12
+ import tau_tui.strings # noqa: F401 (registers the tui.* message keys)
13
+ from tau_tui.app import SessionNotFound, TauApp, build_app, resolve_resume
14
+ from tau_tui.approval import ApprovalScreen, TuiApprover
15
+ from tau_tui.brand import rasterise
16
+ from tau_tui.events import EventLog
17
+ from tau_tui.transcript import Transcript
18
+
19
+ try:
20
+ __version__ = version("tau-tui")
21
+ except PackageNotFoundError:
22
+ __version__ = "0.0.0"
23
+
24
+ __all__ = [
25
+ "ApprovalScreen",
26
+ "EventLog",
27
+ "SessionNotFound",
28
+ "TauApp",
29
+ "Transcript",
30
+ "TuiApprover",
31
+ "__version__",
32
+ "build_app",
33
+ "rasterise",
34
+ "resolve_resume",
35
+ ]