Skip to content

feat(core): add interactive mode and key binding hooks#122

Open
zrosenbauer wants to merge 7 commits intomainfrom
feat/interactive-story
Open

feat(core): add interactive mode and key binding hooks#122
zrosenbauer wants to merge 7 commits intomainfrom
feat/interactive-story

Conversation

@zrosenbauer
Copy link
Copy Markdown
Member

Summary

  • Add interactive mode to the stories viewer — press i to give story components full terminal control (header/sidebar hidden), double-press Esc to exit
  • Introduce reusable key handling primitives: keys.ts (normalizer + pattern parser), useKeyBinding (declarative keymap with sequence support), useKeyInput (enhanced raw input hook)
  • Extend ViewerMode union, status bar, help overlay, and preview component to support the new mode

Test plan

  • Verify pnpm check passes (typecheck + lint + format)
  • Verify pnpm test passes including new keys.test.ts, use-key-binding.test.ts, use-key-input.test.ts
  • Manual: run stories viewer, press i to enter interactive mode, confirm header/sidebar hide
  • Manual: double-press Esc to exit interactive mode, confirm UI restores
  • Manual: verify ? help overlay shows interactive mode shortcuts

zrosenbauer and others added 2 commits March 27, 2026 18:53
Add interactive mode to the stories viewer that gives story components
full terminal control. Introduce reusable key handling primitives
(keys.ts, useKeyBinding, useKeyInput) and a double-escape hook to exit.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 27, 2026

🦋 Changeset detected

Latest commit: f9b0f36

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@kidd-cli/core Minor
@kidd-cli/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
oss-kidd Ignored Ignored Preview Mar 28, 2026 1:23am

Request Review

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 27, 2026

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing feat/interactive-story (f9b0f36) with main (b3049e5)

Open in CodSpeed

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an "interactive" viewer mode and keyboard infrastructure. New UI changes: help-overlay, status-bar, and preview now surface an interactive mode (entered via i when a story is selected and exited via double-Escape). Runtime and context types/remove deprecated spinner option across context/runtime/test code. Introduces keyboard utilities and hooks (keys parsing/matching, useKeyBinding, useKeyInput), tests for key handling, a useDoubleEscape hook, viewer-mode extensions (enter/exit interactive), and plumbing in stories-app to render borderless interactive previews and hide header/sidebar. Minor test/mock refactors and an added trailing newline in an example package.json.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • joggrdocs/kidd PR 110 — touches the same viewer components and keyboard/preview behavior (interactive-mode keyflows and rendering).
  • joggrdocs/kidd PR 91 — overlaps with UI surface and exported keyboard/input hooks and re-exports.
  • joggrdocs/kidd PR 119 — modifies the same context/status spinner surface and types affected by removal of the spinner override.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the main changes: introduction of interactive mode and key binding hooks as core features.
Description check ✅ Passed Description is clearly related to the changeset, detailing interactive mode functionality, key handling primitives, and test coverage expectations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 feat/interactive-story

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

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: 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 `@packages/core/src/stories/viewer/hooks/use-double-escape.ts`:
- Around line 30-37: The JSDoc for the exported hook useDoubleEscape is missing
a `@returns` tag; update the comment block above the useDoubleEscape function to
include a `@returns` description indicating it returns void (e.g., "@returns
{void} No return value — hook sets up side effects"), so the exported function
JSDoc complies with the project's TypeScript documentation standard.

In `@packages/core/src/ui/use-key-binding.ts`:
- Around line 70-103: The historyRef retains past key events across mode toggles
causing false sequence matches; in useKeyBinding add an effect that watches the
isActive option and when isActive transitions to true reset historyRef.current =
[] so stale events are cleared before inputHandler runs (use the existing
historyRef and isActive from options to implement this effect inside
useKeyBinding).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d9586f8-ebba-4aaf-807b-aac2d4ae697a

📥 Commits

Reviewing files that changed from the base of the PR and between b3049e5 and 989f31a.

⛔ Files ignored due to path filters (1)
  • .changeset/interactive-story-mode.md is excluded by !.changeset/**
📒 Files selected for processing (16)
  • examples/diagnostic-output/package.json
  • packages/core/src/stories/viewer/components/help-overlay.tsx
  • packages/core/src/stories/viewer/components/preview.tsx
  • packages/core/src/stories/viewer/components/status-bar.tsx
  • packages/core/src/stories/viewer/hooks/use-double-escape.ts
  • packages/core/src/stories/viewer/hooks/use-panel-focus.ts
  • packages/core/src/stories/viewer/stories-app.tsx
  • packages/core/src/test/context.ts
  • packages/core/src/ui/index.ts
  • packages/core/src/ui/keys.test.ts
  • packages/core/src/ui/keys.ts
  • packages/core/src/ui/output/output.tsx
  • packages/core/src/ui/use-key-binding.test.ts
  • packages/core/src/ui/use-key-binding.ts
  • packages/core/src/ui/use-key-input.test.ts
  • packages/core/src/ui/use-key-input.ts

zrosenbauer and others added 2 commits March 27, 2026 19:01
Deduplicate the groupKey::variantName ID parsing logic shared by
resolveStory and buildPreviewContext into a single parseStoryId helper.
Tighten resolveBoxBase return type and remove unnecessary cast.

Co-Authored-By: Claude <noreply@anthropic.com>
Add missing @returns tag to useDoubleEscape JSDoc. Clear key history
buffer in useKeyBinding when isActive transitions from false to true,
preventing stale escape events from triggering false sequence matches.

Co-Authored-By: Claude <noreply@anthropic.com>
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/stories/viewer/stories-app.tsx`:
- Around line 144-145: The keydown handler currently gates the "i" interactive
entry on selectedStoryId, which can be non-null while selectedStory is null
after reload/rename; change the condition from checking selectedStoryId to
checking selectedStory (e.g., selectedStory !== null or truthiness) so
enterInteractiveMode() only runs when the actual selectedStory exists; update
the conditional that references input === 'i' && selectedStoryId !== null to use
selectedStory instead and ensure any related logic that assumes selectedStory is
present still holds.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fb881bc8-c6d1-4d85-9d8d-728352bb9fdc

📥 Commits

Reviewing files that changed from the base of the PR and between 989f31a and a1880d9.

📒 Files selected for processing (2)
  • packages/core/src/lib/log.ts
  • packages/core/src/stories/viewer/stories-app.tsx

Hard-migrate from the deprecated `spinner` option to `status` across
CliOptions, RuntimeOptions, CreateContextOptions, CreateContextStatusOptions,
and TestContextOptions. Remove all backwards-compatibility shims.

Co-Authored-By: Claude <noreply@anthropic.com>
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: 1

♻️ Duplicate comments (1)
packages/core/src/ui/use-key-binding.ts (1)

61-73: ⚠️ Potential issue | 🟡 Minor

Use a destructured public signature and complete the exported JSDoc.

useKeyBinding is exported, so the two positional parameters and missing @returns both violate the repo's TS public-function standard. Fix the signature now before this API spreads through the UI surface.

As per coding guidelines: "**/*.ts: Functions with 2+ parameters must use object destructuring" and "All exported functions require JSDoc with @param, @returns." (contributing/standards/typescript/functions.md).

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

In `@packages/core/src/ui/use-key-binding.ts` around lines 61 - 73, The exported
function useKeyBinding currently takes two positional params (bindings: readonly
KeyBinding[], options: KeyBindingOptions = {}) and lacks complete JSDoc; change
its public signature to a single destructured object parameter (e.g., {
bindings, options }: { bindings: readonly KeyBinding[]; options?:
KeyBindingOptions }) and update the exported JSDoc to include `@param` entries for
bindings and options and an `@returns` description (void). Ensure references to
the function in the file still match the new call style and keep the existing
types KeyBinding and KeyBindingOptions in the signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/ui/use-key-binding.ts`:
- Around line 25-26: The constant MAX_HISTORY_LENGTH currently hard-caps key
history to 10 and causes matches to fail for longer sequences; change logic in
useKeyBinding to derive the retained history size from the longest parsed
sequence returned by parseKeyPattern() (or refuse to register bindings whose
parseKeyPattern() result exceeds a configurable maximum) instead of always
truncating to MAX_HISTORY_LENGTH; update the history truncation step (where
MAX_HISTORY_LENGTH is used) to use the computed max sequence length from parsed
bindings (or throw/validate at registration time) so sequence matching can
succeed for arbitrarily long, or explicitly rejected, patterns.

---

Duplicate comments:
In `@packages/core/src/ui/use-key-binding.ts`:
- Around line 61-73: The exported function useKeyBinding currently takes two
positional params (bindings: readonly KeyBinding[], options: KeyBindingOptions =
{}) and lacks complete JSDoc; change its public signature to a single
destructured object parameter (e.g., { bindings, options }: { bindings: readonly
KeyBinding[]; options?: KeyBindingOptions }) and update the exported JSDoc to
include `@param` entries for bindings and options and an `@returns` description
(void). Ensure references to the function in the file still match the new call
style and keep the existing types KeyBinding and KeyBindingOptions in the
signature.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4b08ca2f-f279-44bb-9ca0-63c475e88b93

📥 Commits

Reviewing files that changed from the base of the PR and between a1880d9 and 31ae3d0.

📒 Files selected for processing (2)
  • packages/core/src/stories/viewer/hooks/use-double-escape.ts
  • packages/core/src/ui/use-key-binding.ts

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.

Caution

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

⚠️ Outside diff range comments (1)
packages/core/src/types/cli.ts (1)

106-177: ⚠️ Potential issue | 🟠 Major

Breaking API removal needs explicit migration handling.

At Line 106 (CliOptions), removing spinner breaks consumers still passing that option. Please add a migration note/changelog entry (or a temporary compatibility path) before release.

As per coding guidelines: ** reviews must flag breaking changes (incompatible API changes, removed exports).

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

In `@packages/core/src/types/cli.ts` around lines 106 - 177, The CliOptions type
removal of the spinner option is a breaking change for consumers; reinstate a
temporary compatibility path by adding an optional deprecated spinner field back
to the CliOptions interface (e.g., readonly spinner?: unknown) and make the
runtime code that consumes CliOptions detect and log a deprecation warning (or
map it into the new DisplayConfig) so existing callers continue working;
additionally add a clear migration note to the changelog/upgrade guide
explaining removal and how to migrate to DisplayConfig or the new API. Ensure
references to CliOptions and spinner are updated and the deprecation is
documented.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/core/src/types/cli.ts`:
- Around line 106-177: The CliOptions type removal of the spinner option is a
breaking change for consumers; reinstate a temporary compatibility path by
adding an optional deprecated spinner field back to the CliOptions interface
(e.g., readonly spinner?: unknown) and make the runtime code that consumes
CliOptions detect and log a deprecation warning (or map it into the new
DisplayConfig) so existing callers continue working; additionally add a clear
migration note to the changelog/upgrade guide explaining removal and how to
migrate to DisplayConfig or the new API. Ensure references to CliOptions and
spinner are updated and the deprecation is documented.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e5258353-98e1-4ce3-aec1-6cc0daefebb4

📥 Commits

Reviewing files that changed from the base of the PR and between 31ae3d0 and 4179e2f.

📒 Files selected for processing (9)
  • packages/core/src/cli.ts
  • packages/core/src/context/create-context.ts
  • packages/core/src/context/status.ts
  • packages/core/src/runtime/runtime.ts
  • packages/core/src/runtime/types.ts
  • packages/core/src/test/context.test.ts
  • packages/core/src/test/context.ts
  • packages/core/src/test/types.ts
  • packages/core/src/types/cli.ts
💤 Files with no reviewable changes (2)
  • packages/core/src/cli.ts
  • packages/core/src/runtime/runtime.ts

… size

Gate interactive mode entry on selectedStory instead of selectedStoryId
to prevent entering interactive mode when the story was removed by a
reload. Derive key history buffer size from the longest registered
sequence binding instead of a hard-coded cap.

Co-Authored-By: Claude <noreply@anthropic.com>
@zrosenbauer
Copy link
Copy Markdown
Member Author

@coderabbitai This review finding is not valid.

The spinnerstatus removal on CliOptions was an intentional hard migration, not an accidental breakage:

  1. Explicit commit (4179e2f) with message: "Hard-migrate from the deprecated spinner option to status… Remove all backwards-compatibility shims."
  2. A compat shim previously existed and was deliberately removed in c581c9d ("remove deprecated spinner compat shim") — this isn't a missing migration path, it's the completion of one.
  3. Pre-1.0 project — per project conventions, breaking changes use minor bumps (never major), so a clean removal is the expected upgrade path.
  4. The replacement (readonly status?: Status) is a superset that includes spinner plus progress, tasks, and taskLog.
  5. The changeset already documents this migration for consumers.

No changes needed here.

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: 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 `@packages/core/src/ui/use-key-binding.ts`:
- Around line 61-73: The exported hook useKeyBinding currently accepts two
positional parameters and lacks a `@returns` JSDoc, violating the public-function
contract; change its signature to accept a single destructured parameter object
(e.g. { bindings, options }) so callers pass named properties, update the JSDoc
to include explicit `@param` tags for both bindings and options and add an
`@returns` {void} tag, and update any internal/call sites to use the new object
form; keep the implementation and the function name useKeyBinding unchanged
except for its parameter shape and JSDoc.
- Around line 76-83: The refs bindingsRef, prevActiveRef and historyRef are
being mutated during render (lines around bindingsRef.current = bindings and
prevActiveRef.current = isActive) which can leak uncommitted values into the
inputHandler callback; move all ref synchronizations and the isActive-based
history reset into a useEffect that runs when bindings or isActive change so
mutations occur inside a React effect boundary; also refactor the exported
useKeyBinding signature to accept a single object parameter (destructure the two
current parameters) to comply with the 2+ parameter rule and update any
callsites (e.g. useDoubleEscape) to pass an object.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cff539a7-7a6d-4f3e-921c-cc20d91d226b

📥 Commits

Reviewing files that changed from the base of the PR and between 4179e2f and 24db2a0.

📒 Files selected for processing (2)
  • packages/core/src/stories/viewer/stories-app.tsx
  • packages/core/src/ui/use-key-binding.ts

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Tip

For best results, initiate chat on the files or code changes.

@zrosenbauer: Understood — I was wrong to flag this. The removal is the intentional completion of a planned migration: the compat shim was explicitly deleted, status is a superset replacement, the changeset documents the upgrade path, and the pre-1.0 minor-bump convention applies. I'll record this so I don't flag it again.

(ˆ_ˆ)ノ


✏️ Learnings added
Learnt from: zrosenbauer
URL: https://github.com/joggrdocs/kidd/pull/122

Timestamp: 2026-03-28T00:35:08.799Z
Learning: In `joggrdocs/kidd`, the `spinner?: Spinner` option was intentionally removed from `CliOptions` (packages/core/src/types/cli.ts), `CreateContextOptions`, `CreateContextStatusOptions`, `RuntimeOptions`, and `TestContextOptions` as a completed hard migration to `status?: Status`. The `status` option is a superset covering spinner, progress, tasks, and taskLog. A compat shim previously existed and was deliberately removed (commit c581c9d). The changeset documents the migration for consumers. Do not flag the absence of `spinner` or the missing backwards-compat shim as a regression or breaking change.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: zrosenbauer
Repo: joggrdocs/kidd PR: 44
File: .changeset/redesign-auth-middleware.md:1-3
Timestamp: 2026-03-10T21:24:08.399Z
Learning: `kidd-cli/core` in the `joggrdocs/kidd` repo is a pre-release (0.x) package. Per semver, breaking changes are expected and acceptable under minor version bumps for 0.x packages. Do not flag minor changesets as needing a major bump for this package.

Learnt from: zrosenbauer
Repo: joggrdocs/kidd PR: 41
File: packages/core/src/middleware/auth/strategies/token.ts:23-29
Timestamp: 2026-03-10T19:31:09.091Z
Learning: In `packages/core/src/middleware/auth/strategies/token.ts`, returning `null` on all `promptError` values (including cancellation/ContextError) from `resolveFromToken` is intentional pre-existing behavior. The original `try/catch` also returned `null` on any error, so the `attemptAsync` conversion preserves the same semantics. Do not flag this as a regression.

Learnt from: zrosenbauer
Repo: joggrdocs/kidd PR: 41
File: packages/core/src/middleware/auth/strategies/device-code.ts:360-365
Timestamp: 2026-03-10T19:31:02.908Z
Learning: In `packages/core/src/middleware/auth/strategies/device-code.ts`, the `token_type` validation in `requestToken` is intentionally lenient: when `token_type` is absent from the token response, the code assumes bearer (following the RFC 6749 Section 7.1 default convention); it only rejects when `token_type` is explicitly present but is not `bearer`. This is pre-existing behavior and should not be flagged in future reviews.

Learnt from: zrosenbauer
Repo: joggrdocs/kidd PR: 110
File: packages/core/src/stories/viewer/stories-output.tsx:40-45
Timestamp: 2026-03-25T20:19:22.668Z
Learning: In joggrdocs/kidd, `packages/core/src/stories/viewer/stories-output.tsx` implements the `--out` flag for `kidd stories`. It intentionally renders the story component to stdout (Ink terminal output) for visual piping to LLMs — it does NOT emit JSON metadata. Do not flag the ready branch mounting `StoryRenderer` as a bug or missing JSON serialization path.

Refactor useKeyBinding to accept a single UseKeyBindingArgs object
(2+ param rule) and move ref mutations into useEffect to prevent
concurrent render leaks. Add @returns to JSDoc. Update caller
useDoubleEscape and re-export UseKeyBindingArgs type.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant