feat: enhance project context loading and feature flag support in dashboard components

This commit is contained in:
catlog22
2026-02-25 18:59:49 +08:00
parent db5797faa3
commit eb9a62e085
16 changed files with 336 additions and 109 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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' })}

View File

@@ -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 */}

View File

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

View File

@@ -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);
}
}

View File

@@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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[]>;
}
/**

View File

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