@nodetool-ai/nodetool
GitHub Copilot Instructions for NodeTool
Install
agr install @nodetool-ai/nodetool --target copilotWrites 1 file into .github/copilot-instructions.md, pinned to git-7b7c8a49.
- .github/copilot-instructions.md
Document
GitHub Copilot Instructions for NodeTool
This file provides specific guidance for GitHub Copilot when generating code suggestions and completions for the NodeTool project.
Project Context
NodeTool is a React/TypeScript application for building AI workflows visually. Key technologies:
- Frontend (Web): React 18.2, TypeScript 5.7, Vite 6, Material-UI (MUI) v7
- State Management: Zustand 4.5.7 (web), 5.0.3 (electron) with temporal (undo/redo) support
- Flow Editor: ReactFlow (@xyflow/react) v12.10.0
- Data Fetching: TanStack Query (React Query) v5.62.3
- Testing: Jest 29.7.0 + React Testing Library 16.1.0 + Playwright for E2E
- Routing: React Router v7.12.0
- Desktop: Electron 35.7.5 with React 19.1.0
Code Generation Guidelines
TypeScript Patterns
Always use TypeScript with explicit types. Avoid any types.
// ✅ Good
interface NodeData {
properties: Record<string, unknown>;
workflow_id: string;
}
const updateNode = (id: string, data: NodeData): void => {
// implementation
};
// ❌ Bad
const updateNode = (id: any, data: any) => {
// implementation
};
React Component Patterns
Use functional components with TypeScript interfaces for props:
// ✅ Good
interface MyComponentProps {
title: string;
onSave?: (data: string) => void;
isLoading?: boolean;
}
export const MyComponent: React.FC<MyComponentProps> = ({
title,
onSave,
isLoading = false
}) => {
return <div>{title}</div>;
};
// ❌ Bad
export const MyComponent = (props) => {
return <div>{props.title}</div>;
};
Hooks Usage
Custom Hooks
Prefix with use and include TypeScript return types:
// ✅ Good
export const useNodeSelection = (nodeId: string): {
isSelected: boolean;
select: () => void;
deselect: () => void;
} => {
const [isSelected, setIsSelected] = useState(false);
return {
isSelected,
select: () => setIsSelected(true),
deselect: () => setIsSelected(false)
};
};
useCallback and useMemo
Use for optimization when passing to child components or expensive calculations:
// ✅ Good
const handleClick = useCallback((nodeId: string) => {
// handler logic
}, [dependencies]);
const expensiveValue = useMemo(() =>
computeExpensiveValue(data),
[data]
);
Zustand Store Patterns
Follow the existing pattern for store creation:
// ✅ Good
interface MyStoreState {
items: Item[];
selectedId: string | null;
setItems: (items: Item[]) => void;
selectItem: (id: string) => void;
}
export const useMyStore = create<MyStoreState>((set, get) => ({
items: [],
selectedId: null,
setItems: (items) => set({ items }),
selectItem: (id) => set({ selectedId: id }),
}));
// Use selectors to prevent unnecessary re-renders
const items = useMyStore(state => state.items);
const setItems = useMyStore(state => state.setItems);
Material-UI (MUI) Patterns
Use MUI components consistently with theme values:
// ✅ Good
import { Button, Typography, Box } from '@mui/material';
import { styled } from '@mui/material/styles';
const StyledButton = styled(Button)(({ theme }) => ({
borderRadius: theme.spacing(1),
padding: theme.spacing(1, 2),
backgroundColor: theme.vars.palette.primary.main,
}));
// Use sx prop for one-off styles
<Box sx={{ p: 2, mb: 1, bgcolor: 'background.paper' }}>
<Typography variant="h6">Title</Typography>
</Box>
Test Patterns
Generate tests following the React Testing Library approach:
// ✅ Good
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MyComponent } from '../MyComponent';
describe('MyComponent', () => {
it('handles user interaction', async () => {
const user = userEvent.setup();
const onSave = jest.fn();
render(<MyComponent onSave={onSave} />);
await user.click(screen.getByRole('button', { name: /save/i }));
expect(onSave).toHaveBeenCalled();
});
it('displays error state', async () => {
render(<MyComponent error="Failed to load" />);
expect(screen.getByText('Failed to load')).toBeInTheDocument();
});
});
API Client Patterns
Use the existing ApiClient for HTTP requests:
// ✅ Good
import { ApiClient } from '../stores/ApiClient';
const fetchWorkflow = async (id: string): Promise<Workflow> => {
const response = await ApiClient.get(`/api/workflows/${id}`);
return response.data;
};
TanStack Query Patterns
Use React Query for server state management:
// ✅ Good
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export const useWorkflow = (workflowId: string) => {
return useQuery({
queryKey: ['workflows', workflowId],
queryFn: () => fetchWorkflow(workflowId),
staleTime: 5000,
enabled: !!workflowId
});
};
export const useUpdateWorkflow = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateWorkflowData) => updateWorkflow(data),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['workflows'] });
}
});
};
File Structure Conventions
Component Files
MyComponent/
├── MyComponent.tsx # Main component
├── MyComponent.test.tsx # Tests
├── MyComponentStyles.tsx # Styled components (if needed)
└── index.ts # Re-export
Store Files
stores/
├── MyStore.ts
└── __tests__/
└── MyStore.test.ts
Naming Conventions
- Components: PascalCase (
MyComponent) - Hooks: camelCase with
useprefix (useMyHook) - Stores: camelCase with
useprefix (useMyStore) - Utilities: camelCase (
formatDate,validateInput) - Constants: UPPER_SNAKE_CASE (
MAX_NODES,DEFAULT_TIMEOUT) - Types/Interfaces: PascalCase (
NodeData,WorkflowState) - Test files: Same as source +
.test.ts(x)
Import Order
Follow this order for imports:
// 1. React and core libraries
import React, { useState, useEffect } from 'react';
// 2. Third-party libraries
import { Button, Box } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
// 3. Internal stores and contexts
import { useNodeStore } from '../stores/NodeStore';
import { useWorkflowContext } from '../contexts/WorkflowContext';
// 4. Internal components
import { MyComponent } from '../components/MyComponent';
// 5. Internal utilities and types
import { formatDate } from '../utils/formatDate';
import type { NodeData } from '../types';
// 6. Styles
import './styles.css';
Error Handling
Always handle errors appropriately:
// ✅ Good
const MyComponent: React.FC = () => {
const { data, error, isLoading } = useQuery({
queryKey: ['data'],
queryFn: fetchData
});
if (isLoading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage error={error} />;
}
return <div>{data}</div>;
};
// ✅ Good for async functions
const saveData = async (data: Data): Promise<void> => {
try {
await ApiClient.post('/api/data', data);
} catch (error) {
console.error('Failed to save data:', error);
throw error; // Re-throw for caller to handle
}
};
// ✅ Good - Always add comments for intentionally empty catch blocks
try {
const parsed = JSON.parse(jsonString);
return parsed;
} catch (error) {
// JSON parse failed, return original string
return jsonString;
}
// ❌ Bad - Empty catch blocks without explanation
try {
JSON.parse(jsonString);
} catch (error) {}
// ✅ Good - Throw proper Error objects, not strings
if (!data) {
throw new Error("Data is required");
}
// ❌ Bad - Throwing non-Error objects
if (!data) {
throw "Data is required";
}
Code Quality Best Practices
Equality Checks
Always use strict equality (=== and !==) instead of loose equality (== and !=):
// ✅ Good
if (value === null) { }
if (count === 0) { }
if (nodeType === "nodetool.group.Loop") { }
// ❌ Bad
if (value == null) { }
if (count == 0) { }
if (nodeType == "nodetool.group.Loop") { }
// Exception: null checks can use == for brevity (checks both null and undefined)
if (value == null) { } // Acceptable for null/undefined check
Control Flow
Always use curly braces for control statements, even single-line ones:
// ✅ Good
if (condition) {
doSomething();
}
// ⚠️ Acceptable but discouraged
if (condition) doSomething();
// ❌ Never do this
if (condition)
doSomething();
Array Type Checking
Use Array.isArray() instead of typeof when checking for arrays:
// ✅ Good
if (Array.isArray(message.content)) {
message.content.map(item => /* ... */);
}
// ❌ Bad - typeof returns "object" for arrays
if (typeof message.content === "object") {
message.content.map(item => /* ... */); // TypeScript error
}
Variable Declarations
Use const by default, let when reassignment is needed. Never use var:
// ✅ Good
const maxRetries = 3;
let retryCount = 0;
// ❌ Bad
var maxRetries = 3;
var retryCount = 0;
Accessibility
Always consider accessibility:
// ✅ Good
<Button
onClick={handleClick}
aria-label="Save workflow"
disabled={isLoading}
>
{isLoading ? 'Saving...' : 'Save'}
</Button>
<TextField
label="Workflow Name"
aria-describedby="name-helper-text"
error={!!error}
helperText={error || "Enter a unique name"}
/>
Performance Considerations
Avoid Unnecessary Re-renders
// ✅ Good - selective subscription
const nodeName = useNodeStore(state => state.nodes[nodeId]?.name);
// ❌ Bad - subscribes to all nodes
const nodes = useNodeStore(state => state.nodes);
const nodeName = nodes[nodeId]?.name;
// ✅ Good - memoize expensive calculations
const sortedNodes = useMemo(() =>
nodes.sort((a, b) => a.position.x - b.position.x),
[nodes]
);
// ✅ Good - stable callbacks
const handleNodeClick = useCallback((nodeId: string) => {
selectNode(nodeId);
}, [selectNode]);
Code Splitting
Use dynamic imports for large components:
// ✅ Good
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
Security
Sanitize User Input
// ✅ Good
import DOMPurify from 'dompurify';
const sanitizedHtml = DOMPurify.sanitize(userInput);
Avoid Dangerous Props
// ❌ Bad
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ Good
<div>{userInput}</div>
// or use a markdown library with XSS protection
Common Patterns in This Project
Node Operations
// Adding a node
const addNode = useNodeStore(state => state.addNode);
addNode({
id: generateId(),
type: 'custom_node',
position: { x: 0, y: 0 },
data: {
properties: {},
workflow_id: workflowId
}
});
// Updating node data
const updateNode = useNodeStore(state => state.updateNode);
updateNode(nodeId, {
data: { properties: { ...newProperties } }
});
Edge/Connection Operations
const addEdge = useNodeStore(state => state.addEdge);
addEdge({
id: `${sourceId}-${targetId}`,
source: sourceId,
target: targetId,
sourceHandle: 'output',
targetHandle: 'input'
});
Asset Upload
const { uploadAsset } = useAssetUpload();
const handleFileUpload = async (file: File) => {
try {
const asset = await uploadAsset(file, workflowId);
console.log('Uploaded:', asset);
} catch (error) {
console.error('Upload failed:', error);
}
};
What NOT to Do
❌ Don't use deprecated React patterns
// ❌ Bad
class MyComponent extends React.Component { }
// ❌ Bad
componentWillMount() { }
// ✅ Good - use functional components and hooks
const MyComponent: React.FC = () => { };
❌ Don't mutate state directly
// ❌ Bad
state.nodes.push(newNode);
// ✅ Good
setState({ nodes: [...state.nodes, newNode] });
❌ Don't use inline functions in JSX (when passed to child components)
// ❌ Bad - creates new function on every render
<Button onClick={() => handleClick(id)} />
// ✅ Good - stable reference
const onClick = useCallback(() => handleClick(id), [id]);
<Button onClick={onClick} />
❌ Don't test implementation details
// ❌ Bad
expect(component.state.count).toBe(1);
// ✅ Good
expect(screen.getByText('Count: 1')).toBeInTheDocument();
Testing Checklist
When generating test code:
- Use React Testing Library queries (getByRole, getByLabelText, etc.)
- Use userEvent for interactions (not fireEvent)
- Use waitFor for async assertions
- Mock external dependencies
- Test user-facing behavior, not implementation
- Use descriptive test names
- Clean up subscriptions/timers in afterEach
- Test error states
- Test loading states
- Test edge cases
Documentation
Add JSDoc comments for complex functions:
/**
* Calculates the optimal layout for nodes in the workflow.
*
* @param nodes - Array of nodes to layout
* @param edges - Array of edges connecting nodes
* @param direction - Layout direction ('TB' | 'LR')
* @returns Promise resolving to positioned nodes
*
* @example
* const positioned = await calculateLayout(nodes, edges, 'TB');
*/
export async function calculateLayout(
nodes: Node[],
edges: Edge[],
direction: 'TB' | 'LR'
): Promise<Node[]> {
// implementation
}
Resources
- Main AGENTS.md:
/AGENTS.md- Complete project documentation - Web UI Guide:
/web/src/AGENTS.md- Web-specific guidelines - Testing Guide:
/web/TESTING.md- Comprehensive test documentation (Jest, RTL, Playwright) - Electron Guide:
/electron/src/AGENTS.md- Desktop app guidelines - Component Guide:
/web/src/components/AGENTS.md- Component architecture - Store Guide:
/web/src/stores/AGENTS.md- State management patterns - Mobile Guide:
/mobile/README.md- React Native mobile app
Mandatory Post-Change Verification
After making any code changes, you MUST run the following commands to ensure code quality:
npm run typecheck # Type check all packages
npm run lint # Lint all packages
npm run test # Run all tests
Keep it green - All three commands must pass with exit code 0 before considering the task complete.
If any of these commands fail:
- Fix the errors reported
- Re-run the failing command(s)
- Continue until all three pass
This ensures:
- Type safety across the codebase
- Consistent code style and patterns
- No regressions in existing functionality
Code Review for Regressions
After making changes and before submitting a PR, review your code for potential regressions:
- Manual Testing: Run the affected features manually to ensure they work correctly
- Test Coverage: Verify that existing tests still pass and cover the changes
- Side Effects: Look for unintended side effects in related code
- Quality Checks: Confirm no new TypeScript errors or lint warnings were introduced
- Edge Cases: Test edge cases and error handling scenarios
- Performance: Consider any performance implications of your changes
This review helps catch issues before they reach production and ensures your changes don't break existing functionality.
Quick Reference Commands (Make)
# Quality Checks (run after every change)
npm run typecheck # Type check all packages
npm run lint # Lint all packages
npm run test # Run all tests
npm run check # Run all checks (typecheck, lint, test)
# Fix issues
npm run lint:fix # Auto-fix linting issues
npm run format # Alias for lint-fix
# Development
npm run electron # Build web and start electron app
npm run build # Build all packages
Quick Reference Commands (Individual Packages)
Web Package
cd web && npm start # Start dev server
cd web && npm test # Run tests
cd web && npm run test:watch # Watch mode
cd web && npm run test:coverage # With coverage
cd web && npm run typecheck # TypeScript check
cd web && npm run lint # ESLint
cd web && npm run lint:fix # Auto-fix lint issues
cd web && npm run build # Production build
cd web && npm run test:e2e # Run e2e tests
Electron Package
cd electron && npm start # Start electron dev server
cd electron && npm test # Run tests
cd electron && npm run typecheck # TypeScript check
cd electron && npm run lint # ESLint
cd electron && npm run lint:fix # Auto-fix lint issues
cd electron && npm run build # Production build
Setting Up for E2E Tests
E2E tests use a mock API server and the Vite dev server. Follow these steps:
1. Build Backend Packages
nvm use # Activate Node 22
npm install
npm run build:packages # Build all TS packages in dependency order
2. Install Web Dependencies
cd web
npm install
# Install Playwright browsers (first time only)
npx playwright install chromium
3. Run E2E Tests
Option A: Automatic Setup (Recommended) The Playwright configuration automatically starts the mock API server and Vite:
cd web
npm run test:e2e
Option B: Manual Setup Start servers manually for debugging:
# Terminal 1: Start backend
npm run build:packages && node packages/websocket/dist/server.js
# Terminal 2: Start frontend
cd web
npm start
# Terminal 3: Run tests
cd web
npx playwright test
4. Starting the NodeTool Server
The server is a TypeScript Fastify application in packages/websocket/:
# Build and start the server on port 7777 (default)
npm run build:packages
node packages/websocket/dist/server.js
# The server provides:
# - REST API endpoints at http://localhost:7777/api/
# - Health check at http://localhost:7777/health
# - WebSocket connections for workflow execution
# - Models API for managing AI models
5. E2E Test Structure
E2E tests are located in web/tests/e2e/ and use Playwright:
- app-loads.spec.ts: Basic app loading, navigation, and API connectivity tests
- models.spec.ts: Models manager API integration tests
- dashboard.spec.ts: Dashboard page loading and navigation tests
- assets.spec.ts: Asset management interface tests
- templates.spec.ts: Template workflows tests
- auth.spec.ts: Authentication flow tests
- navigation.spec.ts: Navigation and routing tests
- chat.spec.ts: Chat interface tests
- collections.spec.ts: Collections management tests
- miniapps.spec.ts: Mini-apps functionality tests
6. Writing E2E Tests
Follow these patterns when creating new e2e tests:
import { test, expect } from "@playwright/test";
// Skip when run by Jest
if (process.env.JEST_WORKER_ID) {
test.skip("skipped in jest runner", () => {});
} else {
test.describe("Feature Name", () => {
test("should do something", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
// Your test logic here
expect(await page.title()).toBeTruthy();
});
});
}
7. Debugging E2E Tests
# Run with UI mode for interactive debugging
cd web
npm run test:e2e:ui
# Run in headed mode to see the browser
cd web
npm run test:e2e:headed
# View trace files after failure
npx playwright show-trace test-results/path-to-trace/trace.zip
For more details, see web/TESTING.md.
Setting Up for Electron E2E Tests
Electron e2e tests verify the desktop application integration, IPC handlers, and native features.
1. Install Electron Dependencies
cd electron
npm install
2. Install Playwright Browsers
cd electron
npx playwright install chromium
3. Build the Electron App
The e2e tests require a built version of the Electron app:
cd electron
npm run vite:build
npx tsc
4. Run E2E Tests
cd electron
npm run test:e2e # Run e2e tests
npm run test:e2e:ui # Run with Playwright UI
npm run test:e2e:headed # Run in headed mode (see window)
5. Electron E2E Test Structure
E2E tests are located in electron/tests/e2e/ and use Playwright with Electron support:
- app-loads.spec.ts: Basic app loading, window creation, and IPC communication tests
6. Writing Electron E2E Tests
Follow these patterns when creating new Electron e2e tests:
import { test, expect, _electron as electron } from '@playwright/test';
import * as path from 'path';
// Skip when run by Jest
if (process.env.JEST_WORKER_ID) {
test.skip("skipped in jest runner", () => {});
} else {
test.describe("Feature Name", () => {
test("should do something", async () => {
// Launch Electron app
const electronApp = await electron.launch({
args: [
path.join(__dirname, '../../dist-electron/main.js'),
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage'
],
env: {
...process.env,
ELECTRON_DISABLE_SECURITY_WARNINGS: 'true'
}
});
const window = await electronApp.firstWindow();
// Your test logic here
expect(window).toBeTruthy();
// Always close the app
await electronApp.close();
});
});
}
7. Debugging Electron E2E Tests
# Run with UI mode for interactive debugging
cd electron
npm run test:e2e:ui
# Run in headed mode to see the Electron window
cd electron
npm run test:e2e:headed
# View trace files after failure
npx playwright show-trace test-results/path-to-trace/trace.zip
8. Electron E2E CI/CD
The .github/workflows/e2e.yml workflow:
- Runs on Ubuntu, macOS, and Windows
- Installs dependencies and Playwright browsers
- Builds the Electron app
- Runs e2e tests
- Uploads artifacts on failure
For more details, see electron/README.md.
Version Information
Web Package
- React: 18.2.0
- TypeScript: 5.7.2
- Vite: 6.4.1
- Material-UI: 7.2.0
- Zustand: 4.5.7
- ReactFlow: 12.10.0
- TanStack Query: 5.62.3
- React Router: 7.12.0
- Jest: 29.7.0
- React Testing Library: 16.1.0
Electron Package
- Electron: 35.7.5
- React: 19.1.0
- TypeScript: 5.3.3
- Zustand: 5.0.3
Remember: Follow the established patterns in the codebase. When in doubt, look at existing implementations in the same domain before creating new patterns.
Trust
Not scanned yet. Artifacts are graded after they are crawled, so a recently discovered one may have no result for a while.
Versions
git-7b7c8a49f0ac2026-08-04