diff --git a/README.md b/README.md index cd05ee2d..13c3fa2e 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Download the latest release for your platform from the [Releases](https://github ### Core Features - 🔄 **Dual-Mode Sessions** - Each agent has both an AI Terminal and Command Terminal. Switch seamlessly between AI conversation and shell commands with `Cmd+J`. -- ⌨️ **[Keyboard-First Design](#keyboard-shortcuts)** - Full keyboard control with customizable shortcuts. `Cmd+K` quick actions, vim-style navigation, rapid agent switching, and focus management designed for flow state. +- ⌨️ **[Keyboard-First Design](#keyboard-shortcuts)** - Full keyboard control with customizable shortcuts. `Cmd+K` quick actions, rapid agent switching, and focus management designed for flow state. - 📋 **Session Discovery** - Automatically discovers and imports all Claude Code sessions, including conversations from before Maestro was installed. Browse, search, star, rename, and resume any session. - 🔀 **Git Integration** - Automatic repo detection, branch display, diff viewer, commit logs, and git-aware file completion. Work with git without leaving the app. - 📁 **[File Explorer](#ui-overview)** - Browse project files with syntax highlighting, markdown preview, and image viewing. Reference files in prompts with `@` mentions. diff --git a/package.json b/package.json index 5493923f..8a14eeeb 100644 --- a/package.json +++ b/package.json @@ -174,7 +174,6 @@ "react-diff-view": "^3.3.2", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", - "react-virtuoso": "^4.15.0", "rehype-raw": "^7.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", diff --git a/src/__tests__/main/agent-capabilities.test.ts b/src/__tests__/main/agent-capabilities.test.ts index 7bb3fe08..bf2a33a3 100644 --- a/src/__tests__/main/agent-capabilities.test.ts +++ b/src/__tests__/main/agent-capabilities.test.ts @@ -16,6 +16,7 @@ describe('agent-capabilities', () => { supportsJsonOutput: false, supportsSessionId: false, supportsImageInput: false, + supportsImageInputOnResume: false, supportsSlashCommands: false, supportsSessionStorage: false, supportsCostTracking: false, @@ -30,6 +31,7 @@ describe('agent-capabilities', () => { expect(capabilities.supportsJsonOutput).toBe(false); expect(capabilities.supportsSessionId).toBe(false); expect(capabilities.supportsImageInput).toBe(false); + expect(capabilities.supportsImageInputOnResume).toBe(false); expect(capabilities.supportsSlashCommands).toBe(false); expect(capabilities.supportsSessionStorage).toBe(false); expect(capabilities.supportsCostTracking).toBe(false); @@ -47,6 +49,7 @@ describe('agent-capabilities', () => { expect(DEFAULT_CAPABILITIES.supportsJsonOutput).toBe(false); expect(DEFAULT_CAPABILITIES.supportsSessionId).toBe(false); expect(DEFAULT_CAPABILITIES.supportsImageInput).toBe(false); + expect(DEFAULT_CAPABILITIES.supportsImageInputOnResume).toBe(false); expect(DEFAULT_CAPABILITIES.supportsSlashCommands).toBe(false); expect(DEFAULT_CAPABILITIES.supportsSessionStorage).toBe(false); expect(DEFAULT_CAPABILITIES.supportsCostTracking).toBe(false); @@ -152,6 +155,7 @@ describe('agent-capabilities', () => { expect(typeof AGENT_CAPABILITIES[agentId].supportsJsonOutput).toBe('boolean'); expect(typeof AGENT_CAPABILITIES[agentId].supportsSessionId).toBe('boolean'); expect(typeof AGENT_CAPABILITIES[agentId].supportsImageInput).toBe('boolean'); + expect(typeof AGENT_CAPABILITIES[agentId].supportsImageInputOnResume).toBe('boolean'); expect(typeof AGENT_CAPABILITIES[agentId].supportsSlashCommands).toBe('boolean'); expect(typeof AGENT_CAPABILITIES[agentId].supportsSessionStorage).toBe('boolean'); expect(typeof AGENT_CAPABILITIES[agentId].supportsCostTracking).toBe('boolean'); @@ -215,6 +219,7 @@ describe('agent-capabilities', () => { 'supportsJsonOutput', 'supportsSessionId', 'supportsImageInput', + 'supportsImageInputOnResume', 'supportsSlashCommands', 'supportsSessionStorage', 'supportsCostTracking', @@ -241,6 +246,7 @@ describe('agent-capabilities', () => { 'supportsJsonOutput', 'supportsSessionId', 'supportsImageInput', + 'supportsImageInputOnResume', 'supportsSlashCommands', 'supportsSessionStorage', 'supportsCostTracking', diff --git a/src/__tests__/main/agent-detector.test.ts b/src/__tests__/main/agent-detector.test.ts index cd482821..82f9b351 100644 --- a/src/__tests__/main/agent-detector.test.ts +++ b/src/__tests__/main/agent-detector.test.ts @@ -64,6 +64,7 @@ describe('agent-detector', () => { supportsJsonOutput: false, supportsSessionId: false, supportsImageInput: false, + supportsImageInputOnResume: false, supportsSlashCommands: false, supportsSessionStorage: false, supportsCostTracking: false, @@ -96,6 +97,7 @@ describe('agent-detector', () => { supportsJsonOutput: true, supportsSessionId: true, supportsImageInput: false, + supportsImageInputOnResume: false, supportsSlashCommands: false, supportsSessionStorage: false, supportsCostTracking: false, @@ -119,6 +121,7 @@ describe('agent-detector', () => { supportsJsonOutput: true, supportsSessionId: true, supportsImageInput: true, + supportsImageInputOnResume: true, supportsSlashCommands: true, supportsSessionStorage: true, supportsCostTracking: true, @@ -131,6 +134,7 @@ describe('agent-detector', () => { expect(capabilities.supportsResume).toBe(true); expect(capabilities.supportsModelSelection).toBe(true); expect(capabilities.supportsImageInput).toBe(true); + expect(capabilities.supportsImageInputOnResume).toBe(true); }); it('should support select type with options in AgentConfigOption', () => { diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 35e750cc..64e25ad2 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -157,6 +157,7 @@ const mockMaestro = { supportsJsonOutput: true, supportsSessionId: true, supportsImageInput: true, + supportsImageInputOnResume: true, supportsSlashCommands: true, supportsSessionStorage: true, supportsCostTracking: true, diff --git a/src/main/agent-capabilities.ts b/src/main/agent-capabilities.ts index 876df89c..e2a7fb93 100644 --- a/src/main/agent-capabilities.ts +++ b/src/main/agent-capabilities.ts @@ -28,6 +28,9 @@ export interface AgentCapabilities { /** Agent can accept image inputs (screenshots, diagrams, etc.) */ supportsImageInput: boolean; + /** Agent can accept image inputs when resuming an existing session */ + supportsImageInputOnResume: boolean; + /** Agent supports slash commands (e.g., /help, /compact) */ supportsSlashCommands: boolean; @@ -69,6 +72,7 @@ export const DEFAULT_CAPABILITIES: AgentCapabilities = { supportsJsonOutput: false, supportsSessionId: false, supportsImageInput: false, + supportsImageInputOnResume: false, supportsSlashCommands: false, supportsSessionStorage: false, supportsCostTracking: false, @@ -103,6 +107,7 @@ export const AGENT_CAPABILITIES: Record = { supportsJsonOutput: true, // --output-format stream-json supportsSessionId: true, // session_id in JSON output supportsImageInput: true, // Supports image attachments + supportsImageInputOnResume: true, // Can send images via --input-format stream-json on resumed sessions supportsSlashCommands: true, // /help, /compact, etc. supportsSessionStorage: true, // ~/.claude/projects/ supportsCostTracking: true, // Cost info in usage stats @@ -125,6 +130,7 @@ export const AGENT_CAPABILITIES: Record = { supportsJsonOutput: false, supportsSessionId: false, supportsImageInput: false, + supportsImageInputOnResume: false, supportsSlashCommands: false, supportsSessionStorage: false, supportsCostTracking: false, @@ -150,6 +156,7 @@ export const AGENT_CAPABILITIES: Record = { supportsJsonOutput: true, // --json flag - Verified supportsSessionId: true, // thread_id in thread.started event - Verified supportsImageInput: true, // -i, --image flag - Documented + supportsImageInputOnResume: false, // Codex resume subcommand doesn't support -i flag - Verified supportsSlashCommands: false, // None - Verified supportsSessionStorage: true, // ~/.codex/sessions/YYYY/MM/DD/*.jsonl - Verified supportsCostTracking: false, // Token counts only - Codex doesn't provide cost, pricing varies by model @@ -174,6 +181,7 @@ export const AGENT_CAPABILITIES: Record = { supportsJsonOutput: false, supportsSessionId: false, supportsImageInput: true, // Gemini supports multimodal + supportsImageInputOnResume: false, // Not yet investigated supportsSlashCommands: false, supportsSessionStorage: false, supportsCostTracking: false, @@ -198,6 +206,7 @@ export const AGENT_CAPABILITIES: Record = { supportsJsonOutput: false, supportsSessionId: false, supportsImageInput: false, + supportsImageInputOnResume: false, supportsSlashCommands: false, supportsSessionStorage: false, supportsCostTracking: false, // Local model - no cost @@ -223,6 +232,7 @@ export const AGENT_CAPABILITIES: Record = { supportsJsonOutput: false, // Not yet investigated supportsSessionId: false, // Not yet investigated supportsImageInput: true, // Aider supports vision models + supportsImageInputOnResume: false, // Not yet investigated supportsSlashCommands: true, // Aider has /commands supportsSessionStorage: false, // Not yet investigated supportsCostTracking: true, // Aider tracks costs @@ -248,6 +258,7 @@ export const AGENT_CAPABILITIES: Record = { supportsJsonOutput: true, // --format json - Verified supportsSessionId: true, // sessionID in JSON output (camelCase) - Verified supportsImageInput: true, // -f, --file flag documented - Documented + supportsImageInputOnResume: true, // -f flag works with --session flag - Documented supportsSlashCommands: false, // Not investigated supportsSessionStorage: true, // ~/.local/share/opencode/storage/ (JSON files) - Verified supportsCostTracking: true, // part.cost in step_finish events - Verified diff --git a/src/main/preload.ts b/src/main/preload.ts index 2d3ff2e5..47d1ebaa 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -27,6 +27,7 @@ interface AgentCapabilities { supportsJsonOutput: boolean; supportsSessionId: boolean; supportsImageInput: boolean; + supportsImageInputOnResume: boolean; supportsSlashCommands: boolean; supportsSessionStorage: boolean; supportsCostTracking: boolean; diff --git a/src/renderer/components/InputArea.tsx b/src/renderer/components/InputArea.tsx index 5b7a3fef..e8250145 100644 --- a/src/renderer/components/InputArea.tsx +++ b/src/renderer/components/InputArea.tsx @@ -115,6 +115,14 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) { // Get agent capabilities for conditional feature rendering const { hasCapability } = useAgentCapabilities(session.toolType); + // Check if images are supported - depends on whether we're resuming an existing session + // If the active tab has an agentSessionId, we're resuming and need to check supportsImageInputOnResume + const activeTab = session.aiTabs?.find(tab => tab.id === session.activeTabId); + const isResumingSession = !!activeTab?.agentSessionId; + const canAttachImages = isResumingSession + ? hasCapability('supportsImageInputOnResume') + : hasCapability('supportsImageInput'); + // Check if we're in read-only mode (auto mode OR manual toggle - Claude will be in plan mode) const isAutoReadOnly = isAutoModeActive && session.inputMode === 'ai'; const isReadOnlyMode = isAutoReadOnly || (tabReadOnlyMode && session.inputMode === 'ai'); @@ -638,7 +646,7 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) { )} - {session.inputMode === 'ai' && hasCapability('supportsImageInput') && ( + {session.inputMode === 'ai' && canAttachImages && (