diff --git a/docs/configuration.md b/docs/configuration.md index 4c5c17ab..a3231eb1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -76,20 +76,23 @@ Enable desktop notifications to be alerted when: 1. Toggle **Enable OS Notifications** on 2. Click **Test Notification** to verify it works -### Audio Feedback (Text-to-Speech) +### Custom Notification -Maestro can speak a brief summary when AI tasks complete using your system's text-to-speech. +Execute a custom command when AI tasks complete. By default, this uses text-to-speech (TTS), but you can leverage any notification stack you prefer. **To configure:** -1. Toggle **Enable Audio Feedback** on -2. Set the **TTS Command** — the command that accepts text via stdin: - - **macOS:** `say` (built-in) - - **Linux:** `espeak` or `festival --tts` +1. Toggle **Enable Custom Notification** on +2. Set the **Command Chain** — the command(s) that accept text via stdin: + - **TTS (default):** `say` (macOS), `espeak` or `festival --tts` (Linux) - **Windows:** Use a PowerShell script or third-party TTS tool -3. Click **Test** to hear a sample message + - **Custom:** Any command or script that accepts stdin +3. Click **Test** to verify your command works 4. Click **Stop** to interrupt a running test -**Piped commands:** You can pipe through multiple commands, e.g., `cmd1 | cmd2`. +**Command chaining:** Chain multiple commands together using pipes to mix and match tools. Examples: +- `say` — speak aloud using macOS TTS +- `tee ~/log.txt | say` — log to a file AND speak aloud +- `notify-send "Maestro" && espeak` — show desktop notification and speak (Linux) ### Toast Notifications @@ -104,9 +107,9 @@ In-app toast notifications appear in the corner when events occur. Configure how ### When Notifications Trigger Notifications are sent when: -- An AI task completes (OS notification + optional TTS) +- An AI task completes (OS notification + optional custom notification) - A long-running command finishes (OS notification) -- The LLM analysis generates a feedback synopsis (TTS only, if configured) +- The LLM analysis generates a feedback synopsis (custom notification only, if configured) ## Sleep Prevention diff --git a/src/__tests__/renderer/components/SettingsModal.test.tsx b/src/__tests__/renderer/components/SettingsModal.test.tsx index 9d47e7c3..7e1dfc92 100644 --- a/src/__tests__/renderer/components/SettingsModal.test.tsx +++ b/src/__tests__/renderer/components/SettingsModal.test.tsx @@ -1148,14 +1148,14 @@ describe('SettingsModal', () => { ); }); - it('should display audio feedback setting', async () => { + it('should display custom notification setting', async () => { render(); await act(async () => { await vi.advanceTimersByTimeAsync(100); }); - expect(screen.getByText('Enable Audio Feedback')).toBeInTheDocument(); + expect(screen.getByText('Enable Custom Notification')).toBeInTheDocument(); }); it('should call setAudioFeedbackEnabled when toggle switch is changed', async () => { @@ -1175,7 +1175,7 @@ describe('SettingsModal', () => { }); // SettingCheckbox uses a button with role="switch" instead of input[type="checkbox"] - const titleElement = screen.getByText('Enable Audio Feedback'); + const titleElement = screen.getByText('Enable Custom Notification'); const toggleContainer = titleElement.closest('[role="button"]'); const toggleSwitch = toggleContainer?.querySelector('button[role="switch"]'); fireEvent.click(toggleSwitch!); @@ -1183,7 +1183,7 @@ describe('SettingsModal', () => { expect(setAudioFeedbackEnabled).toHaveBeenCalledWith(true); }); - it('should call setAudioFeedbackCommand when TTS command is changed', async () => { + it('should call setAudioFeedbackCommand when Command Chain is changed', async () => { const setAudioFeedbackCommand = vi.fn(); render( { expect(setAudioFeedbackCommand).toHaveBeenCalledWith('espeak'); }); - it('should test TTS when test button is clicked', async () => { + it('should test Command Chain when test button is clicked', async () => { render(); await act(async () => { @@ -1520,8 +1520,8 @@ describe('SettingsModal', () => { }); }); - describe('TTS Stop button', () => { - it('should show Stop button when TTS is playing and handle click', async () => { + describe('Custom notification Stop button', () => { + it('should show Stop button when Command Chain is running and handle click', async () => { // Mock speak to return a ttsId vi.mocked(window.maestro.notification.speak).mockResolvedValue({ success: true, ttsId: 123 }); vi.mocked(window.maestro.notification.stopSpeak).mockResolvedValue({ success: true }); @@ -1532,7 +1532,7 @@ describe('SettingsModal', () => { await vi.advanceTimersByTimeAsync(100); }); - // Click Test button to start TTS + // Click Test button to start Command Chain fireEvent.click(screen.getByRole('button', { name: 'Test' })); await act(async () => { @@ -1564,7 +1564,7 @@ describe('SettingsModal', () => { await vi.advanceTimersByTimeAsync(100); }); - // Click Test button to start TTS + // Click Test button to start Command Chain fireEvent.click(screen.getByRole('button', { name: 'Test' })); await act(async () => { @@ -1604,7 +1604,7 @@ describe('SettingsModal', () => { consoleSpy.mockRestore(); }); - it('should auto-clear TTS state after timeout', async () => { + it('should auto-clear Command Chain state after timeout', async () => { vi.mocked(window.maestro.notification.speak).mockResolvedValue({ success: true, ttsId: 789 }); render(); @@ -1613,7 +1613,7 @@ describe('SettingsModal', () => { await vi.advanceTimersByTimeAsync(100); }); - // Click Test button to start TTS + // Click Test button to start Command Chain fireEvent.click(screen.getByRole('button', { name: 'Test' })); await act(async () => { diff --git a/src/__tests__/renderer/components/TerminalOutput.test.tsx b/src/__tests__/renderer/components/TerminalOutput.test.tsx index 35a0ed66..938a24cf 100644 --- a/src/__tests__/renderer/components/TerminalOutput.test.tsx +++ b/src/__tests__/renderer/components/TerminalOutput.test.tsx @@ -1018,7 +1018,7 @@ describe('TerminalOutput', () => { }); }); - describe('TTS functionality', () => { + describe('Custom notification functionality', () => { it('shows speak button when audioFeedbackCommand is provided', () => { const logs: LogEntry[] = [createLogEntry({ text: 'Text to speak', source: 'stdout' })]; diff --git a/src/renderer/components/NotificationsPanel.tsx b/src/renderer/components/NotificationsPanel.tsx index 603c2dc1..e8164cbd 100644 --- a/src/renderer/components/NotificationsPanel.tsx +++ b/src/renderer/components/NotificationsPanel.tsx @@ -61,21 +61,21 @@ export function NotificationsPanel({ - {/* Audio Feedback */} + {/* Custom Notification */}
- {/* Audio Command Configuration */} + {/* Command Chain Configuration */}
- +

- Command that accepts text via stdin. Pipes are supported (e.g.,{' '} + Command that accepts text via stdin. Chain multiple commands using pipes (e.g.,{' '} cmd1 | cmd2 - ). Examples:{' '} + ) to mix and match tools. Default TTS examples:{' '} festival --tts + . You can also use non-TTS commands or combine them, e.g.,{' '} + + tee ~/log.txt | say + {' '} + to log and speak simultaneously.

@@ -208,8 +216,19 @@ export function NotificationsPanel({
  • • When an AI task completes
  • • When a long-running command finishes
  • -
  • • When the LLM analysis generates a feedback synopsis (audio only, if configured)
  • +
  • + • When the LLM analysis generates a feedback synopsis (custom notification only, if + configured) +
+
+ Tip: The default Command Chain uses TTS (text-to-speech), but you can + leverage any notification stack you prefer. Chain commands together with pipes to mix and + match—for example, log to a file while also speaking aloud. +
);