submitをvec segmentを受け付ける形に変更

This commit is contained in:
2026-04-27 11:03:58 +09:00
parent c9a7d652dc
commit 0a3af686f7
19 changed files with 663 additions and 97 deletions
+83 -8
View File
@@ -20,7 +20,7 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::{Block as UiBlock, BorderType, Borders, Padding, Paragraph, Widget, Wrap};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use protocol::{Greeting, AlertLevel};
use protocol::{AlertLevel, Greeting, Segment};
use crate::app::{App, fmt_tokens, alert_source_label};
use crate::block::{Block, CompactEvent};
@@ -299,13 +299,7 @@ fn render_block_into(
kind_style(MessageKind::TurnHeader),
)));
}
Block::UserMessage { text } => match mode {
Mode::Overview => push_overview_line(lines, text, width, MessageKind::User, "> "),
// User input and assistant prose are the primary readable
// content of a turn — never compressed in detail / normal.
// Only `overview` folds them to a single line.
_ => push_padded_lines(lines, text, MessageKind::User),
},
Block::UserMessage { segments } => render_user_message(lines, segments, width, mode),
Block::AssistantText { text } => match mode {
Mode::Overview => push_overview_line(lines, text, width, MessageKind::Assistant, ""),
_ => push_padded_lines(lines, text, MessageKind::Assistant),
@@ -363,6 +357,87 @@ fn push_padded_lines(lines: &mut Vec<Line<'static>>, text: &str, kind: MessageKi
}
}
/// Render `Block::UserMessage` from typed segments. Paste atoms are
/// reconstructed as `[Clipboard #N | X chars, Y lines]` chips in
/// magenta — matching the input-area presentation — so the user can
/// recognise their own paste in the scrollback. User-entered text uses
/// the standard `MessageKind::User` style; other segment kinds (file /
/// knowledge / workflow refs, unknown variants) render as inline
/// identifiers in the user style and are expected to be rare until the
/// completion ticket lands.
fn render_user_message(
lines: &mut Vec<Line<'static>>,
segments: &[Segment],
width: u16,
mode: Mode,
) {
if matches!(mode, Mode::Overview) {
let text = segments
.iter()
.map(segment_display_text)
.collect::<Vec<_>>()
.join("");
push_overview_line(lines, &text, width, MessageKind::User, "> ");
return;
}
let user_style = kind_style(MessageKind::User);
let paste_style = Style::default().fg(Color::Magenta);
let mut current: Vec<Span<'static>> = Vec::new();
for seg in segments {
match seg {
Segment::Text { content } => {
let mut iter = content.split('\n').peekable();
while let Some(line) = iter.next() {
if !line.is_empty() {
current.push(Span::styled(line.to_owned(), user_style));
}
if iter.peek().is_some() {
lines.push(Line::from(std::mem::take(&mut current)));
}
}
}
Segment::Paste {
id,
chars,
lines: line_count,
..
} => {
current.push(Span::styled(
format!("[Clipboard #{id} | {chars} chars, {line_count} lines]"),
paste_style,
));
}
other => {
current.push(Span::styled(segment_display_text(other), user_style));
}
}
}
if !current.is_empty() {
lines.push(Line::from(current));
}
}
/// One-line textual rendering of a segment, used by `Mode::Overview`
/// (which collapses everything to a single string) and as the fallback
/// inline rendering for non-paste, non-text segments.
fn segment_display_text(seg: &Segment) -> String {
match seg {
Segment::Text { content } => content.replace('\n', " "),
Segment::Paste {
id,
chars,
lines,
..
} => format!("[Clipboard #{id} | {chars} chars, {lines} lines]"),
Segment::FileRef { path } => format!("@{path}"),
Segment::KnowledgeRef { slug } => format!("#{slug}"),
Segment::WorkflowInvoke { slug } => format!("/{slug}"),
Segment::Unknown => "[unknown segment]".to_owned(),
}
}
/// Single-line summary for overview mode. The output is clipped to
/// exactly one rendered terminal row at `width` columns — the first
/// non-empty logical line is truncated (with `…`) to fit alongside an