From 0835672ce097c0a1bcba52f6cee44af69d3557fc Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Thu, 27 Nov 2025 01:59:49 -0600 Subject: [PATCH] feat: Add session naming with search and improve macOS code signing - Add user-defined session names stored with Claude session origins - Display session names in AgentSessionsBrowser with tag icon - Add "Named only" filter to quickly find named sessions - Include session names in search across title and content modes - Sync session names when renaming sessions in the sidebar - Fix macOS code signing with ad-hoc signatures in release workflow - Fix electron-builder CLI syntax (--config.extraMetadata) - Improve lightbox navigation with context-aware image arrays - Fix GitLogViewer search input focus handling - Improve AI command prompt display with multi-line clamp Claude ID: 24a6cdd6-27a7-41e0-af30-679cc2ffe66b Maestro ID: 5a166b38-b7e9-47f0-a8ff-0113c65f2682 --- .github/workflows/release.yml | 33 +++++++++- package.json | 1 + src/main/index.ts | 55 +++++++++++++--- src/main/preload.ts | 18 ++++-- src/renderer/App.tsx | 41 ++++++++++-- src/renderer/components/AICommandsPanel.tsx | 4 +- .../components/AgentSessionsBrowser.tsx | 64 +++++++++++++++---- src/renderer/components/GitLogViewer.tsx | 13 +++- src/renderer/components/InputArea.tsx | 4 +- src/renderer/components/MainPanel.tsx | 2 +- src/renderer/components/TerminalOutput.tsx | 4 +- 11 files changed, 195 insertions(+), 44 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd8ef9b0..e15a0b54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,20 +96,47 @@ jobs: - name: Package for macOS if: matrix.platform == 'mac' - run: npx electron-builder --mac --publish never -c.extraMetadata.version=${{ steps.version.outputs.VERSION }} + run: npx electron-builder --mac --publish never --config.extraMetadata.version=${{ steps.version.outputs.VERSION }} env: CSC_IDENTITY_AUTO_DISCOVERY: false + CSC_LINK: "" DEBUG: electron-builder + # Ad-hoc sign macOS apps and re-create archives + # Fixes "code has no resources but signature indicates they must be present" + - name: Ad-hoc sign macOS apps + if: matrix.platform == 'mac' + run: | + VERSION=${{ steps.version.outputs.VERSION }} + + # Sign x64 app and recreate zip + if [ -d "release/mac/Maestro.app" ]; then + echo "Ad-hoc signing: release/mac/Maestro.app" + codesign --sign - --deep --force "release/mac/Maestro.app" + echo "Re-creating ZIP for x64..." + rm -f "release/Maestro-${VERSION}-mac.zip" + cd release/mac && zip -r -y "../Maestro-${VERSION}-mac.zip" Maestro.app && cd ../.. + fi + + # Sign arm64 app and recreate zip + if [ -d "release/mac-arm64/Maestro.app" ]; then + echo "Ad-hoc signing: release/mac-arm64/Maestro.app" + codesign --sign - --deep --force "release/mac-arm64/Maestro.app" + echo "Re-creating ZIP for arm64..." + rm -f "release/Maestro-${VERSION}-arm64-mac.zip" + cd release/mac-arm64 && zip -r -y "../Maestro-${VERSION}-arm64-mac.zip" Maestro.app && cd ../.. + fi + - name: Package for Windows if: matrix.platform == 'win' - run: npx electron-builder --win --publish never -c.extraMetadata.version=${{ steps.version.outputs.VERSION }} + shell: bash + run: npx electron-builder --win --publish never --config.extraMetadata.version=${{ steps.version.outputs.VERSION }} env: DEBUG: electron-builder - name: Package for Linux if: matrix.platform == 'linux' - run: npx electron-builder --linux --publish never -c.extraMetadata.version=${{ steps.version.outputs.VERSION }} + run: npx electron-builder --linux --publish never --config.extraMetadata.version=${{ steps.version.outputs.VERSION }} env: DEBUG: electron-builder diff --git a/package.json b/package.json index 4d4571ac..0a5e9e82 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ ], "mac": { "category": "public.app-category.developer-tools", + "identity": null, "target": [ { "target": "dmg", diff --git a/src/main/index.ts b/src/main/index.ts index 9673e96a..d0e1b406 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -138,9 +138,13 @@ const historyStore = new Store({ // Claude session origins store - tracks which Claude sessions were created by Maestro // and their origin type (user-initiated vs auto/batch) type ClaudeSessionOrigin = 'user' | 'auto'; +interface ClaudeSessionOriginInfo { + origin: ClaudeSessionOrigin; + sessionName?: string; // User-defined session name from Maestro +} interface ClaudeSessionOriginsData { - // Map of projectPath -> { claudeSessionId -> origin type } - origins: Record>; + // Map of projectPath -> { claudeSessionId -> origin info } + origins: Record>; } const claudeSessionOriginsStore = new Store({ @@ -1086,11 +1090,18 @@ function setupIpcHandlers() { const origins = claudeSessionOriginsStore.get('origins', {}); const projectOrigins = origins[projectPath] || {}; - // Add origin info to each session - const sessionsWithOrigins = validSessions.map(session => ({ - ...session, - origin: projectOrigins[session.sessionId] as ClaudeSessionOrigin | undefined, - })); + // Add origin info and session name to each session + const sessionsWithOrigins = validSessions.map(session => { + const originData = projectOrigins[session.sessionId]; + // Handle both old string format and new object format + const origin = typeof originData === 'string' ? originData : originData?.origin; + const sessionName = typeof originData === 'object' ? originData?.sessionName : undefined; + return { + ...session, + origin: origin as ClaudeSessionOrigin | undefined, + sessionName, + }; + }); logger.info(`Found ${validSessions.length} Claude sessions for project`, 'ClaudeSessions', { projectPath }); return sessionsWithOrigins; @@ -1527,14 +1538,38 @@ function setupIpcHandlers() { }); // Claude session origins tracking (distinguishes Maestro-created sessions from CLI sessions) - ipcMain.handle('claude:registerSessionOrigin', async (_event, projectPath: string, claudeSessionId: string, origin: 'user' | 'auto') => { + ipcMain.handle('claude:registerSessionOrigin', async (_event, projectPath: string, claudeSessionId: string, origin: 'user' | 'auto', sessionName?: string) => { const origins = claudeSessionOriginsStore.get('origins', {}); if (!origins[projectPath]) { origins[projectPath] = {}; } - origins[projectPath][claudeSessionId] = origin; + // Store as object if sessionName provided, otherwise just origin string for backwards compat + origins[projectPath][claudeSessionId] = sessionName + ? { origin, sessionName } + : origin; claudeSessionOriginsStore.set('origins', origins); - logger.debug(`Registered Claude session origin: ${claudeSessionId} = ${origin}`, 'ClaudeSessionOrigins', { projectPath }); + logger.debug(`Registered Claude session origin: ${claudeSessionId} = ${origin}${sessionName ? ` (name: ${sessionName})` : ''}`, 'ClaudeSessionOrigins', { projectPath }); + return true; + }); + + // Update session name for an existing Claude session + ipcMain.handle('claude:updateSessionName', async (_event, projectPath: string, claudeSessionId: string, sessionName: string) => { + const origins = claudeSessionOriginsStore.get('origins', {}); + if (!origins[projectPath]) { + origins[projectPath] = {}; + } + const existing = origins[projectPath][claudeSessionId]; + // Convert string origin to object format, or update existing object + if (typeof existing === 'string') { + origins[projectPath][claudeSessionId] = { origin: existing, sessionName }; + } else if (existing) { + origins[projectPath][claudeSessionId] = { ...existing, sessionName }; + } else { + // No existing origin, default to 'user' since they're naming it + origins[projectPath][claudeSessionId] = { origin: 'user', sessionName }; + } + claudeSessionOriginsStore.set('origins', origins); + logger.debug(`Updated Claude session name: ${claudeSessionId} = ${sessionName}`, 'ClaudeSessionOrigins', { projectPath }); return true; }); diff --git a/src/main/preload.ts b/src/main/preload.ts index 0b599041..79dbadec 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -210,8 +210,10 @@ contextBridge.exposeInMainWorld('maestro', { getCommands: (projectPath: string) => ipcRenderer.invoke('claude:getCommands', projectPath), // Session origin tracking (distinguishes Maestro sessions from CLI sessions) - registerSessionOrigin: (projectPath: string, claudeSessionId: string, origin: 'user' | 'auto') => - ipcRenderer.invoke('claude:registerSessionOrigin', projectPath, claudeSessionId, origin), + registerSessionOrigin: (projectPath: string, claudeSessionId: string, origin: 'user' | 'auto', sessionName?: string) => + ipcRenderer.invoke('claude:registerSessionOrigin', projectPath, claudeSessionId, origin, sessionName), + updateSessionName: (projectPath: string, claudeSessionId: string, sessionName: string) => + ipcRenderer.invoke('claude:updateSessionName', projectPath, claudeSessionId, sessionName), getSessionOrigins: (projectPath: string) => ipcRenderer.invoke('claude:getSessionOrigins', projectPath), }, @@ -371,7 +373,14 @@ export interface MaestroAPI { firstMessage: string; messageCount: number; sizeBytes: number; + costUsd: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheCreationTokens: number; + durationSeconds: number; origin?: 'user' | 'auto'; // Maestro session origin, undefined for CLI sessions + sessionName?: string; // User-defined session name from Maestro }>>; readSessionMessages: (projectPath: string, sessionId: string, options?: { offset?: number; limit?: number }) => Promise<{ messages: Array<{ @@ -395,8 +404,9 @@ export interface MaestroAPI { command: string; description: string; }>>; - registerSessionOrigin: (projectPath: string, claudeSessionId: string, origin: 'user' | 'auto') => Promise; - getSessionOrigins: (projectPath: string) => Promise>; + registerSessionOrigin: (projectPath: string, claudeSessionId: string, origin: 'user' | 'auto', sessionName?: string) => Promise; + updateSessionName: (projectPath: string, claudeSessionId: string, sessionName: string) => Promise; + getSessionOrigins: (projectPath: string) => Promise>; }; tempfile: { write: (content: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }>; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 112ac398..371c478a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -131,6 +131,7 @@ export default function MaestroConsole() { const [quickActionInitialMode, setQuickActionInitialMode] = useState<'main' | 'move-to-group'>('main'); const [settingsTab, setSettingsTab] = useState<'general' | 'shortcuts' | 'theme' | 'network'>('general'); const [lightboxImage, setLightboxImage] = useState(null); + const [lightboxImages, setLightboxImages] = useState([]); // Context images for navigation const [aboutModalOpen, setAboutModalOpen] = useState(false); const [logViewerOpen, setLogViewerOpen] = useState(false); const [processMonitorOpen, setProcessMonitorOpen] = useState(false); @@ -650,8 +651,9 @@ export default function MaestroConsole() { } // Register this as a user-initiated Maestro session (batch sessions are filtered above) - window.maestro.claude.registerSessionOrigin(session.cwd, claudeSessionId, 'user') - .then(() => console.log('[onSessionId] Registered session origin as user:', claudeSessionId)) + // Also pass the session name so it can be searched in the Claude sessions browser + window.maestro.claude.registerSessionOrigin(session.cwd, claudeSessionId, 'user', session.name) + .then(() => console.log('[onSessionId] Registered session origin as user:', claudeSessionId, 'name:', session.name)) .catch(err => console.error('[onSessionId] Failed to register session origin:', err)); return prev.map(s => { @@ -1127,6 +1129,12 @@ export default function MaestroConsole() { } }, [activeSession]); + // Handler to open lightbox with optional context images for navigation + const handleSetLightboxImage = useCallback((image: string | null, contextImages?: string[]) => { + setLightboxImage(image); + setLightboxImages(contextImages || []); + }, []); + // Create sorted sessions array that matches visual display order (includes ALL sessions) const sortedSessions = useMemo(() => { const sorted: Session[] = []; @@ -1860,7 +1868,28 @@ export default function MaestroConsole() { }; const finishRenamingSession = (sessId: string, newName: string) => { - setSessions(prev => prev.map(s => s.id === sessId ? { ...s, name: newName } : s)); + setSessions(prev => { + const updated = prev.map(s => s.id === sessId ? { ...s, name: newName } : s); + // Sync the session name to Claude session storage for searchability + const session = updated.find(s => s.id === sessId); + if (session?.claudeSessionId && session.cwd) { + console.log('[finishRenamingSession] Syncing session name to Claude storage:', { + claudeSessionId: session.claudeSessionId, + cwd: session.cwd, + newName + }); + window.maestro.claude.updateSessionName(session.cwd, session.claudeSessionId, newName) + .then(() => console.log('[finishRenamingSession] Successfully synced session name')) + .catch(err => console.warn('[finishRenamingSession] Failed to sync session name:', err)); + } else { + console.log('[finishRenamingSession] Cannot sync - missing claudeSessionId or cwd:', { + sessId, + claudeSessionId: session?.claudeSessionId, + cwd: session?.cwd + }); + } + return updated; + }); setEditingSessionId(null); }; @@ -2953,8 +2982,8 @@ export default function MaestroConsole() { {lightboxImage && ( setLightboxImage(null)} + stagedImages={lightboxImages.length > 0 ? lightboxImages : stagedImages} + onClose={() => { setLightboxImage(null); setLightboxImages([]); }} onNavigate={(img) => setLightboxImage(img)} /> )} @@ -3196,7 +3225,7 @@ export default function MaestroConsole() { setEnterToSendAI={setEnterToSendAI} setEnterToSendTerminal={setEnterToSendTerminal} setStagedImages={setStagedImages} - setLightboxImage={setLightboxImage} + setLightboxImage={handleSetLightboxImage} setCommandHistoryOpen={setCommandHistoryOpen} setCommandHistoryFilter={setCommandHistoryFilter} setCommandHistorySelectedIndex={setCommandHistorySelectedIndex} diff --git a/src/renderer/components/AICommandsPanel.tsx b/src/renderer/components/AICommandsPanel.tsx index 19d120d6..f2e94792 100644 --- a/src/renderer/components/AICommandsPanel.tsx +++ b/src/renderer/components/AICommandsPanel.tsx @@ -352,11 +352,11 @@ export function AICommandsPanel({ theme, customAICommands, setCustomAICommands } {cmd.description}
- {cmd.prompt.length > 100 ? `${cmd.prompt.slice(0, 100)}...` : cmd.prompt} + {cmd.prompt}
)} diff --git a/src/renderer/components/AgentSessionsBrowser.tsx b/src/renderer/components/AgentSessionsBrowser.tsx index 4590bc97..e48305b8 100644 --- a/src/renderer/components/AgentSessionsBrowser.tsx +++ b/src/renderer/components/AgentSessionsBrowser.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -import { Search, Clock, MessageSquare, HardDrive, Play, ChevronLeft, Loader2, Plus, X, List, Database, BarChart3, ChevronDown, User, Bot, DollarSign, Star, Zap, Timer, Hash, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'; +import { Search, Clock, MessageSquare, HardDrive, Play, ChevronLeft, Loader2, Plus, X, List, Database, BarChart3, ChevronDown, User, Bot, DollarSign, Star, Zap, Timer, Hash, ArrowDownToLine, ArrowUpFromLine, Tag } from 'lucide-react'; import type { Theme, Session, LogEntry } from '../types'; import { useLayerStack } from '../contexts/LayerStackContext'; import { MODAL_PRIORITIES } from '../constants/modalPriorities'; @@ -28,6 +28,7 @@ interface ClaudeSession { cacheCreationTokens: number; durationSeconds: number; origin?: 'user' | 'auto'; // Maestro session origin, undefined for CLI sessions + sessionName?: string; // User-defined session name from Maestro } interface SessionMessage { @@ -61,6 +62,7 @@ export function AgentSessionsBrowser({ const [search, setSearch] = useState(''); const [searchMode, setSearchMode] = useState('all'); const [showAllSessions, setShowAllSessions] = useState(false); + const [namedOnly, setNamedOnly] = useState(false); const [searchModeDropdownOpen, setSearchModeDropdownOpen] = useState(false); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); @@ -327,12 +329,16 @@ export function AgentSessionsBrowser({ } }, [hasMoreMessages, messagesLoading, handleLoadMore]); - // Helper to check if a session should be visible based on showAllSessions + // Helper to check if a session should be visible based on filters const isSessionVisible = useCallback((session: ClaudeSession) => { + // Named only filter - if enabled, only show sessions with a custom name + if (namedOnly && !session.sessionName) { + return false; + } if (showAllSessions) return true; // Hide sessions that start with "agent-" (only show UUID-style sessions by default) return !session.sessionId.startsWith('agent-'); - }, [showAllSessions]); + }, [showAllSessions, namedOnly]); // Calculate stats from visible sessions const stats = useMemo(() => { @@ -368,20 +374,29 @@ export function AgentSessionsBrowser({ return sortWithStarred(visibleSessions); } - // For title search, filter locally (fast) + // For title search, filter locally (fast) - include sessionName if (searchMode === 'title') { const searchLower = search.toLowerCase(); const filtered = visibleSessions.filter(s => s.firstMessage.toLowerCase().includes(searchLower) || - s.sessionId.toLowerCase().includes(searchLower) + s.sessionId.toLowerCase().includes(searchLower) || + (s.sessionName && s.sessionName.toLowerCase().includes(searchLower)) ); return sortWithStarred(filtered); } // For content searches, use backend results to filter sessions - if (searchResults.length > 0) { - const matchingIds = new Set(searchResults.map(r => r.sessionId)); - const filtered = visibleSessions.filter(s => matchingIds.has(s.sessionId)); + // Also include sessions that match by sessionName (strong match in 'all' mode) + const searchLower = search.toLowerCase(); + const matchingIds = new Set(searchResults.map(r => r.sessionId)); + + // Add sessions that match by sessionName to the results + const filtered = visibleSessions.filter(s => + matchingIds.has(s.sessionId) || + (s.sessionName && s.sessionName.toLowerCase().includes(searchLower)) + ); + + if (filtered.length > 0) { return sortWithStarred(filtered); } @@ -798,6 +813,21 @@ export function AgentSessionsBrowser({ )} + {/* Named Only checkbox */} + {/* Show All checkbox */}