## CHANGES

- Added Sentry crash reporting for error tracking and debugging 🐛
- Implemented opt-out privacy setting for anonymous crash reports 🔒
- Enhanced agent spawning with generic config options for session continuity 🔧
- Fixed tab creation null safety checks across the codebase 🛡️
- Added Edit Agent action to Quick Actions modal for faster access 
- Enabled bookmark toggle directly from Quick Actions menu 📌
- Improved batch processing to run in read-only mode by default 📝
- Cleaned up queued items display by removing redundant tab indicators 🧹
- Strengthened null checks in tab helper functions for stability 💪
- Updated version to 0.9.1 with comprehensive bug fixes and improvements 🚀
This commit is contained in:
Pedram Amini
2025-12-17 23:22:02 -06:00
parent 76d74623c4
commit 416f373f63
17 changed files with 1075 additions and 75 deletions

912
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "maestro",
"version": "0.9.0",
"version": "0.9.1",
"description": "Run AI coding agents autonomously for days.",
"main": "dist/main/index.js",
"author": {
@@ -150,6 +150,7 @@
"@fastify/rate-limit": "^9.1.0",
"@fastify/static": "^7.0.4",
"@fastify/websocket": "^9.0.0",
"@sentry/electron": "^7.5.0",
"@tanstack/react-virtual": "^3.13.13",
"@types/dompurify": "^3.0.5",
"adm-zip": "^0.5.16",

View File

@@ -785,10 +785,12 @@ describe('QuickActionsModal', () => {
});
render(<QuickActionsModal {...props} />);
// Filter to just sessions so we can reliably test Cmd+0
const input = screen.getByPlaceholderText('Type a command or jump to agent...');
fireEvent.change(input, { target: { value: 'Session' } });
fireEvent.keyDown(input, { key: '0', metaKey: true });
// Should trigger the 10th item
// Should trigger the 10th session (Session 9 due to alphabetical sorting)
expect(props.setActiveSessionId).toHaveBeenCalled();
});
});

View File

@@ -62,6 +62,7 @@ const AGENT_DEFINITIONS: Omit<AgentConfig, 'available' | 'path' | 'capabilities'
// YOLO mode (--dangerously-skip-permissions) is always enabled - Maestro requires it
args: ['--print', '--verbose', '--output-format', 'stream-json', '--dangerously-skip-permissions'],
resumeArgs: (sessionId: string) => ['--resume', sessionId], // Resume with session ID
readOnlyArgs: ['--permission-mode', 'plan'], // Read-only/plan mode
},
{
id: 'codex',

View File

@@ -3,6 +3,7 @@ import path from 'path';
import os from 'os';
import fs from 'fs/promises';
import fsSync from 'fs';
import * as Sentry from '@sentry/electron/main';
import { ProcessManager } from './process-manager';
import { WebServer } from './web-server';
import { AgentDetector } from './agent-detector';
@@ -17,6 +18,32 @@ import { initializeOutputParsers } from './parsers';
import { DEMO_MODE, DEMO_DATA_PATH } from './constants';
import { initAutoUpdater } from './auto-updater';
// Initialize Sentry for crash reporting (before app.ready)
// Check if crash reporting is enabled (default: true for opt-out behavior)
const crashReportingStore = new Store<{ crashReportingEnabled: boolean }>({
name: 'maestro-settings',
});
const crashReportingEnabled = crashReportingStore.get('crashReportingEnabled', true);
if (crashReportingEnabled) {
Sentry.init({
dsn: 'https://2303c5f787f910863d83ed5d27ce8ed2@o4510554134740992.ingest.us.sentry.io/4510554135789568',
// Set release version for better debugging
release: app.getVersion(),
// Only send errors, not performance data
tracesSampleRate: 0,
// Filter out sensitive data
beforeSend(event) {
// Remove any potential sensitive data from the event
if (event.user) {
delete event.user.ip_address;
delete event.user.email;
}
return event;
},
});
}
// Demo mode: use a separate data directory for fresh demos
if (DEMO_MODE) {
app.setPath('userData', DEMO_DATA_PATH);

View File

@@ -94,7 +94,10 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
agentId: agent?.id,
agentCommand: agent?.command,
agentPath: agent?.path,
hasAgentSessionId: !!config.agentSessionId
hasAgentSessionId: !!config.agentSessionId,
hasPrompt: !!config.prompt,
promptLength: config.prompt?.length,
promptValue: config.prompt,
});
let finalArgs = [...config.args];

View File

@@ -179,6 +179,7 @@ export default function MaestroConsole() {
audioFeedbackCommand, setAudioFeedbackCommand,
toastDuration, setToastDuration,
checkForUpdatesOnStartup, setCheckForUpdatesOnStartup,
crashReportingEnabled, setCrashReportingEnabled,
shortcuts, setShortcuts,
customAICommands, setCustomAICommands,
globalStats, updateGlobalStats,
@@ -1671,12 +1672,9 @@ export default function MaestroConsole() {
// Create a new tab in the session to start fresh
setSessions(prev => prev.map(s => {
if (s.id !== sessionId) return s;
const newTab = createTab();
return {
...s,
aiTabs: [...s.aiTabs, newTab],
activeTabId: newTab.id,
};
const result = createTab(s);
if (!result) return s;
return result.session;
}));
// Focus the input after creating new tab
@@ -3434,8 +3432,8 @@ export default function MaestroConsole() {
return;
}
// Build spawn args with resume if we have an agent session ID
// Use the ACTIVE TAB's agentSessionId (not the deprecated session-level one)
// Get the ACTIVE TAB's agentSessionId for session continuity
// (not the deprecated session-level one)
const activeTab = getActiveTab(session);
const tabAgentSessionId = activeTab?.agentSessionId;
const isReadOnly = activeTab?.readOnlyMode;
@@ -3451,14 +3449,9 @@ export default function MaestroConsole() {
)
: [...agent.args];
if (tabAgentSessionId) {
spawnArgs.push('--resume', tabAgentSessionId);
}
// Add read-only/plan mode if the active tab has readOnlyMode enabled
if (isReadOnly) {
spawnArgs.push('--permission-mode', 'plan');
}
// Note: agentSessionId and readOnlyMode are passed to spawn() config below.
// The main process uses agent-specific argument builders (resumeArgs, readOnlyArgs)
// to construct the correct CLI args for each agent type.
// Include tab ID in targetSessionId for proper output routing
const targetSessionId = `${sessionId}-ai-${activeTab?.id || 'default'}`;
@@ -3526,7 +3519,10 @@ export default function MaestroConsole() {
cwd: session.cwd,
command: commandToUse,
args: spawnArgs,
prompt: promptToSend
prompt: promptToSend,
// Generic spawn options - main process builds agent-specific args
agentSessionId: tabAgentSessionId,
readOnlyMode: isReadOnly,
});
console.log(`[Remote] ${session.toolType} spawn initiated successfully`);
@@ -3618,8 +3614,8 @@ export default function MaestroConsole() {
const agent = await window.maestro.agents.get(session.toolType);
if (!agent) throw new Error(`Agent not found for toolType: ${session.toolType}`);
// Build spawn args with resume if we have a session ID
// Use the TARGET TAB's agentSessionId (not the active tab or deprecated session-level one)
// Get the TARGET TAB's agentSessionId for session continuity
// (not the active tab or deprecated session-level one)
const tabAgentSessionId = targetTab?.agentSessionId;
const isReadOnly = item.readOnlyMode || targetTab?.readOnlyMode;
@@ -3634,15 +3630,9 @@ export default function MaestroConsole() {
)
: [...(agent.args || [])];
if (tabAgentSessionId) {
spawnArgs.push('--resume', tabAgentSessionId);
}
// Add read-only/plan mode if the queued item was from a read-only tab
// or if the target tab currently has readOnlyMode enabled
if (isReadOnly) {
spawnArgs.push('--permission-mode', 'plan');
}
// Note: agentSessionId and readOnlyMode are passed to spawn() config below.
// The main process uses agent-specific argument builders (resumeArgs, readOnlyArgs)
// to construct the correct CLI args for each agent type.
const commandToUse = agent.path || agent.command;
@@ -3656,6 +3646,18 @@ export default function MaestroConsole() {
// If user sends only an image without text, inject the default image-only prompt
const effectivePrompt = isImageOnlyMessage ? DEFAULT_IMAGE_ONLY_PROMPT : item.text!;
console.log('[processQueuedItem] Spawning agent with queued message:', {
sessionId: targetSessionId,
toolType: session.toolType,
prompt: effectivePrompt,
promptLength: effectivePrompt?.length,
hasAgentSessionId: !!tabAgentSessionId,
agentSessionId: tabAgentSessionId,
isReadOnly,
argsLength: spawnArgs.length,
args: spawnArgs,
});
await window.maestro.process.spawn({
sessionId: targetSessionId,
toolType: session.toolType,
@@ -3663,7 +3665,10 @@ export default function MaestroConsole() {
command: commandToUse,
args: spawnArgs,
prompt: effectivePrompt,
images: hasImages ? item.images : undefined
images: hasImages ? item.images : undefined,
// Generic spawn options - main process builds agent-specific args
agentSessionId: tabAgentSessionId,
readOnlyMode: isReadOnly,
});
} else if (item.type === 'command' && item.command) {
// Process a slash command - find the matching custom AI command
@@ -3711,7 +3716,10 @@ export default function MaestroConsole() {
cwd: session.cwd,
command: commandToUse,
args: spawnArgs,
prompt: substitutedPrompt
prompt: substitutedPrompt,
// Generic spawn options - main process builds agent-specific args
agentSessionId: tabAgentSessionId,
readOnlyMode: isReadOnly,
});
} else {
// Unknown command - add error log
@@ -4775,6 +4783,10 @@ export default function MaestroConsole() {
setTourOpen(true);
}}
setFuzzyFileSearchOpen={setFuzzyFileSearchOpen}
onEditAgent={(session) => {
setEditAgentSession(session);
setEditAgentModalOpen(true);
}}
/>
)}
{lightboxImage && (
@@ -5179,6 +5191,7 @@ export default function MaestroConsole() {
setSessions(prev => prev.map(s => {
if (s.id !== activeSession.id) return s;
const result = createTab(s, { saveToHistory: defaultSaveToHistory });
if (!result) return s;
return result.session;
}));
setActiveAgentSessionId(null);
@@ -5377,6 +5390,7 @@ export default function MaestroConsole() {
setSessions(prev => prev.map(s => {
if (s.id !== activeSession.id) return s;
const result = createTab(s, { saveToHistory: defaultSaveToHistory });
if (!result) return s;
return result.session;
}));
}}
@@ -5963,6 +5977,8 @@ export default function MaestroConsole() {
setToastDuration={setToastDuration}
checkForUpdatesOnStartup={checkForUpdatesOnStartup}
setCheckForUpdatesOnStartup={setCheckForUpdatesOnStartup}
crashReportingEnabled={crashReportingEnabled}
setCrashReportingEnabled={setCrashReportingEnabled}
customAICommands={customAICommands}
setCustomAICommands={setCustomAICommands}
initialTab={settingsTab}

View File

@@ -16,7 +16,7 @@ interface QueuedItemsListProps {
/**
* QueuedItemsList displays the execution queue with:
* - Queued message separator with count
* - Individual queued items (commands/messages) with tab indicators
* - Individual queued items (commands/messages)
* - Long message expand/collapse functionality
* - Image attachment indicators
* - Remove button with confirmation modal
@@ -117,16 +117,6 @@ export const QueuedItemsList = memo(({
<X className="w-4 h-4" />
</button>
{/* Tab indicator */}
{item.tabName && (
<div
className="text-xs mb-1 font-mono"
style={{ color: theme.colors.textDim }}
>
{item.tabName}
</div>
)}
{/* Item content */}
<div
className="text-sm pr-8 whitespace-pre-wrap break-words"

View File

@@ -68,6 +68,7 @@ interface QuickActionsModalProps {
setDebugWizardModalOpen?: (open: boolean) => void;
startTour?: () => void;
setFuzzyFileSearchOpen?: (open: boolean) => void;
onEditAgent?: (session: Session) => void;
}
export function QuickActionsModal(props: QuickActionsModalProps) {
@@ -82,7 +83,7 @@ export function QuickActionsModal(props: QuickActionsModalProps) {
setShortcutsHelpOpen, setAboutModalOpen, setLogViewerOpen, setProcessMonitorOpen,
setAgentSessionsOpen, setActiveAgentSessionId, setGitDiffPreview, setGitLogOpen,
onRenameTab, onToggleReadOnlyMode, onOpenTabSwitcher, tabShortcuts, isAiMode, setPlaygroundOpen, onRefreshGitFileState,
onDebugReleaseQueuedItem, markdownEditMode, onToggleMarkdownEditMode, setUpdateCheckModalOpen, openWizard, wizardGoToStep, setDebugWizardModalOpen, startTour, setFuzzyFileSearchOpen
onDebugReleaseQueuedItem, markdownEditMode, onToggleMarkdownEditMode, setUpdateCheckModalOpen, openWizard, wizardGoToStep, setDebugWizardModalOpen, startTour, setFuzzyFileSearchOpen, onEditAgent
} = props;
const [search, setSearch] = useState('');
@@ -204,6 +205,16 @@ export function QuickActionsModal(props: QuickActionsModalProps) {
setRenameInstanceModalOpen(true);
setQuickActionOpen(false);
} }] : []),
...(activeSession && onEditAgent ? [{ id: 'editAgent', label: `Edit Agent: ${activeSession.name}`, action: () => {
onEditAgent(activeSession);
setQuickActionOpen(false);
} }] : []),
...(activeSession ? [{ id: 'toggleBookmark', label: activeSession.bookmarked ? `Unbookmark: ${activeSession.name}` : `Bookmark: ${activeSession.name}`, action: () => {
setSessions(prev => prev.map(s =>
s.id === activeSessionId ? { ...s, bookmarked: !s.bookmarked } : s
));
setQuickActionOpen(false);
} }] : []),
...(activeSession?.groupId ? [{
id: 'renameGroup',
label: 'Rename Group',

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, memo } from 'react';
import { X, Key, Moon, Sun, Keyboard, Check, Terminal, Bell, Cpu, Settings, Palette, Sparkles, History, Download } from 'lucide-react';
import { X, Key, Moon, Sun, Keyboard, Check, Terminal, Bell, Cpu, Settings, Palette, Sparkles, History, Download, Bug } from 'lucide-react';
import type { AgentConfig, Theme, ThemeColors, ThemeId, Shortcut, ShellInfo, CustomAICommand } from '../types';
import { CustomThemeBuilder } from './CustomThemeBuilder';
import { useLayerStack } from '../contexts/LayerStackContext';
@@ -70,6 +70,8 @@ interface SettingsModalProps {
setToastDuration: (value: number) => void;
checkForUpdatesOnStartup: boolean;
setCheckForUpdatesOnStartup: (value: boolean) => void;
crashReportingEnabled: boolean;
setCrashReportingEnabled: (value: boolean) => void;
customAICommands: CustomAICommand[];
setCustomAICommands: (commands: CustomAICommand[]) => void;
initialTab?: 'general' | 'llm' | 'shortcuts' | 'theme' | 'notifications' | 'aicommands';
@@ -871,6 +873,17 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
onChange={props.setCheckForUpdatesOnStartup}
theme={theme}
/>
{/* Crash Reporting */}
<SettingCheckbox
icon={Bug}
sectionLabel="Privacy"
title="Send anonymous crash reports"
description="Help improve Maestro by automatically sending crash reports. No personal data is collected. Changes take effect after restart."
checked={props.crashReportingEnabled}
onChange={props.setCrashReportingEnabled}
theme={theme}
/>
</div>
)}

View File

@@ -299,17 +299,16 @@ export function useAgentExecution(
// Spawn the agent for batch processing
// Use effectiveCwd which may be a worktree path for parallel execution
const commandToUse = agent.path || agent.command;
// Only add Claude-specific permission-mode flag for Claude Code
const spawnArgs = session.toolType === 'claude-code'
? [...(agent.args || []), '--permission-mode', 'plan']
: [...(agent.args || [])];
// Batch processing runs in read-only mode (plan mode) to prevent unintended writes
// The main process uses agent-specific readOnlyArgs builders for correct CLI args
window.maestro.process.spawn({
sessionId: targetSessionId,
toolType: session.toolType,
cwd: effectiveCwd,
command: commandToUse,
args: spawnArgs,
prompt
args: agent.args || [],
prompt,
readOnlyMode: true, // Batch operations run in read-only/plan mode
}).catch(() => {
cleanup();
resolve({ success: false });

View File

@@ -261,15 +261,16 @@ export function useAgentSessionManagement(
if (s.id !== activeSession.id) return s;
// Create tab from the CURRENT session state (not stale closure value)
const { session: updatedSession } = createTab(s, {
const result = createTab(s, {
agentSessionId,
logs: messages,
name,
starred: isStarred,
saveToHistory: defaultSaveToHistory
});
if (!result) return s;
return { ...updatedSession, inputMode: 'ai' };
return { ...result.session, inputMode: 'ai' };
}));
setActiveAgentSessionId(agentSessionId);
} catch (error) {

View File

@@ -308,12 +308,14 @@ export function useMainKeyboardHandler(): UseMainKeyboardHandlerReturn {
if (ctx.isTabShortcut(e, 'newTab')) {
e.preventDefault();
const result = ctx.createTab(ctx.activeSession, { saveToHistory: ctx.defaultSaveToHistory });
ctx.setSessions((prev: Session[]) => prev.map((s: Session) =>
s.id === ctx.activeSession!.id ? result.session : s
));
// Auto-focus the input so user can start typing immediately
ctx.setActiveFocus('main');
setTimeout(() => ctx.inputRef.current?.focus(), 50);
if (result) {
ctx.setSessions((prev: Session[]) => prev.map((s: Session) =>
s.id === ctx.activeSession!.id ? result.session : s
));
// Auto-focus the input so user can start typing immediately
ctx.setActiveFocus('main');
setTimeout(() => ctx.inputRef.current?.focus(), 50);
}
}
if (ctx.isTabShortcut(e, 'closeTab')) {
e.preventDefault();

View File

@@ -265,6 +265,7 @@ export function useRemoteIntegration(deps: UseRemoteIntegrationDeps): UseRemoteI
// Use createTab helper
const result = createTab(s, { saveToHistory: defaultSaveToHistory });
if (!result) return s;
newTabId = result.tab.id;
return result.session;
}));

View File

@@ -152,6 +152,10 @@ export interface UseSettingsReturn {
checkForUpdatesOnStartup: boolean;
setCheckForUpdatesOnStartup: (value: boolean) => void;
// Crash reporting settings
crashReportingEnabled: boolean;
setCrashReportingEnabled: (value: boolean) => void;
// Log Viewer settings
logViewerSelectedLevels: string[];
setLogViewerSelectedLevels: (value: string[]) => void;
@@ -272,6 +276,9 @@ export function useSettings(): UseSettingsReturn {
// Update Config
const [checkForUpdatesOnStartup, setCheckForUpdatesOnStartupState] = useState(true); // Default: on
// Crash Reporting Config
const [crashReportingEnabled, setCrashReportingEnabledState] = useState(true); // Default: on (opt-out)
// Log Viewer Config
const [logViewerSelectedLevels, setLogViewerSelectedLevelsState] = useState<string[]>(['debug', 'info', 'warn', 'error', 'toast']);
@@ -453,6 +460,11 @@ export function useSettings(): UseSettingsReturn {
window.maestro.settings.set('checkForUpdatesOnStartup', value);
}, []);
const setCrashReportingEnabled = useCallback((value: boolean) => {
setCrashReportingEnabledState(value);
window.maestro.settings.set('crashReportingEnabled', value);
}, []);
const setLogViewerSelectedLevels = useCallback((value: string[]) => {
setLogViewerSelectedLevelsState(value);
window.maestro.settings.set('logViewerSelectedLevels', value);
@@ -887,6 +899,7 @@ export function useSettings(): UseSettingsReturn {
const savedAudioFeedbackCommand = await window.maestro.settings.get('audioFeedbackCommand');
const savedToastDuration = await window.maestro.settings.get('toastDuration');
const savedCheckForUpdatesOnStartup = await window.maestro.settings.get('checkForUpdatesOnStartup');
const savedCrashReportingEnabled = await window.maestro.settings.get('crashReportingEnabled');
const savedLogViewerSelectedLevels = await window.maestro.settings.get('logViewerSelectedLevels');
const savedCustomAICommands = await window.maestro.settings.get('customAICommands');
const savedGlobalStats = await window.maestro.settings.get('globalStats');
@@ -929,6 +942,7 @@ export function useSettings(): UseSettingsReturn {
if (savedAudioFeedbackCommand !== undefined) setAudioFeedbackCommandState(savedAudioFeedbackCommand);
if (savedToastDuration !== undefined) setToastDurationState(savedToastDuration);
if (savedCheckForUpdatesOnStartup !== undefined) setCheckForUpdatesOnStartupState(savedCheckForUpdatesOnStartup);
if (savedCrashReportingEnabled !== undefined) setCrashReportingEnabledState(savedCrashReportingEnabled);
if (savedLogViewerSelectedLevels !== undefined) setLogViewerSelectedLevelsState(savedLogViewerSelectedLevels);
// Merge saved shortcuts with defaults (in case new shortcuts were added)
@@ -1113,6 +1127,8 @@ export function useSettings(): UseSettingsReturn {
setToastDuration,
checkForUpdatesOnStartup,
setCheckForUpdatesOnStartup,
crashReportingEnabled,
setCrashReportingEnabled,
logViewerSelectedLevels,
setLogViewerSelectedLevels,
shortcuts,
@@ -1184,6 +1200,7 @@ export function useSettings(): UseSettingsReturn {
audioFeedbackCommand,
toastDuration,
checkForUpdatesOnStartup,
crashReportingEnabled,
logViewerSelectedLevels,
shortcuts,
customAICommands,
@@ -1223,6 +1240,7 @@ export function useSettings(): UseSettingsReturn {
setAudioFeedbackCommand,
setToastDuration,
setCheckForUpdatesOnStartup,
setCrashReportingEnabled,
setLogViewerSelectedLevels,
setShortcuts,
setCustomAICommands,

View File

@@ -1,5 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import * as Sentry from '@sentry/electron/renderer';
import MaestroConsole from './App';
import { ErrorBoundary } from './components/ErrorBoundary';
import { LayerStackProvider } from './contexts/LayerStackContext';
@@ -8,6 +9,12 @@ import { WizardProvider } from './components/Wizard';
import { logger } from './utils/logger';
import './index.css';
// Initialize Sentry for the renderer process
// The main process handles the enabled/disabled check and initializes Sentry there
// Renderer Sentry will automatically connect to main process Sentry
// We initialize unconditionally here - if main process didn't init, this is a no-op
Sentry.init({});
// Set up global error handlers for uncaught exceptions in renderer process
window.addEventListener('error', (event: ErrorEvent) => {
logger.error(

View File

@@ -34,7 +34,7 @@ function hasDraft(tab: AITab): boolean {
* const unreadTabs = getNavigableTabs(session, true);
*/
export function getNavigableTabs(session: Session, showUnreadOnly = false): AITab[] {
if (!session.aiTabs || session.aiTabs.length === 0) {
if (!session || !session.aiTabs || session.aiTabs.length === 0) {
return [];
}
@@ -54,7 +54,7 @@ export function getNavigableTabs(session: Session, showUnreadOnly = false): AITa
* @returns The active AITab or undefined if no tabs exist
*/
export function getActiveTab(session: Session): AITab | undefined {
if (!session.aiTabs || session.aiTabs.length === 0) {
if (!session || !session.aiTabs || session.aiTabs.length === 0) {
return undefined;
}
@@ -106,7 +106,11 @@ export interface CreateTabResult {
* logs: existingLogs
* });
*/
export function createTab(session: Session, options: CreateTabOptions = {}): CreateTabResult {
export function createTab(session: Session, options: CreateTabOptions = {}): CreateTabResult | null {
if (!session) {
return null;
}
const {
agentSessionId = null,
logs = [],
@@ -170,7 +174,7 @@ export interface CloseTabResult {
* }
*/
export function closeTab(session: Session, tabId: string): CloseTabResult | null {
if (!session.aiTabs || session.aiTabs.length === 0) {
if (!session || !session.aiTabs || session.aiTabs.length === 0) {
return null;
}
@@ -345,8 +349,8 @@ export interface SetActiveTabResult {
* }
*/
export function setActiveTab(session: Session, tabId: string): SetActiveTabResult | null {
// Validate that the tab exists
if (!session.aiTabs || session.aiTabs.length === 0) {
// Validate that the session and tab exists
if (!session || !session.aiTabs || session.aiTabs.length === 0) {
return null;
}
@@ -388,7 +392,7 @@ export function setActiveTab(session: Session, tabId: string): SetActiveTabResul
* }
*/
export function getWriteModeTab(session: Session): AITab | undefined {
if (!session.aiTabs || session.aiTabs.length === 0) {
if (!session || !session.aiTabs || session.aiTabs.length === 0) {
return undefined;
}
@@ -416,7 +420,7 @@ export function getWriteModeTab(session: Session): AITab | undefined {
* }
*/
export function getBusyTabs(session: Session): AITab[] {
if (!session.aiTabs || session.aiTabs.length === 0) {
if (!session || !session.aiTabs || session.aiTabs.length === 0) {
return [];
}
@@ -439,7 +443,7 @@ export function getBusyTabs(session: Session): AITab[] {
* }
*/
export function navigateToNextTab(session: Session, showUnreadOnly = false): SetActiveTabResult | null {
if (!session.aiTabs || session.aiTabs.length < 2) {
if (!session || !session.aiTabs || session.aiTabs.length < 2) {
return null;
}
@@ -498,7 +502,7 @@ export function navigateToNextTab(session: Session, showUnreadOnly = false): Set
* }
*/
export function navigateToPrevTab(session: Session, showUnreadOnly = false): SetActiveTabResult | null {
if (!session.aiTabs || session.aiTabs.length < 2) {
if (!session || !session.aiTabs || session.aiTabs.length < 2) {
return null;
}
@@ -559,7 +563,7 @@ export function navigateToPrevTab(session: Session, showUnreadOnly = false): Set
* }
*/
export function navigateToTabByIndex(session: Session, index: number, showUnreadOnly = false): SetActiveTabResult | null {
if (!session.aiTabs || session.aiTabs.length === 0) {
if (!session || !session.aiTabs || session.aiTabs.length === 0) {
return null;
}