Commit graph

74 commits

Author SHA1 Message Date
Kent Overstreet
f63c341f94 fix unused imports 2026-04-06 22:43:55 -04:00
Kent Overstreet
3cb53d7a5d simplify main ui event loop
Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-06 22:43:55 -04:00
Kent Overstreet
0d20d66196 Unify screen dispatch — put InteractScreen in the screens array
active_screen is now the F-key number (1-based), dispatch is just
screens[active_screen - 1].tick() everywhere. Eliminates the
special-cased interact variable and duplicated if/else branching.

Also adds diff_mind_state() for dirty-flag tracking and gates the
bottom-of-loop render on dirty, avoiding redundant redraws.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-06 22:43:55 -04:00
Kent Overstreet
6e9ad04bfc Render only visible lines in conversation and tools panes
ratatui's Paragraph with Wrap does full unicode grapheme segmentation
on render — including for scrolled-off content. Cache per-line wrapped
heights on PaneState (recomputed only on width change or new lines),
then slice to only the visible lines before handing to ratatui.

Eliminates O(total_lines) grapheme work per frame, replacing it with
O(viewport_height) — ~30 lines instead of potentially thousands.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-06 22:43:55 -04:00
Kent Overstreet
f4664ca06f Cache context budget instead of recomputing every frame
budget() called tiktoken on every UI tick, which was the main CPU hog
during rapid key input. Move the cached ContextBudget onto ContextState
and recompute only when entries actually change (push_entry, compact,
restore_from_log).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-06 22:43:55 -04:00
Kent Overstreet
49cd6d6ab6 rendering 2026-04-06 22:43:55 -04:00
ProofOfConcept
36d698a3e1 Remove dead code: append_text, needs_assistant_marker, target param
append_text was the TextDelta streaming handler — replaced by
append_streaming on Agent entries. needs_assistant_marker tracked
turn boundaries for the old message path. target removed from
Agent::turn — routing now determined by entry content.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-05 22:40:38 -04:00
ProofOfConcept
f390fa1617 Delete ui_channel.rs — relocate types, remove all UiMessage/UiSender plumbing
Types relocated:
- StreamTarget → mind/mod.rs (Mind decides Conversation vs Autonomous)
- SharedActiveTools + shared_active_tools() → agent/tools/mod.rs
- ContextSection + SharedContextState → agent/context.rs (already there)
- StatusInfo + ContextInfo → user/mod.rs (UI display state)

Removed UiSender from: Agent::turn, Mind, learn.rs, all function signatures.
The entire message-passing layer is gone. All state flows through
Agent fields (activities, entries, streaming) read by the UI via try_lock.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-05 22:34:48 -04:00
ProofOfConcept
cfddb55ed9 Kill TextDelta, Info — UiMessage is dead. RAII ActivityGuards replace all status feedback
Streaming text now goes directly to agent entries via append_streaming().
sync_from_agent diffs the growing entry each tick. The streaming entry
is popped when the response completes; build_response_message pushes
the final version.

All status feedback uses RAII ActivityGuards:
- push_activity() for long-running work (thinking, streaming, scoring)
- notify() for instant feedback (compacted, DMN state changes, commands)
- Guards auto-remove on Drop, appending "(complete)" and lingering 5s
- expire_activities() cleans up timed-out notifications on render tick

UiMessage enum reduced to a single Info variant with zero sends.
The channel infrastructure remains for now (Mind/Agent still take
UiSender in signatures) — mechanical cleanup for a follow-up.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-05 22:18:07 -04:00
ProofOfConcept
e7914e3d58 Kill Reasoning, Debug, Activity variants — read status from Agent directly
Reasoning tokens: dropped for now, will land in context entries later.
Debug sends: converted to dbglog! macro (writes to debug.log).
Activity: now a field on Agent, set directly, read by UI via try_lock.
score_memories_incremental takes agent Arc for activity writes.

UiMessage down to 2 variants: TextDelta, Info.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-05 21:45:55 -04:00
ProofOfConcept
eafc2887a3 Kill StatusUpdate, Activity, DmnAnnotation, ContextInfoUpdate, AgentUpdate
Status bar reads directly from Agent and MindState on each render tick.
Activity is now a field on Agent — set by agent code directly, read by
UI via try_lock. DmnAnnotation, ContextInfoUpdate, AgentUpdate were
already dead (no senders).

UiMessage down to 4 variants: TextDelta, Reasoning, Debug, Info.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-05 21:34:27 -04:00
ProofOfConcept
1745e03550 Kill UiMessage variants replaced by state-driven rendering
sync_from_agent reads directly from agent entries, so remove:
- UserInput (pending input shown from MindState.input)
- ToolCall, ToolResult (shown from entries on completion)
- ToolStarted, ToolFinished (replaced by shared active_tools)
- replay_session_to_ui (sync_from_agent handles replay)

-139 lines. Remaining variants are streaming (TextDelta, Reasoning),
status bar state, or ephemeral UI messages (Info, Debug).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-05 21:21:08 -04:00
Kent Overstreet
306788e0f1 kill event_loop.rs 2026-04-05 21:16:49 -04:00
ProofOfConcept
48beb8b663 Revert to tokio::sync::Mutex, fix lock-across-await bugs, move input ownership to InteractScreen
The std::sync::Mutex detour caught every place a MutexGuard lived
across an await point in Agent::turn — the compiler enforced Send
safety that tokio::sync::Mutex silently allows. With those fixed,
switch back to tokio::sync::Mutex (std::sync blocks tokio worker
threads and panics inside the runtime).

Input and command dispatch now live in InteractScreen (chat.rs):
- Enter pushes directly to SharedMindState.input (no app.submitted hop)
- sync_from_agent displays pending input with dimmed color
- Slash command table moved from event_loop.rs to chat.rs
- cmd_switch_model kept as pub fn for tool-initiated switches

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-05 21:13:48 -04:00
Kent Overstreet
3e1be4d353 agent: switch from tokio::sync::Mutex to std::sync::Mutex
The agent lock is never held across await points — turns lock briefly,
do work, drop, then do async API calls. std::sync::Mutex works and
can be locked from sync contexts (screen tick inside terminal.draw).

Fixes: blocking_lock() panic when called inside tokio runtime.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 20:08:25 -04:00
Kent Overstreet
f29b4be09c Move chat code to chat.rs 2026-04-05 20:08:18 -04:00
Kent Overstreet
65d23692fb chat: route_entry returns Vec for multi-tool-call entries
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>
2026-04-05 19:43:48 -04:00
Kent Overstreet
222b2cbeb2 chat: PartialEq on ConversationEntry for proper diff
Add PartialEq to Message, FunctionCall, ToolCall, ContentPart,
ImageUrl, MessageContent, ConversationEntry. Sync now compares
entries directly instead of content lengths.

Phase 1 pops mismatched tail entries using PartialEq comparison.
Phase 2 pushes new entries with clone into last_entries buffer.

TODO: route_entry needs to handle multiple tool calls per entry.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 19:41:16 -04:00
Kent Overstreet
ca9f2b2b9a chat: double-buffered sync with content-length diffing
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>
2026-04-05 19:35:19 -04:00
Kent Overstreet
563771e979 chat: route_entry helper separates routing from sync
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>
2026-04-05 19:27:14 -04:00
Kent Overstreet
b89bafdf6b chat: full entry type routing in sync_from_agent
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>
2026-04-05 19:22:31 -04:00
Kent Overstreet
350c447ebc chat: state-driven sync from agent entries
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>
2026-04-05 19:17:13 -04:00
Kent Overstreet
6f000bd0f6 user: hook up screen_legend from ScreenView::label()
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>
2026-04-05 19:03:06 -04:00
Kent Overstreet
68f115b880 user: InteractScreen extracted, all screens use ScreenView trait
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>
2026-04-05 18:57:54 -04:00
Kent Overstreet
8418bc9bc9 event_loop: wire F-key screen switching with ScreenView
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>
2026-04-05 17:59:33 -04:00
Kent Overstreet
927cddd864 user: ScreenView trait, overlay screens extracted from App
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>
2026-04-05 17:54:40 -04:00
Kent Overstreet
7458fe655f event_loop: command table with inline closures
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>
2026-04-05 17:05:34 -04:00
Kent Overstreet
3c4220c079 event_loop: run() takes &Mind instead of individual handles
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>
2026-04-05 16:53:05 -04:00
Kent Overstreet
2b9aba0e5d fix shutdown hang and slow startup
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>
2026-04-05 16:18:10 -04:00
Kent Overstreet
71351574be event_loop: display user input when Mind consumes it
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>
2026-04-05 16:13:04 -04:00
Kent Overstreet
8d045a3e6b use ratatui::crossterm re-exports, add event-stream feature
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>
2026-04-05 06:22:31 -04:00
Kent Overstreet
120ffabfaa Kill socket, read/write subcommands
Redundant with channels

Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 05:58:51 -04:00
Kent Overstreet
3b15c690ec event_loop: diff MindState on input submission, fix input display
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>
2026-04-05 05:58:51 -04:00
Kent Overstreet
7dc515b985 mind: remove Arc from MindState
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>
2026-04-05 05:58:51 -04:00
Kent Overstreet
5eaba3c951 mind: use tokio-scoped for Mind/UI loop lifetimes
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>
2026-04-05 05:58:51 -04:00
Kent Overstreet
aae9687de2 mind: Mind is fully &self — no &mut needed
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>
2026-04-05 05:58:51 -04:00
Kent Overstreet
57b0f94b54 move startup orchestration from mind to event_loop
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>
2026-04-05 04:29:56 -04:00
Kent Overstreet
91033fe754 mind: clean startup split between Mind and event_loop
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>
2026-04-05 04:17:04 -04:00
Kent Overstreet
9d597b5eff mind: zero UI dependencies — init() + run() split
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>
2026-04-05 04:02:16 -04:00
Kent Overstreet
3ee1aa69b0 mind: zero UiMessages from Mind's run loop
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>
2026-04-05 03:54:01 -04:00
Kent Overstreet
84fe757260 mind: remove DMN helper methods, inline field assignments
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>
2026-04-05 03:50:21 -04:00
Kent Overstreet
2d6a17e773 mind: move CycleAutonomy to event_loop, simplify MindCommand
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>
2026-04-05 03:41:47 -04:00
Kent Overstreet
402bae4178 mind: restore age_out_images and publish_context_state after turns
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>
2026-04-05 03:38:42 -04:00
Kent Overstreet
01b07a7f28 mind: unify MindCommand, add command queue pattern
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>
2026-04-05 03:34:43 -04:00
Kent Overstreet
07ca136c14 mind: double-buffer MindState for UI diffing
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>
2026-04-05 03:24:08 -04:00
Kent Overstreet
54cd3783eb mind: move send_context_info and update_status to event_loop
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>
2026-04-05 03:12:50 -04:00
Kent Overstreet
4eb0c891c4 mind: move DMN commands to event_loop, remove MindMessage variants
/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>
2026-04-05 03:08:36 -04:00
Kent Overstreet
792e9440af mind: shared MindState for pending input
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>
2026-04-05 02:52:56 -04:00
Kent Overstreet
05d6bbc912 move hotkey handlers from Mind to event_loop
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>
2026-04-05 02:44:58 -04:00
Kent Overstreet
64add58caa mind: move all slash commands to event_loop dispatch
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>
2026-04-05 02:40:45 -04:00