+
- ) : groups.length > 0 ? (
+ ) : groups.length > 0 && !showUnreadAgentsOnly ? (
/* NO UNGROUPED AGENTS - Show drop zone for ungrouping + New Group button */
{/* Drop zone indicator when dragging */}
@@ -1181,9 +1209,12 @@ function SessionListInner(props: SessionListProps) {
leftSidebarOpen={leftSidebarOpen}
hasNoSessions={sessions.length === 0}
shortcuts={shortcuts}
+ showUnreadAgentsOnly={showUnreadAgentsOnly}
+ hasUnreadAgents={hasUnreadAgents}
addNewSession={addNewSession}
openWizard={openWizard}
setLeftSidebarOpen={setLeftSidebarOpen}
+ toggleShowUnreadAgentsOnly={toggleShowUnreadAgentsOnly}
/>
{/* Session Context Menu */}
diff --git a/src/renderer/components/SessionList/SidebarActions.tsx b/src/renderer/components/SessionList/SidebarActions.tsx
index ccb53c11a..dd3167c5c 100644
--- a/src/renderer/components/SessionList/SidebarActions.tsx
+++ b/src/renderer/components/SessionList/SidebarActions.tsx
@@ -8,9 +8,12 @@ interface SidebarActionsProps {
leftSidebarOpen: boolean;
hasNoSessions: boolean;
shortcuts: Record
;
+ showUnreadAgentsOnly: boolean;
+ hasUnreadAgents: boolean;
addNewSession: () => void;
openWizard?: () => void;
setLeftSidebarOpen: (open: boolean) => void;
+ toggleShowUnreadAgentsOnly: () => void;
}
export const SidebarActions = memo(function SidebarActions({
@@ -18,9 +21,12 @@ export const SidebarActions = memo(function SidebarActions({
leftSidebarOpen,
hasNoSessions,
shortcuts,
+ showUnreadAgentsOnly,
+ hasUnreadAgents,
addNewSession,
openWizard,
setLeftSidebarOpen,
+ toggleShowUnreadAgentsOnly,
}: SidebarActionsProps) {
return (
Wizard
)}
+
+ {/* Unread agents filter toggle */}
+
+
+ {hasUnreadAgents && (
+
+ )}
+
);
});
diff --git a/src/renderer/components/Wizard/tour/tourSteps.tsx b/src/renderer/components/Wizard/tour/tourSteps.tsx
index bedb277a3..8b8c31cb1 100644
--- a/src/renderer/components/Wizard/tour/tourSteps.tsx
+++ b/src/renderer/components/Wizard/tour/tourSteps.tsx
@@ -148,9 +148,9 @@ export const tourSteps: TourStepConfig[] = [
id: 'remote-control',
title: 'Remote Control',
description:
- 'The LIVE/OFFLINE indicator controls a built-in web interface for remote access. Toggle it on to generate a local URL and QR codeāscan it with your phone to control Maestro from the couch, the kitchen, or anywhere on your network.\n\nIf you have Cloudflare Tunnel (cloudflared) installed, one click opens a secure tunnelāno API keys, no login, no configuration. Access Maestro from anywhere, even outside your home network.',
+ 'The LIVE/OFFLINE indicator controls a built-in web interface for remote control. Toggle it on to generate a local URL and QR codeāscan it with your phone to control Maestro from the couch, the kitchen, or anywhere on your network.\n\nIf you have Cloudflare Tunnel (cloudflared) installed, one click opens a secure tunnelāno API keys, no login, no configuration. Access Maestro from anywhere, even outside your home network.',
descriptionGeneric:
- 'The LIVE/OFFLINE indicator controls a built-in web interface for remote access. Toggle it on to generate a local URL and QR codeāscan it with your phone to control Maestro from anywhere on your network.\n\nIf you have Cloudflare Tunnel (cloudflared) installed, one click opens a secure tunnelāno API keys, no login, no configuration. Access Maestro from anywhere, even outside your home network.',
+ 'The LIVE/OFFLINE indicator controls a built-in web interface for remote control. Toggle it on to generate a local URL and QR codeāscan it with your phone to control Maestro from anywhere on your network.\n\nIf you have Cloudflare Tunnel (cloudflared) installed, one click opens a secure tunnelāno API keys, no login, no configuration. Access Maestro from anywhere, even outside your home network.',
wide: true,
selector: '[data-tour="remote-control"]',
position: 'right',
diff --git a/src/renderer/constants/shortcuts.ts b/src/renderer/constants/shortcuts.ts
index 37f530386..2b035f053 100644
--- a/src/renderer/constants/shortcuts.ts
+++ b/src/renderer/constants/shortcuts.ts
@@ -78,6 +78,11 @@ export const DEFAULT_SHORTCUTS: Record
= {
label: "Director's Notes",
keys: ['Meta', 'Shift', 'o'],
},
+ filterUnreadAgents: {
+ id: 'filterUnreadAgents',
+ label: 'Filter Unread Agents',
+ keys: ['Meta', 'Shift', 'u'],
+ },
};
// Non-editable shortcuts (displayed in help but not configurable)
@@ -119,6 +124,21 @@ export const FIXED_SHORTCUTS: Record = {
label: 'File Preview: Go Forward',
keys: ['Meta', 'ArrowRight'],
},
+ fontSizeIncrease: {
+ id: 'fontSizeIncrease',
+ label: 'Increase Font Size',
+ keys: ['Meta', '='],
+ },
+ fontSizeDecrease: {
+ id: 'fontSizeDecrease',
+ label: 'Decrease Font Size',
+ keys: ['Meta', '-'],
+ },
+ fontSizeReset: {
+ id: 'fontSizeReset',
+ label: 'Reset Font Size',
+ keys: ['Meta', '0'],
+ },
};
// Tab navigation shortcuts (AI mode only)
@@ -163,7 +183,7 @@ export const TAB_SHORTCUTS: Record = {
toggleTabUnread: {
id: 'toggleTabUnread',
label: 'Toggle Tab Unread',
- keys: ['Meta', 'Shift', 'u'],
+ keys: ['Alt', 'Shift', 'u'],
},
goToTab1: { id: 'goToTab1', label: 'Go to Tab 1', keys: ['Meta', '1'] },
goToTab2: { id: 'goToTab2', label: 'Go to Tab 2', keys: ['Meta', '2'] },
diff --git a/src/renderer/hooks/git/useFileExplorerEffects.ts b/src/renderer/hooks/git/useFileExplorerEffects.ts
index b4b58e80a..d175cc87a 100644
--- a/src/renderer/hooks/git/useFileExplorerEffects.ts
+++ b/src/renderer/hooks/git/useFileExplorerEffects.ts
@@ -107,6 +107,10 @@ export function useFileExplorerEffects(
() => useFileExplorerStore.getState().setSelectedFileIndex,
[]
);
+ const setFilteredFileTree = useMemo(
+ () => useFileExplorerStore.getState().setFilteredFileTree,
+ []
+ );
const setFlatFileList = useMemo(() => useFileExplorerStore.getState().setFlatFileList, []);
const { hasOpenModal } = useLayerStack();
@@ -199,6 +203,7 @@ export function useFileExplorerEffects(
useEffect(() => {
if (!activeSession || !activeSession.fileExplorerExpanded) {
+ setFilteredFileTree([]);
setFlatFileList([]);
return;
}
@@ -230,6 +235,7 @@ export function useFileExplorerEffects(
}
}
+ setFilteredFileTree(filteredFileTree);
setFlatFileList(newFlatList);
}, [activeSession?.fileExplorerExpanded, filteredFileTree, showHiddenFiles]);
diff --git a/src/renderer/hooks/input/useInputHandlers.ts b/src/renderer/hooks/input/useInputHandlers.ts
index f6e04e8b9..b2a63c8ea 100644
--- a/src/renderer/hooks/input/useInputHandlers.ts
+++ b/src/renderer/hooks/input/useInputHandlers.ts
@@ -459,12 +459,26 @@ export function useInputHandlers(deps: UseInputHandlersDeps): UseInputHandlersRe
const handleReplayMessage = useCallback(
(text: string, images?: string[]) => {
+ // Preserve draft input so replay doesn't clobber what the user was typing
+ const draftInput = aiInputValueLocalRef.current;
+ const draftImages = activeTab?.stagedImages ? [...activeTab.stagedImages] : [];
+
if (images && images.length > 0) {
setStagedImages(images);
}
- setTimeout(() => processInputRef.current(text), 0);
+ setTimeout(() => {
+ processInputRef.current(text);
+ // Restore draft input after processInput clears it
+ if (draftInput) {
+ setInputValue(draftInput);
+ syncAiInputToSession(draftInput);
+ }
+ if (draftImages.length > 0) {
+ setStagedImages(draftImages);
+ }
+ }, 0);
},
- [setStagedImages]
+ [setStagedImages, setInputValue, syncAiInputToSession, activeTab?.stagedImages]
);
const handlePaste = useCallback(
diff --git a/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts b/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
index b60ed62a9..3b4f73f63 100644
--- a/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
+++ b/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
@@ -2,6 +2,13 @@ import { useEffect, useRef, useState } from 'react';
import type { Session, AITab, ThinkingMode } from '../../types';
import { getInitialRenameValue } from '../../utils/tabHelpers';
import { useModalStore } from '../../stores/modalStore';
+import { useSettingsStore } from '../../stores/settingsStore';
+
+// Font size keyboard shortcut constants
+const FONT_SIZE_STEP = 2;
+const FONT_SIZE_MIN = 10;
+const FONT_SIZE_MAX = 24;
+const FONT_SIZE_DEFAULT = 14;
/**
* Context object passed to the main keyboard handler via ref.
@@ -140,22 +147,29 @@ export function useMainKeyboardHandler(): UseMainKeyboardHandlerReturn {
e.altKey && (e.metaKey || e.ctrlKey) && !e.shiftKey && codeKeyLower === 't';
// Allow toggleMode (Cmd+J) to switch to terminal view from file preview
const isToggleModeShortcut = ctx.isShortcut(e, 'toggleMode');
+ // Allow font size shortcuts (Cmd+=/+, Cmd+-, Cmd+0) even when modals/overlays are open
+ const isFontSizeShortcut =
+ (e.metaKey || e.ctrlKey) &&
+ !e.altKey &&
+ !e.shiftKey &&
+ (e.key === '=' || e.key === '+' || e.key === '-' || e.key === '0');
if (ctx.hasOpenModal()) {
// TRUE MODAL is open - block most shortcuts from App.tsx
// The modal's own handler will handle Cmd+Shift+[] if it supports it
// BUT allow layout shortcuts (sidebar toggles), system utility shortcuts, session jump,
- // jumpToBottom, and markdown toggle to work (these are benign viewing preferences)
+ // jumpToBottom, markdown toggle, and font size to work (these are benign viewing preferences)
if (
!isLayoutShortcut &&
!isSystemUtilShortcut &&
!isSessionJumpShortcut &&
!isJumpToBottomShortcut &&
- !isMarkdownToggleShortcut
+ !isMarkdownToggleShortcut &&
+ !isFontSizeShortcut
) {
return;
}
- // Fall through to handle layout/system utility/session jump/jumpToBottom/markdown toggle shortcuts below
+ // Fall through to handle layout/system utility/session jump/jumpToBottom/markdown toggle/font size shortcuts below
} else {
// Only OVERLAYS are open (file tabs, LogViewer, etc.)
// Allow Cmd+Shift+[] to fall through to App.tsx handler
@@ -172,7 +186,8 @@ export function useMainKeyboardHandler(): UseMainKeyboardHandlerReturn {
!isMarkdownToggleShortcut &&
!isTabManagementShortcut &&
!isTabSwitcherShortcut &&
- !isToggleModeShortcut
+ !isToggleModeShortcut &&
+ !isFontSizeShortcut
) {
return;
}
@@ -426,6 +441,10 @@ export function useMainKeyboardHandler(): UseMainKeyboardHandlerReturn {
e.preventDefault();
ctx.setDirectorNotesOpen?.(true);
trackShortcut('directorNotes');
+ } else if (ctx.isShortcut(e, 'filterUnreadAgents')) {
+ e.preventDefault();
+ ctx.toggleShowUnreadAgentsOnly();
+ trackShortcut('filterUnreadAgents');
} else if (ctx.isShortcut(e, 'jumpToBottom')) {
e.preventDefault();
// Jump to the bottom of the current main panel output (AI logs or terminal output)
@@ -482,6 +501,34 @@ export function useMainKeyboardHandler(): UseMainKeyboardHandlerReturn {
}
}
+ // Font size shortcuts: Cmd+= (zoom in), Cmd+- (zoom out), Cmd+0 (reset)
+ // These take priority over tab shortcuts (Cmd+0 was previously goToLastTab)
+ if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey) {
+ if (e.key === '=' || e.key === '+') {
+ e.preventDefault();
+ const { fontSize, setFontSize } = useSettingsStore.getState();
+ const newSize = Math.min(fontSize + FONT_SIZE_STEP, FONT_SIZE_MAX);
+ if (newSize !== fontSize) setFontSize(newSize);
+ trackShortcut('fontSizeIncrease');
+ return;
+ }
+ if (e.key === '-') {
+ e.preventDefault();
+ const { fontSize, setFontSize } = useSettingsStore.getState();
+ const newSize = Math.max(fontSize - FONT_SIZE_STEP, FONT_SIZE_MIN);
+ if (newSize !== fontSize) setFontSize(newSize);
+ trackShortcut('fontSizeDecrease');
+ return;
+ }
+ if (e.key === '0') {
+ e.preventDefault();
+ const { fontSize, setFontSize } = useSettingsStore.getState();
+ if (fontSize !== FONT_SIZE_DEFAULT) setFontSize(FONT_SIZE_DEFAULT);
+ trackShortcut('fontSizeReset');
+ return;
+ }
+ }
+
// Tab shortcuts (AI mode only, requires an explicitly selected session, disabled in group chat view)
if (
ctx.activeSessionId &&
diff --git a/src/renderer/hooks/session/useSessionCategories.ts b/src/renderer/hooks/session/useSessionCategories.ts
index 17e98e5b9..0abbf7746 100644
--- a/src/renderer/hooks/session/useSessionCategories.ts
+++ b/src/renderer/hooks/session/useSessionCategories.ts
@@ -22,7 +22,8 @@ export interface SessionCategories {
export function useSessionCategories(
sessionFilter: string,
- sortedSessions: Session[]
+ sortedSessions: Session[],
+ showUnreadAgentsOnly = false
): SessionCategories {
const sessions = useSessionStore((s) => s.sessions);
const groups = useSessionStore((s) => s.groups);
@@ -67,7 +68,7 @@ export function useSessionCategories(
// Consolidated session categorization and sorting - computed in a single pass
const sessionCategories = useMemo(() => {
- // Step 1: Filter sessions based on search query
+ // Step 1: Filter sessions based on search query and unread filter
const query = sessionFilter?.toLowerCase() ?? '';
const filtered: Session[] = [];
@@ -75,6 +76,12 @@ export function useSessionCategories(
// Exclude worktree children from main list (they appear under parent)
if (s.parentSessionId) continue;
+ // Apply unread agents filter (also keep busy/working agents visible)
+ if (showUnreadAgentsOnly) {
+ const hasUnread = s.aiTabs?.some((tab) => tab.hasUnread);
+ if (!hasUnread && s.state !== 'busy') continue;
+ }
+
if (!query) {
filtered.push(s);
} else {
@@ -150,7 +157,7 @@ export function useSessionCategories(
sortedUngroupedParent,
sortedGrouped,
};
- }, [sessionFilter, sessions, worktreeChildrenByParentId]);
+ }, [sessionFilter, showUnreadAgentsOnly, sessions, worktreeChildrenByParentId]);
const sortedGroups = useMemo(
() => [...groups].sort((a, b) => compareSessionNames(a.name, b.name)),
diff --git a/src/renderer/hooks/session/useSessionCrud.ts b/src/renderer/hooks/session/useSessionCrud.ts
index f318b3f9a..e1b3db85f 100644
--- a/src/renderer/hooks/session/useSessionCrud.ts
+++ b/src/renderer/hooks/session/useSessionCrud.ts
@@ -153,7 +153,8 @@ export function useSessionCrud(deps: UseSessionCrudDeps): UseSessionCrudReturn {
name,
workingDir,
agentId as ToolType,
- currentSessions
+ currentSessions,
+ sessionSshRemoteConfig?.enabled ? sessionSshRemoteConfig.remoteId : null
);
if (!validation.valid) {
console.error(`Session validation failed: ${validation.error}`);
diff --git a/src/renderer/hooks/worktree/useWorktreeHandlers.ts b/src/renderer/hooks/worktree/useWorktreeHandlers.ts
index f94c52d9a..1f3f403e5 100644
--- a/src/renderer/hooks/worktree/useWorktreeHandlers.ts
+++ b/src/renderer/hooks/worktree/useWorktreeHandlers.ts
@@ -353,6 +353,9 @@ export function useWorktreeHandlers(): WorktreeHandlersReturn {
worktreeSession,
]);
+ // Auto-focus the new worktree session
+ useSessionStore.getState().setActiveSessionId(worktreeSession.id);
+
notifyToast({
type: 'success',
title: 'Worktree Created',
@@ -442,6 +445,9 @@ export function useWorktreeHandlers(): WorktreeHandlersReturn {
worktreeSession,
]);
+ // Auto-focus the new worktree session
+ useSessionStore.getState().setActiveSessionId(worktreeSession.id);
+
notifyToast({
type: 'success',
title: 'Worktree Created',
diff --git a/src/renderer/stores/fileExplorerStore.ts b/src/renderer/stores/fileExplorerStore.ts
index dc2cd022a..039542e05 100644
--- a/src/renderer/stores/fileExplorerStore.ts
+++ b/src/renderer/stores/fileExplorerStore.ts
@@ -13,6 +13,7 @@
import { create } from 'zustand';
import type { FlatTreeNode } from '../utils/fileExplorer';
+import type { FileNode } from '../types/fileTree';
// ============================================================================
// Types
@@ -32,6 +33,9 @@ export interface FileExplorerStoreState {
// File preview loading indicator (migrated from App.tsx)
filePreviewLoading: FilePreviewLoading | null;
+ // Filtered file tree (tree-structured, for FileExplorerPanel rendering)
+ filteredFileTree: FileNode[];
+
// Flattened file list for keyboard navigation (migrated from App.tsx)
flatFileList: FlatTreeNode[];
@@ -50,7 +54,8 @@ export interface FileExplorerStoreActions {
// File preview loading
setFilePreviewLoading: (loading: FilePreviewLoading | null) => void;
- // Flat file list
+ // File tree data
+ setFilteredFileTree: (tree: FileNode[]) => void;
setFlatFileList: (list: FlatTreeNode[]) => void;
// Document Graph
@@ -87,6 +92,7 @@ export const useFileExplorerStore = create()((set, get) => ({
fileTreeFilter: '',
fileTreeFilterOpen: false,
filePreviewLoading: null,
+ filteredFileTree: [],
flatFileList: [],
isGraphViewOpen: false,
graphFocusFilePath: undefined,
@@ -100,6 +106,7 @@ export const useFileExplorerStore = create()((set, get) => ({
setFilePreviewLoading: (loading) => set({ filePreviewLoading: loading }),
+ setFilteredFileTree: (tree) => set({ filteredFileTree: tree }),
setFlatFileList: (list) => set({ flatFileList: list }),
focusFileInGraph: (relativePath) =>
@@ -150,6 +157,7 @@ export function getFileExplorerActions() {
setFileTreeFilter: state.setFileTreeFilter,
setFileTreeFilterOpen: state.setFileTreeFilterOpen,
setFilePreviewLoading: state.setFilePreviewLoading,
+ setFilteredFileTree: state.setFilteredFileTree,
setFlatFileList: state.setFlatFileList,
focusFileInGraph: state.focusFileInGraph,
openLastDocumentGraph: state.openLastDocumentGraph,
diff --git a/src/renderer/stores/uiStore.ts b/src/renderer/stores/uiStore.ts
index 40e7aaf9e..e21e52260 100644
--- a/src/renderer/stores/uiStore.ts
+++ b/src/renderer/stores/uiStore.ts
@@ -28,6 +28,7 @@ export interface UIStoreState {
// Session list filter
showUnreadOnly: boolean;
+ showUnreadAgentsOnly: boolean;
preFilterActiveTabId: string | null;
preTerminalFileTabId: string | null;
@@ -79,6 +80,8 @@ export interface UIStoreActions {
// Session list filter
setShowUnreadOnly: (show: boolean | ((prev: boolean) => boolean)) => void;
toggleShowUnreadOnly: () => void;
+ setShowUnreadAgentsOnly: (show: boolean | ((prev: boolean) => boolean)) => void;
+ toggleShowUnreadAgentsOnly: () => void;
setPreFilterActiveTabId: (id: string | null) => void;
setPreTerminalFileTabId: (id: string | null) => void;
@@ -130,6 +133,7 @@ export const useUIStore = create()((set) => ({
bookmarksCollapsed: false,
groupChatsExpanded: true,
showUnreadOnly: false,
+ showUnreadAgentsOnly: false,
preFilterActiveTabId: null,
preTerminalFileTabId: null,
selectedSidebarIndex: 0,
@@ -162,6 +166,9 @@ export const useUIStore = create()((set) => ({
setShowUnreadOnly: (v) => set((s) => ({ showUnreadOnly: resolve(v, s.showUnreadOnly) })),
toggleShowUnreadOnly: () => set((s) => ({ showUnreadOnly: !s.showUnreadOnly })),
+ setShowUnreadAgentsOnly: (v) =>
+ set((s) => ({ showUnreadAgentsOnly: resolve(v, s.showUnreadAgentsOnly) })),
+ toggleShowUnreadAgentsOnly: () => set((s) => ({ showUnreadAgentsOnly: !s.showUnreadAgentsOnly })),
setPreFilterActiveTabId: (id) => set({ preFilterActiveTabId: id }),
setPreTerminalFileTabId: (id) => set({ preTerminalFileTabId: id }),
diff --git a/src/renderer/utils/fileExplorer.ts b/src/renderer/utils/fileExplorer.ts
index e18924d7a..1419ad898 100644
--- a/src/renderer/utils/fileExplorer.ts
+++ b/src/renderer/utils/fileExplorer.ts
@@ -350,8 +350,8 @@ async function loadFileTreeRecursive(
}
seen.add(entry.name);
- // Skip entries that match ignore patterns
- if (shouldIgnore(entry.name, state.ignorePatterns)) {
+ // Skip entries that match ignore patterns (but always show .maestro)
+ if (entry.name !== '.maestro' && shouldIgnore(entry.name, state.ignorePatterns)) {
continue;
}
diff --git a/src/renderer/utils/markdownConfig.ts b/src/renderer/utils/markdownConfig.ts
index 8823a109e..d4a488a29 100644
--- a/src/renderer/utils/markdownConfig.ts
+++ b/src/renderer/utils/markdownConfig.ts
@@ -18,7 +18,7 @@
import type { Components } from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
-import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
+import { getSyntaxStyle } from './syntaxTheme';
import React from 'react';
import type { Theme } from '../types';
@@ -377,7 +377,7 @@ export function createMarkdownComponents(options: MarkdownComponentsOptions): Pa
// Standard syntax-highlighted code block
return React.createElement(SyntaxHighlighter, {
language,
- style: vscDarkPlus,
+ style: getSyntaxStyle(theme.mode),
customStyle: {
margin: '0.5em 0',
padding: '1em',
diff --git a/src/renderer/utils/sessionValidation.ts b/src/renderer/utils/sessionValidation.ts
index d89858f13..c66263b4b 100644
--- a/src/renderer/utils/sessionValidation.ts
+++ b/src/renderer/utils/sessionValidation.ts
@@ -16,18 +16,21 @@ export interface SessionValidationResult {
*
* Rules:
* 1. Session names must be unique across all sessions (hard error)
- * 2. Home directories (projectRoot) shared with any existing agent produce a warning
+ * 2. Home directories (projectRoot) shared with any existing agent on the same host produce a warning
* - Users can acknowledge the risk and proceed
* - Multiple agents in the same directory may clobber each other's work
+ * - Agents on different hosts (local vs SSH, or different SSH remotes) are not considered conflicting
*/
export function validateNewSession(
name: string,
directory: string,
_toolType: ToolType,
- existingSessions: Session[]
+ existingSessions: Session[],
+ sshRemoteId?: string | null
): SessionValidationResult {
const trimmedName = name.trim();
const normalizedDir = normalizeDirectory(directory);
+ const newRemoteId = sshRemoteId || null;
// Check for duplicate name (hard error - cannot proceed)
const duplicateName = existingSessions.find(
@@ -41,10 +44,13 @@ export function validateNewSession(
};
}
- // Check for duplicate directory with ANY existing agent (warning - user can acknowledge)
+ // Check for duplicate directory with existing agents on the SAME host (warning - user can acknowledge)
+ // Agents on different hosts (local vs SSH, or different SSH remotes) are not considered conflicting
const conflictingAgents = existingSessions.filter((session) => {
const sessionDir = normalizeDirectory(session.projectRoot || session.cwd);
- return sessionDir === normalizedDir;
+ if (sessionDir !== normalizedDir) return false;
+ const existingRemoteId = getSessionSshRemoteId(session);
+ return existingRemoteId === newRemoteId;
});
if (conflictingAgents.length > 0) {
@@ -91,6 +97,19 @@ export function validateEditSession(
return { valid: true };
}
+/**
+ * Resolve the SSH remote ID from a session, checking all possible locations.
+ * Returns null for local sessions.
+ */
+function getSessionSshRemoteId(session: Session): string | null {
+ // sessionSshRemoteConfig is the canonical per-session SSH config
+ if (session.sessionSshRemoteConfig?.enabled && session.sessionSshRemoteConfig.remoteId) {
+ return session.sessionSshRemoteConfig.remoteId;
+ }
+ // Fallback to flattened fields set during session lifecycle
+ return session.sshRemoteId || session.sshRemote?.id || null;
+}
+
/**
* Normalize directory path for comparison.
* Removes trailing slashes and resolves common variations.
diff --git a/src/renderer/utils/syntaxTheme.ts b/src/renderer/utils/syntaxTheme.ts
new file mode 100644
index 000000000..36171c049
--- /dev/null
+++ b/src/renderer/utils/syntaxTheme.ts
@@ -0,0 +1,16 @@
+/**
+ * Syntax highlighting theme selection based on app theme mode.
+ *
+ * Light themes need a light syntax style (vs), dark/vibe themes use vscDarkPlus.
+ * This matches the pattern already used in the mobile code.
+ */
+
+import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
+import type { ThemeMode } from '../../shared/theme-types';
+
+/**
+ * Returns the appropriate syntax highlighter style for the given theme mode.
+ */
+export function getSyntaxStyle(mode: ThemeMode) {
+ return mode === 'light' ? vs : vscDarkPlus;
+}
diff --git a/src/shared/themes.ts b/src/shared/themes.ts
index 8bd1455a0..97ee3676d 100644
--- a/src/shared/themes.ts
+++ b/src/shared/themes.ts
@@ -164,15 +164,15 @@ export const THEMES: Record = {
bgSidebar: '#eee8d5',
bgActivity: '#e6dfc8',
border: '#d3cbb7',
- textMain: '#657b83',
- textDim: '#93a1a1',
- accent: '#2aa198',
- accentDim: 'rgba(42, 161, 152, 0.1)',
- accentText: '#2aa198',
+ textMain: '#5f737b',
+ textDim: '#606969',
+ accent: '#207c76',
+ accentDim: 'rgba(32, 124, 118, 0.1)',
+ accentText: '#207c76',
accentForeground: '#fdf6e3',
- success: '#859900',
- warning: '#b58900',
- error: '#dc322f',
+ success: '#687700',
+ warning: '#8d6a00',
+ error: '#d3302d',
},
},
'one-light': {
@@ -185,14 +185,14 @@ export const THEMES: Record = {
bgActivity: '#dbdbdc',
border: '#c8c8c9',
textMain: '#383a42',
- textDim: '#696c77',
+ textDim: '#666873',
accent: '#a626a4',
accentDim: 'rgba(166, 38, 164, 0.1)',
- accentText: '#0184bc',
+ accentText: '#0079ad',
accentForeground: '#ffffff',
- success: '#50a14f',
- warning: '#c18401',
- error: '#e45649',
+ success: '#3f803f',
+ warning: '#996800',
+ error: '#c4493e',
},
},
'gruvbox-light': {
@@ -205,13 +205,13 @@ export const THEMES: Record = {
bgActivity: '#d5c4a1',
border: '#bdae93',
textMain: '#3c3836',
- textDim: '#7c6f64',
- accent: '#458588',
- accentDim: 'rgba(69, 133, 136, 0.1)',
+ textDim: '#695d55',
+ accent: '#3d7578',
+ accentDim: 'rgba(61, 117, 120, 0.1)',
accentText: '#076678',
accentForeground: '#fbf1c7',
- success: '#98971a',
- warning: '#d79921',
+ success: '#707013',
+ warning: '#8e6515',
error: '#cc241d',
},
},
@@ -225,13 +225,13 @@ export const THEMES: Record = {
bgActivity: '#dce0e8',
border: '#acb0be',
textMain: '#4c4f69',
- textDim: '#6c6f85',
+ textDim: '#65667c',
accent: '#8839ef',
accentDim: 'rgba(136, 57, 239, 0.12)',
- accentText: '#ea76cb',
+ accentText: '#a0508b',
accentForeground: '#ffffff',
- success: '#40a02b',
- warning: '#fe640b',
+ success: '#317c21',
+ warning: '#b94908',
error: '#d20f39',
},
},
@@ -245,14 +245,14 @@ export const THEMES: Record = {
bgActivity: '#e7e8e9',
border: '#d9d9d9',
textMain: '#5c6166',
- textDim: '#828c99',
- accent: '#55b4d4',
- accentDim: 'rgba(85, 180, 212, 0.1)',
- accentText: '#399ee6',
+ textDim: '#686f79',
+ accent: '#3a7a90',
+ accentDim: 'rgba(58, 122, 144, 0.1)',
+ accentText: '#2b77ae',
accentForeground: '#1a1a1a',
- success: '#86b300',
- warning: '#f2ae49',
- error: '#f07171',
+ success: '#5d7c00',
+ warning: '#946a2c',
+ error: '#b45555',
},
},
// Vibe themes
diff --git a/symphony-registry.json b/symphony-registry.json
index a42e78528..b70f53c59 100644
--- a/symphony-registry.json
+++ b/symphony-registry.json
@@ -1,154 +1,111 @@
{
- "schemaVersion": "1.0",
- "lastUpdated": "2026-02-28T00:00:00Z",
- "repositories": [
- {
- "slug": "RunMaestro/Maestro",
- "name": "Maestro",
- "description": "Desktop app for managing multiple AI agents with a keyboard-first interface.",
- "url": "https://github.com/RunMaestro/Maestro",
- "category": "ai-ml",
- "tags": [
- "electron",
- "ai",
- "productivity",
- "typescript"
- ],
- "maintainer": {
- "name": "Pedram Amini",
- "url": "https://github.com/pedramamini"
- },
- "isActive": true,
- "featured": true,
- "addedAt": "2025-01-01"
- },
- {
- "slug": "pedramamini/Podsidian",
- "name": "Podsidian",
- "description": "An MCP-capable intelligent Apple podcast transcription and summarization to markdown tool.",
- "url": "https://github.com/pedramamini/Podsidian",
- "category": "productivity",
- "tags": [
- "mcp",
- "podcasts",
- "obsidian",
- "markdown",
- "transcription",
- "python"
- ],
- "maintainer": {
- "name": "Pedram Amini",
- "url": "https://github.com/pedramamini"
- },
- "isActive": true,
- "featured": false,
- "addedAt": "2026-02-14"
- },
- {
- "slug": "pedramamini/RSSidian",
- "name": "RSSidian",
- "description": "An MCP-capable intelligent RSS feed ingestion and summarization to markdown tool.",
- "url": "https://github.com/pedramamini/RSSidian",
- "category": "productivity",
- "tags": [
- "mcp",
- "rss",
- "obsidian",
- "markdown",
- "summarization",
- "python"
- ],
- "maintainer": {
- "name": "Pedram Amini",
- "url": "https://github.com/pedramamini"
- },
- "isActive": true,
- "featured": false,
- "addedAt": "2026-02-14"
- },
- {
- "slug": "thedotmack/claude-mem",
- "name": "claude-mem",
- "description": "A Claude Code plugin that automatically captures everything Claude does during your coding sessions, compresses it with AI, and injects relevant context back into future sessions.",
- "url": "https://github.com/thedotmack/claude-mem",
- "category": "ai-ml",
- "tags": [
- "claude-code",
- "memory",
- "ai-agents",
- "plugin",
- "typescript"
- ],
- "maintainer": {
- "name": "thedotmack",
- "url": "https://github.com/thedotmack"
- },
- "isActive": true,
- "featured": false,
- "addedAt": "2026-02-14"
- },
- {
- "slug": "danielmiessler/Personal_AI_Infrastructure",
- "name": "Personal AI Infrastructure",
- "description": "A guide and framework for building your own personal AI infrastructure, including patterns for AI agents, workflows, and automation.",
- "url": "https://github.com/danielmiessler/Personal_AI_Infrastructure",
- "category": "ai-ml",
- "tags": [
- "ai",
- "infrastructure",
- "agents",
- "automation",
- "guide"
- ],
- "maintainer": {
- "name": "Daniel Miessler",
- "url": "https://github.com/danielmiessler"
- },
- "isActive": true,
- "featured": false,
- "addedAt": "2026-02-25"
- },
- {
- "slug": "danielmiessler/fabric",
- "name": "fabric",
- "description": "An open-source framework for augmenting humans using AI, providing a modular system for solving specific problems with crowdsourced AI prompts.",
- "url": "https://github.com/danielmiessler/fabric",
- "category": "productivity",
- "tags": [
- "ai",
- "prompts",
- "automation",
- "cli",
- "go"
- ],
- "maintainer": {
- "name": "Daniel Miessler",
- "url": "https://github.com/danielmiessler"
- },
- "isActive": true,
- "featured": false,
- "addedAt": "2026-02-25"
- },
- {
- "slug": "volvoxllc/volvox-bot",
- "name": "volvox-bot",
- "description": "An AI-powered Discord bot used for managing servers that includes interaction, question answering, full memory of users, interaction, games, and much more.",
- "url": "https://github.com/VolvoxLLC/volvox-bot",
- "category": "ai-ml",
- "tags": [
- "discord-bot",
- "claude-sdk",
- "moderation",
- "discord.js",
- "typescript"
- ],
- "maintainer": {
- "name": "BillChirico",
- "url": "https://github.com/BillChirico"
- },
- "isActive": true,
- "featured": false,
- "addedAt": "2026-02-28"
- }
- ]
+ "schemaVersion": "1.0",
+ "lastUpdated": "2026-02-28T00:00:00Z",
+ "repositories": [
+ {
+ "slug": "RunMaestro/Maestro",
+ "name": "Maestro",
+ "description": "Desktop app for managing multiple AI agents with a keyboard-first interface.",
+ "url": "https://github.com/RunMaestro/Maestro",
+ "category": "ai-ml",
+ "tags": ["electron", "ai", "productivity", "typescript"],
+ "maintainer": {
+ "name": "Pedram Amini",
+ "url": "https://github.com/pedramamini"
+ },
+ "isActive": true,
+ "featured": true,
+ "addedAt": "2025-01-01"
+ },
+ {
+ "slug": "pedramamini/Podsidian",
+ "name": "Podsidian",
+ "description": "An MCP-capable intelligent Apple podcast transcription and summarization to markdown tool.",
+ "url": "https://github.com/pedramamini/Podsidian",
+ "category": "productivity",
+ "tags": ["mcp", "podcasts", "obsidian", "markdown", "transcription", "python"],
+ "maintainer": {
+ "name": "Pedram Amini",
+ "url": "https://github.com/pedramamini"
+ },
+ "isActive": true,
+ "featured": false,
+ "addedAt": "2026-02-14"
+ },
+ {
+ "slug": "pedramamini/RSSidian",
+ "name": "RSSidian",
+ "description": "An MCP-capable intelligent RSS feed ingestion and summarization to markdown tool.",
+ "url": "https://github.com/pedramamini/RSSidian",
+ "category": "productivity",
+ "tags": ["mcp", "rss", "obsidian", "markdown", "summarization", "python"],
+ "maintainer": {
+ "name": "Pedram Amini",
+ "url": "https://github.com/pedramamini"
+ },
+ "isActive": true,
+ "featured": false,
+ "addedAt": "2026-02-14"
+ },
+ {
+ "slug": "thedotmack/claude-mem",
+ "name": "claude-mem",
+ "description": "A Claude Code plugin that automatically captures everything Claude does during your coding sessions, compresses it with AI, and injects relevant context back into future sessions.",
+ "url": "https://github.com/thedotmack/claude-mem",
+ "category": "ai-ml",
+ "tags": ["claude-code", "memory", "ai-agents", "plugin", "typescript"],
+ "maintainer": {
+ "name": "thedotmack",
+ "url": "https://github.com/thedotmack"
+ },
+ "isActive": true,
+ "featured": false,
+ "addedAt": "2026-02-14"
+ },
+ {
+ "slug": "danielmiessler/Personal_AI_Infrastructure",
+ "name": "Personal AI Infrastructure",
+ "description": "A guide and framework for building your own personal AI infrastructure, including patterns for AI agents, workflows, and automation.",
+ "url": "https://github.com/danielmiessler/Personal_AI_Infrastructure",
+ "category": "ai-ml",
+ "tags": ["ai", "infrastructure", "agents", "automation", "guide"],
+ "maintainer": {
+ "name": "Daniel Miessler",
+ "url": "https://github.com/danielmiessler"
+ },
+ "isActive": true,
+ "featured": false,
+ "addedAt": "2026-02-25"
+ },
+ {
+ "slug": "danielmiessler/fabric",
+ "name": "fabric",
+ "description": "An open-source framework for augmenting humans using AI, providing a modular system for solving specific problems with crowdsourced AI prompts.",
+ "url": "https://github.com/danielmiessler/fabric",
+ "category": "productivity",
+ "tags": ["ai", "prompts", "automation", "cli", "go"],
+ "maintainer": {
+ "name": "Daniel Miessler",
+ "url": "https://github.com/danielmiessler"
+ },
+ "isActive": true,
+ "featured": false,
+ "addedAt": "2026-02-25"
+ },
+ {
+ "slug": "volvoxllc/volvox-bot",
+ "name": "volvox-bot",
+ "description": "An AI-powered Discord bot used for managing servers that includes interaction, question answering, full memory of users, interaction, games, and much more.",
+ "url": "https://github.com/VolvoxLLC/volvox-bot",
+ "category": "ai-ml",
+ "tags": ["discord-bot", "claude-sdk", "moderation", "discord.js", "typescript"],
+ "maintainer": {
+ "name": "BillChirico",
+ "url": "https://github.com/BillChirico"
+ },
+ "isActive": true,
+ "featured": false,
+ "addedAt": "2026-02-28"
+ }
+ ]
}