Skip to content

feat(cli): add gist create to publish session transcripts to GitHub#898

Merged
pedramamini merged 4 commits intoRunMaestro:rcfrom
chr1syy:feat/cli-gist
Apr 27, 2026
Merged

feat(cli): add gist create to publish session transcripts to GitHub#898
pedramamini merged 4 commits intoRunMaestro:rcfrom
chr1syy:feat/cli-gist

Conversation

@chr1syy
Copy link
Copy Markdown
Contributor

@chr1syy chr1syy commented Apr 24, 2026

Summary

  • Adds maestro-cli gist create <agent-id> [-d <desc>] [-p], which asks the running Maestro desktop app to publish an agent's full session transcript (every AI tab) as a single GitHub gist.
  • Reuses the existing git:createGist IPC handler, so the same gh CLI install + auth requirements as the desktop "Publish Gist" flow apply.
  • Defaults to a private gist; --public opts into a public gist. --description is optional.

How it works

Plumbing mirrors the summarize/transfer/merge-context remote pattern already in the codebase:

  1. CLI opens the existing WebSocket connection via withMaestroClient and sends a new create_gist message.
  2. New handleCreateGist in messageHandlers.ts invokes a createGist callback.
  3. web-server-factory.ts forwards to the renderer over remote:createGist (request-response with a timeout).
  4. The renderer looks up the session, formats each AI tab's logs via formatLogsForClipboard(), concatenates them under ## Tab: headers, and calls window.maestro.git.createGist(...).
  5. The main process IPC shells out to gh gist create and the result propagates back through the WebSocket as create_gist_result.

Errors (agent not found, empty conversation, gh not installed/authenticated, desktop app not running, timeout) are mapped to structured JSON with codes AGENT_NOT_FOUND, MAESTRO_NOT_RUNNING, GIST_CREATE_FAILED.

Stacking note

This PR is stacked on top of #888 (feat/cli-ssh-remote-agents). Until that merges, the GitHub diff here will show those commits too; they drop off automatically once #888 lands.

Test plan

  • tsc -p tsconfig.lint.json, tsconfig.main.json, tsconfig.cli.json — clean on touched files
  • eslint on touched files — clean
  • prettier --check — formatted
  • vitest run src/__tests__/renderer/hooks/useRemoteIntegration.test.ts src/__tests__/main/web-server/handlers/messageHandlers.test.ts — 125 tests pass (tests updated to mock the new callback/preload hooks)
  • Manual: run Maestro desktop with an active agent that has some AI-tab activity, then maestro-cli gist create <agent-id> — expect a https://gist.github.com/... URL in the JSON response
  • Manual: repeat with --public and --description "..."
  • Manual: without Maestro running — expect MAESTRO_NOT_RUNNING
  • Manual: with a session that has no AI-tab history — expect GIST_CREATE_FAILED with a clear message

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • CLI command to create gists (description + public/private) that returns a gist URL on success and exits with clear error codes on failure.
    • Create gists from conversation history: per-tab formatted markdown, safe filename generation, publishing via desktop app with timeout and structured error results.
    • Renderer ↔ preload IPC for remote create-gist request/response and server-side callback registration.
  • Tests

    • Added tests for the create-gist WebSocket message handling and remote-integration hooks.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end gist creation: new CLI command, WebSocket message and handler, callback registration, preload IPC handlers, web-server-factory wiring to proxy to renderer, renderer logic to format logs and create gists, and associated tests.

Changes

Cohort / File(s) Summary
CLI Integration
src/cli/commands/gist.ts, src/cli/index.ts
Add gist create <agent-id> command and gistCreate implementation that sends a create_gist request to Maestro, awaits create_gist_result (60s), prints JSON on success, and maps failures to structured exit codes.
Web Server Types & API
src/main/web-server/types.ts, src/main/web-server/WebServer.ts
Introduce CreateGistCallback type and WebServer.setCreateGistCallback setter to register a gist callback.
Callback Registry & Message Handling
src/main/web-server/managers/CallbackRegistry.ts, src/main/web-server/handlers/messageHandlers.ts
Add nullable createGist in registry with setCreateGistCallback and createGist(...) proxy; add create_gist WebSocket handler with strict validation, defaults, error mapping, and standardized create_gist_result responses.
Web Server Factory Wiring
src/main/web-server/web-server-factory.ts
Register setCreateGistCallback to perform IPC round-trip to renderer via per-request responseChannel; includes 60s timeout, cleanup, empty-response handling, and early failure when webContents is unavailable.
Preload IPC & Renderer Types
src/main/preload/process.ts, src/renderer/global.d.ts
Expose onRemoteCreateGist(callback) subscription and sendRemoteCreateGistResponse(responseChannel, result) responder on preload API and update renderer typings.
Renderer Integration
src/renderer/hooks/remote/useRemoteIntegration.ts
Register onRemoteCreateGist listener: resolve session, format per-tab logs to markdown, validate content, build filename, call window.maestro.git.createGist(...), return result via response channel; report unexpected errors to Sentry (without transcripts).
Tests & Mocks
src/__tests__/main/web-server/handlers/messageHandlers.test.ts, src/__tests__/renderer/hooks/useRemoteIntegration.test.ts, src/__tests__/main/web-server/web-server-factory.test.ts
Add tests for create_gist message handling and extend test mocks for onRemoteCreateGist/sendRemoteCreateGistResponse and setCreateGistCallback registration.

Sequence Diagram

sequenceDiagram
    participant CLI
    participant Maestro as Maestro App
    participant WebSocket as WebSocket Handler
    participant Factory as Web Server Factory
    participant Renderer
    participant Git as Git Service

    CLI->>Maestro: send create_gist(sessionId, description, isPublic)
    Maestro->>WebSocket: WebSocket message type: create_gist
    WebSocket->>Factory: invoke registered createGist(sessionId, description, isPublic)
    Factory->>Renderer: ipc: remote:createGist(sessionId, description, isPublic, responseChannel)
    Renderer->>Renderer: gather session, format logs to markdown, build filename
    Renderer->>Git: createGist(filename, content, description, isPublic)
    Git-->>Renderer: { success, gistUrl?, error? }
    Renderer->>Factory: ipc: sendRemoteCreateGistResponse(responseChannel, result)
    Factory-->>WebSocket: callback result { success, gistUrl?, error? }
    WebSocket-->>Maestro: create_gist_result (requestId, result)
    Maestro-->>CLI: deliver result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • pedramamini

Poem

🐰 I nibble logs and stitch a file,
Tabs and headers stack in style,
IPC tunnels hum and sing,
A tiny gist takes wing — ping! ping!
Hooray — a rabbit's little smile 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main feature added: a new CLI command for creating and publishing gists. It aligns with the primary objective of adding 'gist create' functionality to publish session transcripts to GitHub.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

Adds a `maestro-cli gist create <agent-id>` subcommand that routes through
the running Maestro desktop app over the existing WebSocket bridge and
publishes every AI tab's transcript for the agent as a single GitHub gist.

Reuses the existing `git:createGist` IPC handler (shells out to `gh gist
create`), so the same GitHub CLI auth requirements apply as in the
desktop "Publish Gist" flow. Supports `--description` and `--public`;
defaults to private.

Plumbing mirrors the summarize/transfer/merge-context remote pattern:
`create_gist` WS message -> main-process callback -> `remote:createGist`
IPC into the renderer, where `formatLogsForClipboard()` builds the
markdown payload from the session's in-memory `aiTabs`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR adds maestro-cli gist create <agent-id> to publish session transcripts as GitHub gists, routing through the running Maestro desktop app via the existing git:createGist IPC handler. It also backfills SSH-wrapping and config overrides (customModel, customEffort, customArgs, customEnvVars) into the CLI spawner so it reaches parity with the desktop app's spawn behaviour.

The plumbing follows the existing summarize/transfer/merge-context pattern faithfully: WebSocket → handleCreateGist callback → IPC request-response with 60 s timeout → renderer useRemoteIntegration effect → gh gist create. All three P2 findings are minor style/observability gaps that don't affect the primary user path.

Confidence Score: 5/5

Safe to merge — all findings are P2 style/observability issues with no impact on correctness or the primary user path.

The new gist feature is correctly wired end-to-end, follows established IPC patterns, includes tests for the new callback and preload hooks, and handles the main error cases (agent not found, no transcript, desktop not running, timeout). The three P2 comments cover a fragile string-match, a missing captureException call, and a minor error.message cast — none are blocking.

No files require special attention before merging.

Important Files Changed

Filename Overview
src/cli/commands/gist.ts New gist create CLI command; error-detection for MAESTRO_NOT_RUNNING relies on fragile string-matching
src/renderer/hooks/remote/useRemoteIntegration.ts New onRemoteCreateGist effect correctly formats transcripts and calls git IPC, but the catch block omits captureException unlike sibling handlers
src/main/web-server/handlers/messageHandlers.ts New handleCreateGist handler; catch block accesses error.message without instanceof guard
src/main/web-server/web-server-factory.ts createGist callback follows existing IPC request-response pattern with proper timeout handling (60s) and resolved guard
src/cli/services/agent-spawner.ts Adds SSH wrapping and config override support (model, effort, args, env vars) to CLI spawner, mirroring desktop app behavior; helper functions are well-factored
src/cli/commands/send.ts Threads customEffort, customArgs, customEnvVars, and sshRemoteConfig through to spawnAgent
src/cli/services/batch-processor.ts Threads new override fields through to spawnAgent in both the main task and synopsis spawn paths
src/shared/types.ts SessionInfo extended with customArgs, customEnvVars, and sessionSshRemoteConfig; JSDoc comments added

Sequence Diagram

sequenceDiagram
    participant CLI as maestro-cli(gist.ts)
    participant WS as WebSocketMessageHandler
    participant CB as CallbackRegistry
    participant Factory as web-server-factory.ts
    participant IPC as ipcMain
    participant Renderer as useRemoteIntegration
    participant GH as gh gist create

    CLI->>WS: WS: type=create_gist, sessionId, description, isPublic
    WS->>CB: handleCreateGist -> callbacks.createGist(...)
    CB->>Factory: createGist callback
    Factory->>IPC: ipcMain.once(responseChannel)
    Factory->>Renderer: webContents.send(remote:createGist, ...)
    Renderer->>Renderer: find session, format aiTabs via formatLogsForClipboard()
    Renderer->>GH: window.maestro.git.createGist(filename, content, desc, isPublic)
    GH-->>Renderer: { success, gistUrl }
    Renderer->>IPC: ipcRenderer.send(responseChannel, result)
    IPC-->>Factory: handleResponse fires, clearTimeout
    Factory-->>CB: resolve { success, gistUrl }
    CB-->>WS: Promise resolves
    WS-->>CLI: WS: type=create_gist_result, success, gistUrl
Loading

Comments Outside Diff (1)

  1. src/cli/commands/gist.ts, line 676-689 (link)

    P2 Fragile string-matching for connection errors

    The MAESTRO_NOT_RUNNING classification relies on inspecting the error message string. If the underlying WebSocket library changes its error text (or a different transport-level error surfaces), these checks would silently fall through to GIST_CREATE_FAILED instead. Consider also checking error.code (e.g. ECONNREFUSED, ENOTFOUND) alongside the message strings for more reliable detection — or deriving a typed "connection refused" error upstream in withMaestroClient.

Reviews (1): Last reviewed commit: "feat(cli): add `gist create` command to ..." | Re-trigger Greptile

Comment on lines +1096 to +1102
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
window.maestro.process.sendRemoteCreateGistResponse(responseChannel, {
success: false,
error: message,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unexpected errors silently swallowed without Sentry reporting

The catch block responds to the caller but doesn't call captureException for unexpected errors (e.g. window.maestro.git.createGist throws for an unforeseen reason). The cue subscription handler immediately above in this file (remoteTriggerCueSubscription) calls captureException(error, { extra: { context: '...' } }) for the same pattern. Aligning here ensures observability parity across all remote-action handlers.

Comment on lines +2610 to +2612

this.callbacks
.createGist(sessionId, description, isPublic)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 error.message access without instanceof guard

If the createGist callback rejects with a non-Error (a string, a plain object, etc.), error.message evaluates to undefined and the client receives "Failed to create gist: undefined". Guarding with error instanceof Error ? error.message : String(error) is the convention used throughout this codebase (see gist.ts line 675 and web-server-factory.ts error paths) and avoids the unhelpful message.

Suggested change
this.callbacks
.createGist(sessionId, description, isPublic)
.catch((error: unknown) => {
const msg = error instanceof Error ? error.message : String(error);
this.sendError(client, `Failed to create gist: ${msg}`);
});

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/__tests__/main/web-server/handlers/messageHandlers.test.ts (1)

122-122: Consider adding handler-level tests for create_gist.

The mock callback is added to createMockCallbacks, but this suite doesn't yet exercise the create_gist message path (happy path, missing agentId, createGist callback unconfigured, callback rejection → create_gist_result with error). Given the structured error contract promised in the PR description (AGENT_NOT_FOUND, GIST_CREATE_FAILED, timeouts), a small describe block here would lock that contract down.

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

In `@src/__tests__/main/web-server/handlers/messageHandlers.test.ts` at line 122,
Add a new describe block in the existing message handlers test suite that
exercises the create_gist message path using the createMockCallbacks mock: write
tests for the happy path (mock createGist to resolve and assert a
create_gist_result with success and gistUrl), for missing agentId (send message
without agentId and assert create_gist_result returns AGENT_NOT_FOUND), for an
unconfigured callback (ensure createGist is undefined in createMockCallbacks and
assert create_gist_result returns GIST_CREATE_FAILED or appropriate error), and
for callback rejection (mock createGist to reject and assert create_gist_result
contains GIST_CREATE_FAILED and error details). Ensure each test sends the
create_gist message and inspects the returned create_gist_result payload.
src/main/preload/process.ts (1)

1113-1130: Consider the ack-on-sync-throw pattern for consistency with sibling handlers.

Unlike onRemoteOpenBrowserTab, onRemoteOpenTerminalTab, onRemoteNewAITabWithPrompt, and onRemoteConfigureAutoRun (which wrap the callback invocation in try/catch and send a failure ack on synchronous throw), onRemoteCreateGist forwards directly. If the renderer's callback ever throws synchronously (e.g., programming error before the try in useRemoteIntegration.ts runs), the CLI will stall until the 60s web-server-factory timeout instead of failing fast.

In practice the current renderer implementation is async-safe, but matching the surrounding pattern is cheap defense.

♻️ Align with sibling handlers
 		onRemoteCreateGist: (
 			callback: (
 				sessionId: string,
 				description: string,
 				isPublic: boolean,
 				responseChannel: string
 			) => void
 		): (() => void) => {
 			const handler = (
 				_: unknown,
 				sessionId: string,
 				description: string,
 				isPublic: boolean,
 				responseChannel: string
-			) => callback(sessionId, description, isPublic, responseChannel);
+			) => {
+				try {
+					callback(sessionId, description, isPublic, responseChannel);
+				} catch (error) {
+					ipcRenderer.send(responseChannel, {
+						success: false,
+						error: error instanceof Error ? error.message : String(error),
+					});
+					throw error;
+				}
+			};
 			ipcRenderer.on('remote:createGist', handler);
 			return () => ipcRenderer.removeListener('remote:createGist', handler);
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/preload/process.ts` around lines 1113 - 1130, The onRemoteCreateGist
handler currently forwards the renderer callback directly and can let a
synchronous throw bubble up; update the handler in onRemoteCreateGist to mirror
the ack-on-sync-throw pattern used by
onRemoteOpenBrowserTab/onRemoteOpenTerminalTab/onRemoteNewAITabWithPrompt/onRemoteConfigureAutoRun
by wrapping the callback invocation in try/catch and, on a caught synchronous
error, send a failure acknowledgment on the provided responseChannel (e.g.,
ipcRenderer.send(responseChannel, { success: false, error: error?.message ||
String(error) })) before returning; keep the same handler signature and the
existing ipcRenderer.on/removeListener lifecycle.
src/__tests__/cli/services/agent-spawner.test.ts (1)

1637-2105: LGTM on the new config-override + SSH test coverage.

The new driveSpawnToCompletion harness that races waitForSpawnCall() against the promise is a nice touch — it lets the SSH hard-fail tests resolve via the returned error without hanging on a spawn that never happens. Precedence (session vs agent-level), env layering (shell-wins vs user-wins), and SSH wrapper invocation/failure paths are all covered.

One soft nit (non-blocking): in forwards agent binaryName (not local path) to the SSH wrapper (Lines 1967-1982), mockGetAgentCustomPath.mockReturnValue('/opt/local/claude') has no effect on getAgentCommand('claude-code') here because cachedPaths is empty and detectAgent() isn't invoked in this test — so wrapConfig.command is 'claude' too, and the test only proves agentBinaryName === 'claude'. If you want to actually exercise the "local custom path vs remote binary name" distinction, prime the cache via detectAgent (or export a test seam) and additionally assert wrapConfig.command points at the local path while agentBinaryName stays 'claude'.

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

In `@src/__tests__/cli/services/agent-spawner.test.ts` around lines 1637 - 2105,
The test "forwards agent binaryName (not local path) to the SSH wrapper" is not
exercising the local-vs-remote distinction because cachedPaths isn't populated;
prime the agent detection cache before calling spawnAgent so wrapConfig.command
reflects the local path while wrapConfig.agentBinaryName remains the binary
name. Fix by invoking or mocking detectAgent/getAgentCommand to populate
cachedPaths (e.g., await detectAgent('claude-code') or mock detectAgent to
return '/opt/local/claude') prior to spawnAgent, then assert wrapConfig.command
is the local path and wrapConfig.agentBinaryName is 'claude'.
src/cli/services/agent-spawner.ts (1)

90-109: Optional: batch-mode env defaults can't shadow agent defaults in the local path, but they do on SSH.

In applyEnvLayers, both agentDefaults and batchDefaults are written with an if (!env[k]) guard. Because agentDefaults runs first, any key that exists in both def.defaultEnvVars and def.batchModeEnvVars will resolve to the agent-default value (the batch-default write is blocked by the guard once the agent default has populated env[k]). Meanwhile buildSshEnvForRemote (Lines 471-482) uses sequential Object.assign so the batch value wins on the remote side. The "batch mode is more specific than the interactive default" intent is reversed in the local path.

No current overlap between defaultEnvVars and batchModeEnvVars is visible in the provided context, so this is latent today — flagging for when batch-mode-only overrides are added for an agent that also ships defaults for the same key.

♻️ Proposed fix (let batch defaults override agent defaults, still shell-wins against both)
 function applyEnvLayers(
 	env: NodeJS.ProcessEnv,
 	agentDefaults: Record<string, string> | undefined,
 	batchDefaults: Record<string, string> | undefined,
 	userEnvVars: Record<string, string> | undefined,
 	readOnlyOverrides: Record<string, string> | undefined
 ): void {
-	if (agentDefaults) {
-		for (const [k, v] of Object.entries(agentDefaults)) {
-			if (!env[k]) env[k] = v;
-		}
-	}
-	if (batchDefaults) {
-		for (const [k, v] of Object.entries(batchDefaults)) {
-			if (!env[k]) env[k] = v;
-		}
-	}
+	// Merge agent < batch so batch can override agent; then layer with shell-wins
+	const defaults: Record<string, string> = {
+		...(agentDefaults || {}),
+		...(batchDefaults || {}),
+	};
+	for (const [k, v] of Object.entries(defaults)) {
+		if (!env[k]) env[k] = v;
+	}
 	if (userEnvVars) Object.assign(env, userEnvVars);
 	if (readOnlyOverrides) Object.assign(env, readOnlyOverrides);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/services/agent-spawner.ts` around lines 90 - 109, applyEnvLayers
currently lets agentDefaults win over batchDefaults locally because
agentDefaults are set first with an "if (!env[k])" guard; change the logic so
agentDefaults only apply when the key is not already set in env AND not present
in batchDefaults (i.e., treat batchDefaults as more specific), e.g. when
iterating agentDefaults skip keys that exist in batchDefaults; keep userEnvVars
and readOnlyOverrides application order intact so shell/user overrides still
win; refer to applyEnvLayers and buildSshEnvForRemote to ensure local behavior
now matches the remote ssh path semantics.
🤖 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/cli/commands/gist.ts`:
- Around line 57-67: The code treats a truthy result.success as success even
when result.gistUrl is missing, producing an unusable GistCreateResponse; update
the success check to treat a missing or empty result.gistUrl as failure: after
verifying result.success, validate result.gistUrl (e.g., non-empty string) and
if absent call emitErrorJson('Failed to create gist', 'GIST_CREATE_FAILED') and
exit, otherwise build the GistCreateResponse (using agentId and result.gistUrl)
and print it; reference the existing symbols result.success, result.gistUrl,
emitErrorJson, GistCreateResponse, and agentId to locate and change the logic.

In `@src/main/web-server/handlers/messageHandlers.ts`:
- Around line 2592-2616: The handler currently calls this.sendError for missing
sessionId and when createGist is not configured and also on the createGist
.catch path, which causes clients waiting for "create_gist_result" to timeout;
update these failure paths to always send a "create_gist_result" via this.send
with success: false and an error message (include message.requestId and
timestamp) instead of this.sendError. Specifically modify the checks that use
this.sendError (the sessionId null branch and the callbacks.createGist not
configured branch), and change the .catch handler on
this.callbacks.createGist(...) to send a "create_gist_result" payload containing
success: false, error: error.message, gistUrl: null (or omitted), requestId:
message.requestId, and timestamp: Date.now().
- Around line 2589-2591: Replace the loose truthy coercion for isPublic with an
explicit check against boolean true and the literal string "true": instead of
const isPublic = Boolean(message.isPublic), use something like const isPublic =
message.isPublic === true || message.isPublic === 'true' to ensure values like
"false" are not treated as true; update the code near where description and
isPublic are initialized (variables description and isPublic, using
message.isPublic) to use this strict validation.

In `@src/renderer/hooks/remote/useRemoteIntegration.ts`:
- Around line 1096-1102: The catch currently swallows unexpected errors and only
sends a CLI response; change it to capture the error with Sentry (e.g.,
Sentry.captureException(error) or Sentry.captureMessage) following the same
"capture + ack" pattern used in onRemoteTriggerCueSubscription, then still call
window.maestro.process.sendRemoteCreateGistResponse(responseChannel, { success:
false, error: message }); ensure the Sentry payload uses captureException
without attaching tab content/description/filename/transcript or any PII (do not
include those fields in the Sentry scope or extra), and keep the existing error
string assignment (const message = error instanceof Error ? error.message :
String(error)) for the IPC response.

---

Nitpick comments:
In `@src/__tests__/cli/services/agent-spawner.test.ts`:
- Around line 1637-2105: The test "forwards agent binaryName (not local path) to
the SSH wrapper" is not exercising the local-vs-remote distinction because
cachedPaths isn't populated; prime the agent detection cache before calling
spawnAgent so wrapConfig.command reflects the local path while
wrapConfig.agentBinaryName remains the binary name. Fix by invoking or mocking
detectAgent/getAgentCommand to populate cachedPaths (e.g., await
detectAgent('claude-code') or mock detectAgent to return '/opt/local/claude')
prior to spawnAgent, then assert wrapConfig.command is the local path and
wrapConfig.agentBinaryName is 'claude'.

In `@src/__tests__/main/web-server/handlers/messageHandlers.test.ts`:
- Line 122: Add a new describe block in the existing message handlers test suite
that exercises the create_gist message path using the createMockCallbacks mock:
write tests for the happy path (mock createGist to resolve and assert a
create_gist_result with success and gistUrl), for missing agentId (send message
without agentId and assert create_gist_result returns AGENT_NOT_FOUND), for an
unconfigured callback (ensure createGist is undefined in createMockCallbacks and
assert create_gist_result returns GIST_CREATE_FAILED or appropriate error), and
for callback rejection (mock createGist to reject and assert create_gist_result
contains GIST_CREATE_FAILED and error details). Ensure each test sends the
create_gist message and inspects the returned create_gist_result payload.

In `@src/cli/services/agent-spawner.ts`:
- Around line 90-109: applyEnvLayers currently lets agentDefaults win over
batchDefaults locally because agentDefaults are set first with an "if (!env[k])"
guard; change the logic so agentDefaults only apply when the key is not already
set in env AND not present in batchDefaults (i.e., treat batchDefaults as more
specific), e.g. when iterating agentDefaults skip keys that exist in
batchDefaults; keep userEnvVars and readOnlyOverrides application order intact
so shell/user overrides still win; refer to applyEnvLayers and
buildSshEnvForRemote to ensure local behavior now matches the remote ssh path
semantics.

In `@src/main/preload/process.ts`:
- Around line 1113-1130: The onRemoteCreateGist handler currently forwards the
renderer callback directly and can let a synchronous throw bubble up; update the
handler in onRemoteCreateGist to mirror the ack-on-sync-throw pattern used by
onRemoteOpenBrowserTab/onRemoteOpenTerminalTab/onRemoteNewAITabWithPrompt/onRemoteConfigureAutoRun
by wrapping the callback invocation in try/catch and, on a caught synchronous
error, send a failure acknowledgment on the provided responseChannel (e.g.,
ipcRenderer.send(responseChannel, { success: false, error: error?.message ||
String(error) })) before returning; keep the same handler signature and the
existing ipcRenderer.on/removeListener lifecycle.
🪄 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: c4dbc3d0-0c00-41e5-8c46-26bf786f467b

📥 Commits

Reviewing files that changed from the base of the PR and between 60959b1 and beec8f0.

📒 Files selected for processing (20)
  • CLAUDE.md
  • docs/agent-guides/CLI-PLAYBOOKS.md
  • src/__tests__/cli/services/agent-spawner.test.ts
  • src/__tests__/main/web-server/handlers/messageHandlers.test.ts
  • src/__tests__/renderer/hooks/useRemoteIntegration.test.ts
  • src/cli/commands/gist.ts
  • src/cli/commands/send.ts
  • src/cli/index.ts
  • src/cli/services/agent-spawner.ts
  • src/cli/services/batch-processor.ts
  • src/main/preload/process.ts
  • src/main/utils/agent-args.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
  • src/shared/types.ts

Comment thread src/cli/commands/gist.ts Outdated
Comment thread src/main/web-server/handlers/messageHandlers.ts Outdated
Comment thread src/main/web-server/handlers/messageHandlers.ts Outdated
Comment thread src/renderer/hooks/remote/useRemoteIntegration.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/renderer/hooks/remote/useRemoteIntegration.ts (1)

1096-1102: ⚠️ Potential issue | 🟡 Minor

Report unexpected errors to Sentry instead of silently wrapping them into the CLI response.

Known recoverable failures (session missing, empty history, gh not installed/authenticated) are already returned as explicit { success: false, ... } payloads upstream, so anything arriving here is an unexpected failure (IPC crash, null deref, etc.). Follow the onRemoteTriggerCueSubscription pattern at lines 1023-1039 — capture with captureException (already imported on line 5), then send the failure ack. Keep the transcript/description/filename out of the Sentry payload since they can carry PII or secrets.

🛡️ Capture + ack
 				} catch (error) {
 					const message = error instanceof Error ? error.message : String(error);
+					captureException(error, {
+						extra: {
+							context: 'remoteCreateGist',
+							sessionId,
+							isPublic,
+							descriptionProvided: Boolean(description),
+						},
+					});
 					window.maestro.process.sendRemoteCreateGistResponse(responseChannel, {
 						success: false,
 						error: message,
 					});
 				}

As per coding guidelines: "Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production."

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

In `@src/renderer/hooks/remote/useRemoteIntegration.ts` around lines 1096 - 1102,
The catch block that currently converts the unexpected error to a string and
sends a CLI response should first report the raw exception to Sentry using
captureException(error) (captureException is already imported), then continue to
send the failure ack via
window.maestro.process.sendRemoteCreateGistResponse(responseChannel, { success:
false, error: message }); ensure you do not include
transcript/description/filename or other PII in the capture payload; reference
the existing responseChannel and sendRemoteCreateGistResponse call so the
reporting happens before the existing send.
src/cli/commands/gist.ts (1)

57-67: ⚠️ Potential issue | 🟡 Minor

Treat a missing gistUrl on a success: true response as a failure.

The renderer-side contract (window.maestro.git.createGist{ success, gistUrl?, error? }) allows success: true without a URL in theory, and other structured failures at the main/renderer boundary also resolve to success: true payloads with no URL (e.g., empty-response fallbacks at web-server-factory.ts:1737). Without a gistUrl the CLI prints a "successful but unusable" JSON response.

Proposed fix
-		if (!result.success) {
-			emitErrorJson(result.error ?? 'Failed to create gist', 'GIST_CREATE_FAILED');
-			process.exit(1);
-		}
+		if (!result.success || !result.gistUrl) {
+			emitErrorJson(result.error ?? 'Failed to create gist', 'GIST_CREATE_FAILED');
+			process.exit(1);
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/gist.ts` around lines 57 - 67, When handling the renderer
result in gist.ts, treat a success:true that lacks a gistUrl as a failure: after
checking result.success, also verify result.gistUrl is a non-empty string and if
not call emitErrorJson with a descriptive error (e.g., 'Missing gistUrl in
successful response') and exit via process.exit(1); only construct the
GistCreateResponse (using agentId and result.gistUrl) and console.log when
result.success === true and result.gistUrl is present.
🤖 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/web-server-factory.ts`:
- Around line 1715-1764: The tests break because the new server API method
setCreateGistCallback is not stubbed in
src/__tests__/main/web-server/web-server-factory.test.ts; update the test setup
to add a mock/no-op for server.setCreateGistCallback (or a jest.fn()) so the
factory can call it without throwing, and add an assertion that
setCreateGistCallback was called (or that the callback was registered) to
validate the change; reference the server mock used in that test and the
exported setCreateGistCallback method when adding the stub and assertion.

---

Duplicate comments:
In `@src/cli/commands/gist.ts`:
- Around line 57-67: When handling the renderer result in gist.ts, treat a
success:true that lacks a gistUrl as a failure: after checking result.success,
also verify result.gistUrl is a non-empty string and if not call emitErrorJson
with a descriptive error (e.g., 'Missing gistUrl in successful response') and
exit via process.exit(1); only construct the GistCreateResponse (using agentId
and result.gistUrl) and console.log when result.success === true and
result.gistUrl is present.

In `@src/renderer/hooks/remote/useRemoteIntegration.ts`:
- Around line 1096-1102: The catch block that currently converts the unexpected
error to a string and sends a CLI response should first report the raw exception
to Sentry using captureException(error) (captureException is already imported),
then continue to send the failure ack via
window.maestro.process.sendRemoteCreateGistResponse(responseChannel, { success:
false, error: message }); ensure you do not include
transcript/description/filename or other PII in the capture payload; reference
the existing responseChannel and sendRemoteCreateGistResponse call so the
reporting happens before the existing send.
🪄 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: d85236c7-38e0-4c46-be8c-979a0f789be3

📥 Commits

Reviewing files that changed from the base of the PR and between beec8f0 and f23eb0d.

📒 Files selected for processing (12)
  • src/__tests__/main/web-server/handlers/messageHandlers.test.ts
  • src/__tests__/renderer/hooks/useRemoteIntegration.test.ts
  • src/cli/commands/gist.ts
  • src/cli/index.ts
  • src/main/preload/process.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
✅ Files skipped from review due to trivial changes (3)
  • src/tests/main/web-server/handlers/messageHandlers.test.ts
  • src/main/preload/process.ts
  • src/renderer/global.d.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/tests/renderer/hooks/useRemoteIntegration.test.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/types.ts
  • src/main/web-server/handlers/messageHandlers.ts

Comment on lines +1715 to +1764
// Create gist — uses IPC request-response pattern. The renderer holds
// the AI-tab transcripts in memory, so we forward to it and let it
// build the payload + call the existing `git:createGist` handler.
server.setCreateGistCallback(
async (sessionId: string, description: string, isPublic: boolean) => {
const mainWindow = getMainWindow();
if (!mainWindow) {
logger.warn('mainWindow is null for createGist', 'WebServer');
return { success: false, error: 'Desktop app window is not available' };
}

return new Promise<{ success: boolean; gistUrl?: string; error?: string }>((resolve) => {
const responseChannel = `remote:createGist:response:${randomUUID()}`;
let resolved = false;

const handleResponse = (
_event: Electron.IpcMainEvent,
result: { success: boolean; gistUrl?: string; error?: string } | undefined
) => {
if (resolved) return;
resolved = true;
clearTimeout(timeoutId);
resolve(result ?? { success: false, error: 'Empty response' });
};

ipcMain.once(responseChannel, handleResponse);
if (!isWebContentsAvailable(mainWindow)) {
logger.warn('webContents is not available for createGist', 'WebServer');
ipcMain.removeListener(responseChannel, handleResponse);
resolve({ success: false, error: 'Desktop webContents not available' });
return;
}
mainWindow.webContents.send(
'remote:createGist',
sessionId,
description,
isPublic,
responseChannel
);

const timeoutId = setTimeout(() => {
if (resolved) return;
resolved = true;
ipcMain.removeListener(responseChannel, handleResponse);
logger.warn(`createGist callback timed out for session ${sessionId}`, 'WebServer');
resolve({ success: false, error: 'Timed out waiting for gist creation' });
}, 60000);
});
}
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Create-gist callback registration LGTM; 60s timeout matches the end-to-end cost of gh gist create.

The request-response wiring follows the established pattern (per-call responseChannel via randomUUID(), guarded ipcMain.once, timeout cleanup, structured failure payloads). One gap the CI is already flagging: src/__tests__/main/web-server/web-server-factory.test.ts doesn't stub the new setCreateGistCallback, so every factory test in that file throws TypeError: server.setCreateGistCallback is not a function. That needs a mock update (and ideally a positive assertion that the callback was registered) in that test file before this PR can merge green.

🧰 Tools
🪛 GitHub Check: test

[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > callback registrations > should register getSessionDetailCallback
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:349:13


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > callback registrations > should register getSessionsCallback
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:349:13


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should generate and store new token when persistentWebLink is true and no token exists
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:325:19


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should reject invalid stored token and generate a new UUID
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:300:19


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should use stored token when persistentWebLink is true and token is a valid UUID
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:287:19


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should not pass security token when persistentWebLink is false
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:273:19


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should use custom port when enabled
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:260:19


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should use random port (0) when custom port is disabled
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:246:19


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should register a bionify reading mode callback sourced from settings
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:230:19


[failure] 1718-1718: src/tests/main/web-server/web-server-factory.test.ts > web-server/web-server-factory > createWebServerFactory > should create a WebServer when called
TypeError: server.setCreateGistCallback is not a function
❯ createWebServer src/main/web-server/web-server-factory.ts:1718:10
❯ src/tests/main/web-server/web-server-factory.test.ts:217:19

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

In `@src/main/web-server/web-server-factory.ts` around lines 1715 - 1764, The
tests break because the new server API method setCreateGistCallback is not
stubbed in src/__tests__/main/web-server/web-server-factory.test.ts; update the
test setup to add a mock/no-op for server.setCreateGistCallback (or a jest.fn())
so the factory can call it without throwing, and add an assertion that
setCreateGistCallback was called (or that the callback was registered) to
validate the change; reference the server mock used in that test and the
exported setCreateGistCallback method when adding the stub and assertion.

@pedramamini
Copy link
Copy Markdown
Collaborator

Thanks for putting this together, @chr1syy — nice clean reuse of the existing remote-request pattern, and the ## Tab: transcript formatting is a thoughtful touch. A few things to address before we can land it:

Blocking

CI test job is failing. src/__tests__/main/web-server/web-server-factory.test.ts throws TypeError: server.setCreateGistCallback is not a function in ~30 cases (lines 287, 300, 325, 349, 410–611). The mock server used by that suite needs the new setter added alongside the other callback setters. Your PR description has this box checked (vitest run ... messageHandlers.test.ts — 125 tests pass), but that only covered the two files you touched; the factory tests also depend on the new API and weren't updated. Please pull down rc, run the full npm run test locally, and fix the mock.

Worth addressing (bot feedback I agree with)

  1. messageHandlers.ts ~L2591 — isPublic truthy coercion. Boolean(message.isPublic) will treat the string "false" as true and publish a private transcript publicly. Since this is a safety-relevant default, please accept only strict === true (or equivalent) and default to private.
  2. messageHandlers.ts ~L2616 — failure paths send generic error messages instead of create_gist_result. The CLI is waiting for create_gist_result, so anything reported via the generic error channel will eventually time out on the client side and the user sees a timeout instead of the real cause. Please emit create_gist_result with success: false, error: ... for all known failure modes.
  3. useRemoteIntegration.ts ~L1102 — catch-all swallows unexpected errors without Sentry. Per our CLAUDE.md Sentry guidance, known-recoverable errors (gh not installed / not authenticated / missing session / empty history) are already handled upstream, so anything reaching this catch is unexpected and should call captureException before responding to the client.
  4. messageHandlers.ts ~L2612 — error.message without instanceof Error guard can produce "Failed to create gist: undefined" for non-Error rejections. Quick fix alongside Raychaser/claude path 01 #3.
  5. cli/commands/gist.ts ~L67 — treat missing gistUrl as failure. If success: true comes back with no URL, the CLI currently prints a successful-looking JSON blob the user can't do anything with. Handling it as GIST_CREATE_FAILED is cleaner.

Non-blocking, but worth a quick look. Once CI is green and #1#3 are addressed, we should be good to land. No merge conflicts currently — just a stacking dependency on #888 as you noted.

chr1syy and others added 3 commits April 24, 2026 18:16
- Stub the new `setCreateGistCallback` in the factory test's WebServer mock
  so every `createWebServer()` test stops throwing TypeError.
- `handleCreateGist` now always replies with `create_gist_result` on every
  failure path (missing sessionId, missing callback, strict-validation
  refusal, rejected promise). Previously sendError was emitted, which left
  clients hanging until their WebSocket request timeout.
- Strictly validate `isPublic` and `description` types; reject coerced
  truthy values like the string "false" to prevent accidental
  private→public gist publication.
- Guard the rejected-promise `catch` with `instanceof Error`.
- Renderer gist handler now reports unexpected errors via `captureException`
  (omitting transcript/description/filename, which can carry PII).
- CLI treats a `success: true` response with no `gistUrl` as a failure.
- Added handler tests pinning the new contract (success path, defaults,
  missing sessionId, non-boolean isPublic, rejected callback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR RunMaestro#898 added the gist callback to CallbackRegistry and the
factory's setCreateGistCallback, but `setupMessageHandlerCallbacks`
in WebServer.ts was missing the createGist mapping. As a result,
`messageHandlers.handleCreateGist` always saw `this.callbacks.createGist
=== undefined` and replied with "Gist creation not configured", so
`maestro-cli gist create` returned a hard failure for every input
even though the rest of the pipeline was correct.

Mirror the pattern used for summarizeContext: forward the call
through to `this.callbackRegistry.createGist`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts:
- src/cli/index.ts: keep both gist and notify command groups
- src/renderer/hooks/remote/useRemoteIntegration.ts: keep both formatLogsForClipboard
  and notify/session-store imports

Review fixes:
- preload/process.ts onRemoteCreateGist: ack a structured failure on
  synchronous callback throw before rethrowing, mirroring the sibling
  onRemoteOpenBrowserTab/onRemoteOpenTerminalTab/onRemoteNewAITabWithPrompt
  pattern so the CLI doesn't wait on the 60s response timeout.
- Add messageHandlers test for the unconfigured-callback path
  (createGist === undefined → create_gist_result with "not configured").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/main/web-server/web-server-factory.ts (1)

1746-1746: ⚠️ Potential issue | 🔴 Critical

WebServer factory tests must mock the new setCreateGistCallback API.

Adding this setter changes the expected WebServer mock shape; factory tests will fail until the test double includes setCreateGistCallback and validates registration.

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

In `@src/main/web-server/web-server-factory.ts` at line 1746, Update the
web-server factory tests to include and assert the new setter on the WebServer
test double: add a mock implementation for setCreateGistCallback on the
WebServer stub used by the factory tests, ensure the factory calls
server.setCreateGistCallback(...) during setup, and validate that the provided
callback was registered (e.g., called or stored) so the mock shape matches the
updated WebServer interface; look for references to setCreateGistCallback and
the WebServer stub in the web-server-factory tests and add the necessary mock +
assertion.
🧹 Nitpick comments (1)
src/__tests__/main/web-server/handlers/messageHandlers.test.ts (1)

1733-1749: Add a non-Error rejection test for create_gist.

Current coverage only asserts Error rejections. Add a case for string/object rejection to lock in the instanceof Error guard behavior and prevent "undefined" error payload regressions.

🧪 Suggested test addition
 describe('Create Gist', () => {
@@
 		it('surfaces rejected callback errors as create_gist_result', async () => {
 			(callbacks.createGist as any).mockRejectedValue(new Error('boom'));
@@
 			expect(response.error).toContain('boom');
 		});
+
+		it('surfaces non-Error rejections as create_gist_result', async () => {
+			(callbacks.createGist as any).mockRejectedValue('boom-string');
+
+			handler.handleMessage(client, {
+				type: 'create_gist',
+				sessionId: 'session-1',
+			});
+
+			await vi.waitFor(() => {
+				expect(client.socket.send).toHaveBeenCalled();
+			});
+
+			const calls = (client.socket.send as any).mock.calls;
+			const response = JSON.parse(calls[calls.length - 1][0]);
+			expect(response.type).toBe('create_gist_result');
+			expect(response.success).toBe(false);
+			expect(response.error).toContain('boom-string');
+		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/web-server/handlers/messageHandlers.test.ts` around lines
1733 - 1749, Add a new test that mirrors the existing "surfaces rejected
callback errors as create_gist_result" but mocks callbacks.createGist to reject
with a non-Error value (e.g., a string or plain object) and then calls
handler.handleMessage(client, { type: 'create_gist', sessionId: 'session-1' });
wait for client.socket.send and assert the JSON-parsed response still has type
'create_gist_result', success false, and that response.error contains a sensible
message (for string rejects assert the string appears; for object rejects assert
a fallback like JSON.stringify or a default message is used). This ensures the
instanceof Error guard around callbacks.createGist rejection handling produces a
stable error payload when callbacks.createGist is rejected with non-Error
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/web-server/web-server-factory.ts`:
- Line 1746: Update the web-server factory tests to include and assert the new
setter on the WebServer test double: add a mock implementation for
setCreateGistCallback on the WebServer stub used by the factory tests, ensure
the factory calls server.setCreateGistCallback(...) during setup, and validate
that the provided callback was registered (e.g., called or stored) so the mock
shape matches the updated WebServer interface; look for references to
setCreateGistCallback and the WebServer stub in the web-server-factory tests and
add the necessary mock + assertion.

---

Nitpick comments:
In `@src/__tests__/main/web-server/handlers/messageHandlers.test.ts`:
- Around line 1733-1749: Add a new test that mirrors the existing "surfaces
rejected callback errors as create_gist_result" but mocks callbacks.createGist
to reject with a non-Error value (e.g., a string or plain object) and then calls
handler.handleMessage(client, { type: 'create_gist', sessionId: 'session-1' });
wait for client.socket.send and assert the JSON-parsed response still has type
'create_gist_result', success false, and that response.error contains a sensible
message (for string rejects assert the string appears; for object rejects assert
a fallback like JSON.stringify or a default message is used). This ensures the
instanceof Error guard around callbacks.createGist rejection handling produces a
stable error payload when callbacks.createGist is rejected with non-Error
values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31febc86-068d-4f3f-b3c1-ba1494b31a66

📥 Commits

Reviewing files that changed from the base of the PR and between 6eea066 and 2565444.

📒 Files selected for processing (12)
  • src/__tests__/main/web-server/handlers/messageHandlers.test.ts
  • src/__tests__/main/web-server/web-server-factory.test.ts
  • src/__tests__/renderer/hooks/useRemoteIntegration.test.ts
  • src/cli/index.ts
  • src/main/preload/process.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
✅ Files skipped from review due to trivial changes (3)
  • src/tests/main/web-server/web-server-factory.test.ts
  • src/tests/renderer/hooks/useRemoteIntegration.test.ts
  • src/main/web-server/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/cli/index.ts

@chr1syy chr1syy added ready to merge This PR is ready to merge labels Apr 27, 2026
@pedramamini pedramamini merged commit cdd1235 into RunMaestro:rc Apr 27, 2026
3 checks passed
pedramamini pushed a commit that referenced this pull request Apr 28, 2026
…director-notes

Closes documentation gaps in both the bundled agent-facing CLI reference
(`src/prompts/_maestro-cli.md`) and the public user guide (`docs/cli.md`):

- `dispatch` (PR #907): full section with flag table, JSON response shape,
  and error codes. `send --live` row marked deprecated, pointing to dispatch.
- `gist create` (PR #898): section with `gh` auth prerequisite and error codes.
- `open-browser` (Apr 21): http(s)-only constraint and scheme-less auto-prefix
  behavior documented.
- `open-terminal` (Apr 21): cwd-confinement rule and `zsh` default documented.
- `director-notes`: was only in the bundled prompt; now in the public guide too,
  with the Encore-flag prerequisite called out.

Pure documentation — no source changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready to merge This PR is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants