feat: replace the [ok] summary line with a ✓/✗ echo marker

An audit of the command surface found the `[ok] <verb> <subject>`
summary line duplicated the echo line above it everywhere; its only
unique signal was success-vs-error. Retire it: a command's echo line
now resolves from `running: <input>` to `<input> ✓` / `<input> ✗`
on completion, and the symmetric `"<verb> <subject>" failed:` prefix
is dropped (only the reason remains). Content lines (row counts,
structure, plan tree, teaching echo) are unchanged.

Echo lines carry an EchoStatus; executed commands push Pending and
resolve the oldest-pending echo on their result event (FIFO worker —
correct under interleaving). Parse-time and pre-flight rejections are
not executed and keep their running: + caret rendering. App-command
[ok] lines (rebuild/export/replay) are payload-bearing and untouched.
ADR-0040.
This commit is contained in:
claude@clouddev1
2026-05-30 21:38:48 +00:00
parent f62cccec55
commit 8311de44a8
9 changed files with 546 additions and 124 deletions
+96 -27
View File
@@ -12,7 +12,7 @@ use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph, Wrap};
use crate::app::{App, EffectiveMode, OutputKind, OutputLine, OutputStyleClass};
use crate::app::{App, EchoStatus, EffectiveMode, OutputKind, OutputLine, OutputStyleClass};
use crate::mode::Mode;
use crate::theme::Theme;
@@ -718,7 +718,24 @@ fn approximate_wrapped_rows_from_output(
OutputKind::System | OutputKind::TeachingEcho => "[system] ".len(),
OutputKind::Error => "[error] ".len(),
};
let total = tag_len + line.text.chars().count();
// ADR-0040: a completed echo renders `<input> ✓/✗` —
// the `running: ` prefix is dropped and a 2-column marker
// (space + glyph) appended; everything else renders its
// text verbatim.
let content_chars = if matches!(
(line.kind, line.status),
(OutputKind::Echo, Some(EchoStatus::Ok | EchoStatus::Err))
) {
line.text
.strip_prefix(crate::dsl::ECHO_PREFIX)
.unwrap_or(line.text.as_str())
.chars()
.count()
+ 2
} else {
line.text.chars().count()
};
let total = tag_len + content_chars;
if total == 0 { 1 } else { total.div_ceil(w) }
})
.sum()
@@ -753,31 +770,51 @@ fn render_output_line<'a>(line: &'a OutputLine, theme: &Theme) -> Line<'a> {
OutputKind::Error => "[error] ".to_string(),
};
// Simple-mode echo lines get token-class highlighting on
// their input portion (ADR-0022 §5). Echo body shape is
// contracted to `<ECHO_PREFIX><input>`; the prefix is
// pinned to the catalog template by
// ADR-0040: an echo renders `running: <input>` while pending
// (and for untracked parse/pre-flight rejections), and
// `<input> ✓` / `<input> ✗` once an executed command completes
// — the marker replaces the old `[ok]`/`failed:` summary line.
// Simple-mode input keeps its token-class highlighting (ADR-0022
// §5); advanced-mode input renders plain, as before. The body
// shape is `<ECHO_PREFIX><input>`; the prefix is pinned to the
// catalog template by
// `dsl::tests::echo_prefix_matches_catalog_template`.
if line.kind == OutputKind::Echo
&& line.mode_at_submission == Mode::Simple
&& let Some(rest) = line.text.strip_prefix(crate::dsl::ECHO_PREFIX)
{
let mut spans: Vec<Span<'a>> = Vec::with_capacity(2 + rest.len() / 4);
if line.kind == OutputKind::Echo {
let input = line
.text
.strip_prefix(crate::dsl::ECHO_PREFIX)
.unwrap_or(line.text.as_str());
let mut spans: Vec<Span<'a>> = Vec::with_capacity(3 + input.len() / 4);
spans.push(Span::styled(tag, tag_style));
spans.push(Span::styled(
crate::dsl::ECHO_PREFIX,
Style::default().fg(theme.fg),
));
for run in crate::input_render::lex_to_runs(rest, theme) {
// Pending / untracked → keep the `running: ` prefix;
// completed → drop it (the marker carries the outcome).
if !matches!(line.status, Some(EchoStatus::Ok | EchoStatus::Err)) {
spans.push(Span::styled(
&rest[run.byte_range.0..run.byte_range.1],
run.style,
crate::dsl::ECHO_PREFIX,
Style::default().fg(theme.fg),
));
}
if line.mode_at_submission == Mode::Simple {
for run in crate::input_render::lex_to_runs(input, theme) {
spans.push(Span::styled(
&input[run.byte_range.0..run.byte_range.1],
run.style,
));
}
} else {
spans.push(Span::styled(input, Style::default().fg(theme.fg)));
}
match line.status {
Some(EchoStatus::Ok) => {
spans.push(Span::styled("", Style::default().fg(theme.system)));
}
Some(EchoStatus::Err) => {
spans.push(Span::styled("", Style::default().fg(theme.error)));
}
_ => {}
}
return Line::from(spans);
}
// Echo body without the expected prefix, or any non-echo
// line, falls through to the plain rendering below.
// ADR-0038 §4 styled-runs polish — the DSL → SQL teaching echo
// gets the same syntactic treatment as the input echo, but with
@@ -1293,6 +1330,7 @@ mod tests {
kind: OutputKind::TeachingEcho,
mode_at_submission: Mode::Advanced,
styled_runs: None,
status: None,
};
let rendered = render_output_line(&line, &theme);
// [system] tag, then the dim prefix, then ≥1 SQL spans.
@@ -1397,6 +1435,7 @@ mod tests {
kind: OutputKind::System,
mode_at_submission: Mode::Simple,
styled_runs: None,
status: None,
};
let rendered = render_output_line(&line, &theme);
// tag span + single whole-line body span.
@@ -1614,6 +1653,34 @@ mod tests {
insta::assert_snapshot!("rebuild_confirm_modal_dark", snapshot);
}
// ---- ADR-0040: echo completion marker -----------------------
#[test]
fn echo_renders_running_then_marker_per_status() {
use crate::app::EchoStatus;
let mut app = App::new();
// Pending → `running: <input>` (current look).
app.output
.push_back(OutputLine::echo("drop table Orders", Mode::Advanced));
// Ok → `<input> ✓`, no `running:`.
let mut ok = OutputLine::echo("create table T with pk", Mode::Simple);
ok.status = Some(EchoStatus::Ok);
app.output.push_back(ok);
// Err → `<input> ✗`, no `running:`.
let mut err = OutputLine::echo("insert into T values (1)", Mode::Advanced);
err.status = Some(EchoStatus::Err);
app.output.push_back(err);
let out = render_to_string(&mut app, &Theme::dark(), 100, 20);
assert!(out.contains("running: drop table Orders"), "pending keeps running::\n{out}");
assert!(out.contains("create table T with pk ✓"), "ok shows ✓:\n{out}");
assert!(out.contains("insert into T values (1) ✗"), "err shows ✗:\n{out}");
assert!(
!out.contains("running: create table"),
"a completed echo drops the running: prefix:\n{out}"
);
}
// ---- Issue #13: undo confirm dialog -------------------------
#[test]
@@ -1741,30 +1808,32 @@ mod tests {
check_constraints: Vec::new(),
};
app.current_table = Some(desc);
// Mirror what the App writes when a DSL command succeeds.
app.output.push_back(OutputLine {
text: "[ok] create table Customers".to_string(),
kind: OutputKind::System,
mode_at_submission: Mode::Simple,
styled_runs: None,
});
// Mirror what the App writes when a DSL command succeeds
// (ADR-0040): the command's echo line resolves to a ✓ marker —
// there is no separate `[ok]` summary line.
let mut echo = OutputLine::echo("create table Customers", Mode::Simple);
echo.status = Some(crate::app::EchoStatus::Ok);
app.output.push_back(echo);
app.output.push_back(OutputLine {
text: " Customers".to_string(),
kind: OutputKind::System,
mode_at_submission: Mode::Simple,
styled_runs: None,
status: None,
});
app.output.push_back(OutputLine {
text: " id serial [PK]".to_string(),
kind: OutputKind::System,
mode_at_submission: Mode::Simple,
styled_runs: None,
status: None,
});
app.output.push_back(OutputLine {
text: " Name text".to_string(),
kind: OutputKind::System,
mode_at_submission: Mode::Simple,
styled_runs: None,
status: None,
});
let theme = Theme::dark();