refactor(min): unify interactive prompts on inquire with a brand theme - #1114
Conversation
The session attach picker used inquire while the activation policy prompt used dialoguer. Both are single-select prompts, so standardize on inquire: the policy prompt moves behind the existing Prompter seam, the TTY probe switches to std::io::IsTerminal, and dialoguer leaves the dependency tree. A new theme module installs a monochrome inquire RenderConfig (white on near-black, gray secondary text) matching the website branding, applied process-wide at startup. Filtering stays on for the attach picker but off for the fixed six-choice policy prompt. Ctrl-C at the policy prompt no longer re-raises SIGINT (inquire's crossterm backend captures it as an error return), so prompt dismissal now flows through the normal activation error path; the SIGINT guard still covers the non-prompt phases.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 18 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe minimal CLI replaces ChangesMinimal CLI updates
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/minimal/src/theme.rs`:
- Around line 9-24: Use RenderConfig::empty() instead of RenderConfig::default()
when initializing config, while preserving the existing explicit monochrome
style overrides and placeholder assignment.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6f1ac066-0c72-4f5a-bebf-1182a7d09f8b
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
Cargo.tomlcrates/minimal/Cargo.tomlcrates/minimal/src/lib.rscrates/minimal/src/main.rscrates/minimal/src/prompt.rscrates/minimal/src/theme.rscrates/sessions/docs/COMPOSITION.md
💤 Files with no reviewable changes (2)
- Cargo.toml
- crates/minimal/Cargo.toml
| let mut config = RenderConfig::default() | ||
| .with_prompt_prefix(Styled::new("▸").with_fg(Color::White)) | ||
| .with_highlighted_option_prefix(Styled::new("▸").with_fg(Color::White)) | ||
| .with_selected_option(Some( | ||
| StyleSheet::new() | ||
| .with_fg(Color::White) | ||
| .with_attr(Attributes::BOLD), | ||
| )) | ||
| .with_option(StyleSheet::new().with_fg(Color::Grey)) | ||
| .with_answer( | ||
| StyleSheet::new() | ||
| .with_fg(Color::White) | ||
| .with_attr(Attributes::BOLD), | ||
| ) | ||
| .with_help_message(StyleSheet::new().with_fg(Color::DarkGrey)); | ||
| config.placeholder = StyleSheet::new().with_fg(Color::DarkGrey); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- relevant files ---'
git ls-files | rg '(^|/)(Cargo.toml|Cargo.lock|rust-toolchain.toml|theme\.rs)$'
printf '%s\n' '--- theme source ---'
f=$(git ls-files 'crates/minimal/src/theme.rs')
cat -n "$f"
printf '%s\n' '--- inquire dependency declarations ---'
rg -n -C 3 '(^|[^[:alnum:]_])inquire([^[:alnum:]_]|$)' --glob 'Cargo.toml' --glob 'Cargo.lock'
printf '%s\n' '--- local inquire sources, if vendored ---'
git ls-files | rg '(^|/)inquire(/|$)' || trueRepository: gominimal/minimal
Length of output: 2750
🌐 Web query:
inquire RenderConfig default empty source version-specific unhighlighted option prefix answered prompt prefix
💡 Result:
In the context of the Rust library inquire, RenderConfig is a struct used to customize the visual appearance of prompts [1][2]. The configuration options you mentioned are specific fields within this struct: unhighlighted_option_prefix: This field allows you to customize the prefix used for options in Select and MultiSelect prompts that are not currently highlighted [2][3]. It is defined as a Styled<&'a str> [1][2]. answered_prompt_prefix: This field allows you to set a custom prefix (such as a check mark) for prompts that have already been answered [3]. Regarding the "default" and "empty" source configurations: RenderConfig::empty returns a RenderConfig instance where no colors or attributes are applied (essentially a baseline configuration) [1][4]. RenderConfig::default returns a RenderConfig instance with standard default colors and attributes applied [4]. You can customize these by using methods like with_highlighted_option_prefix or by manually creating a RenderConfig instance [1][5]. When using prefix options, note that the library typically adds a space character automatically to separate the prefix from the option value [1][5][2].
Citations:
- 1: https://docs.rs/inquire-clack/latest/inquire/ui/struct.RenderConfig.html
- 2: https://docs.rs/inquire/latest/inquire/ui/struct.RenderConfig.html
- 3: https://github.com/mikaelmello/inquire/blob/main/CHANGELOG.md
- 4: https://hax.cryspen.com/frontend/docs/src/inquire/ui/render_config.rs.html
- 5: https://docs.rs/oma-inquire/latest/inquire/ui/struct.RenderConfig.html
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- workspace and minimal manifests ---'
cat -n Cargo.toml | sed -n '1,180p'
cat -n crates/minimal/Cargo.toml | sed -n '1,180p'
printf '%s\n' '--- locked inquire package ---'
awk '
/^\[\[package\]\]/ { block=0 }
/^\[\[package\]\]/ { block=1; buf=$0 "\n"; next }
block { buf=buf $0 "\n" }
block && /^name = "inquire"$/ { print buf; block=0 }
' Cargo.lock
printf '%s\n' '--- all inquire references ---'
rg -n -i 'inquire|render_config|unhighlighted_option_prefix|answered_prompt_prefix' --glob '!target/**' .Repository: gominimal/minimal
Length of output: 14783
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import json, urllib.request
for url in [
"https://raw.githubusercontent.com/mikael-mello/inquire/main/src/ui/render_config.rs",
"https://api.github.com/repos/mikaelmello/inquire/contents/src/ui/render_config.rs",
]:
print("URL", url)
try:
with urllib.request.urlopen(url, timeout=15) as r:
data = r.read().decode()
print(data[:30000])
except Exception as e:
print(type(e).__name__, e)
PYRepository: gominimal/minimal
Length of output: 617
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Cargo.lock inquire entry ---'
sed -n '2908,2945p' Cargo.lock
printf '%s\n' '--- inquire RenderConfig source at likely v0.9 tags ---'
for tag in v0.9.0 v0.9.1 v0.9.2 v0.9.3 v0.9.4 v0.9.5; do
url="https://raw.githubusercontent.com/mikaelmello/inquire/$tag/src/ui/render_config.rs"
body="$(curl -kfsSL "$url" 2>/dev/null || true)"
if [ -n "$body" ]; then
printf '\n### %s\n' "$tag"
printf '%s\n' "$body" | sed -n '1,260p'
fi
doneRepository: gominimal/minimal
Length of output: 1119
🏁 Script executed:
#!/bin/bash
set -eu
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -kfsSL "https://crates.io/api/v1/crates/inquire/0.9.4/download" -o "$tmp/inquire.tgz"
tar -xzf "$tmp/inquire.tgz" -C "$tmp"
src="$tmp/inquire-0.9.4"
printf '%s\n' '--- RenderConfig symbols ---'
rg -n -C 8 'struct RenderConfig|impl.*RenderConfig|fn empty|fn default|unhighlighted_option_prefix|answered_prompt_prefix|highlighted_option_prefix' "$src/src"
printf '%s\n' '--- relevant source ---'
f="$src/src/ui/render_config.rs"
if [ -f "$f" ]; then cat -n "$f" | sed -n '1,320p'; else find "$src/src" -type f -maxdepth 4 -print; fiRepository: gominimal/minimal
Length of output: 257
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import io, ssl, tarfile, urllib.request
url = "https://crates.io/api/v1/crates/inquire/0.9.4/download"
ctx = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=ctx, timeout=30) as r:
archive = r.read()
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:gz") as t:
names = t.getnames()
candidates = [n for n in names if n.endswith(("render_config.rs", "api.rs"))]
print("archive members:", len(names))
for name in candidates:
print(f"\n--- {name} ---")
text = t.extractfile(name).read().decode()
lines = text.splitlines()
for i, line in enumerate(lines):
if any(x in line for x in (
"struct RenderConfig", "impl Default for RenderConfig",
"fn empty", "fn default", "unhighlighted_option_prefix",
"answered_prompt_prefix", "highlighted_option_prefix",
)):
lo, hi = max(0, i-8), min(len(lines), i+55)
print("\n".join(f"{j+1:4}: {lines[j]}" for j in range(lo, hi)))
PYRepository: gominimal/minimal
Length of output: 50375
Use RenderConfig::empty() as the base.
RenderConfig::default() retains colored defaults for unoverridden fields such as answered_prompt_prefix and unhighlighted_option_prefix, so this theme is not fully monochrome.
Proposed fix
- let mut config = RenderConfig::default()
+ let mut config = RenderConfig::empty()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let mut config = RenderConfig::default() | |
| .with_prompt_prefix(Styled::new("▸").with_fg(Color::White)) | |
| .with_highlighted_option_prefix(Styled::new("▸").with_fg(Color::White)) | |
| .with_selected_option(Some( | |
| StyleSheet::new() | |
| .with_fg(Color::White) | |
| .with_attr(Attributes::BOLD), | |
| )) | |
| .with_option(StyleSheet::new().with_fg(Color::Grey)) | |
| .with_answer( | |
| StyleSheet::new() | |
| .with_fg(Color::White) | |
| .with_attr(Attributes::BOLD), | |
| ) | |
| .with_help_message(StyleSheet::new().with_fg(Color::DarkGrey)); | |
| config.placeholder = StyleSheet::new().with_fg(Color::DarkGrey); | |
| let mut config = RenderConfig::empty() | |
| .with_prompt_prefix(Styled::new("▸").with_fg(Color::White)) | |
| .with_highlighted_option_prefix(Styled::new("▸").with_fg(Color::White)) | |
| .with_selected_option(Some( | |
| StyleSheet::new() | |
| .with_fg(Color::White) | |
| .with_attr(Attributes::BOLD), | |
| )) | |
| .with_option(StyleSheet::new().with_fg(Color::Grey)) | |
| .with_answer( | |
| StyleSheet::new() | |
| .with_fg(Color::White) | |
| .with_attr(Attributes::BOLD), | |
| ) | |
| .with_help_message(StyleSheet::new().with_fg(Color::DarkGrey)); | |
| config.placeholder = StyleSheet::new().with_fg(Color::DarkGrey); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/minimal/src/theme.rs` around lines 9 - 24, Use RenderConfig::empty()
instead of RenderConfig::default() when initializing config, while preserving
the existing explicit monochrome style overrides and placeholder assignment.
DarkGrey help messages and placeholders were near-invisible on the near-black terminal backgrounds the theme targets.
|
Niiiiiicee! |
Unnamed sessions in the attach picker rendered as full UUIDs, so same-path rows were indistinguishable; they now show `(unnamed) · <id-prefix>` like the dash TUI. The prompt help line now inherits the terminal default color, matching the prompt message.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/minimal/src/attach.rs`:
- Around line 486-503: Add a regression test alongside
unnamed_session_shows_short_id_not_full_uuid that creates two unnamed entries
with IDs sharing the same initial segment, renders both through
format_candidate, and asserts their labels are different. Preserve the existing
short-ID truncation assertions while verifying colliding IDs receive distinct
rendered labels.
- Around line 140-149: The unnamed-session label logic in format_candidate must
produce collision-safe labels instead of using only the first UUID segment;
derive the shortest prefix that uniquely identifies each candidate or retain
sufficient ID text. In crates/minimal/src/attach.rs lines 140-149, update
format_candidate and its callers as needed to use the candidate set for
uniqueness. In crates/minimal/src/attach.rs lines 486-503, add two unnamed
sessions sharing a first UUID segment and assert their rendered labels differ.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 821ee04e-10a1-4dbf-83be-0e8b61ae7696
📒 Files selected for processing (2)
crates/minimal/src/attach.rscrates/minimal/src/theme.rs
| /// a glance even in the "pick over all sessions" case. Unnamed sessions | ||
| /// render as `(unnamed) · <id-prefix>` — the first id segment is enough to | ||
| /// tell same-path rows apart without a full UUID. | ||
| fn format_candidate(entry: &ListSessionsEntry, cwd: &paths::HostAbsPath) -> String { | ||
| let glyph = state_glyph(entry.status); | ||
| let name = entry.name.clone().unwrap_or_else(|| entry.id.to_string()); | ||
| let name = entry.name.clone().unwrap_or_else(|| { | ||
| let id = entry.id.to_string(); | ||
| let short = id.split('-').next().unwrap_or(&id); | ||
| format!("(unnamed) · {short}") | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Make shortened unnamed-session labels collision-safe.
The first UUID segment is not guaranteed to distinguish sessions; the provided fixtures already contain IDs sharing that segment. Ensure labels remain unique, then add coverage for two IDs with a shared first segment.
crates/minimal/src/attach.rs#L140-L149: derive a unique prefix from the candidate set or retain enough ID text to distinguish every row.crates/minimal/src/attach.rs#L486-L503: add two unnamed sessions with colliding prefixes and assert their labels differ.
📍 Affects 1 file
crates/minimal/src/attach.rs#L140-L149(this comment)crates/minimal/src/attach.rs#L486-L503
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/minimal/src/attach.rs` around lines 140 - 149, The unnamed-session
label logic in format_candidate must produce collision-safe labels instead of
using only the first UUID segment; derive the shortest prefix that uniquely
identifies each candidate or retain sufficient ID text. In
crates/minimal/src/attach.rs lines 140-149, update format_candidate and its
callers as needed to use the candidate set for uniqueness. In
crates/minimal/src/attach.rs lines 486-503, add two unnamed sessions sharing a
first UUID segment and assert their rendered labels differ.
|
|
||
| #[test] | ||
| fn unnamed_session_shows_short_id_not_full_uuid() { | ||
| let e = entry( | ||
| "019f5d0f-0a99-78b1-9165-0809440f0052", | ||
| "/a", | ||
| SessionStatus::Active, | ||
| ); | ||
| let label = format_candidate(&e, &cwd("/a")); | ||
| assert!( | ||
| label.contains("(unnamed) · 019f5d0f"), | ||
| "short id form: {label}" | ||
| ); | ||
| assert!( | ||
| !label.contains("0a99-78b1"), | ||
| "full uuid must not appear: {label}" | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add a regression test for colliding short IDs.
This test covers truncation for one session but does not catch duplicate labels. Add two unnamed entries whose IDs share the first segment and assert their rendered labels differ.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/minimal/src/attach.rs` around lines 486 - 503, Add a regression test
alongside unnamed_session_shows_short_id_not_full_uuid that creates two unnamed
entries with IDs sharing the same initial segment, renders both through
format_candidate, and asserts their labels are different. Preserve the existing
short-ID truncation assertions while verifying colliding IDs receive distinct
rendered labels.
Un-clobber the theme module, TTY predicate, and doc comments the stale base reverted, and complete the issue's second half: min daemon stop is canonical with min stop kept as the visible top-level alias, mirroring the session list / ls pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f
The upload-gate change was authored against a pre-#1114 file state and reverted the theme module, the TTY predicate, and two doc comments. Restore main's versions; the gate logic is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f
* fix(minimal): skip non-VCS-root upload in headless activate A headless `min session activate` (--no-prompt, --no-input, or a non-TTY) from a directory that is not a VCS root uploaded the whole directory with no confirmation, because the non-VCS-root gate treated the impossibility of a prompt as consent. Split the gate's TTY-free decision into `file_upload::upload_gate` and invert the headless arm: a headless caller with an implicit --sync now skips the upload and warns on stderr with the `--sync tarball` escape hatch, starting the session with an empty workspace. A VCS root or an explicit --sync tarball still uploads; the interactive confirm (default No) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: restore #1114 prompt unification clobbered by a stale base The upload-gate change was authored against a pre-#1114 file state and reverted the theme module, the TTY predicate, and two doc comments. Restore main's versions; the gate logic is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f --------- Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Norrie Taylor <norrie@minimal.dev>
) * fix(min): add canonical `min session list` command `min session list` did not exist: `SessionCommand` had no `List` variant, so the guessable `<noun> list` form of the flagship session noun errored under clap where every other noun (e.g. `min loadout list`) accepts it. Add `SessionCommand::List`, delegating to the existing `cmd_ls` so output is byte-identical, with `ls` as a visible noun-level alias. `min ls` keeps its bare top-level form as a visible alias. Docs now present `min session list` as canonical. * fix: restore #1114 content and add the daemon-stop half Un-clobber the theme module, TTY predicate, and doc comments the stale base reverted, and complete the issue's second half: min daemon stop is canonical with min stop kept as the visible top-level alias, mirroring the session list / ls pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f * refactor(minimal): drop the daemon-noun stop spelling from this change Keep the PR to the session-list half: `min session list` canonical with `min ls`/`min session ls` as visible aliases. `min stop` stays the bare top-level command it is on main, unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f --------- Co-authored-by: gominimal-aw-bot[bot] <281738952+gominimal-aw-bot[bot]@users.noreply.github.com> Co-authored-by: Norrie Taylor <norrie@minimal.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Norrie Taylor <91171431+norrietaylor@users.noreply.github.com>
Summary
The CLI had two prompt stacks: the session attach picker used inquire, the activation policy prompt used dialoguer. Both are single-select prompts, so this standardizes on inquire and drops dialoguer from the dependency tree. It also adds a process-wide prompt theme matching the website branding: monochrome, white on near-black with gray secondary text, no color accents.
Two picker legibility fixes ride along:
(unnamed) · <id-prefix>, matching the dash TUI.[↑↓ to move, ...]) inherits the terminal default color, the same as the prompt message, instead of hard-to-read dark gray.Changes
crates/minimal/src/theme.rsRenderConfig(bold white selection, gray options, default-color help text), installed viainquire::set_global_render_configcrates/minimal/src/prompt.rsDialoguerPrompterbecomesInquirePrompterbehind the existingPrompterseam; filtering off for the fixed six-choice policy prompt (without_filtering());UserChoicegainsDisplaycrates/minimal/src/attach.rscrates/minimal/src/lib.rsstd::io::IsTerminalinstead ofdialoguer::console;ActivationInterruptdocs updatedcrates/minimal/src/main.rsCargo.toml,crates/minimal/Cargo.toml,Cargo.lockcrates/sessions/docs/COMPOSITION.mdOne behavior note: inquire's crossterm backend captures Ctrl-C at a prompt as an error return instead of re-raising SIGINT, so dismissing the policy prompt now flows through the normal activation error path, which runs the abort cleanup. The SIGINT guard still covers the non-prompt phases.
Verification
cargo test -p minimal: 137 pass, including a new test for the short-id picker row.cargo clippy -p minimal --all-targets -- -D warnings: clean.cargo fmt --all -- --check: clean.Cargo.lock.Summary by CodeRabbit