feat(demo): Ctrl-G as a demo-mode F1 alias for casts (ADR-0047 Amendment 1)
ci / gate (push) Successful in 3m3s

The contextual hint overlay (ADR-0053) opens on F1, but F1 is an escape
sequence the autocast recorder can't emit — so casts (and presenter /
teacher sessions) couldn't trigger the most teaching-relevant overlay.

In demo mode only, Ctrl-G now aliases F1: it runs the same hint logic and
badges AS [F1], so a recording is visually identical to a real F1 press.
Ctrl-G is the only fit — Ctrl+digit (e.g. Ctrl-1) isn't encodable in a
legacy terminal (arrives as a bare `1`), and the kitty protocol that would
encode it needs escape sequences autocast can't send (and the app doesn't
enable keyboard-enhancement flags). Demo-gated, so the shipped keymap
stays F1-only; outside demo mode Ctrl-G is inert.

- app.rs: hint_key guard gains the demo-gated Ctrl-G disjunct;
  demo_badge_label maps Ctrl-G -> [F1]; 3 Tier-1 tests + badge assertion.
- ADR-0047 Amendment 1 + README index; also removed two stray
  </content> / </invoke> lines accidentally committed in the ADR file.

docs: drop three more stale "deferred" entries from CLAUDE.md — readline
shortcuts (I1b, ADR-0049), tab completion (I3), and syntax highlighting
(I4) are all implemented; only multi-line input (I1) remains open.
This commit is contained in:
claude@clouddev1
2026-06-15 21:30:37 +00:00
parent 1660a6a17c
commit 4016c3e5cd
4 changed files with 118 additions and 12 deletions
+79 -6
View File
@@ -520,10 +520,11 @@ const HISTORY_CAPACITY: usize = 1000;
/// produces a glyph of its own (and so needs no badge) — ADR-0047 D2.
///
/// The set is exactly the *otherwise-invisible* keys: motion, editing,
/// submission, and the `Ctrl-O` navigation toggle. Plain character keys
/// already appear on the input line, and `Ctrl-C` (quit) / `Ctrl+]`
/// (the caption toggle) are deliberately excluded. Pure and total, so
/// it is exhaustively unit-testable without a running app.
/// submission, the `Ctrl-O` navigation toggle, and the `Ctrl-G` F1-alias
/// (ADR-0047 amendment). Plain character keys already appear on the input
/// line, and `Ctrl-C` (quit) / `Ctrl+]` (the caption toggle) are
/// deliberately excluded. Pure and total, so it is exhaustively
/// unit-testable without a running app.
pub const fn demo_badge_label(key: &KeyEvent) -> Option<&'static str> {
match (key.code, key.modifiers) {
(KeyCode::Tab, _) => Some("[TAB]"),
@@ -541,8 +542,12 @@ pub const fn demo_badge_label(key: &KeyEvent) -> Option<&'static str> {
(KeyCode::PageDown, _) => Some("[PGDN]"),
(KeyCode::Backspace, _) => Some("[BKSP]"),
(KeyCode::Delete, _) => Some("[DEL]"),
// The only badged control chord: the ADR-0046 navigation toggle.
// The ADR-0046 navigation toggle.
(KeyCode::Char('o'), m) if m.contains(KeyModifiers::CONTROL) => Some("[CTRL-O]"),
// ADR-0047 amendment: the Ctrl-G F1-alias badges AS [F1] so a
// cast recorded by a tool that can't send F1 looks identical to a
// real F1 press. (Only consulted in demo mode — the caller gates.)
(KeyCode::Char('g'), m) if m.contains(KeyModifiers::CONTROL) => Some("[F1]"),
_ => None,
}
}
@@ -1223,7 +1228,18 @@ impl App {
// the memo-clearing completion match below. Non-empty input →
// a hint for the command being typed; empty input → expand on
// the most recent error (or a getting-started pointer).
if key.code == KeyCode::F(1) {
//
// ADR-0047 amendment: in demo mode, Ctrl-G is an alias for F1.
// The cast recorder (autocast) can't emit F1 (an escape
// sequence) but can send the single control byte Ctrl-G; it
// badges AS [F1] (see `demo_badge_label`) so the cast is visually
// identical to a real F1 press. Demo-gated, so the shipped keymap
// stays F1-only.
let hint_key = key.code == KeyCode::F(1)
|| (self.demo_mode
&& (key.code, key.modifiers)
== (KeyCode::Char('g'), KeyModifiers::CONTROL));
if hint_key {
if self.input.trim().is_empty() {
self.note_hint_for_recent_error();
} else {
@@ -3459,6 +3475,13 @@ mod tests {
demo_badge_label(&ke(KeyCode::Char('o'), KeyModifiers::CONTROL)),
Some("[CTRL-O]")
);
// ADR-0047 amendment: the Ctrl-G F1-alias badges AS [F1], so a
// cast recorded with autocast (which can't send F1) is visually
// identical to a real F1 press.
assert_eq!(
demo_badge_label(&ke(KeyCode::Char('g'), KeyModifiers::CONTROL)),
Some("[F1]")
);
}
#[test]
@@ -5756,6 +5779,56 @@ mod tests {
assert_eq!(app.input, input, "F1 must not change the buffer");
}
// ── ADR-0047 amendment: Ctrl-G is a demo-mode alias for F1 ──────
// The cast recorder (autocast) cannot emit F1 — an escape sequence —
// but Ctrl-G is a single legacy control byte it can send. Demo-gated
// so the real keymap stays F1-only, and badged as [F1] (see
// `demo_badge_label`) so a recorded cast looks identical to a genuine
// F1 press.
fn ctrl_g() -> AppEvent {
key_mod(KeyCode::Char('g'), KeyModifiers::CONTROL)
}
#[test]
fn ctrl_g_in_demo_mode_aliases_f1_on_input() {
let mut app = App::new();
app.demo_mode = true;
type_str(&mut app, "insert into T");
let input = app.input.clone();
let before = app.output.len();
app.update(ctrl_g());
assert_eq!(app.input, input, "Ctrl-G must not change the buffer (no `g` typed)");
assert!(app.output.len() > before, "Ctrl-G must emit the same hint F1 does");
}
#[test]
fn ctrl_g_in_demo_mode_aliases_f1_on_empty_input() {
let mut app = App::new();
app.demo_mode = true;
let before = app.output.len();
app.update(ctrl_g());
assert!(
app.output.len() > before,
"Ctrl-G on empty input emits the getting-started hint"
);
assert!(output_contains(&app, "press F1"));
}
#[test]
fn ctrl_g_outside_demo_mode_is_inert() {
// Not in demo mode: Ctrl-G is neither the hint alias nor a typed
// glyph (the `Char(c)` insert arm excludes CONTROL), so it falls
// through to the inert catch-all — no `g`, no hint.
let mut app = App::new();
type_str(&mut app, "insert");
let input = app.input.clone();
let before = app.output.len();
app.update(ctrl_g());
assert_eq!(app.input, input, "Ctrl-G must not insert a `g`");
assert_eq!(app.output.len(), before, "Ctrl-G does nothing when demo mode is off");
}
#[test]
fn dsl_failure_sets_hint_class_and_a_later_dsl_command_clears_it() {
let mut app = App::new();