feat(web): expose Playbook Exchange to mobile/web - #947
Conversation
Mobile users can now browse, preview, and import community playbooks from inside the AutoRun setup sheet — closing the AR-PARITY-01 gap between desktop and the mobile interface. - Extracted marketplace cache/fetch/import logic into src/main/services/marketplace-service.ts so the IPC handler and the web-server share one code path (no renderer round-trip). - Added four WebSocket message types: marketplace_get_manifest (with refresh:true), marketplace_get_document, marketplace_get_readme, marketplace_import_playbook. Filename traversal + missing-field validation at the boundary. - Wired callbacks in web-server-factory; the import callback resolves autoRunFolderPath and SSH config from the session itself, so mobile clients can't override them — SSH remote sessions import to the correct host the same way as desktop. - Created MarketplaceSheet (mobile bottom sheet with list → detail → import flow, category chips, search, README/document preview) and added a "Browse Playbook Exchange" entry point at the top of AutoRunSetupSheet. After import, AutoRun docs auto-refresh so freshly imported docs appear in the selector. Tests: new marketplace block in messageHandlers.test.ts covers all four message types + traversal rejection + unconfigured-callback paths. AutoRun App.test mocks the new sheet. All scoped suites pass (marketplace IPC: 45, message handlers: 113, mobile App: 95). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Playbook Exchange (Marketplace): a new marketplace service (manifest caching/merge, document/asset fetch, import with local/SSH targets, local-manifest watcher), thin IPC/WebSocket handlers and CallbackRegistry/WebServer wiring, a mobile MarketplaceSheet UI and AutoRun entry, and updated tests/mocks. ChangesMarketplace Feature Implementation
Sequence Diagram(s)sequenceDiagram
participant Client as Mobile Client
participant Server as WebServer<br/>MessageHandler
participant Registry as CallbackRegistry
participant Service as Marketplace<br/>Service
participant GitHub as GitHub API
participant Disk as Disk Cache<br/>& Local FS
rect rgba(100,150,200,0.5)
Client->>Server: marketplace_get_manifest {refresh?}
Server->>Registry: getMarketplaceManifest(opts)
Registry->>Service: getMarketplaceManifest(app)
alt Cache valid
Service->>Disk: read cache
Disk-->>Service: cached manifest
else Cache miss/expired
Service->>GitHub: fetch official manifest
GitHub-->>Service: manifest.json
Service->>Disk: write cache
end
Service->>Disk: read local manifest
Service->>Service: merge official + local
Service-->>Registry: {manifest, fromCache, cacheAge}
Registry-->>Server: {manifest, fromCache, cacheAge}
Server->>Client: marketplace_get_manifest_result
end
sequenceDiagram
participant Client as Mobile Client
participant Server as WebServer<br/>MessageHandler
participant Registry as CallbackRegistry
participant Service as Marketplace<br/>Service
participant GitHub as GitHub API
participant LocalFS as Local/Remote FS
rect rgba(100,150,200,0.5)
Client->>Server: marketplace_import_playbook {sessionId, playbookId, targetFolderName}
Server->>Registry: importMarketplacePlaybook(...)
Registry->>Service: importMarketplacePlaybook(opts)
Service->>Service: resolve manifest & playbook
Service->>LocalFS: create target directory (local or remote)
loop documents
Service->>GitHub: fetch document.md (or local read)
GitHub-->>Service: content
Service->>LocalFS: write document.md
end
alt local playbook path
Service->>LocalFS: discover local assets/
Service->>Service: merge assets list
end
loop assets
Service->>GitHub: fetch asset (or local read)
GitHub-->>Service: binary
Service->>LocalFS: write asset to assets/
end
Service->>LocalFS: append session playbook JSON (filter only successful docs)
Service-->>Registry: {playbook, importedDocs, importedAssets}
Registry-->>Server: result
Server->>Client: marketplace_import_playbook_result
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 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 |
Greptile SummaryThis PR exposes the Playbook Exchange to mobile/web clients via four new WebSocket message types, backed by a new
Confidence Score: 4/5Safe to merge after resolving the importError state conflation in MarketplaceSheet; all server-side security boundaries are correctly enforced. The server-side guards (local-path rejection, folder-name traversal, session-resolved SSH config) are well-implemented and the IPC refactor preserves existing behaviour. The one concrete defect is in MarketplaceSheet.tsx: importError doubles as a preview-fetch error channel and surfaces README/document load failures in the import footer, where users can reasonably interpret them as failed import attempts. src/web/mobile/MarketplaceSheet.tsx — importError state conflation and the setIsImporting(false) timing after successful close. Important Files Changed
Sequence DiagramsequenceDiagram
participant Mobile as MarketplaceSheet (mobile)
participant WS as WebServer WS Handler
participant Factory as WebServerFactory callback
participant Service as MarketplaceService
participant GitHub as GitHub Raw CDN
participant FS as Local Filesystem
Mobile->>WS: marketplace_get_manifest
WS->>Factory: getMarketplaceManifest()
Factory->>Service: getMarketplaceManifest(app)
Service->>FS: read cache (6h TTL)
alt cache miss / expired
Service->>GitHub: fetch manifest.json
Service->>FS: write cache
end
Service->>FS: read local-manifest.json (optional)
Service-->>Factory: merged manifest
Factory-->>WS: result
WS-->>Mobile: marketplace_get_manifest_result
Mobile->>WS: marketplace_get_document
WS->>WS: isUntrustedLocalPath check
WS->>WS: filename traversal check
WS->>Factory: getMarketplaceDocument(playbookPath, filename)
Factory->>Service: getMarketplaceDocument(...)
Service->>GitHub: fetch .md file
Service-->>WS: content
WS-->>Mobile: marketplace_get_document_result
Mobile->>WS: marketplace_import_playbook
WS->>WS: validate sessionId / playbookId / targetFolderName
WS->>Factory: importMarketplacePlaybook(sessionId, playbookId, folder)
Factory->>Factory: lookup session -> autoRunFolderPath + sshConfig
Factory->>Service: importMarketplacePlaybook(opts)
Service->>Service: assertSafeTargetFolderName
Service->>GitHub: fetch docs + assets
alt local session
Service->>FS: mkdir + writeFile
else SSH remote
Service->>FS: mkdirRemote + writeFileRemote
end
Service->>FS: update playbooks/sessionId.json
Service-->>Factory: result
WS-->>Mobile: marketplace_import_playbook_result
Reviews (2): Last reviewed commit: "fix(web): single-segment slug for mobile..." | Re-trigger Greptile |
| if (typeof targetFolderName !== 'string' || targetFolderName.trim() === '') { | ||
| this.sendError(client, 'Missing targetFolderName', { requestId: message.requestId }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Path traversal in
targetFolderName is not blocked
The handler checks that targetFolderName is non-empty but never validates it against path traversal sequences. The service then calls path.join(autoRunFolderPath, targetFolderName) (local) or string-concatenates it (SSH). A targetFolderName of "../../sensitive" resolves cleanly through path.join to a directory outside autoRunFolderPath, letting an authenticated mobile client write playbook files anywhere the desktop user has write access.
The existing isValidFilename helper (used for Auto Run document names at line 1802) already rejects .., /, and \ — the same check should be applied here before the callback is invoked.
| private handleMarketplaceGetDocument(client: WebClient, message: WebClientMessage): void { | ||
| const playbookPath = message.playbookPath; | ||
| const filename = message.filename; | ||
|
|
||
| if (typeof playbookPath !== 'string' || playbookPath.trim() === '') { | ||
| this.sendError(client, 'Missing or invalid playbookPath', { | ||
| requestId: message.requestId, | ||
| }); | ||
| return; | ||
| } | ||
| if (typeof filename !== 'string' || filename.trim() === '') { | ||
| this.sendError(client, 'Missing or invalid filename', { requestId: message.requestId }); | ||
| return; | ||
| } | ||
| if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { | ||
| this.sendError(client, 'Invalid filename', { requestId: message.requestId }); | ||
| return; | ||
| } | ||
|
|
||
| if (!this.callbacks.getMarketplaceDocument) { | ||
| this.send(client, { | ||
| type: 'marketplace_get_document_result', | ||
| success: false, | ||
| error: 'Marketplace not configured', | ||
| requestId: message.requestId, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| this.callbacks | ||
| .getMarketplaceDocument(playbookPath, filename) |
There was a problem hiding this comment.
Client-controlled
playbookPath enables local filesystem read
marketplace_get_document and marketplace_get_readme accept playbookPath from the client without restricting it to GitHub-hosted paths. The service's isLocalPath() helper returns true for any absolute path or ~/… path, so an authenticated mobile client can send { playbookPath: "/home/user/my-project", filename: "SECRETS" } to read /home/user/my-project/SECRETS.md. The validateSafePath guard inside fetchDocument only ensures the resolved file stays under the caller-supplied playbookPath — it does nothing to prevent the caller from choosing a sensitive base directory.
Mitigation: reject playbookPath values where isLocalPath() is true at the WS handler boundary (local-manifest lookups should be handled entirely server-side using the session's known paths).
| function resolveTildePath(pathStr: string): string { | ||
| if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) { | ||
| const homedir = require('os').homedir(); | ||
| return path.join(homedir, pathStr.slice(2)); | ||
| } | ||
| return pathStr; | ||
| } |
There was a problem hiding this comment.
require('os') is a CommonJS dynamic require inside a function body. Since the file already uses ES module import statements at the top, os should be imported statically to stay consistent and avoid any bundler surprises.
| function resolveTildePath(pathStr: string): string { | |
| if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) { | |
| const homedir = require('os').homedir(); | |
| return path.join(homedir, pathStr.slice(2)); | |
| } | |
| return pathStr; | |
| } | |
| import os from 'os'; | |
| function resolveTildePath(pathStr: string): string { | |
| if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) { | |
| const homedir = os.homedir(); | |
| return path.join(homedir, pathStr.slice(2)); | |
| } | |
| return pathStr; | |
| } |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/web-server/handlers/messageHandlers.ts (1)
103-103:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winMove the
AGENT_IDSimport back into the import block.This
importsits after executable declarations, so the module will not parse.Suggested fix
import { logger } from '../../utils/logger'; +import { AGENT_IDS } from '../../../shared/agentIds'; // Logger context for all message handler logs const LOG_CONTEXT = 'WebServer'; @@ -const EXTERNAL_TOAST_MAX_DURATION_SECONDS = 60; -import { AGENT_IDS } from '../../../shared/agentIds'; +const EXTERNAL_TOAST_MAX_DURATION_SECONDS = 60;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/web-server/handlers/messageHandlers.ts` at line 103, The standalone import of AGENT_IDS is located after executable code and breaks module parsing; move the "AGENT_IDS" import statement back into the top import block with the other imports in messageHandlers.ts so all imports are declared before any executable code, ensuring the symbol AGENT_IDS is imported alongside the existing imports at the file header.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/ipc/handlers/marketplace.ts`:
- Around line 132-140: When sshRemoteId is provided but
getSshRemoteById(sshRemoteId) returns undefined the code currently falls back to
a local import; instead, guard in marketplace handler by checking if sshRemoteId
is truthy and sshConfig is undefined and fail fast (throw or return a rejected
error response) so importMarketplacePlaybook is not called with the wrong
destination; update the logic around sshRemoteId / getSshRemoteById / sshConfig
to validate resolution and surface a clear error to the caller before calling
importMarketplacePlaybook.
In `@src/main/services/marketplace-service.ts`:
- Around line 464-483: The loop currently records successes in importedDocs but
newPlaybook.documents is still persisted with the full
marketplacePlaybook.documents list, causing playbooks to reference missing
files; update the import logic so you only add documents to the playbook when
their import succeeds (either build a newDocuments array and push the doc object
on successful fetch/write, or filter marketplacePlaybook.documents by
importedDocs before assigning to newPlaybook.documents), and apply the same
pattern to the analogous asset import block (the code using
writeFileRemote/fs.writeFile and variables fetchDocument, importedDocs,
marketplacePlaybook.documents, and newPlaybook.documents).
- Around line 448-452: Validate and sanitize targetFolderName before
constructing targetPath: ensure targetFolderName is not an absolute path, does
not contain path separators ('/' or '\\'), and does not include traversal tokens
like '..' (e.g., reject or normalize if path.isAbsolute(targetFolderName) ||
targetFolderName.includes(path.sep) || targetFolderName.includes('/') ||
targetFolderName.includes('..')). Alternatively enforce a whitelist regex (e.g.,
/^[A-Za-z0-9._-]+$/) or use path.basename to strip directories, then use the
sanitized value in the existing targetPath construction (the code around
targetFolderName, autoRunFolderPath and isRemote).
- Around line 275-299: In fetchReadme, don't swallow unexpected I/O/network
errors — only convert ENOENT (from fs.readFile) and 404 (from fetch response) to
null; for all other errors re-throw so they surface to Sentry. Specifically, in
the local-path branch (resolveTildePath/validateSafePath + fs.readFile) catch
and return null only when (error as NodeJS.ErrnoException).code === 'ENOENT' and
otherwise re-throw the error; in the remote branch preserve the existing
MarketplaceFetchError handling but in the outer catch re-throw any
non-MarketplaceFetchError (i.e., network/other unexpected errors) instead of
returning null. Ensure function signature and thrown types remain unchanged.
- Around line 565-574: The current try/catch silently drops existing playbooks
on any read/parse error then overwrites the file; change this so only a missing
file (ENOENT) initializes playbooks = [], while JSON.parse errors or other I/O
errors are re-thrown so they surface to Sentry. Specifically, around
fs.readFile(playbooksFilePath) and JSON.parse(content) handle errors explicitly:
if the caught error has code === 'ENOENT' set playbooks = []; otherwise re-throw
the error (and treat JSON parsing failures as unexpected by not swallowing
SyntaxError). Ensure you still push newPlaybook and write only after a
successful read-or-confirmed-missing case so you don't silently overwrite on
transient failures.
In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 3353-3361: The marketplace callback handlers (e.g.,
callbacks.getMarketplaceDocument used to send 'marketplace_get_document_result')
must check for a null result before constructing the success response; currently
a null is treated as success or causes a dereference crash (see the import
handler reading result.success). Update each handler that calls
callbacks.getMarketplaceDocument, callbacks.getMarketplaceReadme, and
callbacks.getMarketplaceImport (or similarly named methods in CallbackRegistry)
to first if (!result) send a structured failure via this.send with success:
false, an explanatory error string, and the original requestId, otherwise send
the normal success payload using result properties; also avoid unconditional
dereferences like result.success and use the guarded result fields when building
responses.
- Around line 3434-3437: The current validation for targetFolderName only checks
for empty strings but must also reject path traversal/separators; update the
check around the targetFolderName variable in messageHandlers.ts (the same block
that calls this.sendError) to fail if targetFolderName contains '/' or '\' or
the sequence '..' (or any path.sep), and return this.sendError(client, 'Invalid
targetFolderName') so importMarketplacePlaybook cannot be given a value that
escapes the Auto Run folder; keep the existing empty-string check and use the
same requestId in the error payload.
In `@src/web/mobile/AutoRunSetupSheet.tsx`:
- Around line 255-275: The button in AutoRunSetupSheet removes the default
outline (outline: 'none') which hides keyboard focus; restore a visible focus
indicator by adding a focus/focus-visible replacement to the button's styles
used in this component (the element with onClick that calls triggerHaptic and
onOpenMarketplace), e.g. provide a clear outline or boxShadow and a contrasting
border color for :focus/:focus-visible states and keep
touchAction/WebkitTapHighlightColor as-is so keyboard users can see focus
without changing touch behavior.
In `@src/web/mobile/MarketplaceSheet.tsx`:
- Around line 174-215: The async preview handlers handleSelectPlaybook and
handleSelectDocument must guard against stale responses and stop silently
swallowing errors: add a per-request token (e.g., incrementing requestId in a
React ref or use AbortController) before calling sendRequest, capture the token
in the closure, and verify the token still matches before calling
setReadmeContent/setDocumentContent or toggling setIsLoadingDocument so late
responses don't overwrite a newer selection; when filename === null clear
setIsLoadingDocument immediately; in catch blocks do not swallow errors—either
rethrow the caught error or forward it to your error-reporting helper so
exceptions propagate to Sentry while still ensuring local state is guarded by
the request token/abort check.
- Around line 436-439: Replace the truthy-fallback logic for previewContent so
empty README or document strings are preserved: in the ternary that sets
previewContent (which checks selectedDocFilename), use the nullish coalescing
operator (readmeContent ?? '*No README available*' and documentContent ??
'*Document not found*') instead of the logical ORs so only null/undefined
trigger the placeholders; update the expressions around readmeContent and
documentContent accordingly.
---
Outside diff comments:
In `@src/main/web-server/handlers/messageHandlers.ts`:
- Line 103: The standalone import of AGENT_IDS is located after executable code
and breaks module parsing; move the "AGENT_IDS" import statement back into the
top import block with the other imports in messageHandlers.ts so all imports are
declared before any executable code, ensuring the symbol AGENT_IDS is imported
alongside the existing imports at the file header.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3fc7b37c-dcf5-4b14-a768-7c0e05a9015d
📒 Files selected for processing (12)
src/__tests__/main/web-server/handlers/messageHandlers.test.tssrc/__tests__/web/mobile/App.test.tsxsrc/main/ipc/handlers/marketplace.tssrc/main/services/marketplace-service.tssrc/main/web-server/WebServer.tssrc/main/web-server/handlers/messageHandlers.tssrc/main/web-server/managers/CallbackRegistry.tssrc/main/web-server/types.tssrc/main/web-server/web-server-factory.tssrc/web/mobile/App.tsxsrc/web/mobile/AutoRunSetupSheet.tsxsrc/web/mobile/MarketplaceSheet.tsx
Resolves merge conflict in src/main/ipc/handlers/marketplace.ts by keeping the new thin-wrapper refactor; ports the upstream watcher error-handler addition (Windows UNKNOWN / EPERM / ENOENT classification) into createLocalManifestWatcher in the new src/main/services/marketplace-service.ts. Also addresses PR RunMaestro#947 review feedback from greptile + coderabbit: Security - WS handlers reject `playbookPath` values that point at the local filesystem (absolute or `~`-prefixed). Local paths remain reachable via desktop IPC where the renderer is trusted, but mobile/web clients can no longer coerce the server into reading arbitrary files via the marketplace fetch helpers. - WS `marketplace_import_playbook` rejects `targetFolderName` values containing path separators or traversal tokens. Service layer adds `assertSafeTargetFolderName` as the source-of-truth guard. - IPC `marketplace:importPlaybook` fails loudly when `sshRemoteId` is provided but cannot be resolved, instead of silently downgrading to a local import (matches CLAUDE.md SSH-spawn pattern: never silently downgrade). Correctness - Persisted `newPlaybook.documents` only references docs that actually wrote to disk, so a partial import never produces a playbook pointing at missing files. - Reading the per-session playbooks file no longer silently resets to `[]` on non-ENOENT errors. Corrupt JSON / EACCES now throws rather than overwriting existing user data. - `fetchReadme` re-throws unexpected I/O / network failures (only ENOENT and 404 still map to "no README"), so production faults surface in Sentry. - WS document/readme handlers return a structured failure when the underlying callback resolves to `null`, instead of treating unconfigured wiring as a successful empty response. Code quality - Replaced dynamic `require('os')` in marketplace-service with a static `import os from 'os'` (consistent with the rest of the module). UI polish - AutoRunSetupSheet entry button gets a focus ring on `:focus` so keyboard users still see focus state after `outline: none`. - MarketplaceSheet swaps `||` to `??` for README/document fallback text so empty files render instead of showing the placeholder. - MarketplaceSheet adds a monotonic preview-request id; stale README/document responses are discarded if the user has already picked another playbook or document. Error responses now surface in the import-error banner instead of being swallowed. Tests - Updated marketplace IPC tests for the new SSH fail-loud behavior (replaces "fall back to local fs" cases with "fail loudly"). - Added missing `mockRejectedValueOnce({ code: 'ENOENT' })` for the per-session playbooks file read in 7 tests that previously relied on the bare-catch swallowing JSON.parse errors. - Fixed `os` mock to expose `default` for the new static import. - Asserts that partially-imported playbooks only persist the successful docs. All scoped suites pass (marketplace IPC: 45, message handlers: 113, mobile App: 95). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Merged Conflict resolutionThe only conflict was Review feedback addressedSecurity (P1)
Correctness (Major)
Code quality (P2)
UI polish (Minor)
Test updates
Verification
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 3344-3363: Validation branches currently call this.sendError(...)
which emits a generic error frame; change these to send the request-scoped typed
failure frames (e.g., marketplace_get_document_result,
marketplace_get_readme_result, marketplace_import_playbook_result) so callers
receive the expected typed response. For each branch referencing playbookPath,
filename, this.isUntrustedLocalPath, etc., construct and send the appropriate
marketplace_*_result failure object (including requestId: message.requestId and
an error message) to the same client instead of calling this.sendError, and
return immediately after sending; keep usage of sendError only for
non-request-scoped/global errors. Ensure you update all similar validation
blocks noted (around the shown block and at the other ranges: 3409-3420,
3472-3499) and reuse the same result shape the success path uses.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 474810d1-e411-4678-a94f-3857fc41b994
📒 Files selected for processing (8)
src/__tests__/main/ipc/handlers/marketplace.test.tssrc/main/ipc/handlers/marketplace.tssrc/main/services/marketplace-service.tssrc/main/web-server/handlers/messageHandlers.tssrc/main/web-server/types.tssrc/main/web-server/web-server-factory.tssrc/web/mobile/AutoRunSetupSheet.tsxsrc/web/mobile/MarketplaceSheet.tsx
✅ Files skipped from review due to trivial changes (1)
- src/web/mobile/AutoRunSetupSheet.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- src/main/web-server/web-server-factory.ts
- src/web/mobile/MarketplaceSheet.tsx
- src/main/web-server/types.ts
- src/main/services/marketplace-service.ts
Resolves 6 conflicts that came in from upstream/rc's PR2 of the CLI surface refactor (list_desktop_sessions / get_session_history) plus the AutoRunSetupSheet playbook-CRUD redesign and FolderPickerSheet: - WebServer.ts, types.ts, CallbackRegistry.ts: combined the marketplace callback registrations with upstream's listDesktopSessions and getSessionHistory callbacks (both sets coexist; ordering preserves marketplace-first since that was already on the branch). - handlers/messageHandlers.ts: combined the four marketplace WS handlers (get_manifest, get_document, get_readme, import_playbook) with the new list_desktop_sessions and get_session_history handlers. - web/mobile/App.tsx: kept both the MarketplaceSheet wiring and the new FolderPickerSheet + handleAutoRunFolderConfirm flow; passed sendRequest, send, currentDocument and onOpenMarketplace through to the redesigned AutoRunSetupSheet. - web/mobile/AutoRunSetupSheet.tsx: kept the upstream playbook-CRUD refactor (sendRequest/send/currentDocument props, playbooks panel, inline name-prompt / delete-confirm modals) and re-anchored the Playbook Exchange entry button above the new Playbooks section so marketplace import remains discoverable. - __tests__/main/web-server/handlers/messageHandlers.test.ts: kept the marketplace mocks/tests alongside upstream's listDesktopSessions / getSessionHistory mocks and tests. Also addresses CI failures and a new coderabbit comment from the last push: CI fix - web-server-factory.test.ts MockWebServer was missing the four setMarketplaceManifestCallback / setMarketplaceDocumentCallback / setMarketplaceReadmeCallback / setImportMarketplacePlaybookCallback stubs, causing 36 test failures with `TypeError: ... is not a function`. Added the stubs (combined with upstream's new setListDesktopSessionsCallback / setGetSessionHistoryCallback). Coderabbit P3 (Major) - Marketplace WS validation branches were emitting generic `type: 'error'` frames rather than the request-scoped `marketplace_*_result` types. Clients waiting on the typed result would miss the failure or hang. Introduced `sendMarketplaceFailure(client, type, error, message, extra?)` helper and routed all validation-failure paths through it (missing fields, untrusted local paths, traversal in filename, separators in targetFolderName, callback unconfigured). Updated the relevant tests (assert typed-result failures and added two new cases for absolute playbookPath rejection and targetFolderName separator rejection). Verification - `npm run lint` clean (TypeScript, all configs). - `npm run lint:eslint` clean. - `prettier --check` clean across all touched files. - Scoped tests pass: marketplace IPC (45), message handlers (123 — 7 new), web-server-factory (95), mobile App (95). 319 total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Merged `upstream/rc` again, fixed CI failures, and addressed the new coderabbit comment. Pushed `1fb98426`. Conflicts resolved (6 files)Upstream's PR2 of the CLI surface refactor (`list_desktop_sessions` / `get_session_history`), AutoRunSetupSheet playbook-CRUD redesign, and FolderPickerSheet all landed on `rc` after the previous merge. Combined both sides:
CI fix
New coderabbit comment (Major) — typed `marketplace_*_result` failuresMarketplace WS validation branches were emitting generic `type: 'error'` frames rather than the request-scoped `marketplace_*_result` types, which would make typed clients hang waiting for the success type. Introduced a small `sendMarketplaceFailure(client, type, error, message, extra?)` helper and routed every validation-failure path through it (missing fields, untrusted local paths, traversal in filename, separators in `targetFolderName`, callback unconfigured). Tests updated to assert typed-result failures, plus two new cases:
Verification
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/main/web-server/handlers/messageHandlers.ts (1)
4065-4072: 💤 Low valueMisleading error message when callback returns null.
At this point the callback is confirmed to exist (checked at line 4052), so a null result means the document wasn't found or fetching failed—not that marketplace is unconfigured.
Suggested clarification
.then((result) => { if (!result) { this.sendMarketplaceFailure( client, 'marketplace_get_document_result', - 'Marketplace not configured', + 'Document not found', message ); return; }Same applies to
handleMarketplaceGetReadmeat line 4131.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/web-server/handlers/messageHandlers.ts` around lines 4065 - 4072, The current error message 'Marketplace not configured' is misleading when the callback exists but returns null; update the sendMarketplaceFailure call in the block that checks if (!result) inside the handler that invokes the marketplace callback (refer to the sendMarketplaceFailure call with event 'marketplace_get_document_result') to use a clear message like 'Document not found or failed to fetch' (or similar), and make the analogous change in handleMarketplaceGetReadme where the same null-result handling occurs so both handlers report that the document/readme was not found or could not be fetched rather than saying the marketplace is unconfigured.src/web/mobile/App.tsx (1)
1535-1537: 💤 Low valueConsider resetting
showMarketplaceSheetwhen the setup sheet closes.
handleAutoRunCloseSetupresetsshowAutoRunSetupbut notshowMarketplaceSheet. In practice the marketplace's full-screen backdrop prevents clicking the setup sheet's close button, but keyboard-driven dismissal (e.g., an Escape handler inAutoRunSetupSheeton desktop browsers) could close the setup sheet while leaving the marketplace sheet floating. Adding the reset is a one-liner:🛡️ Proposed fix
const handleAutoRunCloseSetup = useCallback(() => { setShowAutoRunSetup(false); + setShowMarketplaceSheet(false); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/web/mobile/App.tsx` around lines 1535 - 1537, The close handler handleAutoRunCloseSetup currently only calls setShowAutoRunSetup(false); update it to also reset the marketplace state by calling setShowMarketplaceSheet(false) so closing the AutoRunSetupSheet (including via keyboard) won't leave the marketplace sheet/backdrop open — locate handleAutoRunCloseSetup in App.tsx and add the call to setShowMarketplaceSheet(false) alongside the existing setShowAutoRunSetup(false).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 4065-4072: The current error message 'Marketplace not configured'
is misleading when the callback exists but returns null; update the
sendMarketplaceFailure call in the block that checks if (!result) inside the
handler that invokes the marketplace callback (refer to the
sendMarketplaceFailure call with event 'marketplace_get_document_result') to use
a clear message like 'Document not found or failed to fetch' (or similar), and
make the analogous change in handleMarketplaceGetReadme where the same
null-result handling occurs so both handlers report that the document/readme was
not found or could not be fetched rather than saying the marketplace is
unconfigured.
In `@src/web/mobile/App.tsx`:
- Around line 1535-1537: The close handler handleAutoRunCloseSetup currently
only calls setShowAutoRunSetup(false); update it to also reset the marketplace
state by calling setShowMarketplaceSheet(false) so closing the AutoRunSetupSheet
(including via keyboard) won't leave the marketplace sheet/backdrop open —
locate handleAutoRunCloseSetup in App.tsx and add the call to
setShowMarketplaceSheet(false) alongside the existing
setShowAutoRunSetup(false).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: abf16d2b-a265-494b-ab1a-a5cff40f898e
📒 Files selected for processing (9)
src/__tests__/main/web-server/handlers/messageHandlers.test.tssrc/__tests__/main/web-server/web-server-factory.test.tssrc/main/web-server/WebServer.tssrc/main/web-server/handlers/messageHandlers.tssrc/main/web-server/managers/CallbackRegistry.tssrc/main/web-server/types.tssrc/main/web-server/web-server-factory.tssrc/web/mobile/App.tsxsrc/web/mobile/AutoRunSetupSheet.tsx
✅ Files skipped from review due to trivial changes (3)
- src/tests/main/web-server/web-server-factory.test.ts
- src/main/web-server/web-server-factory.ts
- src/main/web-server/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/web/mobile/AutoRunSetupSheet.tsx
- src/main/web-server/managers/CallbackRegistry.ts
- src/main/web-server/WebServer.ts
- src/tests/main/web-server/handlers/messageHandlers.test.ts
Two UX findings from soaking #947 into 0.16.0-RC-buildFinding 1 — Import folder prefill contradicts the server validator (functional bug)The mobile import flow prefills the Import to folder input with the marketplace manifest's full identifier (e.g.
So the import does not work out of the box — every user clicking Import on a category-namespaced playbook hits this and has to manually edit the input down to a single segment. Manually editing it down to e.g. Positive side-effect: the path-traversal validator that catches Suggested fix (smallest surface): client-side sanitize the prefill to a single segment before populating the input. Two reasonable options: // Last-segment-only (simplest)
const defaultFolder = manifestId.split('/').pop() ?? manifestId;
// Or replace separators
const defaultFolder = manifestId.replace(/[/\\]/g, '-');Pair with a small "will install at: The alternative — relaxing the server validator to accept nested paths bounded to Finding 2 — Bottom-nav
|
|
Upgrading Finding 1 (import folder prefill contradicts the validator) to blocking — confirmed with chr1syy. "Import doesn't work without manually editing the prefill" is broken-out-of-the-box behavior, not polish. Finding 2 (Run/Playbooks bottom-nav redundancy) remains a non-blocking UX redesign suggestion. |
The mobile Playbook Exchange prefilled the import folder with a `category/title` slug, which the server's `assertSafeTargetFolderName` guard rejects (no separators allowed). Every user tapping Import on a category-namespaced playbook hit this and had to manually edit the input down to a single segment. Switch the prefill to a title-only slug. Category is already shown via the tile chip and detail-view eyebrow, so re-encoding it into the folder name added no information. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@greptileai re-review please |
|
Tip For best results, initiate chat on the files or code changes.
[full_review] |
| // Import state | ||
| const [isImporting, setIsImporting] = useState(false); | ||
| const [importError, setImportError] = useState<string | null>(null); |
There was a problem hiding this comment.
importError is used as a dual-purpose state: it is set both when a README/document preview fetch fails (inside handleSelectPlaybook and handleSelectDocument) and when the actual import fails. Because this error is displayed in the import footer next to the "Import Playbook" button, a README fetch failure immediately surfaces there (e.g. "Failed to load README") with no visual cue that the error relates to the preview panel rather than an import operation. A user who taps a playbook, sees the preview fail, and then reads the error in the import footer will reasonably believe an import was attempted and failed. Consider separating the two concerns with a distinct previewError state displayed in the preview area.
| // Import state | |
| const [isImporting, setIsImporting] = useState(false); | |
| const [importError, setImportError] = useState<string | null>(null); | |
| // Import state | |
| const [isImporting, setIsImporting] = useState(false); | |
| const [importError, setImportError] = useState<string | null>(null); | |
| // Preview-fetch errors (README / document load) — shown in the preview area, | |
| // not in the import footer, so users aren't misled into thinking an import failed. | |
| const [previewError, setPreviewError] = useState<string | null>(null); |
The marketplace sheet's `importError` was overloaded — `handleSelectPlaybook` and `handleSelectDocument` set it on README/document fetch failures, but it rendered in the import footer next to the Import Playbook button. A user who tapped a playbook, saw the preview fail, and read the error in the footer would reasonably believe an import had been attempted and failed. Split into a dedicated `previewError` state that renders inline in the preview area, leaving `importError` exclusively for actual import failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed greptile's P1 inline finding on Fix — split preview vs import error state
Introduced a dedicated
Verification
Other open items from the latest review pass:
|
Folder prefill fix verified — unblockingRe-tested 2026-05-07 against the soak build ( What's fixed
Removing the Non-blocking follow-up (mobile/web parity gap)Desktop has an "Add Docs" / Docs Overview affordance for discovering and adding AutoRun documents. The mobile/web equivalent is missing — users on mobile have to navigate directly to the Playbook Exchange or to already-imported docs, with no equivalent overview surface. The Playbook Exchange (this PR) is one entry point; the Docs Overview is the broader contextualizing surface around it. Added as Gap 3 to the running mobile/web parity follow-ups gist (alongside PR #946's launch-affordance gaps): https://gist.github.com/chr1syy/67630166ff5217a97d7b1fad91201f3b Suggested direction in the gist: mirror the desktop component on the webui, reuse the WS-side IPC via the same shared-service pattern this PR established ( Other items still untested in this pass (deferred from yesterday's plan)
|
Conflict resolution - src/main/ipc/handlers/marketplace.ts: kept this PR's thin-wrapper refactor (logic in src/main/services/marketplace-service.ts) over upstream's inline import body. Ported upstream's marketplace compatibility check (isCompatible against minMaestroVersion) into the service layer instead, so both the IPC and WebSocket import paths get the defense-in-depth gate without duplicating the inline body. Verification - npm run lint clean (TypeScript, all configs). - prettier and eslint clean on touched files. - Scoped suites pass: marketplace IPC, message handlers, web-server-factory, mobile App, marketplace-compatibility (344 total). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Merged `upstream/rc` again. Pushed `7bea26d3`. Conflict resolutionSingle conflicted file: `src/main/ipc/handlers/marketplace.ts`. Upstream introduced marketplace compatibility gating (`isCompatible` against `minMaestroVersion`) inside the inline IPC import body, while this PR had refactored that body into a thin wrapper around `src/main/services/marketplace-service.ts`. Resolved by:
The IPC handler imports pulled in by upstream (`MarketplaceFetchError`, `MarketplaceManifest`, etc.) were dropped; the service file already owns those. Verification
PR is back to `MERGEABLE` / `CLEAN`. |
Resolves seven CodeRabbit findings on PR RunMaestro#947's Playbook Exchange code. All edits keep the existing typed-result protocol and recoverable-UX contracts; the changes are about making real production failures visible and refusing to persist a half-imported playbook. Service layer (`src/main/services/marketplace-service.ts`): - Import path now falls back to a stale official cache when `fetchManifest()` fails, mirroring the browse path. Without this, a visible playbook served from the same stale cache could disappear at import time and the user would see "Playbook not found". - Refuse to persist a playbook when every document failed to write. The per-doc loop is intentionally tolerant so one bad file doesn't block the rest, but the previous code still returned success with `documents: []` — closing the marketplace sheet and leaving the user with an unusable imported entry. Now throws a `MarketplaceImportError` before the playbook file is touched, and only when the playbook actually had documents (asset-only playbooks still import). WebSocket handlers (`src/main/web-server/handlers/messageHandlers.ts`): - `isUntrustedLocalPath` now rejects `.` / `..` segments and any backslash anywhere in the path, not just absolute / `~` / Windows drive prefixes. Defense-in-depth at the entry point so future downstream changes can't re-open a relative-traversal bypass. - All four marketplace catch blocks (`get_manifest`, `get_document`, `get_readme`, `import_playbook`) now route through a new `reportMarketplaceHandlerError` helper that calls `captureException` before sending the typed failure. Mirrors the existing `reportHandlerError` for non-marketplace handlers; preserves the client-visible typed result so mobile clients waiting on the `marketplace_*_result` type still get their response. Web server factory (`src/main/web-server/web-server-factory.ts`): - Marketplace import catch now reports the original exception to Sentry with `{ operation, sessionId, playbookId, targetFolderName }` context before returning `{ success: false }`. Keeps the existing recoverable-UX path while making the real cause visible in production error tracking. Mobile MarketplaceSheet (`src/web/mobile/MarketplaceSheet.tsx`): - Manifest / README / document / import catch blocks now `console.error` the underlying exception so the original cause is visible in browser devtools. Web bundle has no Sentry, so this matches the existing pattern in `AutoRunDocumentViewer` rather than introducing a new Sentry surface. - Returning to the README view from a document preview now clears `previewError` and re-fetches the README via a new `loadReadmeFor` helper. Previously the sheet could stay stuck on a stale "No README available" / fetch error after the user tapped a doc and went back. Tests: - `marketplace.test.ts`: new "should fail the import when all documents fail to fetch" verifies the empty-import guard fires and no playbook is persisted. New "should fall back to expired cache when network fetch fails during import" exercises the import-path stale-cache fallback. - `messageHandlers.test.ts`: new parameterized suite validates that `isUntrustedLocalPath` rejects `..`, `.`, embedded dot segments, and embedded backslashes via the typed `marketplace_get_document_result`. All scoped marketplace + web-server-handler tests pass (198/198). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Gap 3 from the PR RunMaestro#946/RunMaestro#947 mobile/web AutoRun parity follow-up. Desktop has an "Add Docs" affordance that surfaces both Create-doc and Marketplace as co-equal entry points. On mobile/web, the empty state of AutoRunInline only offered "+ Create document" — users with a fresh AutoRun folder had no in-UI path to discover existing playbooks and had to know to open the launch sheet just to find the marketplace entry. - Add onOpenMarketplace?: () => void prop to AutoRunInline. Render a "Browse Playbook Exchange" CTA next to "+ Create document" in the empty state when the prop is provided. Both buttons share the same vertical stack so the discoverability matches desktop's overview. - Plumb the prop through AutoRunPanel (full-screen overlay), AutoRunTabContent / RightDrawer (inline tab), and RightPanel. - Wire handleOpenMarketplaceSheet through from App.tsx for both AutoRunPanel and RightPanel render sites; reuses the same MarketplaceSheet and WS infrastructure introduced by PR RunMaestro#947, so no new IPC/server surface. The CTA is opt-in (prop is optional) so existing callers and tests keep working without change. Existing scoped tests pass; new AutoRunInline.test.tsx covers the empty-state CTA visibility, hidden-when-unset, and onClick behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed all seven CodeRabbit findings in Service layer (
WebSocket handlers (
Web server factory (
Mobile sheet (
Tests (
All 205 tests in the scoped suites pass; lint + prettier clean. |
…e/web # Conflicts: # src/main/web-server/web-server-factory.ts
|
Reviewed and ready to land in spirit, but two things need to happen before merge: 1. Rebase on current 2. Fix the SSH silent-downgrade in The new WS callback path ( Also: Fix: distinguish "no SSH configured" from "SSH configured but unresolvable" in the WS callback. Return Once those two are in I'll merge it. |
Closes Gap 3 from the PR RunMaestro#946/RunMaestro#947 mobile/web AutoRun parity follow-up. Desktop has an "Add Docs" affordance that surfaces both Create-doc and Marketplace as co-equal entry points. On mobile/web, the empty state of AutoRunInline only offered "+ Create document" — users with a fresh AutoRun folder had no in-UI path to discover existing playbooks and had to know to open the launch sheet just to find the marketplace entry. - Add onOpenMarketplace?: () => void prop to AutoRunInline. Render a "Browse Playbook Exchange" CTA next to "+ Create document" in the empty state when the prop is provided. Both buttons share the same vertical stack so the discoverability matches desktop's overview. - Plumb the prop through AutoRunPanel (full-screen overlay), AutoRunTabContent / RightDrawer (inline tab), and RightPanel. - Wire handleOpenMarketplaceSheet through from App.tsx for both AutoRunPanel and RightPanel render sites; reuses the same MarketplaceSheet and WS infrastructure introduced by PR RunMaestro#947, so no new IPC/server surface. The CTA is opt-in (prop is optional) so existing callers and tests keep working without change. Existing scoped tests pass; new AutoRunInline.test.tsx covers the empty-state CTA visibility, hidden-when-unset, and onClick behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The WS marketplace_import_playbook callback's resolveSessionSshConfig
returned undefined for three distinct conditions:
1. No SSH configured on the session.
2. SSH configured (sessionSshRemoteConfig.enabled / legacy sshRemoteId)
but remoteId is missing or null.
3. SSH configured but the matching sshRemotes entry is missing or
disabled.
Cases 2 and 3 silently downgraded to a local import — landing the
playbook on the desktop's local filesystem when the user explicitly
opted into SSH. The desktop IPC handler already rejects this with
'SSH remote not found or disabled' (matching CLAUDE.md's loud-fail
SSH-spawn pattern); the WS path now mirrors that behavior.
Also: gate on sessionSshRemoteConfig.enabled === true before pulling
remoteId. AgentSshRemoteConfig treats `enabled` as the source of
truth — a session with `enabled: false` and a populated remoteId must
not be treated as remote.
Resolver now throws on unresolvable; the import callback catches the
throw before the captureException try-block so user-misconfiguration
errors return a typed { success: false, error } without polluting
Sentry.
Tests: four new web-server-factory specs covering all three
unresolvable conditions plus the enabled:false-with-remoteId case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@pedramamini both asks addressed in `92576d97`. 1. Rebase / merge stateBranch is now in sync with `upstream/rc` (round 4 merge in `8eb5b558`). `gh pr view` reports `mergeable: MERGEABLE`, `mergeStateStatus: CLEAN`. No outstanding conflicts. 2. SSH silent-downgrade in `web-server-factory.ts`Reworked `resolveSessionSshConfig` to mirror the desktop IPC handler's loud-fail behavior:
TestsAdded `importMarketplacePlaybookCallback behavior` block (4 specs) in `src/tests/main/web-server/web-server-factory.test.ts`:
Mocked `marketplace-service.importMarketplacePlaybook` and added `captureException` / `electron.app`+`BrowserWindow` to the existing electron mock so the import callback is testable end-to-end. Verification
|
Closes Gap 3 from the PR #946/#947 mobile/web AutoRun parity follow-up. Desktop has an "Add Docs" affordance that surfaces both Create-doc and Marketplace as co-equal entry points. On mobile/web, the empty state of AutoRunInline only offered "+ Create document" — users with a fresh AutoRun folder had no in-UI path to discover existing playbooks and had to know to open the launch sheet just to find the marketplace entry. - Add onOpenMarketplace?: () => void prop to AutoRunInline. Render a "Browse Playbook Exchange" CTA next to "+ Create document" in the empty state when the prop is provided. Both buttons share the same vertical stack so the discoverability matches desktop's overview. - Plumb the prop through AutoRunPanel (full-screen overlay), AutoRunTabContent / RightDrawer (inline tab), and RightPanel. - Wire handleOpenMarketplaceSheet through from App.tsx for both AutoRunPanel and RightPanel render sites; reuses the same MarketplaceSheet and WS infrastructure introduced by PR #947, so no new IPC/server surface. The CTA is opt-in (prop is optional) so existing callers and tests keep working without change. Existing scoped tests pass; new AutoRunInline.test.tsx covers the empty-state CTA visibility, hidden-when-unset, and onClick behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
MarketplaceModal).autoRunFolderPathand SSH config from the session itself, so mobile clients can't override them.What changed
Server
src/main/services/marketplace-service.ts(new) — pure helpers for cache/manifest/document/import.src/main/ipc/handlers/marketplace.ts— slimmed to thin wrappers around the service. Existing IPC behavior preserved (45 IPC tests still pass).src/main/web-server/handlers/messageHandlers.ts— four new WebSocket message types:marketplace_get_manifest(withrefresh: true),marketplace_get_document,marketplace_get_readme,marketplace_import_playbook. Path-traversal + required-field validation at the boundary.src/main/web-server/web-server-factory.ts— wires callbacks;importMarketplacePlaybookresolvesautoRunFolderPathand SSH config from the session (mobile clients only sendsessionId,playbookId,targetFolderName).WebServer.ts,CallbackRegistry.ts,types.ts— extended for the four new callbacks.Mobile UI
src/web/mobile/MarketplaceSheet.tsx(new) — 90vh bottom sheet, list → detail → import flow with category chips, search, README + document preview viaMobileMarkdownRenderer, and target-folder input.src/web/mobile/AutoRunSetupSheet.tsx— optionalonOpenMarketplaceprop and a "Browse Playbook Exchange" entry point at the top of the scrollable area.src/web/mobile/App.tsx— addsshowMarketplaceSheetstate, marketplace handlers, wires the entry point, and auto-refreshes AutoRun docs after a successful import.Test plan
npm run lint(TypeScript, all configs)npm run lint:eslintnpx prettier --checkfor all changed filesvitest run src/__tests__/main/ipc/handlers/marketplace.test.ts(45 tests pass — IPC handler refactor preserves behavior)vitest run src/__tests__/main/web-server/handlers/messageHandlers.test.ts(113 tests pass — includes 7 new tests for marketplace WS handlers + traversal rejection + unconfigured-callback paths)vitest run src/__tests__/web/mobile/App.test.tsx(95 tests pass — adds mock for newMarketplaceSheet)Notes
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes / Safety
Tests