mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
feat(notifications): rename Audio Feedback to Custom Notification
Rename "Audio Feedback" → "Custom Notification" and "TTS Command" → "Command Chain" to better reflect the flexible nature of this feature. Users can chain commands together with pipes to mix and match notification tools (TTS, logging, desktop notifications, etc.). Closes #168
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -1148,14 +1148,14 @@ describe('SettingsModal', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should display audio feedback setting', async () => {
|
||||
it('should display custom notification setting', async () => {
|
||||
render(<SettingsModal {...createDefaultProps({ initialTab: 'notifications' })} />);
|
||||
|
||||
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(
|
||||
<SettingsModal
|
||||
@@ -1201,7 +1201,7 @@ describe('SettingsModal', () => {
|
||||
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(<SettingsModal {...createDefaultProps({ initialTab: 'notifications' })} />);
|
||||
|
||||
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(<SettingsModal {...createDefaultProps({ initialTab: 'notifications' })} />);
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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' })];
|
||||
|
||||
|
||||
@@ -61,21 +61,21 @@ export function NotificationsPanel({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Audio Feedback */}
|
||||
{/* Custom Notification */}
|
||||
<div>
|
||||
<SettingCheckbox
|
||||
icon={Volume2}
|
||||
sectionLabel="Audio Feedback"
|
||||
title="Enable Audio Feedback"
|
||||
description="Speak the one-sentence feedback synopsis from LLM analysis using text-to-speech"
|
||||
sectionLabel="Custom Notification"
|
||||
title="Enable Custom Notification"
|
||||
description="Execute a custom command when AI tasks complete, such as text-to-speech feedback"
|
||||
checked={audioFeedbackEnabled}
|
||||
onChange={setAudioFeedbackEnabled}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
{/* Audio Command Configuration */}
|
||||
{/* Command Chain Configuration */}
|
||||
<div className="mt-3">
|
||||
<label className="block text-xs font-medium opacity-70 mb-1">TTS Command</label>
|
||||
<label className="block text-xs font-medium opacity-70 mb-1">Command Chain</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -137,14 +137,14 @@ export function NotificationsPanel({
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs opacity-50 mt-2" style={{ color: theme.colors.textDim }}>
|
||||
Command that accepts text via stdin. Pipes are supported (e.g.,{' '}
|
||||
Command that accepts text via stdin. Chain multiple commands using pipes (e.g.,{' '}
|
||||
<code
|
||||
className="px-1 py-0.5 rounded"
|
||||
style={{ backgroundColor: theme.colors.bgActivity }}
|
||||
>
|
||||
cmd1 | cmd2
|
||||
</code>
|
||||
). Examples:{' '}
|
||||
) to mix and match tools. Default TTS examples:{' '}
|
||||
<code
|
||||
className="px-1 py-0.5 rounded"
|
||||
style={{ backgroundColor: theme.colors.bgActivity }}
|
||||
@@ -165,6 +165,14 @@ export function NotificationsPanel({
|
||||
>
|
||||
festival --tts
|
||||
</code>
|
||||
. You can also use non-TTS commands or combine them, e.g.,{' '}
|
||||
<code
|
||||
className="px-1 py-0.5 rounded"
|
||||
style={{ backgroundColor: theme.colors.bgActivity }}
|
||||
>
|
||||
tee ~/log.txt | say
|
||||
</code>{' '}
|
||||
to log and speak simultaneously.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,8 +216,19 @@ export function NotificationsPanel({
|
||||
<ul className="text-xs opacity-70 space-y-1" style={{ color: theme.colors.textDim }}>
|
||||
<li>• When an AI task completes</li>
|
||||
<li>• When a long-running command finishes</li>
|
||||
<li>• When the LLM analysis generates a feedback synopsis (audio only, if configured)</li>
|
||||
<li>
|
||||
• When the LLM analysis generates a feedback synopsis (custom notification only, if
|
||||
configured)
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
className="text-xs opacity-60 mt-3 pt-3"
|
||||
style={{ color: theme.colors.textDim, borderTop: `1px solid ${theme.colors.border}` }}
|
||||
>
|
||||
<strong>Tip:</strong> 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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user