diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 00000000..db3318cb
Binary files /dev/null and b/.DS_Store differ
diff --git a/package.json b/package.json
index ea65e265..2a9fab57 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"description": "A Model Context Protocol server that acts as an intelligent conversation state manager and development guide for LLMs",
"main": "dist/index.js",
"bin": {
- "responsible-vibe-mcp": "dist/index.js"
+ "responsible-vibe-mcp": "dist/index.js",
+ "workflow-visualizer": "workflow-visualizer/bin/visualizer.js"
},
"type": "module",
"engines": {
@@ -13,6 +14,7 @@
"files": [
"dist/**/*",
"resources/**/*",
+ "workflow-visualizer/**/*",
"README.md",
"SYSTEM_PROMPT.md",
"LOGGING.md",
@@ -24,7 +26,8 @@
"url": "git+https://github.com/mrsimpson/vibe-feature-mcp.git"
},
"scripts": {
- "build": "tsc",
+ "build": "tsc && npm run build:visualizer",
+ "build:visualizer": "cd workflow-visualizer && npm install && npm run build",
"inspector": "npx @modelcontextprotocol/inspector",
"dev": "tsc --watch",
"clean": "rm -rf dist",
diff --git a/workflow-visualizer/bin/visualizer.js b/workflow-visualizer/bin/visualizer.js
new file mode 100755
index 00000000..6aa1a8f2
--- /dev/null
+++ b/workflow-visualizer/bin/visualizer.js
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+
+import { spawn } from 'child_process';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const visualizerDir = join(__dirname, '..');
+
+console.log('š Starting Workflow Visualizer...');
+console.log('š Directory:', visualizerDir);
+
+// Check if dependencies are installed
+import { existsSync } from 'fs';
+if (!existsSync(join(visualizerDir, 'node_modules'))) {
+ console.log('š¦ Installing dependencies...');
+ const install = spawn('npm', ['install'], {
+ cwd: visualizerDir,
+ stdio: 'inherit'
+ });
+
+ install.on('close', (code) => {
+ if (code === 0) {
+ startServer();
+ } else {
+ console.error('ā Failed to install dependencies');
+ process.exit(1);
+ }
+ });
+} else {
+ startServer();
+}
+
+function startServer() {
+ console.log('š Starting development server...');
+ const server = spawn('npm', ['run', 'dev'], {
+ cwd: visualizerDir,
+ stdio: 'inherit'
+ });
+
+ server.on('close', (code) => {
+ console.log(`Server exited with code ${code}`);
+ });
+
+ // Handle Ctrl+C
+ process.on('SIGINT', () => {
+ console.log('\nš Shutting down visualizer...');
+ server.kill('SIGINT');
+ process.exit(0);
+ });
+}
diff --git a/workflow-visualizer/package-lock.json b/workflow-visualizer/package-lock.json
index eeab8bc0..56f7f8f3 100644
--- a/workflow-visualizer/package-lock.json
+++ b/workflow-visualizer/package-lock.json
@@ -12,6 +12,9 @@
"js-yaml": "^4.1.0",
"plantuml-encoder": "1.4.0"
},
+ "bin": {
+ "workflow-visualizer": "bin/visualizer.js"
+ },
"devDependencies": {
"@types/d3": "^7.4.3",
"@types/js-yaml": "^4.0.9",
diff --git a/workflow-visualizer/package.json b/workflow-visualizer/package.json
index 4f6ab1d6..dfde6412 100644
--- a/workflow-visualizer/package.json
+++ b/workflow-visualizer/package.json
@@ -3,6 +3,9 @@
"version": "1.0.0",
"description": "Web app for visualizing responsible-vibe workflow state machines",
"type": "module",
+ "bin": {
+ "workflow-visualizer": "bin/visualizer.js"
+ },
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
diff --git a/workflow-visualizer/src/main.ts b/workflow-visualizer/src/main.ts
index 6c907a60..156505f7 100644
--- a/workflow-visualizer/src/main.ts
+++ b/workflow-visualizer/src/main.ts
@@ -8,6 +8,7 @@ import { FileUploadHandler } from './services/FileUploadHandler';
import { ErrorHandler } from './utils/ErrorHandler';
import { PlantUMLRenderer } from './visualization/PlantUMLRenderer';
import { getRequiredElement } from './utils/DomHelpers';
+import type { InteractionEvent } from './types/ui-types';
import { YamlStateMachine, AppState, TransitionData } from './types/ui-types';
class WorkflowVisualizerApp {
@@ -53,7 +54,7 @@ class WorkflowVisualizerApp {
});
} else if (elementType === 'transition') {
this.handleElementClick({
- elementType: 'link',
+ elementType: 'transition',
elementId: elementId,
data: data
});
@@ -203,23 +204,12 @@ class WorkflowVisualizerApp {
if (event.elementType === 'node' && event.data) {
console.log('Selecting state:', event.elementId);
this.selectState(event.elementId!, event.data);
- } else if (event.elementType === 'link' && event.data) {
+ } else if (event.elementType === 'transition' && event.data) {
console.log('Selecting transition:', event.elementId);
this.selectTransition(event.elementId!, event.data);
}
}
- /**
- * Handle element hover in the diagram
- */
- private handleElementHover(event: InteractionEvent): void {
- // For now, just log hover events
- // Could be extended to show tooltips
- if (event.type === 'hover') {
- console.log('Element hovered:', event.elementType, event.elementId);
- }
- }
-
/**
* Select a state node
*/
@@ -272,16 +262,6 @@ class WorkflowVisualizerApp {
}
}
- /**
- * Highlight a transition path
- */
- private highlightTransitionPath(fromState: string, toState: string): void {
- // Note: PlantUML diagrams don't support interactive path highlighting
- // Path information is shown in the side panel instead
- const pathElements = [fromState, toState];
- this.appState.highlightedPath = pathElements;
- }
-
/**
* Update the side panel content
*/
@@ -519,116 +499,6 @@ class WorkflowVisualizerApp {
}
/**
- * Render state details
- */
- private renderStateDetails(stateId: string, stateData: any, container: HTMLElement): void {
- const workflow = this.appState.currentWorkflow!;
- const isInitial = stateId === workflow.initial_state;
-
- container.innerHTML = `
-
-
- ${stateId}
- ${isInitial ? 'Initial' : ''}
-
-
${stateData.description}
-
-
-
-
Default Instructions
-
${stateData.default_instructions}
-
-
-
-
Transitions (${stateData.transitions.length})
-
-
- `;
-
- // Add click handlers to transitions
- const transitionItems = container.querySelectorAll('.clickable-transition');
- transitionItems.forEach(item => {
- item.addEventListener('click', (e) => {
- e.stopPropagation();
- const fromState = item.getAttribute('data-from');
- const toState = item.getAttribute('data-to');
- const trigger = item.getAttribute('data-trigger');
-
- if (fromState && toState && trigger) {
- console.log('Side panel transition clicked:', `${fromState}->${toState}`);
-
- // Find the full transition data
- const fullTransition = stateData.transitions.find((t: any) =>
- t.to === toState && t.trigger === trigger
- );
-
- if (fullTransition) {
- this.selectTransition(`${fromState}->${toState}`, {
- from: fromState,
- to: toState,
- trigger: trigger,
- instructions: fullTransition.instructions,
- additional_instructions: fullTransition.additional_instructions,
- transition_reason: fullTransition.transition_reason
- });
- }
- }
- });
-
- // Add hover effects
- item.addEventListener('mouseenter', () => {
- (item as HTMLElement).style.backgroundColor = '#f0f9ff';
- (item as HTMLElement).style.cursor = 'pointer';
- });
-
- item.addEventListener('mouseleave', () => {
- (item as HTMLElement).style.backgroundColor = '';
- (item as HTMLElement).style.cursor = '';
- });
- });
- }
-
- /**
- * Render transition details
- */
- private renderTransitionDetails(transitionData: TransitionData, container: HTMLElement): void {
- container.innerHTML = `
-
-
Transition: ${transitionData.trigger}
-
- ${transitionData.from} ā ${transitionData.to}
-
-
-
-
-
Reason
-
${transitionData.transition_reason}
-
-
- ${transitionData.instructions ? `
-
-
Instructions
-
${transitionData.instructions}
-
- ` : ''}
-
- ${transitionData.additional_instructions ? `
-
-
Additional Instructions
-
${transitionData.additional_instructions}
-
- ` : ''}
- `;
- }
-
/**
* Clear the visualization
*/
diff --git a/workflow-visualizer/src/types/plantuml-encoder.d.ts b/workflow-visualizer/src/types/plantuml-encoder.d.ts
new file mode 100644
index 00000000..3e317147
--- /dev/null
+++ b/workflow-visualizer/src/types/plantuml-encoder.d.ts
@@ -0,0 +1,4 @@
+declare module 'plantuml-encoder' {
+ export function encode(plantuml: string): string;
+ export function decode(encoded: string): string;
+}
diff --git a/workflow-visualizer/src/types/ui-types.ts b/workflow-visualizer/src/types/ui-types.ts
index b895d9ae..785bf76f 100644
--- a/workflow-visualizer/src/types/ui-types.ts
+++ b/workflow-visualizer/src/types/ui-types.ts
@@ -8,6 +8,16 @@ import type { YamlStateMachine, YamlState, YamlTransition } from '../../../src/s
// Re-export for convenience
export type { YamlStateMachine, YamlState, YamlTransition };
+/**
+ * Interaction event for diagram elements
+ */
+export interface InteractionEvent {
+ elementType: 'node' | 'edge' | 'transition';
+ elementId?: string;
+ data?: any;
+ originalEvent?: Event;
+}
+
/**
* Application state interface
*/
diff --git a/workflow-visualizer/src/visualization/PlantUMLRenderer.ts b/workflow-visualizer/src/visualization/PlantUMLRenderer.ts
index 03a4306d..463fce34 100644
--- a/workflow-visualizer/src/visualization/PlantUMLRenderer.ts
+++ b/workflow-visualizer/src/visualization/PlantUMLRenderer.ts
@@ -1,10 +1,9 @@
-import { YamlStateMachine } from '../types/workflow-types';
+import { YamlStateMachine, YamlState } from '../types/ui-types';
import * as plantumlEncoder from 'plantuml-encoder';
export class PlantUMLRenderer {
private container: HTMLElement;
private onElementClick?: (elementType: 'state' | 'transition', elementId: string, data?: any) => void;
- private currentWorkflow?: YamlStateMachine;
constructor(container: HTMLElement) {
this.container = container;
@@ -23,8 +22,6 @@ export class PlantUMLRenderer {
public async renderWorkflow(workflow: YamlStateMachine): Promise {
console.log(`Rendering workflow with PlantUML: ${workflow.name}`);
- this.currentWorkflow = workflow;
-
// Clear container and set up scrollable area
this.container.innerHTML = '';
this.container.style.overflow = 'auto';
@@ -91,7 +88,7 @@ export class PlantUMLRenderer {
lines.push('');
// Add states with descriptions
- Object.entries(workflow.states).forEach(([stateName, stateConfig]) => {
+ Object.entries(workflow.states).forEach(([stateName, stateConfig]: [string, YamlState]) => {
if (stateConfig.description) {
lines.push(`${stateName} : ${stateConfig.description}`);
}
@@ -99,7 +96,7 @@ export class PlantUMLRenderer {
lines.push('');
// Add transitions
- Object.entries(workflow.states).forEach(([stateName, stateConfig]) => {
+ Object.entries(workflow.states).forEach(([stateName, stateConfig]: [string, YamlState]) => {
if (stateConfig.transitions) {
stateConfig.transitions.forEach(transition => {
const label = transition.trigger.replace(/_/g, ' ');
@@ -177,12 +174,12 @@ export class PlantUMLRenderer {
container.appendChild(svgContainer);
// Add simplified interactive cards (no transitions)
- this.addSimplifiedInteractiveCards(container.parentElement!, workflow);
+ this.addSimplifiedInteractiveCards(container.parentElement!);
} catch (error) {
console.error('Failed to load interactive SVG:', error);
this.showError('Failed to load interactive diagram. Using fallback.');
- this.renderFallbackDiagram(workflow);
+ this.renderFallbackDiagram();
}
}
@@ -201,8 +198,8 @@ export class PlantUMLRenderer {
const stateName = groupId;
// Make the entire group clickable
- group.style.cursor = 'pointer';
- group.style.transition = 'all 0.2s ease';
+ (group as HTMLElement).style.cursor = 'pointer';
+ (group as HTMLElement).style.transition = 'all 0.2s ease';
// Find the rect/shape element for hover effects
const shape = group.querySelector('rect, ellipse, polygon');
@@ -253,8 +250,8 @@ export class PlantUMLRenderer {
// Verify these are valid states
if (states.includes(fromState) && states.includes(toState)) {
// Make the entire link group clickable
- linkGroup.style.cursor = 'pointer';
- linkGroup.style.transition = 'all 0.2s ease';
+ (linkGroup as HTMLElement).style.cursor = 'pointer';
+ (linkGroup as HTMLElement).style.transition = 'all 0.2s ease';
// Find path and text elements for hover effects
const pathEl = linkGroup.querySelector('path');
@@ -317,7 +314,7 @@ export class PlantUMLRenderer {
/**
* Add simplified interactive cards (states only, no transitions)
*/
- private addSimplifiedInteractiveCards(container: HTMLElement, workflow: YamlStateMachine): void {
+ private addSimplifiedInteractiveCards(container: HTMLElement): void {
const instructionDiv = document.createElement('div');
instructionDiv.style.marginTop = '15px';
instructionDiv.style.textAlign = 'center';
@@ -335,7 +332,7 @@ export class PlantUMLRenderer {
/**
* Render fallback diagram if PlantUML fails
*/
- private renderFallbackDiagram(workflow: YamlStateMachine): void {
+ private renderFallbackDiagram(): void {
const fallbackDiv = document.createElement('div');
fallbackDiv.style.padding = '20px';
fallbackDiv.style.border = '2px dashed #94a3b8';
@@ -351,7 +348,6 @@ export class PlantUMLRenderer {
`;
this.container.appendChild(fallbackDiv);
- this.addInteractiveOverlay(fallbackDiv, workflow);
}
/**
diff --git a/workflow-visualizer/src/visualization/StateRenderer.ts b/workflow-visualizer/src/visualization/StateRenderer.ts
index e34f46be..339e1bfa 100644
--- a/workflow-visualizer/src/visualization/StateRenderer.ts
+++ b/workflow-visualizer/src/visualization/StateRenderer.ts
@@ -38,8 +38,8 @@ export class StateRenderer {
// Add circles for states
nodeEnter.append('circle')
- .attr('cx', d => d.x)
- .attr('cy', d => d.y)
+ .attr('cx', d => d.x || 0)
+ .attr('cy', d => d.y || 0)
.attr('r', d => this.getNodeRadius(d))
.style('fill', d => this.getNodeFill(d))
.style('stroke', d => this.getNodeStroke(d))
@@ -48,8 +48,8 @@ export class StateRenderer {
// Add labels for states
nodeEnter.append('text')
.attr('class', 'state-label')
- .attr('x', d => d.x)
- .attr('y', d => d.y)
+ .attr('x', d => d.x || 0)
+ .attr('y', d => d.y || 0)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.style('font-size', this.style.text.fontSize)