diff --git a/src/components/TaskAITerminal.tsx b/src/components/TaskAITerminal.tsx new file mode 100644 index 00000000..9b06e56b --- /dev/null +++ b/src/components/TaskAITerminal.tsx @@ -0,0 +1,277 @@ +import { Show, For, createSignal, onMount, onCleanup } from 'solid-js'; +import { + store, + markAgentExited, + restartAgent, + switchAgent, + setLastPrompt, + markAgentOutput, + getFontScale, + registerFocusFn, + setTaskFocusedPanel, +} from '../store/store'; +import { ScalablePanel } from './ScalablePanel'; +import { InfoBar } from './InfoBar'; +import { TerminalView } from './TerminalView'; +import { theme } from '../lib/theme'; +import { sf } from '../lib/fontScale'; +import type { Task } from '../store/types'; +import type { PromptInputHandle } from './PromptInput'; + +interface TaskAITerminalProps { + task: Task; + isActive: boolean; + promptHandle: PromptInputHandle | undefined; +} + +export function TaskAITerminal(props: TaskAITerminalProps) { + const firstAgent = () => { + const ids = props.task.agentIds; + return ids.length > 0 ? store.agents[ids[0]] : undefined; + }; + + return ( + +
setTaskFocusedPanel(props.task.id, 'ai-terminal')} + > + { + if (props.task.lastPrompt && props.promptHandle && !props.promptHandle.getText()) + props.promptHandle.setText(props.task.lastPrompt); + }} + > + + {props.task.lastPrompt + ? `> ${props.task.lastPrompt}` + : props.task.initialPrompt + ? '⏳ Waiting to send prompt…' + : 'No prompts sent'} + + +
+ + {(a) => ( + <> + +
+ + {a().signal === 'spawn_failed' + ? 'Failed to start' + : `Process exited (${a().exitCode ?? '?'})`} + + + + + +
+
+ + markAgentExited(a().id, code)} + onData={(data) => markAgentOutput(a().id, data, props.task.id)} + onPromptDetected={(text) => setLastPrompt(props.task.id, text)} + onReady={(focusFn) => + registerFocusFn(`${props.task.id}:ai-terminal`, focusFn) + } + fontSize={Math.round(13 * getFontScale(`${props.task.id}:ai-terminal`))} + /> + + + )} +
+
+
+
+ ); +} + +/** Restart/switch-agent dropdown menu shown on the exit badge. */ +function AgentRestartMenu(props: { agentId: string; agentDefId: string }) { + const [showAgentMenu, setShowAgentMenu] = createSignal(false); + let menuRef: HTMLSpanElement | undefined; + + const handleClickOutside = (e: MouseEvent) => { + if (menuRef && !menuRef.contains(e.target as Node)) { + setShowAgentMenu(false); + } + }; + + onMount(() => document.addEventListener('mousedown', handleClickOutside)); + onCleanup(() => document.removeEventListener('mousedown', handleClickOutside)); + + return ( + (menuRef = el)}> + + + +
+
+ Restart with… +
+ ag.available !== false)}> + {(agentDef) => ( + + )} + +
+
+
+ ); +} diff --git a/src/components/TaskBranchInfoBar.tsx b/src/components/TaskBranchInfoBar.tsx new file mode 100644 index 00000000..6324c0db --- /dev/null +++ b/src/components/TaskBranchInfoBar.tsx @@ -0,0 +1,188 @@ +import { Show } from 'solid-js'; +import { store, getProject, showNotification } from '../store/store'; +import { revealItemInDir, openInEditor } from '../lib/shell'; +import { InfoBar } from './InfoBar'; +import { theme } from '../lib/theme'; +import { isMac } from '../lib/platform'; +import type { Task } from '../store/types'; + +interface TaskBranchInfoBarProps { + task: Task; + onEditProject: (projectId: string) => void; +} + +export function TaskBranchInfoBar(props: TaskBranchInfoBarProps) { + const editorTitle = () => + store.editorCommand + ? `Click to open in ${store.editorCommand} · ${isMac ? 'Cmd' : 'Ctrl'}+Click to reveal in file manager` + : 'Click to reveal in file manager'; + + const handleOpenInEditor = (e: MouseEvent) => { + if (store.editorCommand && !e.ctrlKey && !e.metaKey) { + openInEditor(store.editorCommand, props.task.worktreePath).catch((err) => + showNotification( + `Editor failed: ${err instanceof Error ? err.message : 'unknown error'}`, + ), + ); + } else { + revealItemInDir(props.task.worktreePath).catch((err) => + showNotification( + `Could not open folder: ${err instanceof Error ? err.message : String(err)}`, + ), + ); + } + }; + + return ( + + {(() => { + const project = getProject(props.task.projectId); + return ( + + {(p) => ( + + )} + + ); + })()} + + {(url) => ( + + )} + + + + + ); +} diff --git a/src/components/TaskNotesPanel.tsx b/src/components/TaskNotesPanel.tsx new file mode 100644 index 00000000..3f878835 --- /dev/null +++ b/src/components/TaskNotesPanel.tsx @@ -0,0 +1,261 @@ +import { Show, type Accessor } from 'solid-js'; +import { store, updateTaskNotes, setTaskFocusedPanel } from '../store/store'; +import { ResizablePanel } from './ResizablePanel'; +import { ScalablePanel } from './ScalablePanel'; +import { ChangedFilesList } from './ChangedFilesList'; +import { theme } from '../lib/theme'; +import { sf } from '../lib/fontScale'; +import type { Task } from '../store/types'; + +interface TaskNotesPanelProps { + task: Task; + isActive: boolean; + notesTab: Accessor<'notes' | 'plan'>; + setNotesTab: (tab: 'notes' | 'plan') => void; + planHtml: Accessor; + onPlanFullscreen: () => void; + onDiffFileClick: (path: string) => void; + notesRef: (el: HTMLTextAreaElement) => void; + planScrollRef: (el: HTMLDivElement) => void; + changedFilesRef: (el: HTMLDivElement) => void; +} + +export function TaskNotesPanel(props: TaskNotesPanelProps) { + let planScrollEl: HTMLDivElement | undefined; + + return ( + ( + +
setTaskFocusedPanel(props.task.id, 'notes')} + > + +
+ + +
+
+ + +