From d43f603a354960816978359a650ef2ae0ef8fbd8 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:51:39 -0300 Subject: [PATCH 1/7] 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 74fdb2b14f..494b836b9d 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 a770cdb623141e75e20734354aa42532f32316e6 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 10:56:59 -0300 Subject: [PATCH 2/7] 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 703155a818..b836dd1ac3 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 dfb59d4f71..5d6edcd4ff 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 889040adbc5842fdbf645a6d73e872f47dae63c4 Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 11:02:37 -0300 Subject: [PATCH 3/7] 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 4f31351c31..0a8f917f1f 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 86fe27ff25..9384d5af21 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 6cd3090e95..e69d1be31f 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 dd4bd232bb..3b34fd7989 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 c10e6071e9..d7c52f586f 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 494b836b9d..8a2a58b09e 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 72c1ec719b23725e00c1dd6629259a1444b6736e Mon Sep 17 00:00:00 2001 From: Felipe Gobbi Date: Fri, 27 Feb 2026 11:10:14 -0300 Subject: [PATCH 4/7] 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 e69d1be31f..c258d29e5c 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 ? ( +