+ This sends messages immediately, even when the agent is already working. If two
+ operations modify the same files simultaneously, one may overwrite the other's changes.
+
+
+ This is intended for advanced users who understand the risks. Use the assigned shortcut
+ key to force-send while the agent is busy. Regular send keys will continue to queue
+ normally.
+
+
+
+ When enabled, use{' '}
+
+ {shortcuts?.forcedParallelSend
+ ? formatShortcutKeys(shortcuts.forcedParallelSend.keys)
+ : '⌘ ⇧ ↩'}
+ {' '}
+ to send messages even while the agent is busy. Parallel writes to the same files may
+ cause one to overwrite the other.
+
+
+
+
+
{/* Default History Toggle */}
diff --git a/src/renderer/constants/modalPriorities.ts b/src/renderer/constants/modalPriorities.ts
index 4477432f7..49ba2fa78 100644
--- a/src/renderer/constants/modalPriorities.ts
+++ b/src/renderer/constants/modalPriorities.ts
@@ -29,6 +29,9 @@ export const MODAL_PRIORITIES = {
/** Agent error modal - critical, shows recovery options */
AGENT_ERROR: 1010,
+ /** Forced parallel execution warning - one-time acknowledgment */
+ FORCED_PARALLEL_WARNING: 1005,
+
/** Confirmation dialogs - highest priority, always on top */
CONFIRM: 1000,
diff --git a/src/renderer/constants/shortcuts.ts b/src/renderer/constants/shortcuts.ts
index 8b1c7ba2c..f1e85e03f 100644
--- a/src/renderer/constants/shortcuts.ts
+++ b/src/renderer/constants/shortcuts.ts
@@ -99,6 +99,11 @@ export const DEFAULT_SHORTCUTS: Record = {
label: 'Reset Font Size',
keys: ['Meta', 'Shift', '0'],
},
+ forcedParallelSend: {
+ id: 'forcedParallelSend',
+ label: 'Forced Parallel Send',
+ keys: ['Meta', 'Shift', 'Enter'],
+ },
};
// Non-editable shortcuts (displayed in help but not configurable)
diff --git a/src/renderer/hooks/input/useInputHandlers.ts b/src/renderer/hooks/input/useInputHandlers.ts
index b2a63c8ea..a33ff28b5 100644
--- a/src/renderer/hooks/input/useInputHandlers.ts
+++ b/src/renderer/hooks/input/useInputHandlers.ts
@@ -100,9 +100,11 @@ export interface UseInputHandlersReturn {
/** Set staged images for the current message */
setStagedImages: (images: string[] | ((prev: string[]) => string[])) => void;
/** Process and send the current input */
- processInput: (text?: string) => void;
+ processInput: (text?: string, options?: { forceParallel?: boolean }) => void;
/** Ref to latest processInput for use in memoized callbacks */
- processInputRef: React.MutableRefObject<(text?: string) => void>;
+ processInputRef: React.MutableRefObject<
+ (text?: string, options?: { forceParallel?: boolean }) => void
+ >;
/** Keyboard event handler for the input textarea */
handleInputKeyDown: (e: React.KeyboardEvent) => void;
/** Handler for input blur (persists input to session state) */
@@ -421,7 +423,9 @@ export function useInputHandlers(deps: UseInputHandlersDeps): UseInputHandlersRe
});
// processInputRef — maintained for access in memoized callbacks without stale closures
- const processInputRef = useRef<(text?: string) => void>(() => {});
+ const processInputRef = useRef<(text?: string, options?: { forceParallel?: boolean }) => void>(
+ () => {}
+ );
useEffect(() => {
processInputRef.current = processInput;
}, [processInput]);
diff --git a/src/renderer/hooks/input/useInputKeyDown.ts b/src/renderer/hooks/input/useInputKeyDown.ts
index e3c8fabf0..c09cedefd 100644
--- a/src/renderer/hooks/input/useInputKeyDown.ts
+++ b/src/renderer/hooks/input/useInputKeyDown.ts
@@ -40,7 +40,7 @@ export interface InputKeyDownDeps {
/** Sync file tree to highlight the tab completion suggestion */
syncFileTreeToTabCompletion: (suggestion: TabCompletionSuggestion | undefined) => void;
/** Process and send the current input */
- processInput: () => void;
+ processInput: (overrideInputValue?: string, options?: { forceParallel?: boolean }) => void;
/** Get tab completion suggestions for a given input */
getTabCompletionSuggestions: (input: string) => TabCompletionSuggestion[];
/** Ref to the input textarea */
@@ -237,6 +237,34 @@ export function useInputKeyDown(deps: InputKeyDownDeps): InputKeyDownReturn {
const enterToSendTerminal = settings.enterToSendTerminal;
if (e.key === 'Enter') {
+ // Check for forced parallel send shortcut (only in AI mode, only when feature enabled)
+ // Note: This check is inside the `e.key === 'Enter'` guard, so the shortcut's
+ // main key must be Enter. Non-Enter shortcuts are not supported by design.
+ if (settings.forcedParallelExecution && activeSession?.inputMode === 'ai') {
+ const shortcuts = settings.shortcuts;
+ const fpShortcut = shortcuts.forcedParallelSend;
+ if (fpShortcut) {
+ const fpKeys = fpShortcut.keys.map((k: string) => k.toLowerCase());
+ const fpNeedsMeta =
+ fpKeys.includes('meta') || fpKeys.includes('ctrl') || fpKeys.includes('command');
+ const fpNeedsShift = fpKeys.includes('shift');
+ const fpNeedsAlt = fpKeys.includes('alt');
+ const fpMainKey = fpKeys[fpKeys.length - 1];
+ const metaPressed = e.metaKey || e.ctrlKey;
+
+ if (
+ metaPressed === fpNeedsMeta &&
+ e.shiftKey === fpNeedsShift &&
+ e.altKey === fpNeedsAlt &&
+ e.key.toLowerCase() === fpMainKey
+ ) {
+ e.preventDefault();
+ processInput(undefined, { forceParallel: true });
+ return;
+ }
+ }
+ }
+
const currentEnterToSend =
activeSession?.inputMode === 'terminal' ? enterToSendTerminal : enterToSendAI;
diff --git a/src/renderer/hooks/input/useInputProcessing.ts b/src/renderer/hooks/input/useInputProcessing.ts
index 5b6a46a92..b8fc1522b 100644
--- a/src/renderer/hooks/input/useInputProcessing.ts
+++ b/src/renderer/hooks/input/useInputProcessing.ts
@@ -15,6 +15,7 @@ import { filterYoloArgs } from '../../utils/agentArgs';
import { hasCapabilityCached } from '../agent/useAgentCapabilities';
import { gitService } from '../../services/git';
import { imageOnlyDefaultPrompt, maestroSystemPrompt } from '../../../prompts';
+import { useSettingsStore } from '../../stores/settingsStore';
/**
* Default prompt used when user sends only an image without text.
@@ -89,9 +90,14 @@ export type BatchState = BatchRunState;
*/
export interface UseInputProcessingReturn {
/** Process the current input (send message or execute command) */
- processInput: (overrideInputValue?: string) => Promise;
+ processInput: (
+ overrideInputValue?: string,
+ options?: { forceParallel?: boolean }
+ ) => Promise;
/** Ref to processInput for use in callbacks that need latest version */
- processInputRef: React.MutableRefObject<((overrideInputValue?: string) => Promise) | null>;
+ processInputRef: React.MutableRefObject<
+ ((overrideInputValue?: string, options?: { forceParallel?: boolean }) => Promise) | null
+ >;
}
/**
@@ -137,13 +143,15 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
} = deps;
// Ref for the processInput function so external code can access the latest version
- const processInputRef = useRef<((overrideInputValue?: string) => Promise) | null>(null);
+ const processInputRef = useRef<
+ ((overrideInputValue?: string, options?: { forceParallel?: boolean }) => Promise) | null
+ >(null);
/**
* Process user input - handles slash commands, queuing, and message sending.
*/
const processInput = useCallback(
- async (overrideInputValue?: string) => {
+ async (overrideInputValue?: string, options?: { forceParallel?: boolean }) => {
// Flush any pending batched updates before processing user input
// This ensures AI output appears before the user's new message
flushBatchedUpdates?.();
@@ -415,14 +423,21 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
// so we need to explicitly check the batch state to prevent write conflicts
const isAutoRunActive = getBatchState(activeSession.id).isRunning;
+ // Forced parallel: user explicitly chose to bypass queue via modifier shortcut
+ const forceParallel =
+ options?.forceParallel === true && useSettingsStore.getState().forcedParallelExecution;
+
// Determine if we should queue this message
// Read-only tabs can run in parallel - only queue if this specific tab is busy
// Write mode tabs must wait for any busy tab to finish
// EXCEPTION: Write commands bypass queue when all running/queued items are read-only
// ALSO: Always queue write commands when AutoRun is active (to prevent file conflicts)
- const shouldQueue = isReadOnlyMode
- ? activeTab?.state === 'busy' // Read-only: only queue if THIS tab is busy
- : (activeSession.state === 'busy' && !canWriteBypassQueue()) || isAutoRunActive; // Write mode: queue if busy OR AutoRun active
+ // EXCEPTION: Forced parallel bypasses all queue logic (user explicitly chose to send immediately)
+ const shouldQueue = forceParallel
+ ? false
+ : isReadOnlyMode
+ ? activeTab?.state === 'busy' // Read-only: only queue if THIS tab is busy
+ : (activeSession.state === 'busy' && !canWriteBypassQueue()) || isAutoRunActive; // Write mode: queue if busy OR AutoRun active
// Debug logging to diagnose queue issues
console.log('[processInput] Queue decision:', {
@@ -431,6 +446,7 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
tabState: activeTab?.state,
isReadOnlyMode,
isAutoRunActive,
+ forceParallel,
shouldQueue,
queueLength: activeSession.executionQueue.length,
});
@@ -858,10 +874,15 @@ export function useInputProcessing(deps: UseInputProcessingDeps): UseInputProces
const targetPid = currentMode === 'ai' ? activeSession.aiPid : activeSession.terminalPid;
// For batch mode (Claude), include tab ID in session ID to prevent process collision
// This ensures each tab's process has a unique identifier
+ // For forced parallel sends, append a unique suffix so concurrent spawns from the same
+ // tab get distinct process keys and don't clobber each other's bookkeeping
const activeTabForSpawn = getActiveTab(activeSession);
+ const isForceParallel =
+ options?.forceParallel === true && useSettingsStore.getState().forcedParallelExecution;
+ const forceParallelSuffix = isForceParallel ? `-fp-${Date.now()}` : '';
const targetSessionId =
currentMode === 'ai'
- ? `${activeSession.id}-ai-${activeTabForSpawn?.id || 'default'}`
+ ? `${activeSession.id}-ai-${activeTabForSpawn?.id || 'default'}${forceParallelSuffix}`
: `${activeSession.id}-terminal`;
// Check if this is an AI agent in batch mode
diff --git a/src/renderer/hooks/settings/useSettings.ts b/src/renderer/hooks/settings/useSettings.ts
index d601a880b..53c8efec6 100644
--- a/src/renderer/hooks/settings/useSettings.ts
+++ b/src/renderer/hooks/settings/useSettings.ts
@@ -300,6 +300,12 @@ export interface UseSettingsReturn {
symphonyRegistryUrls: string[];
setSymphonyRegistryUrls: (value: string[]) => void;
+ // Forced Parallel Execution
+ forcedParallelExecution: boolean;
+ setForcedParallelExecution: (value: boolean) => void;
+ forcedParallelAcknowledged: boolean;
+ setForcedParallelAcknowledged: (value: boolean) => void;
+
// Director's Notes settings
directorNotesSettings: DirectorNotesSettings;
setDirectorNotesSettings: (value: DirectorNotesSettings) => void;
diff --git a/src/renderer/stores/settingsStore.ts b/src/renderer/stores/settingsStore.ts
index 2422325d9..2dabe9809 100644
--- a/src/renderer/stores/settingsStore.ts
+++ b/src/renderer/stores/settingsStore.ts
@@ -197,6 +197,8 @@ export interface SettingsStoreState {
customThemeBaseId: ThemeId;
enterToSendAI: boolean;
enterToSendTerminal: boolean;
+ forcedParallelExecution: boolean;
+ forcedParallelAcknowledged: boolean;
defaultSaveToHistory: boolean;
defaultShowThinking: ThinkingMode;
leftSidebarWidth: number;
@@ -283,6 +285,8 @@ export interface SettingsStoreActions {
setCustomThemeBaseId: (value: ThemeId) => void;
setEnterToSendAI: (value: boolean) => void;
setEnterToSendTerminal: (value: boolean) => void;
+ setForcedParallelExecution: (value: boolean) => void;
+ setForcedParallelAcknowledged: (value: boolean) => void;
setDefaultSaveToHistory: (value: boolean) => void;
setDefaultShowThinking: (value: ThinkingMode) => void;
setLeftSidebarWidth: (value: number) => void;
@@ -431,6 +435,8 @@ export const useSettingsStore = create()((set, get) => {
customThemeBaseId: 'dracula',
enterToSendAI: false,
enterToSendTerminal: true,
+ forcedParallelExecution: false,
+ forcedParallelAcknowledged: false,
defaultSaveToHistory: true,
defaultShowThinking: 'off',
leftSidebarWidth: 256,
@@ -583,6 +589,16 @@ export const useSettingsStore = create()((set, get) => {
window.maestro.settings.set('enterToSendTerminal', value);
},
+ setForcedParallelExecution: (value) => {
+ set({ forcedParallelExecution: value });
+ window.maestro.settings.set('forcedParallelExecution', value);
+ },
+
+ setForcedParallelAcknowledged: (value) => {
+ set({ forcedParallelAcknowledged: value });
+ window.maestro.settings.set('forcedParallelAcknowledged', value);
+ },
+
setDefaultSaveToHistory: (value) => {
set({ defaultSaveToHistory: value });
window.maestro.settings.set('defaultSaveToHistory', value);
@@ -1492,6 +1508,11 @@ export async function loadAllSettings(): Promise {
if (allSettings['enterToSendTerminal'] !== undefined)
patch.enterToSendTerminal = allSettings['enterToSendTerminal'] as boolean;
+ if (allSettings['forcedParallelExecution'] !== undefined)
+ patch.forcedParallelExecution = allSettings['forcedParallelExecution'] as boolean;
+ if (allSettings['forcedParallelAcknowledged'] !== undefined)
+ patch.forcedParallelAcknowledged = allSettings['forcedParallelAcknowledged'] as boolean;
+
if (allSettings['defaultSaveToHistory'] !== undefined)
patch.defaultSaveToHistory = allSettings['defaultSaveToHistory'] as boolean;
@@ -1906,6 +1927,8 @@ export function getSettingsActions() {
setCustomThemeBaseId: state.setCustomThemeBaseId,
setEnterToSendAI: state.setEnterToSendAI,
setEnterToSendTerminal: state.setEnterToSendTerminal,
+ setForcedParallelExecution: state.setForcedParallelExecution,
+ setForcedParallelAcknowledged: state.setForcedParallelAcknowledged,
setDefaultSaveToHistory: state.setDefaultSaveToHistory,
setDefaultShowThinking: state.setDefaultShowThinking,
setLeftSidebarWidth: state.setLeftSidebarWidth,