From d43f603a354960816978359a650ef2ae0ef8fbd8 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:51:39 -0300 Subject: [PATCH 01/43] MAESTRO: feat: add description field to AITab interface Added optional `description?: string` to AITab for user-defined tab context annotations. This is the data model foundation for the tab description feature. Co-Authored-By: Claude Opus 4.6 --- src/renderer/types/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 74fdb2b14..494b836b9 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -425,6 +425,8 @@ export interface AITab { autoSendOnActivate?: boolean; // When true, automatically send inputValue when tab becomes active wizardState?: SessionWizardState; // Per-tab inline wizard state for /wizard command isGeneratingName?: boolean; // True while automatic tab naming is in progress + /** Optional user-defined description for tab context */ + description?: string; } // A single "thinking item" — one busy tab within a session. From 2be1f5113e919ebac21eac75a2593c9d8026d2ae Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:53:48 -0300 Subject: [PATCH 02/43] MAESTRO: feat: add unifiedInbox and tabDescription to EncoreFeatureFlags Extends the Encore Feature system with two new feature flags for upcoming Unified Inbox and Tab Description features. tabDescription defaults to true (enabled by default), unifiedInbox defaults to false. Co-Authored-By: Claude Opus 4.6 --- src/renderer/stores/settingsStore.ts | 2 ++ src/renderer/types/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/renderer/stores/settingsStore.ts b/src/renderer/stores/settingsStore.ts index c10e6071e..d2e9c21a1 100644 --- a/src/renderer/stores/settingsStore.ts +++ b/src/renderer/stores/settingsStore.ts @@ -109,6 +109,8 @@ export const DEFAULT_ONBOARDING_STATS: OnboardingStats = { export const DEFAULT_ENCORE_FEATURES: EncoreFeatureFlags = { directorNotes: false, + unifiedInbox: false, + tabDescription: true, }; export const DEFAULT_DIRECTOR_NOTES_SETTINGS: DirectorNotesSettings = { diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 74fdb2b14..0039a74d2 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -905,6 +905,8 @@ export interface LeaderboardSubmitResponse { // Each key is a feature ID, value indicates whether it's enabled export interface EncoreFeatureFlags { directorNotes: boolean; + unifiedInbox: boolean; + tabDescription: boolean; } // Director's Notes settings for synopsis generation From 88231509b353eb41257e3c7cf4735297d0cb0784 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:56:58 -0300 Subject: [PATCH 03/43] MAESTRO: test: expand fuzzy search test coverage with 14 new cases Add missing tests for scoring hierarchy (exact > substring > scattered), case-insensitive scored matching, special characters in query (regex metacharacters, unicode, emoji), and deterministic scoring verification. Co-Authored-By: Claude Opus 4.6 --- src/__tests__/renderer/utils/search.test.ts | 91 +++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/__tests__/renderer/utils/search.test.ts b/src/__tests__/renderer/utils/search.test.ts index 58d4b2d7b..f1aa862f3 100644 --- a/src/__tests__/renderer/utils/search.test.ts +++ b/src/__tests__/renderer/utils/search.test.ts @@ -431,6 +431,97 @@ describe('search utils', () => { }); }); + describe('scoring - exact vs fuzzy vs substring ranking', () => { + it('exact match scores higher than scattered fuzzy match', () => { + const exact = fuzzyMatchWithScore('abc', 'abc'); + const scattered = fuzzyMatchWithScore('aXbXc', 'abc'); + + expect(exact.matches).toBe(true); + expect(scattered.matches).toBe(true); + expect(exact.score).toBeGreaterThan(scattered.score); + }); + + it('substring match scores higher than scattered fuzzy match', () => { + const substring = fuzzyMatchWithScore('xxabcxx', 'abc'); + const scattered = fuzzyMatchWithScore('aXXbXXc', 'abc'); + + expect(substring.matches).toBe(true); + expect(scattered.matches).toBe(true); + expect(substring.score).toBeGreaterThan(scattered.score); + }); + + it('exact match scores higher than substring match', () => { + const exact = fuzzyMatchWithScore('hello', 'hello'); + const substring = fuzzyMatchWithScore('say hello there', 'hello'); + + expect(exact.matches).toBe(true); + expect(substring.matches).toBe(true); + expect(exact.score).toBeGreaterThan(substring.score); + }); + }); + + describe('case-insensitive matching in scored search', () => { + it('matches regardless of case', () => { + const upper = fuzzyMatchWithScore('HELLO WORLD', 'hello'); + const lower = fuzzyMatchWithScore('hello world', 'HELLO'); + const mixed = fuzzyMatchWithScore('HeLLo WoRLd', 'hElLo'); + + expect(upper.matches).toBe(true); + expect(lower.matches).toBe(true); + expect(mixed.matches).toBe(true); + }); + + it('case-matching query scores higher than mismatched case', () => { + const caseMatch = fuzzyMatchWithScore('Hello', 'Hello'); + const caseMismatch = fuzzyMatchWithScore('Hello', 'hello'); + + expect(caseMatch.matches).toBe(true); + expect(caseMismatch.matches).toBe(true); + expect(caseMatch.score).toBeGreaterThan(caseMismatch.score); + }); + }); + + describe('special characters in query', () => { + it('does not crash with regex special characters in query', () => { + expect(() => fuzzyMatchWithScore('some text', '.*+?^${}()|[]\\')).not.toThrow(); + expect(() => fuzzyMatch('some text', '.*+?^${}()|[]\\')).not.toThrow(); + }); + + it('does not crash with brackets and parens in query', () => { + const result = fuzzyMatchWithScore('function()', '()'); + expect(result).toHaveProperty('matches'); + expect(result).toHaveProperty('score'); + }); + + it('does not crash with unicode in query', () => { + const result = fuzzyMatchWithScore('caf\u00e9 latte', 'caf\u00e9'); + expect(result.matches).toBe(true); + expect(result.score).toBeGreaterThan(0); + }); + + it('does not crash with emoji in query', () => { + expect(() => fuzzyMatchWithScore('hello world', '\ud83d\ude00')).not.toThrow(); + expect(() => fuzzyMatch('hello world', '\ud83d\ude00')).not.toThrow(); + }); + }); + + describe('deterministic scoring', () => { + it('produces identical scores for identical inputs across multiple calls', () => { + const results = Array.from({ length: 10 }, () => + fuzzyMatchWithScore('src/renderer/utils/search.ts', 'search') + ); + + const firstScore = results[0].score; + expect(results.every((r) => r.score === firstScore)).toBe(true); + }); + + it('produces identical match results for identical inputs', () => { + const results = Array.from({ length: 10 }, () => fuzzyMatch('handleUserInput', 'hui')); + + expect(results.every((r) => r === true)).toBe(true); + }); + }); + describe('integration scenarios', () => { it('handles real file search scenario', () => { const files = [ From a770cdb623141e75e20734354aa42532f32316e6 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:56:59 -0300 Subject: [PATCH 04/43] MAESTRO: feat: implement handleUpdateTabDescription handler in useTabHandlers Adds a new handler for updating AI tab descriptions with trim and empty-string-to-undefined normalization. Follows existing immutable state update pattern via useSessionStore. Includes 4 tests covering set, trim, empty, and whitespace-only cases. Co-Authored-By: Claude Opus 4.6 --- .../renderer/hooks/useTabHandlers.test.ts | 44 +++++++++++++++++++ src/renderer/hooks/tabs/useTabHandlers.ts | 18 ++++++++ 2 files changed, 62 insertions(+) diff --git a/src/__tests__/renderer/hooks/useTabHandlers.test.ts b/src/__tests__/renderer/hooks/useTabHandlers.test.ts index 703155a81..b836dd1ac 100644 --- a/src/__tests__/renderer/hooks/useTabHandlers.test.ts +++ b/src/__tests__/renderer/hooks/useTabHandlers.test.ts @@ -762,6 +762,50 @@ describe('useTabHandlers', () => { expect(getSession().aiTabs[0].showThinking).toBe('off'); }); + it('handleUpdateTabDescription sets description on tab', () => { + const tab = createMockAITab({ id: 'tab-1' }); + const { result } = renderWithSession([tab]); + act(() => { + result.current.handleUpdateTabDescription('tab-1', 'My description'); + }); + + const session = getSession(); + expect(session.aiTabs[0].description).toBe('My description'); + }); + + it('handleUpdateTabDescription trims whitespace', () => { + const tab = createMockAITab({ id: 'tab-1' }); + const { result } = renderWithSession([tab]); + act(() => { + result.current.handleUpdateTabDescription('tab-1', ' spaces around '); + }); + + const session = getSession(); + expect(session.aiTabs[0].description).toBe('spaces around'); + }); + + it('handleUpdateTabDescription sets undefined for empty string', () => { + const tab = createMockAITab({ id: 'tab-1', description: 'existing' } as any); + const { result } = renderWithSession([tab]); + act(() => { + result.current.handleUpdateTabDescription('tab-1', ''); + }); + + const session = getSession(); + expect(session.aiTabs[0].description).toBeUndefined(); + }); + + it('handleUpdateTabDescription sets undefined for whitespace-only string', () => { + const tab = createMockAITab({ id: 'tab-1', description: 'existing' } as any); + const { result } = renderWithSession([tab]); + act(() => { + result.current.handleUpdateTabDescription('tab-1', ' '); + }); + + const session = getSession(); + expect(session.aiTabs[0].description).toBeUndefined(); + }); + it('handleUpdateTabByClaudeSessionId updates tab by agent session id', () => { const tab = createMockAITab({ id: 'tab-1', diff --git a/src/renderer/hooks/tabs/useTabHandlers.ts b/src/renderer/hooks/tabs/useTabHandlers.ts index dfb59d4f7..5d6edcd4f 100644 --- a/src/renderer/hooks/tabs/useTabHandlers.ts +++ b/src/renderer/hooks/tabs/useTabHandlers.ts @@ -75,6 +75,7 @@ export interface TabHandlersReturn { agentSessionId: string, updates: { name?: string | null; starred?: boolean } ) => void; + handleUpdateTabDescription: (tabId: string, description: string) => void; handleTabStar: (tabId: string, starred: boolean) => void; handleTabMarkUnread: (tabId: string) => void; handleToggleTabReadOnlyMode: () => void; @@ -966,6 +967,22 @@ export function useTabHandlers(): TabHandlersReturn { // Tab Properties // ======================================================================== + const handleUpdateTabDescription = useCallback((tabId: string, description: string) => { + const trimmed = description.trim(); + const { setSessions, activeSessionId } = useSessionStore.getState(); + setSessions((prev: Session[]) => + prev.map((s) => { + if (s.id !== activeSessionId) return s; + return { + ...s, + aiTabs: s.aiTabs.map((tab) => + tab.id === tabId ? { ...tab, description: trimmed || undefined } : tab + ), + }; + }) + ); + }, []); + const handleRequestTabRename = useCallback((tabId: string) => { const { sessions, activeSessionId, setSessions } = useSessionStore.getState(); const session = sessions.find((s) => s.id === activeSessionId); @@ -1393,6 +1410,7 @@ export function useTabHandlers(): TabHandlersReturn { handleCloseTabsRight, handleCloseCurrentTab, handleRequestTabRename, + handleUpdateTabDescription, handleUpdateTabByClaudeSessionId, handleTabStar, handleTabMarkUnread, From 22293f5c73e89cb3c7e072650aff3aa094dd45d6 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:59:14 -0300 Subject: [PATCH 05/43] MAESTRO: perf: add fast-paths for fuzzy search engine Add early returns in fuzzyMatch and fuzzyMatchWithScore for: - Empty text (can't match anything) - Query longer than text (impossible to match all chars) Empty query already had early return. All 5 consumers already skip fuzzy matching on empty input. No consumer changes needed. Co-Authored-By: Claude Opus 4.6 --- src/renderer/utils/search.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/utils/search.ts b/src/renderer/utils/search.ts index ba96a113b..b1a60e8b4 100644 --- a/src/renderer/utils/search.ts +++ b/src/renderer/utils/search.ts @@ -14,6 +14,7 @@ export interface FuzzyMatchResult { */ export const fuzzyMatch = (text: string, query: string): boolean => { if (!query) return true; + if (!text || query.length > text.length) return false; const lowerText = text.toLowerCase(); const lowerQuery = query.toLowerCase(); let queryIndex = 0; @@ -45,6 +46,10 @@ export const fuzzyMatchWithScore = (text: string, query: string): FuzzyMatchResu return { matches: true, score: 0 }; } + if (!text || query.length > text.length) { + return { matches: false, score: 0 }; + } + const lowerText = text.toLowerCase(); const lowerQuery = query.toLowerCase(); From 7dba842d7638388295becc5301021642ed68e173 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:59:23 -0300 Subject: [PATCH 06/43] MAESTRO: feat: add Unified Inbox and Tab Descriptions cards to Settings Encore tab - Import Inbox and FileText icons from lucide-react - Change Encore tab icon from FlaskConical to Sparkles per spec - Add Unified Inbox feature card with toggle, Beta badge, left-border accent - Add Tab Descriptions feature card with toggle, left-border accent - Both cards follow existing toggle pattern (spread-copy + flip flag) - All 113 SettingsModal tests pass, lint clean Co-Authored-By: Claude Opus 4.6 --- src/renderer/components/SettingsModal.tsx | 134 +++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/SettingsModal.tsx b/src/renderer/components/SettingsModal.tsx index e67f7b9ea..359b6e8c0 100644 --- a/src/renderer/components/SettingsModal.tsx +++ b/src/renderer/components/SettingsModal.tsx @@ -37,6 +37,8 @@ import { Clapperboard, HelpCircle, AppWindow, + Inbox, + FileText, } from 'lucide-react'; import { useSettings } from '../hooks'; import type { @@ -1234,7 +1236,7 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro tabIndex={-1} title="Encore Features" > - + {activeTab === 'encore' && Encore Features}
@@ -3638,6 +3640,136 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro ); })()}
+ + {/* Unified Inbox Feature Card */} +
+ +
+ + {/* Tab Descriptions Feature Card */} +
+ +
)} From 889040adbc5842fdbf645a6d73e872f47dae63c4 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 11:02:37 -0300 Subject: [PATCH 07/43] MAESTRO: feat: wire handleUpdateTabDescription through props chain to TabBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thread the tab description handler from useTabHandlers through useMainPanelProps → App.tsx → MainPanel → TabBar. Gate with encoreFeatures.tabDescription flag (default: false). Co-Authored-By: Claude Opus 4.6 --- src/renderer/App.tsx | 4 ++++ src/renderer/components/MainPanel.tsx | 3 +++ src/renderer/components/TabBar.tsx | 2 ++ src/renderer/hooks/props/useMainPanelProps.ts | 3 +++ src/renderer/stores/settingsStore.ts | 1 + src/renderer/types/index.ts | 1 + 6 files changed, 14 insertions(+) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 4f31351c3..0a8f917f1 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -847,6 +847,7 @@ function MaestroConsoleInner() { handleCloseCurrentTab, handleRequestTabRename, handleUpdateTabByClaudeSessionId, + handleUpdateTabDescription, handleTabStar, handleTabMarkUnread, handleToggleTabReadOnlyMode, @@ -3212,6 +3213,9 @@ function MaestroConsoleInner() { handleTabReorder, handleUnifiedTabReorder, handleUpdateTabByClaudeSessionId, + handleUpdateTabDescription: encoreFeatures.tabDescription + ? handleUpdateTabDescription + : undefined, handleTabStar, handleTabMarkUnread, handleToggleTabReadOnlyMode, diff --git a/src/renderer/components/MainPanel.tsx b/src/renderer/components/MainPanel.tsx index 86fe27ff2..9384d5af2 100644 --- a/src/renderer/components/MainPanel.tsx +++ b/src/renderer/components/MainPanel.tsx @@ -167,6 +167,7 @@ interface MainPanelProps { onRequestTabRename?: (tabId: string) => void; onTabReorder?: (fromIndex: number, toIndex: number) => void; onUnifiedTabReorder?: (fromIndex: number, toIndex: number) => void; + onUpdateTabDescription?: (tabId: string, description: string) => void; onTabStar?: (tabId: string, starred: boolean) => void; onTabMarkUnread?: (tabId: string) => void; onUpdateTabByClaudeSessionId?: ( @@ -455,6 +456,7 @@ export const MainPanel = React.memo( onRequestTabRename, onTabReorder, onUnifiedTabReorder, + onUpdateTabDescription, onTabStar, onTabMarkUnread, onToggleUnreadFilter, @@ -1470,6 +1472,7 @@ export const MainPanel = React.memo( onRequestRename={onRequestTabRename} onTabReorder={onTabReorder} onUnifiedTabReorder={onUnifiedTabReorder} + onUpdateTabDescription={onUpdateTabDescription} onTabStar={onTabStar} onTabMarkUnread={onTabMarkUnread} onMergeWith={onMergeWith} diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx index 6cd3090e9..e69d1be31 100644 --- a/src/renderer/components/TabBar.tsx +++ b/src/renderer/components/TabBar.tsx @@ -38,6 +38,7 @@ interface TabBarProps { onTabReorder?: (fromIndex: number, toIndex: number) => void; /** Handler to reorder tabs in unified tab order (AI + file tabs) */ onUnifiedTabReorder?: (fromIndex: number, toIndex: number) => void; + onUpdateTabDescription?: (tabId: string, description: string) => void; onTabStar?: (tabId: string, starred: boolean) => void; onTabMarkUnread?: (tabId: string) => void; /** Handler to open merge session modal with this tab as source */ @@ -1512,6 +1513,7 @@ function TabBarInner({ onNewTab, onRequestRename, onTabReorder, + onUpdateTabDescription, onTabStar, onTabMarkUnread, onMergeWith, diff --git a/src/renderer/hooks/props/useMainPanelProps.ts b/src/renderer/hooks/props/useMainPanelProps.ts index dd4bd232b..3b34fd798 100644 --- a/src/renderer/hooks/props/useMainPanelProps.ts +++ b/src/renderer/hooks/props/useMainPanelProps.ts @@ -170,6 +170,7 @@ export interface UseMainPanelPropsDeps { agentSessionId: string, updates: { name?: string | null; starred?: boolean } ) => void; + handleUpdateTabDescription?: (tabId: string, description: string) => void; handleTabStar: (tabId: string, starred: boolean) => void; handleTabMarkUnread: (tabId: string) => void; handleToggleTabReadOnlyMode: () => void; @@ -351,6 +352,7 @@ export function useMainPanelProps(deps: UseMainPanelPropsDeps) { onTabReorder: deps.handleTabReorder, onUnifiedTabReorder: deps.handleUnifiedTabReorder, onUpdateTabByClaudeSessionId: deps.handleUpdateTabByClaudeSessionId, + onUpdateTabDescription: deps.handleUpdateTabDescription, onTabStar: deps.handleTabStar, onTabMarkUnread: deps.handleTabMarkUnread, onToggleTabReadOnlyMode: deps.handleToggleTabReadOnlyMode, @@ -553,6 +555,7 @@ export function useMainPanelProps(deps: UseMainPanelPropsDeps) { deps.handleTabReorder, deps.handleUnifiedTabReorder, deps.handleUpdateTabByClaudeSessionId, + deps.handleUpdateTabDescription, deps.handleTabStar, deps.handleTabMarkUnread, deps.handleToggleTabReadOnlyMode, diff --git a/src/renderer/stores/settingsStore.ts b/src/renderer/stores/settingsStore.ts index c10e6071e..d7c52f586 100644 --- a/src/renderer/stores/settingsStore.ts +++ b/src/renderer/stores/settingsStore.ts @@ -109,6 +109,7 @@ export const DEFAULT_ONBOARDING_STATS: OnboardingStats = { export const DEFAULT_ENCORE_FEATURES: EncoreFeatureFlags = { directorNotes: false, + tabDescription: false, }; export const DEFAULT_DIRECTOR_NOTES_SETTINGS: DirectorNotesSettings = { diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 494b836b9..8a2a58b09 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -907,6 +907,7 @@ export interface LeaderboardSubmitResponse { // Each key is a feature ID, value indicates whether it's enabled export interface EncoreFeatureFlags { directorNotes: boolean; + tabDescription: boolean; } // Director's Notes settings for synopsis generation From 5d60333d2aede190c268b4f63c4efdcbb4ef76f7 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 11:09:23 -0300 Subject: [PATCH 08/43] MAESTRO: feat: gate keyboard shortcuts for agentInbox and directorNotes encore features Add agentInbox shortcut (Alt+I) with encoreFeatures.unifiedInbox guard. Director's Notes shortcut already had its guard. Register agentInbox modal in the modal store and wire through App.tsx keyboard handler context. Co-Authored-By: Claude Opus 4.6 --- src/renderer/App.tsx | 4 ++++ src/renderer/constants/shortcuts.ts | 1 + .../hooks/keyboard/useMainKeyboardHandler.ts | 4 ++++ src/renderer/stores/modalStore.ts | 12 +++++++++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 4f31351c3..ff486d0db 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -344,6 +344,9 @@ function MaestroConsoleInner() { // Director's Notes Modal directorNotesOpen, setDirectorNotesOpen, + // Agent Inbox Modal (Unified Inbox) + agentInboxOpen, + setAgentInboxOpen, } = useModalActions(); // --- MOBILE LANDSCAPE MODE (reading-only view) --- @@ -3009,6 +3012,7 @@ function MaestroConsoleInner() { setMarketplaceModalOpen, setSymphonyModalOpen, setDirectorNotesOpen, + setAgentInboxOpen, encoreFeatures, setShowNewGroupChatModal, deleteGroupChatWithConfirmation, diff --git a/src/renderer/constants/shortcuts.ts b/src/renderer/constants/shortcuts.ts index 37f530386..cdee93147 100644 --- a/src/renderer/constants/shortcuts.ts +++ b/src/renderer/constants/shortcuts.ts @@ -78,6 +78,7 @@ export const DEFAULT_SHORTCUTS: Record = { label: "Director's Notes", keys: ['Meta', 'Shift', 'o'], }, + agentInbox: { id: 'agentInbox', label: 'Unified Inbox', keys: ['Alt', 'i'] }, }; // Non-editable shortcuts (displayed in help but not configurable) diff --git a/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts b/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts index 93675698d..4873d7d35 100644 --- a/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts +++ b/src/renderer/hooks/keyboard/useMainKeyboardHandler.ts @@ -420,6 +420,10 @@ export function useMainKeyboardHandler(): UseMainKeyboardHandlerReturn { e.preventDefault(); ctx.setDirectorNotesOpen?.(true); trackShortcut('directorNotes'); + } else if (ctx.isShortcut(e, 'agentInbox') && ctx.encoreFeatures?.unifiedInbox) { + e.preventDefault(); + ctx.setAgentInboxOpen?.(true); + trackShortcut('agentInbox'); } else if (ctx.isShortcut(e, 'jumpToBottom')) { e.preventDefault(); // Jump to the bottom of the current main panel output (AI logs or terminal output) diff --git a/src/renderer/stores/modalStore.ts b/src/renderer/stores/modalStore.ts index b385ea745..55301501c 100644 --- a/src/renderer/stores/modalStore.ts +++ b/src/renderer/stores/modalStore.ts @@ -218,7 +218,9 @@ export type ModalId = // Platform Warnings | 'windowsWarning' // Director's Notes - | 'directorNotes'; + | 'directorNotes' + // Agent Inbox (Unified Inbox) + | 'agentInbox'; /** * Type mapping from ModalId to its data type. @@ -757,6 +759,10 @@ export function getModalActions() { setDirectorNotesOpen: (open: boolean) => open ? openModal('directorNotes') : closeModal('directorNotes'), + // Agent Inbox Modal (Unified Inbox) + setAgentInboxOpen: (open: boolean) => + open ? openModal('agentInbox') : closeModal('agentInbox'), + // Lightbox refs replacement - use updateModalData instead setLightboxIsGroupChat: (isGroupChat: boolean) => updateModalData('lightbox', { isGroupChat }), setLightboxAllowDelete: (allowDelete: boolean) => updateModalData('lightbox', { allowDelete }), @@ -846,6 +852,7 @@ export function useModalActions() { const symphonyModalOpen = useModalStore(selectModalOpen('symphony')); const windowsWarningModalOpen = useModalStore(selectModalOpen('windowsWarning')); const directorNotesOpen = useModalStore(selectModalOpen('directorNotes')); + const agentInboxOpen = useModalStore(selectModalOpen('agentInbox')); // Get stable actions const actions = getModalActions(); @@ -1014,6 +1021,9 @@ export function useModalActions() { // Director's Notes Modal directorNotesOpen, + // Agent Inbox Modal (Unified Inbox) + agentInboxOpen, + // Lightbox ref replacements (now stored as data) lightboxIsGroupChat: lightboxData?.isGroupChat ?? false, lightboxAllowDelete: lightboxData?.allowDelete ?? false, From 72c1ec719b23725e00c1dd6629259a1444b6736e Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 11:10:14 -0300 Subject: [PATCH 09/43] MAESTRO: feat: add description display/edit UI to tab hover overlay Adds a description section to the AI tab hover overlay menu with two modes: - Display mode: FileText icon + description text (2-line clamp) or italic placeholder - Edit mode: auto-focus textarea with Enter to save, Shift+Enter for newline, Escape to cancel Feature-gated behind onUpdateTabDescription prop. Only calls handler when value actually changed. Co-Authored-By: Claude Opus 4.6 --- src/renderer/components/TabBar.tsx | 112 +++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx index e69d1be31..c258d29e5 100644 --- a/src/renderer/components/TabBar.tsx +++ b/src/renderer/components/TabBar.tsx @@ -20,6 +20,7 @@ import { Loader2, ExternalLink, FolderOpen, + FileText, } from 'lucide-react'; import type { AITab, Theme, FilePreviewTab, UnifiedTab } from '../types'; import { hasDraft } from '../utils/tabHelpers'; @@ -119,6 +120,8 @@ interface TabProps { onExportHtml?: (tabId: string) => void; /** Stable callback - receives tabId */ onPublishGist?: (tabId: string) => void; + /** Stable callback - receives tabId and new description */ + onUpdateTabDescription?: (tabId: string, description: string) => void; /** Stable callback - receives tabId */ onMoveToFirst?: (tabId: string) => void; /** Stable callback - receives tabId */ @@ -215,6 +218,7 @@ const Tab = memo(function Tab({ onCopyContext, onExportHtml, onPublishGist, + onUpdateTabDescription, onMoveToFirst, onMoveToLast, isFirstTab, @@ -232,6 +236,8 @@ const Tab = memo(function Tab({ const [isHovered, setIsHovered] = useState(false); const [overlayOpen, setOverlayOpen] = useState(false); const [showCopied, setShowCopied] = useState(false); + const [isEditingDescription, setIsEditingDescription] = useState(false); + const [descriptionDraft, setDescriptionDraft] = useState(tab.description ?? ''); const [overlayPosition, setOverlayPosition] = useState<{ top: number; left: number; @@ -454,6 +460,50 @@ const Tab = memo(function Tab({ [onCloseTabsRight, tabId] ); + // Description editing handlers + const descriptionButtonRef = useRef(null); + + const handleDescriptionSave = useCallback( + (value: string) => { + const trimmed = value.trim(); + if (trimmed !== (tab.description ?? '')) { + onUpdateTabDescription?.(tabId, trimmed); + } + setIsEditingDescription(false); + setDescriptionDraft(trimmed || (tab.description ?? '')); + }, + [onUpdateTabDescription, tabId, tab.description] + ); + + const handleDescriptionCancel = useCallback(() => { + setDescriptionDraft(tab.description ?? ''); + setIsEditingDescription(false); + }, [tab.description]); + + const handleDescriptionKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleDescriptionSave(descriptionDraft); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleDescriptionCancel(); + } + }, + [descriptionDraft, handleDescriptionSave, handleDescriptionCancel] + ); + + const handleDescriptionBlur = useCallback(() => { + handleDescriptionSave(descriptionDraft); + }, [descriptionDraft, handleDescriptionSave]); + + // Sync draft with tab.description when it changes externally + useEffect(() => { + if (!isEditingDescription) { + setDescriptionDraft(tab.description ?? ''); + } + }, [tab.description, isEditingDescription]); + // Handlers for drag events using stable tabId const handleTabSelect = useCallback(() => { onSelect(tabId); @@ -682,6 +732,66 @@ const Tab = memo(function Tab({ )} + {/* Description section - only render when feature is enabled */} + {onUpdateTabDescription && ( +
+ {isEditingDescription ? ( +