diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e244b5..4116b3f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [3.12.1](https://github.com/breaking-brake/cc-wf-studio/compare/v3.12.0...v3.12.1) (2026-01-04) + +### Improvements + +* add on-canvas description panel for workflow editing ([#360](https://github.com/breaking-brake/cc-wf-studio/issues/360)) ([ee9fa11](https://github.com/breaking-brake/cc-wf-studio/commit/ee9fa1127f8e108198c6609970f00e9c79a6ffeb)) + ## [3.12.0](https://github.com/breaking-brake/cc-wf-studio/compare/v3.11.3...v3.12.0) (2026-01-03) ### Features diff --git a/package-lock.json b/package-lock.json index 01d921e6..d8cd4681 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-wf-studio", - "version": "3.12.0", + "version": "3.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio", - "version": "3.12.0", + "version": "3.12.1", "license": "AGPL-3.0-or-later", "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", diff --git a/package.json b/package.json index b688e3a8..7875a52a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cc-wf-studio", "displayName": "Claude Code Workflow Studio", "description": "Visual workflow editor for Claude Code Slash Commands, Sub Agents, Agent Skills, and MCP Tools", - "version": "3.12.0", + "version": "3.12.1", "publisher": "breaking-brake", "icon": "resources/icon.png", "repository": { diff --git a/src/webview/package-lock.json b/src/webview/package-lock.json index e8da94e5..a7ad3a06 100644 --- a/src/webview/package-lock.json +++ b/src/webview/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-wf-studio-webview", - "version": "3.12.0", + "version": "3.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio-webview", - "version": "3.12.0", + "version": "3.12.1", "license": "AGPL-3.0-or-later", "dependencies": { "@radix-ui/react-collapsible": "^1.1.12", diff --git a/src/webview/package.json b/src/webview/package.json index bc07003c..b5bb1cac 100644 --- a/src/webview/package.json +++ b/src/webview/package.json @@ -1,6 +1,6 @@ { "name": "cc-wf-studio-webview", - "version": "3.12.0", + "version": "3.12.1", "private": true, "license": "AGPL-3.0-or-later", "type": "module", diff --git a/src/webview/src/components/DescriptionPanel.tsx b/src/webview/src/components/DescriptionPanel.tsx new file mode 100644 index 00000000..93fe478e --- /dev/null +++ b/src/webview/src/components/DescriptionPanel.tsx @@ -0,0 +1,423 @@ +/** + * Claude Code Workflow Studio - Description Panel Component + * + * Collapsible panel for editing workflow description. + * Follows the MinimapContainer pattern for toggle functionality. + * Includes AI-powered description generation. + */ + +import { Minus, NotepadText } from 'lucide-react'; +import type React from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from '../i18n/i18n-context'; +import { + cancelSlackDescriptionGeneration, + generateSlackDescription, +} from '../services/slack-integration-service'; +import { serializeWorkflow } from '../services/workflow-service'; +import { useWorkflowStore } from '../stores/workflow-store'; +import { AiGenerateButton } from './common/AiGenerateButton'; +import { EditInEditorButton } from './common/EditInEditorButton'; +import { StyledTooltip } from './common/StyledTooltip'; + +/** + * DescriptionPanel Component + * + * Collapsible panel for workflow description editing. + * When collapsed, shows only a NotepadText icon button to expand. + * When expanded, shows a textarea with AI generation button. + */ +export const DescriptionPanel: React.FC = () => { + const { t, locale } = useTranslation(); + const { + isDescriptionPanelVisible, + toggleDescriptionPanelVisibility, + workflowDescription, + setWorkflowDescription, + nodes, + edges, + activeWorkflow, + workflowName, + subAgentFlows, + } = useWorkflowStore(); + + const [isGeneratingDescription, setIsGeneratingDescription] = useState(false); + const [isEditingInEditor, setIsEditingInEditor] = useState(false); + const [generationError, setGenerationError] = useState(null); + const generationRequestIdRef = useRef(null); + + // Panel size state with localStorage persistence + const [panelWidth, setPanelWidth] = useState(() => { + const saved = localStorage.getItem('cc-wf-studio.descriptionPanelWidth'); + return saved ? Number.parseInt(saved, 10) : 280; + }); + const [panelHeight, setPanelHeight] = useState(() => { + const saved = localStorage.getItem('cc-wf-studio.descriptionPanelHeight'); + return saved ? Number.parseInt(saved, 10) : 160; + }); + + // Resize state + const isResizingRef = useRef<'left' | 'bottom' | 'corner' | null>(null); + const startPosRef = useRef({ x: 0, y: 0 }); + const startSizeRef = useRef({ width: 0, height: 0 }); + + // Size constraints + const MIN_WIDTH = 200; + const MAX_WIDTH = 500; + const MIN_HEIGHT = 100; + const MAX_HEIGHT = 400; + + // Save size to localStorage when changed + useEffect(() => { + localStorage.setItem('cc-wf-studio.descriptionPanelWidth', panelWidth.toString()); + }, [panelWidth]); + + useEffect(() => { + localStorage.setItem('cc-wf-studio.descriptionPanelHeight', panelHeight.toString()); + }, [panelHeight]); + + // Handle resize mouse events + const handleResizeStart = useCallback( + (e: React.MouseEvent, direction: 'left' | 'bottom' | 'corner') => { + e.preventDefault(); + e.stopPropagation(); + isResizingRef.current = direction; + startPosRef.current = { x: e.clientX, y: e.clientY }; + startSizeRef.current = { width: panelWidth, height: panelHeight }; + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (!isResizingRef.current) return; + + const deltaX = startPosRef.current.x - moveEvent.clientX; + const deltaY = moveEvent.clientY - startPosRef.current.y; + + if (isResizingRef.current === 'left' || isResizingRef.current === 'corner') { + const newWidth = Math.min( + MAX_WIDTH, + Math.max(MIN_WIDTH, startSizeRef.current.width + deltaX) + ); + setPanelWidth(newWidth); + } + + if (isResizingRef.current === 'bottom' || isResizingRef.current === 'corner') { + const newHeight = Math.min( + MAX_HEIGHT, + Math.max(MIN_HEIGHT, startSizeRef.current.height + deltaY) + ); + setPanelHeight(newHeight); + } + }; + + const handleMouseUp = () => { + isResizingRef.current = null; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }, + [panelWidth, panelHeight] + ); + + // Handle AI description generation + const handleGenerateDescription = useCallback(async () => { + const currentRequestId = `gen-desc-${Date.now()}`; + generationRequestIdRef.current = currentRequestId; + setIsGeneratingDescription(true); + setGenerationError(null); + + try { + // Serialize current workflow state + const workflow = serializeWorkflow( + nodes, + edges, + workflowName || 'Untitled Workflow', + workflowDescription || undefined, + activeWorkflow?.conversationHistory, + subAgentFlows + ); + const workflowJson = JSON.stringify(workflow, null, 2); + + // Determine target language from locale + let targetLanguage = locale; + if (locale.startsWith('zh-')) { + targetLanguage = locale === 'zh-TW' || locale === 'zh-HK' ? 'zh-TW' : 'zh-CN'; + } else { + targetLanguage = locale.split('-')[0]; + } + + // Generate description with AI (reuse Slack description generator) + const generatedDescription = await generateSlackDescription( + workflowJson, + targetLanguage, + 30000, + currentRequestId + ); + + // Only update if not cancelled + if (generationRequestIdRef.current === currentRequestId) { + setWorkflowDescription(generatedDescription); + } + } catch { + // Only show error if not cancelled + if (generationRequestIdRef.current === currentRequestId) { + setGenerationError(t('slack.description.generateFailed')); + } + } finally { + // Only reset state if not cancelled + if (generationRequestIdRef.current === currentRequestId) { + setIsGeneratingDescription(false); + generationRequestIdRef.current = null; + } + } + }, [ + nodes, + edges, + workflowName, + workflowDescription, + activeWorkflow?.conversationHistory, + locale, + t, + subAgentFlows, + setWorkflowDescription, + ]); + + // Handle cancel AI description generation + const handleCancelGeneration = useCallback(() => { + const requestId = generationRequestIdRef.current; + if (requestId) { + cancelSlackDescriptionGeneration(requestId); + } + generationRequestIdRef.current = null; + setIsGeneratingDescription(false); + setGenerationError(null); + }, []); + + // Common button styles (highly transparent to not obstruct canvas) + const buttonBaseStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'color-mix(in srgb, var(--vscode-editor-background) 30%, transparent)', + border: '1px solid color-mix(in srgb, var(--vscode-panel-border) 30%, transparent)', + borderRadius: '4px', + cursor: 'pointer', + color: 'var(--vscode-foreground)', + }; + + // When collapsed: show expand button only + if (!isDescriptionPanelVisible) { + return ( + + + + ); + } + + // Resize handle style + const resizeHandleBase: React.CSSProperties = { + position: 'absolute', + backgroundColor: 'transparent', + }; + + // When visible: show expanded panel with description textarea + return ( +
+ {/* Resize handle - Left edge */} +
handleResizeStart(e, 'left')} + /> + + {/* Resize handle - Bottom edge */} +
handleResizeStart(e, 'bottom')} + /> + + {/* Resize handle - Bottom-left corner */} +
handleResizeStart(e, 'corner')} + /> + {/* Header with title and minimize button */} +
+
+ + + {t('description.panel.title')} + +
+ +
+ {/* Edit in Editor Button */} + + + {/* AI Generate Button */} + + + {/* Minimize button */} + + + +
+
+ + {/* Error message */} + {generationError && ( +
+ {generationError} +
+ )} + + {/* Description textarea */} +