Skip to content
11 changes: 11 additions & 0 deletions src/__tests__/renderer/components/AboutModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ vi.mock('lucide-react', () => ({
×
</span>
),
MessageSquarePlus: ({
className,
style,
}: {
className?: string;
style?: React.CSSProperties;
}) => (
<span data-testid="message-square-plus-icon" className={className} style={style}>
</span>
),
Wand2: ({ className, style }: { className?: string; style?: React.CSSProperties }) => (
<span data-testid="wand-icon" className={className} style={style}>
🪄
Expand Down
150 changes: 150 additions & 0 deletions src/main/ipc/handlers/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* Feedback IPC Handlers
*
* This module handles:
* - Checking GitHub CLI availability and authentication
* - Submitting feedback text to the selected agent as a structured prompt
*/

import { ipcMain, app } from 'electron';
import fs from 'fs/promises';
import path from 'path';
import { logger } from '../../utils/logger';
import { withIpcErrorLogging, CreateHandlerOptions } from '../../utils/ipcHandler';
import {
isGhInstalled,
setCachedGhStatus,
getCachedGhStatus,
getExpandedEnv,
} from '../../utils/cliDetection';
import { execFileNoThrow } from '../../utils/execFile';
import { ProcessManager } from '../../process-manager';

const LOG_CONTEXT = '[Feedback]';

const GH_NOT_INSTALLED_MESSAGE =
'GitHub CLI (gh) is not installed. Install it from https://cli.github.com';
const GH_NOT_AUTHENTICATED_MESSAGE =
'GitHub CLI is not authenticated. Run "gh auth login" in your terminal.';

function getPromptPath(): string {
if (app.isPackaged) {
return path.join(process.resourcesPath, 'prompts', 'feedback.md');
}

return path.join(app.getAppPath(), 'src', 'prompts', 'feedback.md');
}

/**
* Helper to create handler options with consistent context
*/
const handlerOpts = (
operation: string,
extra?: Partial<CreateHandlerOptions>
): Pick<CreateHandlerOptions, 'context' | 'operation'> => ({
context: LOG_CONTEXT,
operation,
...extra,
});

/**
* Dependencies required for feedback handler registration
*/
export interface FeedbackHandlerDependencies {
getProcessManager: () => ProcessManager | null;
}

/**
* Register feedback IPC handlers.
*/
export function registerFeedbackHandlers(deps: FeedbackHandlerDependencies): void {
const { getProcessManager } = deps;

logger.info('Registering feedback IPC handlers', LOG_CONTEXT);

// Check if GitHub CLI is installed and authenticated
ipcMain.handle(
'feedback:check-gh-auth',
withIpcErrorLogging(
handlerOpts('check-gh-auth'),
async (): Promise<{ authenticated: boolean; message?: string }> => {
// Prefer cache when available
const cached = getCachedGhStatus();
if (cached) {
if (!cached.installed) {
return { authenticated: false, message: GH_NOT_INSTALLED_MESSAGE };
}
if (!cached.authenticated) {
return { authenticated: false, message: GH_NOT_AUTHENTICATED_MESSAGE };
}
return { authenticated: true };
}

// Check if gh is installed
const installed = await isGhInstalled();
if (!installed) {
setCachedGhStatus(false, false);
return { authenticated: false, message: GH_NOT_INSTALLED_MESSAGE };
}

// Check auth status (command output ignored; exit code is the signal)
const authResult = await execFileNoThrow(
'gh',
['auth', 'status'],
undefined,
getExpandedEnv()
);
const authenticated = authResult.exitCode === 0;
setCachedGhStatus(true, authenticated);

if (!authenticated) {
return { authenticated: false, message: GH_NOT_AUTHENTICATED_MESSAGE };
}

return { authenticated: true };
}
)
);

// Submit feedback by writing to an active process
ipcMain.handle(
'feedback:submit',
withIpcErrorLogging(
handlerOpts('submit'),
async ({
sessionId,
feedbackText,
}: {
sessionId: string;
feedbackText: string;
}): Promise<{ success: boolean; error?: string }> => {
if (!sessionId || typeof sessionId !== 'string') {
return { success: false, error: 'No target agent was selected.' };
}

const trimmedFeedback = typeof feedbackText === 'string' ? feedbackText.trim() : '';
if (!trimmedFeedback) {
return { success: false, error: 'Feedback cannot be empty.' };
}
if (trimmedFeedback.length > 5000) {
return { success: false, error: 'Feedback exceeds the maximum length (5000).' };
}

const processManager = getProcessManager();
if (!processManager) {
return { success: false, error: 'Agent process not available' };
}

const promptTemplate = await fs.readFile(getPromptPath(), 'utf-8');
const finalPrompt = promptTemplate.replace('{{FEEDBACK}}', trimmedFeedback);
const writeSuccess = processManager.write(sessionId, `${finalPrompt}\n`);

if (!writeSuccess) {
return { success: false, error: 'Agent process not available' };
}

return { success: true };
}
)
);
}
6 changes: 6 additions & 0 deletions src/main/ipc/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { registerAgentErrorHandlers } from './agent-error';
import { registerTabNamingHandlers, TabNamingHandlerDependencies } from './tabNaming';
import { registerDirectorNotesHandlers, DirectorNotesHandlerDependencies } from './director-notes';
import { registerWakatimeHandlers } from './wakatime';
import { registerFeedbackHandlers } from './feedback';
import { AgentDetector } from '../../agents';
import { ProcessManager } from '../../process-manager';
import { WebServer } from '../../web-server';
Expand Down Expand Up @@ -97,6 +98,7 @@ export type { TabNamingHandlerDependencies };
export { registerDirectorNotesHandlers };
export type { DirectorNotesHandlerDependencies };
export { registerWakatimeHandlers };
export { registerFeedbackHandlers };
export type { AgentsHandlerDependencies };
export type { ProcessHandlerDependencies };
export type { PersistenceHandlerDependencies };
Expand Down Expand Up @@ -280,6 +282,10 @@ export function registerAllHandlers(deps: HandlerDependencies): void {
getProcessManager: deps.getProcessManager,
getAgentDetector: deps.getAgentDetector,
});
// Register Feedback handlers (gh auth + feedback submission)
registerFeedbackHandlers({
getProcessManager: deps.getProcessManager,
});
// Setup logger event forwarding to renderer
setupLoggerEventForwarding(deps.getMainWindow);
}
51 changes: 51 additions & 0 deletions src/main/preload/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Preload API for feedback submission
*
* Provides the window.maestro.feedback namespace for:
* - Checking GitHub CLI auth status for feedback submission
* - Submitting structured feedback to an active agent session
*/

import { ipcRenderer } from 'electron';

/**
* Feedback auth check response
*/
export interface FeedbackAuthResponse {
authenticated: boolean;
message?: string;
}

/**
* Feedback submission response
*/
export interface FeedbackSubmitResponse {
success: boolean;
error?: string;
}

/**
* Feedback API
*/
export interface FeedbackApi {
/**
* Check whether gh CLI is available and authenticated
*/
checkGhAuth: () => Promise<FeedbackAuthResponse>;
/**
* Submit user feedback to an active agent session
*/
submit: (sessionId: string, feedbackText: string) => Promise<FeedbackSubmitResponse>;
}

/**
* Creates the feedback API object for preload exposure
*/
export function createFeedbackApi() {
return {
checkGhAuth: (): Promise<FeedbackAuthResponse> => ipcRenderer.invoke('feedback:check-gh-auth'),

submit: (sessionId: string, feedbackText: string): Promise<FeedbackSubmitResponse> =>
ipcRenderer.invoke('feedback:submit', { sessionId, feedbackText }),
};
}
10 changes: 10 additions & 0 deletions src/main/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { createLeaderboardApi } from './leaderboard';
import { createAttachmentsApi } from './attachments';
import { createProcessApi } from './process';
import { createGitApi } from './git';
import { createFeedbackApi } from './feedback';
import { createFsApi } from './fs';
import { createAgentsApi } from './agents';
import { createSymphonyApi } from './symphony';
Expand All @@ -65,6 +66,7 @@ contextBridge.exposeInMainWorld('maestro', {

// Process/Session API
process: createProcessApi(),
feedback: createFeedbackApi(),

// Agent Error Handling API
agentError: createAgentErrorApi(),
Expand Down Expand Up @@ -250,6 +252,8 @@ export {
createAttachmentsApi,
// Process
createProcessApi,
// Feedback
createFeedbackApi,
// Git
createGitApi,
// Filesystem
Expand Down Expand Up @@ -396,6 +400,12 @@ export type {
AttachmentListResponse,
AttachmentPathResponse,
} from './attachments';
export type {
// From feedback
FeedbackApi,
FeedbackAuthResponse,
FeedbackSubmitResponse,
} from './feedback';
export type {
// From process
ProcessApi,
Expand Down
33 changes: 33 additions & 0 deletions src/prompts/feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Feedback Issue Authoring Instructions

You are creating a GitHub issue from user feedback for RunMaestro.

User-provided feedback:
{{FEEDBACK}}

Do not ask for clarification. Use the text as-is and proceed.

1. Classify feedback type as one of:

- Bug report
- Feature request
- Improvement
- General feedback

2. Write a concise GitHub issue title prefixed with the type, e.g., "Bug: ...".

3. Write issue body with these sections:

- Description
- Expected vs Current Behavior
- Steps to Reproduce (for bug reports; if unavailable, clearly note "Not provided")
- Proposed Solution (for feature/improvement items)
- Impact and Priority (brief)

4. Run:
`gh label create "Maestro-feedback" --repo RunMaestro/Maestro --description "User feedback submitted via Maestro" --color "0E8A16"`

5. Then run:
`gh issue create --repo RunMaestro/Maestro --label "Maestro-feedback" --title "TITLE" --body "BODY"`

6. Reply with only the created issue URL.
7 changes: 7 additions & 0 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ function MaestroConsoleInner() {
// About Modal
aboutModalOpen,
setAboutModalOpen,
feedbackModalOpen,
setFeedbackModalOpen,
// Update Check Modal
setUpdateCheckModalOpen,
// standingOvationData, firstRunCelebrationData — now self-sourced in AppOverlays (Tier 1A)
Expand Down Expand Up @@ -838,6 +840,7 @@ function MaestroConsoleInner() {
handleCloseDebugPackage,
handleCloseShortcutsHelp,
handleCloseAboutModal,
handleCloseFeedbackModal,
handleCloseUpdateCheckModal,
handleCloseProcessMonitor,
handleCloseLogViewer,
Expand Down Expand Up @@ -2468,10 +2471,13 @@ function MaestroConsoleInner() {
hasNoAgents={hasNoAgents}
keyboardMasteryStats={keyboardMasteryStats}
onCloseAboutModal={handleCloseAboutModal}
feedbackModalOpen={feedbackModalOpen}
onCloseFeedbackModal={handleCloseFeedbackModal}
autoRunStats={autoRunStats}
usageStats={usageStats}
handsOnTimeMs={totalActiveTimeMs}
onOpenLeaderboardRegistration={handleOpenLeaderboardRegistrationFromAbout}
onSwitchToSession={setActiveSessionId}
isLeaderboardRegistered={isLeaderboardRegistered}
onCloseUpdateCheckModal={handleCloseUpdateCheckModal}
onCloseProcessMonitor={handleCloseProcessMonitor}
Expand Down Expand Up @@ -2551,6 +2557,7 @@ function MaestroConsoleInner() {
setSettingsTab={setSettingsTab}
setShortcutsHelpOpen={setShortcutsHelpOpen}
setAboutModalOpen={setAboutModalOpen}
setFeedbackModalOpen={setFeedbackModalOpen}
setLogViewerOpen={setLogViewerOpen}
setProcessMonitorOpen={setProcessMonitorOpen}
setUsageDashboardOpen={setUsageDashboardOpen}
Expand Down
Loading