Fix delegate tool box rendering.

b3173303774a · AtlantisPleb · · parent f1dd8c526210

Fix delegate tool box rendering.

- `Entry.output` is now `Option<String>` so ACP text chunks append into a
  single block instead of one token per line. The box renders the last five
  lines split on real newlines.
- Removed the left spacing on `Role::Tool` so the `⏺` bullet sits flush with
  the left edge, directly above the composer caret.
- Updated `tests/tool_box.rs` to exercise the newline-split box.

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/interactive.rs
  • modified crates/coder-lite/src/tui.rs
  • modified crates/coder-lite/tests/tool_box.rs

Diff

3 files changed, +21 -21

crates/coder-lite/src/interactive.rs modified +7 -5

@@ -86,27 +86,29 @@ pub async fn run_tui() -> Result<(), Box<dyn std::error::Error>> {

86 86
                    if let Some(last) = ui.entries.last_mut() {
87 87
                        if last.role == Role::Tool {
88 88
                            last.text = format!("delegate {}: {}", agent, title);
89
                            last.output = Some(Vec::new());
89
                            last.output = Some(String::new());
90 90
                        } else {
91 91
                            ui.entries.push(Entry {
92 92
                                role: Role::Tool,
93 93
                                text: format!("delegate {}: {}", agent, title),
94
                                output: Some(Vec::new()),
94
                                output: Some(String::new()),
95 95
                            });
96 96
                        }
97 97
                    } else {
98 98
                        ui.entries.push(Entry {
99 99
                            role: Role::Tool,
100 100
                            text: format!("delegate {}: {}", agent, title),
101
                            output: Some(Vec::new()),
102
                        });
101
                            output: Some(String::new()),
102
                            });
103 103
                    }
104 104
                    ui.scroll_override = None;
105 105
                }
106 106
                Control::ToolText(chunk) => {
107 107
                    if let Some(last) = ui.entries.last_mut() {
108 108
                        if last.role == Role::Tool {
109
                            last.output.get_or_insert_with(Vec::new).push(chunk);
109
                            last.output
110
                                .get_or_insert_with(String::new)
111
                                .push_str(&chunk);
110 112
                        }
111 113
                    }
112 114
                    ui.scroll_override = None;
crates/coder-lite/src/tui.rs modified +13 -11

@@ -101,7 +101,8 @@ pub enum Role {

101 101
pub struct Entry {
102 102
    pub role: Role,
103 103
    pub text: String,
104
    pub output: Option<Vec<String>>,
104
    /// Tool output text, rendered as a ~5-line box split by newlines.
105
    pub output: Option<String>,
105 106
}
106 107
107 108
#[derive(Debug)]

@@ -327,24 +328,25 @@ impl CoderUi {

327 328
                let text_style = Style::default().fg(TEXT_COLOR).bg(BACKGROUND_COLOR);
328 329
                let mut lines = Vec::new();
329 330
330
                // One-line tool call header.
331
                let header_body = width.saturating_sub(4);
331
                // One-line tool call header, flush left.
332
                let header_body = width.saturating_sub(2);
332 333
                let header_chunks = wrap_text(&entry.text, header_body);
333 334
                let header = header_chunks.first().cloned().unwrap_or_default();
334 335
                lines.push(Line::from(vec![
335
                    Span::styled("  ⏺ ", text_style),
336
                    Span::styled("⏺ ", text_style),
336 337
                    Span::styled(header, text_style),
337 338
                ]));
338 339
339
                // ~5-line output box, one chunk per line.
340
                let out = entry.output.as_ref().map_or(&[][..], |v| v.as_slice());
341
                let start = out.len().saturating_sub(5);
342
                let window = &out[start..];
340
                // ~5-line output box, split by actual newlines.
341
                let out = entry.output.as_deref().unwrap_or("");
342
                let out_lines: Vec<&str> = out.lines().collect();
343
                let start = out_lines.len().saturating_sub(5);
344
                let window = &out_lines[start..];
343 345
                for i in 0..5 {
344
                    let text = window.get(i).map(String::as_str).unwrap_or("");
345
                    let clipped = text.chars().take(width.saturating_sub(4)).collect::<String>();
346
                    let text = if i < window.len() { window[i] } else { "" };
347
                    let clipped = text.chars().take(width.saturating_sub(2)).collect::<String>();
346 348
                    lines.push(Line::from(vec![
347
                        Span::styled("  │ ", text_style),
349
                        Span::styled("│ ", text_style),
348 350
                        Span::styled(clipped, text_style),
349 351
                    ]));
350 352
                }
crates/coder-lite/tests/tool_box.rs modified +1 -5

@@ -8,11 +8,7 @@ fn renders_delegate_tool_call_and_five_line_box() {

8 8
    ui.entries.push(Entry {
9 9
        role: Role::Tool,
10 10
        text: "delegate devin: Read src/main.rs".to_string(),
11
        output: Some(vec![
12
            "Reading file...".to_string(),
13
            "Found main()".to_string(),
14
            "Done".to_string(),
15
        ]),
11
        output: Some("Reading file...\nFound main()\nDone".to_string()),
16 12
    });
17 13
18 14
    let backend = TestBackend::new(80, 24);

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