diff --git a/docs/configuration.md b/docs/configuration.md index 78f6a6f4..9f88c72f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,6 +20,44 @@ Settings are organized into tabs: | **AI Commands** | View and edit slash commands, [Spec-Kit](./speckit-commands), and [OpenSpec](./openspec-commands) prompts | | **SSH Hosts** | Configure remote hosts for [SSH agent execution](./ssh-remote-execution) | +## Conductor Profile + +The **Conductor Profile** (Settings → General → **About Me**) is a short description of yourself that gets injected into every AI agent's system prompt. This helps agents understand your background, preferences, and communication style so they can tailor responses accordingly. + +**To configure:** +1. Open **Settings** (`Cmd+,` / `Ctrl+,`) → **General** tab +2. Find the **About Me** text area at the top +3. Write a brief profile describing yourself + +### What to Include + +A good conductor profile is concise (a few sentences to a short paragraph) and covers: + +- **Your role/background**: Developer, researcher, team lead, etc. +- **Technical context**: Languages you work with, tools you prefer, platforms you use +- **Communication preferences**: Direct vs. detailed, level of explanation needed +- **Work style**: Preferences for how agents should approach tasks + +### Example Profile + +``` +Security researcher. macOS desktop. TypeScript and Python for tools. +Direct communication, no fluff. Action over process. Push back on bad ideas. +Generate markdown for Obsidian. CST timezone. +``` + +### How Agents Use It + +When you start a session, Maestro includes your conductor profile in the system prompt sent to the AI agent. This means: + +- Agents adapt their response style to match your preferences +- Technical context helps agents make appropriate assumptions +- Communication preferences reduce back-and-forth clarification + +### Using in Custom Commands + +You can reference your conductor profile in [slash commands](./slash-commands) using the `{{CONDUCTOR_PROFILE}}` template variable. This is useful for commands that need to remind agents of your preferences mid-conversation. + ## Checking for Updates Maestro checks for updates automatically on startup (configurable in Settings → General → **Check for updates on startup**). @@ -112,7 +150,6 @@ In-app toast notifications appear in the corner when events occur. Configure how Notifications are sent when: - An AI task completes (OS notification + optional custom notification) - A long-running command finishes (OS notification) -- The LLM analysis generates a feedback synopsis (custom notification only, if configured) ## Sleep Prevention diff --git a/src/__tests__/integration/AutoRunRightPanel.test.tsx b/src/__tests__/integration/AutoRunRightPanel.test.tsx index 9be7fd5f..dc3688c0 100644 --- a/src/__tests__/integration/AutoRunRightPanel.test.tsx +++ b/src/__tests__/integration/AutoRunRightPanel.test.tsx @@ -119,6 +119,7 @@ vi.mock('../../renderer/components/TemplateAutocompleteDropdown', () => ({ vi.mock('../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys) => keys?.join('+') || ''), + isMacOS: vi.fn(() => false), })); // Create a mock theme for testing diff --git a/src/__tests__/integration/AutoRunSessionList.test.tsx b/src/__tests__/integration/AutoRunSessionList.test.tsx index e5472c8a..8a4fc375 100644 --- a/src/__tests__/integration/AutoRunSessionList.test.tsx +++ b/src/__tests__/integration/AutoRunSessionList.test.tsx @@ -126,6 +126,7 @@ vi.mock('../../renderer/components/TemplateAutocompleteDropdown', () => ({ vi.mock('../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys) => keys?.join('+') || ''), + isMacOS: vi.fn(() => false), })); vi.mock('../../renderer/hooks/useGitStatusPolling', () => ({ diff --git a/src/__tests__/renderer/components/AutoRunExpandedModal.test.tsx b/src/__tests__/renderer/components/AutoRunExpandedModal.test.tsx index c43e0583..03e67c0a 100644 --- a/src/__tests__/renderer/components/AutoRunExpandedModal.test.tsx +++ b/src/__tests__/renderer/components/AutoRunExpandedModal.test.tsx @@ -96,6 +96,7 @@ vi.mock('../../../renderer/components/AutoRun', () => ({ // Mock shortcut formatter vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys: string[]) => keys.join('+')), + isMacOS: vi.fn(() => false), })); // Create a mock theme for testing diff --git a/src/__tests__/renderer/components/AutoRunnerHelpModal.test.tsx b/src/__tests__/renderer/components/AutoRunnerHelpModal.test.tsx index f79b4d25..46aae932 100644 --- a/src/__tests__/renderer/components/AutoRunnerHelpModal.test.tsx +++ b/src/__tests__/renderer/components/AutoRunnerHelpModal.test.tsx @@ -38,6 +38,7 @@ vi.mock('../../../renderer/contexts/LayerStackContext', async () => { // Mock formatShortcutKeys to return predictable output vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: (keys: string[]) => keys.join('+'), + isMacOS: () => false, })); // Sample theme for testing diff --git a/src/__tests__/renderer/components/FilePreview.test.tsx b/src/__tests__/renderer/components/FilePreview.test.tsx index a0e3a67c..8f5e398a 100644 --- a/src/__tests__/renderer/components/FilePreview.test.tsx +++ b/src/__tests__/renderer/components/FilePreview.test.tsx @@ -109,6 +109,7 @@ vi.mock('../../../renderer/utils/tokenCounter', () => ({ // Mock shortcut formatter vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys: string) => keys), + isMacOS: vi.fn(() => false), })); // Mock remarkFileLinks diff --git a/src/__tests__/renderer/components/InlineWizard/WizardInputPanel.test.tsx b/src/__tests__/renderer/components/InlineWizard/WizardInputPanel.test.tsx index 148df502..fd87e756 100644 --- a/src/__tests__/renderer/components/InlineWizard/WizardInputPanel.test.tsx +++ b/src/__tests__/renderer/components/InlineWizard/WizardInputPanel.test.tsx @@ -333,9 +333,10 @@ describe('WizardInputPanel', () => { expect(screen.getByText('Enter')).toBeInTheDocument(); }); - it('shows "⌘ + Enter" when enterToSend is false', () => { + it('shows "⌘ + Enter" (or "Ctrl + Enter" on non-Mac) when enterToSend is false', () => { render(); - expect(screen.getByText('⌘ + Enter')).toBeInTheDocument(); + // Test environment doesn't have Mac user agent, so it shows Ctrl + Enter + expect(screen.getByText(/⌘ \+ Enter|Ctrl \+ Enter/)).toBeInTheDocument(); }); it('calls setEnterToSend when clicked', () => { diff --git a/src/__tests__/renderer/components/InputArea.test.tsx b/src/__tests__/renderer/components/InputArea.test.tsx index 6c3db3c8..72b03688 100644 --- a/src/__tests__/renderer/components/InputArea.test.tsx +++ b/src/__tests__/renderer/components/InputArea.test.tsx @@ -235,17 +235,18 @@ describe('InputArea', () => { const props = createDefaultProps(); render(); - const button = screen.getByTitle('Switch to Meta+Enter to send'); + const button = screen.getByTitle(/Switch to (Cmd|Ctrl)\+Enter to send/); expect(button).toBeInTheDocument(); expect(button).toHaveTextContent('Enter'); }); - it('renders Cmd+Enter when enterToSend is false', () => { + it('renders Cmd+Enter (or Ctrl+Enter on non-Mac) when enterToSend is false', () => { const props = createDefaultProps({ enterToSend: false }); render(); const button = screen.getByTitle('Switch to Enter to send'); - expect(button).toHaveTextContent('⌘ + Enter'); + // Test environment doesn't have Mac user agent, so it shows Ctrl + Enter + expect(button).toHaveTextContent(/⌘ \+ Enter|Ctrl \+ Enter/); }); }); @@ -1505,7 +1506,7 @@ describe('InputArea', () => { const props = createDefaultProps({ enterToSend: true, setEnterToSend }); render(); - fireEvent.click(screen.getByTitle('Switch to Meta+Enter to send')); + fireEvent.click(screen.getByTitle(/Switch to (Cmd|Ctrl)\+Enter to send/)); expect(setEnterToSend).toHaveBeenCalledWith(false); }); diff --git a/src/__tests__/renderer/components/MainPanel.test.tsx b/src/__tests__/renderer/components/MainPanel.test.tsx index 3f46d353..9adfa6bf 100644 --- a/src/__tests__/renderer/components/MainPanel.test.tsx +++ b/src/__tests__/renderer/components/MainPanel.test.tsx @@ -148,6 +148,7 @@ vi.mock('../../../renderer/utils/tabHelpers', () => ({ // Mock shortcut formatter vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys: string[]) => keys?.join('+') || ''), + isMacOS: vi.fn(() => false), })); // Configurable git status data for tests - can be modified in individual tests diff --git a/src/__tests__/renderer/components/PromptComposerModal.test.tsx b/src/__tests__/renderer/components/PromptComposerModal.test.tsx index ed125fdd..eb59407e 100644 --- a/src/__tests__/renderer/components/PromptComposerModal.test.tsx +++ b/src/__tests__/renderer/components/PromptComposerModal.test.tsx @@ -185,8 +185,9 @@ describe('PromptComposerModal', () => { /> ); - // When enterToSend is false, it shows "⌘ + Enter" - expect(screen.getByText('⌘ + Enter')).toBeInTheDocument(); + // When enterToSend is false, it shows "⌘ + Enter" on Mac or "Ctrl + Enter" on other platforms + // Test environment doesn't have Mac user agent, so it shows Ctrl + Enter + expect(screen.getByText(/⌘ \+ Enter|Ctrl \+ Enter/)).toBeInTheDocument(); }); it('should render close button with X icon', () => { diff --git a/src/__tests__/renderer/components/QuickActionsModal.test.tsx b/src/__tests__/renderer/components/QuickActionsModal.test.tsx index 24396d71..fd9cb348 100644 --- a/src/__tests__/renderer/components/QuickActionsModal.test.tsx +++ b/src/__tests__/renderer/components/QuickActionsModal.test.tsx @@ -59,6 +59,7 @@ vi.mock('../../../renderer/services/git', () => ({ vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys: string[]) => keys.join('+')), + isMacOS: vi.fn(() => false), })); // Mock lucide-react diff --git a/src/__tests__/renderer/components/RightPanel.test.tsx b/src/__tests__/renderer/components/RightPanel.test.tsx index 782266e1..a5c327d0 100644 --- a/src/__tests__/renderer/components/RightPanel.test.tsx +++ b/src/__tests__/renderer/components/RightPanel.test.tsx @@ -21,6 +21,7 @@ vi.mock('../../../renderer/components/AutoRun', () => ({ vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys) => keys.join('+')), + isMacOS: vi.fn(() => false), })); // Mock lucide-react diff --git a/src/__tests__/renderer/components/SettingsModal.test.tsx b/src/__tests__/renderer/components/SettingsModal.test.tsx index d858f297..00029027 100644 --- a/src/__tests__/renderer/components/SettingsModal.test.tsx +++ b/src/__tests__/renderer/components/SettingsModal.test.tsx @@ -38,6 +38,7 @@ vi.mock('../../../renderer/contexts/LayerStackContext', () => ({ // Mock formatShortcutKeys vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys: string[]) => keys.join('+')), + isMacOS: vi.fn(() => false), // Test environment is not Mac })); // Mock AICommandsPanel @@ -808,14 +809,15 @@ describe('SettingsModal', () => { expect(setEnterToSendTerminal).toHaveBeenCalledWith(false); }); - it('should display Cmd+Enter when enter-to-send is false', async () => { + it('should display Cmd+Enter (or Ctrl+Enter on non-Mac) when enter-to-send is false', async () => { render(); await act(async () => { await vi.advanceTimersByTimeAsync(100); }); - expect(screen.getByText('⌘ + Enter')).toBeInTheDocument(); + // Test environment doesn't have Mac user agent, so it shows Ctrl + Enter + expect(screen.getByText(/⌘ \+ Enter|Ctrl \+ Enter/)).toBeInTheDocument(); }); }); diff --git a/src/__tests__/renderer/components/ShortcutEditor.test.tsx b/src/__tests__/renderer/components/ShortcutEditor.test.tsx index 9309f27c..f18ee4c9 100644 --- a/src/__tests__/renderer/components/ShortcutEditor.test.tsx +++ b/src/__tests__/renderer/components/ShortcutEditor.test.tsx @@ -14,6 +14,7 @@ import type { Theme, Shortcut } from '../../../renderer/types'; // Mock the shortcutFormatter module vi.mock('../../../renderer/utils/shortcutFormatter', () => ({ formatShortcutKeys: vi.fn((keys: string[]) => keys.join('+')), + isMacOS: vi.fn(() => false), })); // Import after mock to get the mocked version diff --git a/src/__tests__/renderer/contexts/ToastContext.test.tsx b/src/__tests__/renderer/contexts/ToastContext.test.tsx index 1439da50..78c3a7e9 100644 --- a/src/__tests__/renderer/contexts/ToastContext.test.tsx +++ b/src/__tests__/renderer/contexts/ToastContext.test.tsx @@ -271,7 +271,7 @@ describe('ToastContext', () => { taskDuration: 5000, agentSessionId: 'test-session-id', tabName: 'TestTab', - audioNotification: { enabled: false }, + audioNotification: { enabled: false, reason: 'disabled' }, }); }); diff --git a/src/main/ipc/handlers/notifications.ts b/src/main/ipc/handlers/notifications.ts index 5df413cb..afcdedee 100644 --- a/src/main/ipc/handlers/notifications.ts +++ b/src/main/ipc/handlers/notifications.ts @@ -364,7 +364,10 @@ export function registerNotificationsHandlers(): void { async (_event, text: string, command?: string): Promise => { // Skip if there's no content to send if (!text || text.trim().length === 0) { - logger.debug('Notification skipped - no content to send', 'Notification'); + logger.info('Notification skipped - empty or whitespace-only content', 'Notification', { + textLength: text?.length ?? 0, + hasText: !!text, + }); return { success: true }; // Return success since there's nothing to do } diff --git a/src/prompts/tab-naming.md b/src/prompts/tab-naming.md index 5f5f4d92..4ac412b8 100644 --- a/src/prompts/tab-naming.md +++ b/src/prompts/tab-naming.md @@ -4,6 +4,7 @@ Rules: - 2-4 words max, shorter is better - Title Case, no punctuation - Be specific (mention tech/file names if relevant) +- Include ID numbers (PR #, issue #, ticket ID) when mentioned - Use hyphens for compound concepts - If too vague, use format: YYYY-MM-DD Topic @@ -13,4 +14,7 @@ Examples: "refactor DB queries for pooling" = DB Connection Pooling "write tests for checkout" = Checkout Tests "fix TS errors in parser.ts" = Parser TS Errors +"review PR #256" = PR 256 Review +"fix issue #42 memory leak" = Issue 42 Memory Leak +"implement JIRA-1234 feature" = JIRA-1234 Feature "help with my code" = 2024-01-15 Code Help diff --git a/src/renderer/components/GroupChatInput.tsx b/src/renderer/components/GroupChatInput.tsx index 0eaea199..8a4b5cc8 100644 --- a/src/renderer/components/GroupChatInput.tsx +++ b/src/renderer/components/GroupChatInput.tsx @@ -22,7 +22,7 @@ import type { QueuedItem, Shortcut, } from '../types'; -import { formatShortcutKeys } from '../utils/shortcutFormatter'; +import { formatShortcutKeys, isMacOS } from '../utils/shortcutFormatter'; import { QueuedItemsList } from './QueuedItemsList'; import { normalizeMentionName } from '../utils/participantColors'; @@ -549,10 +549,10 @@ export const GroupChatInput = React.memo(function GroupChatInput({ diff --git a/src/renderer/components/InlineWizard/WizardInputPanel.tsx b/src/renderer/components/InlineWizard/WizardInputPanel.tsx index 87d62a55..474d47c0 100644 --- a/src/renderer/components/InlineWizard/WizardInputPanel.tsx +++ b/src/renderer/components/InlineWizard/WizardInputPanel.tsx @@ -26,6 +26,7 @@ import type { Session, Theme } from '../../types'; import { WizardPill } from './WizardPill'; import { WizardConfidenceGauge } from './WizardConfidenceGauge'; import { WizardExitConfirmDialog } from './WizardExitConfirmDialog'; +import { isMacOS } from '../../utils/shortcutFormatter'; interface WizardInputPanelProps { /** Current session with wizard state */ @@ -322,10 +323,10 @@ export const WizardInputPanel = React.memo(function WizardInputPanel({ diff --git a/src/renderer/components/InputArea.tsx b/src/renderer/components/InputArea.tsx index a2265dd7..985e72dc 100644 --- a/src/renderer/components/InputArea.tsx +++ b/src/renderer/components/InputArea.tsx @@ -18,7 +18,7 @@ import { Pin, } from 'lucide-react'; import type { Session, Theme, BatchRunState, Shortcut, ThinkingMode } from '../types'; -import { formatShortcutKeys } from '../utils/shortcutFormatter'; +import { formatShortcutKeys, isMacOS } from '../utils/shortcutFormatter'; import type { TabCompletionSuggestion, TabCompletionFilter } from '../hooks'; import type { SummarizeProgress, @@ -1087,10 +1087,10 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) { diff --git a/src/renderer/components/NotificationsPanel.tsx b/src/renderer/components/NotificationsPanel.tsx index 7731d4ed..ae85beff 100644 --- a/src/renderer/components/NotificationsPanel.tsx +++ b/src/renderer/components/NotificationsPanel.tsx @@ -306,10 +306,6 @@ export function NotificationsPanel({
  • • When an AI task completes
  • • When a long-running command finishes
  • -
  • - • When the LLM analysis generates a feedback synopsis (custom notification only, if - configured) -
- {enterToSend ? 'Enter' : '⌘ + Enter'} + {enterToSend ? 'Enter' : isMacOS() ? '⌘ + Enter' : 'Ctrl + Enter'} )} diff --git a/src/renderer/components/SettingsModal.tsx b/src/renderer/components/SettingsModal.tsx index 89d79f8d..74a96277 100644 --- a/src/renderer/components/SettingsModal.tsx +++ b/src/renderer/components/SettingsModal.tsx @@ -49,7 +49,7 @@ import { MODAL_PRIORITIES } from '../constants/modalPriorities'; import { AICommandsPanel } from './AICommandsPanel'; import { SpecKitCommandsPanel } from './SpecKitCommandsPanel'; import { OpenSpecCommandsPanel } from './OpenSpecCommandsPanel'; -import { formatShortcutKeys } from '../utils/shortcutFormatter'; +import { formatShortcutKeys, isMacOS } from '../utils/shortcutFormatter'; import { ToggleButtonGroup } from './ToggleButtonGroup'; import { SettingCheckbox } from './SettingCheckbox'; import { FontConfigurationPanel } from './FontConfigurationPanel'; @@ -1302,8 +1302,8 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro Input Send Behavior

- Configure how to send messages in each mode. Choose between Enter or Command+Enter - for each input type. + Configure how to send messages in each mode. Choose between Enter or{' '} + {isMacOS() ? 'Command' : 'Ctrl'}+Enter for each input type.

{/* AI Mode Setting */} @@ -1324,13 +1324,13 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro border: `1px solid ${theme.colors.border}`, }} > - {props.enterToSendAI ? 'Enter' : '⌘ + Enter'} + {props.enterToSendAI ? 'Enter' : isMacOS() ? '⌘ + Enter' : 'Ctrl + Enter'}

{props.enterToSendAI ? 'Press Enter to send. Use Shift+Enter for new line.' - : 'Press Command+Enter to send. Enter creates new line.'} + : `Press ${isMacOS() ? 'Command' : 'Ctrl'}+Enter to send. Enter creates new line.`}

@@ -1352,13 +1352,13 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro border: `1px solid ${theme.colors.border}`, }} > - {props.enterToSendTerminal ? 'Enter' : '⌘ + Enter'} + {props.enterToSendTerminal ? 'Enter' : isMacOS() ? '⌘ + Enter' : 'Ctrl + Enter'}

{props.enterToSendTerminal ? 'Press Enter to send. Use Shift+Enter for new line.' - : 'Press Command+Enter to send. Enter creates new line.'} + : `Press ${isMacOS() ? 'Command' : 'Ctrl'}+Enter to send. Enter creates new line.`}

diff --git a/src/renderer/components/Wizard/screens/ConversationScreen.tsx b/src/renderer/components/Wizard/screens/ConversationScreen.tsx index 071e812a..d8cb4a37 100644 --- a/src/renderer/components/Wizard/screens/ConversationScreen.tsx +++ b/src/renderer/components/Wizard/screens/ConversationScreen.tsx @@ -34,6 +34,7 @@ import type { WizardError } from '../services/wizardErrorDetection'; import { AUTO_RUN_FOLDER_NAME, wizardDebugLogger } from '../services/phaseGenerator'; import { getNextFillerPhrase } from '../services/fillerPhrases'; import { ScreenReaderAnnouncement } from '../ScreenReaderAnnouncement'; +import { isMacOS } from '../../../utils/shortcutFormatter'; interface ConversationScreenProps { theme: Theme; @@ -1613,7 +1614,7 @@ export function ConversationScreen({ className="px-1.5 py-0.5 rounded text-xs" style={{ backgroundColor: theme.colors.border }} > - ⌘+Enter + {isMacOS() ? '⌘' : 'Ctrl'}+Enter Send diff --git a/src/renderer/contexts/ToastContext.tsx b/src/renderer/contexts/ToastContext.tsx index 8f7ca541..124bcfa2 100644 --- a/src/renderer/contexts/ToastContext.tsx +++ b/src/renderer/contexts/ToastContext.tsx @@ -98,7 +98,16 @@ export function ToastProvider({ // Capture audio feedback state for logging const { enabled: audioEnabled, command: audioCommand } = audioFeedbackRef.current; + // Determine if we have content to send for custom notification + // Also skip if there's no content to send + const hasContent = toast.message && toast.message.trim().length > 0; + + // Determine if custom notification will actually be triggered + const willTriggerCustomNotification = + audioEnabled && audioCommand && !toast.skipCustomNotification && hasContent; + // Log toast to system logs (include audio notification info) + // Only log enabled: true when we will actually trigger the notification window.maestro.logger.toast(toast.title, { type: toast.type, message: toast.message, @@ -107,23 +116,29 @@ export function ToastProvider({ taskDuration: toast.taskDuration, agentSessionId: toast.agentSessionId, tabName: toast.tabName, - // Audio/TTS notification info - audioNotification: - audioEnabled && audioCommand - ? { - enabled: true, - command: audioCommand, - } - : { - enabled: false, - }, + // Audio/TTS notification info - reflects whether notification WILL be triggered + audioNotification: willTriggerCustomNotification + ? { + enabled: true, + command: audioCommand, + } + : { + enabled: false, + reason: !audioEnabled + ? 'disabled' + : !audioCommand + ? 'no-command' + : toast.skipCustomNotification + ? 'opted-out' + : !hasContent + ? 'no-content' + : 'unknown', + }, }); // Run custom notification command if enabled and configured // Skip for toasts that explicitly opt out (e.g., synopsis messages) - // Also skip if there's no content to send - const hasContent = toast.message && toast.message.trim().length > 0; - if (audioEnabled && audioCommand && !toast.skipCustomNotification && hasContent) { + if (willTriggerCustomNotification) { console.log( '[ToastContext] Running custom notification with message:', toast.message,