mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: Enhance team skill router with command architecture and role isolation rules
- Added command architecture section to skill router template, detailing role organization and command delegation. - Updated role router input parsing to reflect new file structure for roles. - Introduced role isolation rules to enforce strict boundaries on role responsibilities and output tagging. - Enhanced team configuration section to include role-specific guidelines and message bus requirements. feat: Improve terminal dashboard with session status indicators - Integrated terminal status indicators in the session group tree, displaying active, idle, error, paused, and resuming states. - Updated session click handling to focus on existing panes or assign sessions to available panes. feat: Add session lifecycle controls in terminal pane - Implemented restart, pause, and resume functionalities for terminal sessions with loading states. - Enhanced UI buttons for session control with appropriate loading indicators and tooltips. i18n: Update terminal dashboard localization for session controls - Added translations for restart, pause, and resume session actions in English and Chinese. chore: Create role command template for command file generation - Established a comprehensive template for generating command files in roles, including sections for strategy, execution steps, and error handling. - Included pre-built command patterns for common tasks like exploration, analysis, implementation, validation, review, dispatch, and monitoring.
This commit is contained in:
@@ -22,9 +22,21 @@ import {
|
||||
GripVertical,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useSessionManagerStore, selectGroups, selectSessionManagerActiveTerminalId } from '@/stores';
|
||||
import { useSessionManagerStore, selectGroups, selectSessionManagerActiveTerminalId, selectTerminalMetas } from '@/stores';
|
||||
import { useCliSessionStore } from '@/stores/cliSessionStore';
|
||||
import { useTerminalGridStore, selectTerminalGridPanes } from '@/stores/terminalGridStore';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import type { TerminalStatus } from '@/types/terminal-dashboard';
|
||||
|
||||
// ========== Status Dot Styles ==========
|
||||
|
||||
const statusDotStyles: Record<TerminalStatus, string> = {
|
||||
active: 'bg-green-500',
|
||||
idle: 'bg-gray-400',
|
||||
error: 'bg-red-500',
|
||||
paused: 'bg-yellow-500',
|
||||
resuming: 'bg-blue-400 animate-pulse',
|
||||
};
|
||||
|
||||
// ========== SessionGroupTree Component ==========
|
||||
|
||||
@@ -32,11 +44,17 @@ export function SessionGroupTree() {
|
||||
const { formatMessage } = useIntl();
|
||||
const groups = useSessionManagerStore(selectGroups);
|
||||
const activeTerminalId = useSessionManagerStore(selectSessionManagerActiveTerminalId);
|
||||
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
|
||||
const createGroup = useSessionManagerStore((s) => s.createGroup);
|
||||
const moveSessionToGroup = useSessionManagerStore((s) => s.moveSessionToGroup);
|
||||
const setActiveTerminal = useSessionManagerStore((s) => s.setActiveTerminal);
|
||||
const sessions = useCliSessionStore((s) => s.sessions);
|
||||
|
||||
// Grid store for pane management
|
||||
const panes = useTerminalGridStore(selectTerminalGridPanes);
|
||||
const assignSession = useTerminalGridStore((s) => s.assignSession);
|
||||
const setFocused = useTerminalGridStore((s) => s.setFocused);
|
||||
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleGroup = useCallback((groupId: string) => {
|
||||
@@ -58,9 +76,28 @@ export function SessionGroupTree() {
|
||||
|
||||
const handleSessionClick = useCallback(
|
||||
(sessionId: string) => {
|
||||
// Set active terminal in session manager
|
||||
setActiveTerminal(sessionId);
|
||||
|
||||
// Find pane that already has this session, or switch focused pane
|
||||
const paneWithSession = Object.entries(panes).find(
|
||||
([, pane]) => pane.sessionId === sessionId
|
||||
);
|
||||
|
||||
if (paneWithSession) {
|
||||
// Focus the pane that has this session
|
||||
setFocused(paneWithSession[0]);
|
||||
} else {
|
||||
// Find focused pane or first pane, and assign session to it
|
||||
const focusedPaneId = useTerminalGridStore.getState().focusedPaneId;
|
||||
const targetPaneId = focusedPaneId || Object.keys(panes)[0];
|
||||
if (targetPaneId) {
|
||||
assignSession(targetPaneId, sessionId);
|
||||
setFocused(targetPaneId);
|
||||
}
|
||||
}
|
||||
},
|
||||
[setActiveTerminal]
|
||||
[setActiveTerminal, panes, setFocused, assignSession]
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
@@ -168,38 +205,47 @@ export function SessionGroupTree() {
|
||||
{formatMessage({ id: 'terminalDashboard.sessionTree.emptyGroup' })}
|
||||
</p>
|
||||
) : (
|
||||
group.sessionIds.map((sessionId, index) => (
|
||||
<Draggable
|
||||
key={sessionId}
|
||||
draggableId={sessionId}
|
||||
index={index}
|
||||
>
|
||||
{(dragProvided, dragSnapshot) => (
|
||||
<div
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 mx-1 px-2 py-1.5 rounded-sm cursor-pointer',
|
||||
'hover:bg-muted/50 transition-colors text-sm',
|
||||
activeTerminalId === sessionId && 'bg-primary/10 text-primary',
|
||||
dragSnapshot.isDragging && 'bg-muted shadow-md'
|
||||
)}
|
||||
onClick={() => handleSessionClick(sessionId)}
|
||||
>
|
||||
<span
|
||||
{...dragProvided.dragHandleProps}
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground shrink-0"
|
||||
group.sessionIds.map((sessionId, index) => {
|
||||
const meta = terminalMetas[sessionId];
|
||||
const sessionStatus: TerminalStatus = meta?.status ?? 'idle';
|
||||
return (
|
||||
<Draggable
|
||||
key={sessionId}
|
||||
draggableId={sessionId}
|
||||
index={index}
|
||||
>
|
||||
{(dragProvided, dragSnapshot) => (
|
||||
<div
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 mx-1 px-2 py-1.5 rounded-sm cursor-pointer',
|
||||
'hover:bg-muted/50 transition-colors text-sm',
|
||||
activeTerminalId === sessionId && 'bg-primary/10 text-primary',
|
||||
dragSnapshot.isDragging && 'bg-muted shadow-md'
|
||||
)}
|
||||
onClick={() => handleSessionClick(sessionId)}
|
||||
>
|
||||
<GripVertical className="w-3 h-3" />
|
||||
</span>
|
||||
<Terminal className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="flex-1 truncate text-xs">
|
||||
{sessionNames[sessionId] ?? sessionId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))
|
||||
<span
|
||||
{...dragProvided.dragHandleProps}
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground shrink-0"
|
||||
>
|
||||
<GripVertical className="w-3 h-3" />
|
||||
</span>
|
||||
{/* Status indicator dot */}
|
||||
<span
|
||||
className={cn('w-2 h-2 rounded-full shrink-0', statusDotStyles[sessionStatus])}
|
||||
title={sessionStatus}
|
||||
/>
|
||||
<Terminal className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
||||
<span className="flex-1 truncate text-xs">
|
||||
{sessionNames[sessionId] ?? sessionId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Single terminal pane = PaneToolbar + TerminalInstance.
|
||||
// Renders within the TerminalGrid recursive layout.
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
SplitSquareHorizontal,
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
X,
|
||||
Terminal,
|
||||
ChevronDown,
|
||||
RotateCcw,
|
||||
Pause,
|
||||
Play,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TerminalInstance } from './TerminalInstance';
|
||||
@@ -76,6 +80,15 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
|
||||
const sessions = useCliSessionStore((s) => s.sessions);
|
||||
|
||||
// Session lifecycle actions
|
||||
const pauseSession = useSessionManagerStore((s) => s.pauseSession);
|
||||
const resumeSession = useSessionManagerStore((s) => s.resumeSession);
|
||||
const restartSession = useSessionManagerStore((s) => s.restartSession);
|
||||
|
||||
// Action loading states
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const [isTogglingPause, setIsTogglingPause] = useState(false);
|
||||
|
||||
// Association chain for linked issue badge
|
||||
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
|
||||
const linkedIssueId = useMemo(() => {
|
||||
@@ -133,6 +146,34 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
}
|
||||
}, [paneId, sessionId, assignSession]);
|
||||
|
||||
const handleRestart = useCallback(async () => {
|
||||
if (!sessionId || isRestarting) return;
|
||||
setIsRestarting(true);
|
||||
try {
|
||||
await restartSession(sessionId);
|
||||
} catch (error) {
|
||||
console.error('[TerminalPane] Restart failed:', error);
|
||||
} finally {
|
||||
setIsRestarting(false);
|
||||
}
|
||||
}, [sessionId, isRestarting, restartSession]);
|
||||
|
||||
const handleTogglePause = useCallback(async () => {
|
||||
if (!sessionId || isTogglingPause) return;
|
||||
setIsTogglingPause(true);
|
||||
try {
|
||||
if (status === 'paused') {
|
||||
await resumeSession(sessionId);
|
||||
} else if (status === 'active' || status === 'idle') {
|
||||
await pauseSession(sessionId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[TerminalPane] Toggle pause failed:', error);
|
||||
} finally {
|
||||
setIsTogglingPause(false);
|
||||
}
|
||||
}, [sessionId, isTogglingPause, status, pauseSession, resumeSession]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -197,13 +238,58 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
<SplitSquareVertical className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
{sessionId && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
|
||||
title={formatMessage({ id: 'terminalDashboard.pane.clearTerminal' })}
|
||||
>
|
||||
<Eraser className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<>
|
||||
{/* Restart button */}
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
disabled={isRestarting}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-muted transition-colors',
|
||||
isRestarting ? 'text-muted-foreground/50' : 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
title={formatMessage({ id: 'terminalDashboard.pane.restart' })}
|
||||
>
|
||||
{isRestarting ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
{/* Pause/Resume toggle button */}
|
||||
<button
|
||||
onClick={handleTogglePause}
|
||||
disabled={isTogglingPause || status === 'resuming'}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-muted transition-colors',
|
||||
isTogglingPause || status === 'resuming'
|
||||
? 'text-muted-foreground/50'
|
||||
: status === 'paused'
|
||||
? 'text-yellow-500 hover:text-yellow-600'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
title={formatMessage({
|
||||
id: status === 'paused'
|
||||
? 'terminalDashboard.pane.resume'
|
||||
: 'terminalDashboard.pane.pause',
|
||||
})}
|
||||
>
|
||||
{isTogglingPause || status === 'resuming' ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : status === 'paused' ? (
|
||||
<Play className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Pause className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
{/* Clear terminal button */}
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
|
||||
title={formatMessage({ id: 'terminalDashboard.pane.clearTerminal' })}
|
||||
>
|
||||
<Eraser className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{alertCount > 0 && (
|
||||
<span className="flex items-center gap-0.5 px-1 text-destructive">
|
||||
|
||||
Reference in New Issue
Block a user