feat: add Mark as Unread option to tab context menu

Add a new menu item in the tab hover overlay that allows users to
manually mark a tab as unread. This is useful for using the unread
indicator as a todo/reminder system.

Changes:
- Add onTabMarkUnread callback prop to TabBar and MainPanel
- Add "Mark as Unread" menu item with Mail icon in tab overlay
- Implement handler in App.tsx that sets hasUnread: true on the tab
- Add test case for the new functionality

Claude ID: 4884b984-82ed-4c6d-a2ac-834040680db0
Maestro ID: b9bc0d08-5be2-4fdf-93cd-5618a8d53b35
This commit is contained in:
Pedram Amini
2025-12-15 22:24:46 -06:00
parent 3758ee0f37
commit fa2e087b76
4 changed files with 65 additions and 1 deletions

View File

@@ -86,6 +86,7 @@ describe('TabBar', () => {
const mockOnTabReorder = vi.fn();
const mockOnCloseOthers = vi.fn();
const mockOnTabStar = vi.fn();
const mockOnTabMarkUnread = vi.fn();
const mockOnToggleUnreadFilter = vi.fn();
const mockOnOpenTabSearch = vi.fn();
@@ -1190,6 +1191,35 @@ describe('TabBar', () => {
expect(mockOnRequestRename).toHaveBeenCalledWith('tab-1');
});
it('calls onTabMarkUnread when Mark as Unread clicked', async () => {
const tabs = [createTab({
id: 'tab-1',
name: 'Tab 1',
claudeSessionId: 'abc123'
})];
render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockTheme}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onTabMarkUnread={mockOnTabMarkUnread}
/>
);
const tab = screen.getByText('Tab 1').closest('[data-tab-id]')!;
fireEvent.mouseEnter(tab);
act(() => {
vi.advanceTimersByTime(450);
});
fireEvent.click(screen.getByText('Mark as Unread'));
expect(mockOnTabMarkUnread).toHaveBeenCalledWith('tab-1');
});
it('displays session name in overlay header', async () => {
const tabs = [createTab({
id: 'tab-1',

View File

@@ -5196,6 +5196,18 @@ export default function MaestroConsole() {
};
}));
}}
onTabMarkUnread={(tabId: string) => {
if (!activeSession) return;
setSessions(prev => prev.map(s => {
if (s.id !== activeSession.id) return s;
return {
...s,
aiTabs: s.aiTabs.map(t =>
t.id === tabId ? { ...t, hasUnread: true } : t
)
};
}));
}}
onToggleTabReadOnlyMode={() => {
if (!activeSession) return;
const activeTab = getActiveTab(activeSession);

View File

@@ -144,6 +144,7 @@ interface MainPanelProps {
onTabReorder?: (fromIndex: number, toIndex: number) => void;
onCloseOtherTabs?: (tabId: string) => void;
onTabStar?: (tabId: string, starred: boolean) => void;
onTabMarkUnread?: (tabId: string) => void;
onUpdateTabByClaudeSessionId?: (claudeSessionId: string, updates: { name?: string | null; starred?: boolean }) => void;
onToggleTabReadOnlyMode?: () => void;
onToggleTabSaveToHistory?: () => void;
@@ -212,7 +213,7 @@ export const MainPanel = forwardRef<MainPanelHandle, MainPanelProps>(function Ma
const headerRef = useRef<HTMLDivElement>(null);
// Extract tab handlers from props
const { onTabSelect, onTabClose, onNewTab, onTabRename, onRequestTabRename, onTabReorder, onCloseOtherTabs, onTabStar, showUnreadOnly, onToggleUnreadFilter, onOpenTabSearch } = props;
const { onTabSelect, onTabClose, onNewTab, onTabRename, onRequestTabRename, onTabReorder, onCloseOtherTabs, onTabStar, onTabMarkUnread, showUnreadOnly, onToggleUnreadFilter, onOpenTabSearch } = props;
// Get the active tab for header display
// The header should show the active tab's data (UUID, name, cost, context), not session-level data
@@ -848,6 +849,7 @@ export const MainPanel = forwardRef<MainPanelHandle, MainPanelProps>(function Ma
onTabReorder={onTabReorder}
onCloseOthers={onCloseOtherTabs}
onTabStar={onTabStar}
onTabMarkUnread={onTabMarkUnread}
showUnreadOnly={showUnreadOnly}
onToggleUnreadFilter={onToggleUnreadFilter}
onOpenTabSearch={onOpenTabSearch}

View File

@@ -15,6 +15,7 @@ interface TabBarProps {
onTabReorder?: (fromIndex: number, toIndex: number) => void;
onCloseOthers?: (tabId: string) => void;
onTabStar?: (tabId: string, starred: boolean) => void;
onTabMarkUnread?: (tabId: string) => void;
showUnreadOnly?: boolean;
onToggleUnreadFilter?: () => void;
onOpenTabSearch?: () => void;
@@ -36,6 +37,7 @@ interface TabProps {
isDragOver: boolean;
onRename: () => void;
onStar?: (starred: boolean) => void;
onMarkUnread?: () => void;
shortcutHint?: number | null;
registerRef?: (el: HTMLDivElement | null) => void;
hasDraft?: boolean;
@@ -77,6 +79,7 @@ function Tab({
isDragOver,
onRename,
onStar,
onMarkUnread,
shortcutHint,
registerRef,
hasDraft
@@ -163,6 +166,12 @@ function Tab({
setOverlayOpen(false);
};
const handleMarkUnreadClick = (e: React.MouseEvent) => {
e.stopPropagation();
onMarkUnread?.();
setOverlayOpen(false);
};
const displayName = getTabDisplayName(tab);
// Browser-style tab: all tabs have borders, active tab "connects" to content
@@ -364,6 +373,15 @@ function Tab({
<Edit2 className="w-3.5 h-3.5" style={{ color: theme.colors.textDim }} />
Rename Tab
</button>
<button
onClick={handleMarkUnreadClick}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs hover:bg-white/10 transition-colors"
style={{ color: theme.colors.textMain }}
>
<Mail className="w-3.5 h-3.5" style={{ color: theme.colors.textDim }} />
Mark as Unread
</button>
</div>
</div>,
document.body
@@ -389,6 +407,7 @@ export function TabBar({
onTabReorder,
onCloseOthers,
onTabStar,
onTabMarkUnread,
showUnreadOnly: showUnreadOnlyProp,
onToggleUnreadFilter,
onOpenTabSearch
@@ -585,6 +604,7 @@ export function TabBar({
isDragOver={dragOverTabId === tab.id}
onRename={() => handleRenameRequest(tab.id)}
onStar={onTabStar ? (starred) => onTabStar(tab.id, starred) : undefined}
onMarkUnread={onTabMarkUnread ? () => onTabMarkUnread(tab.id) : undefined}
shortcutHint={!showUnreadOnly && originalIndex < 9 ? originalIndex + 1 : null}
hasDraft={hasDraft(tab)}
registerRef={(el) => {