Conversation
Implement WebSocket server for remote ralph-tui control with: - `ralph-tui listen` command starts server on port 7890 (configurable) - Secure token authentication generated on first run - Token rotation via --rotate-token flag - Daemon mode with --daemon flag for background operation - Binds to localhost-only without token, all interfaces with token - Comprehensive audit logging of all remote actions
Implement remote server configuration management with CLI commands: - `ralph-tui remote add <alias> <host:port> --token <token>` adds a remote - `ralph-tui remote list` shows configured remotes with connection status - `ralph-tui remote remove <alias>` removes a remote - `ralph-tui remote test <alias>` tests connectivity Configuration stored in TOML format at ~/.config/ralph-tui/remotes.toml
Add tab bar for navigating between local and remote ralph-tui instances. - Tab bar shows at top of TUI with connection status indicators - First tab is always "Local" (current machine) - Remote tabs show alias with status (●/◐/○ for connected/connecting/disconnected) - Keyboard navigation: 1-9 for direct tab access, [ and ] to cycle - Selecting a disconnected tab triggers reconnection (no auto-reconnect on startup) - Subtle "+" affordance in tab bar for adding new remotes
Implements full remote control capabilities for ralph-tui remote instances: - Added message types for subscriptions, queries, and operations (Subscribe, GetState, GetTasks, Pause, Resume, Interrupt, etc.) - Extended RemoteClient with request/response correlation system and typed methods for all remote operations - Added engine integration to server with subscription system - Server broadcasts engine events to subscribed clients - All local TUI features now accessible via remote protocol Acceptance Criteria: ✓ View agents, logs, progress (via state queries and subscriptions) ✓ Pause, resume, cancel tasks (via operation messages) ✓ Start new tasks (via continue operation) ✓ Modify queues (via add/remove iterations) ✓ Access all local TUI features (full parity via message protocol) ✓ Log streaming at full fidelity (engine events forwarded to clients)
Implement auto-reconnect with exponential backoff for remote connections: - Add 'reconnecting' connection status with visual indicator (⟳) - Implement exponential backoff (1s→30s max, 10 retries default) - Track connection metrics: latency (ping/pong RTT) and duration - Show metrics in TabBar for selected connected remotes - Add Toast component for reconnection notifications - Silent retries up to threshold (3), then alert user - Toast types: reconnecting, reconnected, reconnect_failed, error
Implement two-tier token system for remote authentication: - Server token (90 days): stored in config, used for initial auth, shown only once - Connection token (24 hours): issued on auth, auto-refreshed while connected Key changes: - types.ts: Add ServerToken, ConnectionToken, TOKEN_LIFETIMES, TokenRefresh messages - token.ts: Full refactor with two-tier tokens, in-memory connection token store - server.ts: Issue connection tokens on auth, handle refresh requests, periodic cleanup - client.ts: Store connection token, auto-refresh 1 hour before expiration - listen.ts: Enhanced token display with expiration info, security warnings - audit.ts: Add logAction helper, extend logAuth with details Includes automatic migration from legacy single-token config format.
- Add push-config protocol messages (check_config, push_config) - Implement server handlers with TOML validation and backup creation - Add client methods for config operations - Add CLI push-config subcommand with --preview, --force, --all, --scope - Add comprehensive test suite for all remoting functionality (168 tests) - Add full documentation for listen and remote commands
- Fix token format in examples to match actual base64url output
(code uses randomBytes(32).toString('base64url'), not rtui_ prefix)
- Fix audit log action names (connection_connect, not connect)
- Fix clientId format in audit log examples
- Update navigation.ts to include remote, listen, doctor, info in CLI Commands section - Create doctor.mdx documentation page - Create info.mdx documentation page
- Add subagentTree field to RemoteEngineState type - Include subagent tree data in server's get_state response - Add remoteSubagentTree state in RunApp for remote viewing - Use appropriate tree based on isViewingRemote in SubagentTreePanel
- Update navigateSubagentTree to use remoteSubagentTree when viewing remote - Use displayCurrentTaskId instead of local currentTaskId for root node - Handle remote subagent output display gracefully (show basic info from tree) - Fix isViewingCurrentTask check to use appropriate task ID for remote
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
WalkthroughThis PR adds a Remote Instance Management subsystem: WebSocket server/client, two-tier token auth, remote config and audit logging, CLI commands ( Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/CLI
participant Listen as Listen Cmd
participant Server as RemoteServer
participant Client as RemoteClient
participant Engine as Engine
participant UI as RunApp UI
User->>Listen: run --listen
Listen->>Listen: Load/create server token
Listen->>Server: createRemoteServer()
Server->>Server: Start WebSocket on port
Server-->>Listen: Server started
Listen->>UI: Display token & connection info
rect rgba(100, 200, 100, 0.5)
Note over User,UI: Connection & Auth Flow
User->>Client: add remote (host:port, token)
Client->>Server: Connect WebSocket
Server->>Server: Accept connection
Client->>Server: AuthMessage (server token)
Server->>Server: Validate server token
Server->>Client: AuthResponseMessage + connection token
Client->>Client: Store connection token
Client-->>User: Remote added & connected
end
rect rgba(100, 150, 200, 0.5)
Note over User,Engine: Remote Command Flow
User->>UI: Switch to remote tab
UI->>Client: subscribe(event types)
Client->>Server: SubscribeMessage
Server->>Engine: Attach listener
Engine-->>Server: Engine events
Server->>Client: EngineEventMessage
Client->>UI: Emit engine_event
UI->>UI: Update remote state
User->>UI: pause (on remote)
UI->>Client: pause()
Client->>Server: PauseMessage + connection token
Server->>Engine: Pause execution
Engine-->>Server: Result
Server->>Client: OperationResultMessage
Client-->>UI: Result received
end
rect rgba(200, 100, 100, 0.5)
Note over Client,Server: Token Refresh Flow
Client->>Server: Ping (check connection)
Server->>Server: Check connection token expiry
alt Token near expiry (< threshold)
Server->>Client: TokenRefreshMessage
Client->>Client: Issue new connection token
Client->>Server: TokenRefreshResponseMessage
Server->>Server: Validate & update
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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 (18.53%) 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 #157 +/- ##
==========================================
- Coverage 45.96% 40.71% -5.26%
==========================================
Files 63 72 +9
Lines 16109 19906 +3797
==========================================
+ Hits 7405 8105 +700
- Misses 8704 11801 +3097
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/tui/components/RunApp.tsx (2)
498-705: Remove debug file writes in UI hot paths.The
/tmp/ralph-keys.logwrites are in production code paths (state updates, remote fetch, key handling). This is noisy, can leak user actions, and adds synchronous I/O. Please remove these debug traces.🧹 Suggested removals
- import('fs').then(fs => { - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] useEffect: isViewingRemote=${isViewingRemote}, instanceManager=${instanceManager ? 'exists' : 'null'}, selectedTabIndex=${selectedTabIndex}, selectedTabStatus=${selectedTabStatus}\n`); - }); @@ - if (selectedTabStatus !== 'connected') { - import('fs').then(fs => { - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] useEffect: Skipping fetch, tab not connected yet (status=${selectedTabStatus})\n`); - }); - return; - } + if (selectedTabStatus !== 'connected') return; @@ - const fs = await import('fs'); - // Debug: Check connection status const selectedTab = instanceManager.getSelectedTab(); const client = instanceManager.getSelectedClient(); - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] fetchRemoteData: Tab=${selectedTab?.label}, Status=${selectedTab?.status}, Client=${client ? 'exists' : 'null'}\n`); @@ - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] getRemoteState: ${state ? 'received' : 'null'}\n`); @@ - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] getRemoteTasks: ${tasks ? `${tasks.length} tasks` : 'null'}\n`); @@ - import('fs').then(fs => { - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] 'p' pressed, isViewingRemote=true, displayStatus=${displayStatus}\n`); - }); @@ - import('fs').then(fs => { - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] Number key: ${key.name}, tabs: ${instanceTabs?.length}, onSelectTab: ${onSelectTab ? 'exists' : 'null'}\n`); - });Also applies to: 1277-1305, 1530-1549
1392-1433: Wrap the +/= case block in braces to prevent switch-scoping issues.The
constdeclarations inside the case (isPlus,isMinus,effectiveStatus) can bleed into other cases without braces. Biome'snoSwitchDeclarationsrule requires wrapping case blocks that contain lexical declarations.✅ Safe fix
case '+': case '=': case '-': - case '_': + case '_': { // Add/remove 10 iterations: +/= add, -/_ remove const isPlus = key.name === '+' || key.name === '='; const isMinus = key.name === '-' || key.name === '_'; const effectiveStatus = isViewingRemote ? displayStatus : status; if ((isPlus || isMinus) && (effectiveStatus === 'ready' || effectiveStatus === 'running' || effectiveStatus === 'executing' || effectiveStatus === 'paused' || effectiveStatus === 'stopped' || effectiveStatus === 'idle' || effectiveStatus === 'complete')) { if (isViewingRemote && instanceManager) { // Route to remote instance if (isPlus) { instanceManager.addRemoteIterations(10); } else { instanceManager.removeRemoteIterations(10); } } else { // Local engine control if (isPlus) { engine.addIterations(10).then((shouldContinue) => { if (shouldContinue || status === 'complete') { setStatus('running'); engine.continueExecution(); } }).catch((err) => { console.error('Failed to add iterations:', err); }); } else { engine.removeIterations(10) .then((success) => { if (!success) { console.log('Cannot reduce below current iteration or minimum of 1'); } }) .catch((err) => { console.error('Failed to remove iterations:', err); }); } } } break; + }
🤖 Fix all issues with AI agents
In `@src/commands/remote.ts`:
- Around line 89-155: testRemoteConnection currently leaves the WebSocket open
when the timeout fires and may call resolve multiple times; modify the function
so the WebSocket (ws) is declared in the outer scope, introduce a single
"settled" guard (boolean) to ensure resolve is only called once, and in every
resolution path (timeout handler, ws.onopen/onmessage/ws.onerror/ws.onclose and
the catch block) clear the timeout, set settled=true, remove/ignore event
handlers, and call ws.close() if ws exists; ensure error paths include the same
cleanup so the socket never stays open.
In `@src/commands/run.tsx`:
- Around line 673-705: Replace the ad-hoc useState side-effect block with a
useEffect that runs once ([]) and inside it register the instanceManager
listeners (instanceManager.onStateChange and instanceManager.onToast), call
instanceManager.initialize() and set instanceTabs/selectedTabIndex from
instanceManager.getTabs() / getSelectedIndex(); remove all debug
fs.appendFileSync imports/uses (including those in handleSelectTab) so no file
writes remain. In the useEffect cleanup return a function that calls
instanceManager.disconnectAll() to tear down connections when the component
unmounts. Keep the existing handleSelectTab logic to call
instanceManager.selectTab(index) but remove the dynamic fs import and logging.
In `@src/remote/client.ts`:
- Around line 964-981: The reconnection onmessage handler for 'auth_response'
(inside this.ws.onmessage) fails to store the returned connectionToken and
connectionTokenExpiresAt, so update the successful-auth branch in that handler
to set this.connectionToken = authResponse.connectionToken and
this.connectionTokenExpiresAt = authResponse.connectionTokenExpiresAt (mirroring
the logic in handleMessage that sets those fields), then proceed with setting
this._status, this._connectedAt, startMetricsInterval(), emit the 'reconnected'
event, reset this.reconnectAttempts, and startPingInterval so the reconnected
session retains the token for future re-authentication.
In `@src/remote/config.test.ts`:
- Around line 97-100: The test is wrong to accept decimal ports because
parseInt('1.5') truncates; update parseHostPort to reject any non-integer port
string instead of truncating: in parseHostPort, validate the port token with a
strict integer check (e.g., /^\d+$/ or Number.isInteger(Number(portStr)) &&
String(Number(portStr)) === portStr) and return an error/invalid result (or
throw) for values like '1.5'; then update the test expectation to assert that
parseHostPort('host:1.5') is treated as invalid (matches the new invalid/throw
behavior). Ensure you only change logic in parseHostPort and corresponding test
assertion.
In `@src/remote/instance-manager.ts`:
- Around line 332-334: Remove the temporary debug writes that use dynamic
import('fs').then(...) to append to '/tmp/ralph-keys.log' (the blocks that
construct a timestamped "engine_event received" message referencing
event.event.type, tab.alias, this.tabs[this.selectedIndex]?.alias, etc.); delete
those import('fs')... appendFileSync calls across the file and replace them with
calls to the existing logging facility (e.g., processLogger or the module's
logger) at an appropriate level if persistent logging is required, ensuring no
sensitive tokens are written and avoiding writes to /tmp and async dynamic fs
imports.
In `@src/tui/components/TabBar.tsx`:
- Around line 79-209: The TabBar currently ignores the onSelectTab and
onAddRemote callbacks declared in TabBarProps; update TabBar to destructure
onSelectTab and onAddRemote and pass them down to Tab and AddRemoteButton
(reference TabBar, Tab, AddRemoteButton, onSelectTab, onAddRemote). Modify Tab
to accept an onSelect prop (or onSelectTab) and call it when its box/text is
activated (e.g., click/press handler supported by the UI library) using the
tab.id or index, and modify AddRemoteButton to accept onAddRemote and invoke it
on activation; alternatively, if click handlers aren't supported, remove
onSelectTab/onAddRemote from TabBarProps and any caller usage to keep the API
consistent.
🧹 Nitpick comments (16)
README.md (1)
189-201: Consider adding a language specifier to the fenced code block.The ASCII diagram doesn't require syntax highlighting, but adding a specifier (e.g.,
textorplaintext) would satisfy markdown linters and maintain consistency.📝 Suggested fix
-``` +```text ┌─────────────────────────────────────────────────────────────────┐ │ LOCAL [1]│ ● prod [2]│ ◐ staging [3]│ ○ dev [4]│ + │ ├─────────────────────────────────────────────────────────────────┤src/remote/audit.ts (3)
30-33: Usedirname()instead ofjoin(logPath, '..').The current approach works but is less idiomatic. Using
dirname()fromnode:pathis clearer and more robust.♻️ Suggested fix
+import { join, dirname } from 'node:path'; -import { join } from 'node:path'; // In constructor: - this.logDir = join(logPath, '..'); + this.logDir = dirname(logPath);
149-170: TOCTOU race condition in rotation logic.There's a time-of-check to time-of-use gap between
stat()andrename(). If multiple processes write concurrently, both could attempt rotation simultaneously. For a single-instance audit logger this is unlikely to cause issues, but consider documenting this limitation or using a lock file for multi-process scenarios.
154-154: Unnecessary dynamic import.
renameandunlinkare already available from the static import at line 9. The dynamic import adds overhead and inconsistency.♻️ Suggested fix
-import { appendFile, mkdir, readFile, stat } from 'node:fs/promises'; +import { appendFile, mkdir, readFile, stat, rename, unlink } from 'node:fs/promises'; // In rotateIfNeeded: - const { rename, unlink } = await import('node:fs/promises'); const oldPath = this.logPath + '.old';src/remote/types.ts (1)
250-258: Move imports to top of file.The imports at line 250 break the convention of placing all imports at the top of the file. This makes dependencies harder to scan and could cause issues with some bundlers or linters.
♻️ Suggested fix
Move these imports to the top of the file after the existing imports:
import type { TrackerTask } from '../plugins/trackers/types.js'; import type { EngineEvent, EngineStatus, IterationResult, ActiveAgentState, RateLimitState, SubagentTreeNode, } from '../engine/types.js';website/content/docs/cli/listen.mdx (1)
29-29: Minor grammatical improvement.Consider adding the definite article for better readability.
-On first run, a secure authentication token is generated: +On the first run, a secure authentication token is generated:src/commands/remote.test.ts (1)
160-171: Restoreconsole.logvia try/finally to avoid cross-test leakage.If
printRemoteHelp()throws or an assertion fails,console.logremains mocked and can taint later tests.♻️ Suggested fix
- const logs: string[] = []; - const originalLog = console.log; - console.log = (...args: unknown[]) => logs.push(args.join(' ')); - - expect(() => printRemoteHelp()).not.toThrow(); - - // Restore console.log - console.log = originalLog; + const logs: string[] = []; + const originalLog = console.log; + try { + console.log = (...args: unknown[]) => logs.push(args.join(' ')); + expect(() => printRemoteHelp()).not.toThrow(); + } finally { + console.log = originalLog; + }src/tui/components/Header.tsx (1)
234-242: Guard against long remote labels to prevent header overflow.Long aliases or hostnames can push the status and task text off-screen. Consider truncating the remote name/host in the header.
♻️ Suggested tweak
- <text> - <span fg={colors.accent.primary}>{REMOTE_ICON} {remoteInfo.name}</span> - <span fg={colors.fg.dim}> ({remoteInfo.host}:{remoteInfo.port})</span> + <text> + <span fg={colors.accent.primary}> + {REMOTE_ICON} {truncateText(remoteInfo.name, 14)} + </span> + <span fg={colors.fg.dim}> + ({truncateText(`${remoteInfo.host}:${remoteInfo.port}`, 24)}) + </span> <span fg={colors.fg.dim}> │ </span> </text>src/commands/listen.test.ts (1)
108-126: Use try/finally when mockingconsole.log.If the assertion fails,
console.logstays mocked and can affect later tests.♻️ Suggested fix
- const logs: string[] = []; - const originalLog = console.log; - console.log = (...args: unknown[]) => logs.push(args.join(' ')); - - expect(() => printListenHelp()).not.toThrow(); - - console.log = originalLog; + const logs: string[] = []; + const originalLog = console.log; + try { + console.log = (...args: unknown[]) => logs.push(args.join(' ')); + expect(() => printListenHelp()).not.toThrow(); + } finally { + console.log = originalLog; + }src/tui/components/Toast.tsx (1)
41-51: Add a defensive default ingetVariantColors.If a variant is ever cast from untyped input, the function could return
undefined. A default keeps behaviour safe and predictable.♻️ Suggested fix
function getVariantColors(variant: ToastVariant): { border: string; text: string } { switch (variant) { case 'success': return { border: colors.status.success, text: colors.status.success }; case 'warning': return { border: colors.status.warning, text: colors.status.warning }; case 'error': return { border: colors.status.error, text: colors.status.error }; case 'info': return { border: colors.status.info, text: colors.status.info }; + default: + return { border: colors.status.info, text: colors.status.info }; } }src/remote/remote.test.ts (1)
347-381: Consider centralising the message type list to avoid drift.The hard-coded
messageTypesarray can fall out of sync with protocol changes. A shared constant (or deriving from a single source) would reduce maintenance overhead.src/remote/token.ts (2)
222-238: Constant-time comparison has a timing leak when string lengths differ.The implementation attempts constant-time comparison, but when
a.length !== b.length, it performs a different amount of work (iterating overb.lengthelements) before returningfalse. An attacker could potentially infer token length by measuring timing differences.A more robust approach would pad both strings to the maximum expected length or use Node's built-in
crypto.timingSafeEqual:♻️ Suggested improvement using crypto.timingSafeEqual
+import { timingSafeEqual } from 'node:crypto'; + /** * Constant-time string comparison to prevent timing attacks. */ function constantTimeCompare(a: string, b: string): boolean { - if (a.length !== b.length) { - // Still do a comparison to maintain constant time - // The _unused variable prevents timing attacks by doing work even when lengths differ - let _unused = 0; - for (let i = 0; i < b.length; i++) { - _unused |= b.charCodeAt(i) ^ (a.charCodeAt(i % a.length) || 0); - } - return false; - } - - let result = 0; - for (let i = 0; i < a.length; i++) { - result |= a.charCodeAt(i) ^ b.charCodeAt(i); - } - return result === 0; + const aBuffer = Buffer.from(a); + const bBuffer = Buffer.from(b); + + if (aBuffer.length !== bBuffer.length) { + // Compare against self to maintain constant time, then return false + timingSafeEqual(aBuffer, aBuffer); + return false; + } + + return timingSafeEqual(aBuffer, bBuffer); }
280-285: Consider periodic automatic cleanup of expired connection tokens.The
activeConnectionTokensMap stores connection tokens in memory, and whilecleanupExpiredTokens()exists, it must be called manually. For long-running server processes, this could lead to memory growth if cleanup isn't invoked regularly.Consider setting up an automatic cleanup interval when the first token is issued:
♻️ Suggested automatic cleanup
let cleanupInterval: ReturnType<typeof setInterval> | null = null; function startCleanupInterval(): void { if (!cleanupInterval) { // Clean up every hour cleanupInterval = setInterval(cleanupExpiredTokens, 60 * 60 * 1000); } } export function issueConnectionToken(clientId: string): ConnectionToken { startCleanupInterval(); // ... rest of implementation }src/remote/client.ts (2)
259-262: Consider supporting secure WebSocket connections (WSS).The client uses unencrypted
ws://for connections. For a remote monitoring and control system that transmits authentication tokens and can execute commands (pause, interrupt, config push), encrypted connections should be strongly considered, especially when connecting over untrusted networks.♻️ Suggested improvement to support WSS
+ private useSecure: boolean; + constructor( host: string, port: number, token: string, eventHandler: RemoteClientEventHandler, - reconnectConfig: Partial<ReconnectConfig> = {} + reconnectConfig: Partial<ReconnectConfig> = {}, + useSecure = false ) { this.host = host; this.port = port; this.serverToken = token; this.eventHandler = eventHandler; this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...reconnectConfig }; + this.useSecure = useSecure; } // In connect(): - const url = `ws://${this.host}:${this.port}`; + const protocol = this.useSecure ? 'wss' : 'ws'; + const url = `${protocol}://${this.host}:${this.port}`;Is there a reason WSS support was not included? Please verify whether the deployment scenarios require encrypted connections.
268-275: Consider logging JSON parse errors for debugging.The empty catch block silently ignores malformed messages. While ignoring invalid messages is reasonable, logging these at debug level would aid troubleshooting protocol issues or malicious input.
this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data as string) as WSMessage; this.handleMessage(message, resolve, reject); - } catch { - // Ignore invalid messages + } catch (error) { + // Log but don't fail on invalid messages + console.debug('Failed to parse WebSocket message:', error); } };src/remote/instance-manager.ts (1)
374-379: Fragile assumption in isViewingRemote().The method assumes the local tab is always at index 0. If tab ordering ever changes, this logic would break silently.
♻️ Suggested improvement for robustness
isViewingRemote(): boolean { - return this.selectedIndex > 0; + const selectedTab = this.tabs[this.selectedIndex]; + return selectedTab ? !selectedTab.isLocal : false; }
- Remove standalone `ralph-tui listen` command - Add `--listen`, `--listen-port`, and `--rotate-token` flags to run command - Move listener documentation from listen.mdx to run.mdx - Update CLI help, README, and all docs references - Update navigation to remove listen page BREAKING CHANGE: `ralph-tui listen` is no longer available. Use `ralph-tui run --listen` instead.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/commands/run.tsx`:
- Around line 1804-1807: The signal handlers in executeRunCommand / runHeadless
never call remoteServer.stop(), so when --listen the server isn’t gracefully
shut down; update the shutdown flow to ensure remoteServer.stop() is invoked
before process.exit(): either pass remoteServer into runHeadless (so its
gracefulShutdown can await remoteServer.stop()), or extract a shared cleanup
function (e.g., stopRemoteServer) that both the normal exit path and the signal
handlers call, and ensure gracefulShutdown/executeRunCommand awaits that
function before calling process.exit(). Reference runHeadless, gracefulShutdown,
executeRunCommand, and remoteServer when applying the change.
In `@website/content/docs/cli/remote.mdx`:
- Around line 217-218: The related-commands list references a non-existent
`listen` doc; either remove the broken list item "`[`listen`](/docs/cli/listen)
- Start a remote listener on a server`" from the Related Commands section in
remote.mdx, or add a new `listen` CLI doc that exports appropriate frontmatter
and content (matching the style of other CLI pages) so the `/docs/cli/listen`
path resolves; ensure the `run --listen` entry (`run --listen`) remains
unchanged.
♻️ Duplicate comments (1)
src/commands/run.tsx (1)
687-713: Move instance-manager initialisation touseEffectwith cleanup; remove debug file writes.This code has multiple issues:
Non-idiomatic React: Using
useState(() => {...})for side effects violates React conventions. The initialiser should be pure and only compute the initial state value.Debug logging to
/tmp/ralph-keys.log: Lines 690-692 and 709-712 write to a hardcoded file path. This is leftover debug code that should be removed before merging—it's a potential security/privacy concern and causes unnecessary I/O.Missing cleanup: When the component unmounts,
instanceManager.disconnectAll()is never called, which can leak connections.🛠️ Suggested fix
+import { useEffect } from 'react'; + // Remote instance management const [instanceManager] = useState(() => new InstanceManager()); const [instanceTabs, setInstanceTabs] = useState<InstanceTab[]>([]); const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [connectionToast, setConnectionToast] = useState<ConnectionToastMessage | null>(null); // Initialize instance manager on mount - useState(() => { - instanceManager.onStateChange((tabs, selectedIndex) => { - import('fs').then(fs => { - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] onStateChange: selectedIndex=${selectedIndex}, tabs=${tabs.length}\n`); - }); - setInstanceTabs(tabs); - setSelectedTabIndex(selectedIndex); - }); - instanceManager.onToast((toast) => { - setConnectionToast(toast as ConnectionToastMessage); - // Auto-clear toast after 3 seconds - setTimeout(() => setConnectionToast(null), 3000); - }); - instanceManager.initialize().then(() => { - setInstanceTabs(instanceManager.getTabs()); - setSelectedTabIndex(instanceManager.getSelectedIndex()); - }); - }); + useEffect(() => { + instanceManager.onStateChange((tabs, selectedIndex) => { + setInstanceTabs(tabs); + setSelectedTabIndex(selectedIndex); + }); + instanceManager.onToast((toast) => { + setConnectionToast(toast as ConnectionToastMessage); + setTimeout(() => setConnectionToast(null), 3000); + }); + void instanceManager.initialize().then(() => { + setInstanceTabs(instanceManager.getTabs()); + setSelectedTabIndex(instanceManager.getSelectedIndex()); + }); + return () => { + void instanceManager.disconnectAll(); + }; + }, [instanceManager]); // Handle tab selection const handleSelectTab = async (index: number): Promise<void> => { - const fs = await import('fs'); - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] handleSelectTab called with index: ${index}\n`); await instanceManager.selectTab(index); - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] selectTab completed\n`); };
🧹 Nitpick comments (4)
website/content/docs/cli/run.mdx (1)
250-253: Consider adding a note about the JSONL format.The audit log example shows JSONL format, but users unfamiliar with this format might benefit from a brief note that each line is a separate JSON object.
📝 Suggested clarification
### Audit Logging -All remote actions are logged to `~/.config/ralph-tui/audit.log`: +All remote actions are logged to `~/.config/ralph-tui/audit.log` in JSONL format (one JSON object per line):README.md (2)
193-201: Consider adding a language identifier to the fenced code block.The ASCII diagram code block has no language specified. While ASCII art diagrams don't have a standard language, you can use
textor leave it empty to satisfy the linter.📝 Suggested fix
-``` +```text ┌─────────────────────────────────────────────────────────────────┐ │ LOCAL [1]│ ● prod [2]│ ◐ staging [3]│ ○ dev [4]│ + │
360-365: Clarify the distinction betweenremote.jsonandremotes.toml.The configuration files table lists both
remote.json(server token storage) andremotes.toml(remote server configurations). This is potentially confusing—consider clarifying thatremote.jsonstores the local server's token when running as a listener, whilstremotes.tomlstores configurations for connecting to remote servers.📝 Suggested clarification
| File | Purpose | |------|---------| -| `~/.config/ralph-tui/remote.json` | Server token storage | -| `~/.config/ralph-tui/remotes.toml` | Remote server configurations | +| `~/.config/ralph-tui/remote.json` | Local server token (when running with `--listen`) | +| `~/.config/ralph-tui/remotes.toml` | Remote server connection configs (client-side) | | `~/.config/ralph-tui/audit.log` | Audit log of all remote actions | | `~/.config/ralph-tui/listen.pid` | Daemon PID file |src/commands/run.tsx (1)
1376-1386: Consider adding a comment explaining the intentional blocking.The never-resolving promise pattern is valid for keeping the process alive, but the code structure is slightly confusing:
await engine.dispose()on line 1388 becomes unreachable in listen mode. While the signal handlers do callengine.dispose()beforeprocess.exit(), a brief comment would clarify the intent.💡 Optional clarification
// In listen mode, keep process alive for remote connections if (listenMode) { logger.info('system', 'Engine idle. Waiting for remote commands (Ctrl+C to stop)...'); - // Keep process alive until signal received + // Block indefinitely until SIGINT/SIGTERM triggers gracefulShutdown, + // which handles engine.dispose() before calling process.exit(). await new Promise<void>((_resolve) => { - // The existing SIGINT/SIGTERM handlers will call process.exit() - // This promise just keeps the event loop alive indefinitely }); }
- Server tries up to 10 ports (configurable via maxPortRetries) - Shows "(requested X was in use)" when bound to different port - Logs requestedPort in audit when fallback occurs - Exposes actualPort getter on RemoteServer
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@examples/dates.ts`:
- Around line 38-41: The 'full' branch in the switch uses d.toLocaleString()
which defaults to the runtime locale and is inconsistent with the 'date' branch
that passes 'en-US'; update the 'full' case (the switch handling with case
'full' / default) to call toLocaleString with the same locale ('en-US' or the
shared locale variable used for 'date') so both branches use a consistent
locale.
- Around line 1-3: Replace the single-line ABOUTME comment block with a
file-level JSDoc comment: convert the leading lines that start with "// ABOUTME:
Date utility functions..." into a JSDoc block /** ... */ at the top of the file,
preserving the same text (ISO 8601 storage, manual duration math, pure
JavaScript Date API) and formatting as a proper file header so tools and linters
recognize it; ensure the JSDoc block appears before any imports or code in
examples/dates.ts and contains the original ABOUTME content.
In `@src/remote/server.ts`:
- Around line 769-795: The handler must validate the incoming iteration count
before calling the engine: in handleAddIterations check that message.count is a
number, is an integer (Number.isInteger) and is > 0, and if not call
this.sendOperationError(ws, message.id, 'add_iterations', 'Invalid iteration
count') and return early; do the same validation in the corresponding remove
handler (e.g., handleRemoveIterations / operation name 'remove_iterations') to
reject zero/negative or non-integer counts up-front instead of passing them to
this.options.engine.
- Around line 432-444: getState() currently returns this.options.port even when
the server actually bound to a different port; change getState() to prefer the
bound port (this._actualPort) when present and fall back to this.options.port so
status consumers see the real port. Locate the getState() method and set the
port property to this._actualPort if defined (or null/undefined check) otherwise
use this.options.port, ensuring the returned RemoteServerState.port reflects the
actual listening port.
♻️ Duplicate comments (2)
src/commands/run.tsx (2)
681-713: Move instance-manager side-effects intouseEffectand drop debug file writes.Side-effects during render plus
/tmp/ralph-keys.logwrites are risky and leave no cleanup. UseuseEffectwith a cleanup that callsdisconnectAll(), and remove the file writes.🔧 Proposed fix
-import { useState } from 'react'; +import { useEffect, useState } from 'react';- // Initialize instance manager on mount - useState(() => { - instanceManager.onStateChange((tabs, selectedIndex) => { - import('fs').then(fs => { - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] onStateChange: selectedIndex=${selectedIndex}, tabs=${tabs.length}\n`); - }); - setInstanceTabs(tabs); - setSelectedTabIndex(selectedIndex); - }); - instanceManager.onToast((toast) => { - setConnectionToast(toast as ConnectionToastMessage); - // Auto-clear toast after 3 seconds - setTimeout(() => setConnectionToast(null), 3000); - }); - instanceManager.initialize().then(() => { - setInstanceTabs(instanceManager.getTabs()); - setSelectedTabIndex(instanceManager.getSelectedIndex()); - }); - }); + // Initialise instance manager on mount + useEffect(() => { + instanceManager.onStateChange((tabs, selectedIndex) => { + setInstanceTabs(tabs); + setSelectedTabIndex(selectedIndex); + }); + instanceManager.onToast((toast) => { + setConnectionToast(toast as ConnectionToastMessage); + setTimeout(() => setConnectionToast(null), 3000); + }); + void instanceManager.initialize().then(() => { + setInstanceTabs(instanceManager.getTabs()); + setSelectedTabIndex(instanceManager.getSelectedIndex()); + }); + return () => { + void instanceManager.disconnectAll(); + }; + }, [instanceManager]); @@ const handleSelectTab = async (index: number): Promise<void> => { - const fs = await import('fs'); - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] handleSelectTab called with index: ${index}\n`); await instanceManager.selectTab(index); - fs.appendFileSync('/tmp/ralph-keys.log', `[${new Date().toISOString()}] selectTab completed\n`); };
1377-1385: Listen-mode shutdown bypasses remote and session cleanup.In listen mode,
runHeadlesskeeps the process alive and its signal handlers callprocess.exit(), so the cleanup afterrunHeadless(includingremoteServer.stop(),endSession, and lock release) never runs. Consider a shared shutdown path (or passremoteServerintorunHeadless) so the signal flow awaits cleanup before exiting.Also applies to: 1809-1812
- Fix auto-commit status not updating when switching to remote instances by propagating autoCommit through server options and RemoteEngineState - Add read-only config viewer (Shift+C) for both local and remote tabs with tabbed view for global/project configs and scroll navigation - Fix token display to only show full token on first run/rotation, showing preview (first 8 chars) on subsequent runs - Fix port retry logic to detect Bun's EADDRINUSE error format (checks error.code property in addition to message text) - Fix reconnection bugs: prevent double-counting retries when both onerror and onclose fire, clear invalid connection token on auth failure to allow fallback to server token
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 148-149: The README entry for the keybindings is ambiguous about
the `C` shortcut; update the table row containing "``,` / `C` | Open settings
(local) or view remote config (remote)`" to clearly state that Shift+C opens the
read‑only config viewer for both local and remote tabs (e.g., "`,` / `C` | Open
settings (local) or open read‑only config viewer (Shift+C) for local or
remote"). Ensure the change references the `,` / `C` key labels and the Shift+C
behavior so readers understand it applies to both local and remote configs.
In `@src/tui/components/RunApp.tsx`:
- Around line 509-535: The effect and its helper fetchRemoteData in RunApp.tsx
contain synchronous fs.appendFileSync calls (inside useEffect and
fetchRemoteData, referencing instanceManager, selectedTabStatus, getRemoteState)
which block the render loop and are unsafe on non-UNIX platforms; remove these
blocking writes or gate them behind an explicit debug flag (e.g.
process.env.DEBUG or a runtime isDebug boolean) and switch to non-blocking async
writes (fs.promises.appendFile) or a platform-safe logging helper that writes to
os.tmpdir() (use path/os APIs), and apply the same change to the other debug
write sites referenced in the review (the other debug blocks around the keypress
handlers and later effects).
♻️ Duplicate comments (6)
src/remote/client.ts (1)
964-977: Persist the connection token on successful reconnect.
The reconnectauth_responsesuccess path still doesn’t storeconnectionToken/expiry or re-arm refresh, so a reconnected session can miss token refresh.🔧 Proposed fix
if (authResponse.success) { this._status = 'connected'; this._connectedAt = new Date().toISOString(); + // Store connection token if provided + if (authResponse.connectionToken && authResponse.connectionTokenExpiresAt) { + this.connectionToken = authResponse.connectionToken; + this.connectionTokenExpiresAt = authResponse.connectionTokenExpiresAt; + this.scheduleTokenRefresh(); + } this.startMetricsInterval(); this.eventHandler({ type: 'reconnected', totalAttempts: this.reconnectAttempts }); this.reconnectAttempts = 0; this.startPingInterval();src/remote/server.ts (2)
448-452: Return the actual bound port ingetState().
When a fallback port is used,getState()still reportsoptions.port. Prefer_actualPortto keep status accurate.🔧 Proposed fix
- port: this.options.port, + port: this._actualPort ?? this.options.port,
782-826: Validate iteration counts from remote clients.
countcan be zero/negative or non-integer, which can lead to unexpected engine behaviour. Reject invalid values up-front.🔧 Proposed fix
private async handleAddIterations( ws: ServerWebSocket<WebSocketData>, message: AddIterationsMessage ): Promise<void> { + if (!Number.isInteger(message.count) || message.count <= 0) { + this.sendOperationError(ws, message.id, 'add_iterations', 'count must be a positive integer'); + return; + } if (!this.options.engine) { this.sendOperationError(ws, message.id, 'add_iterations', 'No engine attached'); return; } @@ private async handleRemoveIterations( ws: ServerWebSocket<WebSocketData>, message: RemoveIterationsMessage ): Promise<void> { + if (!Number.isInteger(message.count) || message.count <= 0) { + this.sendOperationError(ws, message.id, 'remove_iterations', 'count must be a positive integer'); + return; + } if (!this.options.engine) { this.sendOperationError(ws, message.id, 'remove_iterations', 'No engine attached'); return; }Also applies to: 813-839
src/commands/run.tsx (3)
688-713: Debug logging and non-idiomatic React pattern still present.The issues previously identified remain unaddressed:
- Using
useState(() => { ... })for side effects is non-idiomatic and bypasses proper React lifecycle management- Debug file writes to
/tmp/ralph-keys.log(lines 690-692, 709-712) must be removedMove the initialisation to
useEffectwith a cleanup function that callsdisconnectAll(), and remove all debugfs.appendFileSynccalls.
1376-1386: Unreachable cleanup code after never-resolving promise.When
listenModeis true, this promise never resolves and the signal handlers callprocess.exit()directly. This means:
- Line 1388 (
engine.dispose()) is unreachable in listen moderunHeadlessnever returns, soremoteServer.stop()inexecuteRunCommand(line 1816) is never calledThe signal handlers properly dispose the engine, but the remote server cleanup is bypassed. Consider passing
remoteServerintorunHeadlessor refactoring the shutdown flow to ensureremoteServer.stop()is called.
1814-1817:remoteServer.stop()is bypassed in listen mode.As noted in the review of lines 1376-1386, when
--listenis active,runHeadlessblocks indefinitely and the signal handlers callprocess.exit()directly, meaning this cleanup code is never reached.While the OS cleans up sockets on process termination, explicitly stopping the server ensures graceful shutdown logic (audit logging, client notifications) executes correctly.
🧹 Nitpick comments (1)
src/tui/components/RemoteConfigView.tsx (1)
97-115: Clamp scroll offset to the content length.
j/Gcan drivescrollOffsetpast the end of the file, leaving blank content and making “bottom” approximate. Consider clamping against a computed max offset and reusing a single page-size constant.♻️ Suggested refactor
- const contentLines = currentContent?.split('\n') ?? []; - const visibleLines = contentLines.slice(scrollOffset, scrollOffset + 20); - const canScrollDown = scrollOffset + 20 < contentLines.length; - const canScrollUp = scrollOffset > 0; + const pageSize = 20; + const contentLines = currentContent?.split('\n') ?? []; + const maxOffset = Math.max(0, contentLines.length - pageSize); + const clampedOffset = Math.min(scrollOffset, maxOffset); + const visibleLines = contentLines.slice(clampedOffset, clampedOffset + pageSize); + const canScrollDown = clampedOffset < maxOffset; + const canScrollUp = clampedOffset > 0;Also applies to: 142-146
- Fix testRemoteConnection WebSocket cleanup: add settled guard and proper cleanup in all resolution paths to prevent resource leaks - Replace ad-hoc useState with useEffect for instance manager initialization, add cleanup on unmount via instanceManager.disconnectAll() - Remove all debug fs writes to /tmp/ralph-keys.log from run.tsx, RunApp.tsx, and instance-manager.ts - Store connectionToken on successful reconnection to maintain auth for future reconnects, also clear connectionTokenExpiresAt on auth failure - Reject decimal ports in parseHostPort (e.g., '1.5') by validating with regex /^\d+$/ instead of relying on parseInt truncation - Remove unused onSelectTab/onAddRemote from TabBarProps (keyboard navigation handled by parent component, terminal UI doesn't support pointer events) - Call remoteServer.stop() in signal handlers (gracefulShutdown, handleSigterm) by passing remoteServer through HeadlessOptions - Fix broken listen doc reference in remote.mdx (removed non-existent link) - Return actual bound port in server.ts getState() instead of requested port - Add iteration count validation in handleAddIterations and handleRemoveIterations to reject non-integer or non-positive values
- Add RemoteManagementOverlay component for add/edit/delete remotes
- Add keyboard shortcuts: 'a' to add, 'e' to edit, 'x' to delete remote
- Extend InstanceManager with connect/disconnect/reconnect methods
- Show connection status and helpful messages for disconnected remotes
- Display local git info (repo:branch*) in dashboard
- Always show sandbox status with padlock icons (🔒/🔓)
- Disable epic loader ('l') for remote instances with info message
- Redesign dashboard with grid layout for better organization
- Left column: status, remote info, epic, task
- Right column: agent/model, tracker, git, sandbox/commit
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 1723-1724: The handleKeyboard useCallback is missing displayStatus
and instanceManager in its dependency array, causing stale closures for remote
controls; update the dependency array for the handleKeyboard callback (function
handleKeyboard) to include displayStatus and instanceManager so that
pause/resume/continue and other remote actions use the latest state, and re-run
relevant UI interaction flows to verify the handler reacts to remote status
changes.
♻️ Duplicate comments (1)
README.md (1)
148-149: Clarify theCshortcut behaviour.The current wording "Open settings (local) or view remote config (remote)" is ambiguous. Based on the PR description, Shift+C opens a read-only config viewer for both local and remote tabs.
✏️ Suggested doc tweak
-| `,` / `C` | Open settings (local) or view remote config (remote) | +| `,` | Open settings | +| `C` | View config (read-only, local or remote) |
🧹 Nitpick comments (7)
src/remote/instance-manager.ts (1)
374-376: Consider documenting the tab ordering invariant.The method assumes the local tab is always at index 0. While this is established in
initialize()and maintained by other methods, consider adding a brief comment to document this invariant for future maintainers.src/tui/components/LeftPanel.tsx (1)
103-106: ImportConnectionStatusfrom../types.jsinstead of redefining locally.This type is already exported from
src/tui/types.ts(line 159-160) with identical values. Importing it would reduce duplication and ensure consistency if the type definition changes.♻️ Proposed fix
-import type { LeftPanelProps, TaskItem } from '../types.js'; +import type { LeftPanelProps, TaskItem, ConnectionStatus } from '../types.js';Then remove the local type definition at lines 103-106.
src/tui/types.ts (1)
157-160: Consider consolidatingConnectionStatustype definitions.This type is defined in multiple places:
src/tui/types.ts(here)src/remote/client.ts(same values, different order)src/tui/components/LeftPanel.tsx(local copy)While the values are semantically identical, having a single source of truth would reduce maintenance burden. Consider either:
- Re-exporting from
src/remote/client.tsif this is the canonical source- Creating a shared types module for cross-layer types
src/remote/server.ts (3)
161-170: Consider usingcrypto.randomUUID()for stronger ID generation.
Math.random()is not cryptographically secure. While client IDs and message IDs aren't security-critical tokens, usingcrypto.randomUUID()(available in Bun) would be more robust and eliminate any potential for ID collisions in high-throughput scenarios.♻️ Suggested improvement
-function generateClientId(): string { - return Math.random().toString(36).substring(2, 10); -} - -function generateMessageId(): string { - return Math.random().toString(36).substring(2, 15); -} +function generateClientId(): string { + return crypto.randomUUID().slice(0, 8); +} + +function generateMessageId(): string { + return crypto.randomUUID(); +}
1186-1201: Migration error is silently swallowed.The fire-and-forget migration pattern catches errors but does nothing with them. Consider logging the failure for debugging purposes, even if the config write itself succeeded.
♻️ Suggested improvement
// Run migration in background - don't await checkAndMigrate(cwd, { quiet: true }).then((result) => { if (result?.migrated) { - // Migration was performed + // Migration was performed - could emit an event or log } }).catch(() => { - // Migration failed, but config was still written successfully + // Migration failed, but config was still written successfully + // Consider: this.auditLogger.logFailure(clientId, 'push_config_migration', 'Migration failed'); });
765-787: Async operation sends response after promise resolution but doesn't await audit logging.The
handleInterruptmethod correctly awaitsengine.stop()before sending the response. However, unlike other handlers, any audit logging for this operation appears to be missing.src/tui/components/RemoteManagementOverlay.tsx (1)
415-419: Token masking shows fixed 8 asterisks when empty.When the token field is empty and hidden,
'*'.repeat(token.length || 8)displays 8 asterisks. This is a reasonable placeholder length, though it could mislead users about expected token length. Consider whether this is intentional UX or if showing "(hidden)" might be clearer for empty state.♻️ Optional alternative
- value={showToken ? token : '*'.repeat(token.length || 8)} + value={showToken ? token : (token.length > 0 ? '*'.repeat(token.length) : '(hidden)')}
docs: update README with dashboard display info and remote TUI management
- README: Clarify `,` and `C` (Shift+C) keybindings separately - RunApp: Add displayStatus and instanceManager to handleKeyboard deps - Tests: Move remote tests to tests/ directory for CI coverage counting
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/tui/components/RunApp.tsx (1)
1425-1466: Wrap switch case declarations in a block scope.The
const isPlusandconst isMinusdeclarations inside the switch case are accessible to other cases due to JavaScript's hoisting behaviour. The static analysis tool correctly flags this.🔧 Suggested fix
case '+': case '=': case '-': - case '_': + case '_': { // Add/remove 10 iterations: +/= add, -/_ remove const isPlus = key.name === '+' || key.name === '='; const isMinus = key.name === '-' || key.name === '_'; const effectiveStatus = isViewingRemote ? displayStatus : status; if ((isPlus || isMinus) && (effectiveStatus === 'ready' || effectiveStatus === 'running' || effectiveStatus === 'executing' || effectiveStatus === 'paused' || effectiveStatus === 'stopped' || effectiveStatus === 'idle' || effectiveStatus === 'complete')) { // ... rest of the case body } break; + }
🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 374-375: Update the "Edit Remote (`e` key)" description by
inserting a comma in the sentence "You can change any field including the
alias." so it reads "You can change any field, including the alias." — edit the
README text under the Edit Remote section (the line containing "You can change
any field including the alias.") to add the comma for clarity.
In `@src/tui/components/RunApp.tsx`:
- Around line 1685-1698: The promise returned by getRemote(...) used in the 'e'
and 'x' key handlers can reject and currently has no .catch(), causing unhandled
promise rejections; update both call sites that invoke getRemote(tab.alias) (the
handlers that call setEditingRemote, setRemoteManagementMode,
setShowRemoteManagement) to append a .catch(err => { /* handle error */ }) that
logs the error (or shows a user-facing message) and prevents further state
updates on failure, ensuring failures are gracefully handled instead of bubbling
as unhandled rejections.
- Line 624: The effect currently calls fetchRemoteData() without handling
rejections and several event handlers use .then() chains without .catch, so add
proper error handling: convert the effect call to an async IIFE or await
fetchRemoteData inside a try/catch and call setError/state and
processLogger.error (or console.error) on failure, and for each event handler
that uses promise.then(...) (the handlers with the .then() chains referenced in
the review) add .catch(...) to log the error and update error UI/state or
refactor the chain to async/await with try/catch to ensure all rejections from
fetchRemoteData and subsequent async calls are handled. Ensure you modify the
fetchRemoteData invocation and the event handlers' promise chains accordingly so
they consistently surface and log errors instead of allowing unhandled
rejections.
🧹 Nitpick comments (5)
src/tui/components/RunApp.tsx (1)
2555-2575: Consider the non-atomic nature of alias change operations.When changing a remote's alias, the code first removes the old config and then adds the new one. If
addRemotefails afterremoveRemotesucceeds, the remote configuration is lost. While unlikely, this could leave the config in an inconsistent state.Consider either:
- Adding the new config first, then removing the old one (reverse order)
- Adding a rollback mechanism if the add fails
- Documenting this as a known limitation
♻️ Optional: Safer ordering
if (editingRemote && editingRemote.alias !== data.alias) { + // Add new config first to avoid data loss if it fails + const result = await addRemote(data.alias, data.host, data.port, data.token); + if (!result.success) { + throw new Error(result.error || 'Failed to add remote'); + } // Remove old config await removeRemote(editingRemote.alias); // Remove old tab instanceManager.removeTab(editingRemote.alias); - // Add new config - const result = await addRemote(data.alias, data.host, data.port, data.token); - if (!result.success) { - throw new Error(result.error || 'Failed to add remote'); - } // Connect with new alias await instanceManager.addAndConnectRemote(data.alias, data.host, data.port, data.token);tests/remote/remote.test.ts (4)
347-388: Consider improving or removing this low-value test.This test merely verifies that an array of hardcoded string literals are non-empty strings, which provides negligible coverage. The comment correctly notes that actual server testing requires
Bun.serve.Consider either:
- Removing this placeholder test entirely
- Replacing it with an integration test that spins up a real server
- At minimum, importing the message type literals from the actual source to catch typos
866-867: Consider using deterministic waiting instead ofsetTimeout.Using
setTimeout(resolve, 10)for synchronisation is fragile and can lead to flaky tests, especially under CI load. Consider:
- Using
vi.useFakeTimers()/vi.advanceTimersByTime()(or Bun equivalents)- Awaiting specific events or state changes
- Using a utility like
waitForthat polls for a conditionThis pattern appears at lines 867, 901, 929, and 1149.
843-844: Silent error swallowing may mask test failures.The
.catch(() => {})pattern silently ignores all errors, which can hide legitimate failures. Consider either:
- Explicitly expecting specific error types:
.catch(e => expect(e).toBeInstanceOf(ExpectedError))- Using a more complete mock that doesn't throw unexpectedly
- Documenting which specific errors are expected and ignored
This pattern recurs throughout the mocked WebSocket tests.
990-1004: Consider usingexpect(...).resolvesfor cleaner async assertions.The try/catch pattern is verbose. Consider:
await expect(manager.refresh()).resolves.not.toThrow();Or simply:
// If refresh() resolves without throwing, this passes await manager.refresh();
- token.test.ts: 19 tests for token generation, expiration, validation - audit.test.ts: 20 tests for AuditLogger class (99% line coverage) Improves patch coverage for PR diff
- README: Add comma in "You can change any field, including the alias." - RunApp: Add .catch() to fetchRemoteData() call in useEffect - RunApp: Add .catch() to getRemoteState/getRemoteTasks .then() chains - RunApp: Add .catch() to getRemote() calls in 'e' and 'x' key handlers
feat: Remote Instance Monitoring and Control
Summary
This PR adds comprehensive remote instance monitoring and control capabilities to ralph-tui, allowing users to connect to and monitor ralph-tui instances running on other machines.
Features
ralph-tui listen): Start a WebSocket server to accept remote connectionsralph-tui remote add/remove/list): Configure remote instancesralph-tui remote push-config)Bug Fixes
Documentation
listen,remote,doctor,infocommandsTest plan
ralph-tui listenralph-tui remote add <alias> <host>:<port>ralph-tui remote push-config <alias>Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.