Skip to content

Add contextual hints: suggest follow-up commands after every command#52

Merged
ractive merged 2 commits intomainfrom
iter-50/contextual-hints
Apr 23, 2026
Merged

Add contextual hints: suggest follow-up commands after every command#52
ractive merged 2 commits intomainfrom
iter-50/contextual-hints

Conversation

@ractive
Copy link
Copy Markdown
Owner

@ractive ractive commented Apr 23, 2026

Summary

  • Add a contextual hints system (modelled on hyalo) that suggests follow-up commands after every command runs — e.g., after navigate, hints suggest snapshot --depth 3, console --level error, screenshot -o page.png
  • New hints.rs module with 34 command-specific hint generators, many context-sensitive (console hints differ on error presence, a11y contrast differs on --fail-only, storage hints suggest the other storage type)
  • --hints / --no-hints global flags; default on for --format text, off for JSON; --jq always suppresses hints
  • JSON envelope always includes "hints" key (empty [] when suppressed); text mode renders as -> cmd # description lines
  • 8 unit tests + 5 e2e tests covering all hint flag combinations
  • Help text updated (OUTPUT FORMAT section + AI AGENT TIPS)

Test plan

  • cargo fmt — clean
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo test --workspace -q — 726 tests pass (including 5 new e2e hint tests)
  • Verify ff-rdp tabs --format text shows -> hint lines
  • Verify ff-rdp tabs JSON output has "hints": []
  • Verify ff-rdp tabs --hints JSON output has populated hints array
  • Verify ff-rdp tabs --jq '.total' suppresses hints entirely

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added global --hints and --no-hints flags to control contextual command suggestions.
    • Commands now display follow-up suggestions by default in text output; JSON output includes hints in a "hints" field.
    • Hints automatically suppressed with --jq for clean pipeline data.

Modelled on hyalo's hints system. After every command, ff-rdp now suggests
"what next?" commands contextual to what just ran (e.g., after navigate →
snapshot, console --level error, screenshot). Hints help AI agents discover
workflows without trial-and-error, and help humans discover flags they
might not know exist.

- New hints.rs module with HintSource, HintContext, generate_hints()
- 34 command-specific hint generators with context-sensitivity
  (e.g., console hints differ based on error presence)
- --hints / --no-hints global flags; default on for text, off for JSON
- --jq suppresses hints entirely (pipeline needs clean data)
- JSON envelope always includes "hints" key (empty [] when suppressed)
- Text mode renders hints as "  -> cmd  # description" lines
- MAX_HINTS = 5 cap per command
- 8 unit tests + 5 e2e tests covering all hint modes
- Help text updated: OUTPUT FORMAT and AI AGENT TIPS sections

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@ractive has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 51 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 51 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd50bfc8-24a3-4193-a2a3-c7b7a7e24bce

📥 Commits

Reviewing files that changed from the base of the PR and between 78b2342 and 8b9d156.

📒 Files selected for processing (4)
  • crates/ff-rdp-cli/src/hints.rs
  • crates/ff-rdp-cli/src/output.rs
  • crates/ff-rdp-cli/src/output_pipeline.rs
  • crates/ff-rdp-cli/tests/e2e/hints.rs
📝 Walkthrough

Walkthrough

This pull request introduces a contextual hints feature that suggests follow-up commands based on output from CLI operations. It adds hint generation logic, CLI flag controls (--hints/--no-hints), and integrates hint injection into the output pipeline across 25+ commands.

Changes

Cohort / File(s) Summary
CLI Hints Infrastructure
crates/ff-rdp-cli/src/hints.rs, crates/ff-rdp-cli/src/output_pipeline.rs, crates/ff-rdp-cli/src/output.rs, crates/ff-rdp-cli/src/main.rs
New hints module defines Hint, HintContext, HintSource enum, and generate_hints() function dispatching to source-specific generators. OutputPipeline adds HintsMode and finalize_with_hints() method supporting hint generation based on context, with text formatting and JSON injection. Output utilities provide inject_hints() for envelope mutation.
CLI Arguments
crates/ff-rdp-cli/src/cli/args.rs
Global --hints and --no-hints mutually-exclusive flags added to control contextual hint display, with defaults based on output format.
A11y Commands
crates/ff-rdp-cli/src/commands/a11y.rs, crates/ff-rdp-cli/src/commands/a11y_contrast.rs, crates/ff-rdp-cli/src/commands/a11y_summary.rs
Each now constructs HintContext with appropriate source variant and passes it to finalize_with_hints() instead of finalize().
DOM & Tree Commands
crates/ff-rdp-cli/src/commands/dom.rs, crates/ff-rdp-cli/src/commands/dom_tree.rs
Hint contexts created per operation (run, run_count, run_stats for dom; single context for dom_tree) and propagated through output finalization pipeline.
Interactive Commands
crates/ff-rdp-cli/src/commands/click.rs, crates/ff-rdp-cli/src/commands/type_text.rs, crates/ff-rdp-cli/src/commands/wait.rs
Commands construct HintContext with selector information and use finalize_with_hints() for output generation.
Navigation Commands
crates/ff-rdp-cli/src/commands/navigate.rs, crates/ff-rdp-cli/src/commands/nav_action.rs
Navigation/reload operations build hint contexts tagged with appropriate source and propagate through finalization across all output paths.
Style & Computed Commands
crates/ff-rdp-cli/src/commands/styles.rs, crates/ff-rdp-cli/src/commands/computed.rs, crates/ff-rdp-cli/src/commands/geometry.rs
Selector-aware hint contexts created and integrated into finalization for layout/style query operations.
Data Inspection Commands
crates/ff-rdp-cli/src/commands/console.rs, crates/ff-rdp-cli/src/commands/storage.rs, crates/ff-rdp-cli/src/commands/cookies.rs
Hint contexts constructed with source-specific metadata (error count for console, storage type for storage) and passed to finalization pipeline.
Network & Page Commands
crates/ff-rdp-cli/src/commands/network.rs, crates/ff-rdp-cli/src/commands/page_text.rs, crates/ff-rdp-cli/src/commands/sources.rs
Hint contexts parameterized by command-specific flags (detail mode for network) and integrated into output pipeline.
Performance & Comparison Commands
crates/ff-rdp-cli/src/commands/perf.rs, crates/ff-rdp-cli/src/commands/perf_compare.rs
Multiple hint context variants created per sub-operation (Perf, PerfVitals, PerfSummary, PerfAudit) and consistently passed to finalization.
Remaining Commands
crates/ff-rdp-cli/src/commands/eval.rs, crates/ff-rdp-cli/src/commands/inspect.rs, crates/ff-rdp-cli/src/commands/launch.rs, crates/ff-rdp-cli/src/commands/responsive.rs, crates/ff-rdp-cli/src/commands/screenshot.rs, crates/ff-rdp-cli/src/commands/snapshot.rs, crates/ff-rdp-cli/src/commands/tabs.rs
Hint contexts created with source-specific tags and integrated into output finalization uniformly across all commands.
E2E Tests
crates/ff-rdp-cli/tests/e2e/hints.rs, crates/ff-rdp-cli/tests/e2e/main.rs
New e2e test module validates hint behavior across JSON, text, and jq output modes; covers default behavior, explicit flags, and formatting.
Documentation
kb/iterations/iteration-50-contextual-hints.md
Iteration document specifying hint model, JSON envelope structure, text rendering format, CLI behavior, and per-command generators.

Sequence Diagram

sequenceDiagram
    participant User
    participant Command
    participant HintContext
    participant OutputPipeline
    participant HintGenerator
    participant JSONOutput
    
    User->>Command: Execute command (e.g., `ff-rdp tabs`)
    Command->>HintContext: Create HintContext<br/>(source, selector, flags)
    Command->>OutputPipeline: finalize_with_hints(envelope,<br/>hint_ctx)
    
    OutputPipeline->>OutputPipeline: Check hints_mode<br/>(on, off, or json_only)
    alt hints_mode is enabled
        OutputPipeline->>HintGenerator: generate_hints(&hint_ctx)
        HintGenerator->>HintGenerator: Dispatch to source<br/>specific generator
        HintGenerator-->>OutputPipeline: Vec<Hint>
        OutputPipeline->>JSONOutput: inject_hints(envelope, hints)
        JSONOutput-->>OutputPipeline: Updated envelope
    end
    
    alt Output format is text
        OutputPipeline->>JSONOutput: Render text output
        OutputPipeline->>JSONOutput: Print hint lines<br/>(if enabled)
    else Output format is JSON
        OutputPipeline->>JSONOutput: Serialize envelope<br/>with hints array
    end
    
    JSONOutput-->>User: Formatted output<br/>with optional hints
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 ✨ Hints bloom like clover in every command's path,
Suggestions sprout sweetly, with context and craft,
From dom to perf, from tabs to the click,
Each output now whispers the next clever trick! 🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a contextual hints system that suggests follow-up commands after every command runs.
Docstring Coverage ✅ Passed Docstring coverage is 81.73% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch iter-50/contextual-hints

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a contextual hints system to ff-rdp CLI output so each command can suggest likely follow-up commands (rendered in text mode as -> lines and included in JSON output as a hints array), controlled via new global hint flags.

Changes:

  • Introduces hints.rs (hint model + per-command hint generators) and wires hint generation into OutputPipeline.
  • Adds --hints/--no-hints global flags with defaults based on output format, plus updated help text.
  • Adds e2e coverage for hint rendering/suppression combinations (text vs JSON vs --jq).

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
kb/iterations/iteration-50-contextual-hints.md Adds iteration write-up documenting the hints feature and rollout tasks/criteria
crates/ff-rdp-cli/tests/e2e/main.rs Registers the new e2e hints test module
crates/ff-rdp-cli/tests/e2e/hints.rs Adds e2e tests for JSON/text hints and --jq suppression
crates/ff-rdp-cli/src/output_pipeline.rs Adds hints mode selection and hint injection/rendering in the output pipeline
crates/ff-rdp-cli/src/output.rs Adds inject_hints helper to insert hints into the JSON envelope
crates/ff-rdp-cli/src/main.rs Exposes the new hints module
crates/ff-rdp-cli/src/hints.rs Implements hint data model, context, generators, and unit tests
crates/ff-rdp-cli/src/commands/wait.rs Passes HintContext into output finalization for wait
crates/ff-rdp-cli/src/commands/type_text.rs Passes selector-aware HintContext for type
crates/ff-rdp-cli/src/commands/tabs.rs Passes HintContext for tabs
crates/ff-rdp-cli/src/commands/styles.rs Passes selector-aware HintContext for styles subcommands
crates/ff-rdp-cli/src/commands/storage.rs Passes storage-type-aware HintContext for storage
crates/ff-rdp-cli/src/commands/sources.rs Passes HintContext for sources
crates/ff-rdp-cli/src/commands/snapshot.rs Passes HintContext for snapshot
crates/ff-rdp-cli/src/commands/screenshot.rs Passes HintContext for screenshot
crates/ff-rdp-cli/src/commands/responsive.rs Passes HintContext for responsive
crates/ff-rdp-cli/src/commands/perf_compare.rs Passes HintContext for perf compare output
crates/ff-rdp-cli/src/commands/perf.rs Passes appropriate HintContext for perf commands (vitals/summary/audit/etc.)
crates/ff-rdp-cli/src/commands/page_text.rs Passes HintContext for page-text
crates/ff-rdp-cli/src/commands/network.rs Passes detail-aware HintContext for network
crates/ff-rdp-cli/src/commands/navigate.rs Passes HintContext for navigate (incl. with-network paths)
crates/ff-rdp-cli/src/commands/nav_action.rs Passes HintContext for reload/back/forward
crates/ff-rdp-cli/src/commands/launch.rs Passes HintContext for launch
crates/ff-rdp-cli/src/commands/inspect.rs Passes HintContext for inspect
crates/ff-rdp-cli/src/commands/geometry.rs Passes selector-aware HintContext for geometry
crates/ff-rdp-cli/src/commands/eval.rs Passes HintContext for eval
crates/ff-rdp-cli/src/commands/dom_tree.rs Passes HintContext for dom tree
crates/ff-rdp-cli/src/commands/dom.rs Passes selector-aware HintContext for dom + dom stats
crates/ff-rdp-cli/src/commands/cookies.rs Passes HintContext for cookies
crates/ff-rdp-cli/src/commands/console.rs Passes error-aware HintContext for console
crates/ff-rdp-cli/src/commands/computed.rs Passes selector-aware HintContext for computed
crates/ff-rdp-cli/src/commands/click.rs Passes selector-aware HintContext for click
crates/ff-rdp-cli/src/commands/a11y_summary.rs Passes HintContext for a11y summary
crates/ff-rdp-cli/src/commands/a11y_contrast.rs Passes fail-only-aware HintContext for a11y contrast
crates/ff-rdp-cli/src/commands/a11y.rs Passes HintContext for a11y
crates/ff-rdp-cli/src/cli/args.rs Documents hints in help text and adds global --hints/--no-hints flags

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ff-rdp-cli/src/output.rs Outdated
Comment on lines +49 to +56
pub fn inject_hints(envelope: &mut Value, hints: &[Hint]) {
if let Some(obj) = envelope.as_object_mut() {
let hints_json: Vec<Value> = hints
.iter()
.map(|h| serde_json::to_value(h).unwrap_or(Value::Null))
.collect();
obj.insert("hints".to_string(), Value::Array(hints_json));
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inject_hints silently converts any serialization error into null entries via unwrap_or(Value::Null), which can hide bugs and produce a malformed hints array. Since Hint should be infallibly serializable, prefer an expect/unwrap with a clear message, or change inject_hints to return a Result and propagate the error.

Copilot uses AI. Check for mistakes.
Comment thread crates/ff-rdp-cli/src/hints.rs Outdated
Comment on lines +228 to +241
fn hints_dom(ctx: &HintContext) -> Vec<Hint> {
let sel = ctx.selector.as_deref().unwrap_or("selector");
vec![
Hint::new(
format!("Click on \"{sel}\""),
format!(r#"ff-rdp click "{sel}""#),
),
Hint::new(
format!("Inspect styles of \"{sel}\""),
format!(r#"ff-rdp styles "{sel}" --properties color,display"#),
),
Hint::new(
format!("Get computed color for \"{sel}\""),
format!(r#"ff-rdp computed "{sel}" --prop color"#),
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several hint generators build commands like ff-rdp click "{sel}" by interpolating the selector into double quotes without escaping. This produces non-copy-pasteable (and sometimes shell-invalid) hints for selectors containing quotes (common for attribute selectors like a[href="..."]). Consider adding a small shell-escaping/quoting helper and using it consistently when embedding user-provided selectors/strings into hint command lines.

Suggested change
fn hints_dom(ctx: &HintContext) -> Vec<Hint> {
let sel = ctx.selector.as_deref().unwrap_or("selector");
vec![
Hint::new(
format!("Click on \"{sel}\""),
format!(r#"ff-rdp click "{sel}""#),
),
Hint::new(
format!("Inspect styles of \"{sel}\""),
format!(r#"ff-rdp styles "{sel}" --properties color,display"#),
),
Hint::new(
format!("Get computed color for \"{sel}\""),
format!(r#"ff-rdp computed "{sel}" --prop color"#),
fn shell_quote(arg: &str) -> String {
format!("'{}'", arg.replace('\'', r#"'\''"#))
}
fn hints_dom(ctx: &HintContext) -> Vec<Hint> {
let sel = ctx.selector.as_deref().unwrap_or("selector");
let quoted_sel = shell_quote(sel);
vec![
Hint::new(
format!("Click on \"{sel}\""),
format!("ff-rdp click {quoted_sel}"),
),
Hint::new(
format!("Inspect styles of \"{sel}\""),
format!("ff-rdp styles {quoted_sel} --properties color,display"),
),
Hint::new(
format!("Get computed color for \"{sel}\""),
format!("ff-rdp computed {quoted_sel} --prop color"),

Copilot uses AI. Check for mistakes.
Comment thread crates/ff-rdp-cli/src/hints.rs Outdated
"Run interactive accessibility check",
"ff-rdp a11y --interactive",
),
Hint::new("Check colour contrast", "ff-rdp a11y contrast"),
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling/style is inconsistent with the rest of the CLI help text (“color”): this hint uses “colour”. Consider changing to “color” for consistency.

Suggested change
Hint::new("Check colour contrast", "ff-rdp a11y contrast"),
Hint::new("Check color contrast", "ff-rdp a11y contrast"),

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +247
/// `--jq` pipes through jq and must suppress hints entirely — the output is
/// a raw jq value, not a JSON envelope, and contains no hint lines.
#[test]
fn jq_suppresses_hints() {
let fixture = load_fixture("list_tabs_response.json");

let server = MockRdpServer::new().on("listTabs", fixture);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());

let mut args = base_args(port);
args.extend([
"tabs".to_owned(),
"--jq".to_owned(),
".results | length".to_owned(),
]);

let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");

handle.join().unwrap();

assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);

let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();

// jq emits the raw numeric value, not a JSON envelope.
assert_eq!(
trimmed, "2",
"jq output must be the raw count '2'; got: {trimmed}"
);

// No hint lines must appear in jq output.
assert!(
!stdout.contains(" -> "),
"jq output must not contain hint lines; got:\n{stdout}"
);

// The output must not be a JSON object with a hints key.
if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed) {
assert!(
json.get("hints").is_none(),
"jq output must not be an envelope with 'hints'; got: {json}"
);
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jq_suppresses_hints e2e test uses a jq expression that’s unaffected by adding a top-level hints key (.results | length), so it won’t catch regressions where hints are still injected into the jq input. Consider adding an assertion with --jq '.' (or . | keys) to verify the jq input/output does not include a hints field when jq mode is enabled.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +163
- [x] All commands produce contextual hints when hints are enabled
- [x] Hints are on by default in `--format text`, off in JSON
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acceptance criteria claims all commands produce contextual hints when enabled, but at least the scroll command path still uses OutputPipeline::finalize() without providing a HintContext, so it will emit no hints in text mode. Either wire hint contexts into the remaining commands (e.g., scroll) or soften this acceptance criterion to match actual behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +105
let mut envelope = envelope.clone();

// Generate and inject hints when enabled.
let hints = if self.hints_mode == HintsMode::On {
hint_ctx.map(generate_hints).unwrap_or_default()
} else {
vec![]
};
output::inject_hints(&mut envelope, &hints);

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finalize_with_hints injects an empty hints array into the envelope even when --jq is active (because this happens before the jq filter runs). That changes jq-visible data (e.g., . | keys, . | length) and contradicts the stated goal that --jq needs “clean data” / hints are suppressed entirely. Consider skipping inject_hints (and any hint generation) whenever self.jq_filter.is_some() so jq operates on the original envelope.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to 90
/// If a `HintContext` is provided and hints are enabled, generates
/// contextual hints and injects them into the envelope.
///
/// If a jq filter is set, apply it to the full envelope so that users
/// can access any field (`.results`, `.total`, `.meta`, `.hints`).
/// Otherwise pretty-print the envelope as-is (JSON) or render a
/// human-readable table (text).
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says jq users can access .hints, but the CLI behavior/requirements say --jq should suppress hints entirely. After fixing suppression, this comment should be updated so it doesn’t imply .hints is available under --jq.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
crates/ff-rdp-cli/src/commands/dom_tree.rs (1)

66-77: ⚠️ Potential issue | 🟡 Minor

Text short-circuit bypasses hint rendering.

Same pattern as a11y_summary: when format == "text" and jq is unset, render_dom_tree_text prints and returns before finalize_with_hints runs, so no follow-up hints are shown in text mode — which is the default and the mode where hints are supposed to be on by default per the PR.

Consider rendering hints for HintSource::DomTree after the text tree is printed (either via a pipeline helper or by calling generate_hints + a small render step here).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/dom_tree.rs` around lines 66 - 77, The
text-path early return in dom_tree (when cli.format == "text" &&
cli.jq.is_none()) calls render_dom_tree_text(&results) and returns before hint
generation, so HintSource::DomTree hints never run; after printing the text
tree, invoke the same hint pipeline used elsewhere: construct
HintContext::new(HintSource::DomTree), then either call
OutputPipeline::from_cli(cli)? and finalize_with_hints(&envelope,
Some(&hint_ctx)) or call the underlying generate_hints + a small render step so
hints are generated and printed in text mode (use the same envelope/meta/results
inputs as in the JSON path to keep behavior consistent).
crates/ff-rdp-cli/src/commands/a11y_summary.rs (1)

109-118: ⚠️ Potential issue | 🟡 Minor

Text short-circuit bypasses hint rendering.

When cli.format == "text" (the default), this early return prints the custom summary and exits before finalize_with_hints runs, so no hint lines are emitted for a11y summary in text mode — contradicting the PR goal of suggesting follow-up commands after every command (hints default on for --format text). JSON-mode output still flows through the pipeline correctly, but the most common path gets no hints.

Consider rendering hints after the custom text output, e.g. by generating and rendering them inline, or by refactoring OutputPipeline to expose a hints-only render path you can invoke here.

💡 Sketch of a fix
     // Custom text rendering for a11y summary.
     if cli.format == "text" && cli.jq.is_none() {
         render_summary_text(&output_results);
+        let hint_ctx = HintContext::new(HintSource::A11ySummary);
+        OutputPipeline::from_cli(cli)?.render_hints_only(&hint_ctx);
         return Ok(());
     }

     let hint_ctx = HintContext::new(HintSource::A11ySummary);
     OutputPipeline::from_cli(cli)?
         .finalize_with_hints(&envelope, Some(&hint_ctx))
         .map_err(AppError::from)

(exact API up to you — the key point is that the text short-circuit needs a hint-rendering hook.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/a11y_summary.rs` around lines 109 - 118, The
early return after calling render_summary_text(&output_results) skips hint
rendering; instead, keep the custom text path but invoke the hint pipeline
afterwards: create the HintContext (HintContext::new(HintSource::A11ySummary))
and pass it to the existing OutputPipeline (OutputPipeline::from_cli(cli)?) to
render only hints (e.g., call a hints-only API or a new helper like
finalize_hints or finalize_with_hints_only using &envelope and Some(&hint_ctx))
so both render_summary_text and the hint output are emitted when cli.format ==
"text" && cli.jq.is_none(); if needed add a small helper on OutputPipeline to
expose a hints-only render path and call it from this branch.
crates/ff-rdp-cli/src/commands/cookies.rs (1)

65-81: ⚠️ Potential issue | 🟡 Minor

Envelope now has both hint (string) and hints (array) — consider reconciling.

The existing "no cookies found" branch injects a hint string field into the envelope (lines 66–76). With this change, the pipeline will additionally inject the new top-level hints array. JSON consumers (including the AI agents the PR is designed for) now see two similarly-named fields with different semantics, which is easy to misread.

If this is intentional (keeping the narrative hint separate from the structured follow-up hints), consider renaming the string field to something more descriptive like note or diagnostic to avoid collision with the new hints system. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/cookies.rs` around lines 65 - 81, The envelope
insertion currently adds a top-level "hint" string which will collide with the
new structured "hints" array; update the branch that mutates the envelope (the
block that checks total == 0 and uses envelope.as_object_mut()) to insert a
differently named field such as "note" or "diagnostic" instead of "hint" (keep
the same message), so consumers of the envelope, and the downstream pipeline
that constructs HintContext::new(HintSource::Cookies) and
OutputPipeline::from_cli(...).finalize_with_hints(...), will not see both "hint"
and "hints" with different semantics.
crates/ff-rdp-cli/src/commands/a11y.rs (1)

96-105: ⚠️ Potential issue | 🟡 Minor

Multiple text-mode commands silently bypass the hints feature.

Text short-circuits in 10 locations across 8 command files (a11y, snapshot, dom_tree, responsive, network, perf, geometry, a11y_summary) return immediately after calling custom renderers, never invoking the output pipeline where hints are generated and displayed.

Per the output_pipeline configuration (lines 60–73), hints default to HintsMode::On for --format text and are rendered in finalize_with_hints (line 119). However, the custom text renderers used in short-circuits have no hint output capability and return before the pipeline runs, so:

  • Users who run these commands with default text format never see hints, despite hints defaulting to ON
  • Explicit --hints is silently ignored in short-circuit paths
  • Text-mode behavior is inconsistent across commands (some show hints, some don't)

Either integrate hint rendering into each custom text renderer (e.g., call render_hints(&hints) after rendering tables) or refactor the short-circuit logic to flow through the pipeline when hints are enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/a11y.rs` around lines 96 - 105, The
short-circuit in the text path (where render_a11y_text is called) skips the
OutputPipeline hint rendering; update the logic so when hints are enabled (use
the HintContext/HintSource::A11y or the CLI hints flag/state) you do not return
immediately but instead run the pipeline's finalize_with_hints (via
OutputPipeline::from_cli(...).finalize_with_hints(&envelope, Some(&hint_ctx)))
or, alternately, after render_a11y_text call the same hint render path (e.g.,
render_hints using the HintContext/hints produced by the pipeline) so that
finalize_with_hints or an equivalent hint rendering is always invoked when hints
are ON; modify the condition around render_a11y_text to only short-circuit when
hints are OFF.
crates/ff-rdp-cli/src/commands/geometry.rs (1)

98-111: ⚠️ Potential issue | 🟠 Major

Text-mode short-circuit skips hint rendering.

When results are null and the user is in text mode, the code prints and returns Ok(()) without going through OutputPipeline. Since hints default to on in text mode per the PR objectives, this path will never emit the -> cmd # description lines users expect.

The same issue exists at lines 163-166 for the non-empty text path. Consider routing text output through the pipeline (or calling a helper that renders hints after render_geometry_text) so text-mode hints are honored. This pattern also affects network.rs (summary mode) and perf.rs (run_summary/run_audit text short-circuits).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/geometry.rs` around lines 98 - 111, The
text-mode short-circuit in geometry.rs skips hint rendering: when
geometry.is_null() the code calls render_geometry_text(&empty) and returns
Ok(()) instead of running the OutputPipeline hint path; do not return early —
either route text output through
OutputPipeline::from_cli(cli)?.finalize_with_hints(&envelope, Some(&hint_ctx))
(so hints are emitted consistently) or, if you must keep render_geometry_text,
call the same hint-emission helper after render_geometry_text (use
HintContext::new(HintSource::Geometry).with_selector(first_sel) and the existing
envelope/meta) so the -> cmd # description lines are produced; update the
matching non-empty text path (the other short-circuit) similarly and mirror the
same fix pattern used for network.rs and perf.rs.
♻️ Duplicate comments (3)
crates/ff-rdp-cli/src/commands/perf.rs (2)

1014-1027: ⚠️ Potential issue | 🟠 Major

perf audit text mode bypasses hint rendering.

Same issue as perf summary above — render_audit_text is invoked and the function returns Ok(()) without ever going through OutputPipeline, so hints never appear in text mode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/perf.rs` around lines 1014 - 1027, The
text-mode short-circuit in perf audit skips hint rendering by calling
render_audit_text(&results) and returning early; instead, after producing the
text output you must still run the OutputPipeline hint stage. Change the branch
so that render_audit_text(&results) is called but do not return immediately —
create HintContext::new(HintSource::PerfAudit) and call
OutputPipeline::from_cli(cli)?.finalize_with_hints(&envelope, Some(&hint_ctx))
(or call finalize_with_hints after render_audit_text), propagating errors via
map_err(AppError::from), so hints are emitted in text mode just like other
formats.

660-673: ⚠️ Potential issue | 🟠 Major

perf summary text mode bypasses hint rendering.

Same pattern as geometry.rs: the cli.format == "text" branch returns before finalize_with_hints runs, so text-mode users never see the configured hints for perf summary. See root-cause comment on geometry.rs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/perf.rs` around lines 660 - 673, The text-mode
early return in perf summary skips hint rendering; remove the early return and
ensure finalize_with_hints is always invoked. Build meta/envelope and create
hint_ctx (HintContext::new(HintSource::PerfSummary)) as shown, call
OutputPipeline::from_cli(cli)? .finalize_with_hints(&envelope,
Some(&hint_ctx)).map_err(AppError::from) before exiting, and only after that, if
cli.format == "text" && cli.jq.is_none(), call render_summary_text(&results) and
then return Ok(()); keep references to render_summary_text,
OutputPipeline::from_cli, finalize_with_hints, HintContext,
HintSource::PerfSummary, envelope and meta to locate the change.
crates/ff-rdp-cli/src/commands/network.rs (1)

181-196: ⚠️ Potential issue | 🟠 Major

Summary-mode text short-circuit bypasses hint rendering.

Line 182-185 returns Ok(()) before reaching finalize_with_hints, so hints are never printed for network summary in text mode even though text-mode hints default to on. See the root-cause comment on geometry.rs — the same gap affects network summary and perf summary/perf audit text paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/network.rs` around lines 181 - 196, The
short-circuit return after render_network_summary_text skips building the
envelope and calling OutputPipeline::finalize_with_hints, so text-mode hints
never render; instead, either build the envelope before the text short-circuit
or move render_network_summary_text to run then continue to construct `envelope`
and call `OutputPipeline::finalize_with_hints(&envelope, Some(&hint_ctx))`;
ensure `empty_hint` is injected into the same `envelope` (using
`envelope.as_object_mut()`), and keep
`HintContext::new(HintSource::Network).with_detail(cli.detail)` so hints are
emitted for text summary paths without an early `return Ok(())`.
🧹 Nitpick comments (8)
crates/ff-rdp-cli/src/commands/sources.rs (1)

130-133: Hints plumbing is consistent with the rest of the CLI. Optionally, since this command sets fallback/fallback_method in meta when JS fallback was used, you could consider whether HintContext should carry that signal so the generator can suggest e.g. upgrading Firefox or using the native path — but this is clearly optional and can be deferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/sources.rs` around lines 130 - 133, The hint
pipeline is created with HintContext::new(HintSource::Sources) but doesn't
convey that this command may set fallback/fallback_method in the envelope meta;
update the HintContext to carry a fallback flag (or enum) and set it here so
finalize_with_hints(&envelope, Some(&hint_ctx)) can generate fallback-specific
suggestions. Modify HintContext (e.g., add a field like fallback_used or
fallback_method) and its constructor used here (HintContext::new) to accept that
signal, populate it when JS fallback was used (the code path that sets
envelope.meta.fallback/fallback_method), and ensure finalize_with_hints reads
that field to tailor hints (references: HintContext, HintSource::Sources,
finalize_with_hints, envelope, fallback/fallback_method).
crates/ff-rdp-cli/src/commands/inspect.rs (1)

41-44: Hints plumbing consistent with sibling commands. Optionally, if inspect-specific hint generators ever want to suggest follow-ups that reference the inspected actor, you could extend HintContext (e.g. an with_actor builder) — purely optional, defer if hint generators don't need it today.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/inspect.rs` around lines 41 - 44, The inspect
command currently creates a HintContext via
HintContext::new(HintSource::Inspect) and passes it to
OutputPipeline::finalize_with_hints(&envelope, Some(&hint_ctx)); to allow
inspect-specific hint generators to reference the inspected actor, add a builder
method on HintContext (e.g., HintContext::with_actor or a with_actor(self,
actor_id: ActorId) -> Self) and use it here to attach the inspected actor before
calling finalize_with_hints so hint generators can access that actor context
when producing follow-up suggestions.
crates/ff-rdp-cli/src/commands/storage.rs (1)

34-102: Hoist the HintContext construction to avoid repeating it in all three branches.

canonical_type is fixed at the top of run, and the hint context is identical in the Grip::Null, value, and all-keys dump branches. Constructing it once keeps the three finalization sites parallel and makes it harder to let them drift (e.g., if someone later adds .with_url(...) in only one branch).

♻️ Suggested refactor
     let mut ctx = connect_direct(cli)?;
     let console_actor = ctx.target.console_actor.clone();

     let meta = json!({
         "host": cli.host,
         "port": cli.port,
         "storage_type": canonical_type,
     });

+    let hint_ctx = HintContext::new(HintSource::Storage).with_storage_type(canonical_type);
+
     if let Some(k) = key {
         // Single-key lookup: embed key as a JSON-encoded string literal to
         // prevent any injection through the key name.
         let key_json = serde_json::to_string(k)
             .map_err(|e| AppError::Internal(anyhow::anyhow!("key serialisation: {e}")))?;
         ...
         match &eval_result.result {
             Grip::Null => {
                 let envelope = output::envelope(&json!({"key": k, "value": null}), 0, &meta);
-                let hint_ctx =
-                    HintContext::new(HintSource::Storage).with_storage_type(canonical_type);
                 OutputPipeline::from_cli(cli)?
                     .finalize_with_hints(&envelope, Some(&hint_ctx))
                     .map_err(AppError::from)
             }
             grip => {
                 let value = resolve_string_grip(&mut ctx, grip)?;
                 let envelope = output::envelope(&json!({"key": k, "value": value}), 1, &meta);
-                let hint_ctx =
-                    HintContext::new(HintSource::Storage).with_storage_type(canonical_type);
                 OutputPipeline::from_cli(cli)?
                     .finalize_with_hints(&envelope, Some(&hint_ctx))
                     .map_err(AppError::from)
             }
         }
     } else {
         ...
         let envelope = output::envelope(&storage_map, total, &meta);
-        let hint_ctx = HintContext::new(HintSource::Storage).with_storage_type(canonical_type);
         OutputPipeline::from_cli(cli)?
             .finalize_with_hints(&envelope, Some(&hint_ctx))
             .map_err(AppError::from)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/storage.rs` around lines 34 - 102, The
HintContext construction is duplicated across the Grip::Null, value, and
all-keys dump branches; create it once before the if/else branch by calling
HintContext::new(HintSource::Storage).with_storage_type(canonical_type) (use the
same canonical_type already computed) and then pass that single hint_ctx into
each OutputPipeline::from_cli(cli)?.finalize_with_hints(&envelope,
Some(&hint_ctx)). This removes repetition around HintContext and ensures all
finalization calls use the identical context.
crates/ff-rdp-cli/src/commands/navigate.rs (1)

82-86: Hint context wiring is consistent across all three output paths — LGTM.

HintSource::Navigate is applied uniformly to the daemon, direct, and no-network branches, so navigate output is labeled consistently regardless of which code path runs.

Optional: since HintContext::new(HintSource::Navigate) is constructed verbatim three times, you could hoist a tiny helper (fn nav_hint_ctx() -> HintContext) if you anticipate adding more fields (e.g., .with_url(url)) later. Not worth doing now if the context stays trivial.

Also applies to: 214-217, 289-293

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/navigate.rs` around lines 82 - 86, Multiple
places construct the same HintContext via
HintContext::new(HintSource::Navigate); extract that repeated construction into
a small helper function (e.g., fn nav_hint_ctx() -> HintContext) and replace the
three inline calls with nav_hint_ctx() so future additions like .with_url(url)
can be applied in one place; update the call sites that currently use
HintContext::new(HintSource::Navigate) (seen near the
OutputPipeline::from_cli(...).finalize_with_hints(&envelope, Some(&hint_ctx))
usages) to use the new helper.
crates/ff-rdp-cli/src/commands/perf.rs (1)

1217-1221: run_group_by_domain reuses HintSource::Perf.

Minor consistency nit: run_group_by_domain tags its hints with the generic HintSource::Perf, while other perf sub-commands have dedicated variants (PerfVitals, PerfSummary, PerfAudit). If the intent is identical hints to perf <type>, fine; otherwise consider adding a dedicated variant (or at least documenting the reuse) so future hint tweaks can target grouped output independently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/perf.rs` around lines 1217 - 1221,
run_group_by_domain currently tags hints with the generic HintSource::Perf which
prevents per-subcommand differentiation; add a dedicated enum variant (e.g.
PerfGroupByDomain) to HintSource, update its serde/match arms/usages, and
replace the HintSource::Perf in run_group_by_domain with the new variant (or
alternatively add a clear comment in the run_group_by_domain function explaining
intentional reuse) so future hint handling can target this subcommand
independently; ensure you update any pattern matches, constructors, and tests
that exhaustively match HintSource variants (and bump any derived traits if
needed).
crates/ff-rdp-cli/src/commands/network.rs (1)

128-196: Coexisting hint (string) and hints (array) fields in the envelope.

The existing inline "hint" message (e.g. "No network events captured…") is now emitted alongside the new top-level "hints": [...] array. JSON consumers will see both keys with different shapes/meanings. Consider documenting this in the schema notes or folding the empty/filter hint into the structured hints list to keep one canonical field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/network.rs` around lines 128 - 196, The code
currently inserts a top-level "hint" string (via empty_hint) into the JSON
envelope, causing both "hint" and the structured "hints" array to coexist;
change the insertion logic in both places that mutate the envelope (after
output::envelope_with_truncation and after output::envelope) to instead fold the
empty_hint into the structured "hints" array: when envelope.as_object_mut() is
Some, if a "hints" array exists push the empty_hint into it, otherwise create a
new "hints" array containing the empty_hint; remove the obj.insert("hint", ...)
usage and keep the rest (HintContext::new(HintSource::Network),
finalize_with_hints, etc.) unchanged so consumers only see the canonical "hints"
field.
crates/ff-rdp-cli/src/output_pipeline.rs (1)

130-132: Drop the unnecessary turbofish.

None alone is sufficient since the parameter type Option<&HintContext> constrains inference.

♻️ Nit
     pub fn finalize(&self, envelope: &Value) -> anyhow::Result<()> {
-        self.finalize_with_hints(envelope, None::<&HintContext>)
+        self.finalize_with_hints(envelope, None)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/output_pipeline.rs` around lines 130 - 132, The
finalize method calls finalize_with_hints using the explicit turbofish
None::<&HintContext> which is unnecessary because the parameter type
Option<&HintContext> already guides type inference; update the finalize function
to pass plain None to finalize_with_hints (leave finalize, finalize_with_hints,
and HintContext names as-is) to remove the redundant turbofish.
crates/ff-rdp-cli/tests/e2e/hints.rs (1)

242-247: Tautological assertion — can be removed or strengthened.

At this point trimmed == "2" has already been asserted, so serde_json::from_str(trimmed) produces a Number, and json.get("hints") on a non-object always returns None. This block doesn't exercise anything the earlier asserts didn't. Consider dropping it, or instead strengthening the check earlier by also running the command with a jq filter that returns an object (e.g., --jq .) and asserting no "hints" key appears in that envelope-like output.

♻️ Proposed cleanup
-    // The output must not be a JSON object with a hints key.
-    if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed) {
-        assert!(
-            json.get("hints").is_none(),
-            "jq output must not be an envelope with 'hints'; got: {json}"
-        );
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/tests/e2e/hints.rs` around lines 242 - 247, The extra
serde_json check is tautological because earlier assert ensures trimmed == "2",
so drop this block or, preferably, strengthen the test by re-running the CLI
invocation with a jq envelope (e.g., pass the --jq . option) and then parsing
that output: call serde_json::from_str::<serde_json::Value> on the envelope
output and assert json.get("hints").is_none() to ensure the tool does not inject
a "hints" key into object envelopes; update the test in hints.rs to use the
envelope run and an assertion against json.get("hints") instead of the current
redundant branch that parses the scalar "2".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/ff-rdp-cli/src/commands/console.rs`:
- Around line 126-127: The current lookup for error_count uses a case-sensitive
get on by_level ("let error_count = by_level.get(\"error\")...") but msg.level
keys preserve original casing, so normalize case to avoid misses: either
(preferred) change where by_level is constructed (the aggregation that uses
msg.level) to insert keys lowercased (e.g., use msg.level.to_ascii_lowercase())
so all lookups can be lowercase, or instead change the error_count calculation
to sum counts for keys matching "error" case-insensitively (iterate by_level and
sum values where key.eq_ignore_ascii_case("error")); update the reference to
error_count and any has_errors logic accordingly.

In `@crates/ff-rdp-cli/src/hints.rs`:
- Around line 72-87: The HintContext struct exposes all fields as pub which
expands the public surface; remove the pub visibility from its fields so they
are private and only accessible via the builder API (HintContext::new and the
with_* chain methods), ensuring external callers must use the
constructor/builder and cannot bypass invariants—update any in-module code that
accesses those fields to reference them directly (still allowed) and ensure
tests/examples construct HintContext only through new/with_*.
- Around line 274-283: In hints_type_text, the user-facing description
references the input selector variable sel but the command string always clicks
button[type=submit], causing a mismatch; update the Hint::new pair so
description and cmd align—either change the description to mention submitting
via the submit button (e.g., "Click submit button for the form") or change the
command to use sel (or a form-related selector derived from sel) so the command
and the description both reference the same target; locate the Hint::new calls
inside hints_type_text and make the description and the corresponding command
consistent.

In `@kb/iterations/iteration-50-contextual-hints.md`:
- Around line 36-39: Change the fenced block that currently starts with "```"
and contains the illustrative lines "-> ff-rdp console --level error  # Check
for console errors" and "-> ff-rdp screenshot -o page.png  # Capture a
screenshot" to specify a language of "text" (i.e., use "```text") so
markdownlint MD040 is satisfied; keep the block contents unchanged and ensure
the closing fence remains "```".

---

Outside diff comments:
In `@crates/ff-rdp-cli/src/commands/a11y_summary.rs`:
- Around line 109-118: The early return after calling
render_summary_text(&output_results) skips hint rendering; instead, keep the
custom text path but invoke the hint pipeline afterwards: create the HintContext
(HintContext::new(HintSource::A11ySummary)) and pass it to the existing
OutputPipeline (OutputPipeline::from_cli(cli)?) to render only hints (e.g., call
a hints-only API or a new helper like finalize_hints or finalize_with_hints_only
using &envelope and Some(&hint_ctx)) so both render_summary_text and the hint
output are emitted when cli.format == "text" && cli.jq.is_none(); if needed add
a small helper on OutputPipeline to expose a hints-only render path and call it
from this branch.

In `@crates/ff-rdp-cli/src/commands/a11y.rs`:
- Around line 96-105: The short-circuit in the text path (where render_a11y_text
is called) skips the OutputPipeline hint rendering; update the logic so when
hints are enabled (use the HintContext/HintSource::A11y or the CLI hints
flag/state) you do not return immediately but instead run the pipeline's
finalize_with_hints (via
OutputPipeline::from_cli(...).finalize_with_hints(&envelope, Some(&hint_ctx)))
or, alternately, after render_a11y_text call the same hint render path (e.g.,
render_hints using the HintContext/hints produced by the pipeline) so that
finalize_with_hints or an equivalent hint rendering is always invoked when hints
are ON; modify the condition around render_a11y_text to only short-circuit when
hints are OFF.

In `@crates/ff-rdp-cli/src/commands/cookies.rs`:
- Around line 65-81: The envelope insertion currently adds a top-level "hint"
string which will collide with the new structured "hints" array; update the
branch that mutates the envelope (the block that checks total == 0 and uses
envelope.as_object_mut()) to insert a differently named field such as "note" or
"diagnostic" instead of "hint" (keep the same message), so consumers of the
envelope, and the downstream pipeline that constructs
HintContext::new(HintSource::Cookies) and
OutputPipeline::from_cli(...).finalize_with_hints(...), will not see both "hint"
and "hints" with different semantics.

In `@crates/ff-rdp-cli/src/commands/dom_tree.rs`:
- Around line 66-77: The text-path early return in dom_tree (when cli.format ==
"text" && cli.jq.is_none()) calls render_dom_tree_text(&results) and returns
before hint generation, so HintSource::DomTree hints never run; after printing
the text tree, invoke the same hint pipeline used elsewhere: construct
HintContext::new(HintSource::DomTree), then either call
OutputPipeline::from_cli(cli)? and finalize_with_hints(&envelope,
Some(&hint_ctx)) or call the underlying generate_hints + a small render step so
hints are generated and printed in text mode (use the same envelope/meta/results
inputs as in the JSON path to keep behavior consistent).

In `@crates/ff-rdp-cli/src/commands/geometry.rs`:
- Around line 98-111: The text-mode short-circuit in geometry.rs skips hint
rendering: when geometry.is_null() the code calls render_geometry_text(&empty)
and returns Ok(()) instead of running the OutputPipeline hint path; do not
return early — either route text output through
OutputPipeline::from_cli(cli)?.finalize_with_hints(&envelope, Some(&hint_ctx))
(so hints are emitted consistently) or, if you must keep render_geometry_text,
call the same hint-emission helper after render_geometry_text (use
HintContext::new(HintSource::Geometry).with_selector(first_sel) and the existing
envelope/meta) so the -> cmd # description lines are produced; update the
matching non-empty text path (the other short-circuit) similarly and mirror the
same fix pattern used for network.rs and perf.rs.

---

Duplicate comments:
In `@crates/ff-rdp-cli/src/commands/network.rs`:
- Around line 181-196: The short-circuit return after
render_network_summary_text skips building the envelope and calling
OutputPipeline::finalize_with_hints, so text-mode hints never render; instead,
either build the envelope before the text short-circuit or move
render_network_summary_text to run then continue to construct `envelope` and
call `OutputPipeline::finalize_with_hints(&envelope, Some(&hint_ctx))`; ensure
`empty_hint` is injected into the same `envelope` (using
`envelope.as_object_mut()`), and keep
`HintContext::new(HintSource::Network).with_detail(cli.detail)` so hints are
emitted for text summary paths without an early `return Ok(())`.

In `@crates/ff-rdp-cli/src/commands/perf.rs`:
- Around line 1014-1027: The text-mode short-circuit in perf audit skips hint
rendering by calling render_audit_text(&results) and returning early; instead,
after producing the text output you must still run the OutputPipeline hint
stage. Change the branch so that render_audit_text(&results) is called but do
not return immediately — create HintContext::new(HintSource::PerfAudit) and call
OutputPipeline::from_cli(cli)?.finalize_with_hints(&envelope, Some(&hint_ctx))
(or call finalize_with_hints after render_audit_text), propagating errors via
map_err(AppError::from), so hints are emitted in text mode just like other
formats.
- Around line 660-673: The text-mode early return in perf summary skips hint
rendering; remove the early return and ensure finalize_with_hints is always
invoked. Build meta/envelope and create hint_ctx
(HintContext::new(HintSource::PerfSummary)) as shown, call
OutputPipeline::from_cli(cli)? .finalize_with_hints(&envelope,
Some(&hint_ctx)).map_err(AppError::from) before exiting, and only after that, if
cli.format == "text" && cli.jq.is_none(), call render_summary_text(&results) and
then return Ok(()); keep references to render_summary_text,
OutputPipeline::from_cli, finalize_with_hints, HintContext,
HintSource::PerfSummary, envelope and meta to locate the change.

---

Nitpick comments:
In `@crates/ff-rdp-cli/src/commands/inspect.rs`:
- Around line 41-44: The inspect command currently creates a HintContext via
HintContext::new(HintSource::Inspect) and passes it to
OutputPipeline::finalize_with_hints(&envelope, Some(&hint_ctx)); to allow
inspect-specific hint generators to reference the inspected actor, add a builder
method on HintContext (e.g., HintContext::with_actor or a with_actor(self,
actor_id: ActorId) -> Self) and use it here to attach the inspected actor before
calling finalize_with_hints so hint generators can access that actor context
when producing follow-up suggestions.

In `@crates/ff-rdp-cli/src/commands/navigate.rs`:
- Around line 82-86: Multiple places construct the same HintContext via
HintContext::new(HintSource::Navigate); extract that repeated construction into
a small helper function (e.g., fn nav_hint_ctx() -> HintContext) and replace the
three inline calls with nav_hint_ctx() so future additions like .with_url(url)
can be applied in one place; update the call sites that currently use
HintContext::new(HintSource::Navigate) (seen near the
OutputPipeline::from_cli(...).finalize_with_hints(&envelope, Some(&hint_ctx))
usages) to use the new helper.

In `@crates/ff-rdp-cli/src/commands/network.rs`:
- Around line 128-196: The code currently inserts a top-level "hint" string (via
empty_hint) into the JSON envelope, causing both "hint" and the structured
"hints" array to coexist; change the insertion logic in both places that mutate
the envelope (after output::envelope_with_truncation and after output::envelope)
to instead fold the empty_hint into the structured "hints" array: when
envelope.as_object_mut() is Some, if a "hints" array exists push the empty_hint
into it, otherwise create a new "hints" array containing the empty_hint; remove
the obj.insert("hint", ...) usage and keep the rest
(HintContext::new(HintSource::Network), finalize_with_hints, etc.) unchanged so
consumers only see the canonical "hints" field.

In `@crates/ff-rdp-cli/src/commands/perf.rs`:
- Around line 1217-1221: run_group_by_domain currently tags hints with the
generic HintSource::Perf which prevents per-subcommand differentiation; add a
dedicated enum variant (e.g. PerfGroupByDomain) to HintSource, update its
serde/match arms/usages, and replace the HintSource::Perf in run_group_by_domain
with the new variant (or alternatively add a clear comment in the
run_group_by_domain function explaining intentional reuse) so future hint
handling can target this subcommand independently; ensure you update any pattern
matches, constructors, and tests that exhaustively match HintSource variants
(and bump any derived traits if needed).

In `@crates/ff-rdp-cli/src/commands/sources.rs`:
- Around line 130-133: The hint pipeline is created with
HintContext::new(HintSource::Sources) but doesn't convey that this command may
set fallback/fallback_method in the envelope meta; update the HintContext to
carry a fallback flag (or enum) and set it here so
finalize_with_hints(&envelope, Some(&hint_ctx)) can generate fallback-specific
suggestions. Modify HintContext (e.g., add a field like fallback_used or
fallback_method) and its constructor used here (HintContext::new) to accept that
signal, populate it when JS fallback was used (the code path that sets
envelope.meta.fallback/fallback_method), and ensure finalize_with_hints reads
that field to tailor hints (references: HintContext, HintSource::Sources,
finalize_with_hints, envelope, fallback/fallback_method).

In `@crates/ff-rdp-cli/src/commands/storage.rs`:
- Around line 34-102: The HintContext construction is duplicated across the
Grip::Null, value, and all-keys dump branches; create it once before the if/else
branch by calling
HintContext::new(HintSource::Storage).with_storage_type(canonical_type) (use the
same canonical_type already computed) and then pass that single hint_ctx into
each OutputPipeline::from_cli(cli)?.finalize_with_hints(&envelope,
Some(&hint_ctx)). This removes repetition around HintContext and ensures all
finalization calls use the identical context.

In `@crates/ff-rdp-cli/src/output_pipeline.rs`:
- Around line 130-132: The finalize method calls finalize_with_hints using the
explicit turbofish None::<&HintContext> which is unnecessary because the
parameter type Option<&HintContext> already guides type inference; update the
finalize function to pass plain None to finalize_with_hints (leave finalize,
finalize_with_hints, and HintContext names as-is) to remove the redundant
turbofish.

In `@crates/ff-rdp-cli/tests/e2e/hints.rs`:
- Around line 242-247: The extra serde_json check is tautological because
earlier assert ensures trimmed == "2", so drop this block or, preferably,
strengthen the test by re-running the CLI invocation with a jq envelope (e.g.,
pass the --jq . option) and then parsing that output: call
serde_json::from_str::<serde_json::Value> on the envelope output and assert
json.get("hints").is_none() to ensure the tool does not inject a "hints" key
into object envelopes; update the test in hints.rs to use the envelope run and
an assertion against json.get("hints") instead of the current redundant branch
that parses the scalar "2".
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2158da91-e327-4d21-a4c0-e86eb9b7ea17

📥 Commits

Reviewing files that changed from the base of the PR and between 534867d and 78b2342.

📒 Files selected for processing (36)
  • crates/ff-rdp-cli/src/cli/args.rs
  • crates/ff-rdp-cli/src/commands/a11y.rs
  • crates/ff-rdp-cli/src/commands/a11y_contrast.rs
  • crates/ff-rdp-cli/src/commands/a11y_summary.rs
  • crates/ff-rdp-cli/src/commands/click.rs
  • crates/ff-rdp-cli/src/commands/computed.rs
  • crates/ff-rdp-cli/src/commands/console.rs
  • crates/ff-rdp-cli/src/commands/cookies.rs
  • crates/ff-rdp-cli/src/commands/dom.rs
  • crates/ff-rdp-cli/src/commands/dom_tree.rs
  • crates/ff-rdp-cli/src/commands/eval.rs
  • crates/ff-rdp-cli/src/commands/geometry.rs
  • crates/ff-rdp-cli/src/commands/inspect.rs
  • crates/ff-rdp-cli/src/commands/launch.rs
  • crates/ff-rdp-cli/src/commands/nav_action.rs
  • crates/ff-rdp-cli/src/commands/navigate.rs
  • crates/ff-rdp-cli/src/commands/network.rs
  • crates/ff-rdp-cli/src/commands/page_text.rs
  • crates/ff-rdp-cli/src/commands/perf.rs
  • crates/ff-rdp-cli/src/commands/perf_compare.rs
  • crates/ff-rdp-cli/src/commands/responsive.rs
  • crates/ff-rdp-cli/src/commands/screenshot.rs
  • crates/ff-rdp-cli/src/commands/snapshot.rs
  • crates/ff-rdp-cli/src/commands/sources.rs
  • crates/ff-rdp-cli/src/commands/storage.rs
  • crates/ff-rdp-cli/src/commands/styles.rs
  • crates/ff-rdp-cli/src/commands/tabs.rs
  • crates/ff-rdp-cli/src/commands/type_text.rs
  • crates/ff-rdp-cli/src/commands/wait.rs
  • crates/ff-rdp-cli/src/hints.rs
  • crates/ff-rdp-cli/src/main.rs
  • crates/ff-rdp-cli/src/output.rs
  • crates/ff-rdp-cli/src/output_pipeline.rs
  • crates/ff-rdp-cli/tests/e2e/hints.rs
  • crates/ff-rdp-cli/tests/e2e/main.rs
  • kb/iterations/iteration-50-contextual-hints.md

Comment on lines +126 to +127
// Capture error count before consuming by_level.
let error_count = by_level.get("error").copied().unwrap_or(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Case-sensitive lookup on by_level may miss non-lowercase error levels.

by_level keys preserve the original case from msg.level (line 84), while other comparisons in this file use eq_ignore_ascii_case. Firefox's RDP reliably emits lowercase level strings today, but if that ever changes (e.g. "Error") error_count silently drops to 0 and the has_errors hint branch is skipped. Consider normalizing to lowercase when building the key, or summing over case-insensitive matches here.

Proposed defensive fix
-    let error_count = by_level.get("error").copied().unwrap_or(0);
+    let error_count: usize = by_level
+        .iter()
+        .filter(|(k, _)| k.eq_ignore_ascii_case("error"))
+        .map(|(_, v)| *v)
+        .sum();
📝 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.

Suggested change
// Capture error count before consuming by_level.
let error_count = by_level.get("error").copied().unwrap_or(0);
// Capture error count before consuming by_level.
let error_count: usize = by_level
.iter()
.filter(|(k, _)| k.eq_ignore_ascii_case("error"))
.map(|(_, v)| *v)
.sum();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ff-rdp-cli/src/commands/console.rs` around lines 126 - 127, The
current lookup for error_count uses a case-sensitive get on by_level ("let
error_count = by_level.get(\"error\")...") but msg.level keys preserve original
casing, so normalize case to avoid misses: either (preferred) change where
by_level is constructed (the aggregation that uses msg.level) to insert keys
lowercased (e.g., use msg.level.to_ascii_lowercase()) so all lookups can be
lowercase, or instead change the error_count calculation to sum counts for keys
matching "error" case-insensitively (iterate by_level and sum values where
key.eq_ignore_ascii_case("error")); update the reference to error_count and any
has_errors logic accordingly.

Comment thread crates/ff-rdp-cli/src/hints.rs
Comment thread crates/ff-rdp-cli/src/hints.rs Outdated
Comment on lines +36 to +39
```
-> ff-rdp console --level error # Check for console errors
-> ff-rdp screenshot -o page.png # Capture a screenshot
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language to the text-rendering fenced block.

markdownlint flags MD040 on this block. Since this is illustrative output rather than code, text is appropriate.

Proposed fix
-```
+```text
   -> ff-rdp console --level error  # Check for console errors
   -> ff-rdp screenshot -o page.png  # Capture a screenshot
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 36-36: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kb/iterations/iteration-50-contextual-hints.md` around lines 36 - 39, Change
the fenced block that currently starts with "```" and contains the illustrative
lines "-> ff-rdp console --level error  # Check for console errors" and "->
ff-rdp screenshot -o page.png  # Capture a screenshot" to specify a language of
"text" (i.e., use "```text") so markdownlint MD040 is satisfied; keep the block
contents unchanged and ensure the closing fence remains "```".

…uppression

- Propagate serialization error in inject_hints instead of swallowing with unwrap_or
- Gate hint injection: don't inject empty hints array when HintsMode::Off
- Escape selectors for shell safety in hint commands
- Restrict pub fields to pub(crate) on Hint and HintContext
- Remove dead url field from HintContext
- Fix "colour" → "color" spelling, fix misleading type_text description
- Update doc comments and e2e test for hints-off behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ractive ractive merged commit 3fc8b69 into main Apr 23, 2026
4 of 6 checks passed
@ractive ractive deleted the iter-50/contextual-hints branch April 23, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants