Fix cli-kit SelectInput/MultiSelectInput viewport and state management - #8159
Fix cli-kit SelectInput/MultiSelectInput viewport and state management#8159amcaplan wants to merge 20 commits into
Conversation
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>
There was a problem hiding this comment.
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
wizardcommand 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, newMultiSelectPrompt, 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.
| 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. |
| "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' |
|
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 |
Summary
Fixes two critical bugs in cli-kit
SelectInputandMultiSelectInputwhen using the description panel:R1 (Vertical Overflow): Grouped lists could overflow the viewport in stacked layout when the available height budget is tight (availableLines < 7), reintroducing visual ghosting.
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:AdjustVisibleWindowActiontype to handle visibleOptionCount changes without resetting the option setvalue) and only recomputes the visible scroll windowViewport Layout Fixes
SelectInput.tsxandMultiSelectInput.tsx:sectionHeightonly in stacked description layout (gated bydescriptionsEnabled && !showDescriptionBeside)minHeightfloor to ensuresectionHeight + STACKED_HINT_RESERVE <= availableLinesTest Coverage
use-select-state.test.tsx(new file):SelectInput.description.test.tsxandMultiSelectInput.description.test.tsx:listHeight + STACKED_HINT_RESERVE <= availableLinesthroughout navigationTesting
Technical Details
Why the hard-ceiling clamp placement matters
The
minHeightfloor (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.