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:
co-authored by
Claude Opus 4.5
parent
c38a1bd25c
commit
569129ea3f
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user