feat(terminal): Add task selection dropdown with auto-context loading

- Add associatedTaskId field to Terminal interface in terminal-store.ts
- Add setAssociatedTask action to update terminal task association
- Update TerminalGrid to pass tasks from task store to Terminal components
- Add task selection dropdown in Terminal header (shows when Claude active)
- Add tooltip showing task description when hovering over terminal title
- Implement handleTaskSelect callback that:
  - Updates terminal title to match selected task
  - Associates task with terminal
  - Sends formatted context message to Claude

The dropdown filters to only show tasks in 'backlog' (Planning) status.
When a task is selected, Claude receives a message with the task context
and prompts for confirmation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
AndyMik90
2025-12-13 22:51:49 +01:00
co-authored by Claude Opus 4.5
parent c38a1bd25c
commit 569129ea3f
3 changed files with 109 additions and 6 deletions
@@ -3,10 +3,24 @@ import { Terminal as XTerm } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';
import { X, Sparkles, TerminalSquare } from 'lucide-react';
import { X, Sparkles, TerminalSquare, ListTodo } from 'lucide-react';
import { Button } from './ui/button';
import { cn } from '../lib/utils';
import { useTerminalStore, type TerminalStatus } from '../stores/terminal-store';
import type { Task } from '../../shared/types';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from './ui/select';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from './ui/tooltip';
interface TerminalProps {
id: string;
@@ -15,6 +29,7 @@ interface TerminalProps {
isActive: boolean;
onClose: () => void;
onActivate: () => void;
tasks?: Task[]; // Tasks for task selection dropdown
}
const STATUS_COLORS: Record<TerminalStatus, string> = {
@@ -24,7 +39,7 @@ const STATUS_COLORS: Record<TerminalStatus, string> = {
exited: 'bg-destructive',
};
export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate }: TerminalProps) {
export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate, tasks = [] }: TerminalProps) {
const terminalRef = useRef<HTMLDivElement>(null);
const xtermRef = useRef<XTerm | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
@@ -37,6 +52,15 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate }
const setClaudeMode = useTerminalStore((state) => state.setClaudeMode);
const setClaudeSessionId = useTerminalStore((state) => state.setClaudeSessionId);
const updateTerminal = useTerminalStore((state) => state.updateTerminal);
const setAssociatedTask = useTerminalStore((state) => state.setAssociatedTask);
// Filter tasks to only show backlog (Planning) status tasks for dropdown
const backlogTasks = tasks.filter((t) => t.status === 'backlog');
// Find the currently associated task for tooltip
const associatedTask = terminal?.associatedTaskId
? tasks.find((t) => t.id === terminal.associatedTaskId)
: undefined;
const appendOutput = useTerminalStore((state) => state.appendOutput);
const clearOutputBuffer = useTerminalStore((state) => state.clearOutputBuffer);
@@ -96,13 +120,18 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate }
fitAddonRef.current = fitAddon;
// Replay buffered output if this is a remount (output exists in store)
// Skip replay for restored Claude sessions - they'll clear and resume fresh
// Then clear the buffer to prevent duplicate content on subsequent remounts
const terminalState = useTerminalStore.getState().terminals.find((t) => t.id === id);
if (terminalState?.outputBuffer) {
if (terminalState?.outputBuffer && !(terminalState.isRestored && terminalState.isClaudeMode)) {
xterm.write(terminalState.outputBuffer);
// Clear buffer after replay - new output will accumulate fresh
// This prevents duplicates when combined with full-screen redraws from TUI apps
useTerminalStore.getState().clearOutputBuffer(id);
} else if (terminalState?.isRestored && terminalState.isClaudeMode) {
// For restored Claude sessions, just clear the buffer without replay
// The session will clear screen and start fresh
useTerminalStore.getState().clearOutputBuffer(id);
}
// Handle terminal input - send to main process
@@ -313,6 +342,27 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate }
}
}, [onActivate]);
// Handle task selection from dropdown
const handleTaskSelect = useCallback((taskId: string) => {
const selectedTask = tasks.find((t) => t.id === taskId);
if (!selectedTask) return;
// Update terminal with task association and title
setAssociatedTask(id, taskId);
updateTerminal(id, { title: selectedTask.title });
// Format and send context message to Claude
const contextMessage = `I'm working on: ${selectedTask.title}
Description:
${selectedTask.description}
Please confirm you're ready by saying: I'm ready to work on ${selectedTask.title} - Context is loaded.`;
// Send the context message to the terminal
window.electronAPI.sendTerminalInput(id, contextMessage + '\r');
}, [id, tasks, setAssociatedTask, updateTerminal]);
return (
<div
className={cn(
@@ -327,9 +377,25 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate }
<div className={cn('h-2 w-2 rounded-full', STATUS_COLORS[terminal?.status || 'idle'])} />
<div className="flex items-center gap-1.5">
<TerminalSquare className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium text-foreground truncate max-w-32">
{terminal?.title || 'Terminal'}
</span>
{/* Terminal title with optional tooltip showing task description */}
{associatedTask ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs font-medium text-foreground truncate max-w-32 cursor-help">
{terminal?.title || 'Terminal'}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<p className="text-sm">{associatedTask.description}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span className="text-xs font-medium text-foreground truncate max-w-32">
{terminal?.title || 'Terminal'}
</span>
)}
</div>
{terminal?.isClaudeMode && (
<span className="flex items-center gap-1 text-[10px] font-medium text-primary bg-primary/10 px-1.5 py-0.5 rounded">
@@ -337,6 +403,28 @@ export function Terminal({ id, cwd, projectPath, isActive, onClose, onActivate }
Claude
</span>
)}
{/* Task selection dropdown - only show when Claude is active and there are backlog tasks */}
{terminal?.isClaudeMode && backlogTasks.length > 0 && (
<Select
value={terminal?.associatedTaskId || ''}
onValueChange={handleTaskSelect}
>
<SelectTrigger
className="h-6 w-auto min-w-[120px] max-w-[160px] text-[10px] px-2 py-0 border-border/50 bg-card/50"
onClick={(e) => e.stopPropagation()}
>
<ListTodo className="h-3 w-3 mr-1 text-muted-foreground" />
<SelectValue placeholder="Select task..." />
</SelectTrigger>
<SelectContent>
{backlogTasks.map((task) => (
<SelectItem key={task.id} value={task.id} className="text-xs">
<span className="truncate max-w-[200px]">{task.title}</span>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div className="flex items-center gap-1">
{!terminal?.isClaudeMode && terminal?.status !== 'exited' && (
@@ -9,6 +9,7 @@ import { Terminal } from './Terminal';
import { Button } from './ui/button';
import { cn } from '../lib/utils';
import { useTerminalStore } from '../stores/terminal-store';
import { useTaskStore } from '../stores/task-store';
interface TerminalGridProps {
projectPath?: string;
@@ -23,6 +24,9 @@ export function TerminalGrid({ projectPath }: TerminalGridProps) {
const canAddTerminal = useTerminalStore((state) => state.canAddTerminal);
const setClaudeMode = useTerminalStore((state) => state.setClaudeMode);
// Get tasks from task store for task selection dropdown in terminals
const tasks = useTaskStore((state) => state.tasks);
// Handle keyboard shortcut for new terminal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -167,6 +171,7 @@ export function TerminalGrid({ projectPath }: TerminalGridProps) {
isActive={terminal.id === activeTerminalId}
onClose={() => handleCloseTerminal(terminal.id)}
onActivate={() => setActiveTerminal(terminal.id)}
tasks={tasks}
/>
</div>
</Panel>
@@ -14,6 +14,7 @@ export interface Terminal {
claudeSessionId?: string; // Claude Code session ID for resume
outputBuffer: string; // Store terminal output for replay on remount
isRestored?: boolean; // Whether this terminal was restored from a saved session
associatedTaskId?: string; // ID of task associated with this terminal (for context loading)
}
interface TerminalLayout {
@@ -40,6 +41,7 @@ interface TerminalState {
setTerminalStatus: (id: string, status: TerminalStatus) => void;
setClaudeMode: (id: string, isClaudeMode: boolean) => void;
setClaudeSessionId: (id: string, sessionId: string) => void;
setAssociatedTask: (id: string, taskId: string | undefined) => void;
appendOutput: (id: string, data: string) => void;
clearOutputBuffer: (id: string) => void;
clearAllTerminals: () => void;
@@ -163,6 +165,14 @@ export const useTerminalStore = create<TerminalState>((set, get) => ({
}));
},
setAssociatedTask: (id: string, taskId: string | undefined) => {
set((state) => ({
terminals: state.terminals.map((t) =>
t.id === id ? { ...t, associatedTaskId: taskId } : t
),
}));
},
appendOutput: (id: string, data: string) => {
set((state) => ({
terminals: state.terminals.map((t) =>