feat: add file browser component for PRD file selection#263
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>
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>
- Resolve conflict in src/tui/components/index.ts (keep both FileBrowser and parallel components) - Fix info.test.ts to use direct imports instead of query string hack - Fix engine.test.ts to handle undefined HOME correctly and use evergreen comments
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Caution Review failedThe pull request is closed. WalkthroughAdds a keyboard-driven FileBrowser and listDirectory utility; integrates FileBrowser into EpicLoaderOverlay (file-prompt mode) and updates RunApp error handling. Refactors EpicLoaderOverlay props to a discriminated union. Large-scale tests moved mocks into lifecycle hooks and use dynamic imports. Updates SKILL.md example field Changes
Sequence Diagram(s)sequenceDiagram
participant User as User (keyboard)
participant RunApp as RunApp
participant EpicOverlay as EpicLoaderOverlay
participant FileBrowser as FileBrowser
participant FS as Filesystem
User->>RunApp: open epic loader (file-prompt)
RunApp->>EpicOverlay: render file-prompt mode
EpicOverlay->>FileBrowser: mount(initialPath)
FileBrowser->>FS: listDirectory(currentPath, filters)
FS-->>FileBrowser: DirectoryEntry[]
User->>FileBrowser: navigate / edit / select
FileBrowser->>FS: listDirectory(newPath) / validate path
FS-->>FileBrowser: DirectoryEntry[] or error
FileBrowser-->>EpicOverlay: onSelect(selectedFilePath)
EpicOverlay-->>RunApp: onFilePath(selectedFilePath)
RunApp->>RunApp: load epic (try/catch -> set error or close)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
Improves test coverage for the new listDirectory utility function used by the FileBrowser component.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #263 +/- ##
==========================================
- Coverage 44.95% 43.35% -1.61%
==========================================
Files 91 91
Lines 28124 28281 +157
==========================================
- Hits 12643 12260 -383
- Misses 15481 16021 +540
🚀 New features to boost your workflow:
|
- Export formatPath and truncateText from FileBrowser.tsx - Add tests for path formatting and text truncation - Add tests for navigation and path resolution logic
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Fix all issues with AI agents
In `@src/plugins/agents/base.test.ts`:
- Around line 312-319: Update the block comment explaining why tests for
BaseAgentPlugin lifecycle and envExclude live in
tests/plugins/agents/base-execute.test.ts so it is evergreen: remove time‑bound
phrases like “have been moved” and “fail in the full test suite” and instead
state the structural rationale — e.g., that tests requiring real process
execution are placed in base-execute.test.ts to isolate them from other tests
that mock node:child_process and avoid mock pollution — and keep the reference
to the Bun issue link if desired.
In `@src/plugins/trackers/builtin/beads-bv/index.test.ts`:
- Around line 6-8: Update the comment that explains why the mock is applied in
beforeAll to remove temporal phrasing: reword it as a stable invariant stating
that the mock must be configured in beforeAll (rather than at module scope) so
it does not affect other tests, and that the module under test is imported only
after this mock is in place to ensure isolation; reference the existing setup
symbols "beforeAll" and "dynamic import" (or "module under test") so readers
know where the invariant applies.
In `@src/plugins/trackers/builtin/beads.test.ts`:
- Around line 5-7: Reword the setup note to state the invariant: the mock is
configured inside beforeAll and the module under test is imported afterwards to
avoid polluting other tests; update the comment near beforeAll and the dynamic
import of the module under test (the lines describing "mock is set up in
beforeAll" and "module under test is dynamically imported") to remove temporal
phrasing like "after the mock is applied" and replace it with a stable statement
that the test imports the module only once the mock has been established.
In `@src/setup/skill-installer-spawn.test.ts`:
- Around line 5-7: Reword the two temporal test comments so they read as
evergreen invariants: state that mocks are applied in beforeAll to avoid
polluting other tests and that the module under test is dynamically imported
only after the mock has been applied; reference the test setup using the
beforeAll hook and the dynamic import of the module under test (no timing
language like "later" or "afterwards").
In `@src/tui/components/EpicLoaderOverlay.tsx`:
- Around line 180-191: EpicLoaderOverlay drops the passed-in error when mode ===
'file-prompt' because FileBrowser is returned without forwarding the error prop;
update the file-prompt branch to pass the error (and optionally an error display
handler) into <FileBrowser> so the user sees PRD load failures — locate the mode
check in EpicLoaderOverlay (the 'file-prompt' branch that returns FileBrowser)
and add the error prop (and wire it to whatever FileBrowser prop shows errors,
or add a new prop like errorMessage/onError) so failed loads surface in the UI.
In `@src/tui/components/FileBrowser.tsx`:
- Around line 116-123: When the FileBrowser becomes visible the existing
useEffect resets navigation state but not the edit state, so add resetting of
the editing flags/inputs inside that same block: when visible is true call your
edit-state setters (e.g. setIsEditing(false) and clear the edit buffer via
setEditInput('') or setEditValue('')) alongside setCurrentPath,
setSelectedIndex, and setShowHidden so the overlay never reopens with stale edit
input.
In `@tests/engine/auto-commit.test.ts`:
- Around line 110-120: The initGitRepo helper currently calls runProcess
multiple times without verifying success; update initGitRepo to fail fast on any
git command error by checking runProcess results or wrapping each await in
try/catch and rethrowing a descriptive Error that includes which command failed
(e.g., "git init", "git config user.email", "git add", "git commit") and the
underlying error/message; ensure failures from writeFile are also propagated so
tests stop immediately rather than proceeding with a broken repo state.
- Around line 21-108: The test duplicates production logic by defining
runProcess, hasUncommittedChanges, and performAutoCommit locally instead of
importing src/engine/auto-commit.ts; replace the local implementations with an
import from the production module (e.g., import { hasUncommittedChanges,
performAutoCommit } from 'src/engine/auto-commit') and adapt the test to either
run without the node child_process mocks or provide a Bun-specific shim only for
the process spawn (keep a small wrapper named runProcess only if necessary), or
alternatively add a clear TODO and CI note in the test header referencing the
production functions so future changes in
hasUncommittedChanges/performAutoCommit are kept in sync.
In `@tests/plugins/agents/base-execute.test.ts`:
- Around line 1-12: Rewrite the header comment to remove temporal phrasing and
describe the rationale in evergreen language: explain that the test file creates
test plugins that override BaseAgentPlugin.execute() to call Bun.spawn directly
to avoid interference from tests that mock node:child_process, because Bun's
mock.restore() does not reliably restore built-in modules, and note that this
preserves isolation for these tests; replace phrases like "ISOLATION FIX" and
"original tests … fail" with this persistent explanation referencing Bun.spawn
and BaseAgentPlugin.execute.
- Around line 72-163: The async runExecution inside the overridden execute
method can throw (Bun.spawn, stream reads, awaits) and currently never resolves
resolvePromise on error; wrap the entire runExecution body in try/catch/finally,
and in the catch build and call resolvePromise with a failed
AgentExecutionResult (use the existing executionId, status: 'failed', a non-zero
exitCode like 1, include any captured stdout/stderr if available or empty
strings, compute duration using startedAt and new Date(), set interrupted:
false, and set startedAt/endedAt ISO strings), and still invoke
options?.onEnd(result) inside a try/catch so onEnd runs for error cases too;
ensure resolvePromise is always called exactly once even when errors occur.
In `@tests/templates/engine.test.ts`:
- Around line 704-721: The tests currently only override process.env.HOME but
installBuiltinTemplates -> getUserConfigDir uses os.homedir() which on Windows
reads USERPROFILE, so extend the sandbox: in the beforeEach that sets testDir
and originalHome, also capture originalUserProfile (e.g. let
originalUserProfile: string | undefined) and when process.platform === 'win32'
set process.env.USERPROFILE = testDir; in afterEach restore USERPROFILE (delete
if undefined, else set back) alongside the existing HOME restore and
cleanupTestDir call so the sandboxed home is consistent across platforms when
running installBuiltinTemplates.
In `@tests/utils/clipboard.test.ts`:
- Around line 5-7: Update the explanatory comment in
tests/utils/clipboard.test.ts to remove temporal phrasing and state the
invariant: explain that the mock is configured inside beforeAll and the module
under test is dynamically imported afterwards to avoid polluting other test
files; reference the beforeAll setup and the dynamic import order so readers
understand the reason without using words like "prevent" tied to time (e.g.,
phrase it as "The mock is configured in beforeAll and the module is imported
afterwards to avoid cross-test pollution").
🧹 Nitpick comments (3)
src/sandbox/detect.test.ts (1)
14-21: Test exercises Node.js stdlib rather than project code.The describe block is named "sandbox detection constants" but the test only validates
platform()fromnode:os, which is a standard library function. No actual code from the sandbox detection module is imported or tested here. If the intent is to have a placeholder test file, consider either:
- Adding a test that exercises an actual constant or export from
src/sandbox/detect.ts, or- Removing this file entirely if all meaningful tests are in
tests/sandbox/detect.test.ts.Additionally, the platform list is missing
'android', which is a valid return value fromplatform()per Node.js documentation.🔧 Suggested fix to include all valid platforms
const currentPlatform = platform(); // platform() should return a known value - expect(['darwin', 'linux', 'win32', 'freebsd', 'openbsd', 'sunos', 'aix']).toContain( + expect(['darwin', 'linux', 'win32', 'freebsd', 'openbsd', 'sunos', 'aix', 'android']).toContain( currentPlatform );src/plugins/agents/base.test.ts (1)
460-510: Guard env mutation with try/finally for isolation.If an assertion throws, the env vars and agent disposal won’t run. Wrapping the mutation/cleanup in a
try/finallykeeps the suite isolated.tests/plugins/agents/base-execute.test.ts (1)
456-600: Use try/finally around process.env mutations.If a test assertion throws, the env resets and disposal won’t run. Wrapping each mutation block in
try/finallywill keep the suite isolated.
Comment updates (remove temporal phrasing): - src/plugins/agents/base.test.ts: Explain test isolation rationale - src/plugins/trackers/builtin/beads-bv/index.test.ts: Document mock setup invariant - src/plugins/trackers/builtin/beads.test.ts: Document mock setup invariant - src/setup/skill-installer-spawn.test.ts: Document mock setup invariant - tests/utils/clipboard.test.ts: Document mock setup invariant - tests/plugins/agents/base-execute.test.ts: Rewrite header as evergreen - tests/engine/auto-commit.test.ts: Add TODO for production imports Code improvements: - src/tui/components/FileBrowser.tsx: Reset edit state on visibility change, add errorMessage prop for external error display - src/tui/components/EpicLoaderOverlay.tsx: Pass error to FileBrowser - tests/engine/auto-commit.test.ts: Add fail-fast error handling to initGitRepo - tests/plugins/agents/base-execute.test.ts: Add try/catch to ensure resolvePromise is always called, even on errors - tests/templates/engine.test.ts: Add USERPROFILE support for Windows
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/tui/components/FileBrowser.tsx`:
- Around line 158-188: The expandPath / navigateToPath logic resolves typed
relative paths against process.cwd() because expandPath always calls
resolve(expanded); update expandPath (or adjust navigateToPath) to resolve
relative inputs against the current browsing directory (currentPath) instead of
the process CWD: detect absolute paths (path.isAbsolute(expanded) or starting
with '/'), tilde (~) and explicit relative markers (./ or ../) and for plain
relative names call resolve(currentPath, expanded) so that navigateToPath uses
the visible directory context when setting currentPath, while preserving the
existing tilde expansion behavior.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@tests/plugins/agents/base-execute.test.ts`:
- Around line 241-323: The runExecution function inside
BunSpawnEnvTestPlugin.execute can throw (e.g., Bun.spawn or readStream) and
currently never resolves/rejects resolvePromise; wrap the body of runExecution
in a try/catch that mirrors BunSpawnTestPlugin behavior: on success build and
call resolvePromise(result) as before, and in catch create a failed
AgentExecutionResult (status 'failed', include error message in stderr,
appropriate exitCode like 1, duration, startedAt/endedAt) then call
resolvePromise (or rejectPromise if existing pattern uses reject) and still call
options?.onEnd inside its own try/catch; ensure readers/proc are awaited/cleaned
up if necessary and reference runExecution, Bun.spawn, readStream,
resolvePromise, and options?.onEnd when making the changes.
🧹 Nitpick comments (2)
tests/templates/engine.test.ts (2)
704-707: Variable shadowing creates minor inefficiency.The
testDirdeclaration at line 705 shadows the outer scope'stestDir(line 565). This means the outerbeforeEachstill creates a test directory and sets upgetUserConfigDirSpy, but these are unused for tests in this inner describe block (which uses environment variable sandboxing instead).This is functionally correct but slightly wasteful. Consider renaming to
installTestDirfor clarity, or restructuring to avoid the outer hooks running unnecessarily.
767-779: Add sandbox verification assertion and correct misleading comment.The
os.homedir()function readsprocess.env.HOME(on Unix) orprocess.env.USERPROFILE(on Windows) at runtime. SincebeforeEachsets these environment variables before the test runs, the sandboxing is effective. The comment at lines 768–769 is misleading and should be updated.Add a verification assertion (as used in the
installGlobalTemplatestests at lines 644 and 653) to confirm the test writes to the sandbox:Proposed fix
// Use fresh import to bypass mock pollution from migration-install.test.ts - // Note: Cannot use sandboxed testDir here because fresh import uses real getUserConfigDir + // Note: Sandboxing works via HOME/USERPROFILE env vars set in beforeEach const result = freshInstallBuiltinTemplates(false); // The function returns results for all four templates (not undefined) expect(result).toBeDefined(); expect(result.results).toBeDefined(); expect(result.results.length).toBe(4); expect(result.templatesDir).toContain('.config/ralph-tui/templates'); + expect(result.templatesDir.startsWith(testDir)).toBe(true); // Verify templates were actually created or skipped (both are valid outcomes) expect(result.results.every(r => r.created || r.skipped)).toBe(true);Without this assertion, a sandboxing regression could go undetected and cause tests to write to the actual user's home directory.
…rectory The expandPath function was resolving relative paths against process.cwd() instead of the current browsing directory (currentPath). Now: - Tilde (~) expands to home directory - Absolute paths resolve directly - Relative paths (including ./ and ../) resolve against currentPath This makes the file browser behave intuitively when typing relative paths in the path editor.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@tests/engine/auto-commit.test.ts`:
- Around line 5-11: Update the TODO comment to remove future-tense phrasing and
state the current constraint and the follow-up action: note that because Bun's
mock.restore() does not reliably restore builtin modules the test re-implements
hasUncommittedChanges and performAutoCommit locally using Bun.spawn, and
instruct that these should be replaced with imports from
src/engine/auto-commit.ts when Bun's module mock restoration is fixed so tests
stay in sync.
In `@tests/utils/clipboard.test.ts`:
- Around line 88-102: Update the inline comments inside the beforeAll block to
remove temporal wording and state invariants: change "Mock the modules before
importing clipboard" to something like "Ensure node:child_process and node:os
modules are mocked" and "Import after mocking" to "Import clipboard module after
mocks are registered"; keep references to mock.module calls for
'node:child_process' and 'node:os' and the subsequent dynamic import of
'../../src/utils/clipboard.js' that assigns writeToClipboard so the intent
remains clear and evergreen.
🧹 Nitpick comments (1)
website/next-env.d.ts (1)
1-3: Add the required ABOUTME JSDoc header before the reference directives.This file should start with a file-level JSDoc describing its purpose and prefixed with
ABOUTME:.✍️ Proposed fix
+/** + * ABOUTME: Declares Next.js ambient type references and route type imports for the website app. + */ /// <reference types="next" /> /// <reference types="next/image-types/global" /> import "./.next/types/routes.d.ts";As per coding guidelines, Start all code files with a file-level JSDoc comment section explaining the file's purpose, prefixed with 'ABOUTME: '.
- auto-commit.test.ts: Change TODO to NOTE, remove future-tense - clipboard.test.ts: Reword mock setup comments as invariants
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/tui/components/FileBrowser.tsx`:
- Around line 123-131: Normalize initialPath before setting current path when
the FileBrowser becomes visible: call the same normalisation/expansion logic
used by navigateToPath (expand '~' and resolve relative paths) and pass the
resolved path to setCurrentPath (instead of the verbatim initialPath), then
reset transient UI state to avoid stale flashes by also clearing the directory
list, error and loading flags (use the component's setters such as
setSelectedIndex, setShowHidden, setEditingPath, setEditedPath and the
list/error/loading setters — e.g., setEntries, setError, setLoading if present)
so the component immediately shows the fresh loading state and empty list while
the directory reloads.
| // Reset state when becoming visible | ||
| useEffect(() => { | ||
| if (visible) { | ||
| setCurrentPath(initialPath ?? process.cwd()); | ||
| setSelectedIndex(0); | ||
| setShowHidden(false); | ||
| setEditingPath(false); | ||
| setEditedPath(''); | ||
| } |
There was a problem hiding this comment.
Normalise initialPath before use to keep ~/relative paths working.
initialPath is stored verbatim on open, so ~ and relative inputs are not normalised like navigateToPath (which does expand/resolve). This leads to inconsistent behaviour and can fail on valid inputs. While touching this block, also clear transient list/error/loading state to avoid a stale flash before the directory reloads.
🛠️ Proposed fix
useEffect(() => {
if (visible) {
- setCurrentPath(initialPath ?? process.cwd());
+ const nextPath = (() => {
+ if (!initialPath) return process.cwd();
+ let expanded = initialPath.trim();
+ if (expanded.startsWith('~')) {
+ expanded = homedir() + expanded.slice(1);
+ }
+ if (isAbsolute(expanded)) {
+ return resolve(expanded);
+ }
+ return resolve(process.cwd(), expanded);
+ })();
+ setCurrentPath(nextPath);
+ setEntries([]);
+ setError(null);
+ setLoading(true);
setSelectedIndex(0);
setShowHidden(false);
setEditingPath(false);
setEditedPath('');
}
}, [visible, initialPath]);🤖 Prompt for AI Agents
In `@src/tui/components/FileBrowser.tsx` around lines 123 - 131, Normalize
initialPath before setting current path when the FileBrowser becomes visible:
call the same normalisation/expansion logic used by navigateToPath (expand '~'
and resolve relative paths) and pass the resolved path to setCurrentPath
(instead of the verbatim initialPath), then reset transient UI state to avoid
stale flashes by also clearing the directory list, error and loading flags (use
the component's setters such as setSelectedIndex, setShowHidden, setEditingPath,
setEditedPath and the list/error/loading setters — e.g., setEntries, setError,
setLoading if present) so the component immediately shows the fresh loading
state and empty list while the directory reloads.
feat: add file browser component for PRD file selection
Summary
This PR incorporates the work from PR #236 by @esumerfd with fixes and updates merged from main.
Replace manual text input with a keyboard-navigable file browser when selecting prd.json files.
Features (from #236)
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 utilitiesAdditional Fixes
tests/templates/engine.test.ts:HOMEenv var inafterEach(delete instead of assign)tests/commands/info.test.ts:?test-reloadquery string approach to direct imports with singleton resetsrc/tui/components/index.tsTest Status
Closes #236
Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Tests
Documentation