An assistant entry can have text + multiple tool calls. route_entry
now returns Vec<(PaneTarget, String, Marker)> — tool calls go to
tools pane, text goes to conversation, all from the same entry.
Pop phase iterates the vec in reverse to pop correct number of
pane items per entry.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Store content lengths of rendered entries. On each tick:
- Generation changed → full pane reset
- Entries removed → pop from tail
- Last entry content length changed → pop and re-render (streaming)
- New entries → route and push
PaneState gains pop_line() for removing the last rendered entry.
This handles streaming (last entry growing), compaction (generation
bump), and normal appends.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
PaneTarget enum + route_entry() function: given a ConversationEntry,
returns which pane it belongs to (or None to skip). The sync loop
becomes: detect desync → pop, then route new entries.
Routing: User→Conversation, Assistant→ConversationAssistant,
tool_calls→Tools, Tool results→ToolResult, Memory/System→None.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Route agent entries to correct panes:
- User messages → conversation (cyan, User marker)
- Assistant text → conversation (Assistant marker)
- Assistant tool_calls → tools pane (yellow)
- Tool results → tools pane (truncated at 20 lines)
- Memory/system-reminder entries → skipped
- System role → skipped
Two phases: detect generation change (reset panes if needed),
then route new entries. PaneState is the rendered view of agent
entries, updated incrementally.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
InteractScreen holds agent ref, syncs conversation display from
agent.entries() on each tick via blocking_lock(). Tracks generation
counter and entry count to detect compactions and new entries.
Agent gets a generation counter, incremented on compaction and
non-last-entry mutations (age_out_images).
sync_from_agent() is the single path for pane updates. UiMessage
handle_ui_message still exists but will be removed once sync
handles all entry types (streaming, tool calls, DMN).
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Build legend string from actual screen labels instead of hardcoded
constant. Computed once at startup via OnceLock, accessible from
all screen draw methods.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
InteractScreen in chat.rs owns conversation/autonomous/tools panes,
textarea, input history, scroll state. App is now just shared state
(status, sampling params, agent_state, channel_status, idle_info).
Event loop holds InteractScreen separately for UiMessage routing.
Overlay screens (F2-F5) in screens vec. F-key switching preserves
state across screen changes.
handle_ui_message moved from App to InteractScreen.
handle_key split: global keys on App, screen keys in tick().
draw dispatch eliminated — each screen draws itself.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Create overlay screens vec (ConsciousScreen, SubconsciousScreen,
UnconsciousScreen, ThalamusScreen). F-keys switch active_screen.
Screen tick() called during render phase with pending key event.
Screen actions (Switch, Hotkey) applied after draw.
Interact (F1) still draws via App::draw_main(). Overlay screens
draw via ScreenView::tick(). State persists across switches.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.
- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll
Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.
App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.
InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
SlashCommand registry with name, help, and handler as inline
closures in commands(). Single source of truth — send_help reads
it, dispatch_command looks up by name. No separate named functions.
run() takes &Mind instead of individual handles. Dispatch reduced
to: quit check, command lookup, or submit as input.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Simplifies the interface — run() receives one reference to Mind
and extracts agent, shared, turn_watch locally. Reduces parameter
count from 7 to 5.
Also: command table data structure (SlashCommand) and commands()
function for single source of truth. send_help uses it. Dispatch
refactor to follow.
Also: fix input submission — diff before push, clone after push,
so prev_mind captures the input for consumption detection.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Mind::run() breaks when input_rx channel closes (UI shut down).
Previously the DMN timer kept the loop alive forever.
UI renders immediately without blocking on agent lock. Conversation
replay happens lazily on the render tick via try_lock — the UI
shows "consciousness v0.3" instantly, fills in model info and
conversation history once Mind init completes.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Show user text in the conversation window when the MindState diff
detects input was consumed (prev.input non-empty, cur.input empty).
Input stays editable in the text area until Mind takes it.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
All crossterm imports go through ratatui::crossterm. Direct crossterm
dep kept only for the event-stream feature flag (EventStream for
async terminal input).
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Extract diff_mind_state() — reused on render tick and input submit.
When pushing user input, lock shared, diff (catches any Mind state
changes), push input, snapshot. The next diff sees the input was
consumed → displays it.
Fixes: user text not appearing in conversation window.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
MindState is now std::sync::Mutex<MindState> owned by Mind, not
Arc-wrapped. Background scoring completion signals through a
BgEvent channel instead of locking shared directly. Retry sends
a Turn command instead of pushing to shared input.
No Arc on Mind (scoped tasks), no Arc on MindState (owned by Mind).
Only Arc<Mutex<Agent>> remains — needed for background turn spawns.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
Both event loops borrow &mind through a scoped spawn — no Arc on
Mind needed. Interior Arcs on agent/shared stay (background spawns
need 'static), but event_loop::run() now takes &Arc refs instead
of cloned Arcs.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Move turn_handle into MindState (behind the mutex). All Mind
methods now take &self. Mind can be shared across tasks without
Arc — it's Send + Sync and immutable from the outside.
Manual Clone impl for MindState skips turn_handle (not needed
for UI diffing).
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
The top-level run() that creates Mind, wires channels, spawns the
Mind event loop, and starts the UI event loop is orchestration —
it belongs with the UI entry point, not in the cognitive layer.
Renamed to event_loop::start(cli). mind/mod.rs is now purely the
Mind struct: state machine, MindCommand, and the run loop.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Mind::init() now handles channel daemons, observation socket, and
scoring startup. event_loop::run() creates its own idle state and
channel polling — UI concerns owned by the UI.
Startup run() is now: create agent, create Mind, init, spawn Mind,
run event_loop. Seven parameters on event_loop::run() instead of
twelve.
Remove observe_input_rx from event loop (being replaced by channel).
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Mind::init() restores conversation from log and starts scoring.
No UiMessages sent. The UI event loop reads Mind's state after
init and displays startup info (model, restored conversation)
by reading the agent directly.
mind/mod.rs has zero UiMessage imports or sends. Complete
separation between cognitive state machine and user interface.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
UserInput display moved to UI diff — when MindState.input goes from
populated to empty (consumed by a turn), the UI displays it. Mind
no longer sends any UiMessage from its event loop.
Remaining UiMessages are only in the startup function (one-time
init info).
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
dmn_sleep/dmn_wake/dmn_pause/cycle_autonomy were just setting two
fields each. Inline the assignments at call sites. cycle_autonomy
moves to event_loop since it's UI state machine logic (deciding
which label to show).
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
CycleAutonomy just flips DMN state — handled directly in event_loop
by locking shared MindState. MindCommand::Hotkey replaced with
MindCommand::Interrupt — the only command that needs Mind's async
context (abort handles, kill processes).
Remove dmn_interval() wrapper — inline self.dmn.interval().
MindCommand is now: Turn, Compact, Score, Interrupt, NewSession, None.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
These were called from handle_turn_result before the refactor but
got lost during the MindState migration. Re-add them in the turn
completion path. Delete the trivial refresh_context_state wrapper.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
MindCommand replaces both Action and MindMessage — one type for
everything: turns, compaction, scoring, hotkeys, new session.
State methods return MindCommand values. The run loop collects
commands into a Vec, then drains them through run_commands().
Compact and Score now flow through the same command path as
everything else.
Removes execute(), MindMessage from event_loop. Mind's run loop
is now: select! → collect commands → run_commands().
mind/mod.rs: 957 → 516 lines.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
UI event loop clones MindState on each render tick, diffs against
the previous copy, and generates status updates from changes. Mind
no longer sends UiMessage::StatusUpdate — state changes are detected
automatically by the UI.
Removes update_status from both Mind and event_loop. DMN state
changes, turn tracking, scoring status all flow through the diff.
Zero UiMessage sends from Mind's run loop for state changes.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Both are pure UI operations that read config/shared state and format
display messages. No Mind state mutation involved.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
/dmn, /sleep, /wake, /pause now lock MindState directly from the
UI event loop. No MindMessage roundtrip needed — they're just
state transitions + info display.
MindMessage reduced to: Hotkey (Interrupt, CycleAutonomy),
NewSession, Score. Everything else handled directly by UI.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Add MindState behind Arc<Mutex<>> for state shared between Mind
and UI. Pending user input goes through shared state instead of
MindMessage::UserInput — UI pushes, Mind consumes.
Mind checks for pending input after every event (message received,
turn completed, DMN tick). User input is prioritized over DMN ticks.
This enables the UI to display/edit/cancel queued messages, and
removes the last MindMessage variant that carried data.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
cycle_reasoning, kill_processes, and AdjustSampling only need the
Agent lock — they're pure Agent operations. Handle them directly
in the UI event loop instead of routing through Mind.
Mind now only receives Interrupt and CycleAutonomy as hotkeys,
which genuinely need Mind state (turn handles, DMN state).
mind/mod.rs: 957 → 688 lines across the session.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
All slash command routing now lives in user/event_loop.rs. Mind
receives typed messages (NewSession, Score, DmnSleep, etc.) and
handles them as named methods. No more handle_command() dispatch
table or Command enum.
Commands that only need Agent state (/model, /retry) run directly
in the UI task. Commands that need Mind state (/new, /score, /dmn,
/sleep, /wake, /pause) send a MindMessage.
Mind is now purely: turn lifecycle, DMN state machine, and the
named handlers for each message type.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Add tokio::sync::watch for turn_in_progress state. Commands in the
UI event loop can wait for turns to complete via wait_for() instead
of checking-and-bailing.
Move /retry to event_loop: waits for turn completion, pops agent
history, sends retried text as MindMessage::UserInput. Mind doesn't
need to know about retry — it just sees a new input message.
Make agent field pub on Mind for UI access.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
/quit, /help, /save handled directly in the UI event loop.
/model and /model <name> moved to event_loop as cmd_switch_model().
Mind no longer needs tui::App for any command handling.
Mind's handle_command now only has commands that genuinely need
Mind state: /new, /retry, /score (turn_in_progress, DMN, scoring).
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
Disables memory scoring, surface, and observe agents when set.
Useful for testing with external backends (e.g. OpenRouter) where
background agent traffic would be slow and unnecessary.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
journal_tools() was only in memory_and_journal_tools() for
subconscious agents — not in the main tools() registry. Added
so consciousness and MCP server can use journal_new/tail/update.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
F5 screen now shows temperature, top_p, top_k with interactive
adjustment:
- Up/down: select parameter
- Left/right: adjust value (0.05 steps for temp/top_p, 5 for top_k)
- Updates Agent and display immediately via HotkeyAction
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Move temperature from a per-call parameter to an Agent field,
add top_p and top_k. All three are sent to the API via a new
SamplingParams struct, displayed on the F5 thalamus screen.
Defaults: temperature=0.6, top_p=0.95, top_k=20 (Qwen3.5 defaults).
Also adds top_p and top_k to ChatRequest so they're sent in the
API payload. Previously only temperature was sent.
UI controls for adjusting these at runtime are not yet implemented.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
The agent lock was held for the entire duration of turn() — including
API streaming and tool dispatch awaits. This blocked the UI thread
whenever it needed the lock (render tick, compaction check, etc.),
causing 20+ second freezes.
Fix: turn() takes Arc<Mutex<Agent>> and manages locking internally.
Lock is held briefly for prepare/process phases, released during all
I/O (streaming, tool awaits, sleep retries). Also:
- check_compaction: spawns task instead of awaiting on event loop
- start_memory_scoring: already spawned, no change needed
- dispatch_tool_call_unlocked: drops lock before tool handle await
- Subconscious screen: renders all agents from state dynamically
(no more hardcoded SUBCONSCIOUS_AGENTS list)
- Memory scoring shows n/m progress in snapshots
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
One data structure for all in-flight tool calls — metadata for
TUI display + JoinHandle for result collection and cancellation.
Agent spawns tool calls via tokio::spawn, pushes to shared
Arc<Mutex<Vec<ActiveToolCall>>>. TUI reads metadata, can abort().
No separate inflight/background collections.
Non-background: awaited after stream ends.
Background: persists, drained at next turn start.
Co-Developed-By: Kent Overstreet <kent.overstreet@linux.dev>
Move active tool tracking from TUI message-passing to shared
Arc<RwLock> state. Agent pushes on dispatch, removes on
apply_tool_result. TUI reads during render. Background tasks
show as active until drained at next turn start.
Co-Developed-By: Kent Overstreet <kent.overstreet@linux.dev>
When </tool_call> is detected in the content stream, parse and
dispatch immediately via FuturesOrdered. Tool calls execute
concurrently while the stream continues. Results collected in
order after the stream ends.
Structured API path (ToolCallDelta) unchanged — still uses
post-stream parallel dispatch.
Co-Developed-By: Kent Overstreet <kent.overstreet@linux.dev>