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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user