Imported from zeroaltitude/theseus (
crates/theseus-tui/AGENTS.md). Install upstream withnpx skills add zeroaltitude/theseus --skill theseus-tui. Copyright stays with the author.
theseus-tui
theseus-tui, the terminal UI (design stage2 §2.9; theseus-7yx, steps 10b to 10f): every session of a running
theseusd in one sidebar, with its task trees; the queue of what needs you, answered inline; and the session in
focus, with its history and its input line. A client like the CLI, over the same socket: it links theseus-protocol
and the CLI's library (theseus_client), never the core. An installed binary of its own (tool in its manifest),
which theseus tui execs.
Key modules: run.rs (the loop), app.rs (no I/O), board.rs, ui.rs, and chat/ (the inline conversation). Read by: theseus tui and theseus chat (and bare theseus on a terminal), which exec it.
What's here
-
src/main.rs: the arguments (--socket,--notify), raw mode and the alternate screen, the signals that end the loop as a quit does (ending_signals: SIGTERM, SIGHUP, SIGINT and SIGQUIT), and the loop's start. -
The daemon's own work (theseus-jn17m): at each connect the TUI asks
work.watchforsystemalone (Purpose::System), keeps theTheseusroot's views by the position rule fromwork.changed, and the header says the root closed (⚙ importing 41/120, else⚙ Theseus · 2 done, 1 failed:work_system::collapsed). A system job's failure or long end (an Inform) is a line in the footer, never a ring. -
src/term.rs: the TUI's own terminal modes, focus events and bracketed paste: on when the loop starts, off on every way out (a quit, an error, a signal, and a panic's hook) (theseus-8hcg). -
src/run.rs: the loop. One connection, read in oneselect!with the terminal's events (read on a thread of their own) and one deadline; reconnecting with backoff; the frame clock. -
src/app.rs: the state and every change to it (keys, the daemon's messages, answers). It does no I/O: it returns the requests to send. -
src/horizon.rs(theseus-nuhsz): the Horizon section under the task tree (ui::sidebargives it up to a third of the sidebar's height, none while nothing is ahead). The TUI sendswork.watch {limit: 0}at each connect (and afterevents.lostnames theworkstream), keeps the future views by the position rule fromwork.changed, and drawstheseus_client::render::horizon's compact rows.Hgives the section the keys (Mode::Horizon):j/kmove,zsnoozes a reminder (wake.snooze),ppauses or resumes a reminder or a routine,rruns a routine now,xcancels a reminder, a watch or a deferred task (a routine says to pause it ortheseus routine rm), enter opens a routine's conversation, esc/H/tab leave; a key that does not fit the row says so and sends nothing. No timer of its own: the local midnight moves a reminder from This week to Today at the next draw. Tests:src/tests_horizon.rs. -
src/new_task.rs(theseus-7oq0w):Nopens a one-line prompt for a brief; enter sends onetask.startunder the conversation the cursor is on (Home when it is on a task or on nothing), with no model turn, and esc starts nothing.Mode::NewTask,App::brief,Purpose::NewTask. Tests:src/tests_new_task.rs. -
src/board.rs: the board. Views applied by the position rule, titles, the questions, the rows and trees, and the queue. -
src/work.rs(theseus-k2vca): whatwork.watchsays of each session (the position rule, the jobs under them) and the words of a sidebar row:job 2:14 of usual 2:40 · said,model 0:31,asks: …,failed 15:13: why,pausedand the plan'sstep 3 of 5first, the spend against the usual ($0.40 of ~$1) and the time left last (theseus-ypnxt.2, theseus-31pgz); the title is cut before the doing.executions.watchstays the board's feed (the questions, reminders andconfirm.resolvedride it, notwork.watch); this is the overlay that words the rows, and a daemon withoutwork.watchleaveslivefalse so rows say their labels as before. A row's words are used only when the work view's level equals the board's. A finished task's first report line shows under its row (◆until seen). The header counts● needs you · ◐ working · ✗ failed · ✓ done; the window title counts needs-you alone (theseus ●1; a failure is counted apart). Elapsed times repaint on the wall minute (one deadline inApp::deadline), never each second. -
src/ui.rs: drawing into a ratatui buffer. Nothing here changes the app. -
src/styled.rs(theseus-rn7s): the pane's rows. A run ofReplylines is one reply, drawn whole with the CLI's markdown renderer (theseus_client::render::md) at the pane's width and turned into ratatui spans here; every other line wraps as before. A history reply is a header line ([time] model:,Plain) and its text asReplylines. The reply streams in raw lines and is redrawn at each frame, so a half-typed**shows as typed until it closes. Test:src/tests_markdown.rs. -
src/follow.rs(theseus-e1hu4): the follow pane.lon the session under the cursor finds its running job (action.list {execution_id, unsettled}), peeks it (work.peek) and follows from the peek's end (action.output); chunks and the end arrive as notifications andescsendsaction.unfollow.Followis the pane's pure state (a bounded tail of lines, an open line, the next byte expected, a gap said as-- N bytes skipped --);events.lostnaming the job's stream, and a reconnect, peek and follow again. Its header saysjob 0:05 of usual 0:31. The sidebar's own row words aretui-rows'. Tests:src/tests_follow.rs. -
src/board_threads.rs(theseus-5i8i): the threads on the board (App.board_threads;src/threads.rsis thread-cli-tui's picker). Fed by tui-rows' onework.watch(a connection holds one), views andThreadGroups applied by the position rule, and the sidebar's Threads block overtheseus_protocol::group_threads: a fold-able group per live thread (▾/▸, the one header wording the CLI shares), its open work under it, a pending seal as a needs-you line.Tgives the block the keys (up/down, enter folds a thread or opens an item's session, esc leaves). A daemon that refuseswork.watchcosts no word and no block. Tests:src/tests_board_threads.rs. -
src/detail.rs(the focused session's history and events, as the CLI'srenderlines),src/card.rs(a question's card),src/notice.rs(a re-export: the notices, their delivery and the toast aretheseus_client::notify, shared withtheseus status --watch; the TUI keeps the viewer inapp.rs).--notifyisauto(default: a Windows toast under WSL, OSC 9 where known, else the bell),toast,bell,osc9,osc777oroff; without the flag the daemon's[tui] notify(read fromhealth) says (theseus-f0bi9). Done until seen istheseus_client::seen, shared with the CLI. -
src/selfview.rs(theseus-pw1q.19): the self view,L(lis the follow pane's): a pane over the whole body with the switch's state in the CLI's words (render::self_state_line), "Self-improvement is off. Nothing has run." while[self] modeis off, the day's and the newest branch's spend (SelfLogResult.budget, wide screens only), and the last 20self.logrows, newest first, one line each (time, kind, what, cost, undo); enter shows the picked row whole, esc goes back, then closes. A screen under 100 columns folds to the state and the newest five. It readsself.logwhen it opens and again on an event it already hears (an execution that changed state, a task, a question), one read at a time and a second wanted meanwhile made when the first lands: no timer. The switch's move from any surface isself.changed; a row with no execution beside it (a night's exam pass) shows at the next such event or when the pane is opened again. Tests:src/tests_self.rs. -
A reply's citations (theseus-ejdk):
Detail::historydraws a page's cited replies with numbered marks and the footnote block under them (session.history'scites, the CLI'srender::cite), andDetail::citeddoes the same for a turn this TUI sent, from itsturn.submitanswer'scites; a reply another surface's turn streams in stays raw until the history is read again. Tests:src/tests_cite.rs. -
The owner's standing records (theseus-slvl.4;
src/standing.rs): a📌 Npill in the header, the live records that hold in the focused session on this surface (standing.list {session_id, surface: "terminal"}: asked when a session is focused, after a turn of this TUI's, at a reconnect and whenpopens the list), andp, a box over the body with a line a record (id, kind, scope, text) that any key closes. Nothing polls; an older daemon's refusal says nothing. Tests:src/tests_standing.rs. -
src/task_return.rs(theseus-mu0e6): inspect, join and return for a task's pane. A task's header is a breadcrumb (task a1b2c3 of <parent> · p parent · I inspector);por Backspace focuses the parent conversation (in a conversationpis still the standing records' key);Iasksturn.inspectfor the pane's newest turn (Detail::turn) and prints the link; the input line's prompt saystask a1b2c3 reads it at its next step>and a sent line adds↪ the task reads this at its next step. A message for a task that has reported (ended_task, bytheseus_protocol::work::joinable), or one the daemon refuses because the task ended as it left (Purpose::SubmitTask), goes to the parent withtask_redirect_line, and the pane follows it. Tests:src/tests_task_return.rs.
The chat (theseus-cyha)
theseus-tui chat [SESSION] is a second mode of this binary: an inline conversation in the current directory, which
bare theseus on a terminal and theseus chat [SID] exec (the CLI's chat_cmd.rs; a pipe or any argument keeps the
usage text and exit 2). It is inline and append-only: a finished line is written once into the terminal's own
scrollback (so search, copy and scroll work as always, in tmux too), and only the dock at the bottom is ever redrawn.
It uses no ratatui Terminal: chat/screen.rs writes the bytes (cursor up to the dock's first row, erase down, the
new lines, the dock again, in synchronized-output mode 2026).
-
chat/state.rs,events.rs: the state and every change to it, no I/O (asapp.rs). A key, a paste, a daemon message or the clock goes in; requests come out asEffects and finished lines inChat::out. A turn's events (the session's alone, whoever sent the turn) become dock rows while they run and one scrollback line when they end; a turn is ended once however it is told (turn.endedorturn.submit's answer). -
chat/keys.rs: the one key map (Action).chat/commands.rs: the one command registry (COMMANDS;/help,/quit; a debug build also has/panic, the test that proves the panic hook). A later row adds an entry and its handler in a module of its own./lists them, filtered as you type. -
A turn's end prints
inspect <url>under its line when the result carries a link (theseus-x9swu), and/inspectsays the last one again (Chat::last_inspect). -
chat/dock.rs: the dock's rows, from provider functions (ABOVE_INPUT: the live line, calls in flight, a question, a notice) a later row adds to (cli-tasks puts its task rows there), the input, the command list, and the status line as a list ofSegments (mode · directory · model · cost; the lowestkeepgoes first when it is too wide). -
chat/rewind.rs(theseus-23rk): Esc Esc on an empty line while no turn runs (Chat::rewind_escape; a single Esc during a turn is chat-ask's stop and is not bound) asksfiles.changesand opens the picker in the dock (rewind::rows, a provider inABOVE_INPUT): each of your messages with its time andtides.py +1 −1orno file changes; Enter, thenf(files.restore, thensession.forkatfork_at),c(fork only) oro(restore only), Esc never mind. After a fork the chat goes on in it: the screen is cleared (screen::CLEAR, a finished line written raw), the fork's history is read and shown, and the message is back in the input. The first message has no node before it, sofandcrefuse it. Tests:chat/tests_rewind.rs. -
chat/render_seam.rs: the one place a reply and a tool call are worded:reply_linesis term-render's markdown renderer,tool_linetool-words'summarize; nothing else renders text. -
chat/editor.rs: the multi-line input (Ctrl-J, Alt/Shift-Enter,\then Enter), this directory's history (a file of its own under$XDG_STATE_HOME/theseus/history/, named for the directory's last component and an FNV-1a hash of its path,chat/env.rs; a directory with no file yet reads the one file of before,chat-history.json, and never changes it), and the folded paste: more than 5 lines is one chip (a private-use character in the buffer, written back whole bytake), so the whole message is oneturn.submit. -
chat/run.rs: the loop, asrun.rs: quiet by construction (a deadline only while something runs: the dock's clock at 10 Hz, the banner's 250 ms wait, a notice), at most 30 frames a second.chat/guard.rs: raw mode and bracketed paste on, put back on every way out; on a panic the hook alone does it (Dropskips it, as a release build's abort would). -
It does one
session.open({dir}) and onesession.watchat start (andsession.list {n: 100}for the banner's last conversation here);theseus chat SIDreadssession.listto find the session, thensession.historyonce. A message typed before the session opens is held and sent once it does.session.typinggoes once per idle spell. -
chat-ask (theseus-wy6v.1):
chat/ask.rsshows a question in the input's place (the call in words, the hunk throughchat/diff.rs, who asks, the expiry, the keys ofkeys::ASK_KEYS, one table: y n v t a e) and answers withaction.confirm, authortheseus chat; the keys work only with the line empty and the focus on the question (Esc goes back to typing, Tab back to it).chat/stop.rs: Esc during a turn sendsexecution.stopwithkeep_jobs(jobs that went to the background run on) and the transcript says what was verified.chat/queue.rs: a message typed while a turn runs waits in the dock, ↑ pulls it back, Ctrl-Enter stops and sends; it goes when the turn ends (steer-queued will deliver it into the turn: onlyrelease_queuechanges). Tests:chat/tests_ask.rs. -
Coming back (theseus-6yka).
theseus -c(theseus-tui chat -c) askssession.list {dir, n: 50}and continues the directory's most recently active conversation (chat/reopen.rs:last_here: a conversation, with a turn, not retired,started_onnotdiscord, so never a task's, a DM's or a shared place's); with none it says so and opens a new one.theseus -r(chat -r) askssession.list {dir, n: 200, with_text}once and shows the picker (chat/picker.rs, in the dock in place of the input): typing filters the list in memory (every word of the query in the title, labels, directory, or the words of the first and last message and of the newest prompts; no request, no model), up/down choose, Enter continues it, Tab reads and shows every directory's (session.list {n: 1000, with_text}, once), Esc clears the query and then opens a new conversation, Ctrl-D leaves. The chosen conversation'ssession.history {n: 500}is replayed through the transcript's own lines (events.rs::history): the last 30 turns (reopen::window; a message over 6 lines is shown folded, its last lines kept), older turns one line,N earlier turns · /history shows them, which/historyanswers a page back (beforethe first replayed node). Leaving printstheseus -candtheseus chat END. Tests:chat/tests_reopen.rs,picker.rs's own. -
Tasks (theseus-mnd9g;
chat/tasks.rs,chat/footer.rs; the questions join chat-ask'schat/ask.rs): onework.watch {roots: [session], interactive: true}at open and at each reconnect. The chat keeps the conversation's views by the position rule; a task that starts, asks, reports or fails prints one titled line in the words of the CLI'srender::task_tree::event_line, and a snapshot prints none for what already runs. The footer is a dock provider above the question: a summary line, then a row for each task that works or asks (5 at most, then+N more · ctrl-t), with minute clocks (task_tree::footer,coarse), so it changes only on a push or the minute a clock turns over (Board::repaint_atis the chat's one deadline for it; the expiry of the question that has the keys ticks each second while it shows). One question model (ask.rs,Asked): the conversation's own (aConfirmRequest, answered byaction.confirm) or a task's (tasks::TaskQfrom the board's views, answered bywork.answer, authortheseus chat, refused inside a job); the dock draws either with the same rows (a task's title says who asks), and the same keys (keys::ASK_KEYS) answer either:y,nthen an optional note,t,v, and1..6anaskquestion's choices (text for any other question). The conversation's own comes first, then the tasks', oldest asked first; Tab with the line empty gives the keys back after Esc, else moves them to the next question (AskState::focus). Esc is chat-ask's: back to typing./tasksdraws the tree in place (ctrl-t is the picker, below),/cancel IDistask.cancel,/stop [--tasks]is Esc's stop (keep_jobs) and with--tasks(orall) also cancels the conversation's tasks (refused in a job). An Interrupt notice (work.noticethroughnotices::deliver) rings the bell once (Effect::Bell); the status line's◐ N tasksis the board's count once it is live. Tests:chat/tests_tasks.rs,chat/tests_ask.rs. Gaps closed (theseus-xock5): a footer row and a tree row end with the task's progress line (paused · step 3 of 5 · $1.20 of ~$3 (limit $5) · ~6 min left,theseus_protocol::work::progress_line, theseus-ypnxt.2), and the task view's header carries it too;/taskslists a routine, a watch and a project as rows (routine · next Fri 09:00,watch · …,project · …) that count as no task; and the chat reads the machine's seen file (Cfg::seen_path,chat/welcome.rs): a chat that opens after an absence of a minute or more, or whose connection comes back after one, askswork.digestsince the last look and tells one line under the banner (◆ 2 reported since 14:02 · theseus digest; nothing on a first start, whose first board is the look); the look is written when the digest lands and on leaving. A notice'sViewertakesseenfrom the file's displayed position of the work (a question shown by another client does not ring here) andaway_msfrom the quiet of this terminal and of the machine's last look. The chat never writes a displayed mark itself: its own told line would silence its own bell. Tests:chat/tests_welcome.rs. -
Inspect, join, return (theseus-rrkb7;
chat/task_view.rs,chat/task_picker.rs): ctrl-t opens the task picker in the dock (↑↓, Enter inspects,jjoins,ctwice cancels,agives the keys to the task's question, Esc closes;/tasksstill prints the tree),/task [ID],/join ID,/back,/inspect [ID],/follow. A shown task is a view, never stored:Chat::sessionbecomes the task's (so its history, turns and calls come through the conversation's code,session.historythensession.watch), the conversation's own state waits in the view'sStashand its events are only counted (Away), and the shared viewer'sfocusedis the task's session. Inspect is read-only (jjoin,ithe inspector's link,ftask.retryonce it has ended, on an empty line); join sends each line asturn.submit {session_id: <task's>}at once, never queued (the daemon steers it), the prompt reads1abc1f ›. Esc on an empty line, or/back, restores the stash, reads the conversation again and says── back in main · 1abc1f still running · main: 1 reply while you were away ──; in a task's view Esc never stops anything and/stopcancels the task after asking once. A task that ends while shown is told once (🏁), the prompt becomesmain ›, and a line typed goes to the conversation behindtask_redirect_line(the one line every surface says); a line the daemon refuses asexecution_*or not found (it ended after the view last heard) is redirected the same way, never lost.theseus chat --task ID [--inspect](Cfg::task) lists the sessions, finds the task and itsparent_session_id(the conversation to go back to), and opens the view without replaying the conversation first. Header clocks arecoarseminutes (view_repaint_atis the one deadline). Tests:chat/tests_task_view.rs(goldentests/golden/chat_task_view_100.txt). -
Start, pause, search (theseus-ypnxt.4;
chat/task_new.rs,chat/task_picker.rs,new_task.rs):/task new BRIEFand the typed wordsmake that a task(the whole message, case blind; with/task newalone) sendtask.startwithhome= the conversation, no model turn, the brief the owner's last message (Chat::last_said, never the model's reply), and print◐ started <title> (<short>) · /task <id> to look, /join <id> to talk to it;/pause [ID]and/resume [ID]sendtask.pause/task.resume(no id: the task shown, else the only one they could change, else they list the candidates and send nothing). In the TUIPholds the task under the cursor or lets it go on. The ctrl-t picker readstask.list {session_id}once as it opens (its rows: the board's, then the ended ones the board lacks) and typing searches title and report line;j/c/astay commands until a query starts (/starts one). Tests:chat/tests_task_new.rs,tests_new_task.rs. -
Modes (theseus-lyxy): Shift+Tab asks the daemon for the conversation's next mode (
session.mode, ask → edits → plan → auto); in the chat it is the key map'sCycleMode, in the board it works at the input line only (i), since in the list Shift+Tab is still the walk back through the queue. The mode and the plan are read once when a conversation is opened or focused (session.modewithout a mode) and kept by thesession.modenotification; the status line saysedits (shift+tab)andplan 2/5(the board's pane header too), the checklist goes into the transcript each time it moves (a notification whose plan changed, never one that only repeats it), and a plan's question shows its numbered steps and takesefor the edits-mode yes: chat-ask's one key table (keys::ASK_KEYS) haseasAsk::Grant, and the plan's grant offer is the question's own. The plan is the daemon's plan items (session.mode'splan), the same the work board counts. Tests:src/tests_mode.rs,chat/tests_mode.rs.
Tests: chat/tests.rs (a scripted daemon through Conn::over, its bytes read back as a screen by vt100, a dev-dependency
only; the paused clock and the chat's wall clock move together; goldens tests/golden/chat_80.txt, chat_120.txt,
rewritten by THESEUS_GOLDEN=write), and tests/chat_pty.rs (the real binary on a pseudo-terminal against a fake daemon
on a Unix socket: the first screen, a bracketed paste of 60 lines as one message, and the terminal put back after SIGTERM,
SIGHUP, a panic and Ctrl-C twice). vt100 can read back only one screen of scrollback: keep a test's transcript within it.
-
A question's card shows the call in words and the daemon's
preview(card::preview, at most 12 rows ofdiffview::preview_lines); the pane shows tool lines in words, a failed command's last lines and a finished edit's diff (render::Flight). The changed words of a diff line areMARK_ON/MARK_OFFin its text:ui::putdraws them on a background of their own andui::wrapcarries a mark over a cut. Tests:src/tests_diff.rs. -
src/threads.rs(theseus-j0yh): the current thread, the header's status line (only once a thread is known), Ctrl-T's picker (type to filter;prd(twice)aact while the filter is empty,/types them; Enter switches),/tin the input line, a thread's session tagged→ titlein the pane header. Requests:thread.liston opening the picker, at a connect with a thread current, and after a reply in a thread; keys in the picker send none but the acts. Tests:src/tests_threads.rs. -
Since you last looked (theseus-qkoi3): while the terminal has focus, each view the board takes moves the machine's look (
Seen::look, written with the seen file); a blur writes it. At each connect, and when focus returns afterAWAY_MS(60 s), the app askswork.digest {since}(never on a first start) and shows the summary as the footer's notice line, then moves the look to the digest's end. A daemon that does not know the method is not complained of. Tests:src/tests_digest.rs.
Invariants
- The protocol is the only way in. What needs you is each view's
attention, which the daemon computes; an answer isaction.confirm, a stopexecution.stop. - Quiet by construction. No timer runs but the deadlines the app asks for. It redraws on events, at most 30 frames a second.
- The position rule. A view applies only if its position is greater than the last one applied for its session. The first snapshot after a connect is the truth for every session no event has updated on the new connection: a daemon restarted on another store may have lower positions.
- What you have seen is the client's, on this machine, never the server's:
$XDG_STATE_HOME/theseus/seen.json, one file per machine shared with the CLI, whosehistory,watch, andconfirmrecord what they showed (theseus-yus0). A save merges by the greatest position and replaces the file atomically; the TUI takes up what the CLI recorded at each save.tui-seen.jsonis read when the new one is absent. theseus tuipasses its--socketfirst, and the last--socketwins, so one typed aftertuiis kept.
Tests
src/tests.rs: the TUI over a scripted daemon (a JSON-RPC stream on a duplex, throughConn::over), drawn on ratatui'sTestBackend, whose buffer each test reads as text. Run them ascargo nextest run --workspace -E 'package(theseus-tui)'.src/tests_failure.rs(theseus-7zgsm): a failed task's card ([r] retry [d] dismiss [o] open),randdon the failed row under the cursor sendingtask.retryandexecution.dismiss(anywhere else they still filter ready and done), a refusal said on the card, and the row leaving needs-you when the push says dismissed.src/tests_answer.rs(theseus-ds5os): the answer keys overwork.answer(a budget question'sysendsreset; a question that saysmay_answer: falsesends nothing and says where to answer; aconfirm.resolvedfrom another surface clears the card).src/tests_answer.rsalso holds a question with options (owner_ask, theseus-j7cje): its card lists the options (card.rs), the keys 1 to 6 sendchoice:<i>throughwork.answer, andyandnsend nothing.src/tests_notice.rs: the policy's notices on the harbour rig (a question rings once with its words, spend rings nothing, a wake on a root rings once, a burst rings once). Push a view,settle, then moveNOW: a view the loop takes after the clock moved is due a second later than the test expects.src/tests_names.rs(theseus-0n1v): a task named by its title in the tree, its notice and the arm prompt; a session whose first answer lacked its name asked again as its turns grow; the input line'sprofileandcarried; Enter after a filter. The rig pins the CLI's time zone (render::time::pin_for_tests).src/tests_paste.rs: a paste is one event, never keys: text in the input line, sent on enter; elsewhere it opens the input line and answers, quits, stops, and arms nothing; with no session open it is dropped, never sent to a later one. Enter at start opens the first row. Bracketed paste is off on every way out, each of the four signals sent to the test's own process among them, read from the bytes the loop wrote (theseus-8hcg).src/tests_order.rs: the app alone, its order forced by hand: another surface's message lands in the place itsnode.writtenmarked, above a reply that streamed while it was read (theseus-v6yc); and a fired reminder (wake.noticed, theseus-8ux6x) rings once with the policy's words (notices::reminder_notice), a check-in rings nothing, and--notify offrings nothing.src/tests_stop_all.rs(theseus-cdcwr):Sasks once more (Soryconfirms, any other key keeps the conversation) and sendsexecution.stopwithtasks;sis unchanged.src/tests_typing.rs(theseus-tnky): the input line's first key sends onesession.typingper idle spell (not one a key, none from the filter's keys), and an older daemon's refusal says nothing.src/tests_work.rs(theseus-k2vca): the regatta (a job, a model, a tool, a question, a failure) at 80 and 120 columns, the minute tick and the countdown on the paused clock, header and title counts, a report under its row, the old daemon's fallback, andevents.loston the work stream.src/tests_standing.rs(theseus-slvl.4): the pill counts the records the daemon says hold in the open session (asked for its session and theterminalsurface), none shows no pill, an older daemon's refusal says nothing,plists them by kind and scope, and with no session open it says so.src/tests_sync.rs(theseus-pjmj7):healthis read at each connect (Purpose::Health) and the OpenClaw sync'swarn, a failed or a missed night, shows in the header; an older daemon's answer or refusal says nothing.- The CLI's
tests/tui.rsholdstheseus tui: the exec, the socket and the arguments passed through, and exit 2 whentheseus-tuiis found nowhere.
Traps
- The
start_pausedtests need tokio'stest-util, which the manifest's dev-dependencies name, socargo test -p theseus-tuibuilds alone too (it once built only through the workspace's feature unification). - The input line's
turn.submitcarries the session's last profile withcarried: true, aswatch --interactivedoes (theseus-0n1v): without it the daemon's live profile takes the turn, another model without a word. - A paste is never keys.
TermEvent::Pastegoes toApp::paste, never throughkey: a pastedqwould quit and a pastedywould answer the card. A new terminal mode goes interm.rs'senterandleave, both. - A change to
theseus_client'sclientorrenderchanges the TUI too. theseus-tuihas no--spawn: a TUI over a spawned--stdiodaemon would show only that daemon's sessions.- A live check runs it in tmux, at 80×24 and 160×48, over a scratch daemon's socket. Never point it at the operator's socket from a test.
