mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-06 16:31:12 +08:00
Add orchestrator types and error handling configurations
- Introduced new TypeScript types for orchestrator functionality, including `SessionStrategy`, `ErrorHandlingStrategy`, and `OrchestrationStep`. - Defined interfaces for `OrchestrationPlan` and `ManualOrchestrationParams` to facilitate orchestration management. - Added a new PNG image file for visual representation. - Created a placeholder file named 'nul' for future use.
This commit is contained in:
@@ -6,19 +6,27 @@
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { X, Terminal as TerminalIcon } from 'lucide-react';
|
||||
import {
|
||||
X,
|
||||
Terminal as TerminalIcon,
|
||||
Plus,
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { Terminal as XTerm } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTerminalPanelStore } from '@/stores/terminalPanelStore';
|
||||
import { useCliSessionStore, type CliSessionMeta } from '@/stores/cliSessionStore';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { QueueExecutionListView } from './QueueExecutionListView';
|
||||
import {
|
||||
createCliSession,
|
||||
fetchCliSessionBuffer,
|
||||
sendCliSessionText,
|
||||
resizeCliSession,
|
||||
executeInCliSession,
|
||||
closeCliSession,
|
||||
} from '@/lib/api';
|
||||
|
||||
// ========== Types ==========
|
||||
@@ -33,11 +41,15 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const panelView = useTerminalPanelStore((s) => s.panelView);
|
||||
const activeTerminalId = useTerminalPanelStore((s) => s.activeTerminalId);
|
||||
const openTerminal = useTerminalPanelStore((s) => s.openTerminal);
|
||||
const removeTerminal = useTerminalPanelStore((s) => s.removeTerminal);
|
||||
|
||||
const sessions = useCliSessionStore((s) => s.sessions);
|
||||
const outputChunks = useCliSessionStore((s) => s.outputChunks);
|
||||
const setBuffer = useCliSessionStore((s) => s.setBuffer);
|
||||
const clearOutput = useCliSessionStore((s) => s.clearOutput);
|
||||
const upsertSession = useCliSessionStore((s) => s.upsertSession);
|
||||
const removeSessionFromStore = useCliSessionStore((s) => s.removeSession);
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
@@ -56,9 +68,12 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
|
||||
const pendingInputRef = useRef<string>('');
|
||||
const flushTimerRef = useRef<number | null>(null);
|
||||
|
||||
// Command execution
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
// Toolbar state
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
|
||||
// Available CLI tools
|
||||
const CLI_TOOLS = ['claude', 'gemini', 'qwen', 'codex', 'opencode'] as const;
|
||||
|
||||
const flushInput = useCallback(async () => {
|
||||
const sessionKey = activeTerminalId;
|
||||
@@ -187,34 +202,44 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
|
||||
return () => ro.disconnect();
|
||||
}, [activeTerminalId, projectPath]);
|
||||
|
||||
// ========== Command Execution ==========
|
||||
// ========== CLI Session Actions ==========
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (!activeTerminalId || !prompt.trim()) return;
|
||||
setIsExecuting(true);
|
||||
const sessionTool = (activeSession?.tool || 'claude') as 'claude' | 'codex' | 'gemini';
|
||||
const handleCreateSession = useCallback(async (tool: string) => {
|
||||
if (!projectPath || isCreating) return;
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await executeInCliSession(activeTerminalId, {
|
||||
tool: sessionTool,
|
||||
prompt: prompt.trim(),
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
}, projectPath || undefined);
|
||||
setPrompt('');
|
||||
const created = await createCliSession(
|
||||
{ workingDir: projectPath, tool },
|
||||
projectPath
|
||||
);
|
||||
upsertSession(created.session);
|
||||
openTerminal(created.session.sessionKey);
|
||||
} catch (err) {
|
||||
// Error shown in terminal output; log for DevTools debugging
|
||||
console.error('[TerminalMainArea] executeInCliSession failed:', err);
|
||||
console.error('[TerminalMainArea] createCliSession failed:', err);
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
}, [projectPath, isCreating, upsertSession, openTerminal]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void handleExecute();
|
||||
const handleCloseSession = useCallback(async () => {
|
||||
if (!activeTerminalId || isClosing) return;
|
||||
setIsClosing(true);
|
||||
try {
|
||||
await closeCliSession(activeTerminalId, projectPath || undefined);
|
||||
removeTerminal(activeTerminalId);
|
||||
removeSessionFromStore(activeTerminalId);
|
||||
} catch (err) {
|
||||
console.error('[TerminalMainArea] closeCliSession failed:', err);
|
||||
} finally {
|
||||
setIsClosing(false);
|
||||
}
|
||||
};
|
||||
}, [activeTerminalId, isClosing, projectPath, removeTerminal, removeSessionFromStore]);
|
||||
|
||||
const handleClearTerminal = useCallback(() => {
|
||||
const term = xtermRef.current;
|
||||
if (!term) return;
|
||||
term.clear();
|
||||
}, []);
|
||||
|
||||
// ========== Render ==========
|
||||
|
||||
@@ -242,59 +267,90 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
{panelView === 'terminal' && (
|
||||
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-border bg-muted/30">
|
||||
{/* New CLI session buttons */}
|
||||
{CLI_TOOLS.map((tool) => (
|
||||
<Button
|
||||
key={tool}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs gap-1"
|
||||
disabled={isCreating || !projectPath}
|
||||
onClick={() => handleCreateSession(tool)}
|
||||
title={`New ${tool} session`}
|
||||
>
|
||||
{isCreating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Plus className="h-3 w-3" />}
|
||||
{tool}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Terminal actions */}
|
||||
{activeTerminalId && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={handleClearTerminal}
|
||||
title="Clear terminal"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs text-destructive hover:text-destructive"
|
||||
disabled={isClosing}
|
||||
onClick={handleCloseSession}
|
||||
title="Close session"
|
||||
>
|
||||
{isClosing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{panelView === 'queue' ? (
|
||||
/* Queue View - Placeholder */
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<TerminalIcon className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-sm">{formatMessage({ id: 'home.terminalPanel.executionQueueDesc' })}</p>
|
||||
<p className="text-xs mt-1">{formatMessage({ id: 'home.terminalPanel.executionQueuePhase2' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
/* Queue View */
|
||||
<QueueExecutionListView />
|
||||
) : activeTerminalId ? (
|
||||
/* Terminal View */
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{/* xterm container */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<div
|
||||
ref={terminalHostRef}
|
||||
className="h-full w-full bg-black/90 rounded-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Command Input */}
|
||||
<div className="border-t border-border p-3 bg-card">
|
||||
<div className="space-y-2">
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={formatMessage({ id: 'home.terminalPanel.commandPlaceholder' })}
|
||||
className={cn(
|
||||
'w-full min-h-[60px] p-2 bg-background border border-input rounded-md text-sm resize-none',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleExecute}
|
||||
disabled={!activeTerminalId || isExecuting || !prompt.trim()}
|
||||
>
|
||||
{formatMessage({ id: 'home.terminalPanel.execute' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<div
|
||||
ref={terminalHostRef}
|
||||
className="h-full w-full bg-black/90 rounded-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Empty State */
|
||||
/* Empty State - with quick launch */
|
||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<TerminalIcon className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-sm">{formatMessage({ id: 'home.terminalPanel.noTerminalSelected' })}</p>
|
||||
<p className="text-xs mt-1">{formatMessage({ id: 'home.terminalPanel.selectTerminalHint' })}</p>
|
||||
<p className="text-xs mt-1 mb-4">{formatMessage({ id: 'home.terminalPanel.selectTerminalHint' })}</p>
|
||||
{projectPath && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{CLI_TOOLS.map((tool) => (
|
||||
<Button
|
||||
key={tool}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1"
|
||||
disabled={isCreating}
|
||||
onClick={() => handleCreateSession(tool)}
|
||||
>
|
||||
{isCreating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Plus className="h-3 w-3" />}
|
||||
{tool}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
// Left-side icon navigation bar (w-16) inside TerminalPanel.
|
||||
// Shows fixed queue entry icon + dynamic terminal icons with status badges.
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { ClipboardList, Terminal, Loader2, CheckCircle, XCircle, Circle } from 'lucide-react';
|
||||
import { ClipboardList, Terminal, Loader2, CheckCircle, XCircle, Circle, Plus } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTerminalPanelStore } from '@/stores/terminalPanelStore';
|
||||
import { useCliSessionStore, type CliSessionMeta, type CliSessionOutputChunk } from '@/stores/cliSessionStore';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { createCliSession } from '@/lib/api';
|
||||
|
||||
// ========== Status Badge Mapping ==========
|
||||
|
||||
@@ -48,11 +51,37 @@ export function TerminalNavBar() {
|
||||
const terminalOrder = useTerminalPanelStore((s) => s.terminalOrder);
|
||||
const setPanelView = useTerminalPanelStore((s) => s.setPanelView);
|
||||
const setActiveTerminal = useTerminalPanelStore((s) => s.setActiveTerminal);
|
||||
const openTerminal = useTerminalPanelStore((s) => s.openTerminal);
|
||||
|
||||
const sessions = useCliSessionStore((s) => s.sessions);
|
||||
const outputChunks = useCliSessionStore((s) => s.outputChunks);
|
||||
const upsertSession = useCliSessionStore((s) => s.upsertSession);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [showToolMenu, setShowToolMenu] = useState(false);
|
||||
|
||||
const CLI_TOOLS = ['claude', 'gemini', 'qwen', 'codex', 'opencode'] as const;
|
||||
|
||||
const handleCreateSession = useCallback(async (tool: string) => {
|
||||
if (!projectPath || isCreating) return;
|
||||
setIsCreating(true);
|
||||
setShowToolMenu(false);
|
||||
try {
|
||||
const created = await createCliSession(
|
||||
{ workingDir: projectPath, tool },
|
||||
projectPath
|
||||
);
|
||||
upsertSession(created.session);
|
||||
openTerminal(created.session.sessionKey);
|
||||
} catch (err) {
|
||||
console.error('[TerminalNavBar] createCliSession failed:', err);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [projectPath, isCreating, upsertSession, openTerminal]);
|
||||
|
||||
const handleQueueClick = () => {
|
||||
setPanelView('queue');
|
||||
};
|
||||
@@ -120,6 +149,44 @@ export function TerminalNavBar() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* New Terminal Button - Fixed at bottom */}
|
||||
<div className="relative">
|
||||
<div className="w-8 border-t border-border mb-2" />
|
||||
<button
|
||||
className={cn(
|
||||
'w-10 h-10 rounded-md flex items-center justify-center transition-colors hover:bg-accent',
|
||||
!projectPath && 'opacity-40 cursor-not-allowed'
|
||||
)}
|
||||
onClick={() => projectPath && setShowToolMenu(!showToolMenu)}
|
||||
disabled={isCreating || !projectPath}
|
||||
title={formatMessage({ id: 'home.terminalPanel.newSession' })}
|
||||
>
|
||||
{isCreating ? (
|
||||
<Loader2 className="h-5 w-5 text-muted-foreground animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Tool Selection Popup */}
|
||||
{showToolMenu && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowToolMenu(false)} />
|
||||
<div className="absolute left-full bottom-0 ml-1 z-50 bg-card border border-border rounded-md shadow-lg py-1 min-w-[120px]">
|
||||
{CLI_TOOLS.map((tool) => (
|
||||
<button
|
||||
key={tool}
|
||||
className="w-full text-left px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
||||
onClick={() => handleCreateSession(tool)}
|
||||
>
|
||||
{tool}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user