Skip to content

Fix cli-kit SelectInput/MultiSelectInput viewport and state management - #8159

Closed
amcaplan wants to merge 20 commits into
mainfrom
ariel/wizard
Closed

Fix cli-kit SelectInput/MultiSelectInput viewport and state management#8159
amcaplan wants to merge 20 commits into
mainfrom
ariel/wizard

Conversation

@amcaplan

Copy link
Copy Markdown
Contributor

Summary

Fixes two critical bugs in cli-kit SelectInput and MultiSelectInput when using the description panel:

  1. R1 (Vertical Overflow): Grouped lists could overflow the viewport in stacked layout when the available height budget is tight (availableLines < 7), reintroducing visual ghosting.

  2. R2 (Selection Reset): Width-only terminal resizes that cross the description-panel layout breakpoint would silently reset the highlighted item back to the first option.

Changes

Core State Management Fix

use-select-state.ts:

  • Added AdjustVisibleWindowAction type to handle visibleOptionCount changes without resetting the option set
  • New reducer case that preserves the current selection (value) and only recomputes the visible scroll window
  • Updated the effect that detects visibleOptionCount changes to dispatch the new action instead of resetting to default state

Viewport Layout Fixes

SelectInput.tsx and MultiSelectInput.tsx:

  • Apply a hard-ceiling clamp to sectionHeight only in stacked description layout (gated by descriptionsEnabled && !showDescriptionBeside)
  • Clamp is applied after the minHeight floor to ensure sectionHeight + STACKED_HINT_RESERVE <= availableLines
  • Preserves byte-for-byte invariance: no-description and beside-layout paths are unchanged

Test Coverage

use-select-state.test.tsx (new file):

  • Unit tests for the state hook with two scenarios:
    • Verifies selection is preserved when visibleOptionCount changes (R2 path)
    • Verifies reset path still works when the option set itself changes

SelectInput.description.test.tsx and MultiSelectInput.description.test.tsx:

  • R1 regression test: "keeps a grouped stacked list within the vertical budget (availableLines=3|6)"
    • Tests both tight budgets to catch the overflow at edge cases
    • Validates the invariant listHeight + STACKED_HINT_RESERVE <= availableLines throughout navigation
  • R2 regression test: "preserves the highlighted item across a width-only resize (beside↔stacked)"
    • Navigates to an item in narrow/stacked layout, then simulates a resize to wide/beside layout
    • Verifies the highlight stays on the same item (not reset to item 0)
    • Verifies onChange is not called with a different value (no silent selection jump)

Testing

  • All 249 ui tests pass (33 test files)
  • New regression tests exercise both bugs explicitly
  • No type errors or lint issues

Technical Details

Why the hard-ceiling clamp placement matters

The minHeight floor (5 for grouped lists, 2 otherwise) was originally applied unconditionally, forcing the list height to a minimum even when the available budget couldn't accommodate it. In stacked layout, this meant the list + gap + preview could exceed the available height. The hard-ceiling clamp comes after the floor, ensuring we never exceed the budget.

Why the state action preserves selection

When only the visible-row budget changes (e.g., a width resize), the option set is unchanged, so the user's current selection is still valid. Resetting to the first option would silently move the highlight without user input, violating the principle of least surprise.

amcaplan and others added 20 commits July 20, 2026 20:25
Turns a natural-language question into a store data query via the
shopify.dev assistant, routing to either ShopifyQL (analytics) or raw
Admin GraphQL (catalog/state), running it against the store's Admin API,
and rendering the result as a table/tree or JSON (`--json`).

Includes the command, service layer (prompt building, tolerant JSON
parsing, self-contained SSE assistant client, query execution with
retry-once + access-denied handling, output rendering) and 39 unit
tests. Reuses the existing `store auth` session; all flags, no
positional args.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap the single-shot shopify.dev assistant SSE call (assistant.ts + parse.ts
plus one blind retry) for an in-process @openai/agents tool-calling loop. The
loop points at Shopify's internal LLM proxy, mounts @shopify/dev-mcp over stdio
for docs/schema knowledge, and exposes two CLI-hosted store-data tools:
run_shopifyql (model writes only the ShopifyQL string; the tool wraps it in the
correct shopifyqlQuery shape) and run_admin_graphql (raw Admin GraphQL). The
command derives {api, query, result} from the last successful tool call — ground
truth — and uses the model's final text as the rationale.

The model plane (internal proxy) is employee-only, so this is a prototype: a
real merchant-facing version would swap only that plane for a hosted-inference
backend. Everything else — the store-data tools and the query execution — is
production-shaped.

In text mode the agent streams its summary live to stderr, so the renderer no
longer reprints it (avoids a duplicate); --json still carries it as `rationale`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The flag only biased the agent toward one API surface (advisory, not a hard
lock, since the agent runs and verifies its own queries). It added little over
the agent's own routing, so remove it: the agent always chooses between
ShopifyQL and Admin GraphQL based on the question.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a report query is access-denied, the stored token is just missing a
scope the model can't fix by rewriting the query. Parse the required scope
from the error, re-run the `shopify store auth` OAuth flow to grant it, and
retry once. A per-run guard stops us reopening the browser in a loop, and the
refreshed session is shared so later queries reuse the new token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default (non-JSON) output now asks the model to emit a @json-render
spec that is rendered with Ink through a closed 16-component catalog,
falling back to the existing text output if generation, validation, or
rendering fails. The `--json` path is unchanged and short-circuits before
any UI/model machinery loads. Spec generation reuses a shared
tracing-safe proxy runner and is strictly validated before rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The report agent now runs as many read-only queries as a question needs
and derives analytics from raw Admin GraphQL records when ShopifyQL can't
express them, instead of stopping after one query. Every successful query
is surfaced through the report, the terminal visualization, and --json via
a new queries[] field on StoreReportResult (replacing the single
api/query/result), so a compound answer shows all of its parts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dev-mcp server inherited its stderr straight into the CLI, leaking a
startup banner and a per-tool-call usage-telemetry line into `store report`
output. `@openai/agents`' MCPServerStdio exposes no stderr control, so launch
dev-mcp through a small node -e wrapper that re-spawns it with stderr
discarded and OPT_OUT_INSTRUMENTATION=true (which also stops the telemetry
being sent), forwarding the JSON-RPC stdio channel and termination signals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace @json-render/ink stock renderers with per-component, cli-kit-styled renderers (badge, bar-chart, box, callout, card, divider, heading, key-value, list, list-item, markdown, metric, sparkline, status-line, table) wired through reportComponents, and add visual tests.

Pin @types/react to 18.3.12 workspace-wide via root pnpm.overrides, replacing the scoped 19.2.3 overrides that split the type tree and broke the workspace build. @json-render/ink and store compile clean on 18.3.12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agent sometimes printed CLI instructions (shopify store auth/execute) telling the user to run queries themselves instead of calling its own tools. Strengthen the tool-usage guidance to forbid that and require it to execute the needed queries directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The visualization model often emitted an invalid borderStyle value, which failed strict validation and silently fell back to raw JSON. Enumerate the legal borderStyle values in the component cheatsheet, retry generation up to three times by feeding the exact validation error back to the model, and print a visible failure summary when all attempts fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the per-query "Running the report query" spinner with a single cli-kit task bar, owned by the command, that spans the whole model phase — the agent loop and the dashboard-spec generation — and updates its title by phase: analyzing the question, querying the store (with a live query count), consulting the Shopify dev docs, and building the report. Suppress the model's streamed narration in normal mode, routing it to the debug log so it only appears under --verbose. Authenticate before the bar goes up and render the dashboard after it closes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The text report now shows only when the dashboard can't be generated, and the agent's summary is no longer streamed live (it goes to the debug log under --verbose). Print result.rationale as the headline of the text report so the fallback still surfaces the answer, not just the raw query results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A reusable Ink-based checkbox prompt: up/down focuses, space toggles, Enter
confirms, resolving to the selected values in declared-choice order. Selecting
zero items is valid and resolves to []. Mirrors the existing SelectPrompt/
SelectInput conventions and is demonstrated in the kitchen-sink prompts showcase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A guided, metadata-driven walkthrough over the full command catalog:
search or browse by topic to find a command, fill its required
parameters (and optional flags on request) with typed prompts and
validation, preview the assembled command line, then hand off via
config.runCommand — the target command re-parses, runs its own runtime
prompts, and renders its own output.

Handles oclif exactlyOne/atLeastOne required flag groups and boolean
negation (--no-<flag>). Thin and delegating by design: no bespoke
dynamic selectors — dynamic values are left to the target command's
own prompts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a SelectInput's items carry a `description`, render list rows as
single truncated lines and show the highlighted item's description in a
side panel (wide terminals) or below the list (narrow terminals). This
keeps the rendered height stable, fixing layout breakage and scroll
ghosting that occurred when descriptions were concatenated into labels.
The panel is opt-in: lists without any description render exactly as
before. Demoed in the kitchen-sink command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give MultiSelectInput the same opt-in description panel as SelectInput:
when items carry a `description`, rows render as single truncated lines
and the focused item's description shows in a side panel (wide) or below
the list (narrow), keeping the rendered height stable. The two shared
layout constants move into DescriptionPanel so both components import
them. The no-description path is unchanged. Demoed in the kitchen-sink
command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move each discovery choice's description out of its label and into the
choice's `description` field so the cli-kit select panel renders it,
keeping list rows id-only and single-line instead of wrapping long
`id — summary` strings. Search still matches on description, and browse
by topic shows descriptions to match discovery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix two narrow-terminal regressions in the description panel: long group
titles now truncate to a single line (they previously wrapped and, since
the list reserves one line per title, pushed the focused row out of the
clipped list box), and the narrow/stacked layout now shows a single
truncated preview line instead of a multi-line panel whose rows are
reserved out of the list's vertical budget, so the total height stays
within the viewport and no longer ghosts while scrolling. Press Shift+Tab
to toggle a full-description takeover when the preview is truncated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the same narrow-terminal fixes as SelectInput to MultiSelectInput:
truncate long group titles to one line, reserve the stacked hint's rows
out of the list's vertical budget, replace the stacked multi-line panel
with a single truncated preview line, and add the Shift+Tab
full-description takeover. Drop the now-unused DESCRIPTION_PANEL_LINES_BELOW
constant from DescriptionPanel, whose last consumer this removes. The
no-description path stays byte-for-byte unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes two bugs in the description panel layout:

R1 (vertical overflow): Grouped lists with minHeight=5 could overflow the
viewport in stacked layout at tight budgets (availableLines < 7). Apply a
hard-ceiling clamp AFTER the minHeight floor, only in stacked layout, to
ensure list + preview + gap always fits within availableLines.

R2 (selection reset): Width-only resizes crossing the description-panel
breakpoint would reset the highlighted item to the first option. Add a new
AdjustVisibleWindowAction to preserve the user's selection while recomputing
the visible scroll window when only visibleOptionCount changes.

Changes:
- use-select-state.ts: Add AdjustVisibleWindowAction type and reducer case
- SelectInput/MultiSelectInput: Apply hard-ceiling clamp only in stacked case
- Tests: Add unit tests for state hook + regression tests for both bugs

All 249 ui tests pass; no type or lint errors.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@amcaplan
amcaplan requested review from a team as code owners July 22, 2026 19:01
Copilot AI review requested due to automatic review settings July 22, 2026 19:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR significantly expands CLI functionality and ui-kit capabilities: it adds a new AI-backed shopify store report command (including model orchestration, query tooling, validation, and Ink rendering), introduces an interactive shopify wizard command for discovering/running commands, and updates cli-kit prompts (SelectInput descriptions + a new MultiSelect prompt) with accompanying state-management changes and tests.

Changes:

  • Add store:report (agent loop + proxy client + store query tools + UI spec generation/validation + Ink renderers) and wire it into @shopify/store.
  • Add wizard command and supporting “thin wizard” services (catalog search/browse, parameter introspection, argv assembly) and wire it into @shopify/cli.
  • Extend cli-kit prompt surface with renderMultiSelectPrompt, new MultiSelectPrompt, and SelectInput description-panel behavior + state hook updates + tests.

Reviewed changes

Copilot reviewed 78 out of 80 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/store/tsconfig.json Include TSX sources for new Ink/React renderers.
packages/store/tsconfig.build.json Exclude TSX test files from build.
packages/store/src/index.ts Register new store:report command.
packages/store/src/cli/services/store/report/ui/spec.ts Generate/parse/validate static JSON render specs from model output.
packages/store/src/cli/services/store/report/ui/spec.test.ts Unit tests for spec parsing/validation and retry flow.
packages/store/src/cli/services/store/report/ui/renderers/terminal-width.ts Helper to match cli-kit layout width behavior.
packages/store/src/cli/services/store/report/ui/renderers/table.tsx cli-kit-styled Table renderer for reports.
packages/store/src/cli/services/store/report/ui/renderers/status-line.tsx StatusLine renderer with icon/color conventions.
packages/store/src/cli/services/store/report/ui/renderers/sparkline.tsx Sparkline renderer with resampling and optional color/label.
packages/store/src/cli/services/store/report/ui/renderers/safe-props.ts Guardrails for model-controlled styling props.
packages/store/src/cli/services/store/report/ui/renderers/metric.tsx Metric renderer (value + optional trend/detail).
packages/store/src/cli/services/store/report/ui/renderers/markdown.tsx Markdown-to-Ink renderer using marked.
packages/store/src/cli/services/store/report/ui/renderers/list.tsx List renderer + shared row layout helper.
packages/store/src/cli/services/store/report/ui/renderers/list-item.tsx ListItem renderer aligned with List indentation.
packages/store/src/cli/services/store/report/ui/renderers/key-value.tsx Key/Value renderer with coercion and missing-value handling.
packages/store/src/cli/services/store/report/ui/renderers/index.ts Registry of report renderers for json-render catalog.
packages/store/src/cli/services/store/report/ui/renderers/heading.tsx Heading renderer and shared heading text helper.
packages/store/src/cli/services/store/report/ui/renderers/divider.tsx Divider renderer matching cli-kit banner rule style.
packages/store/src/cli/services/store/report/ui/renderers/card.tsx Card renderer using rounded border + optional title.
packages/store/src/cli/services/store/report/ui/renderers/callout.tsx Callout renderer using left-border bar styling.
packages/store/src/cli/services/store/report/ui/renderers/box.tsx Box/Text renderers with safe prop spreading.
packages/store/src/cli/services/store/report/ui/renderers/bar-chart.tsx BarChart renderer with width logic and optional value/percent labels.
packages/store/src/cli/services/store/report/ui/renderers/badge.tsx Badge renderer with bracket styling + variant colors.
packages/store/src/cli/services/store/report/ui/renderers.test.tsx Renderer behavior tests (output + element tree assertions).
packages/store/src/cli/services/store/report/ui/render.tsx Ink rendering wrapper with explicit teardown and fake stdin.
packages/store/src/cli/services/store/report/ui/render.test.tsx Render smoke test ensuring no hang and expected output fragments.
packages/store/src/cli/services/store/report/ui/prompt.ts Visualization-model system instructions and request framing.
packages/store/src/cli/services/store/report/ui/prompt.test.ts Tests for deterministic instruction/request construction.
packages/store/src/cli/services/store/report/ui/index.ts Orchestrate spec generation, failure reporting, and fallback rendering.
packages/store/src/cli/services/store/report/ui/index.test.ts Tests for generation outcomes and fallback behavior.
packages/store/src/cli/services/store/report/ui/fake-stdin.ts Synthetic stdin implementation for deterministic Ink behavior.
packages/store/src/cli/services/store/report/ui/catalog.ts Closed json-render component catalog for reports (no actions).
packages/store/src/cli/services/store/report/ui/catalog.test.ts Catalog completeness/no-actions test.
packages/store/src/cli/services/store/report/types.ts Types for report envelope and query record data.
packages/store/src/cli/services/store/report/tools.ts Agent tools (run_shopifyql, run_admin_graphql) with retry/reauth logic.
packages/store/src/cli/services/store/report/tools.test.ts Tests for tool execution, accumulation, and access-denied recovery.
packages/store/src/cli/services/store/report/reauth.ts Parse missing scopes and run re-auth flow to refresh context.
packages/store/src/cli/services/store/report/reauth.test.ts Scope parsing tests.
packages/store/src/cli/services/store/report/prompt.ts Agent instructions for routing/tool-use and injection guard.
packages/store/src/cli/services/store/report/prompt.test.ts Tests for agent instruction contents/constraints.
packages/store/src/cli/services/store/report/progress.ts Progress title helpers + tool classification.
packages/store/src/cli/services/store/report/progress.test.ts Progress helper tests.
packages/store/src/cli/services/store/report/output.ts Text/JSON output rendering for report results.
packages/store/src/cli/services/store/report/output.test.ts Output formatting tests.
packages/store/src/cli/services/store/report/index.ts Prepare/run report pipeline; read proxy config; shape results.
packages/store/src/cli/services/store/report/index.test.ts Tests for prepare/run behavior and env configuration.
packages/store/src/cli/services/store/report/execute.ts Execute ShopifyQL/Admin GraphQL via cli-kit APIs with mutation guard translation.
packages/store/src/cli/services/store/report/execute.test.ts Tests for success, parse errors, access-denied, mutations, and classified errors.
packages/store/src/cli/services/store/report/dev-mcp-launch.ts Wrapper launch config to silence dev-mcp stderr/instrumentation noise.
packages/store/src/cli/services/store/report/dev-mcp-launch.test.ts Tests for stderr silencing and stdio passthrough behavior.
packages/store/src/cli/services/store/report/client.ts Proxy-configured Agents SDK runner creation with tracing disabled.
packages/store/src/cli/services/store/report/agent.ts Full agent loop: proxy runner + dev-mcp + streamed progress + query accumulation.
packages/store/src/cli/services/store/report/agent.test.ts Agent orchestration tests with injected loop/executors.
packages/store/src/cli/commands/store/report.ts New shopify store report oclif command wiring and UX flow.
packages/store/src/cli/commands/store/report.test.ts Command-level tests for json/text paths and progress title updates.
packages/store/project.json Update lint commands to lint src (incl. TSX).
packages/store/package.json Add dependencies for agents, MCP, json-render, Ink, React, marked, zod, etc.
packages/cli/src/index.ts Register new wizard command.
packages/cli/src/cli/services/wizard/parameters.ts Derive prompt kinds + normalize oclif flags/args + group rules.
packages/cli/src/cli/services/wizard/parameters.test.ts Tests for parameter derivation and validators.
packages/cli/src/cli/services/wizard/command-line.ts Assemble argv tokens and render a preview command line.
packages/cli/src/cli/services/wizard/command-line.test.ts Tests for argv assembly and preview quoting.
packages/cli/src/cli/services/wizard/catalog.ts Build/search/browse command catalog and choice construction.
packages/cli/src/cli/services/wizard/catalog.test.ts Tests for catalog construction, searching, and topic browsing.
packages/cli/src/cli/services/kitchen-sink/prompts.ts Add example usage of select/multiselect prompts with descriptions.
packages/cli/src/cli/commands/wizard.ts New interactive wizard command implementation.
packages/cli/src/cli/commands/wizard.test.ts Wizard flow tests (search, browse, groups, optional flags, handoff).
packages/cli-kit/src/public/node/ui.tsx Public API: add renderMultiSelectPrompt.
packages/cli-kit/src/private/node/ui/hooks/use-select-state.ts Add adjust-visible-window action to preserve selection on visible-row changes.
packages/cli-kit/src/private/node/ui/hooks/use-select-state.test.tsx New tests covering resize vs options-changed behavior.
packages/cli-kit/src/private/node/ui/components/SelectInput.tsx Description panel + stacked/beside layout + full-description toggle + viewport clamping.
packages/cli-kit/src/private/node/ui/components/MultiSelectPrompt.tsx New multi-select prompt wrapper component.
packages/cli-kit/src/private/node/ui/components/MultiSelectPrompt.test.tsx Tests for toggling/submission/defaults/group ordering/abort behavior.
packages/cli-kit/src/private/node/ui/components/DescriptionPanel.tsx Shared description panel component for select/multiselect.
package.json Add pnpm package extension for json-render/ink zod dep; add @types/react override note/config.

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

Comment on lines 327 to +331
if (visibleOptionCount !== lastVisibleOptionCount) {
// Only the visible-row count changed (the option set is unchanged — that case is handled by the
// reset above). Keep the current highlight and just re-fit the visible window; do NOT reset to
// the first option, or a width-only resize across the description-panel breakpoint would jump
// the selection back to item 0.
Comment thread package.json
Comment on lines +105 to +107
"overrides": {
"@types/react": "18.3.12"
},
import StoreInfo from './cli/commands/store/info.js'
import StoreList from './cli/commands/store/list.js'
import StoreOpen from './cli/commands/store/open.js'
import StoreReport from './cli/commands/store/report.js'
@amcaplan

Copy link
Copy Markdown
Contributor Author

Closing: this PR was opened in error by an automated tool and was never intended to exist. The branch stays on origin, but this is prototype/hackday work (the store report command depends on an employee-only internal LLM proxy) and is not a merge candidate. No review needed.

@amcaplan amcaplan closed this Jul 22, 2026
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