Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
79aee08
style: crate-wide comment sweep — trim verbose docs, reflow to 100-co…
hakula139 May 5, 2026
2624a1f
style: second-pass comment sweep — tighten verbose blocks across all …
hakula139 May 5, 2026
93ced72
style: expand doc comments to state contracts where signatures alone …
hakula139 May 5, 2026
34ccf1e
style(tool): document path / cap helpers and fix misplaced test divider
hakula139 May 5, 2026
11e8a96
style(tool/read): rename ambiguous result_view divider to match produ…
hakula139 May 5, 2026
642d431
style(session): reorder handle test sections to mirror production
hakula139 May 5, 2026
0fc2951
style(config): reorder test sections to mirror production
hakula139 May 5, 2026
679b8e5
style(file_tracker): document outcome / sidecar types
hakula139 May 5, 2026
e74b25a
style(model): document ModelInfo / Capabilities and add missing test …
hakula139 May 5, 2026
e164105
style(agent/event): balance AgentEvent variant docs and document INTE…
hakula139 May 5, 2026
001926f
style(slash/init): document the /init PROMPT template
hakula139 May 5, 2026
86dd6e2
style(message): add production section dividers and move is_default h…
hakula139 May 5, 2026
4dd15f5
refactor(slash): rename SessionInfo to LiveSessionInfo to disambiguate
hakula139 May 5, 2026
6cf26e0
docs(CLAUDE): fix tui/ subtree alphabetical ordering and SessionInfo …
hakula139 May 5, 2026
96a54cb
docs: refresh slash-commands docs for /effort typed-arg-only contract
hakula139 May 5, 2026
ae59307
docs(research): align cross-doc consistency
hakula139 May 5, 2026
0b0bf97
docs: cross-doc style consistency sweep
hakula139 May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ ox # Start an interactive session
├── slash/
│ ├── clear.rs # /clear (new, reset) — forwards UserAction::Clear, resets ChatView, drops the AI title
│ ├── config.rs # /config — read-only resolved config + layered file paths
│ ├── context.rs # SlashContext (borrowed ChatView + SessionInfo + modal slot) handed to each command's execute
│ ├── context.rs # SlashContext (borrowed ChatView + LiveSessionInfo + modal slot) handed to each command's execute
│ ├── diff.rs # /diff — `git diff HEAD` + untracked, 64 KB cap on UTF-8 boundary
│ ├── effort.rs # /effort — bare opens picker focused on effort; `/effort <level>` swaps directly
│ ├── effort.rs # /effort <level> — direct effort swap; bare form errors with usage hint pointing at /model
│ ├── format.rs # Shared kv-section / kv-table renderer
│ ├── help.rs # /help — registry-driven command listing
│ ├── init.rs # /init — synthesize an AGENTS.md / CLAUDE.md author-or-update prompt
Expand All @@ -96,11 +96,6 @@ ox # Start an interactive session
├── tui/
│ ├── app.rs # Root App struct, tokio::select! event loop, render dispatch
│ ├── components.rs # Components module root
│ ├── theme.rs # Theme palette (Slot{fg,bg,modifiers} per role) + style helpers + LazyLock-cached Mocha default
│ ├── theme/
│ │ ├── builtin.rs # Built-in TOML catalogue (Mocha / Macchiato / Frappe / Latte / Material via include_str!) + lookup
│ │ ├── color.rs # Color string parsing (hex, ANSI named, indexed, reset)
│ │ └── loader.rs # Theme TOML deserialization + base+overrides resolution (resolve_theme + SlotPatch)
│ ├── components/
│ │ ├── chat.rs # ChatView: scrollable chat with block stacking and history load
│ │ ├── chat/
Expand Down Expand Up @@ -138,6 +133,11 @@ ox # Start an interactive session
│ │ └── list_picker.rs # Generic ListPicker<T: PickerItem> — cursor + render primitive used by concrete pickers
│ ├── pending_calls.rs # Tool-call correlation state for streaming and transcript resume
│ ├── terminal.rs # Terminal init / restore, synchronized output, panic hook
│ ├── theme.rs # Theme palette (Slot{fg,bg,modifiers} per role) + style helpers + LazyLock-cached Mocha default
│ ├── theme/
│ │ ├── builtin.rs # Built-in TOML catalogue (Mocha / Macchiato / Frappe / Latte / Material via include_str!) + lookup
│ │ ├── color.rs # Color string parsing (hex, ANSI named, indexed, reset)
│ │ └── loader.rs # Theme TOML deserialization + base+overrides resolution (resolve_theme + SlotPatch)
│ └── wrap.rs # Word-wrap with continuation indent for styled lines
├── util.rs # Shared utilities module root
└── util/
Expand Down
156 changes: 58 additions & 98 deletions crates/oxide-code/src/agent.rs

Large diffs are not rendered by default.

55 changes: 37 additions & 18 deletions crates/oxide-code/src/agent/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,58 +11,68 @@ use crate::tool::ToolRegistry;

// ── Visible Markers ──

/// Inline glyph appended to the chat (and stderr in `--no-tui` mode) when a turn is cancelled
/// mid-stream. Matched by the chat block layer to render as a dim italic separator.
pub(crate) const INTERRUPTED_MARKER: &str = "(interrupted)";

// ── Agent Events ──

/// Events emitted by the agent loop for display.
/// One-way notifications from the agent loop to whichever UI is rendering it
/// (TUI [`ChannelSink`](crate::tui::event::ChannelSink) or stdio [`StdioSink`]).
#[derive(Debug, Clone)]
pub(crate) enum AgentEvent {
/// Assistant text chunk to append to the in-flight reply.
StreamToken(String),
/// Extended-thinking chunk; rendered separately from `StreamToken` and only when the user
/// has thinking display enabled.
ThinkingToken(String),
/// The model invoked a tool — render the call row and prepare the result slot.
ToolCallStart {
id: String,
name: String,
input: serde_json::Value,
},
/// Tool finished — fill the slot opened by the matching [`Self::ToolCallStart`].
ToolCallEnd {
id: String,
content: String,
is_error: bool,
metadata: crate::tool::ToolMetadata,
},
/// A queued mid-turn submit was just spliced into the live transcript; the UI clears its
/// queued-prompt indicator.
PromptDrained(String),
/// Turn ended cleanly with a final assistant reply (no further tool rounds).
TurnComplete,
/// User cancelled mid-turn ([`UserAction::Cancel`]); the in-flight reply is truncated and the
/// inline [`INTERRUPTED_MARKER`] is rendered.
Cancelled,
SessionTitleUpdated {
session_id: String,
title: String,
},
SessionRolled {
id: String,
},
/// Live config after a [`UserAction::SwapConfig`] applied. `effort`
/// is the resolved value (post-clamp); `requested_effort` is the
/// user's pick if they explicitly chose one — used to surface
/// `(clamped from X)` in the confirmation message.
/// Background title generator finished; UI updates the chrome label.
SessionTitleUpdated { session_id: String, title: String },
/// `/clear` rolled the session — a new session UUID is now active.
SessionRolled { id: String },
/// Live config after a [`UserAction::SwapConfig`]. `effort` is the resolved value (post-clamp);
/// `requested_effort` is the user's explicit pick, used to surface `(clamped from X)`.
ConfigChanged {
model_id: String,
effort: Option<Effort>,
requested_effort: Option<Effort>,
},
/// User-visible error from the agent loop, session writer, or tool dispatch. Renders as a
/// dedicated chat block.
Error(String),
}

// ── User Actions ──

/// UI → agent-loop commands. Multiplexed onto a single `mpsc` so the loop can race them
/// against in-flight stream / tool futures with one biased `select!`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum UserAction {
SubmitPrompt(String),
Clear,
/// Symmetric model + effort swap. At least one field must be `Some`;
/// `None` means "leave that axis as-is". Modal pickers and the
/// typed-arg `/model <id>` / `/effort <tier>` paths both flow
/// through here.
/// Symmetric model + effort swap. At least one field must be `Some`; `None` leaves that axis
/// as-is. Modal pickers and typed-arg `/model <id>` / `/effort <tier>` both flow through here.
SwapConfig {
model: Option<ResolvedModelId>,
effort: Option<Effort>,
Expand All @@ -73,20 +83,26 @@ pub(crate) enum UserAction {
Quit,
}

/// Channel pair whose `recv()` stays pending forever (no in-process sender).
/// Channel pair whose `recv()` stays pending forever.
pub(crate) fn inert_user_action_channel() -> (mpsc::Sender<UserAction>, mpsc::Receiver<UserAction>)
{
mpsc::channel(1)
}

// ── Agent Sink ──

/// Sized to absorb a full streaming-token burst without blocking the agent loop; the TUI drains
/// per frame, so dropping events here would visibly stutter the rendered response.
pub(crate) const AGENT_EVENT_CHANNEL_CAP: usize = 4096;

/// Abstraction over where agent events are sent (TUI channel / stdio).
/// Transport from the agent loop to a UI. Implementations must be cheap to clone-by-`&` (the
/// loop calls `send` on a hot path) and tolerate dropped events without panicking — `_ =
/// sink.send(...)` is the standard call pattern at the call sites.
pub(crate) trait AgentSink: Send + Sync {
fn send(&self, event: AgentEvent) -> Result<()>;

/// Convenience wrapper: surfaces a session-writer failure (sticky once-flag upstream) as a
/// single user-visible [`AgentEvent::Error`]. No-op when `failure` is `None`.
fn session_write_error(&self, failure: Option<&str>) {
if let Some(msg) = failure {
_ = self.send(AgentEvent::Error(format!("Session write failed: {msg}")));
Expand All @@ -96,6 +112,9 @@ pub(crate) trait AgentSink: Send + Sync {

// ── Stdio Sink (bare REPL / headless) ──

/// [`AgentSink`] for the `--no-tui` REPL and `--prompt` headless modes: assistant tokens stream
/// to stdout, everything else (tool calls, errors, the interrupted marker) to stderr so piped
/// output stays clean.
pub(crate) struct StdioSink {
show_thinking: bool,
tools: Arc<ToolRegistry>,
Expand Down
Loading