Move the loading indicator inline under the user message.

03bbdf9353b8 · AtlantisPleb · · parent 6d10d5a29cd8

Move the loading indicator inline under the user message.

- Remove the status bar above the composer.
- Render an animated braille spinner directly under the last user message,
  aligned with the > caret.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified crates/coder-lite/src/tui.rs

Diff

1 file changed, +15 -25

crates/coder-lite/src/tui.rs modified +15 -25

@@ -10,6 +10,7 @@ use ratatui::{

10 10
11 11
const TEXT_COLOR: Color = Color::Rgb(255, 176, 0);
12 12
const BACKGROUND_COLOR: Color = Color::Rgb(8, 6, 0);
13
const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13 14
14 15
#[derive(Debug, Clone)]
15 16
pub enum Role {

@@ -40,6 +41,7 @@ pub struct CoderUi {

40 41
    pub scroll_max: u16,
41 42
    pub transcript_height: u16,
42 43
    pub loading: bool,
44
    pub tick: u64,
43 45
}
44 46
45 47
fn wrap_text(text: &str, width: usize) -> Vec<String> {

@@ -133,10 +135,12 @@ impl CoderUi {

133 135
            scroll_max: 0,
134 136
            transcript_height: 0,
135 137
            loading: false,
138
            tick: 0,
136 139
        }
137 140
    }
138 141
139 142
    pub fn render(&mut self, frame: &mut Frame, area: Rect) {
143
        self.tick = self.tick.wrapping_add(1);
140 144
        let style = Style::default().fg(TEXT_COLOR).bg(BACKGROUND_COLOR);
141 145
142 146
        // Fill the entire terminal with the background color first.

@@ -158,12 +162,10 @@ impl CoderUi {

158 162
        let visible_input_lines = total_input_lines.min(max_input_lines);
159 163
        let input_scroll = total_input_lines.saturating_sub(visible_input_lines);
160 164
        let input_box_height = visible_input_lines + 2;
161
        let status_height = if self.loading { 1 } else { 0 };
162
        let bottom_height = input_box_height + status_height;
163 165
164 166
        let main_bottom = Layout::default()
165 167
            .direction(Direction::Vertical)
166
            .constraints([Constraint::Min(0), Constraint::Length(bottom_height)])
168
            .constraints([Constraint::Min(0), Constraint::Length(input_box_height)])
167 169
            .split(area);
168 170
169 171
        let transcript_area = main_bottom[0];

@@ -173,6 +175,14 @@ impl CoderUi {

173 175
            all_lines.extend(self.render_entry(entry, transcript_area.width as usize));
174 176
        }
175 177
178
        if self.loading {
179
            let spinner = SPINNER_FRAMES[self.tick as usize % SPINNER_FRAMES.len()];
180
            all_lines.push(Line::from(vec![Span::styled(
181
                format!("> {}", spinner),
182
                style,
183
            )]));
184
        }
185
176 186
        let total = all_lines.len() as u16;
177 187
        self.transcript_height = transcript_area.height;
178 188
        self.scroll_max = total.saturating_sub(transcript_area.height);

@@ -183,26 +193,7 @@ impl CoderUi {

183 193
            .style(style);
184 194
        frame.render_widget(transcript, transcript_area);
185 195
186
        let bottom = main_bottom[1];
187
        let bottom_chunks = if self.loading {
188
            Layout::default()
189
                .direction(Direction::Vertical)
190
                .constraints([Constraint::Length(1), Constraint::Length(input_box_height)])
191
                .split(bottom)
192
        } else {
193
            Layout::default()
194
                .direction(Direction::Vertical)
195
                .constraints([Constraint::Length(input_box_height)])
196
                .split(bottom)
197
        };
198
199
        if self.loading {
200
            let status = Paragraph::new(Line::from(vec![Span::styled("● working…", style)]))
201
                .style(style);
202
            frame.render_widget(status, bottom_chunks[0]);
203
        }
204
205
        let input_idx = if self.loading { 1 } else { 0 };
196
        let input_area = main_bottom[1];
206 197
207 198
        let mut input_lines = Vec::new();
208 199
        for (i, chunk) in input_chunks.iter().enumerate().skip(input_scroll as usize) {

@@ -221,9 +212,8 @@ impl CoderUi {

221 212
                    .border_style(style)
222 213
                    .style(style),
223 214
            );
224
        frame.render_widget(input, bottom_chunks[input_idx]);
215
        frame.render_widget(input, input_area);
225 216
226
        let input_area = bottom_chunks[input_idx];
227 217
        let last_chunk = input_chunks.last().map(|s| s.as_str()).unwrap_or("");
228 218
        let cursor_x = input_area.x + 1 + 3 + last_chunk.chars().count() as u16;
229 219
        let cursor_y = input_area.y + 1 + visible_input_lines.saturating_sub(1);

This page updates live while a promote is in flight · changelog