Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,20 @@ function MaestroConsoleInner() {
);
}, []);

const handleTogglePauseQueuedItem = useCallback((itemId: string) => {
setSessions((prev) =>
prev.map((s) => {
if (s.id !== activeSessionIdRef.current) return s;
return {
...s,
executionQueue: s.executionQueue.map((item) =>
item.id === itemId ? { ...item, paused: !item.paused } : item
),
};
Comment on lines 1372 to +1384
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate toggle-pause implementation drifts from useQueueHandlers

App.tsx defines its own handleTogglePauseQueuedItem locally (used for the inline QueuedItemsList) while useQueueHandlers exports handleTogglePauseQueueItem (used for ExecutionQueueBrowser). Both do the same !item.paused flip, but through different code paths — one reading activeSessionIdRef.current, the other taking an explicit sessionId. If the logic ever needs to change (e.g., adding a "cannot pause the first non-paused item" guard), it must be updated in two places. Consider wiring the inline list through the hook handler by passing the active session ID, or exposing an ID-less variant from the hook.

})
);
}, []);

// toggleBookmark — provided by useSessionCrud hook

const handleFocusFileInGraph = useFileExplorerStore.getState().focusFileInGraph;
Expand Down Expand Up @@ -1964,8 +1978,12 @@ function MaestroConsoleInner() {
});

// Queue browser handlers — extracted to useQueueHandlers hook
const { handleRemoveQueueItem, handleSwitchQueueSession, handleReorderQueueItems } =
useQueueHandlers();
const {
handleRemoveQueueItem,
handleSwitchQueueSession,
handleReorderQueueItems,
handleTogglePauseQueueItem,
} = useQueueHandlers();

// Symphony contribution handler — extracted to useSymphonyContribution hook
const { handleStartContribution } = useSymphonyContribution({
Expand Down Expand Up @@ -2278,6 +2296,7 @@ function MaestroConsoleInner() {
handleStopBatchRun,
handleDeleteLog,
handleRemoveQueuedItem,
handleTogglePauseQueuedItem,
handleOpenQueueBrowser,

// Tab management handlers
Expand Down Expand Up @@ -2812,6 +2831,7 @@ function MaestroConsoleInner() {
onRemoveQueueItem={handleRemoveQueueItem}
onSwitchQueueSession={handleSwitchQueueSession}
onReorderQueueItems={handleReorderQueueItems}
onTogglePauseQueueItem={handleTogglePauseQueueItem}
// AppGroupChatModals props
onCloseNewGroupChatModal={handleCloseNewGroupChatModal}
onCreateGroupChat={handleCreateGroupChat}
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/components/AppModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ export interface AppUtilityModalsProps {
onRemoveQueueItem: (sessionId: string, itemId: string) => void;
onSwitchQueueSession: (sessionId: string) => void;
onReorderQueueItems: (sessionId: string, fromIndex: number, toIndex: number) => void;
onTogglePauseQueueItem: (sessionId: string, itemId: string) => void;
}

/**
Expand Down Expand Up @@ -1136,6 +1137,7 @@ export const AppUtilityModals = memo(function AppUtilityModals({
onRemoveQueueItem,
onSwitchQueueSession,
onReorderQueueItems,
onTogglePauseQueueItem,
}: AppUtilityModalsProps) {
return (
<>
Expand Down Expand Up @@ -1381,6 +1383,7 @@ export const AppUtilityModals = memo(function AppUtilityModals({
onRemoveItem={onRemoveQueueItem}
onSwitchSession={onSwitchQueueSession}
onReorderItems={onReorderQueueItems}
onTogglePauseItem={onTogglePauseQueueItem}
/>
)}
</>
Expand Down Expand Up @@ -2045,6 +2048,7 @@ export interface AppModalsProps {
onRemoveQueueItem: (sessionId: string, itemId: string) => void;
onSwitchQueueSession: (sessionId: string) => void;
onReorderQueueItems: (sessionId: string, fromIndex: number, toIndex: number) => void;
onTogglePauseQueueItem: (sessionId: string, itemId: string) => void;

// --- AppGroupChatModals props ---
onCloseNewGroupChatModal: () => void;
Expand Down Expand Up @@ -2404,6 +2408,7 @@ export const AppModals = memo(function AppModals(props: AppModalsProps) {
onRemoveQueueItem,
onSwitchQueueSession,
onReorderQueueItems,
onTogglePauseQueueItem,
// Group Chat modals
onCloseNewGroupChatModal,
onCreateGroupChat,
Expand Down Expand Up @@ -2715,6 +2720,7 @@ export const AppModals = memo(function AppModals(props: AppModalsProps) {
onRemoveQueueItem={onRemoveQueueItem}
onSwitchQueueSession={onSwitchQueueSession}
onReorderQueueItems={onReorderQueueItems}
onTogglePauseQueueItem={onTogglePauseQueueItem}
/>

{/* Group Chat Modals */}
Expand Down
56 changes: 54 additions & 2 deletions src/renderer/components/ExecutionQueueBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React, { useState, useEffect, useRef } from 'react';
import { X, MessageSquare, Command, Trash2, Clock, Folder, FolderOpen } from 'lucide-react';
import {
X,
MessageSquare,
Command,
Trash2,
Clock,
Folder,
FolderOpen,
Pause,
Play,
} from 'lucide-react';
import { useLayerStack } from '../contexts/LayerStackContext';
import { MODAL_PRIORITIES } from '../constants/modalPriorities';
import type { Session, Theme, QueuedItem } from '../types';
Expand All @@ -13,6 +23,7 @@ interface ExecutionQueueBrowserProps {
onRemoveItem: (sessionId: string, itemId: string) => void;
onSwitchSession: (sessionId: string) => void;
onReorderItems?: (sessionId: string, fromIndex: number, toIndex: number) => void;
onTogglePauseItem?: (sessionId: string, itemId: string) => void;
}

interface DragState {
Expand All @@ -39,6 +50,7 @@ export function ExecutionQueueBrowser({
onRemoveItem,
onSwitchSession,
onReorderItems,
onTogglePauseItem,
}: ExecutionQueueBrowserProps) {
const [viewMode, setViewMode] = useState<'current' | 'global'>('current');
const [dragState, setDragState] = useState<DragState | null>(null);
Expand Down Expand Up @@ -244,6 +256,11 @@ export function ExecutionQueueBrowser({
index={index}
theme={theme}
onRemove={() => onRemoveItem(session.id, item.id)}
onTogglePause={
onTogglePauseItem
? () => onTogglePauseItem(session.id, item.id)
: undefined
}
onSwitchToSession={() => {
onSwitchSession(session.id);
onClose();
Expand Down Expand Up @@ -314,6 +331,7 @@ interface QueueItemRowProps {
index: number;
theme: Theme;
onRemove: () => void;
onTogglePause?: () => void;
onSwitchToSession: () => void;
isDragging?: boolean;
canDrag?: boolean;
Expand All @@ -329,6 +347,7 @@ function QueueItemRow({
index,
theme,
onRemove,
onTogglePause,
onSwitchToSession,
isDragging,
canDrag,
Expand Down Expand Up @@ -440,7 +459,8 @@ function QueueItemRow({
// Visual states
const showDragReady = canDrag && isHovered && !isDragging && !isAnyDragging;
const showGrabbed = isPressed || isDragging;
const isDimmed = isAnyDragging && !isDragging;
const isPaused = !!item.paused;
const isDimmed = (isAnyDragging && !isDragging) || isPaused;

return (
<div
Expand Down Expand Up @@ -594,6 +614,38 @@ function QueueItemRow({
)}
</div>

{/* Pause/Resume + HELD badge */}
{onTogglePause && (
<button
onClick={(e) => {
e.stopPropagation();
onTogglePause();
}}
className={`p-1.5 rounded transition-all ${
isPaused ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
} hover:bg-black/20`}
style={{ color: isPaused ? theme.colors.warning : theme.colors.textDim }}
title={
isPaused
? 'Resume — let this message run when its turn comes up'
: 'Hold — keep this message in the queue but skip it during dispatch'
}
>
{isPaused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
</button>
)}
{isPaused && (
<span
className="px-1.5 py-0.5 self-start rounded text-[10px] font-bold tracking-wider"
style={{
backgroundColor: theme.colors.warning + '30',
color: theme.colors.warning,
}}
>
HELD
</span>
)}

{/* Remove button */}
<button
onClick={(e) => {
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/components/MainPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ interface MainPanelProps {
setActiveSessionId: (id: string) => void;
onDeleteLog?: (logId: string) => number | null;
onRemoveQueuedItem?: (itemId: string) => void;
onTogglePauseQueuedItem?: (itemId: string) => void;
onOpenQueueBrowser?: () => void;

// Auto mode props
Expand Down Expand Up @@ -381,6 +382,7 @@ export const MainPanel = React.memo(
currentSessionBatchState,
onStopBatchRun,
onRemoveQueuedItem,
onTogglePauseQueuedItem,
onOpenQueueBrowser,
isMobileLandscape = false,
showFlashNotification,
Expand Down Expand Up @@ -1685,6 +1687,7 @@ export const MainPanel = React.memo(
maxOutputLines={maxOutputLines}
onDeleteLog={props.onDeleteLog}
onRemoveQueuedItem={onRemoveQueuedItem}
onTogglePauseQueuedItem={onTogglePauseQueuedItem}
onInterrupt={handleInterrupt}
onScrollPositionChange={props.onScrollPositionChange}
onAtBottomChange={props.onAtBottomChange}
Expand Down
56 changes: 49 additions & 7 deletions src/renderer/components/QueuedItemsList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useCallback, useRef, memo } from 'react';
import { X, ChevronDown, ChevronUp, GripVertical } from 'lucide-react';
import { X, ChevronDown, ChevronUp, GripVertical, Pause, Play } from 'lucide-react';
import type { Theme, QueuedItem } from '../types';

// ============================================================================
Expand All @@ -11,6 +11,7 @@ interface QueuedItemsListProps {
theme: Theme;
onRemoveQueuedItem?: (itemId: string) => void;
onReorderItems?: (fromIndex: number, toIndex: number) => void;
onTogglePauseItem?: (itemId: string) => void;
activeTabId?: string; // If provided, only show queued items for this tab
}

Expand All @@ -29,6 +30,7 @@ export const QueuedItemsList = memo(
theme,
onRemoveQueuedItem,
onReorderItems,
onTogglePauseItem,
activeTabId,
}: QueuedItemsListProps) => {
// Filter to only show items for the active tab if activeTabId is provided
Expand Down Expand Up @@ -138,6 +140,7 @@ export const QueuedItemsList = memo(
const isQueuedExpanded = expandedQueuedMessages.has(item.id);
const isDragging = dragIndex === index;
const isDropTarget = dropIndex === index;
const isPaused = !!item.paused;

return (
<div
Expand All @@ -149,12 +152,19 @@ export const QueuedItemsList = memo(
onDragLeave={handleDragLeave}
className="mx-6 mb-2 p-3 rounded-lg relative group transition-all"
style={{
backgroundColor:
item.type === 'command'
backgroundColor: isPaused
? theme.colors.warning + '15'
: item.type === 'command'
? theme.colors.success + '20'
: theme.colors.accent + '20',
borderLeft: `3px solid ${item.type === 'command' ? theme.colors.success : theme.colors.accent}`,
opacity: isDragging ? 0.4 : 0.6,
borderLeft: `3px ${isPaused ? 'dashed' : 'solid'} ${
isPaused
? theme.colors.warning
: item.type === 'command'
? theme.colors.success
: theme.colors.accent
}`,
opacity: isDragging ? 0.4 : isPaused ? 0.45 : 0.6,
transform: isDropTarget ? 'translateY(4px)' : 'none',
boxShadow: isDropTarget ? `0 -2px 0 0 ${theme.colors.accent}` : 'none',
cursor: canDrag ? 'grab' : 'default',
Expand All @@ -180,9 +190,41 @@ export const QueuedItemsList = memo(
<X className="w-4 h-4" />
</button>

{/* Item content */}
{/* Pause/Resume button */}
{onTogglePauseItem && (
<button
onClick={() => onTogglePauseItem(item.id)}
className="absolute top-2 right-9 p-1 rounded hover:bg-black/20 transition-colors"
style={{ color: isPaused ? theme.colors.warning : theme.colors.textDim }}
title={
isPaused
? 'Resume — let this message run when its turn comes up'
: 'Hold — keep this message in the queue but skip it during dispatch'
}
>
{isPaused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
</button>
)}

{/* HELD badge for paused items */}
{isPaused && (
<div
className="absolute top-2 left-2 px-1.5 py-0.5 rounded text-[10px] font-bold tracking-wider"
style={{
backgroundColor: theme.colors.warning + '30',
color: theme.colors.warning,
}}
>
HELD
</div>
)}

{/* Item content — extra right padding to clear stacked X + Pause buttons,
and a top spacer when the HELD badge is shown */}
<div
className={`text-sm pr-8 whitespace-pre-wrap break-words ${canDrag ? 'pl-4' : ''}`}
className={`text-sm pr-16 whitespace-pre-wrap break-words ${canDrag ? 'pl-4' : ''} ${
isPaused ? 'pt-5' : ''
}`}
style={{ color: theme.colors.textMain }}
>
{item.type === 'command' && (
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/components/TerminalOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ interface TerminalOutputProps {
maxOutputLines: number;
onDeleteLog?: (logId: string) => number | null; // Returns the index to scroll to after deletion
onRemoveQueuedItem?: (itemId: string) => void; // Callback to remove a queued item from execution queue
onTogglePauseQueuedItem?: (itemId: string) => void; // Callback to toggle the held/paused state of a queued item
onInterrupt?: () => void; // Callback to interrupt the current process
Comment on lines 1057 to 1060
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include the new callback in the React.memo comparison.

TerminalOutput is memoized with a custom comparator, but onTogglePauseQueuedItem is not part of the equality check. If that handler reference changes, the queue UI can keep calling the stale callback.

🔧 Suggested fix
       prevProps.bionifyReadingMode === nextProps.bionifyReadingMode &&
       prevProps.bionifyIntensity === nextProps.bionifyIntensity &&
       prevProps.bionifyAlgorithm === nextProps.bionifyAlgorithm &&
+      prevProps.onTogglePauseQueuedItem === nextProps.onTogglePauseQueuedItem &&
       prevProps.fontFamily === nextProps.fontFamily &&

Also applies to: 1088-1121, 1844-1855

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TerminalOutput.tsx` around lines 1057 - 1060, The
memoized TerminalOutput component's custom comparator is missing the
onTogglePauseQueuedItem prop, so changes to that handler won't trigger
re-renders; update the equality check used in React.memo for TerminalOutput (the
comparator function that compares props like onRemoveQueuedItem, onDeleteLog,
onInterrupt, etc.) to also compare prevProps.onTogglePauseQueuedItem !==
nextProps.onTogglePauseQueuedItem (and mirror this change in the other
comparator occurrences mentioned around the other blocks), ensuring the memo
returns false when the toggle handler reference changes so the component
updates.

onScrollPositionChange?: (scrollTop: number) => void; // Callback to save scroll position
onAtBottomChange?: (isAtBottom: boolean) => void; // Callback when user scrolls to/away from bottom
Expand Down Expand Up @@ -1100,6 +1101,7 @@ export const TerminalOutput = memo(
maxOutputLines,
onDeleteLog,
onRemoveQueuedItem,
onTogglePauseQueuedItem,
onInterrupt: _onInterrupt,
onScrollPositionChange,
onAtBottomChange,
Expand Down Expand Up @@ -1847,6 +1849,7 @@ export const TerminalOutput = memo(
executionQueue={session.executionQueue}
theme={theme}
onRemoveQueuedItem={onRemoveQueuedItem}
onTogglePauseItem={onTogglePauseQueuedItem}
activeTabId={activeTabId || undefined}
/>
)}
Expand Down
Loading
Loading