Skip to main content

recursive/tui/
model.rs

1//! Transcript data model types for the Recursive TUI.
2//!
3//! Contains the screen enum and the types used to represent rendered
4//! transcript blocks (user messages, assistant replies, tool calls/results,
5//! diffs, etc.).
6
7// ──────────────────────────────────────────────────────────────────────
8// Screens
9// ──────────────────────────────────────────────────────────────────────
10
11/// Which top-level screen is currently rendered.
12///
13/// Goal 147 removed the `PlanReview` variant — the plan-mode
14/// confirmation now lives on the modal stack as
15/// [`crate::tui::ui::modal::Modal::PlanReview`], so we are down to one
16/// screen: the chat surface. The splash screen was replaced by a
17/// startup banner printed to stdout before the inline TUI starts.
18#[derive(Clone, Debug, PartialEq)]
19pub enum AppScreen {
20    Chat,
21}
22
23// ──────────────────────────────────────────────────────────────────────
24// Transcript model
25// ──────────────────────────────────────────────────────────────────────
26
27/// One unit of context within a [`Diff`] block.
28///
29/// We model only the three line kinds we need to colour-code: addition,
30/// removal, and unchanged context.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub enum DiffLineKind {
33    Add,
34    Remove,
35    Context,
36}
37
38/// A single line inside a diff hunk.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct DiffLine {
41    pub kind: DiffLineKind,
42    pub text: String,
43}
44
45/// A grouped sequence of diff lines belonging to one logical change.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct DiffHunk {
48    pub lines: Vec<DiffLine>,
49}
50
51/// Result payload of a tool execution. Lives inside a
52/// [`TranscriptBlock::ToolCall`] once the runtime delivers the
53/// matching `UiEvent::ToolResult`.
54///
55/// The UI used to keep `ToolCall` and `ToolResult` as two separate
56/// blocks, but Claude-Code-style renderings pair them into a single
57/// "function call" unit: one bullet, then the result on the next
58/// line. `None` on the call side means the tool is still running.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct ToolResultData {
61    pub success: bool,
62    pub output: String,
63    /// `false` = collapsed (first 3 lines + "… N more" hint),
64    /// `true` = full output. Toggled by Ctrl+E on the chat
65    /// surface when the input buffer is empty.
66    pub expanded: bool,
67}
68
69/// One renderable transcript block.
70///
71/// The chat screen renders a `Vec<TranscriptBlock>` in order, with one
72/// blank line between adjacent blocks. Each variant has a corresponding
73/// renderer in [`crate::tui::ui::transcript`].
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum TranscriptBlock {
76    User {
77        text: String,
78    },
79    Assistant {
80        text: String,
81        streaming: bool,
82        latency_ms: Option<u64>,
83    },
84    /// Reasoning / thinking content for a step. Emitted by
85    /// [`UiEvent::Reasoning`] before the matching assistant text
86    /// block; rendered inline as a `thinking…` header followed by
87    /// the reasoning text in dim grey italics.
88    Reasoning {
89        text: String,
90    },
91    /// Tool call (paired with its result once available).
92    ///
93    /// While the tool is still running, `result` is `None`; the
94    /// renderer shows the call in a "running" state (yellow ⏺,
95    /// `Running…` placeholder). When the runtime pushes the
96    /// matching `UiEvent::ToolResult`, the field is filled in.
97    ToolCall {
98        id: String,
99        name: String,
100        args_preview: String,
101        result: Option<ToolResultData>,
102    },
103    Diff {
104        path: String,
105        hunks: Vec<DiffHunk>,
106    },
107    Compacted {
108        removed: usize,
109        kept: usize,
110    },
111    System {
112        text: String,
113    },
114    Error {
115        text: String,
116    },
117    /// Goal-E: plan-mode proposal rendered inline in the transcript.
118    /// Replaces the `Modal::PlanReview` pop-up so the plan is visible
119    /// in the message stream without obscuring prior context.
120    PlanProposal {
121        plan_text: String,
122        tool_calls: Vec<serde_json::Value>,
123    },
124    /// Goal-202: plan-mode entry request rendered inline in the transcript.
125    /// Agent called `request_plan_mode`; user should approve or skip.
126    PlanModeRequest {
127        reason: String,
128        /// Set to `Some(true/false)` after the user decides.
129        approved: Option<bool>,
130    },
131    /// Incoming message from a WeChat user, forwarded through the iLink channel.
132    /// Rendered with a 📱 prefix so it is visually distinct from local TUI input.
133    #[cfg(feature = "weixin")]
134    WeixinMessage {
135        user_id: String,
136        text: String,
137    },
138}