feat: add file browser component for PRD file selection#236
feat: add file browser component for PRD file selection#236esumerfd wants to merge 30 commits intosubsy:mainfrom
Conversation
Replace manual text input with a keyboard-navigable file browser when selecting prd.json files. Features include: - Directory navigation with j/k, Enter, Backspace, ~, and arrow keys - File filtering to show only prd*.json files - Editable path field (press / to type a path directly) - Hidden files toggle (press .) - Visual hint showing typical prd.json location - Auto-scroll to keep selection visible in viewport Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@esumerfield is attempting to deploy a commit to the plgeek Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAdds a keyboard-driven FileBrowser and listDirectory utility; refactors EpicLoaderOverlay into a discriminated-union API with 'list' and 'file-prompt' modes (file-prompt delegates to FileBrowser); RunApp selects overlay mode; exports FileBrowser; many tests switched to lifecycle-scoped mocks and dynamic imports. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant RunApp
participant EpicLoaderOverlay
participant FileBrowser
participant listDirectory
User->>RunApp: open "Load Epic"
RunApp->>EpicLoaderOverlay: render(mode="file-prompt" or "list")
alt file-prompt
EpicLoaderOverlay->>FileBrowser: mount(visible=true, initialPath, filters)
User->>FileBrowser: navigate / edit path / select
FileBrowser->>listDirectory: listDirectory(path, options)
listDirectory-->>FileBrowser: DirectoryEntry[]
alt select directory
FileBrowser->>FileBrowser: update currentPath & refresh entries
else select file
FileBrowser->>EpicLoaderOverlay: onSelect(filePath)
EpicLoaderOverlay->>RunApp: onFilePath(filePath)
end
User->>FileBrowser: Esc
FileBrowser->>EpicLoaderOverlay: onCancel()
else list
EpicLoaderOverlay->>User: show epics list
User->>EpicLoaderOverlay: navigate / select epic
EpicLoaderOverlay->>RunApp: onSelect(epic)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 🧪 Unit Test Generation v2 is now available!We have significantly improved our unit test generation capabilities. To enable: Add this to your reviews:
finishing_touches:
unit_tests:
enabled: trueTry it out by using the Have feedback? Share your thoughts on our Discord thread! 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. Comment |
Codecov Report❌ Patch coverage is
❌ Your patch status has failed because the patch coverage (8.69%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #236 +/- ##
==========================================
- Coverage 45.45% 43.48% -1.97%
==========================================
Files 84 84
Lines 24396 24553 +157
==========================================
- Hits 11088 10678 -410
- Misses 13308 13875 +567
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/tui/components/FileBrowser.tsx`:
- Around line 44-49: The formatPath function uses homedir() and currently checks
path.startsWith(home), which can falsely match siblings (e.g., /home/alice2);
update formatPath to only substitute with '~' when the match is on a
path-separator boundary by verifying that either the path equals home or the
character after home is path.sep (or '/' on POSIX) before returning '~' +
path.slice(home.length); keep the rest of the function and use the existing
homedir() and formatPath symbols to locate the change.
- Around line 224-227: The default branch is appending raw key.sequence into
editedPath (via setEditedPath) which allows control/escape sequences
(arrow/function keys) to be inserted; update the handler that uses setEditedPath
in the FileBrowser component so it only appends printable characters — e.g.,
validate key.sequence/key.name before calling setEditedPath by ensuring the
sequence is a single printable character (ASCII printable range or matches a
printable regex) and ignore entries where key.name indicates control keys (like
'up','down','left','right','escape','f1' etc.) or where key.sequence contains
non-printable characters.
🧹 Nitpick comments (2)
src/tui/components/FileBrowser.tsx (1)
349-359: Consider making the hint text configurable.The hard‑coded “Pattern: prd*.json” hint will be misleading if this component is reused with different filters. Exposing a prop (or deriving it from
fileExtension/filenamePrefix) would keep the UI accurate.src/tui/components/EpicLoaderOverlay.tsx (1)
22-51: MakeonFilePathrequired infile-promptmode to prevent silent no-ops.When
mode='file-prompt', the component delegates toFileBrowserand callsonFilePath?.(path)using optional chaining. IfonFilePathis omitted, file selection silently fails. A discriminated union type would enforce this contract at compile time, ensuring thatonFilePathis always provided whenevermode='file-prompt'is set.🔧 Example type-safe shape
+type EpicLoaderOverlayBaseProps = { + visible: boolean; + trackerName: string; + onCancel: () => void; +}; + -export interface EpicLoaderOverlayProps { - /** Whether the overlay is visible */ - visible: boolean; - /** Mode: 'list' for beads-style selection, 'file-prompt' for json-style */ - mode: EpicLoaderMode; - /** Available epics (for list mode) */ - epics: TrackerTask[]; - /** Whether epics are loading */ - loading: boolean; - /** Error message if loading failed */ - error?: string; - /** Tracker name for display */ - trackerName: string; - /** Current epic ID (for highlighting) */ - currentEpicId?: string; - /** Callback when an epic is selected */ - onSelect: (epic: TrackerTask) => void; - /** Callback when user cancels (Escape) */ - onCancel: () => void; - /** Callback when file path is submitted (file-prompt mode) */ - onFilePath?: (path: string) => void; -} +export type EpicLoaderOverlayProps = + | (EpicLoaderOverlayBaseProps & { + mode: 'list'; + epics: TrackerTask[]; + loading: boolean; + error?: string; + currentEpicId?: string; + onSelect: (epic: TrackerTask) => void; + onFilePath?: never; + }) + | (EpicLoaderOverlayBaseProps & { + mode: 'file-prompt'; + onFilePath: (path: string) => void; + onSelect?: never; + epics?: never; + loading?: never; + error?: string; + currentEpicId?: string; + });
The example prd.json output was using "project" instead of "name", causing Claude to sometimes generate invalid JSON that failed ralph-tui validation with "Missing required field: name". Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix formatPath to check path separator boundary, preventing false ~ substitution on sibling directories (e.g., /home/alice2) - Fix setEditedPath to only accept printable characters, preventing control/escape sequences from being inserted - Add filePatternHint prop to FileBrowser for configurable hint text - Refactor EpicLoaderOverlayProps to use discriminated union type for type-safe mode handling - Update RunApp.tsx to use conditional rendering for proper type narrowing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/tui/components/RunApp.tsx`:
- Around line 2573-2578: Wrap the onEpicSwitch call inside the onSelect handler
in a try/catch/finally: call await onEpicSwitch(epic) inside try, catch any
thrown error and set the component's epic error state (the same state used by
the onFilePath handler, e.g., setEpicError or setErrorMessage) so the user sees
feedback, and ensure setShowEpicLoader(false) runs in finally so the loader is
always hidden. Locate the onSelect callback in RunApp.tsx and update it to
mirror the error handling pattern used by onFilePath.
🧹 Nitpick comments (2)
src/tui/components/FileBrowser.tsx (1)
168-178: Consider user feedback on invalid paths.When
navigateToPathreceives an invalid path that causesisDirectoryto returnfalse(including when the path doesn't exist), the error message says "Not a directory" which may be misleading for non-existent paths.💡 Optional improvement for clearer error messaging
const navigateToPath = useCallback(async (path: string) => { const expanded = expandPath(path); - const isDir = await isDirectory(expanded); - if (isDir) { - setCurrentPath(expanded); - setEditingPath(false); - setEditedPath(''); - } else { - setError(`Not a directory: ${path}`); + try { + const isDir = await isDirectory(expanded); + if (isDir) { + setCurrentPath(expanded); + setEditingPath(false); + setEditedPath(''); + } else { + setError(`Not a directory or does not exist: ${path}`); + } + } catch { + setError(`Cannot access: ${path}`); } }, [expandPath]);src/tui/components/EpicLoaderOverlay.tsx (1)
180-192: Simplify callback wrapper.The arrow function wrapper
(path) => onFilePath(path)is redundant sinceonFilePathalready has the correct signature.♻️ Simplify to direct reference
return ( <FileBrowser visible={visible} fileExtension=".json" filenamePrefix="prd" trackerLabel={trackerName} - onSelect={(path) => onFilePath(path)} + onSelect={onFilePath} onCancel={onCancel} /> );
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@src/plugins/trackers/builtin/beads-rust/index.test.ts`:
- Around line 92-95: The inline comment "Now import the module - it will get the
mocked versions" uses temporal wording; replace it with an evergreen phrasing
such as "Import the module so it uses the mocked implementations" to describe
intent without referencing time; update the comment immediately above the import
in index.test.ts (the lines that import './index.js' and assign
BeadsRustTrackerPlugin) to the new wording.
In `@src/setup/skill-installer-spawn.test.ts`:
- Around line 53-55: The inline comment above the dynamic import should be
reworded to remove temporal phrasing ("Now ..."); update the comment that
precedes the import of './skill-installer.js' and the assignment to
installViaAddSkill so it is evergreen (e.g., "Import the module to use the
mocked spawn" or "Import module to obtain installViaAddSkill with mocked spawn")
while keeping the rest of the test unchanged.
In `@src/setup/skill-installer.test.ts`:
- Around line 5-8: Update the header comment to match the actual import query
string used in this test file: replace the mention of "?reload" with
"?test-reload" (or alternatively change the import to use "?reload" if that was
intended); ensure the header text describing the reason for the reload
explicitly references the same query string ("?test-reload") so future readers
aren't misled.
In `@tests/utils/process.test.ts`:
- Around line 72-73: The regex uses the unescaped testDir variable in
expect(...).toMatch(new RegExp(`^(/private)?${testDir}$`)), which can
misinterpret path characters as regex metacharacters; fix by escaping regex
metacharacters in testDir before creating the RegExp (e.g., const escapedTestDir
= testDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') and then use new
RegExp(`^(/private)?${escapedTestDir}$`) in the expectation).
|
Thanks @esumerfd - mind taking a look at the outstanding feedback from CR ? |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/commands/skills.test.ts`:
- Around line 10-54: Add an afterAll hook to restore Bun's global module mocks
created by mock.module(...) so the pass-through mock for
'../setup/skill-installer.js' does not leak to other suites; specifically add
afterAll(() => { mock.restore(); }) (or the appropriate mock.restore call from
bun:test) in src/commands/skills.test.ts after the beforeAll block to undo the
mock.module registration used when importing the skills module (affecting the
mock object used around
executeSkillsCommand/printSkillsHelp/parseInstallArgs/buildAddSkillArgs/parseAddSkillOutput).
In `@src/tui/components/FileBrowser.tsx`:
- Around line 184-186: The catch block in navigateToPath declares an unused
variable err causing an ESLint no-unused-vars error; update the catch to either
remove the parameter or use err in the error message—e.g., change catch (err) to
catch (err) and call setError(`Cannot access path: ${path} - ${String(err)}`) or
simply use catch { setError(`Cannot access path: ${path}`) } so the linter stops
complaining; locate this in the navigateToPath function where setError is
invoked.
In `@src/tui/components/RunApp.tsx`:
- Around line 2552-2560: The onFilePath handler currently awaits
onFilePathSwitch(path) but doesn't catch rejections, so failures can leave the
overlay open; wrap the call to onFilePathSwitch(path) in a try/catch inside the
onFilePath async callback (the same block that calls setShowEpicLoader and
setEpicLoaderError), setShowEpicLoader(false) on success,
setEpicLoaderError(`Failed to load file: ${path}`) on failure, and ensure the
loader is hidden in the catch path (and if needed also clear or set error when
onFilePathSwitch returns false) so all failure modes surface an error and close
the loading overlay.
In `@tests/commands/info.test.ts`:
- Around line 50-61: The imports in the beforeAll hook should use cache-busting
query params and TS-ignore comments to avoid module cache pollution: update the
three dynamic imports that provide AgentRegistry, registerBuiltinAgents, and
collectSystemInfo to append "?test-reload" to their module specifiers and add
the corresponding `@ts-expect-error` comment above each import so the test loads
fresh modules when calling AgentRegistry.resetInstance(),
registerBuiltinAgents(), and assigning collectSystemInfo.
In `@tests/templates/engine.test.ts`:
- Line 6: Remove the temporal NOTE and add afterAll to the test imports (so the
import line includes afterAll), then ensure any mock.module() call created in
beforeAll is restored in an afterAll inside the same describe block;
specifically update the describe block for installBuiltinTemplates to call
mock.module().restore() (or the appropriate mock restore API) in afterAll to
undo the module mock created by beforeAll, and delete the NOTE lines that use
temporal language; target the mock.module(), beforeAll, and
installBuiltinTemplates describe block to scope and cleanup the mock.
- Around line 738-750: The test calls freshInstallBuiltinTemplates(false) but
doesn't sandbox the user config dir, so update the test to ensure
getUserConfigDir is redirected to a temp testDir: either set
process.env.XDG_CONFIG_HOME (or process.env.HOME) to testDir in a beforeEach and
restore in afterEach, or change the mock.module() factory used for the fresh
import to return a mocked getUserConfigDir that resolves to testDir; make sure
to apply the sandbox before importing the fresh module and restore
environment/mocks after the test so freshInstallBuiltinTemplates and any code
that reads getUserConfigDir write to the isolated testDir instead of the real
~/.config directory.
🧹 Nitpick comments (4)
src/sandbox/detect.test.ts (1)
14-21: Consider renaming the describe block or testing actual module exports.The test suite is named "sandbox detection constants" but only verifies that Node.js's
platform()function returns expected values. This tests stdlib behaviour rather than any code from the sandbox detection module itself. If the intent is to have a placeholder test suite, consider either:
- Renaming to something more accurate (e.g., "platform utility behaviour")
- Importing and testing actual constants or exports from the sandbox detection module
Given the header comment explains this file is intentionally minimal, this is a minor clarity improvement.
💡 Suggested rename for clarity
-describe('sandbox detection constants', () => { - test('platform returns expected values', () => { +describe('platform utility', () => { + test('returns a known OS identifier', () => { const currentPlatform = platform(); - // platform() should return a known value expect(['darwin', 'linux', 'win32', 'freebsd', 'openbsd', 'sunos', 'aix']).toContain( currentPlatform ); }); });tests/sandbox/detect.test.ts (2)
18-36: Consider adding a comment about keeping this in sync with production code.The local implementation mirrors
src/sandbox/detect.tsbut usesBun.spawnto avoid mock pollution. This creates a maintenance risk if the production logic changes.📝 Suggested documentation addition
/** * Local implementation of commandExists using Bun.spawn to bypass mock pollution. * Mirrors the logic in src/sandbox/detect.ts but uses Bun's native spawn. + * + * IMPORTANT: Keep this implementation in sync with src/sandbox/detect.ts. + * If the production logic changes, update this function accordingly. * * NOTE: Uses process.platform instead of platform() from node:os to avoid * mock pollution from other tests that mock the node:os module. */
45-53: Usetest.skipIf()for clearer platform-specific test skipping.Early
return;statements in conditional blocks mask issues in test reporting. Bun's test runner supportstest.skipIf()to make the intent explicit and improve test output clarity.♻️ Example refactor
- test('returns true for which command', async () => { - // 'which' doesn't exist on Windows (uses 'where' instead) - if (process.platform === 'win32') { - return; // Skip on Windows - } - // 'which' should exist on Linux/macOS - const result = await commandExists('which'); - expect(result).toBe(true); - }); + test.skipIf(process.platform === 'win32')('returns true for which command', async () => { + // 'which' should exist on Linux/macOS (Windows uses 'where' instead) + const result = await commandExists('which'); + expect(result).toBe(true); + });Also applies to: lines 131–144, 146–159, 161–168
tests/plugins/agents/base-execute.test.ts (1)
458-600: Guard env mutations with try/finally to prevent leakage between tests.
If a spawn/expectation throws before the restore block,process.envchanges can bleed into later tests. A small helper keeps cleanup consistent.♻️ Example helper to guarantee env restore
+async function withEnv( + key: string, + value: string, + run: () => Promise<void> +): Promise<void> { + const original = process.env[key]; + process.env[key] = value; + try { + await run(); + } finally { + if (original === undefined) { + delete process.env[key]; + } else { + process.env[key] = original; + } + } +} @@ - const originalValue = process.env.TEST_KEPT_VAR; - process.env.TEST_KEPT_VAR = 'should_remain'; - - let stdout = ''; - const handle = agent.execute('TEST_KEPT_VAR', [], { - onStdout: (data) => { - stdout += data; - }, - }); - - await handle.promise; - - if (originalValue === undefined) { - delete process.env.TEST_KEPT_VAR; - } else { - process.env.TEST_KEPT_VAR = originalValue; - } - - await agent.dispose(); + let stdout = ''; + await withEnv('TEST_KEPT_VAR', 'should_remain', async () => { + const handle = agent.execute('TEST_KEPT_VAR', [], { + onStdout: (data) => { + stdout += data; + }, + }); + + await handle.promise; + }); + + await agent.dispose();
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@tests/templates/engine.test.ts`:
- Around line 720-723: The inline comment above the dynamic import for
realTemplateEngine uses temporal wording ("first"); update the comment to be
evergreen by removing temporal cues and clarifying intent—for example, change
the comment that begins "CRITICAL: Get the REAL template engine module first,
bypassing any cached mock" to something like "CRITICAL: Load the real template
engine module, bypassing any cached mock" so it clearly documents why the import
with ?test-reload-install is used; locate the comment around the
realTemplateEngine variable and the import of
'../../src/templates/engine.js?test-reload-install' and replace the wording
accordingly.
- Around line 714-716: The afterEach teardown currently restores HOME by
assigning process.env.HOME = originalHome which writes the string "undefined" if
originalHome was not set; change the restore logic in the afterEach block so it
checks originalHome (the saved value) and if it is undefined deletes
process.env.HOME instead of assigning, otherwise reassigns it, keeping the
existing await cleanupTestDir(testDir) call; update references in the afterEach
closure where originalHome and process.env.HOME are used to implement this
conditional restore.
🧹 Nitpick comments (2)
src/commands/skills.test.ts (1)
24-44: Reduce maintenance drift in the pass-through mock.
Manually listing every export is easy to forget when new exports are added. Consider spreading the real module and override only when needed.♻️ Proposed refactor
- mock.module('../setup/skill-installer.js', () => ({ - listBundledSkills: realSkillInstaller.listBundledSkills, - isSkillInstalledAt: realSkillInstaller.isSkillInstalledAt, - resolveSkillsPath: realSkillInstaller.resolveSkillsPath, - installViaAddSkill: realSkillInstaller.installViaAddSkill, - resolveAddSkillAgentId: realSkillInstaller.resolveAddSkillAgentId, - buildAddSkillInstallArgs: realSkillInstaller.buildAddSkillInstallArgs, - expandTilde: realSkillInstaller.expandTilde, - computeSkillsPath: realSkillInstaller.computeSkillsPath, - getBundledSkillsDir: realSkillInstaller.getBundledSkillsDir, - isEloopOnlyFailure: realSkillInstaller.isEloopOnlyFailure, - getSkillStatusForAgent: realSkillInstaller.getSkillStatusForAgent, - AGENT_ID_MAP: realSkillInstaller.AGENT_ID_MAP, - })); + mock.module('../setup/skill-installer.js', () => ({ + ...realSkillInstaller, + }));src/tui/components/FileBrowser.tsx (1)
158-188: Resolve typed relative paths against the current directory for more intuitive navigation.Right now, relative inputs resolve against process CWD; using
currentPathis more consistent with the browser context and matches user expectations when typing paths relative to the currently displayed directory.♻️ Proposed change
const expandPath = useCallback((path: string): string => { let expanded = path.trim(); if (expanded.startsWith('~')) { expanded = homedir() + expanded.slice(1); } - return resolve(expanded); - }, []); + return resolve(currentPath, expanded); + }, [currentPath]);
|
dude, what happened, we were so close? Did I need to do something else? |
@esumerfd fear not - all the great work is in main via #263 After the massive parallel execution branch was merged it was just much easier for me to pick your commits into a new PR, combining with @bigs fuzzy search changes. 0.7.0 should go out tonight with all the changes |
Summary
Replace manual text input with a keyboard-navigable file browser when selecting prd.json files. Features include:
Features
j/k,Enter,Backspace,~, and arrow keysprd*.jsonfiles/to type a path directly).)Files Changed
src/tui/components/FileBrowser.tsx- New file browser componentsrc/tui/components/EpicLoaderOverlay.tsx- Integrate file browsersrc/tui/components/index.ts- Export new componentsrc/utils/files.ts- File utilities🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Refactor
New API
Tests
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.