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}