mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
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
This commit is contained in:
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
"identity": null,
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
|
||||
@@ -138,9 +138,13 @@ const historyStore = new Store<HistoryData>({
|
||||
// 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<string, Record<string, ClaudeSessionOrigin>>;
|
||||
// Map of projectPath -> { claudeSessionId -> origin info }
|
||||
origins: Record<string, Record<string, ClaudeSessionOrigin | ClaudeSessionOriginInfo>>;
|
||||
}
|
||||
|
||||
const claudeSessionOriginsStore = new Store<ClaudeSessionOriginsData>({
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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<boolean>;
|
||||
getSessionOrigins: (projectPath: string) => Promise<Record<string, 'user' | 'auto'>>;
|
||||
registerSessionOrigin: (projectPath: string, claudeSessionId: string, origin: 'user' | 'auto', sessionName?: string) => Promise<boolean>;
|
||||
updateSessionName: (projectPath: string, claudeSessionId: string, sessionName: string) => Promise<boolean>;
|
||||
getSessionOrigins: (projectPath: string) => Promise<Record<string, 'user' | 'auto' | { origin: 'user' | 'auto'; sessionName?: string }>>;
|
||||
};
|
||||
tempfile: {
|
||||
write: (content: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }>;
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [lightboxImages, setLightboxImages] = useState<string[]>([]); // 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 && (
|
||||
<LightboxModal
|
||||
image={lightboxImage}
|
||||
stagedImages={stagedImages}
|
||||
onClose={() => 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}
|
||||
|
||||
@@ -352,11 +352,11 @@ export function AICommandsPanel({ theme, customAICommands, setCustomAICommands }
|
||||
{cmd.description}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs p-2 rounded font-mono overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
className="text-xs p-2 rounded font-mono overflow-hidden line-clamp-3"
|
||||
style={{ backgroundColor: theme.colors.bgActivity, color: theme.colors.textMain }}
|
||||
title={cmd.prompt}
|
||||
>
|
||||
{cmd.prompt.length > 100 ? `${cmd.prompt.slice(0, 100)}...` : cmd.prompt}
|
||||
{cmd.prompt}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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<SearchMode>('all');
|
||||
const [showAllSessions, setShowAllSessions] = useState(false);
|
||||
const [namedOnly, setNamedOnly] = useState(false);
|
||||
const [searchModeDropdownOpen, setSearchModeDropdownOpen] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||
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({
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
{/* Named Only checkbox */}
|
||||
<label
|
||||
className="flex items-center gap-1.5 text-xs cursor-pointer select-none"
|
||||
style={{ color: namedOnly ? theme.colors.accent : theme.colors.textDim }}
|
||||
title="Only show sessions with custom names"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={namedOnly}
|
||||
onChange={(e) => setNamedOnly(e.target.checked)}
|
||||
className="w-3.5 h-3.5 rounded cursor-pointer accent-current"
|
||||
style={{ accentColor: theme.colors.accent }}
|
||||
/>
|
||||
<span>Named</span>
|
||||
</label>
|
||||
{/* Show All checkbox */}
|
||||
<label
|
||||
className="flex items-center gap-1.5 text-xs cursor-pointer select-none"
|
||||
@@ -904,10 +934,22 @@ export function AgentSessionsBrowser({
|
||||
/>
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Line 1: Title/first message */}
|
||||
{/* Line 1: Session name (if available) with tag icon */}
|
||||
{session.sessionName && (
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<Tag className="w-3.5 h-3.5 flex-shrink-0" style={{ color: theme.colors.accent }} />
|
||||
<span
|
||||
className="font-semibold text-sm truncate"
|
||||
style={{ color: theme.colors.accent }}
|
||||
>
|
||||
{session.sessionName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Line 2: First message / title */}
|
||||
<div
|
||||
className="font-medium truncate text-sm mb-1.5"
|
||||
style={{ color: theme.colors.textMain }}
|
||||
className={`font-medium truncate text-sm ${session.sessionName ? 'mb-1' : 'mb-1.5'}`}
|
||||
style={{ color: session.sessionName ? theme.colors.textDim : theme.colors.textMain }}
|
||||
>
|
||||
{session.firstMessage || `Session ${session.sessionId.slice(0, 8)}...`}
|
||||
</div>
|
||||
|
||||
@@ -312,11 +312,17 @@ export function GitLogViewer({ cwd, theme, onClose }: GitLogViewerProps) {
|
||||
|
||||
{/* Search bar */}
|
||||
<div
|
||||
className="px-6 py-3 border-b flex items-center gap-3"
|
||||
className="px-6 py-3 border-b flex items-center gap-3 cursor-text"
|
||||
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgSidebar }}
|
||||
onClick={() => searchInputRef.current?.focus()}
|
||||
onMouseDown={(e) => {
|
||||
// Prevent the modal from stealing focus back
|
||||
if (e.target !== searchInputRef.current) {
|
||||
e.preventDefault();
|
||||
searchInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Search className="w-4 h-4" style={{ color: isSearchFocused ? theme.colors.accent : theme.colors.textDim }} />
|
||||
<Search className="w-4 h-4 flex-shrink-0 pointer-events-none" style={{ color: isSearchFocused ? theme.colors.accent : theme.colors.textDim }} />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
@@ -326,6 +332,7 @@ export function GitLogViewer({ cwd, theme, onClose }: GitLogViewerProps) {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onFocus={() => setIsSearchFocused(true)}
|
||||
onBlur={() => setIsSearchFocused(false)}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="flex-1 bg-transparent text-sm outline-none border-none"
|
||||
style={{
|
||||
color: theme.colors.textMain,
|
||||
|
||||
@@ -18,7 +18,7 @@ interface InputAreaProps {
|
||||
setEnterToSend: (value: boolean) => void;
|
||||
stagedImages: string[];
|
||||
setStagedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setLightboxImage: (image: string | null) => void;
|
||||
setLightboxImage: (image: string | null, contextImages?: string[]) => void;
|
||||
commandHistoryOpen: boolean;
|
||||
setCommandHistoryOpen: (open: boolean) => void;
|
||||
commandHistoryFilter: string;
|
||||
@@ -99,7 +99,7 @@ export function InputArea(props: InputAreaProps) {
|
||||
src={img}
|
||||
className="h-16 rounded border cursor-pointer hover:opacity-80 transition-opacity"
|
||||
style={{ borderColor: theme.colors.border }}
|
||||
onClick={() => setLightboxImage(img)}
|
||||
onClick={() => setLightboxImage(img, stagedImages)}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -66,7 +66,7 @@ interface MainPanelProps {
|
||||
setEnterToSendAI: (value: boolean) => void;
|
||||
setEnterToSendTerminal: (value: boolean) => void;
|
||||
setStagedImages: (images: string[]) => void;
|
||||
setLightboxImage: (image: string | null) => void;
|
||||
setLightboxImage: (image: string | null, contextImages?: string[]) => void;
|
||||
setCommandHistoryOpen: (open: boolean) => void;
|
||||
setCommandHistoryFilter: (filter: string) => void;
|
||||
setCommandHistorySelectedIndex: (index: number) => void;
|
||||
|
||||
@@ -17,7 +17,7 @@ interface TerminalOutputProps {
|
||||
setOutputSearchOpen: (open: boolean) => void;
|
||||
setOutputSearchQuery: (query: string) => void;
|
||||
setActiveFocus: (focus: string) => void;
|
||||
setLightboxImage: (image: string | null) => void;
|
||||
setLightboxImage: (image: string | null, contextImages?: string[]) => void;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
logsEndRef: React.RefObject<HTMLDivElement>;
|
||||
maxOutputLines: number;
|
||||
@@ -780,7 +780,7 @@ export const TerminalOutput = forwardRef<HTMLDivElement, TerminalOutputProps>((p
|
||||
{log.images && log.images.length > 0 && (
|
||||
<div className="flex gap-2 mb-2 overflow-x-auto scrollbar-thin">
|
||||
{log.images.map((img, imgIdx) => (
|
||||
<img key={imgIdx} src={img} className="h-20 rounded border cursor-zoom-in" onClick={() => setLightboxImage(img)} />
|
||||
<img key={imgIdx} src={img} className="h-20 rounded border cursor-zoom-in" onClick={() => setLightboxImage(img, log.images)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user