diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ff36c6..71bdc326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [3.8.1](https://github.com/breaking-brake/cc-wf-studio/compare/v3.8.0...v3.8.1) (2025-12-17) + +### Bug Fixes + +* dialog positioning broken by NodePalette transform ([#290](https://github.com/breaking-brake/cc-wf-studio/issues/290)) ([b1adb8f](https://github.com/breaking-brake/cc-wf-studio/commit/b1adb8fdda690aa9288def8088d29da7325c289f)) +* improve JSON parsing to handle nested markdown code blocks ([#289](https://github.com/breaking-brake/cc-wf-studio/issues/289)) ([d0ca208](https://github.com/breaking-brake/cc-wf-studio/commit/d0ca20880ed22a2ed1550746136487015567545d)) + ## [3.8.0](https://github.com/breaking-brake/cc-wf-studio/compare/v3.7.4...v3.8.0) (2025-12-17) ### Features diff --git a/package-lock.json b/package-lock.json index 61f14254..10e53599 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-wf-studio", - "version": "3.8.0", + "version": "3.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio", - "version": "3.8.0", + "version": "3.8.1", "license": "AGPL-3.0-or-later", "dependencies": { "@modelcontextprotocol/sdk": "^1.24.3", diff --git a/package.json b/package.json index 110f01a6..d0dc4e46 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.8.0", + "version": "3.8.1", "publisher": "breaking-brake", "icon": "resources/icon.png", "repository": { diff --git a/src/extension/services/claude-code-service.ts b/src/extension/services/claude-code-service.ts index 987df97f..d504e5d1 100644 --- a/src/extension/services/claude-code-service.ts +++ b/src/extension/services/claude-code-service.ts @@ -251,16 +251,31 @@ function isSubprocessError(error: unknown): error is SubprocessError { /** * Parse JSON output from Claude Code CLI * + * Handles two output formats: + * 1. Markdown-wrapped: ```json { ... } ``` + * 2. Raw JSON: { ... } + * + * Note: Uses string position-based extraction (not regex) to handle cases + * where the JSON content itself contains markdown code blocks. + * * @param output - Raw output string from CLI * @returns Parsed JSON object or null if parsing fails */ export function parseClaudeCodeOutput(output: string): unknown { try { - // Claude Code might wrap output in markdown code blocks, so extract JSON - const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/); - const jsonString = jsonMatch ? jsonMatch[1] : output; + const trimmed = output.trim(); + + // Strategy 1: If wrapped in ```json...```, remove outer markers only + if (trimmed.startsWith('```json') && trimmed.endsWith('```')) { + const jsonContent = trimmed + .slice(7) // Remove ```json + .slice(0, -3) // Remove trailing ``` + .trim(); + return JSON.parse(jsonContent); + } - return JSON.parse(jsonString.trim()); + // Strategy 2: Try parsing as-is + return JSON.parse(trimmed); } catch (_error) { // If parsing fails, return null return null; diff --git a/src/webview/package-lock.json b/src/webview/package-lock.json index 566a23fb..02b4d01a 100644 --- a/src/webview/package-lock.json +++ b/src/webview/package-lock.json @@ -1,15 +1,17 @@ { "name": "cc-wf-studio-webview", - "version": "3.8.0", + "version": "3.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio-webview", - "version": "3.8.0", + "version": "3.8.1", "license": "AGPL-3.0-or-later", "dependencies": { + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-portal": "^1.1.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-toggle-group": "^1.1.11", @@ -885,6 +887,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -1110,6 +1142,30 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -1143,12 +1199,12 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.10.tgz", + "integrity": "sha512-4kY9IVa6+9nJPsYmngK5Uk2kUmZnv7ChhHAFeQ5oaj8jrR1bIi3xww8nH71pz1/Ve4d/cXO3YxT8eikt1B0a8w==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { @@ -1166,6 +1222,47 @@ } } }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", @@ -1287,6 +1384,30 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1422,6 +1543,30 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/src/webview/package.json b/src/webview/package.json index dbe5b919..1e102a04 100644 --- a/src/webview/package.json +++ b/src/webview/package.json @@ -1,6 +1,6 @@ { "name": "cc-wf-studio-webview", - "version": "3.8.0", + "version": "3.8.1", "private": true, "license": "AGPL-3.0-or-later", "type": "module", @@ -13,7 +13,9 @@ "test:watch": "vitest watch" }, "dependencies": { + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-portal": "^1.1.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-toggle-group": "^1.1.11", diff --git a/src/webview/src/App.tsx b/src/webview/src/App.tsx index 5814ff63..1d5905b9 100644 --- a/src/webview/src/App.tsx +++ b/src/webview/src/App.tsx @@ -5,6 +5,7 @@ * Based on: /specs/001-cc-wf-studio/plan.md */ +import * as Collapsible from '@radix-ui/react-collapsible'; import type { ErrorPayload, ImportWorkflowFromSlackPayload, @@ -214,31 +215,21 @@ const App: React.FC = () => { overflow: 'hidden', }} > - {/* Left Panel: Node Palette with collapse/expand animation */} -
- {/* NodePalette with slide animation */} -
+ -
+ {/* Simple overlay for Left Panel */} -
+ {/* Center: Workflow Editor with processing overlay (Phase 3.10 - modified) */}
@@ -362,6 +353,39 @@ const App: React.FC = () => { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + + /* Node Palette Collapsible Animation */ + .node-palette-collapsible { + overflow: hidden; + } + + .node-palette-collapsible[data-state='open'] { + width: 200px; + animation: slideOpen 150ms ease-out; + } + + .node-palette-collapsible[data-state='closed'] { + width: 0px; + animation: slideClose 150ms ease-out; + } + + @keyframes slideOpen { + from { + width: 0px; + } + to { + width: 200px; + } + } + + @keyframes slideClose { + from { + width: 200px; + } + to { + width: 0px; + } + } `}
diff --git a/src/webview/src/components/dialogs/McpNodeDialog.tsx b/src/webview/src/components/dialogs/McpNodeDialog.tsx index 57a352b9..0384ab3a 100644 --- a/src/webview/src/components/dialogs/McpNodeDialog.tsx +++ b/src/webview/src/components/dialogs/McpNodeDialog.tsx @@ -7,6 +7,7 @@ * Based on: specs/001-mcp-natural-language-mode/tasks.md T017, T048 */ +import * as Portal from '@radix-ui/react-portal'; import type { McpToolReference } from '@shared/types/mcp-node'; import { NodeType } from '@shared/types/workflow-definition'; import { useEffect, useState } from 'react'; @@ -358,115 +359,99 @@ export function McpNodeDialog({ isOpen, onClose }: McpNodeDialogProps) { } }; + // Portal ensures dialog renders at document.body, avoiding transform containment issues return ( -
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: onClick is only used to stop event propagation, not for click actions */} +
e.stopPropagation()} - role="dialog" - aria-modal="true" + onClick={handleClose} + role="presentation" > - {/* Dialog Header */} -

- {t('mcp.dialog.title')} -

- - {/* Step Indicator */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: onClick is only used to stop event propagation, not for click actions */}
e.stopPropagation()} + role="dialog" + aria-modal="true" > - {t('mcp.dialog.wizardStep', { - current: wizard.state.currentStep.toString(), - total: '7', - })} -
+ {/* Dialog Header */} +

+ {t('mcp.dialog.title')} +

- {/* Error Message */} - {error && ( + {/* Step Indicator */}
- {error} + {t('mcp.dialog.wizardStep', { + current: wizard.state.currentStep.toString(), + total: '7', + })}
- )} - {/* Step Content */} -
{renderStepContent()}
+ {/* Error Message */} + {error && ( +
+ {error} +
+ )} - {/* Dialog Actions */} -
-
+ + {/* Dialog Actions */} +
- {t('mcp.dialog.cancelButton')} - - - {/* Back Button */} - {wizard.state.currentStep !== WizardStep.ServerSelection && ( - )} - {/* Next/Save Button */} - + {/* Back Button */} + {wizard.state.currentStep !== WizardStep.ServerSelection && ( + + )} + + {/* Next/Save Button */} + +
- + ); } diff --git a/src/webview/src/components/dialogs/SkillBrowserDialog.tsx b/src/webview/src/components/dialogs/SkillBrowserDialog.tsx index 3ee57629..b48d8eef 100644 --- a/src/webview/src/components/dialogs/SkillBrowserDialog.tsx +++ b/src/webview/src/components/dialogs/SkillBrowserDialog.tsx @@ -7,6 +7,7 @@ * Based on: specs/001-skill-node/design.md Section 6.2 */ +import * as Portal from '@radix-ui/react-portal'; import type { SkillReference } from '@shared/types/messages'; import { NodeType } from '@shared/types/workflow-definition'; import { useEffect, useState } from 'react'; @@ -182,390 +183,393 @@ export function SkillBrowserDialog({ isOpen, onClose }: SkillBrowserDialogProps) const currentSkills = activeTab === 'personal' ? personalSkills : projectSkills; + // Portal ensures dialog renders at document.body, avoiding transform containment issues return ( -
{ - if (e.key === 'Escape') { - handleClose(); - } - }} - role="presentation" - > +
{ + if (e.key === 'Escape') { + handleClose(); + } }} - onClick={(e) => e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - role="dialog" - aria-modal="true" + role="presentation" > - {/* Header */} -

- {t('skill.browser.title')} -

-

- {t('skill.browser.description')} -

- - {/* Tabs */}
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + role="dialog" + aria-modal="true" > - - -
- - {/* Refresh Button */} - - - {/* Loading State */} - {loading && ( -
- {t('skill.browser.loading')} -
- )} + {t('skill.browser.description')} +

- {/* Error State */} - {error && !loading && ( + {/* Tabs */}
- {error} + +
- )} - {/* Skills List */} - {!loading && !error && currentSkills.length === 0 && ( -
- {t('skill.browser.noSkills')} -
- )} - - {!loading && !error && currentSkills.length > 0 && ( -
- {currentSkills.map((skill) => ( -
setSelectedSkill(skill)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setSelectedSkill(skill); - } - }} - role="button" - tabIndex={0} - style={{ - padding: '12px', - borderBottom: '1px solid var(--vscode-panel-border)', - cursor: 'pointer', - backgroundColor: - selectedSkill?.skillPath === skill.skillPath - ? 'var(--vscode-list-activeSelectionBackground)' - : 'transparent', - }} - onMouseEnter={(e) => { - if (selectedSkill?.skillPath !== skill.skillPath) { - e.currentTarget.style.backgroundColor = 'var(--vscode-list-hoverBackground)'; - } - }} - onMouseLeave={(e) => { - if (selectedSkill?.skillPath !== skill.skillPath) { - e.currentTarget.style.backgroundColor = 'transparent'; - } - }} - > + {refreshing ? t('skill.refreshing') : t('skill.action.refresh')} + + + {/* Loading State */} + {loading && ( +
+ {t('skill.browser.loading')} +
+ )} + + {/* Error State */} + {error && !loading && ( +
+ {error} +
+ )} + + {/* Skills List */} + {!loading && !error && currentSkills.length === 0 && ( +
+ {t('skill.browser.noSkills')} +
+ )} + + {!loading && !error && currentSkills.length > 0 && ( +
+ {currentSkills.map((skill) => (
setSelectedSkill(skill)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setSelectedSkill(skill); + } + }} + role="button" + tabIndex={0} style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: '4px', + padding: '12px', + borderBottom: '1px solid var(--vscode-panel-border)', + cursor: 'pointer', + backgroundColor: + selectedSkill?.skillPath === skill.skillPath + ? 'var(--vscode-list-activeSelectionBackground)' + : 'transparent', + }} + onMouseEnter={(e) => { + if (selectedSkill?.skillPath !== skill.skillPath) { + e.currentTarget.style.backgroundColor = 'var(--vscode-list-hoverBackground)'; + } + }} + onMouseLeave={(e) => { + if (selectedSkill?.skillPath !== skill.skillPath) { + e.currentTarget.style.backgroundColor = 'transparent'; + } }} > -
- - {skill.name} - +
+
+ + {skill.name} + + + {skill.scope === 'personal' + ? t('skill.browser.personalTab') + : t('skill.browser.projectTab')} + +
- {skill.scope === 'personal' - ? t('skill.browser.personalTab') - : t('skill.browser.projectTab')} + {skill.validationStatus === 'valid' + ? '✓' + : skill.validationStatus === 'missing' + ? '⚠' + : '✗'}
- - {skill.validationStatus === 'valid' - ? '✓' - : skill.validationStatus === 'missing' - ? '⚠' - : '✗'} - -
-
- {skill.description} -
- {skill.allowedTools && (
- 🔧 {skill.allowedTools} + {skill.description}
- )} -
- ))} -
- )} - - {/* Create New Skill Button */} - {!loading && ( - - )} - - {/* Actions */} -
- -
+ )} +
+ ))} +
+ )} + + {/* Create New Skill Button */} + {!loading && ( + + )} + + {/* Actions */} +
- {t('skill.browser.selectButton')} - + + +
-
- {/* Skill Creation Dialog */} - setIsSkillCreationOpen(false)} - onSubmit={handleSkillCreate} - /> - + {/* Skill Creation Dialog */} + setIsSkillCreationOpen(false)} + onSubmit={handleSkillCreate} + /> + + ); }