M0: compilable skeleton — Kigi 0.1.0 fork surgery

Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,42 @@
name: ansi-execute-output
description: >-
A user `!` bash command whose stdout contains ANSI SGR renders styled plain
text in the execute scrollback. The filenames appear cleanly in the stdout
body (gutter-prefixed) rather than as literal escape sequences.
terminal:
rows: 45
cols: 110
mock:
response: "BASH_SESSION_READY"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 25000
- action: type_text
text: "go"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: BASH_SESSION_READY
timeout_ms: 45000
- action: paste_prompt
text: "! printf '\\033[1m\\033[36mbazel\\033[39;49m\\033[0m\\nAGENTS.md\\n'"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: "Run (user)"
timeout_ms: 30000
- action: wait_for_text
text: bazel
timeout_ms: 20000
- action: wait
millis: 500
# Stdout body shows clean filenames (gutter-prefixed), proving SGR was rendered
# natively rather than printed as literal `[1m[36m...` escape codes.
- action: assert_contains
text: "┃ bazel"
- action: assert_contains
text: "┃ AGENTS.md"
- action: screenshot
name: ansi-execute-native-render
note: Bash stdout shows styled bazel/AGENTS.md (SGR rendered, no literal escapes).
@@ -0,0 +1,143 @@
name: auto-compact-resize
description: >
Auto-compact on small terminals: shrinking to 18 rows (above the 16-row
short-terminal trims, at or below the 20-row auto-compact threshold) renders
the UI as if compact mode were on — the sticky prev-question header
(compact-disabled chrome) disappears — without persisting anything; growing
back to 32 rows restores it. The user setting never changes across the run.
terminal:
rows: 32
cols: 100
environment:
env:
# Pin the contextual hints master OFF so no ephemeral tip can land in the
# banner row and confuse the text assertions.
- key: KIGI_CONTEXTUAL_HINTS
value: "0"
mock:
# Blank-line-separated paragraphs render one row each (no soft-wrap merge),
# so the transcript is ~40 rows tall and the question scrolls off on every
# terminal size this scenario uses — the sticky header is the only place
# the prompt text can appear.
response: |
TOPLINE_AAA
filler line 01 keeps the transcript tall
filler line 02 keeps the transcript tall
filler line 03 keeps the transcript tall
filler line 04 keeps the transcript tall
filler line 05 keeps the transcript tall
filler line 06 keeps the transcript tall
filler line 07 keeps the transcript tall
filler line 08 keeps the transcript tall
filler line 09 keeps the transcript tall
filler line 10 keeps the transcript tall
filler line 11 keeps the transcript tall
filler line 12 keeps the transcript tall
filler line 13 keeps the transcript tall
filler line 14 keeps the transcript tall
filler line 15 keeps the transcript tall
filler line 16 keeps the transcript tall
filler line 17 keeps the transcript tall
filler line 18 keeps the transcript tall
filler line 19 keeps the transcript tall
filler line 20 keeps the transcript tall
filler line 21 keeps the transcript tall
filler line 22 keeps the transcript tall
filler line 23 keeps the transcript tall
filler line 24 keeps the transcript tall
filler line 25 keeps the transcript tall
filler line 26 keeps the transcript tall
filler line 27 keeps the transcript tall
filler line 28 keeps the transcript tall
filler line 29 keeps the transcript tall
filler line 30 keeps the transcript tall
filler line 31 keeps the transcript tall
filler line 32 keeps the transcript tall
filler line 33 keeps the transcript tall
filler line 34 keeps the transcript tall
filler line 35 keeps the transcript tall
filler line 36 keeps the transcript tall
BOTTOMLINE_QQQ
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# The first char promotes the welcome prompt to a real session; Enter submits.
- action: type_text
text: "stickyprobe"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: BOTTOMLINE_QQQ
timeout_ms: 30000
- action: wait
millis: 500
# Pinned to the bottom of a ~40-row response: the question is scrolled off,
# so "stickyprobe" on screen means the sticky prev-question header is up.
- action: assert_contains
text: "stickyprobe"
# Shrink to the 20-row auto-compact threshold zone: auto-compact engages
# and the sticky header (disabled in compact mode) vanishes.
- action: resize
rows: 18
cols: 100
- action: wait
millis: 700
- action: assert_running
- action: assert_not_contains
text: "stickyprobe"
- action: screenshot
name: auto-compact-engaged
note: 18 rows — sticky prev-question header suppressed by auto-compact.
# Grow back past the threshold: the derived value reverts to the user's
# (non-compact) setting and the sticky header returns.
- action: resize
rows: 32
cols: 100
- action: wait
millis: 700
- action: assert_contains
text: "stickyprobe"
- action: assert_not_contains
text: panicked
- action: screenshot
name: auto-compact-restored
note: Back at 32 rows — sticky prev-question header restored.
@@ -0,0 +1,27 @@
name: bracket-prompt-input
description: >
Regression — typing a lone `[` in the prompt must render immediately
(CsiFragmentFilter must not hold it until the next keystroke).
terminal:
rows: 32
cols: 100
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
- action: type_text
text: "["
- action: wait
millis: 100
- action: assert_contains
text: "["
- action: type_text
text: "link]"
- action: assert_contains
text: "[link]"
- action: screenshot
name: bracket-prompt
note: Prompt shows typed brackets without waiting for a follow-up key.
@@ -0,0 +1,27 @@
name: copy-selection
description: Validate mouse text selection copies through the real TUI clipboard path with OSC 52 readback.
terminal:
rows: 34
cols: 100
environment:
env:
- key: SSH_CONNECTION
value: "scripted-test 1 127.0.0.1 2"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_prompt
text: "copy source text"
- action: assert_contains
text: "copy source text"
- action: select_text_range
from_text: "copy"
to_text: "text"
- action: assert_osc52_contains
text: "source text"
- action: screenshot
name: copied-selection
note: Prompt text selected by mouse drag and copied via OSC 52.
@@ -0,0 +1,47 @@
name: dashboard-model-list-click
description: >
On the agent dashboard, open the /model completion list and mouse-click a
model row. The click must select the model into the dispatch prompt and must
NOT attach the underlying session row (click-through regression).
terminal:
rows: 50
cols: 120
environment:
args:
- dashboard
env:
- key: KIGI_AGENT_DASHBOARD
value: "1"
mock:
response: "UNUSED_DASHBOARD_MODEL_CLICK"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 30000
# Dashboard header / list chrome — tolerate either branding string.
- action: wait_for_text
text: Agent
timeout_ms: 20000
# Open the /model arg suggestion list on the dispatch prompt.
# Mock server exposes the default `test-model` catalog entry.
- action: type_text
text: "/model "
- action: wait_for_text
text: test-model
timeout_ms: 15000
# Click the model row in the dropdown (not the session list under it).
- action: click_text
text: test-model
- action: wait
millis: 400
# Completion accepted into the dispatch input (model id inserted as args).
- action: assert_contains
text: test-model
# Still on the dashboard — not inside a session overlay attach.
- action: assert_contains
text: Agent
- action: assert_not_contains
text: panicked
- action: screenshot
name: dashboard-model-selected-no-attach
note: Model list mouse click selected test-model without attaching a session.
@@ -0,0 +1,61 @@
name: folder-trust-prompt
description: >
Full session e2e for the client-side folder-trust prompt. With the feature on
(`KIGI_FOLDER_TRUST=1`) and the pager running in a git repo that ships a
repo-local `.mcp.json`, `decide` returns `Prompt` so the trust question renders
BEFORE any session starts. Accepting it (`y`) persists the grant, lets the
session proceed, and a submitted prompt streams the mock assistant response —
proving repo-local loading was gated until the user answered.
terminal:
rows: 50
cols: 120
environment:
env:
- key: KIGI_FOLDER_TRUST
value: "1"
# A self-built (unstamped) grok auto-trusts and never prompts; simulate a
# release build so the folder-trust prompt is actually evaluated.
- key: KIGI_TEST_VERSION
value: "0.0.0-sim"
workspace:
git_init: true
files:
".mcp.json": "{\"mcpServers\": {}}"
mock:
response: "FOLDER_TRUST_SESSION_OK assistant reply streamed after the folder was trusted."
steps:
# 1. The trust question renders before any session exists.
- action: wait_for_text
text: "Do you trust the contents of this directory"
timeout_ms: 20000
- action: assert_not_contains
text: panicked
# The decline affordance is on screen (keyboard-driven y/N answer; n = quit).
- action: assert_contains
text: "No, quit"
# 2. Accept the folder.
- action: keys
keys: "y"
# 3. The question is gone and the normal welcome / prompt is ready.
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: assert_not_contains
text: "Do you trust the contents of this directory"
# 4. Submit a prompt — the session must now start (gate answered).
- action: type_text
text: "summarize the trusted repo"
- action: keys
keys: "<Enter>"
# 5. The mock assistant response renders ⇒ the session actually started and
# repo-local loading proceeded ungated.
- action: wait_for_text
text: FOLDER_TRUST_SESSION_OK
timeout_ms: 30000
- action: assert_chat_completion
- action: assert_not_contains
text: panicked
- action: assert_running
- action: screenshot
name: folder-trust-session
note: "Trust prompt accepted; prompt submitted; mock assistant response rendered."
@@ -0,0 +1,39 @@
name: goal-slash-presession
description: >
Regression for the enterprise deploy report: with the goal
feature flag on (`KIGI_GOAL=1`), `/goal` must appear in the slash-command menu
on the welcome screen *before* the first user turn creates a session. Pre-fix
the pre-session advertise paths fail-closed and only emit AlwaysOn commands,
so `/goal` showed up only after the first turn. Typing `/goal` on the welcome
screen must surface the command (and its description) in the dropdown.
terminal:
rows: 40
cols: 100
environment:
env:
- key: KIGI_GOAL
value: "1"
mock:
response: "unused — this scenario never submits a prompt."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: assert_not_contains
text: panicked
# Sanity: nothing has been submitted yet, so no session exists.
- action: assert_not_contains
text: autonomous goal
- action: focus_prompt
- action: type_text
text: "/goal"
- action: wait
millis: 300
# The dropdown row carries the builtin's description, which only renders when
# /goal is actually advertised pre-session.
- action: assert_contains
text: autonomous goal
- action: assert_running
- action: screenshot
name: goal-in-slash-menu-presession
note: "/goal advertised in the slash menu on the welcome screen (KIGI_GOAL=1), before any prompt."
@@ -0,0 +1,47 @@
name: goal-slash-presession-disabled
description: >
Counterpart to goal-slash-presession: with the goal feature flag explicitly
OFF (`KIGI_GOAL=0` — goal mode now defaults on), `/goal` must NOT appear in
the pre-session slash menu — the gate stays fail-closed. The menu itself
still works pre-session: an always-on builtin like `/compact` shows up,
proving the absence of `/goal` is the gate and not a broken dropdown.
terminal:
rows: 40
cols: 100
environment:
env:
- key: KIGI_GOAL
value: "0"
mock:
response: "unused — this scenario never submits a prompt."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: assert_not_contains
text: panicked
- action: focus_prompt
- action: type_text
text: "/goal"
- action: wait
millis: 300
# Flag off ⇒ /goal is gated out, so its description never renders.
- action: assert_not_contains
text: autonomous goal
- action: screenshot
name: goal-absent-from-slash-menu-presession
note: "/goal NOT advertised pre-session when the flag is off (fail-closed)."
# Prove the pre-session menu is functional: an AlwaysOn builtin still shows.
# Clear the typed `/goal` (5 chars) with explicit backspaces, then type /compact.
- action: keys
keys: "<BS><BS><BS><BS><BS>"
- action: type_text
text: "/compact"
- action: wait
millis: 300
- action: assert_contains
text: Compact conversation history
- action: assert_running
- action: screenshot
name: always-on-builtin-in-slash-menu-presession
note: "AlwaysOn /compact still advertised pre-session — the menu works; only /goal is gated."
@@ -0,0 +1,40 @@
name: html-entities
description: >
Regression: HTML character entities in assistant prose must
render decoded (`&lt;` -> `<`, `&gt;` -> `>`, `&amp;` -> `&`, `&mdash;` -> em
dash) rather than as literal `&lt;`/`&gt;` text in the real pager.
terminal:
rows: 40
cols: 110
mock:
response: "Result is vec&lt;T&gt; with a &amp; b and an &mdash; dash."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "show entities"
- action: keys
keys: "<Enter>"
# The decoded angle brackets only appear once the entity transform runs.
- action: wait_for_text
text: "vec<T>"
timeout_ms: 30000
- action: assert_contains
text: "a & b"
- action: assert_contains
text: "an — dash"
# The raw entity source must not survive to the screen.
- action: assert_not_contains
text: "&lt;"
- action: assert_not_contains
text: "&gt;"
- action: assert_not_contains
text: "&amp;"
- action: assert_not_contains
text: "&mdash;"
- action: assert_not_contains
text: panicked
- action: screenshot
name: html-entities
note: Assistant prose with decoded HTML entities.
@@ -0,0 +1,38 @@
name: html-entities-rendering
description: >
Capture the real pager's rendering of HTML character entities in BOTH a
prose paragraph and a markdown table cell. Used for a before/after
comparison against the pre-fix binary (origin/main) and the with-fix
binary; no `assert_not_contains` so the same scenario runs against both.
terminal:
rows: 40
cols: 110
mock:
response: |
Prose: vec&lt;T&gt; and a &amp; b and an &mdash; dash.
| Kind | Sample |
|------|--------|
| code | vec&lt;T&gt; |
| pair | a &amp; b |
| dash | an &mdash; dash |
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "show entities"
- action: keys
keys: "<Enter>"
# Wait for the table to be fully rendered — the "dash" row is the last
# one, and its content reaches the screen once streaming completes.
- action: wait_for_text
text: "dash"
timeout_ms: 30000
- action: wait
millis: 500
- action: assert_not_contains
text: panicked
- action: screenshot
name: rendering
note: Prose paragraph + markdown table with HTML entities.
@@ -0,0 +1,49 @@
name: image-chip-preview-path-free
description: >
Path-free image chips with post-paste preview (no-pixels terminal matrix cell).
PTY harness has no TERM_PROGRAM brand, so overlay is metadata + path.
Screenshots are retained for video stills.
terminal:
rows: 40
cols: 120
mock:
response: "SCRIPTED_IMAGE_PREVIEW ok"
fixture_images:
- name: path_preview
extension: png
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_image_paths
images: [path_preview]
- action: assert_contains
text: "Image #1"
- action: assert_not_contains
text: "Image #1:"
- action: assert_contains
text: "Format:"
- action: assert_contains
text: "path_preview.png"
- action: screenshot
name: image-chip-preview-after-paste
note: Path-free Image #1 chip with metadata/path preview (no graphics protocol).
- action: keys
keys: "PREVIEW_DISMISSED_64280529"
- action: assert_contains
text: "PREVIEW_DISMISSED_64280529"
- action: assert_not_contains
text: "Format:"
- action: assert_not_contains
text: "Path:"
- action: assert_not_contains
text: "path_preview.png"
- action: screenshot
name: image-chip-after-type
note: Typing after the chip dismisses the near-cursor preview.
- action: keys
keys: "<Esc>"
- action: keys
keys: "<Esc>"
- action: assert_not_contains
text: panicked
@@ -0,0 +1,66 @@
name: image-inputs
description: "Validate all supported image input forms: path paste, file URL paste, escaped path paste, drag/drop path paste, and clipboard image paste."
terminal:
rows: 40
cols: 120
mock:
response: "SCRIPTED_IMAGE_RESPONSE received image attachments."
fixture_images:
- name: path_image
extension: png
- name: file_url_image
extension: png
- name: escaped path image
extension: png
- name: dropped_image
extension: png
- name: clipboard_image
extension: png
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_image_paths
images: [path_image]
- action: assert_contains
text: "Image #1"
- action: paste_image_file_urls
images: [file_url_image]
- action: assert_contains
text: "Image #2"
- action: paste_escaped_image_paths
images: ["escaped path image"]
- action: assert_contains
text: "Image #3"
- action: drop_images_prompt
images: [dropped_image]
- action: assert_contains
text: "Image #4"
- action: paste_clipboard_image
image: clipboard_image
- action: assert_contains
text: "Image #4"
- action: keys
keys: "<Right>"
- action: screenshot
name: image-inputs
note: Prompt with five image chips from all supported input forms.
- action: keys
keys: "<Esc>"
- action: drop_text_prompt
text: " describe these images"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: SCRIPTED_IMAGE_RESPONSE
timeout_ms: 30000
- action: assert_chat_completion
- action: assert_request_image_count
min: 4
- action: assert_inline_images
min: 4
mime_type: image/png
width: { min: 8 }
height: { min: 8 }
- action: assert_not_contains
text: panicked
@@ -0,0 +1,44 @@
name: image-normalize-corrupt-dropped
description: >-
Fix 2 + Fix 5: a CRC-corrupt PNG is rejected by the integrity check
(no image bytes reach the model), and the user-turn system reminder
surfaces an <image_dropped_notice> tag describing the drop.
terminal:
rows: 40
cols: 120
mock:
response: "SCRIPTED_IMAGE_RESPONSE received image attachments."
fixture_images:
- name: corrupt_image
extension: png
kind: crc_corrupt_png
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_image_paths
images: [corrupt_image]
- action: assert_contains
text: "Image #1"
- action: drop_text_prompt
text: "Describe the image."
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: SCRIPTED_IMAGE_RESPONSE
timeout_ms: 30000
- action: assert_chat_completion
# Fix 2: integrity check failed → image dropped before send.
- action: assert_request_image_count
min: 0
- action: assert_inline_images
min: 0
mime_type: image/png
# Fix 5: <image_dropped_notice> system-reminder reaches the model.
- action: assert_request_contains
text: "<image_dropped_notice>"
- action: screenshot
name: corrupt-dropped
note: Corrupt PNG dropped before send; drop notice forwarded to model.
- action: assert_not_contains
text: panicked
@@ -0,0 +1,54 @@
name: image-normalize-persist-atomic
description: >-
Fix 4: pasted images persist atomically into the session store and
leave no `*.tmp` orphans behind, even when multiple images are pasted
in quick succession.
terminal:
rows: 40
cols: 120
mock:
response: "SCRIPTED_IMAGE_RESPONSE received image attachments."
fixture_images:
- name: img_a
extension: png
- name: img_b
extension: png
- name: img_c
extension: png
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_image_paths
images: [img_a]
- action: assert_contains
text: "Image #1"
- action: paste_image_paths
images: [img_b]
- action: assert_contains
text: "Image #2"
- action: paste_image_paths
images: [img_c]
- action: assert_contains
text: "Image #3"
- action: keys
keys: "<Right>"
- action: keys
keys: "<Esc>"
- action: drop_text_prompt
text: " Describe the images."
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: SCRIPTED_IMAGE_RESPONSE
timeout_ms: 30000
- action: assert_chat_completion
- action: assert_request_image_count
min: 3
# Fix 4: persist_to_session is atomic — no leftover *.tmp files.
- action: assert_no_temp_artifacts
- action: screenshot
name: persist-atomic
note: Three images pasted and persisted with no .tmp orphans left behind.
- action: assert_not_contains
text: panicked
@@ -0,0 +1,86 @@
name: inline-edit-dismiss-returns-to-editor
description: >-
Esc from the inline resubmit popup returns to the still-open editor with
the edit intact (proven live by typing another char), and a second Esc
discards the edit entirely: the entry re-renders its original text and the
transcript is untouched.
terminal:
rows: 40
cols: 110
mock:
response: "DISMISS_MOCK_ANSWER from the scripted server."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "polish the docs DISMISS_BASE"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: DISMISS_MOCK_ANSWER
timeout_ms: 30000
- action: wait
millis: 500
# Tab focuses the scrollback (raw byte: ptyctl's <Tab> notation is not
# encodable by the Xterm key encoder and would be silently skipped).
- action: type_text
text: "\t"
# Footer swaps to the scrollback hint set once the pane owns keys.
- action: wait_for_text
text: "Space:prompt"
timeout_ms: 10000
# Up-arrow clamps the selection onto the first selectable entry — the
# user prompt (bare `k` needs vim mode; in the default mode a letter
# bounces focus back to the composer and types itself).
- action: keys
keys: "<Up><Up><Up><Up><Up><Up>"
- action: keys
keys: "<Enter>"
- action: type_text
text: " DISMISS_EDIT"
- action: wait_for_text
text: "DISMISS_BASE DISMISS_EDIT"
timeout_ms: 10000
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: "Resubmit from here"
timeout_ms: 15000
- action: screenshot
name: popup-over-editor
note: Inline resubmit popup raised over the open editor.
# Esc dismisses the popup and drops back into the editor, edit intact ...
- action: keys
keys: "<Esc>"
- action: wait
millis: 500
- action: assert_not_contains
text: "Resubmit from here"
- action: assert_contains
text: "DISMISS_BASE DISMISS_EDIT"
# ... and the editor is still LIVE: another typed char lands in it.
- action: keys
keys: "X"
- action: wait_for_text
text: "DISMISS_EDITX"
timeout_ms: 10000
- action: keys
keys: "<BS>"
# Second Esc discards the edit: the entry re-renders the original text.
- action: keys
keys: "<Esc>"
- action: wait
millis: 500
- action: assert_not_contains
text: "DISMISS_EDIT"
- action: assert_contains
text: "DISMISS_BASE"
- action: assert_contains
text: DISMISS_MOCK_ANSWER
- action: assert_not_contains
text: panicked
- action: assert_running
- action: screenshot
name: edit-discarded
note: Edit discarded by Esc; original prompt text restored in place.
@@ -0,0 +1,100 @@
name: inline-edit-resubmit
description: >-
Inline edit-and-resubmit happy path: select a previous user prompt in the
scrollback (Tab, k), Enter opens the in-place editor, append text, Enter
raises the inline rewind popup ("Resubmit from here", files-only row
hidden), `b` (conversation only) + `y` (last-prompt confirm) rewinds the
conversation and resubmits the EDITED text as a fresh turn. Distinct
per-turn mock sentinels prove the old tail was truncated and a new turn
streamed; no "Reverted conversation" note is shown.
terminal:
rows: 40
cols: 110
mock:
response: "FALLBACK_MOCK_RESPONSE"
turns:
- "FIRST_TURN_ANSWER from the mock server before any edit."
- "SECOND_TURN_ANSWER streamed after the inline resubmit."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "fix the bug INLINE_BASE"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: FIRST_TURN_ANSWER
timeout_ms: 30000
# Let the turn-end chrome (completion marker) settle before navigating.
- action: wait
millis: 500
# Tab focuses the scrollback; k clamps the selection upward onto the
# first selectable entry — the user prompt.
# Tab focuses the scrollback (raw byte: ptyctl's <Tab> notation is not
# encodable by the Xterm key encoder and would be silently skipped).
- action: type_text
text: "\t"
# Footer swaps to the scrollback hint set once the pane owns keys.
- action: wait_for_text
text: "Space:prompt"
timeout_ms: 10000
# Up-arrow clamps the selection onto the first selectable entry — the
# user prompt (bare `k` needs vim mode; in the default mode a letter
# bounces focus back to the composer and types itself).
- action: keys
keys: "<Up><Up><Up><Up><Up><Up>"
- action: keys
keys: "<Enter>"
# The in-place editor is live: typed text mutates the entry on screen.
- action: type_text
text: " EDITED_RESUBMIT"
- action: wait_for_text
text: "INLINE_BASE EDITED_RESUBMIT"
timeout_ms: 10000
- action: keys
keys: "<Enter>"
# Inline variant of the rewind mode-select: retitled, files-only hidden.
- action: wait_for_text
text: "Resubmit from here"
timeout_ms: 15000
- action: assert_not_contains
text: "File changes only"
- action: screenshot
name: inline-mode-select
note: Inline resubmit popup with a/b rows and no files-only option.
# b = Conversation only; target is the first prompt, so the classic
# last-prompt confirm popup follows.
- action: keys
keys: "b"
- action: wait_for_text
text: "Confirm rewind"
timeout_ms: 15000
- action: keys
keys: "y"
# The rewind lands and the EDITED prompt is resubmitted: the second
# scripted turn streams its own sentinel.
- action: wait_for_text
text: SECOND_TURN_ANSWER
timeout_ms: 30000
# Old tail is gone (truncated at the edited prompt) ...
- action: assert_not_contains
text: FIRST_TURN_ANSWER
# ... without the classic rewind's system note ...
- action: assert_not_contains
text: "Reverted conversation"
# ... and the popup is gone while the edited text stands as the prompt.
- action: assert_not_contains
text: "Resubmit from here"
- action: assert_contains
text: "INLINE_BASE EDITED_RESUBMIT"
# The resubmitted request carried the edited text to the model.
- action: assert_request_contains
text: EDITED_RESUBMIT
- action: assert_chat_completion
- action: assert_not_contains
text: panicked
- action: assert_running
- action: screenshot
name: resubmitted
note: Transcript truncated at the prompt and regenerated from the edit.
@@ -0,0 +1,77 @@
name: inline-edit-unchanged-exit
description: >-
Enter on an inline prompt editor whose text is unchanged just closes the
editor: no rewind popup, no resubmit, transcript untouched. The editor
being live is proven by a typed char mutating the entry on screen (then
backspaced away); the editor being closed afterwards is proven by a
follow-up `j` NOT appearing in the entry text (it moves the scrollback
selection instead).
terminal:
rows: 40
cols: 110
mock:
response: "UNCHANGED_MOCK_ANSWER from the scripted server."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "leave me alone UNCHANGED_BASE"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: UNCHANGED_MOCK_ANSWER
timeout_ms: 30000
- action: wait
millis: 500
# Tab focuses the scrollback (raw byte: ptyctl's <Tab> notation is not
# encodable by the Xterm key encoder and would be silently skipped).
- action: type_text
text: "\t"
# Footer swaps to the scrollback hint set once the pane owns keys.
- action: wait_for_text
text: "Space:prompt"
timeout_ms: 10000
# Up-arrow clamps the selection onto the first selectable entry — the
# user prompt (bare `k` needs vim mode; in the default mode a letter
# bounces focus back to the composer and types itself).
- action: keys
keys: "<Up><Up><Up><Up><Up><Up>"
- action: keys
keys: "<Enter>"
# Editor is live: a typed char shows up inside the entry ...
- action: keys
keys: "Z"
- action: wait_for_text
text: "UNCHANGED_BASEZ"
timeout_ms: 10000
# ... then restore the original text so the submit is a no-op.
- action: keys
keys: "<BS>"
- action: keys
keys: "<Enter>"
- action: wait
millis: 800
# No rewind popup of either flavor ever appears.
- action: assert_not_contains
text: "Resubmit from here"
- action: assert_not_contains
text: "What do you want to rewind"
# Editor closed: j navigates the scrollback instead of inserting.
- action: keys
keys: "j"
- action: wait
millis: 300
- action: assert_not_contains
text: "UNCHANGED_BASEj"
# Transcript intact.
- action: assert_contains
text: "UNCHANGED_BASE"
- action: assert_contains
text: UNCHANGED_MOCK_ANSWER
- action: assert_not_contains
text: panicked
- action: assert_running
- action: screenshot
name: unchanged-exit
note: Unchanged Enter closed the editor silently; transcript untouched.
@@ -0,0 +1,41 @@
name: inline-image-memory
description: "Paste one PNG, hammer redraws, verify Kitty graphics use transmit-once + place (not a=T every frame)."
terminal:
rows: 40
cols: 120
mock:
response: "unused — scenario does not submit"
fixture_images:
- name: memory_test_image
extension: png
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_clipboard_image
image: memory_test_image
- action: assert_contains
text: "Image #1"
# Move cursor to image chip to trigger overlay preview graphics.
- action: keys
keys: "<Left><Left><Left>"
- action: wait_for_kitty_graphics
min: 1
timeout_ms: 10000
# Hammer redraws — each should NOT re-upload the full image.
- action: keys
keys: "<Right><Left><Right><Left><Right><Left><Right><Left>"
- action: keys
keys: "hello world this is noise to force redraws"
- action: keys
keys: "<Backspace><Backspace><Backspace><Backspace><Backspace>"
- action: keys
keys: "<Right><Right><Right><Right>"
- action: wait_for_kitty_graphics
min: 1
timeout_ms: 5000
- action: assert_kitty_graphics
min: 1
- action: screenshot
name: inline-image-memory
note: Single pasted PNG after redraw hammering.
@@ -0,0 +1,47 @@
name: input-modalities
description: Validate scripted clipboard paste, mouse click, double/triple click, scroll, mouse selection, and drop-style input events.
terminal:
rows: 36
cols: 100
mock:
response: "SCRIPTED_INPUT_RESPONSE from pasted and dropped content."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_prompt
text: "clipboard paste text"
- action: assert_contains
text: "clipboard paste text"
- action: double_click_text
text: "paste"
- action: triple_click_text
text: "paste"
- action: select_text_range
from_text: "clipboard"
to_text: "text"
- action: scroll_at
target:
kind: prompt
direction: down
count: 3
- action: scroll_at
target:
kind: prompt
direction: up
count: 2
- action: drop_text_prompt
text: " dropped text"
- action: assert_contains
text: "dropped text"
- action: screenshot
name: input-modalities
note: Prompt after click, bracketed paste, double/triple click, scroll, selection drag, and drop text.
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: SCRIPTED_INPUT_RESPONSE
timeout_ms: 30000
- action: assert_chat_completion
- action: assert_not_contains
text: panicked
@@ -0,0 +1,62 @@
name: mermaid-affordances
description: >
A ```mermaid fence renders inline as Unicode box-drawing art (drawn by the
markdown renderer), with a clickable affordance row beneath it led by a dim
"◇ mermaid" label ([Open Image] [Copy Image Path] [Copy Source]). Rendering of
the full-fidelity PNG is lazy (on click), so no diagram PNG is produced just by
detection. The PNG is NEVER transmitted as an inline image, so the treatment is
identical in every terminal and no Kitty graphics escape is emitted — asserted
against the raw PTY output.
terminal:
rows: 40
cols: 100
mock:
response: |
Here is a flow:
```mermaid
flowchart TD
A --> B
B --> C
```
Done rendering the flow.
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "draw me a diagram"
- action: keys
keys: "<Enter>"
# Mermaid detection runs when the turn finishes, so the affordance row appears
# only after the response is complete. Poll for one of its buttons rather than
# asserting mid-stream.
- action: wait_for_text
text: "[Copy Source]"
timeout_ms: 30000
- action: assert_chat_completion
# The fence is rendered inline as Unicode art by the markdown renderer: a TD
# flowchart routes forward edges through "▼" arrowheads. Asserting the art (not
# the source keyword) confirms the inline diagram replaced the raw source.
- action: assert_contains
text: "▼"
# The full affordance row is plain text: [Open Image] [Copy Image Path] [Copy Source].
- action: assert_contains
text: "[Open Image]"
# The affordance row is led by the dim "◇ mermaid" label.
- action: assert_contains
text: "◇ mermaid"
# Rendering is LAZY: detection alone never dispatches a PNG render, so the row's
# transient "rendering diagram…" hint is absent until an [Open Image]/[Copy
# Image Path] click. This distinguishes lazy from eager without depending on the
# OS clipboard/viewer.
- action: assert_not_contains
text: "rendering diagram"
# The full-fidelity PNG is text + mouse hit-rects only, never an inline image.
- action: assert_no_kitty_graphics
- action: assert_not_contains
text: panicked
- action: screenshot
name: mermaid-affordances
note: Mermaid fence rendered inline as Unicode art with a clickable affordance row.
@@ -0,0 +1,52 @@
name: mid-text-skill-token-echo
description: >
A plain prompt referencing a skill mid-message
("great /test-skill do it") keeps the composer's teal token highlight in the
scrollback echo after submit. The workspace seeds a repo-local
.grok/skills/test-skill/SKILL.md so the real shell advertises the skill; the
screenshots capture the styled composer (pre-submit) and the styled echo
(post-submit) — the styled JSON artifacts carry the per-run fg colors for
visual verification and demo material.
terminal:
rows: 40
cols: 100
environment:
args: ["--trust"]
workspace:
git_init: true
files:
".grok/skills/test-skill/SKILL.md": "---\nname: test-skill\ndescription: Test skill for echo styling.\n---\n\nDo the thing.\n"
mock:
response: "MIDTEXT_SKILL_ECHO_DONE assistant reply after the mid-text skill prompt."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: assert_not_contains
text: panicked
- action: focus_prompt
- action: type_text
text: "great /test-skill do it"
- action: wait_for_text
text: "great /test-skill do it"
timeout_ms: 10000
# Give the shell time to advertise the skill so the composer highlight (and
# the submit-time range capture) is in effect before Enter.
- action: wait
millis: 2000
- action: screenshot
name: composer-token-highlighted
note: "Composer with the mid-text /test-skill token teal while typing."
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: MIDTEXT_SKILL_ECHO_DONE
timeout_ms: 30000
- action: assert_contains
text: "great /test-skill do it"
- action: assert_not_contains
text: panicked
- action: assert_running
- action: screenshot
name: echo-token-highlighted
note: "Scrollback echo keeps the /test-skill token teal after submit (compare styled JSON with the composer capture)."
@@ -0,0 +1,60 @@
name: minimal-flush-left
description: >-
Minimal mode is flush-left: the welcome card paints edge-to-edge, and the
prompt, status row, committed user/agent blocks, and info bar share that left
edge (no block_pad / accent gutter inset). Screenshots capture idle and
post-response for before/after PR review.
terminal:
rows: 30
cols: 100
# Minimal's inline-viewport startup probe needs cursor-position replies;
# without them --minimal silently downgrades to full-height inline.
respond_to_queries: true
environment:
args: ["--minimal", "--no-leader"]
mock:
response: "FLUSH_LEFT_RESPONSE — short answer aligned with the welcome card."
steps:
- action: wait_for_text
text: "minimal · /help"
timeout_ms: 20000
- action: assert_not_contains
text: panicked
- action: screenshot
name: idle
note: >-
Idle minimal: welcome card at col 0; status "minimal · /help", prompt ,
and info bar should also start at column 0 (no 3-col left pad).
- action: type_text
text: "hello flush left"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: FLUSH_LEFT_RESPONSE
timeout_ms: 30000
- action: assert_chat_completion
- action: wait_for_text
text: "minimal · /help"
timeout_ms: 20000
- action: assert_not_contains
text: panicked
- action: screenshot
name: after-response
note: >-
After commit: user prompt line, agent response, status, and input are all
flush-left with the welcome card's outer border.
- action: type_text
text: "/tra"
- action: wait_for_text
text: "View the full conversation transcript"
timeout_ms: 10000
- action: screenshot
name: slash-menu
note: >-
Slash dropdown: the selected row ( /transcript …) and divider start at
column 0, flush with the prompt above; the dim nav footer sits at col 1
like the /resume and /mcps panels.
- action: keys
keys: "<Esc>"
- action: assert_not_contains
text: panicked
@@ -0,0 +1,45 @@
name: minimal-transcript-hint
description: >-
Minimal (scrollback-native) mode advertises the full-transcript shortcut in
the info row under the prompt ("ctrl+o transcript"), minimal's stand-in for
the shortcuts bar it never renders. Verifies the hint is present at idle and
survives a prompt/response round-trip.
terminal:
rows: 30
cols: 100
# Minimal's inline-viewport startup probe needs cursor-position replies;
# without them --minimal silently downgrades to full-height inline.
respond_to_queries: true
environment:
args: ["--minimal", "--no-leader"]
mock:
response: "MINIMAL_SCENARIO_RESPONSE from the mock server."
steps:
- action: wait_for_text
text: "minimal · /help"
timeout_ms: 20000
- action: assert_contains
text: "ctrl+o transcript"
- action: assert_not_contains
text: panicked
- action: screenshot
name: idle
note: Idle minimal mode; the info row under the prompt ends with the "ctrl+o transcript" hint.
- action: type_text
text: "hello minimal"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: MINIMAL_SCENARIO_RESPONSE
timeout_ms: 30000
- action: assert_chat_completion
- action: wait_for_text
text: "minimal · /help"
timeout_ms: 20000
- action: assert_contains
text: "ctrl+o transcript"
- action: assert_not_contains
text: panicked
- action: screenshot
name: after-response
note: After a mocked response commits to scrollback the hint is still in the info row.
@@ -0,0 +1,40 @@
name: minimal-transcript-hint-apple-terminal
description: >-
On Apple Terminal Ctrl+O doubles as the (only working) interject chord. At
idle / with an empty composer an interject is a silent no-op, so minimal's
transcript remap keeps the key — the "Ctrl+O appears dead on Mac" fix. The
info row advertises "ctrl+o transcript" at idle, and pressing Ctrl+O acts on
the transcript (empty conversation → the "nothing to show" system block).
terminal:
rows: 30
cols: 100
# Minimal's inline-viewport startup probe needs cursor-position replies;
# without them --minimal silently downgrades to full-height inline.
respond_to_queries: true
environment:
args: ["--minimal", "--no-leader"]
env:
# Detected as TerminalName::AppleTerminal → InterjectPrompt binds Ctrl+O.
- { key: TERM_PROGRAM, value: Apple_Terminal }
steps:
- action: wait_for_text
text: "minimal · /help"
timeout_ms: 20000
- action: assert_contains
text: "ctrl+o transcript"
- action: assert_not_contains
text: panicked
- action: screenshot
name: apple-idle
note: Apple Terminal detection, idle — the info row still advertises ctrl+o transcript.
- action: keys
keys: "<C-o>"
- action: wait_for_text
text: "No conversation transcript to view yet"
timeout_ms: 20000
- action: assert_running
- action: assert_not_contains
text: panicked
- action: screenshot
name: apple-after-ctrl-o
note: Ctrl+O at idle acts on the transcript instead of silently no-op'ing into interject.
@@ -0,0 +1,24 @@
name: mock-response
description: Validate a real prompt submission renders mocked assistant output and captures the resulting UI.
terminal:
rows: 40
cols: 110
mock:
response: "SCRIPTED_MOCK_RESPONSE rendered by the real pager from the local mock inference server."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "hello scripted tui"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: SCRIPTED_MOCK_RESPONSE
timeout_ms: 30000
- action: assert_chat_completion
- action: assert_not_contains
text: panicked
- action: screenshot
name: response
note: Prompt submission and mocked assistant response.
@@ -0,0 +1,33 @@
name: paste-chip-double-click
description: >
Double-clicking a `[Pasted: N lines]` chip in the prompt expands it back
into plain editable text; a single click only places the cursor on it.
terminal:
rows: 36
cols: 100
mock:
response: "UNUSED_PASTE_CHIP_DOUBLE_CLICK"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
# A 4-line bracketed paste collapses into a chip.
- action: paste_prompt
text: "alpha\nbravo\ncharlie\ndelta"
- action: wait_for_text
text: "[Pasted: 4 lines]"
timeout_ms: 10000
- action: double_click_text
text: "Pasted:"
- action: wait
millis: 300
# Chip expanded: label gone, buffer text plain and editable.
- action: assert_not_contains
text: "Pasted:"
- action: assert_contains
text: "charlie"
- action: assert_not_contains
text: panicked
- action: screenshot
name: paste-chip-expanded
note: Paste chip expanded to plain text after double-click.
@@ -0,0 +1,35 @@
name: paste-chip-repaste-expands
description: >
Pasting the same multiline content twice expands the `[Pasted: N lines]`
chip back into plain text instead of inserting a duplicate chip.
terminal:
rows: 36
cols: 100
mock:
response: "UNUSED_PASTE_CHIP_REPASTE"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: paste_prompt
text: "alpha\nbravo\ncharlie\ndelta"
- action: wait_for_text
text: "[Pasted: 4 lines]"
timeout_ms: 10000
- action: paste_prompt
text: "alpha\nbravo\ncharlie\ndelta"
- action: wait
millis: 300
# Chip expanded, not duplicated: label gone, plain text present once.
- action: assert_not_contains
text: "Pasted:"
- action: assert_contains
text: "charlie"
# A duplicate insert would abut the copies as "deltaalpha".
- action: assert_not_contains
text: "deltaalpha"
- action: assert_not_contains
text: panicked
- action: screenshot
name: paste-chip-repaste-expanded
note: Second identical paste expanded the chip instead of adding another.
@@ -0,0 +1,45 @@
name: path-space-hyperlink
description: >
Tutor report (bottlerocket): plain-text file paths with spaces only partially
linkify — OSC 8 / click / underline stopped at the first space in
`Demo App.app`. Stream the repro path through the mock agent and
capture the rendered UI so CI / local PTY runs prove the full path stays one
clickable region.
terminal:
rows: 40
cols: 160
# Native OSC 8 brand so the pager actually emits hyperlink sequences in the
# PTY stream (default harness brand is Unknown and skips OSC 8).
environment:
env:
- key: TERM_PROGRAM
value: WezTerm
mock:
response: |
PATHSPACE_SENTINEL open "/Users/alice/src/app/release/mac-arm64/Demo App.app"
(or release/mac/ on Intel).
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "show path"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: PATHSPACE_SENTINEL
timeout_ms: 30000
- action: wait_for_text
text: "App.app"
timeout_ms: 10000
- action: wait
millis: 400
- action: assert_contains
text: "Demo App.app"
- action: assert_not_contains
text: panicked
- action: screenshot
name: spaced-path
note: >
Full macOS app-bundle path with a space must render as one link region
(not truncated at Demo).
@@ -0,0 +1,30 @@
name: plan-nudge-opt-out-no-show
description: >
Explicit opt-out: contextual hints now ship default-ON, so this scenario pins
the master env `KIGI_CONTEXTUAL_HINTS=0` (global kill switch) before typing a
planning keyword — the "Planning? Check out plan mode via shift+tab" banner
must NOT appear. Mirrors plan_nudge_shows (which pins `=1`), exercising the
disabled path explicitly.
terminal:
rows: 32
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "0"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
- action: type_text
text: "lets plan the refactor"
- action: wait
millis: 700
- action: assert_not_contains
text: "plan mode via"
- action: screenshot
name: plan-nudge-opt-out
note: No plan-nudge banner when the contextual-hints flag is explicitly disabled.
@@ -0,0 +1,44 @@
name: plan-nudge-shows
description: >
Plan-nudge end-to-end with the contextual-hints master pinned on
(`KIGI_CONTEXTUAL_HINTS=1` for deterministic env control; the feature now ships
default-ON): typing a planning keyword into the prompt shows the "Planning?
Check out plan mode via shift+tab" banner. The seen cap is in-memory only, so a
fresh run starts uncapped; the banner clears on its own ~3s TTL.
terminal:
rows: 32
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# Promote the welcome prompt to a real session with a keyword-free prefix
# (the agent view is where the plan-nudge routing lives), and let the session
# settle to idle — the nudge is gated on `session.state.is_idle()`, so the
# keyword must cross in after the freshly-created session is ready.
- action: type_text
text: "lets "
- action: wait_for_text
text: "test-model"
timeout_ms: 10000
- action: wait
millis: 1500
# Cross into the "plan" keyword now that the session is idle → rising-edge
# nudge fires once.
- action: type_text
text: "plan the refactor"
- action: wait_for_text
text: "plan mode via"
timeout_ms: 10000
- action: assert_not_contains
text: panicked
- action: screenshot
name: plan-nudge-shown
note: Plan-nudge banner after typing a planning keyword.
@@ -0,0 +1,52 @@
name: release-notes-scroll
description: >
Open /release-notes (DocViewer modal) and verify keyboard + mouse-wheel scrolling
moves the changelog body. Seeds CHANGELOG cache offline via setup is not available
in YAML, so this scenario relies on CDN/cache if present and still asserts the
modal chrome + scroll affordances. Prefer the pty_e2e Rust tests for deterministic
offline seeding; this YAML exercises the scripted ptyctl runner path.
terminal:
rows: 40
cols: 110
mock:
response: "SCRIPTED_RELEASE_NOTES_SCROLL unused for this scenario."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
# Promote welcome → session and open release notes (uses CDN or disk cache).
- action: type_text
text: "/release-notes"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: Release Notes
timeout_ms: 20000
- action: assert_contains
text: scroll
# Keyboard scroll through the modal body.
- action: keys
keys: "<Down><Down><Down><Down><Down><Down><Down><Down><Down><Down>"
- action: wait
millis: 200
- action: keys
keys: "jjjjjjjjjj"
- action: wait
millis: 200
# Mouse wheel at the center of the modal.
- action: scroll
row: 20
col: 55
direction: down
count: 15
- action: wait
millis: 250
- action: assert_contains
text: Release Notes
- action: assert_not_contains
text: panicked
- action: keys
keys: "<Esc>"
- action: screenshot
name: release-notes-after-scroll
note: Release notes modal after keyboard + wheel scroll.
@@ -0,0 +1,97 @@
name: shortcuts-help-detail
description: >
End-to-end cheatsheet flow exercising the full shortcuts-help stack: open the
Keyboard Shortcuts modal from a live session, inline-expand a hint with the
right arrow and collapse it with the left arrow (directional collapse),
open the man-style detail screen with Enter (pattern B) showing the action's
long_help, return to the browse list with Esc (not close), then dismiss the
modal with the global Ctrl+X close chord.
Open/close uses Ctrl+X rather than Ctrl+. : both are bound to ShortcutsHelp
(When::AgentScreen) and both are the in-modal global-close chord, but Ctrl+X
has a legacy byte (0x18) the PTY reliably delivers whereas Ctrl+. has no
Xterm-encodable byte. The prompt ignores Ctrl+X, so it bubbles to the
AgentScreen binding.
terminal:
rows: 40
cols: 110
mock:
response: "MOCK_SHORTCUTS_E2E assistant reply for the cheatsheet scenario."
steps:
# Reach a session so ShortcutsHelp (When::AgentScreen) is reachable.
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "hello cheatsheet"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: MOCK_SHORTCUTS_E2E
timeout_ms: 30000
# Open the cheatsheet via the reliably-encodable Ctrl+X (== Ctrl+. binding).
- action: keys
keys: "<C-x>"
- action: wait_for_text
text: Keyboard Shortcuts
timeout_ms: 15000
# Browse-mode chrome + shipped affordances (expand / collapse / detail hints).
- action: assert_contains
text: Essentials
- action: assert_contains
text: Enter details
- action: assert_contains
text: collapse
- action: assert_not_contains
text: panicked
# Move selection off the section header onto the CancelTurn hint
# (Essentials order: send, prompt, scrollback, cancel).
- action: keys
keys: "<Down><Down><Down>"
- action: wait
millis: 200
# Inline-expand (right arrow): the action's long_help renders under the row.
- action: keys
keys: "<Right>"
- action: wait_for_text
text: Interrupts the
timeout_ms: 15000
# Collapse the same row with the left arrow (directional left-collapse).
- action: keys
keys: "<Left>"
- action: wait
millis: 200
- action: assert_not_contains
text: Interrupts the
# Enter opens the in-modal man-style detail screen (pattern B).
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: Esc back
timeout_ms: 15000
- action: assert_contains
text: Interrupts the
- action: assert_not_contains
text: panicked
# Esc returns to the browse list (must NOT close the modal).
- action: keys
keys: "<Esc>"
- action: wait_for_text
text: Enter details
timeout_ms: 15000
- action: assert_contains
text: Keyboard Shortcuts
# Global close: Ctrl+X dismisses the modal and restores the session.
- action: keys
keys: "<C-x>"
- action: wait
millis: 300
- action: assert_not_contains
text: Keyboard Shortcuts
- action: assert_contains
text: MOCK_SHORTCUTS_E2E
- action: assert_not_contains
text: panicked
- action: screenshot
name: shortcuts-help-detail-closed
note: Cheatsheet inline-expand / detail / collapse flow complete; modal dismissed via Ctrl+X.
@@ -0,0 +1,57 @@
name: slash-resize-storm
description: >
Regression — resizing while the slash-command dropdown is open must not crash
the pager (ratatui "index outside of buffer" on item row paint).
terminal:
rows: 40
cols: 100
mock:
response: "SCRIPTED_SLASH_RESIZE_RESPONSE"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# Open the slash command menu (full list of matches).
- action: type_text
text: "/"
- action: wait
millis: 200
- action: assert_running
- action: assert_not_contains
text: panicked
# Height shrink while dropdown is painted — the historical failure mode.
- action: resize
rows: 12
cols: 100
- action: wait
millis: 150
- action: assert_running
- action: resize
rows: 8
cols: 80
- action: wait
millis: 150
- action: assert_running
# Oscillate a few times like a dragged IDE panel.
- action: resize
rows: 28
cols: 120
- action: wait
millis: 80
- action: resize
rows: 10
cols: 60
- action: wait
millis: 80
- action: resize
rows: 6
cols: 40
- action: wait
millis: 150
- action: assert_running
- action: assert_not_contains
text: panicked
- action: screenshot
name: slash-after-resize-storm
note: Pager still alive after slash dropdown + aggressive height shrinks.
@@ -0,0 +1,59 @@
name: small-screen-tip-band
description: >
Small-screen tip happy path with the contextual-hints master pinned on: a 24
row terminal sits in the 21..=28 band (above the 20-row auto-compact
threshold), so the first stable agent-view draw shows the one-shot "Tight on
space? Try /compact-mode" banner. It expires on its ~3s TTL and — because the
trigger is a per-run one-shot — never re-shows, even after dipping into the
auto-compact zone and resizing back into the band.
terminal:
rows: 24
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
# The prompt marker renders at every height; the first char promotes the
# welcome prompt to a real session (the agent view, where the tip fires).
- action: wait_for_text
text: ""
timeout_ms: 20000
- action: focus_prompt
- action: type_text
text: "hi"
- action: wait_for_text
text: "Tight on space? Try /compact-mode"
timeout_ms: 15000
- action: screenshot
name: small-screen-tip-shown
note: One-shot /compact-mode tip on a 24-row terminal.
# Past the ~3s TTL the banner clears on its own.
- action: wait
millis: 4500
- action: assert_not_contains
text: "Tight on space"
# Dip into the auto-compact zone (19 <= 20) and back into the band: the
# one-shot must not re-trigger.
- action: resize
rows: 19
cols: 100
- action: wait
millis: 400
- action: resize
rows: 24
cols: 100
- action: wait
millis: 400
- action: type_text
text: "x"
- action: wait
millis: 600
- action: assert_not_contains
text: "Tight on space"
- action: assert_running
- action: screenshot
name: small-screen-tip-no-reshow
note: No re-show after resizes — the trigger is one-shot per run.
@@ -0,0 +1,35 @@
name: small-screen-tip-no-show-tall
description: >
No-show edge above the band: a 40-row terminal is roomy, so the first stable
agent-view draw consumes the one-shot evaluation without showing the
"Tight on space? Try /compact-mode" banner — even with the contextual-hints
master pinned on.
terminal:
rows: 40
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# Promote the welcome prompt to a real session; the status bar's model name
# proves the agent view has drawn (the trigger's evaluation point).
- action: type_text
text: "hi"
- action: wait_for_text
text: "test-model"
timeout_ms: 15000
- action: wait
millis: 1200
- action: assert_not_contains
text: "Tight on space"
- action: assert_running
- action: screenshot
name: small-screen-tip-absent-tall
note: No /compact-mode tip on a 40-row terminal.
@@ -0,0 +1,36 @@
name: small-screen-tip-no-show-tiny
description: >
No-show edge below the band: at 14 rows auto-compact is active instead (and
the banner row cannot render at or below 16 rows), so the "Tight on space?
Try /compact-mode" tip never appears — even with the contextual-hints master
pinned on.
terminal:
rows: 14
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
# The welcome menu may be trimmed at 14 rows; the prompt marker always paints.
- action: wait_for_text
text: ""
timeout_ms: 20000
- action: focus_prompt
# Promote the welcome prompt to a real session; the status bar's model name
# proves the agent view has drawn (the trigger's evaluation point).
- action: type_text
text: "hi"
- action: wait_for_text
text: "test-model"
timeout_ms: 15000
- action: wait
millis: 1200
- action: assert_not_contains
text: "Tight on space"
- action: assert_running
- action: screenshot
name: small-screen-tip-absent-tiny
note: No /compact-mode tip at 14 rows (auto-compact active instead).
@@ -0,0 +1,119 @@
name: table-cell-selection
description: Validate table-aware selection — a drag inside a rendered table selects by cell (within-cell text, cross-cell TSV) instead of sweeping whole lines across columns.
terminal:
rows: 40
cols: 100
environment:
env:
# Force the remote clipboard route so copies surface as OSC 52 (readback).
- key: SSH_CONNECTION
value: "scripted-test 1 127.0.0.1 2"
# word_select persists highlights (no 150ms flash race) and enables the
# multi-click ladder the whole-table triple-click rides on.
config_toml: |
[ui]
keep_text_selection = "word_select"
mock:
response: |
Here is the roster:
| Animal | Sound |
| --- | --- |
| ZebraOne | `NeighNoise` |
| BravoCat | MeowNoise |
| Quick Fox | IgnoreMe |
Done.
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: type_text
text: "show the roster table"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: BravoCat
timeout_ms: 30000
# Column drag: copies that column only — no other column, no │ glyphs.
- action: select_text_range
from_text: "ZebraOne"
to_text: "BravoCat"
- action: assert_osc52_contains
text: "ZebraOne\nBravoCat"
- action: assert_osc52_not_contains
text: "│"
- action: assert_osc52_not_contains
text: "NeighNoise"
# Highlight matches the copy: cell content only.
- action: assert_highlight_run
at_text: "ZebraOne"
equals: "ZebraOne"
- action: assert_highlight_run
at_text: "BravoCat"
equals: "BravoCat"
- action: assert_text_not_highlighted
text: "NeighNoise"
# Dead-zone: overshooting onto the border + neighbor padding must not
# escalate — copy and highlight stay the single cell.
- action: select_text_range
from_text: "ZebraOne"
to_text: "ZebraOne"
to_offset_cols: 3
- action: assert_highlight_run
at_text: "ZebraOne"
equals: "ZebraOne"
- action: assert_text_not_highlighted
text: "NeighNoise"
- action: assert_osc52_not_contains
text: "NeighNoise"
# Rectangle across both columns: TSV (tab-separated cells, newline rows).
# NeighNoise is an inline-code chip: it must join the uniform highlight
# band (assert_highlight_run checks background uniformity).
- action: select_text_range
from_text: "ZebraOne"
to_text: "MeowNoise"
- action: assert_osc52_contains
text: "ZebraOne\tNeighNoise\nBravoCat\tMeowNoise"
- action: assert_highlight_run
at_text: "NeighNoise"
equals: "NeighNoise"
# Within-cell drag stays clamped to the cell.
- action: select_text_range
from_text: "Quick"
to_text: "Fox"
- action: assert_osc52_contains
text: "Quick Fox"
- action: assert_osc52_not_contains
text: "IgnoreMe"
# Interior spaces stay highlighted, edge padding does not.
- action: assert_highlight_run
at_text: "Quick"
equals: "Quick Fox"
- action: assert_text_not_highlighted
text: "IgnoreMe"
# Grid-line-anchored drag stays linear: copy carries border glyphs
# (impossible for any TSV copy).
- action: select_text_range
from_text: "├"
to_text: "BravoCat"
- action: assert_osc52_contains
text: "┼"
- action: assert_osc52_contains
text: "│ ZebraOne"
# Triple-click on a grid line is the whole-table TSV shortcut.
- action: triple_click_text
text: "├"
- action: assert_osc52_contains
text: "Animal\tSound\nZebraOne\tNeighNoise\nBravoCat\tMeowNoise\nQuick Fox\tIgnoreMe"
- action: assert_highlight_run
at_text: "Animal"
equals: "Animal"
- action: assert_highlight_run
at_text: "IgnoreMe"
equals: "IgnoreMe"
- action: assert_not_contains
text: panicked
- action: screenshot
name: table-grid-selection
note: Cell-aware selection in a rendered markdown table.
@@ -0,0 +1,51 @@
name: tool-header-path-selection
description: >-
Drag-select on a tool command header copies only the meaningful operand
(command / path), not the label prefix. Uses a user `!` bash execute block
because the label style is `Run {cmd}` and the command is the selectable span
— same Selectable::Spans mechanism as Read {path}.
terminal:
rows: 45
cols: 110
environment:
env:
- key: SSH_CONNECTION
value: "scripted-test 1 127.0.0.1 2"
mock:
response: "BASH_SESSION_READY"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 25000
- action: type_text
text: "go"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: BASH_SESSION_READY
timeout_ms: 45000
# Unique token so we can locate the command span unambiguously.
- action: paste_prompt
text: "! printf 'TOOL_HDR_SEL_TOKEN\\n'"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: "TOOL_HDR_SEL_TOKEN"
timeout_ms: 30000
- action: wait
millis: 400
# Select only the command operand (not the "Run " / "$ " label prefix).
- action: select_text_range
from_text: "printf"
to_text: "TOOL_HDR_SEL_TOKEN"
- action: assert_osc52_contains
text: "TOOL_HDR_SEL_TOKEN"
# Label prefix must not appear alone as the copied selection payload.
# (Full line copy would include "Run " or "$ " before the command.)
- action: assert_osc52_not_contains
text: "Run printf"
- action: assert_osc52_not_contains
text: "$ printf"
- action: screenshot
name: tool-header-operand-selected
note: Tool header selection copies the command operand, not the Run/$ label.
@@ -0,0 +1,41 @@
name: undo-tip-clear-shows
description: >
Undo tip end-to-end with the contextual-hints master pinned on
(`KIGI_CONTEXTUAL_HINTS=1` for deterministic env control; the feature now ships
default-ON): wiping a substantial prompt draft with Ctrl+U shows the "… to
undo" banner. The seen cap is in-memory only, so a fresh run starts uncapped;
the banner clears on its own ~3s TTL (no early retire on undo).
terminal:
rows: 32
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# The first printable char promotes the welcome prompt to a real session
# (the agent view, where the undo-tip routing lives); the rest accumulate
# into that draft. 25 chars clears the detector's FIRE_PEAK_LEN (20).
- action: type_text
text: "aaaaaaaaaaaaaaaaaaaaaaaaa"
- action: wait
millis: 400
- action: assert_contains
text: "aaaaaaaaaa"
# Ctrl+U kills the whole line: a substantial, recoverable wipe → tip shows.
- action: keys
keys: "<C-u>"
- action: wait_for_text
text: "to undo"
timeout_ms: 10000
- action: assert_not_contains
text: panicked
- action: screenshot
name: undo-tip-shown
note: Undo tip banner after a Ctrl+U wipe of a substantial draft.
@@ -0,0 +1,42 @@
name: undo-tip-completion-accept-no-show
description: >
No-show edge: accepting an @-file completion is not a wipe and must never
show the undo tip, even though the accept replaces the typed @query with a
file ref. (The precise residue-band regression — a long query collapsing to
a tiny chip — is unit-tested in prompt_widget via FileSearchState test state;
the real fuzzy matcher cannot match a >=20-char query to a tiny filename, so
this scenario covers the end-to-end "accept never hints" behavior.)
terminal:
rows: 32
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
workspace:
files:
"undo_tip_completion_target.md": "fixture file for @-completion accept"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# Type an @-query that matches the seeded file; wait for the dropdown.
- action: type_text
text: "@undo_tip_completion_targ"
- action: wait_for_text
text: "undo_tip_completion_target.md"
timeout_ms: 10000
# Accept with Tab — a completion, not a wipe.
- action: keys
keys: "<Tab>"
- action: wait
millis: 600
- action: assert_not_contains
text: "to undo"
- action: assert_running
- action: screenshot
name: undo-tip-completion-accept
note: Accepting an @-file completion shows no undo tip.
@@ -0,0 +1,39 @@
name: undo-tip-ctrl-c-clear-shows
description: >
Ctrl+C variant of the undo tip with the contextual-hints master pinned on
(`KIGI_CONTEXTUAL_HINTS=1` for deterministic env control; the feature now ships
default-ON): with a substantial draft present, Ctrl+C clears the textarea (the
cancel-with-draft fall-through) — a recoverable wipe that shows the "… to undo"
banner.
terminal:
rows: 32
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# Type first so Ctrl+C lands in the agent view with a non-empty draft
# (clear), not on the welcome screen (where bare Ctrl+C would quit).
- action: type_text
text: "bbbbbbbbbbbbbbbbbbbbbbbbb"
- action: wait
millis: 400
- action: assert_contains
text: "bbbbbbbbbb"
- action: keys
keys: "<C-c>"
- action: wait_for_text
text: "to undo"
timeout_ms: 10000
- action: assert_not_contains
text: panicked
- action: screenshot
name: undo-tip-ctrl-c-shown
note: Undo tip after a Ctrl+C clear of a substantial draft.
@@ -0,0 +1,34 @@
name: undo-tip-short-draft-no-show
description: >
No-show edge: wiping a SHORT draft (below the detector's FIRE_PEAK_LEN of 20
chars) must NOT show the undo tip — only substantial wipes are worth hinting.
terminal:
rows: 32
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# 10 chars — below the 20-char peak threshold.
- action: type_text
text: "cccccccccc"
- action: wait
millis: 400
- action: assert_contains
text: "cccccccccc"
- action: keys
keys: "<C-u>"
- action: wait
millis: 700
- action: assert_not_contains
text: "to undo"
- action: screenshot
name: undo-tip-short-draft
note: No tip after wiping a short (<20 char) draft.
@@ -0,0 +1,42 @@
name: undo-tip-short-terminal-no-show
description: >
No-show edge: on a short terminal (<= SHORT_TERMINAL_ROWS rows) the
renderability gate refuses the banner, so even a substantial wipe shows no
undo tip — counts/TTL never burn on a row the user cannot see.
terminal:
rows: 32
cols: 100
environment:
env:
- key: KIGI_CONTEXTUAL_HINTS
value: "1"
mock:
response: "unused"
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: focus_prompt
# Build a substantial draft in the agent view while the terminal is tall.
- action: type_text
text: "ddddddddddddddddddddddddd"
- action: wait
millis: 400
- action: assert_contains
text: "dddddddddd"
# Shrink to a short terminal: the banner row can no longer paint.
- action: resize
rows: 14
cols: 100
- action: wait
millis: 300
- action: keys
keys: "<C-u>"
- action: wait
millis: 700
- action: assert_not_contains
text: "to undo"
- action: assert_running
- action: screenshot
name: undo-tip-short-terminal
note: No tip on a short terminal even after a substantial wipe.
@@ -0,0 +1,92 @@
name: vim-modal-command-palette
description: >
Type-to-find pickers default to INPUT on open while staying vim-toggleable.
With vim-mode ON, the command palette opens in INPUT mode: a letter filters the
list immediately (no-match query shows "No matches"). Esc clears the query
(command palette's modal chrome clears search text first); a second Esc drops to
NAV mode where letters no longer filter; `i` re-enters INPUT (a letter filters
again). The footer surfaces an `i search` hint in NAV mode (discoverability) and
hides it in INPUT mode. Drives the real pager via the scripted ptyctl runner.
terminal:
rows: 40
cols: 110
mock:
response: "SCRIPTED_VIM_MODAL acknowledged."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
# Promote welcome → session so we are unambiguously on the agent screen.
- action: type_text
text: "hello"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: SCRIPTED_VIM_MODAL
timeout_ms: 30000
# Turn vim-mode ON (the setting that gates vim_normal_first at the call sites).
- action: type_text
text: "/vim-mode"
- action: keys
keys: "<Enter>"
- action: wait_for_text
text: "Vim mode: on"
timeout_ms: 10000
# Open the command palette (Ctrl+P) — opens in INPUT mode (type-to-find).
- action: keys
keys: "<C-p>"
- action: wait_for_text
text: Commands
timeout_ms: 10000
- action: assert_contains
text: New Session
# INPUT mode on open: the footer does NOT advertise the `i` search hint.
- action: assert_not_contains
text: i search
# INPUT mode on open: a letter filters immediately. "xyz" matches nothing → "No matches".
- action: type_text
text: "xyz"
- action: wait
millis: 200
- action: assert_contains
text: No matches
# Esc clears the query (command palette chrome) → list restored, still input.
- action: keys
keys: "<Esc>"
- action: wait
millis: 200
- action: assert_not_contains
text: No matches
- action: assert_contains
text: New Session
# Second Esc (empty query) drops to NAV mode via the picker's vim Esc.
- action: keys
keys: "<Esc>"
- action: wait
millis: 200
# NAV mode: the footer now advertises `i search` so users discover how to type.
- action: assert_contains
text: i search
# NAV mode: typing a letter must NOT filter (list stays intact, no "No matches").
- action: type_text
text: "xyz"
- action: wait
millis: 200
- action: assert_not_contains
text: No matches
- action: assert_contains
text: New Session
# `i` re-enters INPUT mode; typing now filters again → "No matches".
- action: keys
keys: "i"
- action: type_text
text: "xyz"
- action: wait
millis: 200
- action: assert_contains
text: No matches
- action: assert_not_contains
text: panicked
- action: screenshot
name: vim-modal-command-palette
note: Command palette input-default, Esc clears query, Esc→nav (no filter), i→input.
@@ -0,0 +1,27 @@
name: welcome-screen
description: Validate that the real pager boots to the welcome screen and produces visual artifacts.
terminal:
rows: 32
cols: 100
mock:
response: "SCRIPTED_WELCOME_RESPONSE from the mock server."
steps:
- action: wait_for_text
text: Quit
timeout_ms: 20000
- action: assert_contains
text: Quit
- action: assert_not_contains
text: panicked
- action: screenshot
name: welcome
note: Welcome screen after startup.
- action: resize
rows: 28
cols: 90
- action: wait
millis: 250
- action: assert_running
- action: screenshot
name: resized-welcome
note: Welcome screen after resize.