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:
catlog22
2026-02-15 12:38:32 +08:00
parent 731f1ea775
commit a897858c6a
15 changed files with 1818 additions and 182 deletions

View File

@@ -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>

View File

@@ -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">