diff --git a/src/__tests__/renderer/components/AgentTaskListBar.test.tsx b/src/__tests__/renderer/components/AgentTaskListBar.test.tsx new file mode 100644 index 0000000000..39cb0fc2ab --- /dev/null +++ b/src/__tests__/renderer/components/AgentTaskListBar.test.tsx @@ -0,0 +1,192 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AgentTaskListBar } from '../../../renderer/components/AgentTaskListBar'; +import { mockTheme } from '../../helpers/mockTheme'; +import type { LogEntry } from '../../../renderer/types'; + +// The bar reads two booleans off the settings store; stub them rather than +// hydrating the whole store from IPC. +let showBar = true; +let autoExpand = false; +vi.mock('../../../renderer/stores/settingsStore', () => ({ + useSettingsStore: ( + selector: (s: { showAgentTaskListBar: boolean; autoExpandAgentTaskListBar: boolean }) => unknown + ) => selector({ showAgentTaskListBar: showBar, autoExpandAgentTaskListBar: autoExpand }), +})); + +function todoEntry(id: string, statuses: string[]): LogEntry { + return { + id, + timestamp: 0, + source: 'tool', + text: 'TodoWrite', + metadata: { + toolState: { + status: 'completed', + input: { + todos: statuses.map((status, i) => ({ + content: `task ${i}`, + status, + activeForm: `doing task ${i}`, + })), + }, + }, + }, + } as unknown as LogEntry; +} + +describe('AgentTaskListBar', () => { + beforeEach(() => { + showBar = true; + autoExpand = false; + // The bar remembers its expanded state; start every case collapsed. + globalThis.localStorage?.clear(); + }); + + it('renders nothing until an agent writes a checklist', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('shows the active task and progress count, collapsed by default', () => { + render( + + ); + + expect(screen.getByText('doing task 1 (1/3)')).toBeInTheDocument(); + // Collapsed: the individual task rows are not rendered yet. + expect(screen.queryByText('task 2')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Expand agent task list' })).toHaveAttribute( + 'aria-expanded', + 'false' + ); + }); + + it('expands to the full list on click', () => { + render( + + ); + + fireEvent.click(screen.getByRole('button', { name: 'Expand agent task list' })); + + expect(screen.getByText('task 0')).toBeInTheDocument(); + expect(screen.getByText('doing task 1')).toBeInTheDocument(); + expect(screen.getByText('task 2')).toBeInTheDocument(); + }); + + it('tracks the newest checklist as the agent rewrites it', () => { + const { rerender } = render( + + ); + expect(screen.getByText('doing task 0 (0/2)')).toBeInTheDocument(); + + rerender( + + ); + expect(screen.getByText('doing task 1 (1/2)')).toBeInTheDocument(); + }); + + it('dismisses the current list, and comes back when the agent writes a new one', () => { + const first = todoEntry('a', ['in_progress', 'pending']); + const { rerender } = render(); + + fireEvent.click(screen.getByRole('button', { name: 'Hide this task list' })); + expect(screen.queryByTestId('agent-task-list-bar')).not.toBeInTheDocument(); + + // A re-render alone must not resurrect the dismissed list... + rerender(); + expect(screen.queryByTestId('agent-task-list-bar')).not.toBeInTheDocument(); + + // ...but the next checklist update does. + rerender( + + ); + expect(screen.getByTestId('agent-task-list-bar')).toBeInTheDocument(); + }); + + it('stays hidden when the setting is off', () => { + showBar = false; + const { container } = render( + + ); + expect(container).toBeEmptyDOMElement(); + }); + + it('ignores a checklist written inside a subagent', () => { + const nested = todoEntry('b', ['pending', 'pending']); + nested.metadata = { ...nested.metadata, parentToolUseId: 'task_1' }; + + render( + + ); + + // The parent's plan, not the delegated worker's private one. + expect(screen.getByText('doing task 0 (0/2)')).toBeInTheDocument(); + }); + + describe('with auto-expand on', () => { + beforeEach(() => { + autoExpand = true; + }); + + it('opens a new checklist to its full list', () => { + render( + + ); + + expect(screen.getByText('task 2')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Collapse agent task list' })).toHaveAttribute( + 'aria-expanded', + 'true' + ); + }); + + it('re-expands when the agent writes the next checklist, after a manual collapse', () => { + const first = todoEntry('a', ['in_progress', 'pending']); + const { rerender } = render(); + + fireEvent.click(screen.getByRole('button', { name: 'Collapse agent task list' })); + expect(screen.queryByText('task 1')).not.toBeInTheDocument(); + + // The collapse applies to THAT checklist only. + rerender( + + ); + expect(screen.getByText('task 0')).toBeInTheDocument(); + }); + + it('does not carry the sticky collapsed preference over', () => { + // A user who collapsed the bar with auto-expand OFF still gets the + // full list once they turn auto-expand ON. + globalThis.localStorage?.setItem('agentTaskList.bar.expanded', 'false'); + + render(); + + expect(screen.getByText('doing task 0')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/__tests__/renderer/utils/agentTaskList.test.ts b/src/__tests__/renderer/utils/agentTaskList.test.ts index 446c031a24..1945b087ce 100644 --- a/src/__tests__/renderer/utils/agentTaskList.test.ts +++ b/src/__tests__/renderer/utils/agentTaskList.test.ts @@ -1,8 +1,10 @@ import { describe, it, expect } from 'vitest'; import { extractAgentTaskList, + findLatestAgentTaskList, summarizeAgentTaskList, } from '../../../renderer/utils/agentTaskList'; +import type { LogEntry } from '../../../renderer/types'; describe('extractAgentTaskList', () => { it('extracts a Claude Code / OpenCode TodoWrite todos array', () => { @@ -128,3 +130,59 @@ describe('summarizeAgentTaskList', () => { expect(summarizeAgentTaskList(list)).toBe('Patch the parser (0/1)'); }); }); + +describe('findLatestAgentTaskList', () => { + const toolEntry = (id: string, input: unknown) => + ({ + id, + timestamp: 0, + source: 'tool', + text: 'TodoWrite', + metadata: { toolState: { status: 'completed', input } }, + }) as unknown as LogEntry; + + const todos = (...statuses: string[]) => ({ + todos: statuses.map((status, i) => ({ content: `task ${i}`, status })), + }); + + it('returns the newest checklist in the log', () => { + const latest = findLatestAgentTaskList([ + toolEntry('a', todos('in_progress', 'pending')), + toolEntry('b', { file_path: '/tmp/x.ts' }), + toolEntry('c', todos('completed', 'in_progress')), + ]); + + expect(latest?.entryId).toBe('c'); + expect(latest?.list.completed).toBe(1); + }); + + it('returns null when no tool call carried a checklist', () => { + expect(findLatestAgentTaskList([toolEntry('a', { command: 'ls' })])).toBeNull(); + expect(findLatestAgentTaskList([])).toBeNull(); + expect(findLatestAgentTaskList(undefined)).toBeNull(); + }); + + it('ignores non-tool entries', () => { + const chat = { id: 'msg', timestamp: 0, source: 'ai', text: 'todos: 1. do it' } as LogEntry; + expect(findLatestAgentTaskList([chat])).toBeNull(); + }); + + it("skips a subagent's private checklist", () => { + const nested = toolEntry('b', todos('pending', 'pending')); + nested.metadata = { ...nested.metadata, parentToolUseId: 'task_1' }; + + const latest = findLatestAgentTaskList([ + toolEntry('a', todos('in_progress', 'pending')), + nested, + ]); + + expect(latest?.entryId).toBe('a'); + }); + + it('returns null when the only checklist came from a subagent', () => { + const nested = toolEntry('a', todos('pending')); + nested.metadata = { ...nested.metadata, parentToolUseId: 'task_1' }; + + expect(findLatestAgentTaskList([nested])).toBeNull(); + }); +}); diff --git a/src/renderer/components/AgentTaskItems.tsx b/src/renderer/components/AgentTaskItems.tsx new file mode 100644 index 0000000000..d8ff568313 --- /dev/null +++ b/src/renderer/components/AgentTaskItems.tsx @@ -0,0 +1,55 @@ +import type { Theme } from '../types'; +import type { AgentTask } from '../utils/agentTaskList'; + +/** + * The task rows of an agent checklist: status glyph, label, completed styling. + * + * Shared by the two surfaces that render a checklist - the inline + * `AgentTaskListCard` in the transcript and the docked `AgentTaskListBar` above + * the composer - so "what a completed task looks like" is decided once. The + * surfaces differ in chrome and placement, not in how a task reads. + */ + +/** Status glyph + color for a single task row. */ +export function taskGlyph(task: AgentTask, theme: Theme): { glyph: string; color: string } { + if (task.status === 'completed') return { glyph: '✓', color: theme.colors.success }; + if (task.status === 'in_progress') return { glyph: '▸', color: theme.colors.warning }; + return { glyph: '○', color: theme.colors.textDim }; +} + +interface AgentTaskItemsProps { + theme: Theme; + tasks: AgentTask[]; + className?: string; +} + +export function AgentTaskItems({ theme, tasks, className }: AgentTaskItemsProps) { + return ( +
    + {tasks.map((task, index) => { + const { glyph, color } = taskGlyph(task, theme); + return ( +
  • + + + {task.status === 'in_progress' && task.activeForm ? task.activeForm : task.content} + +
  • + ); + })} +
+ ); +} diff --git a/src/renderer/components/AgentTaskListBar.tsx b/src/renderer/components/AgentTaskListBar.tsx new file mode 100644 index 0000000000..5d6af014fb --- /dev/null +++ b/src/renderer/components/AgentTaskListBar.tsx @@ -0,0 +1,131 @@ +import React, { useMemo, useState } from 'react'; +import { ListChecks, X } from 'lucide-react'; +import type { LogEntry, Theme } from '../types'; +import { findLatestAgentTaskList, summarizeAgentTaskList } from '../utils/agentTaskList'; +import { AgentTaskItems } from './AgentTaskItems'; +import { usePersistedToggle } from '../hooks/ui/usePersistedToggle'; +import { useSettingsStore } from '../stores/settingsStore'; + +/** + * The agent's current checklist, docked directly above the composer. + * + * `AgentTaskListCard` already renders every checklist update inline, but those + * cards scroll away as the conversation grows, so "what is the agent working on + * and how much is left" stops being answerable without scrolling back. This bar + * pins the NEWEST checklist in the active tab in place instead: collapsed to a + * one-line summary by default, expandable to the full list, and rewritten in + * place as the agent updates its plan. + * + * Nothing new is captured for it - the list is derived from the tab's own logs, + * so it survives tab switches and app restarts for free and there is no second + * copy of the state to fall out of sync with the transcript. + * + * Two Display settings drive it, both off by default: `showAgentTaskListBar` + * renders it at all, and `autoExpandAgentTaskListBar` opens each NEW checklist + * to its full list rather than the one-line summary. + */ + +interface AgentTaskListBarProps { + theme: Theme; + /** Conversation log of the active AI tab. */ + logs: LogEntry[] | undefined; +} + +export const AgentTaskListBar = React.memo(function AgentTaskListBar({ + theme, + logs, +}: AgentTaskListBarProps) { + const enabled = useSettingsStore((s) => s.showAgentTaskListBar); + const autoExpand = useSettingsStore((s) => s.autoExpandAgentTaskListBar); + const { value: stickyExpanded, toggle: toggleStickyExpanded } = usePersistedToggle( + 'agentTaskList.bar.expanded', + false + ); + // Keyed by the source log entry, so dismissing hides THIS list and the next + // checklist the agent writes brings the bar back on its own. + const [dismissedEntryId, setDismissedEntryId] = useState(null); + // Under auto-expand the sticky preference is not the answer: the bar opens + // for every new checklist, and a click only overrides THAT list. Keying the + // override off the entry id is what re-expands on the next update without an + // effect to re-sync a second copy of the state. + const [expandOverride, setExpandOverride] = useState<{ + entryId: string; + value: boolean; + } | null>(null); + + // PERF: the composer re-renders on every keystroke but `logs` only changes + // when the agent writes, so memoizing on it keeps the reverse scan off the + // typing path entirely. + const latest = useMemo(() => (enabled ? findLatestAgentTaskList(logs) : null), [enabled, logs]); + if (!latest || latest.entryId === dismissedEntryId) return null; + + const isExpanded = autoExpand + ? expandOverride?.entryId === latest.entryId + ? expandOverride.value + : true + : stickyExpanded; + const toggleExpanded = () => { + if (autoExpand) setExpandOverride({ entryId: latest.entryId, value: !isExpanded }); + else toggleStickyExpanded(); + }; + + const { tasks, completed } = latest.list; + const percent = tasks.length > 0 ? Math.round((completed / tasks.length) * 100) : 0; + const isComplete = completed === tasks.length; + + return ( +
+
+ + +
+ {isExpanded && ( + + )} +
+ ); +}); + +AgentTaskListBar.displayName = 'AgentTaskListBar'; diff --git a/src/renderer/components/AgentTaskListCard.tsx b/src/renderer/components/AgentTaskListCard.tsx index f61c1a8f44..1c1934b103 100644 --- a/src/renderer/components/AgentTaskListCard.tsx +++ b/src/renderer/components/AgentTaskListCard.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; import type { Theme } from '../types'; -import type { AgentTask, AgentTaskList } from '../utils/agentTaskList'; +import type { AgentTaskList } from '../utils/agentTaskList'; import { summarizeAgentTaskList } from '../utils/agentTaskList'; +import { AgentTaskItems } from './AgentTaskItems'; /** * Inline task list card for the chat history. @@ -13,6 +14,10 @@ import { summarizeAgentTaskList } from '../utils/agentTaskList'; * tool log has always shown. Clicking it expands the individual task items with * their states - the GUI equivalent of Claude Code's Ctrl+T overlay, kept inline * with the agent rather than in a separate panel. + * + * This is the historical, scroll-with-the-conversation view. `AgentTaskListBar` + * is its docked counterpart above the composer, for following the current list + * without scrolling back to find it. */ interface AgentTaskListCardProps { @@ -20,13 +25,6 @@ interface AgentTaskListCardProps { taskList: AgentTaskList; } -/** Status glyph + color for a single task row. */ -function taskGlyph(task: AgentTask, theme: Theme): { glyph: string; color: string } { - if (task.status === 'completed') return { glyph: '✓', color: theme.colors.success }; - if (task.status === 'in_progress') return { glyph: '▸', color: theme.colors.warning }; - return { glyph: '○', color: theme.colors.textDim }; -} - export function AgentTaskListCard({ theme, taskList }: AgentTaskListCardProps) { const [isExpanded, setIsExpanded] = useState(false); const { tasks, completed } = taskList; @@ -54,36 +52,7 @@ export function AgentTaskListCard({ theme, taskList }: AgentTaskListCardProps) { /> - {isExpanded && ( -
    - {tasks.map((task, index) => { - const { glyph, color } = taskGlyph(task, theme); - return ( -
  • - - - {task.status === 'in_progress' && task.activeForm - ? task.activeForm - : task.content} - -
  • - ); - })} -
- )} + {isExpanded && } ); } diff --git a/src/renderer/components/InputArea/InputArea.tsx b/src/renderer/components/InputArea/InputArea.tsx index 15351dc85e..8c672719da 100644 --- a/src/renderer/components/InputArea/InputArea.tsx +++ b/src/renderer/components/InputArea/InputArea.tsx @@ -13,6 +13,7 @@ import { CrossAgentResponseIndicator } from '../CrossAgentResponseIndicator'; import { getActiveTab } from '../../utils/tabHelpers'; import { MergeProgressOverlay } from '../MergeProgressOverlay'; import { ExecutionQueueIndicator } from '../ExecutionQueueIndicator'; +import { AgentTaskListBar } from '../AgentTaskListBar'; import { ContextWarningSash } from '../ContextWarningSash'; import { SummarizeProgressOverlay } from '../SummarizeProgressOverlay'; import { WizardInputPanel } from '../InlineWizard'; @@ -499,6 +500,10 @@ export const InputArea = React.memo(function InputArea(props: InputAreaProps) { /> )} + {/* AgentTaskListBar - the agent's current checklist, pinned above the + composer so it doesn't scroll away with the conversation. */} + {session.inputMode === 'ai' && } + + void; + autoExpandAgentTaskListBar: boolean; + setAutoExpandAgentTaskListBar: (value: boolean) => void; +} + +export function AgentTaskListSection({ + theme, + showAgentTaskListBar, + setShowAgentTaskListBar, + autoExpandAgentTaskListBar, + setAutoExpandAgentTaskListBar, +}: AgentTaskListSectionProps) { + return ( +
+ Agent Task List + +
+ +
+
+ +
+
+
+ ); +} diff --git a/src/renderer/components/Settings/tabs/DisplayTab/components/index.ts b/src/renderer/components/Settings/tabs/DisplayTab/components/index.ts index 33a97c6975..412dc2bdf2 100644 --- a/src/renderer/components/Settings/tabs/DisplayTab/components/index.ts +++ b/src/renderer/components/Settings/tabs/DisplayTab/components/index.ts @@ -1,4 +1,5 @@ export { AccessibilitySection } from './AccessibilitySection'; +export { AgentTaskListSection } from './AgentTaskListSection'; export { BionifyInfoModal } from './BionifyInfoModal'; export { ContextWarningsSection } from './ContextWarningsSection'; export { DocumentGraphSection } from './DocumentGraphSection'; diff --git a/src/renderer/hooks/settings/useSettings.ts b/src/renderer/hooks/settings/useSettings.ts index 8ef4e2b5ba..e83df29946 100644 --- a/src/renderer/hooks/settings/useSettings.ts +++ b/src/renderer/hooks/settings/useSettings.ts @@ -471,6 +471,14 @@ export interface UseSettingsReturn { showFullGroupLabelInBookmarks: boolean; setShowFullGroupLabelInBookmarks: (value: boolean) => void; + /** Show the agent's current checklist docked above the composer. */ + showAgentTaskListBar: boolean; + setShowAgentTaskListBar: (value: boolean) => void; + + /** Open that docked checklist in full whenever the agent writes a new one. */ + autoExpandAgentTaskListBar: boolean; + setAutoExpandAgentTaskListBar: (value: boolean) => void; + // File Edit & Preview fileEditWordWrap: boolean; setFileEditWordWrap: (value: boolean) => void; diff --git a/src/renderer/stores/settingsStore.ts b/src/renderer/stores/settingsStore.ts index 2ba660b1db..8d9467c926 100644 --- a/src/renderer/stores/settingsStore.ts +++ b/src/renderer/stores/settingsStore.ts @@ -428,6 +428,10 @@ export interface SettingsStoreState directorNotesSettings: DirectorNotesSettings; useNativeTitleBar: boolean; autoHideMenuBar: boolean; + /** Show the agent's current checklist docked above the composer. */ + showAgentTaskListBar: boolean; + /** Open that docked checklist in full whenever the agent writes a new one. */ + autoExpandAgentTaskListBar: boolean; // File Edit & Preview fileEditWordWrap: boolean; fileEditShowLineNumbers: boolean; @@ -538,6 +542,8 @@ export interface SettingsStoreActions setDirectorNotesSettings: (value: DirectorNotesSettings) => void; setUseNativeTitleBar: (value: boolean) => void; setAutoHideMenuBar: (value: boolean) => void; + setShowAgentTaskListBar: (value: boolean) => void; + setAutoExpandAgentTaskListBar: (value: boolean) => void; setFileEditWordWrap: (value: boolean) => void; setFileEditShowLineNumbers: (value: boolean) => void; setFilePreviewToolbarButtonVisibility: (button: FilePreviewToolbarButton, value: boolean) => void; @@ -775,6 +781,8 @@ export const useSettingsStore = create()((set, get, api) => { directorNotesSettings: DEFAULT_DIRECTOR_NOTES_SETTINGS, useNativeTitleBar: isWindowsPlatform(), autoHideMenuBar: false, + showAgentTaskListBar: false, + autoExpandAgentTaskListBar: false, fileEditWordWrap: true, fileEditShowLineNumbers: true, filePreviewToolbarVisibility: { ...DEFAULT_FILE_PREVIEW_TOOLBAR_VISIBILITY }, @@ -1315,6 +1323,16 @@ export const useSettingsStore = create()((set, get, api) => { window.maestro.settings.set('autoHideMenuBar', value); }, + setShowAgentTaskListBar: (value) => { + set({ showAgentTaskListBar: value }); + window.maestro.settings.set('showAgentTaskListBar', value); + }, + + setAutoExpandAgentTaskListBar: (value) => { + set({ autoExpandAgentTaskListBar: value }); + window.maestro.settings.set('autoExpandAgentTaskListBar', value); + }, + setFileEditWordWrap: (value) => { set({ fileEditWordWrap: value }); window.maestro.settings.set('fileEditWordWrap', value); @@ -2405,6 +2423,12 @@ export async function loadAllSettings(): Promise { hydrateLeftPanelDisplaySettings(allSettings, patch); + if (allSettings['showAgentTaskListBar'] !== undefined) + patch.showAgentTaskListBar = allSettings['showAgentTaskListBar'] as boolean; + + if (allSettings['autoExpandAgentTaskListBar'] !== undefined) + patch.autoExpandAgentTaskListBar = allSettings['autoExpandAgentTaskListBar'] as boolean; + if (allSettings['fileEditWordWrap'] !== undefined) patch.fileEditWordWrap = allSettings['fileEditWordWrap'] as boolean; @@ -2692,6 +2716,8 @@ export function getSettingsActions() { setShowLeftPanelGitIndicator: state.setShowLeftPanelGitIndicator, setShowLeftPanelCueIndicator: state.setShowLeftPanelCueIndicator, setShowLeftPanelStartupCommandIndicator: state.setShowLeftPanelStartupCommandIndicator, + setShowAgentTaskListBar: state.setShowAgentTaskListBar, + setAutoExpandAgentTaskListBar: state.setAutoExpandAgentTaskListBar, setFileEditWordWrap: state.setFileEditWordWrap, setFileEditShowLineNumbers: state.setFileEditShowLineNumbers, setFilePreviewToolbarButtonVisibility: state.setFilePreviewToolbarButtonVisibility, diff --git a/src/renderer/utils/agentTaskList.ts b/src/renderer/utils/agentTaskList.ts index f6f0df25a6..86ab798ec7 100644 --- a/src/renderer/utils/agentTaskList.ts +++ b/src/renderer/utils/agentTaskList.ts @@ -12,6 +12,8 @@ * the same structure gets the richer rendering for free. */ +import type { LogEntry } from '../types'; + /** Normalized task state shared by every agent's checklist format. */ export type AgentTaskStatus = 'pending' | 'in_progress' | 'completed'; @@ -119,3 +121,40 @@ export function summarizeAgentTaskList(list: AgentTaskList): string { if (!label) return `${tasks.length} tasks`; return `${label} (${completed}/${tasks.length})`; } + +/** A checklist plus the log entry it came from. */ +export interface LatestAgentTaskList { + /** + * Id of the tool entry the checklist was read from. The docked bar keys its + * dismissal off this, so dismissing hides that one list and the next + * checklist the agent writes brings the bar back. + */ + entryId: string; + list: AgentTaskList; +} + +/** + * Newest checklist in a tab's conversation, or null when the agent has not + * written one. Agents rewrite the whole list on every update, so the last + * checklist-shaped tool call in the log IS the current state - there is nothing + * to merge across entries. + * + * Checklists written INSIDE a subagent (`metadata.parentToolUseId`) are skipped. + * A delegated worker keeps its own private plan, and it is written last, so + * without this guard a Task tool call would replace the plan the user is + * actually following with a scratch list they never asked to see. + */ +export function findLatestAgentTaskList( + logs: readonly LogEntry[] | undefined +): LatestAgentTaskList | null { + if (!logs) return null; + for (let i = logs.length - 1; i >= 0; i--) { + const entry = logs[i]; + if (entry.metadata?.parentToolUseId) continue; + const toolState = entry.metadata?.toolState; + if (!toolState || toolState.input === undefined) continue; + const list = extractAgentTaskList(toolState.input); + if (list) return { entryId: entry.id, list }; + } + return null; +} diff --git a/src/shared/settingsMetadataAppearance.ts b/src/shared/settingsMetadataAppearance.ts index e15824573a..afc9f179bc 100644 --- a/src/shared/settingsMetadataAppearance.ts +++ b/src/shared/settingsMetadataAppearance.ts @@ -123,6 +123,20 @@ export const APPEARANCE_SETTINGS_METADATA: Record = { default: true, category: 'appearance', }, + showAgentTaskListBar: { + description: + "Dock the agent's current checklist (TodoWrite / update_plan) in a collapsible bar above the composer.", + type: 'boolean', + default: false, + category: 'appearance', + }, + autoExpandAgentTaskListBar: { + description: + 'Open the docked agent task list to its full checklist whenever the agent writes a new one, instead of the one-line summary. Requires showAgentTaskListBar.', + type: 'boolean', + default: false, + category: 'appearance', + }, showWorktreePill: { description: 'Show the WORKTREE badge next to worktree child agents in the left panel.', type: 'boolean',