mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-27 09:13:07 +08:00
feat: enhance project context loading and feature flag support in dashboard components
This commit is contained in:
@@ -54,6 +54,10 @@ When invoked with `process_docs: true` in input context:
|
||||
|
||||
## Input Context
|
||||
|
||||
**Project Context** (read from init.md products at startup):
|
||||
- `.workflow/project-tech.json` → tech_stack, architecture, key_components
|
||||
- `.workflow/project-guidelines.json` → conventions, constraints, quality_rules
|
||||
|
||||
```javascript
|
||||
{
|
||||
// Required
|
||||
|
||||
@@ -37,6 +37,8 @@ jq --arg ts "$(date -Iseconds)" '.status="in_progress" | .status_history += [{"f
|
||||
- Existing documentation and code examples
|
||||
- Project CLAUDE.md standards
|
||||
- **context-package.json** (when available in workflow tasks)
|
||||
- **project-tech.json** (if exists) → tech_stack, architecture, key_components
|
||||
- **project-guidelines.json** (if exists) → conventions, constraints, quality_rules
|
||||
|
||||
**Context Package** :
|
||||
`context-package.json` provides artifact paths - read using Read tool or ccw session:
|
||||
|
||||
@@ -35,6 +35,10 @@ Phase 5: Fix & Verification
|
||||
|
||||
## Phase 1: Bug Analysis
|
||||
|
||||
**Load Project Context** (from init.md products):
|
||||
- Read `.workflow/project-tech.json` (if exists) for tech stack context
|
||||
- Read `.workflow/project-guidelines.json` (if exists) for coding constraints
|
||||
|
||||
**Session Setup**:
|
||||
```javascript
|
||||
const bugSlug = bug_description.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 30)
|
||||
|
||||
@@ -26,6 +26,10 @@ color: green
|
||||
|
||||
### 1.1 Input Context
|
||||
|
||||
**Project Context** (load at startup):
|
||||
- Read `.workflow/project-tech.json` (if exists) → tech_stack, architecture
|
||||
- Read `.workflow/project-guidelines.json` (if exists) → constraints, conventions
|
||||
|
||||
```javascript
|
||||
{
|
||||
issue_ids: string[], // Issue IDs only (e.g., ["GH-123", "GH-124"])
|
||||
|
||||
@@ -471,11 +471,26 @@ ${recommendations.map(r => \`- ${r}\`).join('\\n')}
|
||||
|
||||
2. **Build Execution Context**
|
||||
|
||||
**Load Project Context** (from init.md products):
|
||||
```javascript
|
||||
// Read project-tech.json (if exists)
|
||||
const projectTech = file_exists('.workflow/project-tech.json')
|
||||
? JSON.parse(Read('.workflow/project-tech.json')) : null
|
||||
// Read project-guidelines.json (if exists)
|
||||
const projectGuidelines = file_exists('.workflow/project-guidelines.json')
|
||||
? JSON.parse(Read('.workflow/project-guidelines.json')) : null
|
||||
```
|
||||
|
||||
```javascript
|
||||
const executionContext = `
|
||||
⚠️ Execution Notes from Previous Tasks
|
||||
${relevantNotes} // Categorized notes with severity
|
||||
|
||||
📋 Project Context (from init.md)
|
||||
- Tech Stack: ${projectTech?.technology_analysis?.technology_stack || 'N/A'}
|
||||
- Architecture: ${projectTech?.technology_analysis?.architecture?.style || 'N/A'}
|
||||
- Constraints: ${projectGuidelines?.constraints || 'None defined'}
|
||||
|
||||
Current Task: ${task.id}
|
||||
- Original ID: ${task.original_id}
|
||||
- Source Plan: ${task.source_plan}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { toast } from '@/stores/notificationStore';
|
||||
import { useExecutionMonitorStore, selectActiveExecutionCount } from '@/stores/executionMonitorStore';
|
||||
import { useSessionManagerStore } from '@/stores/sessionManagerStore';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { CliConfigModal, type CliSessionConfig } from './CliConfigModal';
|
||||
|
||||
// ========== Types ==========
|
||||
@@ -94,6 +95,12 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
// Execution monitor count
|
||||
const executionCount = useExecutionMonitorStore(selectActiveExecutionCount);
|
||||
|
||||
// Feature flags for panel visibility
|
||||
const featureFlags = useConfigStore((s) => s.featureFlags);
|
||||
const showQueue = featureFlags.dashboardQueuePanelEnabled;
|
||||
const showInspector = featureFlags.dashboardInspectorEnabled;
|
||||
const showExecution = featureFlags.dashboardExecutionMonitorEnabled;
|
||||
|
||||
// Layout preset handler
|
||||
const resetLayout = useTerminalGridStore((s) => s.resetLayout);
|
||||
const handlePreset = useCallback(
|
||||
@@ -141,6 +148,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
tool: config.tool,
|
||||
model: config.model,
|
||||
launchMode: config.launchMode,
|
||||
settingsEndpointId: config.settingsEndpointId,
|
||||
},
|
||||
projectPath
|
||||
);
|
||||
@@ -209,27 +217,33 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
||||
onClick={() => onTogglePanel('issues')}
|
||||
badge={openCount > 0 ? openCount : undefined}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={ListChecks}
|
||||
label={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
||||
isActive={activePanel === 'queue'}
|
||||
onClick={() => onTogglePanel('queue')}
|
||||
badge={queueCount > 0 ? queueCount : undefined}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Info}
|
||||
label={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
||||
isActive={activePanel === 'inspector'}
|
||||
onClick={() => onTogglePanel('inspector')}
|
||||
dot={hasChain}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Activity}
|
||||
label={formatMessage({ id: 'terminalDashboard.toolbar.executionMonitor', defaultMessage: 'Execution Monitor' })}
|
||||
isActive={activePanel === 'execution'}
|
||||
onClick={() => onTogglePanel('execution')}
|
||||
badge={executionCount > 0 ? executionCount : undefined}
|
||||
/>
|
||||
{showQueue && (
|
||||
<ToolbarButton
|
||||
icon={ListChecks}
|
||||
label={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
||||
isActive={activePanel === 'queue'}
|
||||
onClick={() => onTogglePanel('queue')}
|
||||
badge={queueCount > 0 ? queueCount : undefined}
|
||||
/>
|
||||
)}
|
||||
{showInspector && (
|
||||
<ToolbarButton
|
||||
icon={Info}
|
||||
label={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
||||
isActive={activePanel === 'inspector'}
|
||||
onClick={() => onTogglePanel('inspector')}
|
||||
dot={hasChain}
|
||||
/>
|
||||
)}
|
||||
{showExecution && (
|
||||
<ToolbarButton
|
||||
icon={Activity}
|
||||
label={formatMessage({ id: 'terminalDashboard.toolbar.executionMonitor', defaultMessage: 'Execution Monitor' })}
|
||||
isActive={activePanel === 'execution'}
|
||||
onClick={() => onTogglePanel('execution')}
|
||||
badge={executionCount > 0 ? executionCount : undefined}
|
||||
/>
|
||||
)}
|
||||
<ToolbarButton
|
||||
icon={FolderOpen}
|
||||
label={formatMessage({ id: 'terminalDashboard.toolbar.files', defaultMessage: 'Files' })}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function FloatingPanel({
|
||||
style={{ top: '40px', bottom: 0, left: 0, right: 0 }}
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
<div className="absolute inset-0 bg-black/20 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Panel */}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Integrates with issueQueueIntegrationStore for selection state
|
||||
// and association chain highlighting.
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -14,8 +14,18 @@ import {
|
||||
AlertTriangle,
|
||||
CircleDot,
|
||||
Terminal,
|
||||
Check,
|
||||
Send,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/Select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useIssues } from '@/hooks/useIssues';
|
||||
import {
|
||||
@@ -29,6 +39,18 @@ import { useTerminalGridStore, selectTerminalGridFocusedPaneId, selectTerminalGr
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { toast } from '@/stores/notificationStore';
|
||||
|
||||
// ========== Execution Method Type ==========
|
||||
|
||||
type ExecutionMethod = 'skill-team-issue' | 'ccw-cli' | 'direct-send';
|
||||
|
||||
// ========== Prompt Templates ==========
|
||||
|
||||
const PROMPT_TEMPLATES: Record<ExecutionMethod, (idStr: string) => string> = {
|
||||
'skill-team-issue': (idStr) => `完成 ${idStr} issue`,
|
||||
'ccw-cli': (idStr) => `完成.issue.jsonl中 ${idStr} issue`,
|
||||
'direct-send': (idStr) => `根据@.workflow/issues/issues.jsonl中的 ${idStr} 需求,进行开发`,
|
||||
};
|
||||
|
||||
// ========== Priority Badge ==========
|
||||
|
||||
const PRIORITY_STYLES: Record<Issue['priority'], { variant: 'destructive' | 'warning' | 'info' | 'secondary'; label: string }> = {
|
||||
@@ -172,12 +194,49 @@ export function IssuePanel() {
|
||||
// Multi-select state
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [justSent, setJustSent] = useState(false);
|
||||
const [executionMethod, setExecutionMethod] = useState<ExecutionMethod>('skill-team-issue');
|
||||
const [isSendConfigOpen, setIsSendConfigOpen] = useState(false);
|
||||
const [customPrompt, setCustomPrompt] = useState('');
|
||||
const sentTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Terminal refs
|
||||
const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId);
|
||||
const panes = useTerminalGridStore(selectTerminalGridPanes);
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const sessionKey = focusedPaneId ? panes[focusedPaneId]?.sessionId : null;
|
||||
const focusedPane = focusedPaneId ? panes[focusedPaneId] : null;
|
||||
const sessionKey = focusedPane?.sessionId ?? null;
|
||||
const sessionCliTool = focusedPane?.cliTool ?? null;
|
||||
|
||||
// Compute available methods based on the focused session's CLI tool
|
||||
const availableMethods = useMemo(() => {
|
||||
// Only offer skill methods when the session is claude (supports / slash commands)
|
||||
if (sessionCliTool === 'claude') {
|
||||
return [
|
||||
{ value: 'skill-team-issue' as const, label: 'team-issue' },
|
||||
{ value: 'ccw-cli' as const, label: 'ccw' },
|
||||
{ value: 'direct-send' as const, label: 'Direct send' },
|
||||
];
|
||||
}
|
||||
// For unknown/null cliTool or non-claude tools, only offer direct send
|
||||
return [
|
||||
{ value: 'direct-send' as const, label: 'Direct send' },
|
||||
];
|
||||
}, [sessionCliTool]);
|
||||
|
||||
// Auto-switch method when the current selection is unavailable for this tool
|
||||
useEffect(() => {
|
||||
if (!availableMethods.find(m => m.value === executionMethod)) {
|
||||
setExecutionMethod(availableMethods[0].value);
|
||||
}
|
||||
}, [availableMethods, executionMethod]);
|
||||
|
||||
// Cleanup sent feedback timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (sentTimerRef.current) clearTimeout(sentTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Sort: open/in_progress first, then by priority (critical > high > medium > low)
|
||||
const sortedIssues = useMemo(() => {
|
||||
@@ -232,24 +291,60 @@ export function IssuePanel() {
|
||||
setSelectedIds(new Set());
|
||||
}, []);
|
||||
|
||||
const handleOpenSendConfig = useCallback(() => {
|
||||
const idStr = Array.from(selectedIds).join(' ');
|
||||
setCustomPrompt(PROMPT_TEMPLATES[executionMethod](idStr));
|
||||
setIsSendConfigOpen(true);
|
||||
}, [selectedIds, executionMethod]);
|
||||
|
||||
const handleSendToTerminal = useCallback(async () => {
|
||||
if (!sessionKey || selectedIds.size === 0) return;
|
||||
const effectiveTool = sessionCliTool || 'claude';
|
||||
setIsSending(true);
|
||||
try {
|
||||
await executeInCliSession(sessionKey, {
|
||||
tool: 'claude',
|
||||
prompt: Array.from(selectedIds).join(' '),
|
||||
instructionType: 'skill',
|
||||
skillName: 'team-issue',
|
||||
}, projectPath || undefined);
|
||||
toast.success('Sent to terminal', `/team-issue ${Array.from(selectedIds).join(' ')}`);
|
||||
setSelectedIds(new Set());
|
||||
const prompt = customPrompt.trim();
|
||||
|
||||
let executeInput: Parameters<typeof executeInCliSession>[1];
|
||||
|
||||
switch (executionMethod) {
|
||||
case 'skill-team-issue':
|
||||
executeInput = {
|
||||
tool: effectiveTool,
|
||||
prompt,
|
||||
instructionType: 'skill',
|
||||
skillName: 'team-issue',
|
||||
};
|
||||
break;
|
||||
case 'ccw-cli':
|
||||
executeInput = {
|
||||
tool: effectiveTool,
|
||||
prompt,
|
||||
instructionType: 'skill',
|
||||
skillName: 'ccw',
|
||||
};
|
||||
break;
|
||||
case 'direct-send':
|
||||
executeInput = {
|
||||
tool: effectiveTool,
|
||||
prompt,
|
||||
instructionType: 'prompt',
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
await executeInCliSession(sessionKey, executeInput, projectPath || undefined);
|
||||
|
||||
toast.success('Sent to terminal', prompt.length > 60 ? prompt.slice(0, 60) + '...' : prompt);
|
||||
setJustSent(true);
|
||||
setIsSendConfigOpen(false);
|
||||
if (sentTimerRef.current) clearTimeout(sentTimerRef.current);
|
||||
sentTimerRef.current = setTimeout(() => setJustSent(false), 2000);
|
||||
} catch (err) {
|
||||
toast.error('Failed to send', err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
}, [sessionKey, selectedIds, projectPath]);
|
||||
}, [sessionKey, selectedIds, projectPath, executionMethod, sessionCliTool, customPrompt]);
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
@@ -330,33 +425,107 @@ export function IssuePanel() {
|
||||
|
||||
{/* Send to Terminal bar */}
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="px-3 py-2 border-t border-border shrink-0 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{selectedIds.size} selected
|
||||
</span>
|
||||
<div className="border-t border-border shrink-0">
|
||||
{/* Send Config Panel (expandable) */}
|
||||
{isSendConfigOpen && (
|
||||
<div className="px-3 py-2 space-y-2 border-b border-border bg-muted/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-foreground">Send Configuration</span>
|
||||
<button
|
||||
type="button"
|
||||
className="p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setIsSendConfigOpen(false)}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/* Method selector */}
|
||||
{availableMethods.length > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">Method</span>
|
||||
<Select
|
||||
value={executionMethod}
|
||||
onValueChange={(v) => {
|
||||
const method = v as ExecutionMethod;
|
||||
setExecutionMethod(method);
|
||||
const idStr = Array.from(selectedIds).join(' ');
|
||||
setCustomPrompt(PROMPT_TEMPLATES[method](idStr));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-6 w-full text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableMethods.map((m) => (
|
||||
<SelectItem key={m.value} value={m.value} className="text-xs">
|
||||
{m.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{/* Prompt preview label */}
|
||||
{executionMethod !== 'direct-send' && (
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
Prefix: <span className="font-mono text-foreground">/{executionMethod === 'skill-team-issue' ? 'team-issue' : 'ccw'}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Editable prompt */}
|
||||
<textarea
|
||||
className="w-full text-xs bg-background border border-border rounded-md px-2 py-1.5 resize-none focus:outline-none focus:ring-1 focus:ring-primary/40 text-foreground"
|
||||
rows={3}
|
||||
value={customPrompt}
|
||||
onChange={(e) => setCustomPrompt(e.target.value)}
|
||||
placeholder="Enter prompt..."
|
||||
/>
|
||||
{/* Send button */}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'w-full flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors',
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
disabled={!sessionKey || isSending || !customPrompt.trim()}
|
||||
onClick={handleSendToTerminal}
|
||||
>
|
||||
{isSending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Send className="w-3.5 h-3.5" />}
|
||||
Confirm Send
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Bottom bar */}
|
||||
<div className="px-3 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{selectedIds.size} selected
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={handleDeselectAll}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={handleDeselectAll}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors',
|
||||
justSent
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
disabled={!sessionKey || isSending}
|
||||
onClick={isSendConfigOpen ? handleSendToTerminal : handleOpenSendConfig}
|
||||
title={!sessionKey ? 'No terminal session focused' : `Send via ${executionMethod}`}
|
||||
>
|
||||
Clear
|
||||
{isSending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : justSent ? <Check className="w-3.5 h-3.5" /> : <Terminal className="w-3.5 h-3.5" />}
|
||||
{justSent ? 'Sent!' : `Send (${selectedIds.size})`}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors',
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
disabled={!sessionKey || isSending}
|
||||
onClick={handleSendToTerminal}
|
||||
title={!sessionKey ? 'No terminal session focused' : 'Send /team-issue to terminal'}
|
||||
>
|
||||
{isSending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Terminal className="w-3.5 h-3.5" />}
|
||||
Send to Terminal ({selectedIds.size})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,8 @@ export function SessionGroupTree() {
|
||||
const focusedPaneId = useTerminalGridStore.getState().focusedPaneId;
|
||||
const targetPaneId = focusedPaneId || Object.keys(panes)[0];
|
||||
if (targetPaneId) {
|
||||
assignSession(targetPaneId, sessionId);
|
||||
const session = sessions[sessionId];
|
||||
assignSession(targetPaneId, sessionId, session?.cliTool ?? session?.tool ?? null);
|
||||
setFocused(targetPaneId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +186,10 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
const handleSessionChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
assignSession(paneId, value || null);
|
||||
const session = value ? sessions[value] : null;
|
||||
assignSession(paneId, value || null, session?.cliTool ?? session?.tool ?? null);
|
||||
},
|
||||
[paneId, assignSession]
|
||||
[paneId, assignSession, sessions]
|
||||
);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { ExecutionMonitorPanel } from '@/components/terminal-dashboard/Execution
|
||||
import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPanel';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
|
||||
// ========== Main Page Component ==========
|
||||
|
||||
@@ -36,6 +37,9 @@ export function TerminalDashboardPage() {
|
||||
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
// Feature flags for panel visibility
|
||||
const featureFlags = useConfigStore((s) => s.featureFlags);
|
||||
|
||||
// Use global immersive mode state (only affects AppShell chrome)
|
||||
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||
@@ -106,35 +110,41 @@ export function TerminalDashboardPage() {
|
||||
<IssuePanel />
|
||||
</FloatingPanel>
|
||||
|
||||
<FloatingPanel
|
||||
isOpen={activePanel === 'queue'}
|
||||
onClose={closePanel}
|
||||
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
||||
side="right"
|
||||
width={400}
|
||||
>
|
||||
<QueuePanel />
|
||||
</FloatingPanel>
|
||||
{featureFlags.dashboardQueuePanelEnabled && (
|
||||
<FloatingPanel
|
||||
isOpen={activePanel === 'queue'}
|
||||
onClose={closePanel}
|
||||
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
||||
side="right"
|
||||
width={400}
|
||||
>
|
||||
<QueuePanel />
|
||||
</FloatingPanel>
|
||||
)}
|
||||
|
||||
<FloatingPanel
|
||||
isOpen={activePanel === 'inspector'}
|
||||
onClose={closePanel}
|
||||
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
||||
side="right"
|
||||
width={360}
|
||||
>
|
||||
<InspectorContent />
|
||||
</FloatingPanel>
|
||||
{featureFlags.dashboardInspectorEnabled && (
|
||||
<FloatingPanel
|
||||
isOpen={activePanel === 'inspector'}
|
||||
onClose={closePanel}
|
||||
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
||||
side="right"
|
||||
width={360}
|
||||
>
|
||||
<InspectorContent />
|
||||
</FloatingPanel>
|
||||
)}
|
||||
|
||||
<FloatingPanel
|
||||
isOpen={activePanel === 'execution'}
|
||||
onClose={closePanel}
|
||||
title={formatMessage({ id: 'terminalDashboard.toolbar.executionMonitor', defaultMessage: 'Execution Monitor' })}
|
||||
side="right"
|
||||
width={380}
|
||||
>
|
||||
<ExecutionMonitorPanel />
|
||||
</FloatingPanel>
|
||||
{featureFlags.dashboardExecutionMonitorEnabled && (
|
||||
<FloatingPanel
|
||||
isOpen={activePanel === 'execution'}
|
||||
onClose={closePanel}
|
||||
title={formatMessage({ id: 'terminalDashboard.toolbar.executionMonitor', defaultMessage: 'Execution Monitor' })}
|
||||
side="right"
|
||||
width={380}
|
||||
>
|
||||
<ExecutionMonitorPanel />
|
||||
</FloatingPanel>
|
||||
)}
|
||||
</AssociationHighlightProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface CliSessionMeta {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isPaused: boolean;
|
||||
/** When set, this session is a native CLI interactive process. */
|
||||
cliTool?: string;
|
||||
}
|
||||
|
||||
export interface CliSessionOutputChunk {
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface TerminalPaneState {
|
||||
id: PaneId;
|
||||
/** Bound terminal session key (null = empty pane awaiting assignment) */
|
||||
sessionId: string | null;
|
||||
/** CLI tool used by the bound session (e.g. 'claude', 'gemini') */
|
||||
cliTool: string | null;
|
||||
/** Display mode: 'terminal' for terminal output, 'file' for file preview */
|
||||
displayMode: 'terminal' | 'file';
|
||||
/** File path for file preview mode (null when in terminal mode) */
|
||||
@@ -40,7 +42,7 @@ export interface TerminalGridActions {
|
||||
updateLayoutSizes: (sizes: number[]) => void;
|
||||
splitPane: (paneId: PaneId, direction: 'horizontal' | 'vertical') => PaneId;
|
||||
closePane: (paneId: PaneId) => void;
|
||||
assignSession: (paneId: PaneId, sessionId: string | null) => void;
|
||||
assignSession: (paneId: PaneId, sessionId: string | null, cliTool?: string | null) => void;
|
||||
setFocused: (paneId: PaneId) => void;
|
||||
resetLayout: (preset: 'single' | 'split-h' | 'split-v' | 'grid-2x2') => void;
|
||||
/** Create a new CLI session and assign it to a new pane (auto-split from specified pane) */
|
||||
@@ -69,6 +71,7 @@ const GRID_STORAGE_VERSION = 2;
|
||||
interface LegacyPaneState {
|
||||
id: PaneId;
|
||||
sessionId: string | null;
|
||||
cliTool?: string | null;
|
||||
displayMode?: 'terminal' | 'file';
|
||||
filePath?: string | null;
|
||||
}
|
||||
@@ -84,6 +87,7 @@ function migratePaneState(pane: LegacyPaneState): TerminalPaneState {
|
||||
return {
|
||||
id: pane.id,
|
||||
sessionId: pane.sessionId,
|
||||
cliTool: pane.cliTool ?? null,
|
||||
displayMode: pane.displayMode ?? 'terminal',
|
||||
filePath: pane.filePath ?? null,
|
||||
};
|
||||
@@ -117,7 +121,7 @@ function createInitialLayout(): { layout: AllotmentLayoutGroup; panes: Record<Pa
|
||||
const paneId = generatePaneId(1);
|
||||
return {
|
||||
layout: { direction: 'horizontal', sizes: [100], children: [paneId] },
|
||||
panes: { [paneId]: { id: paneId, sessionId: null, displayMode: 'terminal', filePath: null } },
|
||||
panes: { [paneId]: { id: paneId, sessionId: null, cliTool: null, displayMode: 'terminal', filePath: null } },
|
||||
focusedPaneId: paneId,
|
||||
nextPaneIdCounter: 2,
|
||||
};
|
||||
@@ -162,7 +166,7 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
layout: newLayout,
|
||||
panes: {
|
||||
...state.panes,
|
||||
[newPaneId]: { id: newPaneId, sessionId: null, displayMode: 'terminal', filePath: null },
|
||||
[newPaneId]: { id: newPaneId, sessionId: null, cliTool: null, displayMode: 'terminal', filePath: null },
|
||||
},
|
||||
focusedPaneId: newPaneId,
|
||||
nextPaneIdCounter: state.nextPaneIdCounter + 1,
|
||||
@@ -200,7 +204,7 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
);
|
||||
},
|
||||
|
||||
assignSession: (paneId, sessionId) => {
|
||||
assignSession: (paneId, sessionId, cliTool) => {
|
||||
const state = get();
|
||||
const pane = state.panes[paneId];
|
||||
if (!pane) return;
|
||||
@@ -209,7 +213,12 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
{
|
||||
panes: {
|
||||
...state.panes,
|
||||
[paneId]: { ...pane, sessionId },
|
||||
[paneId]: {
|
||||
...pane,
|
||||
sessionId,
|
||||
// Update cliTool when explicitly provided; clear when session is unassigned
|
||||
cliTool: cliTool !== undefined ? (cliTool ?? null) : (sessionId ? pane.cliTool : null),
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
@@ -228,7 +237,7 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
|
||||
const createPane = (): TerminalPaneState => {
|
||||
const id = generatePaneId(counter++);
|
||||
return { id, sessionId: null, displayMode: 'terminal', filePath: null };
|
||||
return { id, sessionId: null, cliTool: null, displayMode: 'terminal', filePath: null };
|
||||
};
|
||||
|
||||
let layout: AllotmentLayoutGroup;
|
||||
@@ -312,7 +321,7 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
{
|
||||
panes: {
|
||||
...state.panes,
|
||||
[paneId]: { ...currentPane, sessionId: session.sessionKey },
|
||||
[paneId]: { ...currentPane, sessionId: session.sessionKey, cliTool: session.tool ?? config.tool ?? null },
|
||||
},
|
||||
focusedPaneId: paneId,
|
||||
},
|
||||
@@ -331,7 +340,7 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
layout: newLayout,
|
||||
panes: {
|
||||
...state.panes,
|
||||
[newPaneId]: { id: newPaneId, sessionId: session.sessionKey, displayMode: 'terminal', filePath: null },
|
||||
[newPaneId]: { id: newPaneId, sessionId: session.sessionKey, cliTool: session.tool ?? config.tool ?? null, displayMode: 'terminal', filePath: null },
|
||||
},
|
||||
focusedPaneId: newPaneId,
|
||||
nextPaneIdCounter: state.nextPaneIdCounter + 1,
|
||||
|
||||
@@ -93,7 +93,8 @@ export async function handleCliSessionsRoutes(ctx: RouteContext): Promise<boolea
|
||||
tool,
|
||||
model,
|
||||
resumeKey,
|
||||
launchMode
|
||||
launchMode,
|
||||
settingsEndpointId
|
||||
} = (body || {}) as any;
|
||||
|
||||
if (tool && typeof tool === 'string') {
|
||||
@@ -114,11 +115,12 @@ export async function handleCliSessionsRoutes(ctx: RouteContext): Promise<boolea
|
||||
workingDir: desiredWorkingDir,
|
||||
cols: typeof cols === 'number' ? cols : undefined,
|
||||
rows: typeof rows === 'number' ? rows : undefined,
|
||||
preferredShell: preferredShell === 'pwsh' ? 'pwsh' : 'bash',
|
||||
preferredShell: preferredShell === 'pwsh' ? 'pwsh' : preferredShell === 'cmd' ? 'cmd' : 'bash',
|
||||
tool: typeof tool === 'string' ? tool.trim() : undefined,
|
||||
model,
|
||||
resumeKey,
|
||||
launchMode: launchMode === 'yolo' ? 'yolo' : 'default',
|
||||
settingsEndpointId: typeof settingsEndpointId === 'string' ? settingsEndpointId : undefined,
|
||||
});
|
||||
|
||||
appendCliSessionAudit({
|
||||
|
||||
@@ -489,7 +489,7 @@ async function fetchSkillDirectoryContents(skillPath: string): Promise<GitHubTre
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return response.json() as Promise<GitHubTreeEntry[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
16
package.json
16
package.json
@@ -9,8 +9,7 @@
|
||||
"ccw-mcp": "ccw/bin/ccw-mcp.js"
|
||||
},
|
||||
"workspaces": [
|
||||
"ccw/frontend",
|
||||
"ccw/docs-site"
|
||||
"ccw/frontend"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p ccw/tsconfig.json",
|
||||
@@ -21,14 +20,9 @@
|
||||
"prepublishOnly": "npm run build && node ccw/scripts/prepublish-clean.mjs && echo 'Ready to publish @dyw/claude-code-workflow'",
|
||||
"frontend": "npm run dev --workspace=ccw/frontend",
|
||||
"frontend:build": "npm run build --workspace=ccw/frontend",
|
||||
"docs": "npm run start --workspace=ccw/docs-site",
|
||||
"docs:en": "npm run start --workspace=ccw/docs-site -- --locale en --port 3001 --no-open",
|
||||
"docs:zh": "npm run start --workspace=ccw/docs-site -- --locale zh --port 3001 --no-open",
|
||||
"docs:serve": "npm run serve --workspace=ccw/docs-site -- --build --port 3001 --no-open",
|
||||
"docs:build": "npm run build --workspace=ccw/docs-site",
|
||||
"ws:install": "npm install",
|
||||
"ws:all": "concurrently \"npm run frontend\" \"npm run docs\" --names \"FRONTEND,DOCS\" --prefix-colors \"blue,green\"",
|
||||
"ws:build-all": "npm run build && npm run frontend:build && npm run docs:build",
|
||||
"ws:all": "npm run frontend",
|
||||
"ws:build-all": "npm run build && npm run frontend:build",
|
||||
"postinstall": "(npm rebuild better-sqlite3 || echo [CCW] better-sqlite3 rebuild skipped) && (npm rebuild node-pty || echo [CCW] node-pty rebuild skipped)"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -70,10 +64,7 @@
|
||||
"ccw/scripts/",
|
||||
".claude/agents/",
|
||||
".claude/commands/",
|
||||
".claude/output-styles/",
|
||||
".claude/scripts/",
|
||||
".claude/prompt-templates/",
|
||||
".claude/python_script/",
|
||||
".claude/skills/",
|
||||
".codex/",
|
||||
".gemini/",
|
||||
@@ -82,7 +73,6 @@
|
||||
"codex-lens/pyproject.toml",
|
||||
"ccw-litellm/src/ccw_litellm/",
|
||||
"ccw-litellm/pyproject.toml",
|
||||
"CLAUDE.md",
|
||||
"README.md"
|
||||
],
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user