From 330a82787df394d700feb2f1d6504e92c248adeb Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Thu, 19 Feb 2026 19:52:50 +0000 Subject: [PATCH 01/13] Visual workflow editor - complete implementation --- docs/editor-app/ARCHITECTURE.md | 323 ++ docs/editor-app/DESIGN.md | 1585 ++++++++ docs/editor-app/SECURITY-REVIEW.md | 117 + docs/editor-app/USER-JOURNEYS.md | 1179 ++++++ docs/editor-app/index.html | 12 + docs/editor-app/package-lock.json | 3470 +++++++++++++++++ docs/editor-app/package.json | 45 + docs/editor-app/playwright.config.ts | 26 + docs/editor-app/public/wasm | 1 + docs/editor-app/src/App.tsx | 108 + .../src/components/Canvas/EmptyState.tsx | 13 + .../src/components/Canvas/WorkflowGraph.tsx | 240 ++ .../components/Header/CompilationStatus.tsx | 18 + .../src/components/Header/ExportMenu.tsx | 4 + .../src/components/Header/Header.tsx | 267 ++ .../src/components/Nodes/BaseNode.tsx | 45 + .../src/components/Nodes/EngineNode.tsx | 44 + .../src/components/Nodes/InstructionsNode.tsx | 41 + .../src/components/Nodes/NetworkNode.tsx | 43 + .../src/components/Nodes/PermissionsNode.tsx | 44 + .../src/components/Nodes/SafeOutputsNode.tsx | 65 + .../src/components/Nodes/StepsNode.tsx | 27 + .../src/components/Nodes/ToolsNode.tsx | 43 + .../src/components/Nodes/TriggerNode.tsx | 56 + .../src/components/Onboarding/GuidedTour.tsx | 4 + .../components/Onboarding/WelcomeModal.tsx | 154 + .../src/components/Panels/EnginePanel.tsx | 153 + .../components/Panels/InstructionsPanel.tsx | 82 + .../src/components/Panels/NetworkPanel.tsx | 143 + .../src/components/Panels/PanelContainer.tsx | 20 + .../components/Panels/PermissionsPanel.tsx | 161 + .../src/components/Panels/PropertiesPanel.tsx | 65 + .../components/Panels/SafeOutputsPanel.tsx | 140 + .../src/components/Panels/StepsPanel.tsx | 78 + .../src/components/Panels/ToolsPanel.tsx | 124 + .../src/components/Panels/TriggerPanel.tsx | 253 ++ .../src/components/Sidebar/NodePalette.tsx | 139 + .../src/components/Sidebar/Sidebar.tsx | 73 + .../components/Sidebar/TemplateGallery.tsx | 134 + .../components/YamlPreview/MarkdownSource.tsx | 60 + .../components/YamlPreview/YamlPreview.tsx | 263 ++ .../src/components/shared/FieldLabel.tsx | 4 + .../src/components/shared/HelpTooltip.tsx | 56 + .../src/components/shared/ResizablePanel.tsx | 4 + .../src/components/shared/StatusBadge.tsx | 4 + docs/editor-app/src/hooks/useAutoCompile.ts | 76 + docs/editor-app/src/hooks/useCompiler.ts | 48 + docs/editor-app/src/hooks/useTheme.ts | 29 + docs/editor-app/src/main.tsx | 12 + docs/editor-app/src/stores/uiStore.ts | 62 + docs/editor-app/src/stores/workflowStore.ts | 237 ++ docs/editor-app/src/styles/globals.css | 115 + docs/editor-app/src/styles/nodes.css | 254 ++ docs/editor-app/src/styles/panels.css | 117 + docs/editor-app/src/types/compiler.ts | 38 + docs/editor-app/src/types/nodes.ts | 19 + docs/editor-app/src/types/workflow.ts | 249 ++ docs/editor-app/src/utils/compiler.ts | 68 + .../editor-app/src/utils/fieldDescriptions.ts | 421 ++ .../editor-app/src/utils/markdownGenerator.ts | 319 ++ docs/editor-app/src/utils/templates.ts | 233 ++ docs/editor-app/src/vite-env.d.ts | 1 + docs/editor-app/tsconfig.json | 26 + docs/editor-app/vite.config.ts | 20 + 64 files changed, 12274 insertions(+) create mode 100644 docs/editor-app/ARCHITECTURE.md create mode 100644 docs/editor-app/DESIGN.md create mode 100644 docs/editor-app/SECURITY-REVIEW.md create mode 100644 docs/editor-app/USER-JOURNEYS.md create mode 100644 docs/editor-app/index.html create mode 100644 docs/editor-app/package-lock.json create mode 100644 docs/editor-app/package.json create mode 100644 docs/editor-app/playwright.config.ts create mode 120000 docs/editor-app/public/wasm create mode 100644 docs/editor-app/src/App.tsx create mode 100644 docs/editor-app/src/components/Canvas/EmptyState.tsx create mode 100644 docs/editor-app/src/components/Canvas/WorkflowGraph.tsx create mode 100644 docs/editor-app/src/components/Header/CompilationStatus.tsx create mode 100644 docs/editor-app/src/components/Header/ExportMenu.tsx create mode 100644 docs/editor-app/src/components/Header/Header.tsx create mode 100644 docs/editor-app/src/components/Nodes/BaseNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/EngineNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/InstructionsNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/NetworkNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/PermissionsNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/SafeOutputsNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/StepsNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/ToolsNode.tsx create mode 100644 docs/editor-app/src/components/Nodes/TriggerNode.tsx create mode 100644 docs/editor-app/src/components/Onboarding/GuidedTour.tsx create mode 100644 docs/editor-app/src/components/Onboarding/WelcomeModal.tsx create mode 100644 docs/editor-app/src/components/Panels/EnginePanel.tsx create mode 100644 docs/editor-app/src/components/Panels/InstructionsPanel.tsx create mode 100644 docs/editor-app/src/components/Panels/NetworkPanel.tsx create mode 100644 docs/editor-app/src/components/Panels/PanelContainer.tsx create mode 100644 docs/editor-app/src/components/Panels/PermissionsPanel.tsx create mode 100644 docs/editor-app/src/components/Panels/PropertiesPanel.tsx create mode 100644 docs/editor-app/src/components/Panels/SafeOutputsPanel.tsx create mode 100644 docs/editor-app/src/components/Panels/StepsPanel.tsx create mode 100644 docs/editor-app/src/components/Panels/ToolsPanel.tsx create mode 100644 docs/editor-app/src/components/Panels/TriggerPanel.tsx create mode 100644 docs/editor-app/src/components/Sidebar/NodePalette.tsx create mode 100644 docs/editor-app/src/components/Sidebar/Sidebar.tsx create mode 100644 docs/editor-app/src/components/Sidebar/TemplateGallery.tsx create mode 100644 docs/editor-app/src/components/YamlPreview/MarkdownSource.tsx create mode 100644 docs/editor-app/src/components/YamlPreview/YamlPreview.tsx create mode 100644 docs/editor-app/src/components/shared/FieldLabel.tsx create mode 100644 docs/editor-app/src/components/shared/HelpTooltip.tsx create mode 100644 docs/editor-app/src/components/shared/ResizablePanel.tsx create mode 100644 docs/editor-app/src/components/shared/StatusBadge.tsx create mode 100644 docs/editor-app/src/hooks/useAutoCompile.ts create mode 100644 docs/editor-app/src/hooks/useCompiler.ts create mode 100644 docs/editor-app/src/hooks/useTheme.ts create mode 100644 docs/editor-app/src/main.tsx create mode 100644 docs/editor-app/src/stores/uiStore.ts create mode 100644 docs/editor-app/src/stores/workflowStore.ts create mode 100644 docs/editor-app/src/styles/globals.css create mode 100644 docs/editor-app/src/styles/nodes.css create mode 100644 docs/editor-app/src/styles/panels.css create mode 100644 docs/editor-app/src/types/compiler.ts create mode 100644 docs/editor-app/src/types/nodes.ts create mode 100644 docs/editor-app/src/types/workflow.ts create mode 100644 docs/editor-app/src/utils/compiler.ts create mode 100644 docs/editor-app/src/utils/fieldDescriptions.ts create mode 100644 docs/editor-app/src/utils/markdownGenerator.ts create mode 100644 docs/editor-app/src/utils/templates.ts create mode 100644 docs/editor-app/src/vite-env.d.ts create mode 100644 docs/editor-app/tsconfig.json create mode 100644 docs/editor-app/vite.config.ts diff --git a/docs/editor-app/ARCHITECTURE.md b/docs/editor-app/ARCHITECTURE.md new file mode 100644 index 00000000000..4b5594613cb --- /dev/null +++ b/docs/editor-app/ARCHITECTURE.md @@ -0,0 +1,323 @@ +# Visual Workflow Editor — Architecture + +## Tech Stack + +| Layer | Technology | Purpose | +|-------|-----------|---------| +| Build | Vite 6 + React 19 + TypeScript 5.7 | Fast HMR, modern bundling | +| Visual Graph | @xyflow/react v12 (React Flow) | Node-based flow editor | +| UI Components | @primer/react v37 | GitHub-native design system | +| Accessible Primitives | @radix-ui/* | Dialog, tooltip, select, tabs, accordion | +| State Management | Zustand v5 | Lightweight, middleware-ready store | +| Drag & Drop | @dnd-kit/core + @dnd-kit/sortable | Sidebar-to-canvas DnD | +| Icons | @primer/octicons-react + lucide-react | Consistent iconography | +| Animations | framer-motion v11 | Panel transitions, node effects | +| Syntax Highlighting | Prism.js (via CDN or prism-react-renderer) | YAML output highlighting | +| Notifications | sonner | Toast notifications | +| WASM Runtime | Go WASM (existing gh-aw.wasm) | Real-time compilation | + +## Build Configuration + +``` +docs/editor-app/ ← Vite project root +├── vite.config.ts ← Output to ../public/editor/ +├── index.html ← SPA entry point +├── public/ ← Static assets (copied as-is) +└── src/ ← React source +``` + +**Vite config key points:** +- `build.outDir`: `../public/editor/` +- `base`: `/gh-aw/editor/` (GitHub Pages subpath) +- WASM files served from `/gh-aw/wasm/` (existing location) +- Dev server proxies `/wasm/` to `../public/wasm/` + +## Component Hierarchy + +``` + +├── # Primer theme (light/dark) +│ ├──
# Logo, workflow name, status, actions +│ │ ├── +│ │ ├── # Ready / Compiling / Error badge +│ │ ├── +│ │ ├── +│ │ ├── # Download .md, .yml, copy clipboard +│ │ └── +│ │ +│ ├── # Three-panel resizable layout +│ │ ├── # Left panel (240px, collapsible) +│ │ │ ├── # Drag-to-add node categories +│ │ │ │ ├── # "Triggers", "Configuration", etc. +│ │ │ │ └── # Individual draggable node type +│ │ │ ├── # Pre-built workflow templates +│ │ │ │ ├── +│ │ │ │ └── +│ │ │ └── # Switch: Visual / Markdown / YAML +│ │ │ +│ │ ├── # Center panel (flex-grow) +│ │ │ ├── +│ │ │ │ ├── # React Flow instance +│ │ │ │ │ ├── +│ │ │ │ │ ├── +│ │ │ │ │ ├── +│ │ │ │ │ ├── +│ │ │ │ │ ├── +│ │ │ │ │ ├── +│ │ │ │ │ ├── +│ │ │ │ │ └── +│ │ │ │ ├── +│ │ │ │ └── # Zoom, fit, lock +│ │ │ └── # Shown when no nodes +│ │ │ +│ │ └── # Right panel (360px, collapsible) +│ │ ├── +│ │ ├── +│ │ ├── +│ │ ├── +│ │ ├── +│ │ ├── +│ │ ├── +│ │ └── +│ │ +│ ├── # Bottom drawer / side panel +│ │ ├── # "YAML Output" | "Markdown Source" +│ │ ├── +│ │ └── +│ │ +│ ├── # First-visit welcome +│ ├── # Step-by-step tooltips +│ └── # Notification toasts +``` + +## State Model (Zustand) + +```typescript +interface WorkflowState { + // Metadata + name: string; + description: string; + + // Trigger configuration + trigger: { + event: string; // 'issues' | 'pull_request' | 'issue_comment' | ... + activityTypes: string[]; // 'opened' | 'closed' | 'labeled' | ... + branches?: string[]; + paths?: string[]; + schedule?: string; // cron expression + skipRoles?: string[]; + skipBots?: boolean; + }; + + // Permissions + permissions: Record; + // e.g. { contents: 'write', issues: 'write', pull_requests: 'read' } + + // Engine + engine: { + type: 'claude' | 'copilot' | 'codex' | 'custom'; + model?: string; + config?: Record; + }; + + // Tools + tools: string[]; + // e.g. ['github', 'playwright', 'bash', 'web-search'] + + // Instructions (markdown body) + instructions: string; + + // Safe outputs + safeOutputs: Record>; + // e.g. { 'create-issue': true, 'add-comment': true, 'add-labels': { labels: ['bug'] } } + + // Network + network: { + allowed: string[]; + blocked: string[]; + }; + + // Advanced + sandbox?: Record; + mcpServers?: Record; + steps?: unknown[]; + postSteps?: unknown[]; + imports?: string[]; + environment?: Record; + cache?: boolean; + strict?: boolean; + + // UI state + selectedNodeId: string | null; + viewMode: 'visual' | 'markdown' | 'yaml'; + compiledYaml: string; + compiledMarkdown: string; + warnings: string[]; + error: string | null; + isCompiling: boolean; + isReady: boolean; + + // Actions + setTrigger: (trigger: Partial) => void; + setPermissions: (perms: Record) => void; + setEngine: (engine: Partial) => void; + toggleTool: (tool: string) => void; + setInstructions: (text: string) => void; + toggleSafeOutput: (key: string) => void; + setNetwork: (network: Partial) => void; + selectNode: (id: string | null) => void; + setViewMode: (mode: 'visual' | 'markdown' | 'yaml') => void; + loadTemplate: (template: WorkflowTemplate) => void; + reset: () => void; +} +``` + +## Data Flow + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ Visual UI │────▶│ Zustand Store │────▶│ Markdown Generator│ +│ (React) │ │ (state) │ │ (state → .md) │ +└──────────────┘ └──────────────┘ └──────────────────┘ + │ + ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ YAML Output │◀────│ Web Worker │◀────│ WASM Compiler │ +│ (preview) │ │ (postMessage)│ │ (gh-aw.wasm) │ +└──────────────┘ └──────────────┘ └──────────────────┘ +``` + +1. User interacts with visual UI (clicks, types, toggles) +2. React components call Zustand actions +3. Zustand middleware triggers markdown generation (debounced 400ms) +4. Generated markdown is sent to Web Worker via postMessage +5. Worker runs WASM `compileWorkflow(markdown)` +6. Result (yaml, warnings, error) posted back to main thread +7. Zustand store updates `compiledYaml`, `warnings`, `error` +8. YAML preview panel re-renders with new output + +## File Structure + +``` +docs/editor-app/ +├── index.html +├── vite.config.ts +├── tsconfig.json +├── package.json +├── ARCHITECTURE.md +├── DESIGN.md +├── USER-JOURNEYS.md +└── src/ + ├── main.tsx # React entry point + ├── App.tsx # Root component + ├── types/ + │ ├── workflow.ts # WorkflowState, WorkflowTemplate types + │ ├── nodes.ts # React Flow node type definitions + │ └── compiler.ts # WASM compiler message types + ├── stores/ + │ ├── workflowStore.ts # Main Zustand store + │ └── uiStore.ts # UI-only state (panels, theme) + ├── utils/ + │ ├── markdownGenerator.ts # State → markdown conversion + │ ├── compiler.ts # WASM compiler bridge + │ ├── templates.ts # Template definitions + │ └── fieldDescriptions.ts # Plain English field descriptions + ├── components/ + │ ├── Header/ + │ │ ├── Header.tsx + │ │ ├── CompilationStatus.tsx + │ │ └── ExportMenu.tsx + │ ├── Sidebar/ + │ │ ├── Sidebar.tsx + │ │ ├── NodePalette.tsx + │ │ └── TemplateGallery.tsx + │ ├── Canvas/ + │ │ ├── WorkflowGraph.tsx + │ │ └── EmptyState.tsx + │ ├── Nodes/ + │ │ ├── BaseNode.tsx # Shared node chrome + │ │ ├── TriggerNode.tsx + │ │ ├── PermissionsNode.tsx + │ │ ├── EngineNode.tsx + │ │ ├── ToolsNode.tsx + │ │ ├── InstructionsNode.tsx + │ │ ├── SafeOutputsNode.tsx + │ │ ├── NetworkNode.tsx + │ │ └── StepsNode.tsx + │ ├── Panels/ + │ │ ├── PanelContainer.tsx # Shared panel chrome + │ │ ├── TriggerPanel.tsx + │ │ ├── PermissionsPanel.tsx + │ │ ├── EnginePanel.tsx + │ │ ├── ToolsPanel.tsx + │ │ ├── InstructionsPanel.tsx + │ │ ├── SafeOutputsPanel.tsx + │ │ ├── NetworkPanel.tsx + │ │ └── StepsPanel.tsx + │ ├── YamlPreview/ + │ │ ├── YamlPreview.tsx + │ │ └── MarkdownSource.tsx + │ ├── Onboarding/ + │ │ ├── WelcomeModal.tsx + │ │ └── GuidedTour.tsx + │ └── shared/ + │ ├── ResizablePanel.tsx + │ ├── HelpTooltip.tsx + │ ├── FieldLabel.tsx # Label + help icon + description + │ └── StatusBadge.tsx + ├── hooks/ + │ ├── useCompiler.ts # WASM compilation hook + │ ├── useAutoCompile.ts # Debounced auto-compile + │ └── useTheme.ts # Theme detection + persistence + └── styles/ + ├── globals.css # Reset, Primer tokens + ├── nodes.css # React Flow node styles + └── panels.css # Property panel styles +``` + +## WASM Integration + +The existing WASM infrastructure is preserved: +- `docs/public/wasm/gh-aw.wasm` — 16MB Go WASM binary +- `docs/public/wasm/wasm_exec.js` — Go runtime glue +- `docs/public/wasm/compiler-loader.js` — Worker API wrapper +- `docs/public/wasm/compiler-worker.js` — Web Worker script + +The React app creates a typed bridge: + +```typescript +// src/utils/compiler.ts +import { createWorkerCompiler } from '/wasm/compiler-loader.js'; + +let compiler: WorkerCompiler | null = null; + +export async function initCompiler(): Promise { + compiler = await createWorkerCompiler('/wasm/'); +} + +export async function compile(markdown: string): Promise { + if (!compiler) throw new Error('Compiler not initialized'); + return compiler.compile(markdown); +} +``` + +The `useAutoCompile` hook subscribes to store changes and triggers compilation: + +```typescript +// src/hooks/useAutoCompile.ts +export function useAutoCompile() { + const store = useWorkflowStore(); + + useEffect(() => { + const unsubscribe = useWorkflowStore.subscribe( + (state) => generateMarkdown(state), + debounce((markdown) => { + compile(markdown).then(result => { + store.setCompilationResult(result); + }); + }, 400) + ); + return unsubscribe; + }, []); +} +``` diff --git a/docs/editor-app/DESIGN.md b/docs/editor-app/DESIGN.md new file mode 100644 index 00000000000..55be13fcd89 --- /dev/null +++ b/docs/editor-app/DESIGN.md @@ -0,0 +1,1585 @@ +# Visual Agentic Workflow Editor - Design Specification + +## Table of Contents + +1. [Design Philosophy](#1-design-philosophy) +2. [Layout Design](#2-layout-design) +3. [Visual Flow Graph Design](#3-visual-flow-graph-design) +4. [Property Panel Designs](#4-property-panel-designs) +5. [Color System & Theme](#5-color-system--theme) +6. [Interactions & Animations](#6-interactions--animations) +7. [Component Specifications](#7-component-specifications) +8. [Onboarding Design](#8-onboarding-design) + +--- + +## 1. Design Philosophy + +**Target Users**: Business analysts, product managers, team leads - people who understand *what* they want an AI agent to do but shouldn't need to write YAML. + +**Core Principles**: +- **Visual-first**: Every concept has a visual representation. No raw text editing required. +- **Progressive disclosure**: Show the simplest view by default; advanced options are one click away. +- **Safe defaults**: Pre-fill smart defaults so users can create working workflows with minimal input. +- **Immediate feedback**: Compile in real-time so users see results as they build. + +**Mental Model**: A workflow is a visual pipeline: *"When X happens, use Y engine with Z tools to follow these instructions, then produce these outputs."* Each concept is a card (node) on a canvas, connected by lines showing the flow. + +--- + +## 2. Layout Design + +### 2.1 Overall Layout + +``` ++------------------------------------------------------------------+ +| [Logo] Workflow Name [Status] [Theme] [Compile] [Export] | <- Header (48px) ++----------+-----------------------------------+--------------------+ +| | | | +| SIDEBAR | CANVAS | PROPERTIES | +| (240px) | (flex) | (360px) | +| | | | +| [Nodes] | +-------+ +--------+ | [Form fields for | +| [Search] | |Trigger |---->|Engine | | selected node] | +| | +-------+ +--------+ | | +| Trigger | | | | +| Engine | +--------+ | | +| Tools | | Tools | | | +| Instruct | +--------+ | | +| Outputs | | | | +| Network | +--------+ | | +| Perms | |Outputs | | | +| | +--------+ | | +| | | | ++----------+-----------------------------------+--------------------+ +| [Zoom: -/+/Fit] [Minimap] [YAML Preview Toggle] | <- Footer bar (32px) ++------------------------------------------------------------------+ +``` + +### 2.2 Header Bar + +``` ++------------------------------------------------------------------+ +| [>_] gh-aw | my-workflow.md [*] | Ready | [D] [Compile] [v] | ++------------------------------------------------------------------+ + ^ ^ ^ ^ ^ ^ ^ + Logo Name (editable) Dirty Status Theme Button Export + dot badge toggle dropdown +``` + +- **Height**: 48px +- **Background**: `--bgColor-default` (Primer) +- **Border**: 1px bottom border `--borderColor-default` +- **Logo**: Terminal icon `>_` in accent color, links back to docs. 20x20px. +- **Workflow Name**: Inline-editable text field, `font-size: 14px`, `font-weight: 600`. Click to edit, blur to save. +- **Dirty Indicator**: Small 6px circle, `--fgColor-attention` (amber), appears when unsaved changes exist. +- **Status Badge**: Primer `Label` component: + - `Label--success` + green dot: "Ready" + - `Label--accent` + pulsing dot: "Compiling..." + - `Label--danger` + red dot: "Error" +- **Theme Toggle**: `btn-octicon` with sun/moon icon. 32x32px hit target. +- **Compile Button**: Primer `btn btn-primary btn-sm`. Play icon + "Compile" text. +- **Export Dropdown**: `btn btn-sm` with caret. Options: "Download .md", "Download .lock.yml", "Copy YAML", "Open in Playground". + +### 2.3 Sidebar (Node Palette) + +``` ++-----------+ +| PALETTE | ++-----------+ +| [Search] | <- Filter nodes ++-----------+ +| | +| [*] Trigg | <- Draggable node cards +| [B] Engin | +| [W] Tools | +| [D] Instr | +| [M] Outpu | +| [G] Netwo | +| [S] Perms | +| | ++-----------+ +| TEMPLATES | <- Expandable section ++-----------+ +| PR Review | +| Issue Tri | +| Code Scan | ++-----------+ +``` + +- **Width**: 240px (collapsible to 48px icon-only rail) +- **Background**: `--bgColor-default` +- **Border**: 1px right border +- **Search**: Primer `TextInput` with search icon, `font-size: 13px`, 8px padding +- **Node Cards**: + - Dimensions: 208px wide x 56px tall + - Border-radius: 8px + - Left accent bar: 3px wide, colored by node type + - Icon: 20x20px, left-aligned + - Label: `font-size: 13px`, `font-weight: 500` + - Subtitle: `font-size: 11px`, `color: --fgColor-muted` + - Cursor: `grab` on hover + - On drag start: opacity 0.6, show ghost card following cursor +- **Collapse Button**: Chevron icon at top-right of sidebar. Sidebar collapses to 48px rail showing only icons. +- **Templates Section**: Collapsible accordion. Each template is a pre-built workflow graph that can be dragged onto the canvas. + +### 2.4 Canvas (Center) + +- **Flex**: Takes all remaining horizontal space +- **Background**: Dot grid pattern (dots spaced 20px apart) + - Light mode: dots `rgba(0,0,0,0.06)` on `#ffffff` + - Dark mode: dots `rgba(255,255,255,0.04)` on `#0d1117` +- **Pan**: Middle-click drag or Space+drag +- **Zoom**: Scroll wheel, range 25%-200%, default 100% +- **Selection**: Click node to select (blue outline), click canvas to deselect +- **Multi-select**: Shift+click or drag selection rectangle + +### 2.5 Properties Panel (Right) + +- **Width**: 360px (collapsible) +- **Background**: `--bgColor-default` +- **Border**: 1px left border +- **Header**: Selected node type icon + name, 40px height +- **Content**: Scrollable form area below header +- **Empty State**: "Select a node to edit its properties" with pointer illustration +- **Collapse**: Click the `<<` button to hide panel; a `>>` tab appears on the right edge + +### 2.6 Footer Bar + +``` ++------------------------------------------------------------------+ +| [-] 100% [+] [Fit] [Minimap] [YAML Preview: Off/On] | ++------------------------------------------------------------------+ +``` + +- **Height**: 32px +- **Background**: `--bgColor-subtle` +- **Zoom Controls**: `-` / percentage / `+` / "Fit" button. All `btn-octicon` style. +- **Minimap Toggle**: Small rectangle icon, opens a 160x120px minimap overlay in bottom-right of canvas. +- **YAML Preview Toggle**: Slide toggle. When enabled, a bottom drawer slides up showing the compiled YAML (read-only, syntax-highlighted). + +--- + +## 3. Visual Flow Graph Design + +### 3.1 Node Anatomy + +Every node follows the same base structure: + +``` ++--+---------------------------------------+ +| | [Icon] Node Title | +| | subtitle / preview text | +| +---------------------------------------+ +|AC| Node-specific content | +| | (badges, chips, preview) | ++--+---------------------------------------+ + ^ + Accent bar (3px, node-type color) +``` + +- **Base dimensions**: 260px wide, variable height (min 64px) +- **Border-radius**: 12px +- **Border**: 1px solid `--borderColor-default` +- **Shadow**: `0 1px 3px rgba(0,0,0,0.08)` (light), `0 1px 3px rgba(0,0,0,0.3)` (dark) +- **Background**: `--bgColor-default` +- **Accent bar**: 3px left border, colored by node type +- **Title**: `font-size: 13px`, `font-weight: 600` +- **Subtitle**: `font-size: 11px`, `color: --fgColor-muted`, max 1 line, ellipsis overflow + +### 3.2 Connection Lines (Edges) + +- **Style**: Bezier curves with smooth entry/exit +- **Color**: `--borderColor-default` (idle), `--fgColor-accent` (active/selected) +- **Width**: 2px +- **Arrow**: Small chevron at target end, 8px +- **Animation on creation**: Line draws from source to target over 200ms + +### 3.3 Node Type Designs + +#### 3.3.1 Trigger Node + +``` ++--+---------------------------------------+ +| | [Zap] Trigger | +| | When this happens... | +|G |---------------------------------------+ +|R | | +|E | +------------------+ | +|E | | issue_comment | [opened] | +|N | +------------------+ | +| | Schedule: every Monday 9am | ++--+---------------------------------------+ +``` + +- **Accent**: `#2da44e` (green-500) +- **Icon**: Zap/lightning bolt, 18x18px, filled green +- **Content**: + - Event type shown as a Primer `Label` badge + - Activity types shown as small gray `Label--secondary` badges + - Schedule displayed as human-readable text (converted from cron) + - If `workflow_dispatch`: shows "Manual trigger" with play button icon +- **Handles**: Single output handle on the right edge (small 8px circle) + +#### 3.3.2 Permissions Node + +``` ++--+---------------------------------------+ +| | [Shield] Permissions | +|Y | Token access levels | +|E |---------------------------------------+ +|L | | +|L | [issues: write] [contents: read] | +|O | [pull-requests: write] [metadata: R] | +|W | [models: read] | +| | | ++--+---------------------------------------+ +``` + +- **Accent**: `#d4a72c` (yellow-600) +- **Icon**: Shield with checkmark, 18x18px +- **Content**: + - Permission badges in a flex-wrap grid + - Each badge: `Label` with colored variant + - `write`: `Label--attention` (amber background) + - `read`: `Label--success` (green background) + - `none`: `Label--secondary` (gray, dimmed) + - Format: `scope: level` (e.g., "issues: write") + - If `read-all` or `write-all` shorthand: single large badge +- **Handles**: Input on left, output on right + +#### 3.3.3 Engine Node + +``` ++--+---------------------------------------+ +| | [Brain] Engine | +|B | AI processor | +|L |---------------------------------------+ +|U | | +|E | +------+ Claude | +| | | LOGO | Model: claude-sonnet-4-... | +| | +------+ Max turns: 10 | +| | | ++--+---------------------------------------+ +``` + +- **Accent**: `#0969da` (blue-500) +- **Icon**: Brain/sparkle, 18x18px +- **Content**: + - Engine logo (24x24px): Claude (purple anthropic icon), Copilot (GitHub Copilot icon), Codex (OpenAI icon) + - Engine name in bold: "Claude", "Copilot", or "Codex" + - Model shown below in muted text (if set) + - Max turns shown as a small detail + - If custom engine: shows "Custom" with gear icon +- **Handles**: Input on left, output on right + +#### 3.3.4 Tools Node + +``` ++--+---------------------------------------+ +| | [Wrench] Tools | +|P | MCP servers & capabilities | +|U |---------------------------------------+ +|R | | +|P | [GH] GitHub [PW] Playwright | +|L | [B] Bash [E] Edit | +|E | [WF] Web Fetch [S] Serena | +| | | ++--+---------------------------------------+ +``` + +- **Accent**: `#8250df` (purple-500) +- **Icon**: Wrench, 18x18px +- **Content**: + - Tool chips in a flex-wrap grid + - Each chip: rounded pill shape (border-radius: 12px) + - 16x16px icon on left (tool-specific icon) + - Tool name text, `font-size: 12px` + - Background: light purple tint `rgba(130,80,223,0.08)` + - Tool-specific icons: + - GitHub: Octocat mark + - Bash: Terminal `>_` + - Playwright: Drama mask + - Edit: Pencil + - Web Fetch: Globe with arrow + - Web Search: Magnifying glass + - Serena: Code brackets + - Cache Memory: Database + - Repo Memory: Git branch +- **Handles**: Input on left, output on right + +#### 3.3.5 Instructions Node + +``` ++--+---------------------------------------+ +| | [Doc] Instructions | +|G | Mission & context for the agent | +|R |---------------------------------------+ +|A | | +|Y | # Mission | +| | Review the pull request for | +| | security issues and suggest... | +| | ~~~ | ++--+---------------------------------------+ +``` + +- **Accent**: `#57606a` (gray-500) +- **Icon**: Document/file text, 18x18px +- **Content**: + - Rendered markdown preview (first ~4 lines) + - Monospace font, `font-size: 12px`, `line-height: 1.5` + - Overflow: fade-to-background gradient at bottom + - Click opens full markdown editor in Properties panel +- **Handles**: Input on left, output on right + +#### 3.3.6 Safe Outputs Node + +``` ++--+---------------------------------------+ +| | [Megaphone] Safe Outputs | +|G | What the agent can produce | +|R |---------------------------------------+ +|E | | +|E | [+] add-comment [+] create-issue | +|N | [+] add-labels [+] create-pr | +| | [+] submit-review | +| | | ++--+---------------------------------------+ +``` + +- **Accent**: `#1a7f37` (green-600) +- **Icon**: Megaphone/broadcast, 18x18px +- **Content**: + - Output type badges in flex-wrap grid + - Each badge: Primer `Label--success` with `+` icon prefix + - Human-readable names (hyphenated): "add-comment", "create-issue", "create-pr" + - Badge count shown in node title area: "Safe Outputs (5)" +- **Handles**: Input on left (no output - terminal node) + +#### 3.3.7 Network Node + +``` ++--+---------------------------------------+ +| | [Globe] Network | +|R | Egress control & firewall | +|E |---------------------------------------+ +|D | | +| | Allowed: 5 domains | +| | [github.com] [api.anthropic.com] | +| | [pypi.org] +2 more | +| | Firewall: AWF v0.13.5 | ++--+---------------------------------------+ +``` + +- **Accent**: `#cf222e` (red-500) +- **Icon**: Globe with shield overlay, 18x18px +- **Content**: + - "Allowed: N domains" summary line + - First 2-3 domain chips shown, `+N more` overflow indicator + - Domain chips: monospace font, `font-size: 11px`, gray background + - Firewall status line if configured + - If `network: defaults`: shows "Defaults" badge +- **Handles**: Input on left, output on right + +### 3.4 Default Flow Layout + +When creating a new workflow, nodes auto-arrange in this vertical order: + +``` + +----------+ + | Trigger | (always first - defines "when") + +----------+ + | + +----------+ + | Perms | (what access the workflow has) + +----------+ + | + +----------+ + | Engine | (which AI processes it) + +----------+ + | + +----------+ + | Tools | (what tools the AI can use) + +----------+ + | + +----------+ + | Instruct | (what the AI should do) + +----------+ + | + +----------+ + | Outputs | (what the AI can produce) + +----------+ + | + +----------+ + | Network | (network restrictions) + +----------+ +``` + +Edges connect each node to the next automatically. The user can rearrange freely, but this is the default. + +--- + +## 4. Property Panel Designs + +When a node is selected, the Properties panel shows a form specific to that node type. + +### 4.1 Common Panel Structure + +``` ++--------------------------------------------+ +| [NodeIcon] Node Type Name [X] | <- Panel header (40px) ++--------------------------------------------+ +| [Tab1] [Tab2] [Tab3] | <- Optional tabs ++--------------------------------------------+ +| | +| Section Header | +| ---------------------------------------- | +| Field Label [?] | +| [Input / Selector / Toggle] | +| | +| Field Label [?] | +| [Input / Selector / Toggle] | +| | +| > Advanced Options | <- Collapsible section +| | ++--------------------------------------------+ +``` + +- **Panel header**: 40px, node icon + type name + close button +- **Section headers**: `font-size: 12px`, `font-weight: 600`, uppercase, `letter-spacing: 0.5px`, `color: --fgColor-muted`. 1px bottom border. +- **Field labels**: `font-size: 13px`, `font-weight: 500`. Inline `[?]` tooltip icon. +- **Tooltips**: On `[?]` hover, show a Primer `Tooltip` with plain-English explanation. Max width 280px. +- **Spacing**: 16px padding, 12px gap between fields, 20px gap between sections +- **Scrolling**: Panel body scrolls independently of header + +### 4.2 Trigger Node Properties + +``` ++--------------------------------------------+ +| [Zap] Trigger [X] | ++--------------------------------------------+ +| | +| Event Type | +| ---------------------------------------- | +| Choose what triggers this workflow [?] | +| | +| +------------------+ +------------------+ | +| | [Chat] Issue | | [PR] Pull | | +| | Comment | | Request | | +| +------------------+ +------------------+ | +| +------------------+ +------------------+ | +| | [Cal] Schedule | | [Zap] Push | | +| +------------------+ +------------------+ | +| +------------------+ +------------------+ | +| | [Play] Manual | | [Tag] Release | | +| +------------------+ +------------------+ | +| +------------------+ +------------------+ | +| | [Slash] Slash | | [Bug] Issues | | +| | Command | | | | +| +------------------+ +------------------+ | +| | +| Activity Types [?] | +| (shown dynamically after event selection) | +| [x] opened [x] edited [ ] closed | +| [ ] reopened [ ] labeled | +| | +| > Conditional Execution | +| ---------------------------------------- | +| Skip Roles [?] | +| [Tag input: admin, write, ...] | +| | +| Skip Bots [?] | +| [Tag input: dependabot, ...] | +| | +| Skip If Match [?] | +| [Text input: GitHub search query] | +| | +| > Reactions & Status | +| ---------------------------------------- | +| Reaction [?] | +| [Emoji picker: +1 -1 heart rocket ...] | +| | +| Status Comment [?] | +| [Toggle: On/Off] | +| | +| > Manual Approval | +| ---------------------------------------- | +| Environment [?] | +| [Text input: environment name] | +| | ++--------------------------------------------+ +``` + +**Event Type Selector**: +- 2-column grid of selectable cards +- Each card: ~152px wide, 56px tall +- Icon (24x24) + event name +- Border: 1px solid `--borderColor-default` +- Selected: 2px solid `--fgColor-accent`, light accent background +- Hover: `--bgColor-subtle` + +**Event cards and their icons**: +| Event | Icon | Label | +|-------|------|-------| +| `issue_comment` | Chat bubble | Issue Comment | +| `pull_request` | Git merge | Pull Request | +| `schedule` | Calendar/clock | Schedule | +| `push` | Arrow up | Push | +| `workflow_dispatch` | Play circle | Manual | +| `release` | Tag | Release | +| `slash_command` | Forward slash | Slash Command | +| `issues` | Circle dot | Issues | +| `discussion` | Discussion bubble | Discussion | +| `pull_request_review` | Eye | PR Review | + +**Activity Types**: +- Checkbox grid, shown dynamically based on selected event type +- Pre-checked with common defaults (e.g., "opened" for issues) +- Maps to the event's `types:` field + +**Schedule Input** (when Schedule event selected): +- Natural language input: "every Monday at 9am" +- Shows converted cron expression below in muted monospace +- Preset buttons: "Hourly", "Daily", "Weekly", "Monthly" + +**Reaction Selector**: +- Horizontal row of clickable emoji icons +- Options: +1, -1, laugh, confused, heart, hooray, rocket, eyes, none +- Selected reaction gets accent ring +- Maps to `on.reaction` field + +### 4.3 Permissions Node Properties + +``` ++--------------------------------------------+ +| [Shield] Permissions [X] | ++--------------------------------------------+ +| | +| Quick Setup [?] | +| ---------------------------------------- | +| Choose a preset or customize individually | +| | +| ( ) Read All ( ) Write All (*) Custom | +| | +| Permission Scopes | +| ---------------------------------------- | +| | +| issues [None | Read | Write v] | +| contents [None | Read | Write v] | +| pull-requests [None | Read | Write v] | +| discussions [None | Read | Write v] | +| actions [None | Read | Write v] | +| checks [None | Read | Write v] | +| deployments [None | Read | Write v] | +| models [None | Read v] | +| packages [None | Read | Write v] | +| pages [None | Read | Write v] | +| security-events [None | Read | Write v] | +| statuses [None | Read | Write v] | +| id-token [None | Write v] | +| metadata [None | Read | Write v] | +| attestations [None | Read | Write v] | +| | +| [Auto-detect minimum permissions] | +| | ++--------------------------------------------+ +``` + +**Quick Setup**: Radio button group with three options. Selecting "Read All" or "Write All" disables the individual selectors and sets all to that level. + +**Permission Selectors**: +- Each row: scope name (left-aligned, `font-size: 13px`) + segmented control (right-aligned) +- Segmented control: 3 options ("None", "Read", "Write") as Primer `SegmentedControl` + - None: gray + - Read: green tint + - Write: amber tint +- Rows with non-default values are highlighted with subtle accent background +- Special cases: + - `id-token`: Only shows "None" and "Write" (no Read option) + - `models`: Only shows "None" and "Read" (no Write option) + +**Smart Defaults**: Based on selected engine and tools, auto-suggest permissions: +- If tools include `github` with write actions: suggest `issues: write`, `pull-requests: write`, `contents: read` +- If engine is `copilot`: suggest `models: read` +- "Auto-detect" button analyzes the full workflow and recommends the minimum set + +### 4.4 Engine Node Properties + +``` ++--------------------------------------------+ +| [Brain] Engine [X] | ++--------------------------------------------+ +| | +| AI Engine [?] | +| ---------------------------------------- | +| Which AI processes your workflow | +| | +| +-------------+ +-------------+ | +| | [A] Claude | | [GH] Copil | | +| | Anthropic | | ot (Rec.)| | +| +-------------+ +-------------+ | +| +-------------+ +-------------+ | +| | [OA] Codex | | [Gear] Cus | | +| | OpenAI | | tom | | +| +-------------+ +-------------+ | +| | +| Model (optional) [?] | +| [Dropdown: claude-sonnet-4-... v] | +| Tip: Leave blank for the latest default | +| | +| Max Turns [?] | +| [Slider: 1 ----o---------- 50] [10] | +| How many iterations the agent can take | +| | +| > Advanced | +| ---------------------------------------- | +| Version [?] | +| [Text input] | +| | +| Custom Command [?] | +| [Text input] | +| | +| Additional Args [?] | +| [Tag input: --flag, --verbose, ...] | +| | +| Environment Variables [?] | +| [Key-Value editor] | +| | +| Concurrency | +| Group [Text input] | +| Cancel in-progress [Toggle] | +| | ++--------------------------------------------+ +``` + +**Engine Selector**: +- 2-column grid of selectable cards (same pattern as Trigger events) +- Each card shows: engine logo (24x24) + engine name + company name +- "Copilot" card has "(Recommended)" tag +- Selected card: accent border + background tint + +**Engine cards**: +| Engine | Logo | Name | Subtitle | +|--------|------|------|----------| +| `claude` | Anthropic A mark (purple) | Claude | By Anthropic | +| `copilot` | GitHub Copilot icon | Copilot | By GitHub | +| `codex` | OpenAI icon | Codex | By OpenAI | +| custom | Gear icon | Custom | Bring your own | + +**Model Dropdown**: +- Shown after engine selection +- Pre-populated with known models for the selected engine: + - Claude: claude-sonnet-4-20250514, claude-opus-4-20250514, etc. + - Copilot: (default, managed by GitHub) + - Codex: gpt-4, etc. +- Free-text entry allowed for custom model strings + +**Max Turns Slider**: +- Range: 1-50, default 10 +- Slider with numeric input box beside it +- Tooltip: "Higher values let the agent take more steps but cost more" + +### 4.5 Tools Node Properties + +``` ++--------------------------------------------+ +| [Wrench] Tools [X] | ++--------------------------------------------+ +| | +| Available Tools [?] | +| ---------------------------------------- | +| Toggle the tools your agent can use | +| | +| +------------------------------------------+ +| | [GH] GitHub API [Toggle: ON ] || +| | Repository operations [Configure >]|| +| +------------------------------------------+ +| | [>_] Bash [Toggle: ON ] || +| | Shell command execution || +| +------------------------------------------+ +| | [Pencil] Edit [Toggle: OFF] || +| | File reading & writing || +| +------------------------------------------+ +| | [Drama] Playwright [Toggle: OFF] || +| | Browser automation [Configure >]|| +| +------------------------------------------+ +| | [Globe] Web Fetch [Toggle: OFF] || +| | Download web pages || +| +------------------------------------------+ +| | [Search] Web Search [Toggle: OFF] || +| | Internet search || +| +------------------------------------------+ +| | [Code] Serena [Toggle: OFF] || +| | AI code intelligence [Configure >]|| +| +------------------------------------------+ +| | [DB] Cache Memory [Toggle: OFF] || +| | Persistent storage [Configure >]|| +| +------------------------------------------+ +| | [Git] Repo Memory [Toggle: OFF] || +| | Git-based storage [Configure >]|| +| +------------------------------------------+ +| | [AW] Agentic Workflows [Toggle: OFF] || +| | Workflow introspection || +| +------------------------------------------+ +| | +| > GitHub Tool Configuration | +| (expanded when "Configure >" is clicked) | +| ---------------------------------------- | +| Mode: ( ) Local (*) Remote | +| Read-only: [Toggle: OFF] | +| Lockdown: [Toggle: OFF] | +| Allowed Functions [?] | +| [Tag input: create_issue, ...] | +| Toolsets [?] | +| [Tag input: repos, issues, ...] | +| | +| Timeouts | +| ---------------------------------------- | +| Operation Timeout [?] | +| [Number input: 300] seconds | +| Startup Timeout [?] | +| [Number input: 60] seconds | +| | ++--------------------------------------------+ +``` + +**Tool Toggle Cards**: +- Full-width list items, 52px tall +- Icon (20x20) + tool name (bold) + description (muted) on left +- Toggle switch on right +- "Configure >" link appears for tools with sub-options (GitHub, Playwright, Serena, Cache Memory, Repo Memory) +- Clicking "Configure >" expands an inline configuration section below the toggle card + +**Tool list with descriptions**: +| Tool | Icon | Description | Has Config | +|------|------|-------------|------------| +| GitHub | Octocat | Repository operations (issues, PRs, content) | Yes | +| Bash | Terminal | Shell command execution | No | +| Edit | Pencil | File reading & writing | No | +| Playwright | Drama mask | Browser automation & screenshots | Yes | +| Web Fetch | Globe+arrow | Download web pages & API responses | No | +| Web Search | Magnifier | Internet search & results | No | +| Serena | Code brackets | AI code intelligence with language services | Yes | +| Cache Memory | Database | Persistent cross-run storage via cache | Yes | +| Repo Memory | Git branch | Git-based persistent storage | Yes | +| Agentic Workflows | Workflow icon | Workflow introspection & analysis | No | + +**GitHub Sub-configuration** (expanded): +- Mode selector: Radio buttons "Local" / "Remote" +- Read-only toggle +- Lockdown toggle +- Allowed functions: Tag input with autocomplete from known GitHub API functions +- Toolsets: Tag input with suggestions (repos, issues, pull_requests, etc.) +- GitHub App: Optional section for app-id and private-key fields + +**Playwright Sub-configuration** (expanded): +- Allowed domains: Tag input +- Version: Text input + +**Cache Memory Sub-configuration** (expanded): +- Cache key: Text input +- Description: Text input +- Retention days: Number input (1-90) +- Restore only: Toggle +- Scope: Radio buttons "Workflow" / "Repo" +- Allowed extensions: Tag input + +**Repo Memory Sub-configuration** (expanded): +- Branch prefix: Text input (default: "memory") +- Target repo: Text input (default: current repo) +- File glob: Tag input +- Max file size: Number input (bytes) +- Max file count: Number input + +### 4.6 Instructions Node Properties + +``` ++--------------------------------------------+ +| [Doc] Instructions [X] | ++--------------------------------------------+ +| [Edit] [Preview] | ++--------------------------------------------+ +| | +| # Mission | +| | +| Review the pull request and check for: | +| - Security vulnerabilities | +| - Code quality issues | +| - Test coverage gaps | +| | +| ## Guidelines | +| | +| - Be constructive and specific | +| - Suggest fixes, not just problems | +| - Reference relevant documentation | +| | +| 1,247 / 20,000 characters | +| | +| > Quick Snippets | +| ---------------------------------------- | +| [Review checklist] | +| [Issue triage rules] | +| [Security guidelines] | +| [Documentation style] | +| | ++--------------------------------------------+ +``` + +**Editor Mode**: +- Full markdown editor filling the panel body +- Monospace font, `font-size: 13px`, `line-height: 1.6` +- Syntax highlighting for markdown headings, lists, bold/italic +- Tab key inserts 2 spaces +- Character count shown below: current / max + +**Preview Mode**: +- Rendered markdown preview using GitHub-flavored markdown +- Read-only + +**Tabs**: "Edit" and "Preview" toggle tabs at top (Primer `UnderlineNav`) + +**Quick Snippets**: Pre-built instruction templates the user can insert with one click. Each inserts a block of markdown at the cursor position. + +### 4.7 Safe Outputs Node Properties + +``` ++--------------------------------------------+ +| [Megaphone] Safe Outputs [X] | ++--------------------------------------------+ +| | +| Output Actions [?] | +| ---------------------------------------- | +| What your agent can create or modify | +| | +| Comments & Reviews | +| ........................................ | +| [x] Add Comment | +| [x] Create PR Review Comment | +| [x] Submit PR Review | +| [x] Reply to Review Comment | +| [ ] Resolve Review Thread | +| [ ] Hide Comment | +| | +| Issues | +| ........................................ | +| [x] Create Issue | +| [ ] Close Issue | +| [ ] Update Issue | +| [ ] Link Sub-Issue | +| | +| Pull Requests | +| ........................................ | +| [ ] Create Pull Request | +| [ ] Close Pull Request | +| [ ] Update Pull Request | +| [ ] Mark PR Ready for Review | +| [ ] Push to PR Branch | +| | +| Labels & Assignment | +| ........................................ | +| [x] Add Labels | +| [ ] Remove Labels | +| [ ] Add Reviewer | +| [ ] Assign to User | +| [ ] Unassign from User | +| [ ] Assign to Agent (@copilot) | +| [ ] Assign Milestone | +| | +| Discussions | +| ........................................ | +| [ ] Create Discussion | +| [ ] Close Discussion | +| [ ] Update Discussion | +| | +| Projects | +| ........................................ | +| [ ] Update Project | +| [ ] Create Project | +| [ ] Create Project Status Update | +| | +| Code Scanning | +| ........................................ | +| [ ] Create Code Scanning Alert | +| [ ] Autofix Code Scanning Alert | +| | +| Other | +| ........................................ | +| [ ] Dispatch Workflow | +| [ ] Upload Asset | +| [ ] Update Release | +| | +| > Global Settings | +| ---------------------------------------- | +| Staged Mode [?] [Toggle: OFF] | +| Preview without making real changes | +| | +| Allowed Domains [?] | +| [Tag input: github.com, ...] | +| | +| Max Patch Size [?] | +| [Number input: 100] KB | +| | +| Footer [?] [Toggle: ON] | +| | +| Threat Detection [?] [Toggle: OFF] | +| | ++--------------------------------------------+ +``` + +**Output Checkboxes**: +- Grouped by category with section sub-headers +- Each checkbox: `font-size: 13px` +- Categories separated by 12px gap + dotted sub-header line +- Count shown in node: "N actions enabled" + +**Complete output action list** (mapped from `safe-outputs` schema): +| Category | Action | Schema Key | +|----------|--------|------------| +| Comments & Reviews | Add Comment | `add-comment` | +| Comments & Reviews | Create PR Review Comment | `create-pull-request-review-comment` | +| Comments & Reviews | Submit PR Review | `submit-pull-request-review` | +| Comments & Reviews | Reply to Review Comment | `reply-to-pull-request-review-comment` | +| Comments & Reviews | Resolve Review Thread | `resolve-pull-request-review-thread` | +| Comments & Reviews | Hide Comment | `hide-comment` | +| Issues | Create Issue | `create-issue` | +| Issues | Close Issue | `close-issue` | +| Issues | Update Issue | `update-issue` | +| Issues | Link Sub-Issue | `link-sub-issue` | +| Pull Requests | Create Pull Request | `create-pull-request` | +| Pull Requests | Close Pull Request | `close-pull-request` | +| Pull Requests | Update Pull Request | `update-pull-request` | +| Pull Requests | Mark PR Ready | `mark-pull-request-as-ready-for-review` | +| Pull Requests | Push to PR Branch | `push-to-pull-request-branch` | +| Labels & Assignment | Add Labels | `add-labels` | +| Labels & Assignment | Remove Labels | `remove-labels` | +| Labels & Assignment | Add Reviewer | `add-reviewer` | +| Labels & Assignment | Assign to User | `assign-to-user` | +| Labels & Assignment | Unassign from User | `unassign-from-user` | +| Labels & Assignment | Assign to Agent | `assign-to-agent` | +| Labels & Assignment | Assign Milestone | `assign-milestone` | +| Discussions | Create Discussion | `create-discussion` | +| Discussions | Close Discussion | `close-discussion` | +| Discussions | Update Discussion | `update-discussion` | +| Projects | Update Project | `update-project` | +| Projects | Create Project | `create-project` | +| Projects | Project Status Update | `create-project-status-update` | +| Code Scanning | Create Alert | `create-code-scanning-alert` | +| Code Scanning | Autofix Alert | `autofix-code-scanning-alert` | +| Other | Dispatch Workflow | `dispatch-workflow` | +| Other | Upload Asset | `upload-asset` | +| Other | Update Release | `update-release` | +| Other | Create Agent Task | `create-agent-task` | +| Other | Create Agent Session | `create-agent-session` | + +### 4.8 Network Node Properties + +``` ++--------------------------------------------+ +| [Globe] Network [X] | ++--------------------------------------------+ +| | +| Network Access [?] | +| ---------------------------------------- | +| Control which domains the agent can reach | +| | +| Quick Setup | +| (*) Defaults (recommended) | +| ( ) Custom | +| ( ) Unrestricted (no firewall) | +| | +| Allowed Domains [?] | +| (shown when Custom is selected) | +| ---------------------------------------- | +| +-----------------------------------+ | +| | github.com [x] | | +| | api.anthropic.com [x] | | +| | pypi.org [x] | | +| | registry.npmjs.org [x] | | +| +-----------------------------------+ | +| [+ Add domain] | +| | +| Ecosystem Presets [?] | +| [x] Python (pypi.org, etc.) | +| [x] Node (npmjs.org, etc.) | +| [ ] Go (proxy.golang.org, etc.) | +| [ ] Rust (crates.io, etc.) | +| | +| Blocked Domains [?] | +| ---------------------------------------- | +| [Tag input: tracker.example.com, ...] | +| | +| > Firewall Settings | +| ---------------------------------------- | +| Firewall [?] | +| [Toggle: ON] AWF (Agent Workflow FW) | +| | +| Version [?] | +| [Text input: 0.13.5] | +| | +| Log Level [?] | +| [Dropdown: debug | info | warn | error] | +| | +| SSL Bump [?] | +| [Toggle: OFF] | +| | +| Allow URLs (requires SSL Bump) [?] | +| [Tag input: pattern, ...] | +| | ++--------------------------------------------+ +``` + +**Domain List**: +- Each domain in its own row with a delete `[x]` button +- `+ Add domain` button below the list +- Input validates domain format (hostname with optional wildcard) +- Supports `*.example.com` wildcard patterns + +**Ecosystem Presets**: +- Checkboxes that auto-add/remove known domains for each ecosystem +- Shows tooltip listing all domains in the preset on hover +- Maps to `network.allowed` array values like `"python"`, `"node"` + +--- + +## 5. Color System & Theme + +### 5.1 Node Accent Colors + +| Node Type | Light Mode | Dark Mode | CSS Variable | +|--------------|---------------|---------------|--------------------------| +| Trigger | `#2da44e` | `#3fb950` | `--node-trigger` | +| Permissions | `#d4a72c` | `#d29922` | `--node-permissions` | +| Engine | `#0969da` | `#58a6ff` | `--node-engine` | +| Tools | `#8250df` | `#bc8cff` | `--node-tools` | +| Instructions | `#57606a` | `#8b949e` | `--node-instructions` | +| Safe Outputs | `#1a7f37` | `#56d364` | `--node-safe-outputs` | +| Network | `#cf222e` | `#f85149` | `--node-network` | + +### 5.2 Status Colors + +| Status | Light Mode | Dark Mode | Usage | +|----------|---------------|---------------|--------------------------| +| Success | `#2da44e` | `#3fb950` | Compile success, ready | +| Warning | `#d4a72c` | `#d29922` | Compile warnings | +| Error | `#cf222e` | `#f85149` | Compile errors | +| Info | `#0969da` | `#58a6ff` | Loading, compiling | + +### 5.3 Light Theme Palette + +``` +Background (canvas): #ffffff +Background (subtle): #f6f8fa +Background (muted): #eaeef2 +Foreground (default): #1f2328 +Foreground (muted): #57606a +Foreground (subtle): #6e7781 +Border (default): #d0d7de +Border (muted): #d8dee4 +Accent (fg): #0969da +Accent (bg): #ddf4ff +``` + +### 5.4 Dark Theme Palette + +``` +Background (canvas): #0d1117 +Background (subtle): #161b22 +Background (muted): #21262d +Foreground (default): #e6edf3 +Foreground (muted): #8b949e +Foreground (subtle): #6e7681 +Border (default): #30363d +Border (muted): #21262d +Accent (fg): #58a6ff +Accent (bg): #388bfd26 +``` + +### 5.5 Canvas Grid + +- **Light mode**: `radial-gradient(circle, rgba(0,0,0,0.06) 1px, transparent 1px)` every 20px +- **Dark mode**: `radial-gradient(circle, rgba(255,255,255,0.04) 1px, transparent 1px)` every 20px + +### 5.6 Primer Design Tokens + +The editor uses GitHub Primer design tokens wherever possible: + +- Colors: `--bgColor-*`, `--fgColor-*`, `--borderColor-*` +- Typography: `--fontStack-monospace`, `--fontStack-system` +- Spacing: 4px base unit (4, 8, 12, 16, 20, 24, 32, 40, 48) +- Border radius: 6px (default), 8px (cards), 12px (nodes), 100px (pills) +- Shadows: Primer shadow utilities (`shadow-sm`, `shadow-md`) + +--- + +## 6. Interactions & Animations + +### 6.1 Node Interactions + +**Hover**: +- Border color transitions to `--borderColor-emphasis` (darker) +- Subtle shadow elevation: `0 2px 8px rgba(0,0,0,0.12)` +- Transition: `border-color 150ms ease, box-shadow 150ms ease` + +**Selected**: +- Border: 2px solid `--fgColor-accent` (blue) +- Shadow: `0 0 0 3px var(--bgColor-accent-muted)` (blue glow ring) +- Properties panel populates with node data + +**Dragging**: +- Opacity: 0.85 +- Shadow: `0 8px 24px rgba(0,0,0,0.15)` (elevated) +- Cursor: `grabbing` +- Other nodes dim slightly (opacity 0.7) + +**Delete** (when node selected): +- Press `Delete` or `Backspace` key +- Node fades out with scale-down: `opacity 0 + scale(0.95)` over 150ms +- Connected edges animate retraction + +### 6.2 Panel Transitions + +**Sidebar collapse**: +- Width animates from 240px to 48px over 200ms, `ease-out` +- Node labels fade out during first 100ms +- Icons remain visible in collapsed state + +**Properties panel collapse**: +- Width animates from 360px to 0px over 200ms, `ease-out` +- Content fades out during first 100ms +- A 32px wide "expand" tab slides in from the right edge + +**YAML Preview drawer**: +- Slides up from bottom of canvas area +- Height: 40% of canvas height +- Transition: `transform 250ms cubic-bezier(0.4, 0, 0.2, 1)` +- Drag handle at top for resizing + +### 6.3 Compilation Status Animation + +**Compile button press**: +1. Button text changes to "Compiling..." with a spinner icon (replaces play icon) +2. Status badge pulses with blue dot animation +3. On success: + - Status badge transitions to green "Ready" + - Brief green flash on the YAML preview header (200ms) + - If YAML preview is open, content updates with a subtle fade-in +4. On error: + - Status badge transitions to red "Error" + - Error banner slides down from below the header (200ms ease-out) + - Affected node(s) get a red border pulse + +### 6.4 Drag-and-Drop from Sidebar + +1. **Grab**: User mousedowns on a sidebar node card +2. **Drag**: A ghost card follows the cursor at 0.7 opacity, with a subtle shadow +3. **Over canvas**: Canvas shows a translucent placement preview (dashed border outline where the node will land) +4. **Drop**: Node materializes with a scale-up animation (`scale(0.9)` to `scale(1)` over 150ms) +5. **Auto-connect**: If dropped near an existing node's output handle, an edge auto-connects with a drawing animation + +### 6.5 Connection Drawing + +1. **Start**: User drags from a node's output handle (small circle on the edge) +2. **Drawing**: A bezier curve follows the cursor from the source handle +3. **Snap**: When cursor approaches another node's input handle, the line snaps to it with a subtle glow on the target handle +4. **Complete**: Line solidifies and the handle highlights briefly +5. **Cancel**: Releasing over empty canvas causes the line to retract back to source (150ms) + +### 6.6 Node Addition Animation + +When a new node is added (via sidebar drag or template): +- Node appears with `opacity: 0, scale: 0.9` and animates to `opacity: 1, scale: 1` over 200ms +- If auto-connecting, the edge draws in after the node appears (100ms delay) + +--- + +## 7. Component Specifications + +### 7.1 Selectable Card + +Used for: Event type selection, Engine selection + +``` ++---------------------------+ +| [Icon] Label | +| Description text | ++---------------------------+ +``` + +- **Dimensions**: Flex, min-width 140px, height 56px +- **Padding**: 12px +- **Border**: 1px solid `--borderColor-default` +- **Border-radius**: 8px +- **Background**: `--bgColor-default` +- **Icon**: 24x24px, left-aligned, `color: --fgColor-muted` +- **Label**: `font-size: 13px`, `font-weight: 600`, `color: --fgColor-default` +- **Description**: `font-size: 11px`, `color: --fgColor-muted` +- **Hover**: `background: --bgColor-subtle`, `border-color: --borderColor-emphasis` +- **Selected**: `border: 2px solid --fgColor-accent`, `background: --bgColor-accent-muted` +- **Focus**: `box-shadow: 0 0 0 3px --bgColor-accent-muted` + +### 7.2 Toggle Switch + +Used for: Tool toggles, boolean settings + +- **Track**: 34px x 20px, border-radius 10px +- **Knob**: 16px x 16px circle, white, `box-shadow: 0 1px 3px rgba(0,0,0,0.2)` +- **Off state**: Track background `--bgColor-neutral-muted` +- **On state**: Track background `--fgColor-accent` +- **Transition**: Knob `transform: translateX(14px)` over 180ms ease + +### 7.3 Segmented Control + +Used for: Permission levels (None/Read/Write) + +- **Container**: Inline flex, border-radius 6px, border 1px solid `--borderColor-default` +- **Segment**: Padding `4px 12px`, `font-size: 12px`, `font-weight: 500` +- **Selected segment**: Background fill with accent color, white text +- **Segment variants**: + - None: gray background when selected + - Read: green tint background when selected + - Write: amber tint background when selected +- **Transition**: Background color 150ms ease + +### 7.4 Tag Input + +Used for: Domains, labels, args, allowed functions + +``` ++------------------------------------------+ +| [github.com x] [pypi.org x] [type...] | ++------------------------------------------+ +``` + +- **Container**: Flex-wrap, min-height 36px, border 1px solid `--borderColor-default`, border-radius 6px, padding 4px +- **Tag**: Inline pill, `font-size: 12px`, background `--bgColor-muted`, border-radius 12px, padding `2px 8px` +- **Tag remove**: Small `x` button (14px), appears on hover +- **Input**: Borderless, `font-size: 13px`, grows to fill remaining width +- **Enter or comma**: Creates a new tag from current input +- **Autocomplete**: Dropdown appears below with matching suggestions when applicable + +### 7.5 Number Input with Slider + +Used for: Max turns, timeouts + +``` +[----o------------------] [10] +``` + +- **Slider track**: Full width minus 60px, height 4px, border-radius 2px, `--bgColor-muted` +- **Slider fill**: Height 4px, `--fgColor-accent` +- **Thumb**: 16px circle, white, `box-shadow: 0 1px 3px rgba(0,0,0,0.2)`, accent border +- **Number input**: 56px wide, border 1px solid `--borderColor-default`, border-radius 6px, `font-size: 13px`, text-align center + +### 7.6 Key-Value Editor + +Used for: Environment variables, secrets + +``` ++------------------------------------------+ +| Key | Value [x] | ++------------------------------------------+ +| API_KEY | ${{ secrets.KEY }} [x] | +| NODE_ENV | production [x] | ++------------------------------------------+ +| [+ Add variable] | ++------------------------------------------+ +``` + +- **Row height**: 36px +- **Key input**: 40% width, `font-family: monospace`, `font-size: 13px` +- **Value input**: 50% width, same styling +- **Delete button**: 10% width, `btn-octicon` with trash icon +- **Add button**: Full width, dashed border, `color: --fgColor-muted` + +### 7.7 Tooltip + +Used for: `[?]` help icons on every field + +- **Trigger**: Small `?` circle icon (16px), `color: --fgColor-muted`, `cursor: help` +- **Tooltip body**: Max-width 280px, `font-size: 12px`, `line-height: 1.5` +- **Background**: `--bgColor-emphasis` (dark in light mode, light in dark mode) +- **Text color**: `--fgColor-onEmphasis` +- **Border-radius**: 6px +- **Padding**: 8px 12px +- **Arrow**: 6px CSS triangle pointing toward the trigger +- **Show delay**: 200ms hover +- **Hide delay**: 100ms mouse leave + +### 7.8 Dropdown Select + +Used for: Model selection, log level, mode + +- **Trigger**: Button-style, `font-size: 13px`, border 1px solid `--borderColor-default`, border-radius 6px, padding `6px 12px` +- **Chevron**: Small down arrow on right +- **Dropdown**: Max-height 200px, overflow-y auto, `box-shadow: 0 4px 12px rgba(0,0,0,0.15)`, border-radius 8px +- **Item**: Padding `8px 12px`, `font-size: 13px` +- **Item hover**: `background: --bgColor-accent-muted` +- **Selected item**: Checkmark icon on left + +### 7.9 YAML Preview Panel + +``` ++------------------------------------------------------------------+ +| ===== drag handle ===== [Copy] [X] | ++------------------------------------------------------------------+ +| 1 | # Auto-generated by gh-aw visual editor | +| 2 | name: my-workflow | +| 3 | on: | +| 4 | issue_comment: | +| 5 | types: [created] | +| 6 | | ++------------------------------------------------------------------+ +``` + +- **Height**: 40% of canvas, resizable via drag handle +- **Background**: `--bgColor-subtle` +- **Font**: Monospace, `font-size: 13px`, `line-height: 1.6` +- **Line numbers**: 48px gutter, right-aligned, `color: --fgColor-muted` +- **Syntax highlighting**: YAML keywords in accent color, strings in green, comments in muted +- **Read-only**: No editing allowed (output only) +- **Copy button**: Top-right corner, copies full YAML to clipboard + +--- + +## 8. Onboarding Design + +### 8.1 Welcome Modal + +Shown on first visit (tracked in localStorage). + +``` ++----------------------------------------------------------+ +| | +| [Illustration: connected workflow nodes] | +| | +| Welcome to the Visual Workflow Editor | +| | +| Build AI-powered GitHub workflows visually. | +| No code required. | +| | +| Start from a template: | +| | +| +-------------------+ +-------------------+ | +| | [Chat] PR Review | | [Bug] Issue Triage| | +| | AI reviews pull | | Auto-label and | | +| | requests for... | | route issues... | | +| +-------------------+ +-------------------+ | +| +-------------------+ +-------------------+ | +| | [Shield] Security | | [Zap] Custom Bot | | +| | Scan code for | | Start with a | | +| | vulnerabilities...| | blank canvas... | | +| +-------------------+ +-------------------+ | +| | +| [Start from scratch] [Don't show again] | +| | ++----------------------------------------------------------+ +``` + +- **Overlay**: Semi-transparent backdrop `rgba(0,0,0,0.5)` +- **Modal**: 560px wide, border-radius 12px, `--bgColor-default` +- **Illustration**: Simple SVG showing connected nodes (120px tall) +- **Title**: `font-size: 24px`, `font-weight: 600` +- **Subtitle**: `font-size: 14px`, `color: --fgColor-muted` +- **Template cards**: 2x2 grid, same selectable card pattern (240px wide, 72px tall) +- **"Start from scratch"**: Primer `btn btn-primary` +- **"Don't show again"**: Text link, `font-size: 12px`, `color: --fgColor-muted` + +### 8.2 Template Definitions + +| Template | Trigger | Engine | Tools | Safe Outputs | Description | +|----------|---------|--------|-------|--------------|-------------| +| PR Review | `pull_request: opened, synchronize` | copilot | github, bash | add-comment, submit-review | Reviews PRs for quality | +| Issue Triage | `issues: opened, labeled` | copilot | github | add-labels, add-comment, assign-to-user | Auto-labels and routes issues | +| Security Scan | `push` + `schedule: daily` | claude | github, bash | create-issue, add-labels, create-code-scanning-alert | Scans for vulnerabilities | +| Custom Bot | `issue_comment` (slash_command) | copilot | github, bash | add-comment | Blank slash-command bot | + +Each template generates a complete node graph pre-populated with: +- Correct trigger configuration +- Appropriate engine selection +- Recommended tools enabled +- Safe outputs selected +- Network set to "defaults" +- Permissions auto-detected from tools/outputs +- Starter instructions in the Instructions node + +### 8.3 Guided Tour + +A 5-step tooltip tour that highlights key areas. Uses a spotlight effect (dims everything except the highlighted area). + +**Step 1: Sidebar** +``` ++-------------------+ +| Drag nodes from | +| here onto the | +| canvas to build | +| your workflow. | +| | +| [1/5] [Next >] | ++-------------------+ + \ + v (points to sidebar) +``` + +**Step 2: Canvas** +``` ++-------------------+ +| This is your | +| workflow canvas. | +| Nodes represent | +| each part of | +| your workflow. | +| | +| [2/5] [Next >] | ++-------------------+ + \ + v (points to canvas center) +``` + +**Step 3: Node connections** +``` ++-------------------+ +| Drag from one | +| node's handle to | +| another to | +| connect them. | +| | +| [3/5] [Next >] | ++-------------------+ + \ + v (points to an edge) +``` + +**Step 4: Properties panel** +``` ++-------------------+ +| Click any node | +| to configure it | +| here. Each node | +| type has its own | +| settings. | +| | +| [4/5] [Next >] | ++-------------------+ + \ + v (points to properties panel) +``` + +**Step 5: Compile** +``` ++-------------------+ +| When you're done, | +| hit Compile to | +| generate the | +| GitHub Actions | +| YAML file. | +| | +| [5/5] [Done!] | ++-------------------+ + \ + v (points to compile button) +``` + +**Tour tooltip styling**: +- Background: `--bgColor-emphasis` (dark) +- Text: `--fgColor-onEmphasis` (white) +- Border-radius: 8px +- Padding: 16px +- Width: 240px +- Arrow: CSS triangle pointing to highlighted element +- Progress: "N/5" counter +- "Next" button: Primer `btn btn-sm btn-primary` +- "Skip tour" link: `font-size: 12px`, underlined, `color: --fgColor-onEmphasis` +- Spotlight: Highlighted element gets `z-index: 1000` and the rest of the page is dimmed with a dark overlay + +### 8.4 Empty State Designs + +**Empty Canvas** (no nodes placed): +``` ++--------------------------------------------------+ +| | +| [Illustration: dashed rectangle | +| with arrow from sidebar] | +| | +| Drag a node from the sidebar | +| to start building your workflow | +| | +| or | +| | +| [Choose a template] | +| | ++--------------------------------------------------+ +``` + +**Empty Properties Panel** (no node selected): +``` ++--------------------------------------------+ +| | +| [Illustration: cursor clicking node] | +| | +| Select a node on the canvas | +| to edit its properties | +| | ++--------------------------------------------+ +``` + +**Empty state styling**: +- Illustration: Simple SVG line art, 80px tall, `color: --fgColor-muted` at 40% opacity +- Text: `font-size: 14px`, `color: --fgColor-muted`, text-align center +- CTA button: Primer `btn btn-sm btn-outline` + +--- + +## Appendix A: Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| `Ctrl/Cmd + Enter` | Compile workflow | +| `Ctrl/Cmd + S` | Save / Download .md | +| `Ctrl/Cmd + Z` | Undo | +| `Ctrl/Cmd + Shift + Z` | Redo | +| `Delete` / `Backspace` | Delete selected node | +| `Ctrl/Cmd + A` | Select all nodes | +| `Ctrl/Cmd + D` | Duplicate selected node | +| `Space + Drag` | Pan canvas | +| `Ctrl/Cmd + +/-` | Zoom in/out | +| `Ctrl/Cmd + 0` | Reset zoom to 100% | +| `Ctrl/Cmd + 1` | Fit all nodes to view | +| `Escape` | Deselect all / Close panel | +| `?` | Open keyboard shortcut help | + +## Appendix B: Responsive Behavior + +**Breakpoints**: +- Desktop: >= 1024px - full 3-panel layout +- Tablet: 768px - 1023px - sidebar collapses to icon rail, properties panel becomes overlay +- Mobile: < 768px - single panel view with bottom sheet navigation + +**Tablet mode**: +- Sidebar: 48px icon rail (always collapsed) +- Canvas: fills remaining space +- Properties: Slides in as overlay from right (320px), with backdrop dim + +**Mobile mode**: +- Canvas takes full screen +- Bottom tab bar replaces sidebar: [Palette] [Canvas] [Properties] [YAML] +- Each tab shows its panel as full-screen view +- Nodes are touch-draggable with long-press to initiate + +## Appendix C: Typography + +| Element | Font | Size | Weight | Color | +|---------|------|------|--------|-------| +| Header title | System | 14px | 600 | `--fgColor-default` | +| Node title | System | 13px | 600 | `--fgColor-default` | +| Node subtitle | System | 11px | 400 | `--fgColor-muted` | +| Node content | System | 12px | 400 | `--fgColor-default` | +| Panel section header | System | 12px | 600 | `--fgColor-muted` | +| Panel field label | System | 13px | 500 | `--fgColor-default` | +| Panel help text | System | 12px | 400 | `--fgColor-muted` | +| Code / YAML | Monospace | 13px | 400 | `--fgColor-default` | +| Badge / Label | System | 12px | 500 | varies | +| Tooltip | System | 12px | 400 | `--fgColor-onEmphasis` | + +Font stacks: +- **System**: `-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif` (Primer default) +- **Monospace**: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace` (Primer monospace) diff --git a/docs/editor-app/SECURITY-REVIEW.md b/docs/editor-app/SECURITY-REVIEW.md new file mode 100644 index 00000000000..b3fd62e52ce --- /dev/null +++ b/docs/editor-app/SECURITY-REVIEW.md @@ -0,0 +1,117 @@ +# Security Review — Visual Workflow Editor + +**Reviewer**: Security Review Agent +**Date**: 2026-02-19 +**Scope**: All source files in `docs/editor-app/src/` +**Overall Risk**: LOW + +--- + +## Summary + +The visual editor is a client-side-only React SPA with no backend, no authentication, and no network requests. All compilation happens via WASM in a Web Worker. The attack surface is minimal. No critical or high-severity issues found. + +--- + +## 1. XSS Risk Assessment + +**Status**: PASS + +- **No `dangerouslySetInnerHTML`** usage found anywhere in the codebase. +- All user inputs (workflow name, instructions, domain names, trigger config) are rendered as React text content, which is auto-escaped by React's JSX rendering. +- The Instructions node preview (`InstructionsNode.tsx:31`) renders text with `whiteSpace: pre-wrap` — safe, no HTML interpretation. +- YAML/Markdown preview panels use `prism-react-renderer`, which tokenizes and renders via `` elements — no raw HTML injection path. +- Template names and descriptions are hardcoded constants (`templates.ts`), not user-supplied. + +## 2. Input Sanitization + +**Status**: PASS with minor notes + +- **Domain inputs** (`NetworkPanel.tsx`): Only validates for empty/whitespace and duplicates. Does not validate domain format (e.g., rejects nothing like `">`). However, this is a **non-issue** because: + - The domain string is only used to generate markdown text output + - It's properly YAML-quoted via `yamlString()` in `markdownGenerator.ts:301-313` + - No network calls are ever made to these domains from the editor +- **YAML generation** (`markdownGenerator.ts`): The `yamlString()` function properly escapes special YAML characters (`:#{}[]|>!%@` etc.) and wraps in double quotes when needed. Backslashes and double quotes are escaped. This prevents YAML injection. +- **Workflow name**: Used as a filename in export (`Header.tsx:79`). The name is sanitized via Blob download (browser handles filename safety). No path traversal risk since it's a client-side download. + +## 3. WASM / Compiler Bridge Security + +**Status**: PASS + +- **Dynamic import path** (`compiler.ts:20-21`): Uses `/* @vite-ignore */` dynamic import with `wasmBasePath` parameter. This path is hardcoded as `'/gh-aw/wasm/'` in `App.tsx:33` — **not user-controllable**. +- **Worker communication**: The `compile()` function sends markdown strings to the Web Worker via `postMessage`. The response is typed via `CompileResult` interface. No arbitrary code execution path. +- **WASM isolation**: The Go WASM binary runs in a Web Worker, isolated from the main thread's DOM. Even if the WASM binary had a vulnerability, it cannot access the page's DOM or user data directly. + +## 4. LocalStorage Usage + +**Status**: PASS + +Two Zustand stores persist to `localStorage`: + +| Store Key | Data Stored | Sensitive? | +|-----------|-------------|------------| +| `workflow-editor-state` | Workflow config: name, trigger, engine, tools, instructions, network domains, permissions, safe outputs | No | +| `workflow-editor-ui` | UI preferences: theme, sidebar state, onboarding flag, auto-compile toggle, disclosure level | No | + +- **No secrets, tokens, API keys, or credentials** are stored. +- The `partialize` option in `workflowStore.ts:219-234` correctly excludes transient UI state (`selectedNodeId`, `compiledYaml`, `error`, etc.) from persistence. +- User-authored instruction text is stored, which could theoretically contain sensitive content, but this is user-controlled and expected behavior for a local editor. + +**Note**: No handling for `localStorage` being unavailable (e.g., private browsing mode). Zustand's `persist` middleware silently falls back, so this won't crash, but state won't persist. + +## 5. Dependency Audit + +**Status**: PASS + +``` +$ npm audit +found 0 vulnerabilities +``` + +All dependencies are well-known, actively maintained packages: +- React 19, Zustand 5, Vite 7 — current major versions +- `@xyflow/react` v12, `@primer/react` v38, `@radix-ui/*` — established UI libraries +- `prism-react-renderer` v2 — syntax highlighting (no eval/code execution) +- `framer-motion` v12, `sonner` v2, `lucide-react` — UI utilities +- `@dagrejs/dagre` v2 — graph layout algorithm (pure computation) + +No transitive vulnerabilities detected. + +## 6. CSP Compatibility + +**Status**: PASS with one note + +- **No inline scripts** in `index.html`. The entry point uses ` + + diff --git a/docs/editor-app/package-lock.json b/docs/editor-app/package-lock.json new file mode 100644 index 00000000000..fed75e7e26b --- /dev/null +++ b/docs/editor-app/package-lock.json @@ -0,0 +1,3470 @@ +{ + "name": "editor-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "editor-app", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@dagrejs/dagre": "^2.0.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@primer/octicons-react": "^19.22.0", + "@primer/react": "^38.12.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@xyflow/react": "^12.10.0", + "framer-motion": "^12.34.2", + "lucide-react": "^0.574.0", + "prism-react-renderer": "^2.4.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "sonner": "^2.0.7", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.4", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dagrejs/dagre": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-2.0.4.tgz", + "integrity": "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "3.0.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-3.0.4.tgz", + "integrity": "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==", + "license": "MIT" + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@github/mini-throttle": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@github/mini-throttle/-/mini-throttle-2.1.1.tgz", + "integrity": "sha512-KtOPaB+FiKJ6jcKm9UKyaM5fPURHGf+xcp+b4Mzoi81hOc6M1sIGpMZMAVbNzfa2lW5+RPGKq888Px0j76OZ/A==", + "license": "MIT" + }, + "node_modules/@github/relative-time-element": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.5.1.tgz", + "integrity": "sha512-uxCxCwe9vdwUDmRmM84tN0UERlj8MosLV44+r/VDj7DZUVUSTP4vyWlE9mRK6vHelOmT8DS3RMlaMrLlg1h1PQ==", + "license": "MIT" + }, + "node_modules/@github/tab-container-element": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@github/tab-container-element/-/tab-container-element-4.8.2.tgz", + "integrity": "sha512-WkaM4mfs8x7dXRWEaDb5deC0OhH6sGQ5cw8i/sVw25gikl4f8C7mHj0kihL5k3eKIIqmGT1Fdswdoi+9ZLDpRA==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lit-labs/react": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz", + "integrity": "sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@oddbird/popover-polyfill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@oddbird/popover-polyfill/-/popover-polyfill-0.5.2.tgz", + "integrity": "sha512-iFrvar5SOMtKFOSjYvs4z9UlLqDdJbMx0mgISLcPedv+g0ac5sgeETLGtipHCVIae6HJPclNEH5aCyD1RZaEHw==", + "license": "BSD-3-Clause" + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@primer/behaviors": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.10.2.tgz", + "integrity": "sha512-93juWZbWg2DRhC11+7RT7hMpY1VD3lBosLmccqEZ65yrCHqkBCjI8Uj8wxs3y0U+wWE07LAoLHAPylyWbifg5A==", + "license": "MIT" + }, + "node_modules/@primer/live-region-element": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@primer/live-region-element/-/live-region-element-0.7.2.tgz", + "integrity": "sha512-wdxCHfcJzE1IPPjZNFR4RTwRcSWb7TN0fRdMH5HcxphLEnuZBWy0TAxk3xPA+/6lwiN3uEJ+ZWV4UF/glXh43A==", + "license": "MIT", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, + "node_modules/@primer/octicons-react": { + "version": "19.22.0", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-19.22.0.tgz", + "integrity": "sha512-I6hQfmxbM6YbjwQ/VRaGAk+/aFHzqhWh0hThPkO/CyYKrNzEfy/sFAsoSwDH2hRDR7CCCdQaILEDQBT3gaV1bw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, + "node_modules/@primer/primitives": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-11.4.0.tgz", + "integrity": "sha512-JIt98Fs0c8vhOw3uNf+sxqmvCdo0VoCZPBRg4frNK/xNpDMZsQh7V0Rp7wiGbr3f1w+4oqv40sfgaftMQTnwXQ==", + "license": "MIT" + }, + "node_modules/@primer/react": { + "version": "38.12.0", + "resolved": "https://registry.npmjs.org/@primer/react/-/react-38.12.0.tgz", + "integrity": "sha512-NYGWkKF78bhmGYmhmuDLFP6YO0H5BavIByyKkT2qs+PKhga34qZGLSmKTtJkPFzEpuDhJuXS55tAr8vNzpZG/A==", + "license": "MIT", + "dependencies": { + "@github/mini-throttle": "^2.1.1", + "@github/relative-time-element": "^4.5.0", + "@github/tab-container-element": "^4.8.2", + "@lit-labs/react": "1.2.1", + "@oddbird/popover-polyfill": "^0.5.2", + "@primer/behaviors": "^1.10.2", + "@primer/live-region-element": "^0.7.1", + "@primer/octicons-react": "^19.21.0", + "@primer/primitives": "10.x || 11.x", + "clsx": "^2.1.1", + "color2k": "^2.0.3", + "deepmerge": "^4.3.1", + "focus-visible": "^5.2.1", + "history": "^5.0.0", + "hsluv": "1.0.1", + "lodash.isempty": "^4.4.0", + "lodash.isobject": "^3.0.2", + "react-compiler-runtime": "^1.0.0", + "react-intersection-observer": "^9.16.0" + }, + "engines": { + "node": ">=12", + "npm": ">=7" + }, + "peerDependencies": { + "@types/react": "18.x || 19.x", + "@types/react-dom": "18.x || 19.x", + "@types/react-is": "18.x || 19.x", + "react": "18.x || 19.x", + "react-dom": "18.x || 19.x", + "react-is": "18.x || 19.x" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "@types/react-is": { + "optional": true + } + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "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-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", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "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-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "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-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "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-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "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-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "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-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "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-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "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-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "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-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-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.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-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "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-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "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", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "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-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "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", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "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-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "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-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "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-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "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-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "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-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "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/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.0.tgz", + "integrity": "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.74", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/react/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.74", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.74.tgz", + "integrity": "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color2k": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", + "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/focus-visible": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/focus-visible/-/focus-visible-5.2.1.tgz", + "integrity": "sha512-8Bx950VD1bWTQJEH/AM6SpEk+SU55aVnp4Ujhuuxy3eMEBCRwBnTBnVXr9YAPvZL3/CNjCa8u4IWfNmEO53whA==", + "license": "W3C" + }, + "node_modules/framer-motion": { + "version": "12.34.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.2.tgz", + "integrity": "sha512-CcnYTzbRybm1/OE8QLXfXI8gR1cx5T4dF3D2kn5IyqsGNeLAKl2iFHb2BzFyXBGqESntDt6rPYl4Jhrb7tdB8g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.34.2", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, + "node_modules/hsluv": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hsluv/-/hsluv-1.0.1.tgz", + "integrity": "sha512-zCaFTiDqBLQjCCFBu0qg7z9ASYPd+Bxx2GDCVZJsnehjK80S+jByqhuFz0pCd2Aw3FSKr18AWbRlwnKR0YdizQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "license": "MIT" + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.574.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.574.0.tgz", + "integrity": "sha512-dJ8xb5juiZVIbdSn3HTyHsjjIwUwZ4FNwV0RtYDScOyySOeie1oXZTymST6YPJ4Qwt3Po8g4quhYl4OxtACiuQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/motion-dom": { + "version": "12.34.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.2.tgz", + "integrity": "sha512-n7gknp7gHcW7DUcmet0JVPLVHmE3j9uWwDp5VbE3IkCNnW5qdu0mOhjNYzXMkrQjrgr+h6Db3EDM2QBhW2qNxQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-compiler-runtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-1.0.0.tgz", + "integrity": "sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-intersection-observer": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", + "integrity": "sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/docs/editor-app/package.json b/docs/editor-app/package.json new file mode 100644 index 00000000000..5ebca13f401 --- /dev/null +++ b/docs/editor-app/package.json @@ -0,0 +1,45 @@ +{ + "name": "editor-app", + "version": "1.0.0", + "main": "index.js", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@dagrejs/dagre": "^2.0.4", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@primer/octicons-react": "^19.22.0", + "@primer/react": "^38.12.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@xyflow/react": "^12.10.0", + "framer-motion": "^12.34.2", + "lucide-react": "^0.574.0", + "prism-react-renderer": "^2.4.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "sonner": "^2.0.7", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.4", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } +} diff --git a/docs/editor-app/playwright.config.ts b/docs/editor-app/playwright.config.ts new file mode 100644 index 00000000000..8ce0f656ca4 --- /dev/null +++ b/docs/editor-app/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + timeout: 30_000, + expect: { timeout: 5_000 }, + fullyParallel: true, + retries: 0, + reporter: 'list', + use: { + baseURL: 'http://localhost:5173/gh-aw/editor/', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173/gh-aw/editor/', + reuseExistingServer: !process.env.CI, + timeout: 15_000, + }, +}); diff --git a/docs/editor-app/public/wasm b/docs/editor-app/public/wasm new file mode 120000 index 00000000000..9fbe466068a --- /dev/null +++ b/docs/editor-app/public/wasm @@ -0,0 +1 @@ +/home/mossaka/developer/gh-aw-repos/gh-aw-visual-editor/docs/public/wasm \ No newline at end of file diff --git a/docs/editor-app/src/App.tsx b/docs/editor-app/src/App.tsx new file mode 100644 index 00000000000..67e09172530 --- /dev/null +++ b/docs/editor-app/src/App.tsx @@ -0,0 +1,108 @@ +import { useEffect } from 'react'; +import { ThemeProvider, BaseStyles } from '@primer/react'; +import { ReactFlowProvider } from '@xyflow/react'; +import { Toaster } from 'sonner'; +import { Header } from './components/Header/Header'; +import { Sidebar } from './components/Sidebar/Sidebar'; +import { WorkflowGraph } from './components/Canvas/WorkflowGraph'; +import { WelcomeModal } from './components/Onboarding/WelcomeModal'; +import { PropertiesPanel } from './components/Panels/PropertiesPanel'; +import { YamlPreview } from './components/YamlPreview/YamlPreview'; +import { useUIStore } from './stores/uiStore'; +import { useWorkflowStore } from './stores/workflowStore'; +import { useAutoCompile } from './hooks/useAutoCompile'; +import { initCompiler } from './utils/compiler'; +import './styles/globals.css'; +import './styles/nodes.css'; +import './styles/panels.css'; + +export default function App() { + const { + sidebarOpen, + propertiesPanelOpen, + theme, + hasSeenOnboarding, + } = useUIStore(); + + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const setIsReady = useWorkflowStore((s) => s.setIsReady); + const setError = useWorkflowStore((s) => s.setError); + + // Initialize WASM compiler + useEffect(() => { + // WASM files are in public/wasm/ → served at BASE_URL/wasm/ + const wasmPath = `${import.meta.env.BASE_URL}wasm/`; + initCompiler(wasmPath) + .then(() => setIsReady(true)) + .catch((err) => { + console.error('Failed to initialize compiler:', err); + setError(`Compiler initialization failed: ${err instanceof Error ? err.message : String(err)}`); + }); + }, [setIsReady, setError]); + + // Enable auto-compilation + useAutoCompile(); + + // Resolve theme for attribute + useEffect(() => { + const resolveAndApply = (mode: typeof theme) => { + if (mode === 'auto') { + const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; + document.documentElement.setAttribute('data-color-mode', dark ? 'dark' : 'light'); + } else { + document.documentElement.setAttribute('data-color-mode', mode); + } + }; + + resolveAndApply(theme); + + if (theme === 'auto') { + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + const handler = () => resolveAndApply('auto'); + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + } + }, [theme]); + + const resolvedTheme = theme === 'auto' + ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + : theme; + + const showProperties = propertiesPanelOpen && selectedNodeId !== null; + + const layoutClasses = [ + 'app-layout', + sidebarOpen ? '' : 'sidebar-collapsed', + showProperties ? 'properties-open' : '', + ] + .filter(Boolean) + .join(' '); + + return ( + + +
+
+
+
+
+ {sidebarOpen && } +
+
+ + + +
+ {showProperties && ( +
+ +
+ )} +
+ + {!hasSeenOnboarding && } + +
+
+ ); +} diff --git a/docs/editor-app/src/components/Canvas/EmptyState.tsx b/docs/editor-app/src/components/Canvas/EmptyState.tsx new file mode 100644 index 00000000000..6c855aee8ce --- /dev/null +++ b/docs/editor-app/src/components/Canvas/EmptyState.tsx @@ -0,0 +1,13 @@ +import { Workflow } from 'lucide-react'; + +export function EmptyState() { + return ( +
+ +

Build your workflow

+

+ Get started by choosing a template from the sidebar, or add blocks to build your workflow step by step. +

+
+ ); +} diff --git a/docs/editor-app/src/components/Canvas/WorkflowGraph.tsx b/docs/editor-app/src/components/Canvas/WorkflowGraph.tsx new file mode 100644 index 00000000000..4d36ace35c9 --- /dev/null +++ b/docs/editor-app/src/components/Canvas/WorkflowGraph.tsx @@ -0,0 +1,240 @@ +import { useMemo, useCallback } from 'react'; +import { + ReactFlow, + MiniMap, + Controls, + Background, + BackgroundVariant, + useNodesState, + useEdgesState, + type NodeTypes, + type Node, + type Edge, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import dagre from '@dagrejs/dagre'; + +import { TriggerNode } from '../Nodes/TriggerNode'; +import { PermissionsNode } from '../Nodes/PermissionsNode'; +import { EngineNode } from '../Nodes/EngineNode'; +import { ToolsNode } from '../Nodes/ToolsNode'; +import { InstructionsNode } from '../Nodes/InstructionsNode'; +import { SafeOutputsNode } from '../Nodes/SafeOutputsNode'; +import { NetworkNode } from '../Nodes/NetworkNode'; +import { StepsNode } from '../Nodes/StepsNode'; +import { EmptyState } from './EmptyState'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; +import '../../styles/nodes.css'; + +const nodeTypes: NodeTypes = { + trigger: TriggerNode, + permissions: PermissionsNode, + engine: EngineNode, + tools: ToolsNode, + instructions: InstructionsNode, + safeOutputs: SafeOutputsNode, + network: NetworkNode, + steps: StepsNode, +}; + +const NODE_WIDTH = 240; +const NODE_HEIGHT = 120; + +interface NodeDef { + id: string; + type: string; + label: string; + description: string; + isRequired: boolean; + isConfigured: (state: ReturnType) => boolean; +} + +const ALL_NODES: NodeDef[] = [ + { + id: 'trigger', + type: 'trigger', + label: 'When this happens', + description: 'Choose what starts this workflow', + isRequired: true, + isConfigured: (s) => s.trigger.event !== '', + }, + { + id: 'permissions', + type: 'permissions', + label: 'Permissions', + description: 'Set access permissions', + isRequired: false, + isConfigured: (s) => Object.keys(s.permissions).length > 0, + }, + { + id: 'engine', + type: 'engine', + label: 'AI Assistant', + description: 'Choose which AI runs this', + isRequired: true, + isConfigured: (s) => s.engine.type !== '', + }, + { + id: 'tools', + type: 'tools', + label: 'Agent Tools', + description: 'What tools the agent can use', + isRequired: false, + isConfigured: (s) => s.tools.length > 0, + }, + { + id: 'instructions', + type: 'instructions', + label: 'Instructions', + description: 'Tell the agent what to do', + isRequired: true, + isConfigured: (s) => s.instructions.trim().length > 0, + }, + { + id: 'safeOutputs', + type: 'safeOutputs', + label: 'What it can do', + description: 'Actions the agent can take', + isRequired: false, + isConfigured: (s) => Object.keys(s.safeOutputs).length > 0, + }, + { + id: 'network', + type: 'network', + label: 'Network Access', + description: 'Allowed network domains', + isRequired: false, + isConfigured: (s) => + s.network.allowed.length > 0 || s.network.blocked.length > 0, + }, + { + id: 'steps', + type: 'steps', + label: 'Custom Steps', + description: 'Additional workflow steps', + isRequired: false, + isConfigured: () => false, + }, +]; + +function getLayoutedElements(nodes: Node[], edges: Edge[]) { + const g = new dagre.graphlib.Graph(); + g.setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: 'TB', nodesep: 40, ranksep: 60 }); + + nodes.forEach((node) => { + g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); + }); + + edges.forEach((edge) => { + g.setEdge(edge.source, edge.target); + }); + + dagre.layout(g); + + const layoutedNodes = nodes.map((node) => { + const pos = g.node(node.id); + return { + ...node, + position: { + x: pos.x - NODE_WIDTH / 2, + y: pos.y - NODE_HEIGHT / 2, + }, + }; + }); + + return { nodes: layoutedNodes, edges }; +} + +export function WorkflowGraph() { + const state = useWorkflowStore(); + const selectNode = useWorkflowStore((s) => s.selectNode); + + // Determine which nodes to show: required nodes + configured optional nodes + const visibleNodeDefs = useMemo(() => { + return ALL_NODES.filter((def) => def.isRequired || def.isConfigured(state)); + }, [state]); + + // Build React Flow nodes + const rawNodes: Node[] = useMemo(() => { + return visibleNodeDefs.map((def) => ({ + id: def.id, + type: def.type, + position: { x: 0, y: 0 }, + data: { + label: def.label, + type: def.type as WorkflowNodeData['type'], + description: def.description, + isConfigured: def.isConfigured(state), + }, + })); + }, [visibleNodeDefs, state]); + + // Build edges connecting sequential nodes + const rawEdges: Edge[] = useMemo(() => { + return visibleNodeDefs.slice(1).map((def, i) => ({ + id: `e-${visibleNodeDefs[i].id}-${def.id}`, + source: visibleNodeDefs[i].id, + target: def.id, + animated: true, + style: { stroke: 'var(--borderColor-default, #d1d9e0)' }, + })); + }, [visibleNodeDefs]); + + // Apply dagre layout + const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo( + () => getLayoutedElements(rawNodes, rawEdges), + [rawNodes, rawEdges] + ); + + const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes); + const [edges, , onEdgesChange] = useEdgesState(layoutedEdges); + + // Sync layout when nodes change + useMemo(() => { + setNodes(layoutedNodes); + }, [layoutedNodes, setNodes]); + + const onNodeClick = useCallback( + (_: React.MouseEvent, node: Node) => { + selectNode(node.id); + }, + [selectNode] + ); + + const onPaneClick = useCallback(() => { + selectNode(null); + }, [selectNode]); + + if (visibleNodeDefs.length === 0) { + return ; + } + + return ( + + + + + + ); +} diff --git a/docs/editor-app/src/components/Header/CompilationStatus.tsx b/docs/editor-app/src/components/Header/CompilationStatus.tsx new file mode 100644 index 00000000000..5e69d12d425 --- /dev/null +++ b/docs/editor-app/src/components/Header/CompilationStatus.tsx @@ -0,0 +1,18 @@ +import { useWorkflowStore } from '../../stores/workflowStore'; + +export function CompilationStatus() { + const isCompiling = useWorkflowStore((s) => s.isCompiling); + const error = useWorkflowStore((s) => s.error); + const warnings = useWorkflowStore((s) => s.warnings); + + if (error) { + return Error; + } + if (isCompiling) { + return Compiling...; + } + if (warnings.length > 0) { + return {warnings.length} warning(s); + } + return Ready; +} diff --git a/docs/editor-app/src/components/Header/ExportMenu.tsx b/docs/editor-app/src/components/Header/ExportMenu.tsx new file mode 100644 index 00000000000..e59b95776df --- /dev/null +++ b/docs/editor-app/src/components/Header/ExportMenu.tsx @@ -0,0 +1,4 @@ +export function ExportMenu() { + // TODO: Implement export menu with download .md, .yml, copy to clipboard + return null; +} diff --git a/docs/editor-app/src/components/Header/Header.tsx b/docs/editor-app/src/components/Header/Header.tsx new file mode 100644 index 00000000000..c540cd02368 --- /dev/null +++ b/docs/editor-app/src/components/Header/Header.tsx @@ -0,0 +1,267 @@ +import { useState, useRef, useEffect } from 'react'; +import { + Sun, + Moon, + Play, + Download, + Copy, + ChevronDown, + Loader2, + CircleCheck, + CircleAlert, + PanelLeftClose, + PanelLeft, +} from 'lucide-react'; +import { toast } from 'sonner'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import { useUIStore } from '../../stores/uiStore'; + +export function Header() { + const name = useWorkflowStore((s) => s.name); + const setName = useWorkflowStore((s) => s.setName); + const isCompiling = useWorkflowStore((s) => s.isCompiling); + const error = useWorkflowStore((s) => s.error); + const compiledYaml = useWorkflowStore((s) => s.compiledYaml); + const compiledMarkdown = useWorkflowStore((s) => s.compiledMarkdown); + + const { + autoCompile, + setAutoCompile, + theme, + setTheme, + sidebarOpen, + toggleSidebar, + } = useUIStore(); + + const [exportOpen, setExportOpen] = useState(false); + const exportRef = useRef(null); + + // Close export menu on outside click + useEffect(() => { + if (!exportOpen) return; + const handler = (e: MouseEvent) => { + if (exportRef.current && !exportRef.current.contains(e.target as Node)) { + setExportOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [exportOpen]); + + const statusIcon = isCompiling ? ( + + ) : error ? ( + + ) : ( + + ); + + const statusColor = isCompiling + ? '#0969da' + : error + ? '#cf222e' + : '#1a7f37'; + const statusText = isCompiling ? 'Compiling...' : error ? 'Error' : 'Ready'; + + const resolvedTheme = theme === 'auto' + ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + : theme; + + const handleExport = (type: 'md' | 'yml' | 'clipboard') => { + setExportOpen(false); + if (type === 'clipboard') { + navigator.clipboard.writeText(compiledYaml || '').then(() => { + toast.success('YAML copied to clipboard'); + }); + return; + } + const content = type === 'md' ? compiledMarkdown : compiledYaml; + const filename = `${name || 'workflow'}.${type === 'md' ? 'md' : 'lock.yml'}`; + const blob = new Blob([content || ''], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + toast.success(`Downloaded ${filename}`); + }; + + return ( +
+ {/* Sidebar toggle */} + + + {/* Workflow name */} + setName(e.target.value)} + placeholder="workflow-name" + style={{ + border: '1px solid transparent', + borderRadius: 6, + padding: '4px 8px', + fontSize: 14, + fontWeight: 600, + background: 'transparent', + color: 'var(--fgColor-default, #1f2328)', + width: 200, + outline: 'none', + }} + onFocus={(e) => + (e.target.style.borderColor = 'var(--borderColor-default, #d1d9e0)') + } + onBlur={(e) => (e.target.style.borderColor = 'transparent')} + /> + + {/* Status badge */} +
+ {statusIcon} + {statusText} +
+ +
+ + {/* Auto-compile toggle */} + + + {/* Compile button */} + + + {/* Export dropdown */} +
+ + {exportOpen && ( +
+ + + +
+ )} +
+ + {/* Theme toggle */} + +
+ ); +} + +const iconButtonStyle: React.CSSProperties = { + background: 'none', + border: 'none', + cursor: 'pointer', + display: 'flex', + padding: 4, + color: 'var(--fgColor-muted, #656d76)', + borderRadius: 6, +}; + +const actionButtonStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: 4, + padding: '4px 12px', + fontSize: 13, + fontWeight: 500, + border: '1px solid var(--borderColor-default, #d1d9e0)', + borderRadius: 6, + background: 'var(--bgColor-default, #ffffff)', + color: 'var(--fgColor-default, #1f2328)', + cursor: 'pointer', +}; + +const menuItemStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: 8, + width: '100%', + padding: '8px 12px', + fontSize: 13, + border: 'none', + background: 'none', + color: 'var(--fgColor-default, #1f2328)', + cursor: 'pointer', + textAlign: 'left' as const, +}; diff --git a/docs/editor-app/src/components/Nodes/BaseNode.tsx b/docs/editor-app/src/components/Nodes/BaseNode.tsx new file mode 100644 index 00000000000..bf686e536e2 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/BaseNode.tsx @@ -0,0 +1,45 @@ +import { memo, type ReactNode } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import '../../styles/nodes.css'; + +interface BaseNodeProps { + type: string; + icon: ReactNode; + title: string; + selected?: boolean; + dimmed?: boolean; + children: ReactNode; +} + +export const BaseNode = memo(function BaseNode({ + type, + icon, + title, + selected = false, + dimmed = false, + children, +}: BaseNodeProps) { + const classes = [ + 'workflow-node', + `node-${type}`, + selected ? 'selected' : '', + dimmed ? 'dimmed' : '', + ] + .filter(Boolean) + .join(' '); + + return ( + <> + +
+
+
{icon}
+
{title}
+
+
+
{children}
+
+ + + ); +}); diff --git a/docs/editor-app/src/components/Nodes/EngineNode.tsx b/docs/editor-app/src/components/Nodes/EngineNode.tsx new file mode 100644 index 00000000000..a371cb10246 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/EngineNode.tsx @@ -0,0 +1,44 @@ +import { memo } from 'react'; +import { Bot } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; + +const ENGINE_INFO: Record = { + claude: { name: 'Claude', tagline: 'Best for complex reasoning' }, + copilot: { name: 'GitHub Copilot', tagline: 'Great for GitHub integration' }, + codex: { name: 'OpenAI Codex', tagline: 'Good for code editing tasks' }, + custom: { name: 'Custom', tagline: 'Bring your own engine' }, +}; + +interface EngineNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const EngineNode = memo(function EngineNode({ data, selected }: EngineNodeProps) { + const engine = useWorkflowStore((s) => s.engine); + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + const info = engine.type ? ENGINE_INFO[engine.type] : null; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + {info ? ( + <> +
{info.name}
+
{info.tagline}
+ + ) : ( + Click to choose an AI + )} +
+ ); +}); diff --git a/docs/editor-app/src/components/Nodes/InstructionsNode.tsx b/docs/editor-app/src/components/Nodes/InstructionsNode.tsx new file mode 100644 index 00000000000..f6ec3a94592 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/InstructionsNode.tsx @@ -0,0 +1,41 @@ +import { memo } from 'react'; +import { FileText } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; + +interface InstructionsNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const InstructionsNode = memo(function InstructionsNode({ data, selected }: InstructionsNodeProps) { + const instructions = useWorkflowStore((s) => s.instructions); + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + const lines = instructions.split('\n').filter((l) => l.trim()); + const preview = lines.slice(0, 3).join('\n'); + const truncated = lines.length > 3; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + {instructions ? ( + <> +
+ {preview} + {truncated && '...'} +
+ + ) : ( + Click to write instructions + )} +
+ ); +}); diff --git a/docs/editor-app/src/components/Nodes/NetworkNode.tsx b/docs/editor-app/src/components/Nodes/NetworkNode.tsx new file mode 100644 index 00000000000..63cbb94a93e --- /dev/null +++ b/docs/editor-app/src/components/Nodes/NetworkNode.tsx @@ -0,0 +1,43 @@ +import { memo } from 'react'; +import { Globe } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; + +interface NetworkNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const NetworkNode = memo(function NetworkNode({ data, selected }: NetworkNodeProps) { + const network = useWorkflowStore((s) => s.network); + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + const allowedCount = network.allowed.length; + const blockedCount = network.blocked.length; + const total = allowedCount + blockedCount; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + {total > 0 ? ( + <> + {allowedCount > 0 && ( +
{allowedCount} allowed domain{allowedCount !== 1 ? 's' : ''}
+ )} + {blockedCount > 0 && ( +
{blockedCount} blocked domain{blockedCount !== 1 ? 's' : ''}
+ )} + + ) : ( + Click to configure network + )} +
+ ); +}); diff --git a/docs/editor-app/src/components/Nodes/PermissionsNode.tsx b/docs/editor-app/src/components/Nodes/PermissionsNode.tsx new file mode 100644 index 00000000000..34a9f9f9e25 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/PermissionsNode.tsx @@ -0,0 +1,44 @@ +import { memo } from 'react'; +import { Shield } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; + +interface PermissionsNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const PermissionsNode = memo(function PermissionsNode({ data, selected }: PermissionsNodeProps) { + const permissions = useWorkflowStore((s) => s.permissions); + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + const entries = Object.entries(permissions).filter(([, v]) => v && v !== 'none'); + const count = entries.length; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + {count > 0 ? ( + <> + {entries.slice(0, 3).map(([scope, level]) => ( +
+ {scope}: {level} +
+ ))} +
+ {count} scope{count !== 1 ? 's' : ''} configured +
+ + ) : ( + Click to set permissions + )} +
+ ); +}); diff --git a/docs/editor-app/src/components/Nodes/SafeOutputsNode.tsx b/docs/editor-app/src/components/Nodes/SafeOutputsNode.tsx new file mode 100644 index 00000000000..7bfc112d667 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/SafeOutputsNode.tsx @@ -0,0 +1,65 @@ +import { memo } from 'react'; +import { Send, Check } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; + +const OUTPUT_LABELS: Record = { + 'add-comment': 'Post comments', + 'create-issue': 'Create issues', + 'update-issue': 'Edit issues', + 'create-pull-request': 'Create pull requests', + 'submit-pull-request-review': 'Submit PR reviews', + 'add-labels': 'Add labels', + 'remove-labels': 'Remove labels', + 'add-reviewer': 'Request reviewers', + 'assign-to-user': 'Assign to person', + 'close-issue': 'Close issues', + 'close-pull-request': 'Close pull requests', + 'create-discussion': 'Create discussions', + 'update-release': 'Edit releases', + 'push-to-pull-request-branch': 'Push code to PR', + 'create-pull-request-review-comment': 'Review code', + 'dispatch-workflow': 'Trigger workflows', + 'upload-asset': 'Upload files', +}; + +interface SafeOutputsNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const SafeOutputsNode = memo(function SafeOutputsNode({ data, selected }: SafeOutputsNodeProps) { + const safeOutputs = useWorkflowStore((s) => s.safeOutputs); + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + const enabledOutputs = Object.entries(safeOutputs).filter(([, v]) => v?.enabled); + const count = enabledOutputs.length; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + {count > 0 ? ( + <> + {enabledOutputs.slice(0, 4).map(([key]) => ( +
+ + {OUTPUT_LABELS[key] || key} +
+ ))} +
+ {count} action{count !== 1 ? 's' : ''} enabled +
+ + ) : ( + Click to enable outputs + )} +
+ ); +}); diff --git a/docs/editor-app/src/components/Nodes/StepsNode.tsx b/docs/editor-app/src/components/Nodes/StepsNode.tsx new file mode 100644 index 00000000000..34c05681750 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/StepsNode.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react'; +import { List } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import type { WorkflowNodeData } from '../../types/nodes'; +import { useWorkflowStore } from '../../stores/workflowStore'; + +interface StepsNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const StepsNode = memo(function StepsNode({ data, selected }: StepsNodeProps) { + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + Click to add custom steps + + ); +}); diff --git a/docs/editor-app/src/components/Nodes/ToolsNode.tsx b/docs/editor-app/src/components/Nodes/ToolsNode.tsx new file mode 100644 index 00000000000..318ccb01a28 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/ToolsNode.tsx @@ -0,0 +1,43 @@ +import { memo } from 'react'; +import { Wrench } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; + +interface ToolsNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const ToolsNode = memo(function ToolsNode({ data, selected }: ToolsNodeProps) { + const tools = useWorkflowStore((s) => s.tools); + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + {tools.length > 0 ? ( + <> +
+ {tools.map((tool) => ( + + {tool} + + ))} +
+
+ {tools.length} tool{tools.length !== 1 ? 's' : ''} enabled +
+ + ) : ( + Click to add tools + )} +
+ ); +}); diff --git a/docs/editor-app/src/components/Nodes/TriggerNode.tsx b/docs/editor-app/src/components/Nodes/TriggerNode.tsx new file mode 100644 index 00000000000..147c46b0803 --- /dev/null +++ b/docs/editor-app/src/components/Nodes/TriggerNode.tsx @@ -0,0 +1,56 @@ +import { memo } from 'react'; +import { Bell } from 'lucide-react'; +import { BaseNode } from './BaseNode'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import type { WorkflowNodeData } from '../../types/nodes'; + +const EVENT_LABELS: Record = { + issues: 'Issues', + pull_request: 'Pull Request', + issue_comment: 'Comment', + push: 'Code Push', + schedule: 'Schedule', + workflow_dispatch: 'Manual Trigger', + slash_command: 'Slash Command', + release: 'Release', + discussion: 'Discussion', + pull_request_review: 'PR Review', + pull_request_review_comment: 'PR Review Comment', + discussion_comment: 'Discussion Comment', +}; + +interface TriggerNodeProps { + data: WorkflowNodeData; + selected: boolean; +} + +export const TriggerNode = memo(function TriggerNode({ data, selected }: TriggerNodeProps) { + const trigger = useWorkflowStore((s) => s.trigger); + const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId); + const dimmed = selectedNodeId !== null && !selected; + + const eventLabel = trigger.event ? EVENT_LABELS[trigger.event] || trigger.event : ''; + const activityText = trigger.activityTypes.length > 0 + ? trigger.activityTypes.join(', ') + : ''; + + return ( + } + title={data.label} + selected={selected} + dimmed={dimmed} + > + {trigger.event ? ( + <> +
{eventLabel}
+ {activityText &&
{activityText}
} + {trigger.skipBots &&
Skip bots: Yes
} + + ) : ( + Click to choose a trigger + )} +
+ ); +}); diff --git a/docs/editor-app/src/components/Onboarding/GuidedTour.tsx b/docs/editor-app/src/components/Onboarding/GuidedTour.tsx new file mode 100644 index 00000000000..6d996add464 --- /dev/null +++ b/docs/editor-app/src/components/Onboarding/GuidedTour.tsx @@ -0,0 +1,4 @@ +export function GuidedTour() { + // TODO: Implement step-by-step guided tour with tooltips + return null; +} diff --git a/docs/editor-app/src/components/Onboarding/WelcomeModal.tsx b/docs/editor-app/src/components/Onboarding/WelcomeModal.tsx new file mode 100644 index 00000000000..0feda42db58 --- /dev/null +++ b/docs/editor-app/src/components/Onboarding/WelcomeModal.tsx @@ -0,0 +1,154 @@ +import * as Dialog from '@radix-ui/react-dialog'; +import { Rocket, LayoutTemplate, X } from 'lucide-react'; +import { useUIStore } from '../../stores/uiStore'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import { templates } from '../../utils/templates'; +import { toast } from 'sonner'; + +export function WelcomeModal() { + const setHasSeenOnboarding = useUIStore((s) => s.setHasSeenOnboarding); + const loadTemplate = useWorkflowStore((s) => s.loadTemplate); + + const handleStartScratch = () => { + setHasSeenOnboarding(true); + }; + + const handleBrowseTemplates = () => { + const first = templates.find((t) => t.id !== 'blank-canvas'); + if (first) { + loadTemplate(first); + toast.success(`Loaded "${first.name}" template`); + } + setHasSeenOnboarding(true); + }; + + return ( + !open && setHasSeenOnboarding(true)}> + + + + + + + + + Welcome to the Workflow Builder! + + + + Create AI-powered GitHub workflows visually — no coding required. + + +
+ } + title="Start from scratch" + description="Build your workflow step by step" + onClick={handleStartScratch} + /> + } + title="Browse templates" + description="Start with a pre-built workflow and customize" + onClick={handleBrowseTemplates} + /> +
+
+
+
+ ); +} + +function OptionCard({ + icon, + title, + description, + onClick, +}: { + icon: React.ReactNode; + title: string; + description: string; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/docs/editor-app/src/components/Panels/EnginePanel.tsx b/docs/editor-app/src/components/Panels/EnginePanel.tsx new file mode 100644 index 00000000000..f5da5a83a1c --- /dev/null +++ b/docs/editor-app/src/components/Panels/EnginePanel.tsx @@ -0,0 +1,153 @@ +import { useWorkflowStore } from '../../stores/workflowStore'; +import { PanelContainer } from './PanelContainer'; +import { getFieldDescription } from '../../utils/fieldDescriptions'; +import type { EngineType } from '../../types/workflow'; + +interface EngineOption { + type: EngineType; + fieldKey: string; + color: string; + bgColor: string; +} + +const engineOptions: EngineOption[] = [ + { type: 'copilot', fieldKey: 'engine.copilot', color: '#0969da', bgColor: '#ddf4ff' }, + { type: 'claude', fieldKey: 'engine.claude', color: '#8250df', bgColor: '#fbefff' }, + { type: 'codex', fieldKey: 'engine.codex', color: '#1a7f37', bgColor: '#dafbe1' }, + { type: 'custom', fieldKey: 'engine.copilot', color: '#656d76', bgColor: '#f6f8fa' }, +]; + +export function EnginePanel() { + const engine = useWorkflowStore((s) => s.engine); + const setEngine = useWorkflowStore((s) => s.setEngine); + const desc = getFieldDescription('engine'); + + return ( + + {/* Engine selector */} +
+
AI Engine
+
+ {engineOptions.map((opt) => { + const fd = opt.type === 'custom' + ? { label: 'Custom Engine', description: 'Use a custom or self-hosted AI engine.' } + : getFieldDescription(opt.fieldKey); + const active = engine.type === opt.type; + return ( + + ); + })} +
+
+ + {/* Model input */} + {engine.type && ( +
+
+ {getFieldDescription('engine.model').label} +
+ setEngine({ model: e.target.value })} + placeholder={getModelPlaceholder(engine.type)} + style={inputStyle} + /> +
+ {getFieldDescription('engine.model').description} Leave blank for the default. +
+
+ )} + + {/* Max turns */} + {engine.type && ( +
+
+ {getFieldDescription('engine.maxTurns').label} +
+
+ setEngine({ maxTurns: parseInt(e.target.value, 10) })} + style={{ flex: 1 }} + /> + { + const v = e.target.value; + setEngine({ maxTurns: v ? parseInt(v, 10) : '' }); + }} + placeholder="10" + style={{ ...inputStyle, width: '60px', textAlign: 'center' }} + /> +
+
+ {getFieldDescription('engine.maxTurns').description} +
+
+ )} +
+ ); +} + +function getModelPlaceholder(type: EngineType): string { + switch (type) { + case 'claude': return 'e.g. claude-sonnet-4-20250514'; + case 'copilot': return 'e.g. gpt-4o'; + case 'codex': return 'e.g. codex-mini'; + case 'custom': return 'Enter model identifier'; + } +} + +const engineCardStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'flex-start', + gap: '10px', + padding: '12px', + border: '1px solid #d0d7de', + borderRadius: '8px', + cursor: 'pointer', + textAlign: 'left', + background: '#ffffff', + transition: 'border-color 150ms ease, background 150ms ease', +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '6px 10px', + fontSize: '13px', + border: '1px solid #d0d7de', + borderRadius: '6px', + outline: 'none', +}; diff --git a/docs/editor-app/src/components/Panels/InstructionsPanel.tsx b/docs/editor-app/src/components/Panels/InstructionsPanel.tsx new file mode 100644 index 00000000000..9b0113b81f6 --- /dev/null +++ b/docs/editor-app/src/components/Panels/InstructionsPanel.tsx @@ -0,0 +1,82 @@ +import { useWorkflowStore } from '../../stores/workflowStore'; +import { PanelContainer } from './PanelContainer'; +import { getFieldDescription } from '../../utils/fieldDescriptions'; + +const snippets = [ + { label: 'Be concise', text: 'Keep your responses brief and to the point.' }, + { label: 'Review code', text: 'Review the code changes for bugs, security issues, and best practices.' }, + { label: 'Create issue', text: 'Create a new issue summarizing your findings.' }, + { label: 'Add comment', text: 'Add a comment on the pull request with your analysis.' }, + { label: 'Check tests', text: 'Run the test suite and report any failures.' }, +]; + +export function InstructionsPanel() { + const instructions = useWorkflowStore((s) => s.instructions); + const setInstructions = useWorkflowStore((s) => s.setInstructions); + const desc = getFieldDescription('instructions'); + + return ( + +
+ -
-
- -
- - -
-
- - - - - Compiled Output (.lock.yml) - -
-
-
- Compiled YAML will appear here -
-

-      
-
- - - - - - - + +
diff --git a/docs/public/editor/wasm/compiler-loader.js b/docs/public/editor/wasm/compiler-loader.js new file mode 100644 index 00000000000..7117fa06e5f --- /dev/null +++ b/docs/public/editor/wasm/compiler-loader.js @@ -0,0 +1,196 @@ +/** + * compiler-loader.js -- ES module that spawns compiler-worker.js and + * provides a clean async API for the Astro docs site. + * + * Usage: + * import { createWorkerCompiler } from '/wasm/compiler-loader.js'; + * + * const compiler = createWorkerCompiler(); + * await compiler.ready; + * const { yaml, warnings, error } = await compiler.compile(markdownString); + * const { errors, warnings } = await compiler.validate(markdownString); + * // With imports: + * const { yaml } = await compiler.compile(markdown, { 'shared/tools.md': '...' }); + * compiler.terminate(); + */ + +/** + * Create a worker-backed compiler instance. + * + * @param {Object} [options] + * @param {string} [options.workerUrl] - URL to compiler-worker.js + * (default: resolves relative to this module) + * @returns {{ compile: (markdown: string, files?: Record) => Promise<{yaml: string, warnings: string[], error: string|null}>, + * ready: Promise, + * terminate: () => void }} + */ +export function createWorkerCompiler(options = {}) { + const moduleDir = new URL('.', import.meta.url).href; + const workerUrl = options.workerUrl || new URL('compiler-worker.js', moduleDir).href; + + const worker = new Worker(workerUrl); + + // Monotonically increasing message ID for correlating requests/responses. + let nextId = 1; + + // Map of pending compile requests: id -> { resolve, reject } + const pending = new Map(); + + // Ready promise -- resolves when the worker sends { type: 'ready' }. + let readyResolve; + let readyReject; + const ready = new Promise((resolve, reject) => { + readyResolve = resolve; + readyReject = reject; + }); + + let isReady = false; + let isTerminated = false; + + /** + * Handle messages from the worker. + */ + worker.onmessage = function (event) { + const msg = event.data; + + switch (msg.type) { + case 'ready': + isReady = true; + readyResolve(); + break; + + case 'result': { + const entry = pending.get(msg.id); + if (entry) { + pending.delete(msg.id); + entry.resolve({ + yaml: msg.yaml, + warnings: msg.warnings || [], + error: msg.error || null, + }); + } + break; + } + + case 'validate_result': { + const entry = pending.get(msg.id); + if (entry) { + pending.delete(msg.id); + entry.resolve({ + errors: msg.errors || [], + warnings: msg.warnings || [], + }); + } + break; + } + + case 'error': { + // An error with id === null means init failure. + if (msg.id === null || msg.id === undefined) { + readyReject(new Error(msg.error)); + break; + } + + const entry = pending.get(msg.id); + if (entry) { + pending.delete(msg.id); + // Wrap raw error strings into a structured CompilerError object + entry.resolve({ + yaml: '', + warnings: [], + error: { message: msg.error, severity: 'error' }, + }); + } + break; + } + } + }; + + /** + * Handle worker-level errors (e.g. script load failure). + */ + worker.onerror = function (event) { + const errorMsg = event.message || 'Unknown worker error'; + + if (!isReady) { + readyReject(new Error(errorMsg)); + } + + // Reject all pending requests. + for (const [id, entry] of pending) { + entry.reject(new Error(errorMsg)); + } + pending.clear(); + }; + + /** + * Compile a markdown workflow string to GitHub Actions YAML. + * + * @param {string} markdown + * @param {Record} [files] - Optional map of file paths to content + * for import resolution (e.g. {"shared/tools.md": "---\ntools:..."}) + * @returns {Promise<{yaml: string, warnings: string[], error: string|null}>} + */ + function compile(markdown, files) { + if (isTerminated) { + return Promise.reject(new Error('Compiler worker has been terminated.')); + } + + const id = nextId++; + + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + const msg = { type: 'compile', id, markdown }; + if (files && Object.keys(files).length > 0) { + msg.files = files; + } + worker.postMessage(msg); + }); + } + + /** + * Validate a markdown workflow string with full schema validation enabled. + * + * @param {string} markdown + * @param {Record} [files] - Optional map of file paths to content + * @returns {Promise<{errors: Array<{field: string, message: string, severity: string}>, warnings: string[]}>} + */ + function validate(markdown, files) { + if (isTerminated) { + return Promise.reject(new Error('Compiler worker has been terminated.')); + } + + const id = nextId++; + + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + const msg = { type: 'validate', id, markdown }; + if (files && Object.keys(files).length > 0) { + msg.files = files; + } + worker.postMessage(msg); + }); + } + + /** + * Terminate the worker. After this, compile() will reject. + */ + function terminate() { + if (isTerminated) return; + isTerminated = true; + worker.terminate(); + + // Reject anything still pending. + for (const [id, entry] of pending) { + entry.reject(new Error('Compiler worker was terminated.')); + } + pending.clear(); + } + + return { + compile, + validate, + ready, + terminate, + }; +} diff --git a/docs/public/editor/wasm/compiler-worker.js b/docs/public/editor/wasm/compiler-worker.js new file mode 100644 index 00000000000..682befd4cd3 --- /dev/null +++ b/docs/public/editor/wasm/compiler-worker.js @@ -0,0 +1,233 @@ +/** + * compiler-worker.js -- Web Worker that loads gh-aw.wasm and exposes + * the compileWorkflow and validateWorkflow functions via postMessage. + * + * Message protocol (inbound): + * { type: 'compile', id: , markdown: , files?: } + * { type: 'validate', id: , markdown: , files?: } + * + * Message protocol (outbound): + * { type: 'ready' } + * { type: 'result', id, yaml: , warnings: , error: null } + * { type: 'validate_result', id, errors: [{field, message, severity}], warnings: } + * { type: 'error', id, error: } + * + * This file is a classic script (not an ES module) because Web Workers + * need importScripts() to load wasm_exec.js synchronously. + */ + +/* global importScripts, Go, compileWorkflow, validateWorkflow, WebAssembly */ + +'use strict'; + +(function () { + // 1. Load Go's wasm_exec.js (provides the global `Go` class) + importScripts('./wasm_exec.js'); + + var ready = false; + + /** + * Initialize the Go WebAssembly runtime. + */ + async function init() { + try { + var go = new Go(); + + // Try streaming instantiation first; fall back to array buffer + // for servers that don't serve .wasm with application/wasm MIME type. + var result; + try { + result = await WebAssembly.instantiateStreaming( + fetch('./gh-aw.wasm'), + go.importObject, + ); + } catch (streamErr) { + var resp = await fetch('./gh-aw.wasm'); + var buf = await resp.arrayBuffer(); + result = await WebAssembly.instantiate(buf, go.importObject); + } + + // Start the Go program. go.run() never resolves because main() + // does `select{}`, so we intentionally do NOT await it. + go.run(result.instance); + + // Poll until the Go code has registered compileWorkflow on globalThis. + await waitForGlobal('compileWorkflow', 5000); + + ready = true; + self.postMessage({ type: 'ready' }); + } catch (err) { + self.postMessage({ + type: 'error', + id: null, + error: 'Worker initialization failed: ' + err.message, + }); + } + } + + /** + * Poll for a global property to appear. + */ + function waitForGlobal(name, timeoutMs) { + return new Promise(function (resolve, reject) { + var start = Date.now(); + (function check() { + if (typeof self[name] !== 'undefined') { + resolve(); + } else if (Date.now() - start > timeoutMs) { + reject(new Error('Timed out waiting for globalThis.' + name)); + } else { + setTimeout(check, 10); + } + })(); + }); + } + + /** + * Handle incoming messages from the main thread. + */ + self.onmessage = async function (event) { + var msg = event.data; + + if (msg.type === 'compile') { + await handleCompile(msg); + } else if (msg.type === 'validate') { + await handleValidate(msg); + } + }; + + /** + * Handle a compile request. + */ + async function handleCompile(msg) { + var id = msg.id; + + if (!ready) { + self.postMessage({ + type: 'error', + id: id, + error: 'Compiler is not ready yet.', + }); + return; + } + + if (typeof msg.markdown !== 'string') { + self.postMessage({ + type: 'error', + id: id, + error: 'markdown must be a string.', + }); + return; + } + + try { + // compileWorkflow returns a Promise (Go side). + // Pass optional files object for import resolution. + var files = msg.files || null; + var result = await compileWorkflow(msg.markdown, files); + + // The Go function returns { yaml: string, warnings: Array, error: null|object } + var warnings = []; + if (result.warnings) { + for (var i = 0; i < result.warnings.length; i++) { + warnings.push(result.warnings[i]); + } + } + + // error is now a structured object (or null) from Go, not a string + var errorObj = null; + if (result.error && typeof result.error === 'object') { + errorObj = { + message: result.error.message || '', + field: result.error.field || null, + line: result.error.line || null, + column: result.error.column || null, + severity: result.error.severity || 'error', + suggestion: result.error.suggestion || null, + docsUrl: result.error.docsUrl || null, + }; + } + + self.postMessage({ + type: 'result', + id: id, + yaml: result.yaml || '', + warnings: warnings, + error: errorObj, + }); + } catch (err) { + // Promise rejection = unexpected error, wrap in structured format + self.postMessage({ + type: 'error', + id: id, + error: err.message || String(err), + }); + } + } + + /** + * Handle a validate request. + */ + async function handleValidate(msg) { + var id = msg.id; + + if (!ready) { + self.postMessage({ + type: 'error', + id: id, + error: 'Compiler is not ready yet.', + }); + return; + } + + if (typeof msg.markdown !== 'string') { + self.postMessage({ + type: 'error', + id: id, + error: 'markdown must be a string.', + }); + return; + } + + try { + var files = msg.files || null; + var result = await validateWorkflow(msg.markdown, files); + + // The Go function returns { errors: [{field, message, severity}], warnings: [] } + var errors = []; + if (result.errors) { + for (var i = 0; i < result.errors.length; i++) { + var e = result.errors[i]; + errors.push({ + field: e.field || '', + message: e.message || '', + severity: e.severity || 'error', + }); + } + } + + var warnings = []; + if (result.warnings) { + for (var j = 0; j < result.warnings.length; j++) { + warnings.push(result.warnings[j]); + } + } + + self.postMessage({ + type: 'validate_result', + id: id, + errors: errors, + warnings: warnings, + }); + } catch (err) { + self.postMessage({ + type: 'error', + id: id, + error: err.message || String(err), + }); + } + } + + // Start initialization immediately. + init(); +})(); diff --git a/docs/public/editor/wasm/wasm_exec.js b/docs/public/editor/wasm/wasm_exec.js new file mode 100644 index 00000000000..d71af9e97e8 --- /dev/null +++ b/docs/public/editor/wasm/wasm_exec.js @@ -0,0 +1,575 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!globalThis.path) { + globalThis.path = { + resolve(...pathSegments) { + return pathSegments.join("/"); + } + } + } + + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const testCallExport = (a, b) => { + this._inst.exports.testExport0(); + return this._inst.exports.testExport(a, b); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + _gotest: { + add: (a, b) => a + b, + callExport: testCallExport, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8), + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})(); diff --git a/docs/public/wasm/compiler-loader.js b/docs/public/wasm/compiler-loader.js index 5cb87783565..7117fa06e5f 100644 --- a/docs/public/wasm/compiler-loader.js +++ b/docs/public/wasm/compiler-loader.js @@ -8,6 +8,7 @@ * const compiler = createWorkerCompiler(); * await compiler.ready; * const { yaml, warnings, error } = await compiler.compile(markdownString); + * const { errors, warnings } = await compiler.validate(markdownString); * // With imports: * const { yaml } = await compiler.compile(markdown, { 'shared/tools.md': '...' }); * compiler.terminate(); @@ -65,7 +66,19 @@ export function createWorkerCompiler(options = {}) { entry.resolve({ yaml: msg.yaml, warnings: msg.warnings || [], - error: null, + error: msg.error || null, + }); + } + break; + } + + case 'validate_result': { + const entry = pending.get(msg.id); + if (entry) { + pending.delete(msg.id); + entry.resolve({ + errors: msg.errors || [], + warnings: msg.warnings || [], }); } break; @@ -81,10 +94,11 @@ export function createWorkerCompiler(options = {}) { const entry = pending.get(msg.id); if (entry) { pending.delete(msg.id); + // Wrap raw error strings into a structured CompilerError object entry.resolve({ yaml: '', warnings: [], - error: msg.error, + error: { message: msg.error, severity: 'error' }, }); } break; @@ -134,6 +148,30 @@ export function createWorkerCompiler(options = {}) { }); } + /** + * Validate a markdown workflow string with full schema validation enabled. + * + * @param {string} markdown + * @param {Record} [files] - Optional map of file paths to content + * @returns {Promise<{errors: Array<{field: string, message: string, severity: string}>, warnings: string[]}>} + */ + function validate(markdown, files) { + if (isTerminated) { + return Promise.reject(new Error('Compiler worker has been terminated.')); + } + + const id = nextId++; + + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + const msg = { type: 'validate', id, markdown }; + if (files && Object.keys(files).length > 0) { + msg.files = files; + } + worker.postMessage(msg); + }); + } + /** * Terminate the worker. After this, compile() will reject. */ @@ -151,6 +189,7 @@ export function createWorkerCompiler(options = {}) { return { compile, + validate, ready, terminate, }; diff --git a/docs/public/wasm/compiler-worker.js b/docs/public/wasm/compiler-worker.js index 5d9349a151c..682befd4cd3 100644 --- a/docs/public/wasm/compiler-worker.js +++ b/docs/public/wasm/compiler-worker.js @@ -1,20 +1,22 @@ /** * compiler-worker.js -- Web Worker that loads gh-aw.wasm and exposes - * the compileWorkflow function via postMessage. + * the compileWorkflow and validateWorkflow functions via postMessage. * * Message protocol (inbound): - * { type: 'compile', id: , markdown: , files?: } + * { type: 'compile', id: , markdown: , files?: } + * { type: 'validate', id: , markdown: , files?: } * * Message protocol (outbound): * { type: 'ready' } - * { type: 'result', id: , yaml: , warnings: , error: null } - * { type: 'error', id: , error: } + * { type: 'result', id, yaml: , warnings: , error: null } + * { type: 'validate_result', id, errors: [{field, message, severity}], warnings: } + * { type: 'error', id, error: } * * This file is a classic script (not an ES module) because Web Workers * need importScripts() to load wasm_exec.js synchronously. */ -/* global importScripts, Go, compileWorkflow, WebAssembly */ +/* global importScripts, Go, compileWorkflow, validateWorkflow, WebAssembly */ 'use strict'; @@ -87,10 +89,17 @@ self.onmessage = async function (event) { var msg = event.data; - if (msg.type !== 'compile') { - return; + if (msg.type === 'compile') { + await handleCompile(msg); + } else if (msg.type === 'validate') { + await handleValidate(msg); } + }; + /** + * Handle a compile request. + */ + async function handleCompile(msg) { var id = msg.id; if (!ready) { @@ -117,7 +126,7 @@ var files = msg.files || null; var result = await compileWorkflow(msg.markdown, files); - // The Go function returns { yaml: string, warnings: Array, error: null|string } + // The Go function returns { yaml: string, warnings: Array, error: null|object } var warnings = []; if (result.warnings) { for (var i = 0; i < result.warnings.length; i++) { @@ -125,29 +134,99 @@ } } - if (result.error) { - self.postMessage({ - type: 'error', - id: id, - error: String(result.error), - }); - } else { - self.postMessage({ - type: 'result', - id: id, - yaml: result.yaml || '', - warnings: warnings, - error: null, - }); + // error is now a structured object (or null) from Go, not a string + var errorObj = null; + if (result.error && typeof result.error === 'object') { + errorObj = { + message: result.error.message || '', + field: result.error.field || null, + line: result.error.line || null, + column: result.error.column || null, + severity: result.error.severity || 'error', + suggestion: result.error.suggestion || null, + docsUrl: result.error.docsUrl || null, + }; } + + self.postMessage({ + type: 'result', + id: id, + yaml: result.yaml || '', + warnings: warnings, + error: errorObj, + }); } catch (err) { + // Promise rejection = unexpected error, wrap in structured format self.postMessage({ type: 'error', id: id, error: err.message || String(err), }); } - }; + } + + /** + * Handle a validate request. + */ + async function handleValidate(msg) { + var id = msg.id; + + if (!ready) { + self.postMessage({ + type: 'error', + id: id, + error: 'Compiler is not ready yet.', + }); + return; + } + + if (typeof msg.markdown !== 'string') { + self.postMessage({ + type: 'error', + id: id, + error: 'markdown must be a string.', + }); + return; + } + + try { + var files = msg.files || null; + var result = await validateWorkflow(msg.markdown, files); + + // The Go function returns { errors: [{field, message, severity}], warnings: [] } + var errors = []; + if (result.errors) { + for (var i = 0; i < result.errors.length; i++) { + var e = result.errors[i]; + errors.push({ + field: e.field || '', + message: e.message || '', + severity: e.severity || 'error', + }); + } + } + + var warnings = []; + if (result.warnings) { + for (var j = 0; j < result.warnings.length; j++) { + warnings.push(result.warnings[j]); + } + } + + self.postMessage({ + type: 'validate_result', + id: id, + errors: errors, + warnings: warnings, + }); + } catch (err) { + self.postMessage({ + type: 'error', + id: id, + error: err.message || String(err), + }); + } + } // Start initialization immediately. init(); From f67c25139c2ff9d2bf14542c8bfe313fcf555ecd Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Tue, 3 Mar 2026 18:03:59 +0000 Subject: [PATCH 12/13] Add deploy-to-GitHub feature with multi-step dialog, security review, and rebuilt assets Introduces a deploy workflow that lets users push workflows directly to GitHub repos from the visual editor, with token input, repo selection, progress tracking, and success confirmation steps. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/editor-app/DEPLOY-REQUIREMENTS.md | 297 ++++++++++++++++++ docs/editor-app/e2e-deploy-test.ts | 231 ++++++++++++++ docs/editor-app/src/App.tsx | 4 + docs/editor-app/src/SECURITY-REVIEW-V2.md | 142 +++++++++ docs/editor-app/src/SECURITY-REVIEW.md | 210 +++++++++++++ .../src/components/Deploy/DeployDialog.tsx | 114 +++++++ .../src/components/Deploy/ProgressStep.tsx | 46 +++ .../src/components/Deploy/RepoStep.tsx | 203 ++++++++++++ .../src/components/Deploy/SuccessStep.tsx | 43 +++ .../src/components/Deploy/TokenStep.tsx | 131 ++++++++ .../src/components/Header/ExportMenu.tsx | 23 +- .../src/components/Header/Header.tsx | 39 ++- .../src/components/Panels/NetworkPanel.tsx | 1 - docs/editor-app/src/stores/deployStore.ts | 161 +++++++--- docs/editor-app/src/styles/globals.css | 55 ++++ docs/editor-app/src/utils/deploy.ts | 162 ++++++++++ docs/editor-app/src/utils/githubApi.ts | 131 ++++---- docs/editor-app/src/utils/validation.ts | 2 +- .../assets/CanvasWithProvider-16gt3nDF.js | 3 + .../assets/CanvasWithProvider-Dyo1Jspo.js | 3 - ...iew-CjlFMWmw.js => EditorView-D7yKdvcD.js} | 32 +- .../editor/assets/GuidedTour-B1jWtr81.js | 1 + .../editor/assets/GuidedTour-C5M5ZOCR.js | 1 - .../editor/assets/PropertiesPanel-B7IrNCmT.js | 5 + .../editor/assets/PropertiesPanel-X99s2t6t.js | 5 - .../editor/assets/ShortcutsHelp-B4w7TheO.js | 1 - .../editor/assets/ShortcutsHelp-COduh4Oz.js | 1 + docs/public/editor/assets/Sidebar-Bg9u_eYq.js | 86 +++++ docs/public/editor/assets/Sidebar-CsF1dhTT.js | 26 -- .../editor/assets/WelcomeModal-B5fTnVgL.js | 1 + .../editor/assets/WelcomeModal-BcOpPeA2.js | 1 - .../editor/assets/YamlPreview-B8ZgOPvw.js | 5 + .../editor/assets/YamlPreview-DUoC6eaq.js | 5 - docs/public/editor/assets/index-7l71UumV.js | 9 + docs/public/editor/assets/index-BnMoMuy9.css | 1 - docs/public/editor/assets/index-CQccKy73.css | 1 + docs/public/editor/assets/index-D2nmaYOD.js | 9 - ...sQmQ2fv.js => layout-template-DawlbNTY.js} | 2 +- docs/public/editor/assets/wrench-BtizfR87.js | 1 + docs/public/editor/assets/wrench-C08fmUNq.js | 1 - docs/public/editor/index.html | 5 +- 41 files changed, 2003 insertions(+), 197 deletions(-) create mode 100644 docs/editor-app/DEPLOY-REQUIREMENTS.md create mode 100644 docs/editor-app/e2e-deploy-test.ts create mode 100644 docs/editor-app/src/SECURITY-REVIEW-V2.md create mode 100644 docs/editor-app/src/SECURITY-REVIEW.md create mode 100644 docs/editor-app/src/components/Deploy/DeployDialog.tsx create mode 100644 docs/editor-app/src/components/Deploy/ProgressStep.tsx create mode 100644 docs/editor-app/src/components/Deploy/RepoStep.tsx create mode 100644 docs/editor-app/src/components/Deploy/SuccessStep.tsx create mode 100644 docs/editor-app/src/components/Deploy/TokenStep.tsx create mode 100644 docs/editor-app/src/utils/deploy.ts create mode 100644 docs/public/editor/assets/CanvasWithProvider-16gt3nDF.js delete mode 100644 docs/public/editor/assets/CanvasWithProvider-Dyo1Jspo.js rename docs/public/editor/assets/{EditorView-CjlFMWmw.js => EditorView-D7yKdvcD.js} (51%) create mode 100644 docs/public/editor/assets/GuidedTour-B1jWtr81.js delete mode 100644 docs/public/editor/assets/GuidedTour-C5M5ZOCR.js create mode 100644 docs/public/editor/assets/PropertiesPanel-B7IrNCmT.js delete mode 100644 docs/public/editor/assets/PropertiesPanel-X99s2t6t.js delete mode 100644 docs/public/editor/assets/ShortcutsHelp-B4w7TheO.js create mode 100644 docs/public/editor/assets/ShortcutsHelp-COduh4Oz.js create mode 100644 docs/public/editor/assets/Sidebar-Bg9u_eYq.js delete mode 100644 docs/public/editor/assets/Sidebar-CsF1dhTT.js create mode 100644 docs/public/editor/assets/WelcomeModal-B5fTnVgL.js delete mode 100644 docs/public/editor/assets/WelcomeModal-BcOpPeA2.js create mode 100644 docs/public/editor/assets/YamlPreview-B8ZgOPvw.js delete mode 100644 docs/public/editor/assets/YamlPreview-DUoC6eaq.js create mode 100644 docs/public/editor/assets/index-7l71UumV.js delete mode 100644 docs/public/editor/assets/index-BnMoMuy9.css create mode 100644 docs/public/editor/assets/index-CQccKy73.css delete mode 100644 docs/public/editor/assets/index-D2nmaYOD.js rename docs/public/editor/assets/{layout-template-dsQmQ2fv.js => layout-template-DawlbNTY.js} (77%) create mode 100644 docs/public/editor/assets/wrench-BtizfR87.js delete mode 100644 docs/public/editor/assets/wrench-C08fmUNq.js diff --git a/docs/editor-app/DEPLOY-REQUIREMENTS.md b/docs/editor-app/DEPLOY-REQUIREMENTS.md new file mode 100644 index 00000000000..01abe099130 --- /dev/null +++ b/docs/editor-app/DEPLOY-REQUIREMENTS.md @@ -0,0 +1,297 @@ +# Deploy to GitHub — Requirements + +**Status**: Approved +**Date**: 2026-02-24 +**References**: [DEPLOY-TO-GITHUB-PLAN.md](./DEPLOY-TO-GITHUB-PLAN.md), [SECURITY-REVIEW.md](./src/SECURITY-REVIEW.md) + +--- + +## 1. User Workflows + +### W-1: First-Time User (No Token) + +1. User opens the editor, configures a workflow (or loads a template). +2. User clicks "Deploy to GitHub" in the Export menu. +3. Dialog opens at the **Token Setup** step. +4. User clicks "Create a token on GitHub" link — opens `github.com/settings/tokens/new?scopes=repo,workflow&description=gh-aw-editor-deploy` in a new tab. +5. User pastes token into the input field. +6. "Remember this token" checkbox is **unchecked by default**. +7. User clicks "Save & Continue". +8. System calls `GET /user` to validate token. On success, transitions to **Repo Selection** step. +9. User enters `owner/repo`, confirms branch name (auto-derived from workflow name), and clicks "Deploy". +10. System executes the 5-step deploy sequence (verify repo → create branch → upload .md → upload .lock.yml → create PR). +11. **Success** step shows a link to the PR. User clicks "View PR on GitHub" or "Done". + +### W-2: Returning User (Saved Token) + +1. User clicks "Deploy to GitHub". +2. System detects a saved token, calls `GET /user` to validate it. +3. If valid, dialog opens directly at the **Repo Selection** step (skips token setup). +4. If invalid/expired, dialog shows an error and falls back to the **Token Setup** step with the message "Your saved token is no longer valid. Please enter a new one." + +### W-3: Deploy to a New/Empty Repo + +1. User enters a repo slug for a repo that has no `.github/workflows/` directory yet. +2. Deploy succeeds — the GitHub Contents API creates intermediate directories automatically. +3. PR is created as normal. No special handling required. + +### W-4: Deploy a Second Workflow (Branch Conflict) + +1. User deploys workflow "issue-triage" → branch `aw/issue-triage` is created, PR opened. +2. User modifies the workflow and deploys again with the same branch name. +3. GitHub returns 422 ("Reference already exists") on `POST /repos/{owner}/{repo}/git/refs`. +4. System shows an error: "Branch `aw/issue-triage` already exists. Change the branch name or delete the existing branch on GitHub." +5. User changes branch name to `aw/issue-triage-v2` and retries successfully. + +### W-5: Invalid or Expired Token + +1. User's previously saved token is revoked or expired. +2. On deploy attempt, `GET /user` returns 401. +3. System clears the invalid token from memory (and localStorage if persisted). +4. Dialog shows **Token Setup** step with error: "Your token is invalid or expired. Please enter a new one." + +--- + +## 2. Edge Cases + +### E-1: Empty Workflow + +- **Trigger**: User clicks "Deploy to GitHub" with no template loaded and no configuration. +- **Expected**: Deploy button is **disabled** in the Export menu. Tooltip: "Configure a workflow before deploying." +- **Gate condition**: `compiledMarkdown` is empty OR `compiledYaml` is empty OR `error` is non-null in `workflowStore`. + +### E-2: Workflow Compiles with Warnings + +- **Trigger**: WASM compiler returns warnings but no error, and `compiledYaml` is non-empty. +- **Expected**: Deploy is **allowed**. Warnings are shown in the Repo Selection step as a dismissible banner: "Compilation produced {N} warning(s). The workflow may not behave as expected." + +### E-3: WASM Compiler Not Yet Loaded + +- **Trigger**: User clicks "Deploy to GitHub" before the WASM compiler has initialized (`isReady === false` in `workflowStore`). +- **Expected**: Deploy button is **disabled** with tooltip "Compiler loading..." until `isReady` becomes `true`. + +### E-4: Token Lacks Required Scope (403) + +- **Trigger**: Token is valid (`GET /user` succeeds) but lacks `repo` or `workflow` scope. File upload returns 403. +- **Expected**: System shows error: "Your token doesn't have permission to push to this repository. Ensure your token has `repo` and `workflow` scopes." Deploy stops at the failed step; prior steps keep their checkmarks. + +### E-5: Repo Doesn't Exist (404) + +- **Trigger**: User enters a non-existent repo slug. +- **Expected**: Step 1 ("Verify repository access") fails with error: "Repository `{owner}/{repo}` not found. Check the name and your token's access." + +### E-6: Branch Already Exists (422) + +- See W-4 above. Error message includes the branch name and suggests renaming. + +### E-7: Network Timeout / Failure + +- **Trigger**: Any GitHub API call fails with a network error (no HTTP status). +- **Expected**: Current step shows error icon. Error message: "Network error — check your internet connection and try again." A "Retry" button appears that retries from the failed step (not from the beginning). + +### E-8: User Closes Dialog Mid-Deploy + +- **Trigger**: User clicks X or presses Escape while deploy is in progress. +- **Expected**: A confirmation prompt appears: "Deploy is in progress. Closing will cancel the remaining steps. Already-uploaded files will remain on GitHub." Options: "Continue Deploying" | "Close Anyway". +- If closed: deploy is cancelled, no PR is created (orphaned branch may remain). + +### E-9: Malformed Repo Slug + +- **Trigger**: User enters `/`, `a/b/c`, `../..`, or special characters. +- **Expected**: Inline validation error under the repo input: "Enter a valid repository in the format `owner/repo`." Deploy button stays disabled. +- **Validation regex**: `^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$` + +### E-10: Invalid Branch Name + +- **Trigger**: User edits auto-generated branch name to include `..`, spaces, `~`, `^`, `:`, `\`, or trailing `.lock`. +- **Expected**: Inline validation error: "Invalid branch name." Deploy button stays disabled. + +### E-11: File Already Exists on Target Branch + +- **Trigger**: User deploys a workflow whose `.md` or `.lock.yml` file already exists at the path on the base branch. +- **Expected**: The Contents API `PUT` creates or **updates** the file (specify `sha` of existing file). This is fine for a new branch. If the branch is new, no conflict is possible. No special handling needed. + +### E-12: Very Large Workflow + +- **Trigger**: Workflow markdown or compiled YAML exceeds GitHub's 1MB file size limit. +- **Expected**: System shows error: "Workflow file is too large (>{size}). Reduce the instructions or configuration." This is unlikely in practice. + +--- + +## 3. Acceptance Criteria + +Each criterion is testable via Playwright e2e tests or unit tests. + +### AC-1: End-to-End Deploy (Happy Path) + +**Test**: Load "Issue Triage" template → open Deploy dialog → enter a valid token → enter `{test-owner}/{test-repo}` → click Deploy → verify PR is created. + +**Assertions**: +- [ ] PR exists at `https://github.com/{owner}/{repo}/pull/{number}` +- [ ] PR title matches `Add Agentic Workflow: {workflow-name}` +- [ ] PR base branch is `main` (or the repo's default branch) +- [ ] PR head branch matches the `branchName` field from the dialog + +### AC-2: PR Contains Valid Workflow Source (.md) + +**Test**: After AC-1, fetch the `.md` file from the PR branch. + +**Assertions**: +- [ ] File exists at `.github/workflows/{name}.md` +- [ ] File content starts with `---` (YAML frontmatter delimiter) +- [ ] File contains `name:` field in frontmatter +- [ ] File contains `on:` field in frontmatter +- [ ] File contains `engine:` field in frontmatter +- [ ] File contains an instructions section (markdown body after frontmatter) +- [ ] File content matches `workflowStore.compiledMarkdown` byte-for-byte + +### AC-3: PR Contains Valid Compiled YAML (.lock.yml) + +**Test**: After AC-1, fetch the `.lock.yml` file from the PR branch. + +**Assertions**: +- [ ] File exists at `.github/workflows/{name}.lock.yml` +- [ ] File parses as valid YAML +- [ ] YAML contains top-level `name:` key +- [ ] YAML contains top-level `on:` key +- [ ] YAML contains `jobs:` with at least one job +- [ ] File content matches `workflowStore.compiledYaml` byte-for-byte + +### AC-4: Error Messages Are Clear and Actionable + +**Test**: Trigger each error condition and verify the displayed message. + +| Condition | Expected Message (substring match) | +|-----------|-----------------------------------| +| Invalid token (401) | "invalid or expired" | +| Repo not found (404) | "not found" | +| Missing scope (403) | "permission" | +| Branch exists (422) | "already exists" | +| Network failure | "Network error" | +| Malformed repo slug | "owner/repo" (format hint) | + +**Assertions**: +- [ ] Error messages do NOT contain the token string +- [ ] Error messages include the specific resource that failed (repo name, branch name) +- [ ] Each error message suggests a corrective action + +### AC-5: Token Not Persisted by Default + +**Test**: Enter token with "Remember" unchecked → close dialog → reopen. + +**Assertions**: +- [ ] `localStorage.getItem('gh-aw-deploy')` does NOT contain a token string after dialog close (when "Remember" is unchecked) +- [ ] `rememberToken` defaults to `false` in `deployStore` initial state +- [ ] After page refresh, the token is gone and the dialog starts at the Token Setup step +- [ ] When "Remember" IS checked: `localStorage` contains the token, and re-opening skips to Repo Selection + +### AC-6: Deploy Button Disabled When Workflow Is Empty + +**Test**: Reset the editor to empty state → check Deploy button. + +**Assertions**: +- [ ] Deploy menu item is disabled (not clickable) when `compiledYaml` is empty +- [ ] Deploy menu item is disabled when `compiledMarkdown` is empty +- [ ] Deploy menu item is disabled when `error` is non-null in `workflowStore` +- [ ] Deploy menu item is disabled when `isReady` is `false` (compiler not loaded) +- [ ] Deploy menu item becomes enabled after loading a template and successful compilation + +### AC-7: Progress Steps Animate Correctly + +**Test**: Initiate a deploy and observe each step. + +**Assertions**: +- [ ] Exactly 5 steps are shown: "Verify repository access", "Create branch", "Upload workflow source", "Upload compiled YAML", "Create pull request" +- [ ] Each step starts with a pending indicator (e.g., empty circle) +- [ ] The currently executing step shows a spinner/loading indicator +- [ ] Completed steps show a checkmark +- [ ] On error, the failed step shows an error icon; subsequent steps remain pending +- [ ] Steps transition in order (no skipping) + +### AC-8: Dark Mode Compatible + +**Test**: Toggle theme to dark mode → open Deploy dialog. + +**Assertions**: +- [ ] Dialog background uses dark theme colors (not white) +- [ ] Text is readable (sufficient contrast ratio) +- [ ] Input fields have dark theme styling +- [ ] Progress indicators (checkmarks, spinners, error icons) are visible on dark background +- [ ] No hardcoded colors that break in dark mode + +--- + +## 4. Security Requirements + +These are derived from the [security review](./src/SECURITY-REVIEW.md) and must be addressed in implementation. + +### S-1: Input Validation + +- [ ] Repo slug validated with `^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$` before any API call +- [ ] Branch name validated against git ref rules (no `..`, no `\`, no trailing `.lock`, no ASCII control chars) +- [ ] All user-supplied path segments (`owner`, `repo`, `branch`, `filepath`) use `encodeURIComponent()` in API URLs + +### S-2: Token Safety + +- [ ] `rememberToken` defaults to `false` +- [ ] Token is never logged to console +- [ ] Token is never included in error messages shown to the user +- [ ] Token is transmitted only via `Authorization` header over HTTPS +- [ ] A "Clear saved token" option is accessible from the deploy dialog at all times + +### S-3: PR Body Safety + +- [ ] `${{ }}` patterns in user-authored content are escaped (rendered as code literals) before inclusion in PR body +- [ ] PR body does not include raw workflow instructions that could contain injection strings + +### S-4: Modern APIs + +- [ ] No usage of deprecated `unescape()` — use `TextEncoder` for UTF-8 base64 encoding + +### S-5: Double-Submit Protection + +- [ ] Deploy button is disabled while a deploy is in progress +- [ ] Re-clicking "Deploy" during an active deploy does not trigger a second API call sequence + +--- + +## 5. Non-Functional Requirements + +### NF-1: No Backend Required + +- All API calls go directly from the browser to `api.github.com` via `fetch()`. +- No CORS proxy, no Cloudflare Worker, no server-side component. + +### NF-2: No New Heavy Dependencies + +- Use native `fetch()` for GitHub API calls — do NOT add `@octokit/rest` or `axios`. +- The only new dependencies should be those already in `package.json` (Radix Dialog is already present). + +### NF-3: Copy CLI Command Fallback + +- The Export menu should also offer "Copy CLI command" that generates a `gh aw compile` + `gh pr create` command sequence for users who prefer not to use browser tokens. + +### NF-4: State Cleanup + +- On dialog close (after success or cancel), reset the deploy store's transient state (progress, error, prUrl) but preserve token/username if remembered. + +--- + +## 6. Files to Create + +| File | Purpose | +|------|---------| +| `src/stores/deployStore.ts` | Zustand store: token, step, repo, progress, prUrl | +| `src/utils/githubApi.ts` | `fetch()` wrapper for GitHub REST API (7 endpoints) | +| `src/utils/deploy.ts` | Orchestration: validate → branch → files → PR | +| `src/components/Deploy/DeployDialog.tsx` | Multi-step Radix Dialog container | +| `src/components/Deploy/TokenSetup.tsx` | Step 1: token input + validation | +| `src/components/Deploy/RepoSelector.tsx` | Step 2: repo/branch inputs + validation | +| `src/components/Deploy/DeployProgress.tsx` | Step 3: animated progress checklist | +| `src/components/Deploy/DeploySuccess.tsx` | Step 4: PR link + done button | + +## 7. Files to Modify + +| File | Change | +|------|--------| +| `src/components/Header/ExportMenu.tsx` | Add "Deploy to GitHub" and "Copy CLI Command" items | diff --git a/docs/editor-app/e2e-deploy-test.ts b/docs/editor-app/e2e-deploy-test.ts new file mode 100644 index 00000000000..1ca88414469 --- /dev/null +++ b/docs/editor-app/e2e-deploy-test.ts @@ -0,0 +1,231 @@ +import { chromium } from '@playwright/test'; + +const BASE_URL = 'http://localhost:5175/gh-aw/editor/'; +const TOKEN = process.env.GITHUB_TOKEN || ''; +const REPO = 'Mossaka/gh-aw-deploy'; +const BRANCH = `aw/e2e-test-${Date.now()}`; + +async function main() { + console.log('--- E2E Deploy Test ---'); + console.log(`Branch: ${BRANCH}`); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); + const page = await context.newPage(); + + page.on('console', (msg) => { + if (msg.type() === 'error') { + console.log(`[BROWSER ERROR] ${msg.text()}`); + } + }); + + // Step 1: Navigate to the editor — clear localStorage first so welcome modal shows + console.log('\n1. Loading editor...'); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + // Mark onboarding as seen to avoid it, then set up a template programmatically + await page.evaluate(() => { + // Mark onboarding as seen so the modal closes + const uiState = JSON.parse(localStorage.getItem('workflow-editor-ui') || '{}'); + uiState.state = { ...uiState.state, hasSeenOnboarding: true }; + localStorage.setItem('workflow-editor-ui', JSON.stringify(uiState)); + }); + // Reload with onboarding dismissed + await page.reload({ waitUntil: 'networkidle' }); + console.log(' Editor loaded (onboarding dismissed).'); + + // Step 2: Load a template by clicking "Browse templates" button in sidebar + console.log('\n2. Loading a template...'); + + // Find and click the Templates tab in the sidebar + const templatesTabBtn = page.locator('button:has-text("Templates")').first(); + if (await templatesTabBtn.isVisible({ timeout: 3000 })) { + await templatesTabBtn.click(); + console.log(' Clicked Templates tab.'); + await page.waitForTimeout(500); + } + + await page.screenshot({ path: '/tmp/deploy-02-templates-tab.png' }); + + // Look for template items and click the first one (Issue Triage) + // Templates in the sidebar might be buttons with the template name + const issueTriageBtn = page.locator('button:has-text("Issue Triage")').first(); + if (await issueTriageBtn.isVisible({ timeout: 3000 })) { + await issueTriageBtn.click(); + console.log(' Clicked Issue Triage template.'); + } else { + // Load template programmatically + console.log(' Loading template programmatically...'); + await page.evaluate(() => { + // Access the workflow store and load Issue Triage template + const store = (window as unknown as { __ZUSTAND_STORES__?: Record }).__ZUSTAND_STORES__; + if (store) console.log('Store found'); + }); + + // Set the state directly via localStorage and reload + await page.evaluate(() => { + const templateState = { + state: { + name: 'issue-triage', + description: 'Analyze new issues, apply labels, detect spam, find duplicates, and post triage notes', + trigger: { + event: 'issues', + activityTypes: ['opened', 'reopened'], + branches: [], + paths: [], + schedule: '', + skipRoles: [], + skipBots: false, + roles: [], + bots: [], + reaction: 'eyes', + statusComment: false, + manualApproval: '', + slashCommandName: '', + }, + permissions: { + contents: 'read', + issues: 'read', + 'pull-requests': 'read', + actions: 'read', + checks: 'read', + 'security-events': 'read', + statuses: 'read', + metadata: 'read', + }, + engine: { type: 'claude', model: '', maxTurns: '', version: '', config: {} }, + tools: ['web-fetch', 'github'], + toolConfigs: {}, + instructions: 'You are a triage assistant for GitHub issues. When a new issue is opened or reopened:\n1. Fetch the issue content. If it is spam or bot-generated, add a one-sentence comment and exit.\n2. Gather context: fetch available labels, find similar open issues, and read any comments.\n3. Analyze the issue type (bug, feature, question), severity, affected components, and user impact.\n4. Apply up to 5 labels that accurately reflect the issue. Only use labels from the repository\'s label list.\n5. Post a triage comment.', + safeOutputs: { + 'add-labels': { enabled: true, config: { max: 5 } }, + 'add-comment': { enabled: true, config: {} }, + }, + network: { allowed: ['defaults'], blocked: [] }, + timeoutMinutes: 15, + imports: [], + environment: {}, + cache: false, + strict: false, + concurrency: { group: '', cancelInProgress: false }, + rateLimit: { max: '', window: '' }, + platform: '', + }, + }; + localStorage.setItem('workflow-editor-state', JSON.stringify(templateState)); + }); + await page.reload({ waitUntil: 'networkidle' }); + console.log(' Template loaded via localStorage.'); + } + + // Step 3: Wait for WASM compilation + console.log('\n3. Waiting for WASM compilation...'); + for (let i = 0; i < 30; i++) { + const statusEl = page.locator('[aria-label^="Compilation status"]'); + if (await statusEl.isVisible({ timeout: 500 })) { + const text = await statusEl.textContent(); + if (text?.includes('Ready') || text?.includes('warning')) { + console.log(` Status: ${text}`); + break; + } + if (i % 5 === 0) console.log(` Waiting... (${text})`); + } + await page.waitForTimeout(500); + } + + // Verify we have compiled content + const hasContent = await page.evaluate(() => { + const state = JSON.parse(localStorage.getItem('workflow-editor-state') || '{}'); + return { + hasName: !!state?.state?.name, + name: state?.state?.name, + }; + }); + console.log(` Workflow name: ${hasContent.name}`); + + await page.screenshot({ path: '/tmp/deploy-03-ready.png' }); + + // Step 4: Open Export menu + console.log('\n4. Opening Export menu...'); + const exportBtn = page.locator('[aria-label="Export workflow"]'); + await exportBtn.waitFor({ state: 'visible', timeout: 5000 }); + await exportBtn.click(); + await page.waitForTimeout(300); + await page.screenshot({ path: '/tmp/deploy-04-export-menu.png' }); + + // Step 5: Click Deploy to GitHub + console.log('\n5. Clicking Deploy to GitHub...'); + const deployBtn = page.locator('[data-testid="deploy-to-github-btn"]'); + await deployBtn.waitFor({ state: 'visible', timeout: 5000 }); + await deployBtn.click(); + await page.waitForTimeout(500); + + // Step 6: Deploy dialog open + console.log('\n6. Deploy dialog open...'); + const dialog = page.locator('[data-testid="deploy-dialog"]'); + await dialog.waitFor({ timeout: 5000 }); + await page.screenshot({ path: '/tmp/deploy-05-dialog.png' }); + + // Step 7: Enter token + console.log('\n7. Entering token...'); + const tokenInput = page.locator('[data-testid="token-input"]'); + await tokenInput.waitFor({ timeout: 5000 }); + await tokenInput.fill(TOKEN); + const saveContinueBtn = page.locator('[data-testid="save-continue-btn"]'); + await saveContinueBtn.click(); + console.log(' Validating token...'); + + // Wait for repo step + const repoInput = page.locator('[data-testid="repo-input"]'); + await repoInput.waitFor({ timeout: 15000 }); + console.log(' Token valid. On repo step.'); + await page.screenshot({ path: '/tmp/deploy-06-repo-step.png' }); + + // Step 8: Fill repo and branch + console.log('\n8. Filling repo and branch...'); + await repoInput.fill(REPO); + const branchInput = page.locator('[data-testid="branch-input"]'); + await branchInput.clear(); + await branchInput.fill(BRANCH); + await page.screenshot({ path: '/tmp/deploy-07-filled.png' }); + + // Step 9: Deploy! + console.log('\n9. Clicking Deploy...'); + const deployNowBtn = page.locator('[data-testid="deploy-btn"]'); + await deployNowBtn.click(); + console.log(' Deploy initiated!'); + + // Step 10: Wait for result + console.log('\n10. Waiting for deploy to complete...'); + try { + await Promise.race([ + page.locator('[data-testid="pr-link"]').waitFor({ timeout: 60000 }), + page.locator('[data-testid="deploy-error"]').waitFor({ timeout: 60000 }), + ]); + } catch { + console.log(' Timeout!'); + await page.screenshot({ path: '/tmp/deploy-timeout.png' }); + } + + await page.screenshot({ path: '/tmp/deploy-08-result.png' }); + + const prLink = page.locator('[data-testid="pr-link"]'); + const errorEl = page.locator('[data-testid="deploy-error"]'); + + if (await prLink.isVisible({ timeout: 2000 })) { + const href = await prLink.getAttribute('href'); + console.log(`\n SUCCESS! PR created: ${href}`); + } else if (await errorEl.isVisible({ timeout: 2000 })) { + const errorText = await errorEl.textContent(); + console.log(`\n DEPLOY ERROR: ${errorText}`); + } else { + console.log('\n UNKNOWN STATE'); + } + + await browser.close(); + console.log('\n--- Test Complete ---'); +} + +main().catch((err) => { + console.error('Test failed:', err); + process.exit(1); +}); diff --git a/docs/editor-app/src/App.tsx b/docs/editor-app/src/App.tsx index 73998811fed..0df1e0389be 100644 --- a/docs/editor-app/src/App.tsx +++ b/docs/editor-app/src/App.tsx @@ -24,6 +24,7 @@ const WelcomeModal = lazy(() => import('./components/Onboarding/WelcomeModal').t const LazyToaster = lazy(() => import('sonner').then(m => ({ default: m.Toaster }))); const ShortcutsHelp = lazy(() => import('./components/shared/ShortcutsHelp').then(m => ({ default: m.ShortcutsHelp }))); const GuidedTour = lazy(() => import('./components/Onboarding/GuidedTour').then(m => ({ default: m.GuidedTour }))); +const DeployDialog = lazy(() => import('./components/Deploy/DeployDialog').then(m => ({ default: m.DeployDialog }))); // Canvas is large (~220KB with ReactFlow) -- lazy load with a loading skeleton const CanvasWithProvider = lazy(() => import('./components/Canvas/CanvasWithProvider')); @@ -224,6 +225,9 @@ export default function App() { + + + diff --git a/docs/editor-app/src/SECURITY-REVIEW-V2.md b/docs/editor-app/src/SECURITY-REVIEW-V2.md new file mode 100644 index 00000000000..69d2a2dd02e --- /dev/null +++ b/docs/editor-app/src/SECURITY-REVIEW-V2.md @@ -0,0 +1,142 @@ +# Security Review: Deploy to GitHub (v2) + +**Date:** 2026-02-25 +**Reviewer:** security-tester (automated) +**Scope:** `githubApi.ts`, `deployStore.ts`, `deploy.ts`, `components/Deploy/*.tsx` + +--- + +## Summary + +The deploy v2 implementation is well-structured with good baseline security practices: +React JSX auto-escaping prevents XSS, `encodeURIComponent` is applied to owner/repo in URLs, +`${{ }}` expression injection is escaped in PR body content, and token persistence is opt-in. + +Two medium-severity issues require code fixes. No critical or high-severity findings. + +--- + +## Findings + +### M-1: Workflow name used in file path without sanitization [MEDIUM] + +**File:** `src/utils/deploy.ts:33-34` + +```ts +const mdPath = `.github/workflows/${workflowName}.md`; +const ymlPath = `.github/workflows/${workflowName}.lock.yml`; +``` + +`workflowName` originates from user input (`workflowStore.name`) and is used to construct the +`path` parameter for the GitHub Contents API. If the name contains path traversal sequences +(e.g., `../../../README`), files could be written outside `.github/workflows/`. + +While the GitHub API may normalize paths server-side, this is a defense-in-depth gap. +The name should be sanitized to allow only safe filename characters. + +**Fix:** Sanitize `workflowName` before constructing paths. Strip path separators, `..`, +and restrict to `[a-zA-Z0-9._-]`. + +--- + +### M-2: PR URL rendered as href without origin validation [MEDIUM] + +**File:** `src/components/Deploy/SuccessStep.tsx:15` + +```tsx + +``` + +`prUrl` comes from the GitHub API response (`html_url`). If the API response were tampered +with (MITM, compromised proxy, or future API changes), a malicious `javascript:` or data URI +could be rendered as a clickable link. + +**Fix:** Validate that `prUrl` starts with `https://github.com/` before rendering as a link. + +--- + +### L-1: File path parameter not URL-encoded [LOW] + +**File:** `src/utils/githubApi.ts:104` + +```ts +`${API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path}` +``` + +The `path` variable is not URL-encoded. Characters like `#`, `?`, or `%` in the workflow name +would break the URL. This is partially mitigated by M-1's fix (restricting characters in the +name), but the path segments should also be encoded for correctness. + +**Fix:** Encode individual path segments of `path` while preserving `/` separators. + +--- + +### L-2: Token stored in plaintext localStorage [LOW] + +**File:** `src/stores/deployStore.ts:148-161` + +When "Remember this token" is checked, the PAT is persisted to `localStorage` under key +`gh-aw-deploy` in plaintext. Any XSS vulnerability in the page (or browser extensions with +page access) could exfiltrate the token. + +**Mitigating factors:** Opt-in only, React auto-escaping reduces XSS risk, and this is the +standard pattern for client-side token storage without a backend. + +**Recommendation:** Document the risk clearly to users. Consider adding a warning in the UI. +No code fix required. + +--- + +### L-3: Branch name validation incomplete [LOW] + +**File:** `src/components/Deploy/RepoStep.tsx:10` + +```ts +const BRANCH_INVALID = /(\.\.|[ ~^:\\]|\.lock$)/; +``` + +Missing checks: leading/trailing `/`, leading `-`, names starting with `.`, names containing +`@{`. These are rejected by git but would only fail at the GitHub API level, producing a +less clear error message. + +**Recommendation:** Add additional checks or improve the error message for API rejections. +Low priority since GitHub API enforces these rules. + +--- + +### L-4: GitHub API error messages may leak internal details [LOW] + +**File:** `src/utils/githubApi.ts:32-33` + +```ts +const msg = (body as { message?: string }).message || res.statusText; +``` + +Raw GitHub API error messages are captured. The `deploy.ts` error handler (lines 136-148) +already replaces messages for common status codes (401, 403, 404) with user-friendly text, +which is good. Remaining edge cases (422, 500, etc.) pass through the raw API message. + +**Mitigating factors:** GitHub API messages are generally safe for end users. +No code fix required. + +--- + +## Positive Findings + +| Area | Assessment | +|------|-----------| +| XSS prevention | React JSX auto-escaping used throughout. No `dangerouslySetInnerHTML`. | +| URL injection | `encodeURIComponent` on owner/repo in all API URLs. | +| Expression injection | `escapeGitHubExpressions()` neutralizes `${{ }}` in PR body. | +| Token lifecycle | 401 clears token automatically. `clearToken` resets all auth state. | +| Input validation | Repo slug regex and branch name checks prevent most bad inputs. | +| Error handling | User-friendly messages for common failures. No stack traces leaked. | +| Token input | Uses `type="password"` to prevent shoulder surfing. | + +--- + +## Recommended Fixes (ordered by priority) + +1. **M-1** — Sanitize `workflowName` in `deploy.ts` before path construction +2. **M-2** — Validate `prUrl` origin in `SuccessStep.tsx` +3. **L-1** — Encode path segments in `createOrUpdateFile` URL diff --git a/docs/editor-app/src/SECURITY-REVIEW.md b/docs/editor-app/src/SECURITY-REVIEW.md new file mode 100644 index 00000000000..b9efc0f8017 --- /dev/null +++ b/docs/editor-app/src/SECURITY-REVIEW.md @@ -0,0 +1,210 @@ +# Security Review: Deploy to GitHub Feature + +**Date**: 2026-02-24 +**Reviewer**: security-reviewer (automated audit) +**Scope**: `src/utils/githubApi.ts`, `src/stores/deployStore.ts`, `src/utils/deploy.ts`, `src/components/Deploy/*.tsx` + +--- + +## Summary + +The Deploy to GitHub feature allows users to authenticate with a GitHub Personal Access Token, select a repository, and create a pull request containing workflow files. Overall the implementation follows reasonable patterns, but there are several findings that should be addressed before production use. + +**Overall Risk**: Medium + +--- + +## Findings + +### 1. Token Persisted in localStorage by Default + +**Severity**: Medium +**File**: `src/stores/deployStore.ts:63` + +The `rememberToken` flag defaults to `true`, which means the PAT is silently persisted in `localStorage` (under key `gh-aw-deploy`) without the user explicitly opting in. On shared or public computers, this leaves the token accessible to anyone with physical access or any JavaScript running on the same origin. + +**Recommendation**: +- Change `rememberToken` default to `false` so users must explicitly opt in to persistence. +- Add a brief warning near the checkbox explaining that the token will be stored in the browser. + +--- + +### 2. Token Stored in Plain Text (XSS Exposure) + +**Severity**: Medium +**File**: `src/stores/deployStore.ts:107-116` + +When `rememberToken` is true, the raw PAT string is stored unencrypted in `localStorage`. Any XSS vulnerability on the same origin would allow an attacker to read `localStorage.getItem('gh-aw-deploy')` and extract the token. Since this is a GitHub Pages site, the attack surface includes any other code running on the `githubnext.github.io` origin. + +**Note**: Client-side encryption wouldn't provide real protection against XSS (since the decryption key must also be in JS). The primary mitigation is preventing XSS and minimizing persistence. + +**Recommendation**: +- Keep `rememberToken` off by default (see finding #1). +- Use `sessionStorage` instead of `localStorage` when `rememberToken` is false — currently the token is held only in Zustand memory, which is correct. Verify this stays the case. +- Add a "Clear saved token" option accessible from the UI at all times (not just during the auth step). +- Consider using the `partialize` function to exclude the token entirely and re-prompt each session, storing only the username for UX convenience. + +--- + +### 3. Inconsistent URL Encoding in API Client + +**Severity**: Medium +**File**: `src/utils/githubApi.ts:44-57, 88` + +`createOrUpdateFile` correctly uses `encodeURIComponent()` for `owner` and `repo` in the URL path (line 88), but `getRepo` (line 45), `getDefaultBranchSha` (line 57), `createBranch` (line 68), and `createPullRequest` (line 109) do **not** encode these parameters. If a user enters a malformed repo slug (e.g., containing `/`, `?`, `#`, or `..`), the resulting URL could resolve to an unintended API endpoint. + +**Example**: A repo slug of `owner/repo/../../orgs/secret-org` would produce an API call to an unintended endpoint. + +**Recommendation**: +- Apply `encodeURIComponent()` to all user-supplied path segments (`owner`, `repo`, `branch`) in every API function. +- Alternatively, add a single validation function for repo slug format (`^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$`) and call it before any API calls. + +--- + +### 4. No Client-Side Input Validation on Repo Slug + +**Severity**: Medium +**File**: `src/components/Deploy/RepoSelector.tsx:30`, `src/utils/deploy.ts:26` + +The deploy gate is simply `repoSlug.includes('/')` (RepoSelector.tsx:30), and the split is an unchecked `repoSlug.split('/')` (deploy.ts:26). This permits values like `/`, `a/b/c/d`, `../..`, or strings with special characters. These values propagate directly into GitHub API URLs. + +**Recommendation**: +- Validate the repo slug with a regex: `^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$` +- Show a validation error in the UI if the format doesn't match. +- Ensure `split('/')` produces exactly 2 non-empty segments before proceeding. + +--- + +### 5. Branch Name Not Validated + +**Severity**: Low +**File**: `src/components/Deploy/RepoSelector.tsx:20-27`, `src/utils/githubApi.ts:57,68` + +The auto-derived branch name sanitizes the workflow name well (line 21-25), but the user can freely edit it afterward. Branch names containing `..`, spaces, `~`, `^`, `:`, `\`, or leading dots are invalid in git but would be sent to the API, resulting in confusing error messages. More critically, special characters could alter the URL path in `getDefaultBranchSha`. + +**Recommendation**: +- Validate branch names against git ref rules (no `..`, no ASCII control chars, no `\`, no trailing `.lock`, etc.). +- Or at minimum, apply the same sanitization used for auto-generation when the user edits the field. + +--- + +### 6. PR Body Content Injection — GitHub Actions Expression Risk + +**Severity**: Medium +**File**: `src/utils/deploy.ts:87-113` + +The `buildPrBody` function extracts `description` and `trigger` from user-authored markdown via regex and interpolates them directly into the PR body. If the workflow markdown contains GitHub Actions expressions like `${{ github.event.issue.title }}` in its frontmatter fields, these strings end up verbatim in the PR body. + +While PR bodies themselves are not evaluated as Actions expressions, the pattern normalizes unsafe content in contexts that could be copy-pasted or referenced by workflows that read PR bodies (e.g., `${{ github.event.pull_request.body }}`). + +**Recommendation**: +- Escape or strip `${{ }}` patterns from extracted values before inserting into the PR body. +- A simple replacement: `value.replace(/\$\{\{/g, '`${{`')` would render them as code literals. + +--- + +### 7. Deprecated `unescape()` Usage + +**Severity**: Low +**File**: `src/utils/githubApi.ts:93` + +The expression `btoa(unescape(encodeURIComponent(content)))` uses the deprecated `unescape()` function. While this is a common pattern for UTF-8-safe base64 encoding and works correctly in all modern browsers, it may trigger linter warnings and could theoretically be removed from future browser standards. + +**Recommendation**: +- Replace with a `TextEncoder`-based approach: + ```ts + const bytes = new TextEncoder().encode(content); + const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join(''); + const base64 = btoa(binary); + ``` + +--- + +### 8. Error Messages — No Token Leakage + +**Severity**: Info (Positive Finding) +**File**: `src/utils/githubApi.ts:27-30`, `src/components/Deploy/TokenSetup.tsx:36-39` + +Error handling is safe. The `GitHubApiError` only includes `body.message` (from GitHub's API response) or `res.statusText` — never the token itself. The `TokenSetup` component shows generic error messages ("Invalid token", "GitHub returned {status}"). No token fragments leak into error UI or console. + +--- + +### 9. HTTPS-Only API Communication + +**Severity**: Info (Positive Finding) +**File**: `src/utils/githubApi.ts:1` + +`API_BASE` is hardcoded to `https://api.github.com`. Token is transmitted only in the `Authorization` header, never in URL query parameters. This prevents token leakage in browser history, server logs, or referrer headers. + +--- + +### 10. Token Scope Guidance + +**Severity**: Info +**File**: `src/components/Deploy/TokenSetup.tsx:83` + +The "Create a token" link requests `repo` and `workflow` scopes on classic PATs. These are broad scopes — `repo` grants full access to all repositories. + +**Recommendation**: +- Add guidance suggesting **fine-grained PATs** as the preferred option, scoped to specific repositories with only: + - `contents: write` (for pushing files) + - `pull_requests: write` (for creating PRs) + - `metadata: read` (for repo info) +- Keep classic PAT instructions as a fallback. + +--- + +### 11. No CORS / CSRF Concerns + +**Severity**: Info (Positive Finding) + +This is a static SPA making direct `fetch()` calls to `api.github.com`. GitHub's CORS policy permits cross-origin requests with `Authorization` headers. There is no server-side session or cookie-based auth, so CSRF is not applicable. + +--- + +### 12. `deployWorkflow` Not Yet Connected + +**Severity**: Info +**File**: `src/utils/deploy.ts:16` + +The `deployWorkflow` function is exported but not imported or called by any component. The `RepoSelector` transitions to the `'deploying'` step, but nothing triggers the actual API call sequence. This appears to be work-in-progress. When wiring this up, ensure errors are caught and don't leak token info, and that the deploy cannot be double-triggered. + +--- + +### 13. Shared Origin Risk (GitHub Pages) + +**Severity**: Low + +If deployed on `githubnext.github.io`, the app shares an origin with other projects under the `githubnext` org on GitHub Pages. Any XSS in a sibling project could potentially read this app's `localStorage`. This is a platform-level concern, not a code bug. + +**Recommendation**: +- Consider deploying on a custom domain for origin isolation. +- Or accept this risk given that `githubnext` controls all projects on that subdomain. + +--- + +## Dependencies Audit + +| Dependency | Version | Risk | +|---|---|---| +| `zustand` | ^5.0.11 | Low — well-maintained state library, `persist` middleware uses `localStorage` directly | +| `@radix-ui/react-dialog` | ^1.1.15 | Low — widely used, accessible dialog primitive | +| `lucide-react` | ^0.574.0 | Low — icon library, no security surface | +| `react` / `react-dom` | ^19.2.4 | Low — React auto-escapes JSX output, strong XSS protection | +| `sonner` | ^2.0.7 | Low — toast library | +| `vite` | ^7.3.1 (dev) | Low — build tool only | + +No high-risk dependencies identified. The app has a small dependency surface. + +--- + +## Recommended Mitigations (Priority Order) + +1. **[Medium]** Validate repo slug format before API calls (finding #3, #4) +2. **[Medium]** Apply `encodeURIComponent()` consistently across all API functions (finding #3) +3. **[Medium]** Default `rememberToken` to `false` (finding #1) +4. **[Medium]** Escape `${{ }}` in PR body content (finding #6) +5. **[Low]** Validate branch names client-side (finding #5) +6. **[Low]** Replace deprecated `unescape()` (finding #7) +7. **[Info]** Add fine-grained PAT guidance (finding #10) +8. **[Info]** Wire up `deployWorkflow` with error boundary and double-trigger protection (finding #12) diff --git a/docs/editor-app/src/components/Deploy/DeployDialog.tsx b/docs/editor-app/src/components/Deploy/DeployDialog.tsx new file mode 100644 index 00000000000..c592503ee2f --- /dev/null +++ b/docs/editor-app/src/components/Deploy/DeployDialog.tsx @@ -0,0 +1,114 @@ +import * as Dialog from '@radix-ui/react-dialog'; +import { X, AlertCircle } from 'lucide-react'; +import { useDeployStore } from '../../stores/deployStore'; +import { TokenStep } from './TokenStep'; +import { RepoStep } from './RepoStep'; +import { ProgressStep } from './ProgressStep'; +import { SuccessStep } from './SuccessStep'; + +const STEP_TITLES: Record = { + auth: 'Deploy to GitHub', + repo: 'Deploy to GitHub', + deploying: 'Deploying...', + success: 'Deployed!', + error: 'Deploy Failed', +}; + +export function DeployDialog() { + const isOpen = useDeployStore((s) => s.isOpen); + const step = useDeployStore((s) => s.step); + const error = useDeployStore((s) => s.error); + const closeDialog = useDeployStore((s) => s.closeDialog); + const isDeploying = useDeployStore((s) => s.isDeploying); + const setStep = useDeployStore((s) => s.setStep); + + const handleOpenChange = (open: boolean) => { + if (!open) { + if (isDeploying) { + if (window.confirm('Deploy is in progress. Closing will cancel the remaining steps. Already-uploaded files will remain on GitHub.')) { + closeDialog(); + } + } else { + closeDialog(); + } + } + }; + + return ( + + + + + {/* Header */} +
+ + {STEP_TITLES[step] || 'Deploy to GitHub'} + + + + +
+ + {/* Step content */} + {step === 'auth' && } + {step === 'repo' && } + {step === 'deploying' && } + {step === 'success' && } + {step === 'error' && ( +
+ +
+ + + {error} + +
+
+ + +
+
+ )} +
+
+
+ ); +} + +const overlayStyle: React.CSSProperties = { + position: 'fixed', inset: 0, + background: 'rgba(0, 0, 0, 0.4)', + zIndex: 1000, +}; + +const contentStyle: React.CSSProperties = { + position: 'fixed', + top: '50%', left: '50%', + transform: 'translate(-50%, -50%)', + width: 'min(480px, 90vw)', + maxHeight: '85vh', + overflow: 'auto', + padding: 24, + borderRadius: 12, + background: 'var(--color-bg-default, #ffffff)', + border: '1px solid var(--color-border-default, #d0d7de)', + boxShadow: '0 8px 30px rgba(0, 0, 0, 0.12)', + zIndex: 1001, +}; + +const primaryBtnStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', gap: 6, + padding: '8px 16px', fontSize: 13, fontWeight: 600, + border: 'none', borderRadius: 6, cursor: 'pointer', + background: 'var(--color-btn-primary-bg, #1f883d)', + color: '#ffffff', +}; + +const secondaryBtnStyle: React.CSSProperties = { + padding: '8px 16px', fontSize: 13, fontWeight: 500, + border: '1px solid var(--color-border-default, #d0d7de)', borderRadius: 6, + background: 'var(--color-bg-default, #ffffff)', + color: 'var(--color-fg-default, #1f2328)', cursor: 'pointer', +}; diff --git a/docs/editor-app/src/components/Deploy/ProgressStep.tsx b/docs/editor-app/src/components/Deploy/ProgressStep.tsx new file mode 100644 index 00000000000..5731aae0a7a --- /dev/null +++ b/docs/editor-app/src/components/Deploy/ProgressStep.tsx @@ -0,0 +1,46 @@ +import { CheckCircle2, Circle, Loader2, XCircle } from 'lucide-react'; +import { useDeployStore } from '../../stores/deployStore'; + +export function ProgressStep() { + const progress = useDeployStore((s) => s.progress); + + return ( +
+ {progress.map((step) => ( +
+ {step.status === 'done' && ( + + )} + {step.status === 'running' && ( + + )} + {step.status === 'pending' && ( + + )} + {step.status === 'error' && ( + + )} + + {step.label} + {step.status === 'running' && '...'} + +
+ ))} +
+ ); +} diff --git a/docs/editor-app/src/components/Deploy/RepoStep.tsx b/docs/editor-app/src/components/Deploy/RepoStep.tsx new file mode 100644 index 00000000000..617aa6a2d01 --- /dev/null +++ b/docs/editor-app/src/components/Deploy/RepoStep.tsx @@ -0,0 +1,203 @@ +import { useState, useMemo } from 'react'; +import { GitBranch, Loader2 } from 'lucide-react'; +import { useDeployStore } from '../../stores/deployStore'; +import { useWorkflowStore } from '../../stores/workflowStore'; +import { generateMarkdown } from '../../utils/markdownGenerator'; +import { compile, isCompilerReady } from '../../utils/compiler'; +import { runDeploy } from '../../utils/deploy'; + +const REPO_REGEX = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/; +const BRANCH_INVALID = /(\.\.|[ ~^:\\]|\.lock$)/; + +export function RepoStep() { + const store = useDeployStore(); + const { + username, + repoSlug, + branchName, + baseBranch, + setRepoSlug, + setBranchName, + setBaseBranch, + setStep, + closeDialog, + clearToken, + isDeploying, + setIsDeploying, + initProgress, + } = store; + + const workflowName = useWorkflowStore((s) => s.name); + const [deploying, setDeploying] = useState(false); + + // Auto-derive branch name from workflow name + const derivedBranch = useMemo(() => { + if (branchName) return branchName; + if (workflowName) return `aw/${workflowName}`; + return 'aw/workflow'; + }, [branchName, workflowName]); + + const repoValid = REPO_REGEX.test(repoSlug); + const branchValid = derivedBranch.length > 0 && !BRANCH_INVALID.test(derivedBranch); + const canDeploy = repoValid && branchValid && !deploying && !isDeploying; + + const handleDeploy = async () => { + if (!canDeploy) return; + setDeploying(true); + setIsDeploying(true); + + // Generate markdown RIGHT HERE from current workflow state + const wfState = useWorkflowStore.getState(); + const markdown = generateMarkdown(wfState); + + // Try to get compiled YAML + let yaml = wfState.compiledYaml || ''; + if (!yaml && isCompilerReady()) { + try { + const result = await Promise.race([ + compile(markdown), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Compile timeout')), 10000), + ), + ]); + yaml = result.yaml || ''; + } catch { + // Compile failed or timed out + } + } + + if (!yaml) { + useDeployStore.getState().setError('No compiled YAML available. Make sure your workflow compiles successfully first.'); + setDeploying(false); + return; + } + + if (!markdown) { + useDeployStore.getState().setError('No workflow markdown available.'); + setDeploying(false); + return; + } + + // Switch to progress view and initialize + const finalBranch = branchName || derivedBranch; + setBranchName(finalBranch); + initProgress(); + setStep('deploying'); + + // Run deploy with a fresh store reference + const currentStore = useDeployStore.getState(); + await runDeploy(currentStore, markdown, yaml, wfState.name || 'workflow'); + setDeploying(false); + }; + + return ( +
+ {/* Logged in as */} +
+ + Logged in as {username} + + +
+ + {/* Repository */} +
+ + setRepoSlug(e.target.value)} + placeholder="owner/repo" + data-testid="repo-input" + style={inputStyle} + /> + {repoSlug && !repoValid && ( +
Enter a valid repository in the format owner/repo.
+ )} +
+ + {/* Branch name */} +
+ + setBranchName(e.target.value)} + placeholder="aw/my-workflow" + data-testid="branch-input" + style={inputStyle} + /> + {(branchName || derivedBranch) && !branchValid && ( +
Invalid branch name.
+ )} +
+ + {/* Base branch */} +
+ + setBaseBranch(e.target.value)} + placeholder="main" + data-testid="base-branch-input" + style={inputStyle} + /> +
+ + {/* Actions */} +
+ + +
+
+ ); +} + +const labelStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', + fontSize: 12, fontWeight: 600, marginBottom: 4, + color: 'var(--color-fg-default, #1f2328)', +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '8px 12px', + fontSize: 13, + border: '1px solid var(--color-border-default, #d0d7de)', + borderRadius: 6, + background: 'var(--color-bg-default, #ffffff)', + color: 'var(--color-fg-default, #1f2328)', + outline: 'none', + boxSizing: 'border-box', +}; + +const errorHintStyle: React.CSSProperties = { + fontSize: 11, color: 'var(--color-danger-fg, #cf222e)', marginTop: 4, +}; + +const primaryBtnStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', gap: 6, + padding: '8px 16px', fontSize: 13, fontWeight: 600, + border: 'none', borderRadius: 6, cursor: 'pointer', + background: 'var(--color-btn-primary-bg, #1f883d)', + color: '#ffffff', +}; + +const secondaryBtnStyle: React.CSSProperties = { + padding: '8px 16px', fontSize: 13, fontWeight: 500, + border: '1px solid var(--color-border-default, #d0d7de)', borderRadius: 6, + background: 'var(--color-bg-default, #ffffff)', + color: 'var(--color-fg-default, #1f2328)', cursor: 'pointer', +}; diff --git a/docs/editor-app/src/components/Deploy/SuccessStep.tsx b/docs/editor-app/src/components/Deploy/SuccessStep.tsx new file mode 100644 index 00000000000..bdeae2d2a1c --- /dev/null +++ b/docs/editor-app/src/components/Deploy/SuccessStep.tsx @@ -0,0 +1,43 @@ +import { CheckCircle2, ExternalLink } from 'lucide-react'; +import { useDeployStore } from '../../stores/deployStore'; + +export function SuccessStep() { + const prUrl = useDeployStore((s) => s.prUrl); + const closeDialog = useDeployStore((s) => s.closeDialog); + + return ( +
+ ); +} diff --git a/docs/editor-app/src/components/Deploy/TokenStep.tsx b/docs/editor-app/src/components/Deploy/TokenStep.tsx new file mode 100644 index 00000000000..0edc496cf2c --- /dev/null +++ b/docs/editor-app/src/components/Deploy/TokenStep.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { KeyRound, ExternalLink, Loader2 } from 'lucide-react'; +import { useDeployStore } from '../../stores/deployStore'; +import { validateToken } from '../../utils/githubApi'; + +export function TokenStep() { + const setToken = useDeployStore((s) => s.setToken); + const setStep = useDeployStore((s) => s.setStep); + const rememberToken = useDeployStore((s) => s.rememberToken); + const setRememberToken = useDeployStore((s) => s.setRememberToken); + const closeDialog = useDeployStore((s) => s.closeDialog); + const storeError = useDeployStore((s) => s.error); + + const [input, setInput] = useState(''); + const [validating, setValidating] = useState(false); + const [error, setError] = useState(storeError); + + const handleContinue = async () => { + if (!input.trim()) return; + setValidating(true); + setError(null); + try { + const user = await validateToken(input.trim()); + setToken(input.trim(), user.login); + setStep('repo'); + } catch { + setError('Invalid token. Please check and try again.'); + } finally { + setValidating(false); + } + }; + + return ( +
+
+ + + To deploy workflows, you need a GitHub Personal Access Token with{' '} + repo and{' '} + workflow scopes. + +
+ + + Create a token on GitHub + + +
+ + setInput(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') handleContinue(); }} + placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + data-testid="token-input" + style={inputStyle} + autoFocus + /> +
+ + {error && ( +
+ {error} +
+ )} + + + +
+ + +
+
+ ); +} + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '8px 12px', + fontSize: 13, + fontFamily: 'monospace', + border: '1px solid var(--color-border-default, #d0d7de)', + borderRadius: 6, + background: 'var(--color-bg-default, #ffffff)', + color: 'var(--color-fg-default, #1f2328)', + outline: 'none', + boxSizing: 'border-box', +}; + +const primaryBtnStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', gap: 6, + padding: '8px 16px', fontSize: 13, fontWeight: 600, + border: 'none', borderRadius: 6, cursor: 'pointer', + background: 'var(--color-btn-primary-bg, #1f883d)', + color: '#ffffff', +}; + +const secondaryBtnStyle: React.CSSProperties = { + padding: '8px 16px', fontSize: 13, fontWeight: 500, + border: '1px solid var(--color-border-default, #d0d7de)', borderRadius: 6, + background: 'var(--color-bg-default, #ffffff)', + color: 'var(--color-fg-default, #1f2328)', cursor: 'pointer', +}; diff --git a/docs/editor-app/src/components/Header/ExportMenu.tsx b/docs/editor-app/src/components/Header/ExportMenu.tsx index 8965601ee97..bcce968e20e 100644 --- a/docs/editor-app/src/components/Header/ExportMenu.tsx +++ b/docs/editor-app/src/components/Header/ExportMenu.tsx @@ -1,7 +1,8 @@ import { useState, useRef, useEffect } from 'react'; -import { Download, Copy, ChevronDown, Link, FileText, AlertTriangle } from 'lucide-react'; +import { Download, Copy, ChevronDown, Link, FileText, AlertTriangle, Rocket } from 'lucide-react'; import { toast } from '../../utils/lazyToast'; import { useWorkflowStore } from '../../stores/workflowStore'; +import { useDeployStore } from '../../stores/deployStore'; import { encodeState } from '../../utils/deepLink'; function formatSize(bytes: number): string { @@ -168,6 +169,26 @@ export function ExportMenu() { {/* Divider */}
+ {/* Deploy section */} +
Deploy
+ + + {/* Divider */} +
+ {/* Share link */} - {/* Compile */} - - {/* New workflow */} + + ); } @@ -230,3 +236,4 @@ const viewToggleActiveStyle: React.CSSProperties = { fontWeight: 600, boxShadow: '0 1px 2px rgba(0,0,0,0.06)', }; + diff --git a/docs/editor-app/src/components/Panels/NetworkPanel.tsx b/docs/editor-app/src/components/Panels/NetworkPanel.tsx index e180903b639..3a5d68ee672 100644 --- a/docs/editor-app/src/components/Panels/NetworkPanel.tsx +++ b/docs/editor-app/src/components/Panels/NetworkPanel.tsx @@ -21,7 +21,6 @@ const ecosystemPresets: EcosystemPreset[] = [ { name: 'Rust', domains: ['rust'], description: 'Cargo crates and Rust ecosystem' }, { name: 'Ruby', domains: ['ruby'], description: 'RubyGems and Bundler' }, { name: 'Java', domains: ['java'], description: 'Maven Central and Gradle' }, - { name: 'Docker', domains: ['docker'], description: 'Docker Hub and registries' }, ]; export function NetworkPanel() { diff --git a/docs/editor-app/src/stores/deployStore.ts b/docs/editor-app/src/stores/deployStore.ts index 70d0353f3df..b69ffe2ac05 100644 --- a/docs/editor-app/src/stores/deployStore.ts +++ b/docs/editor-app/src/stores/deployStore.ts @@ -1,8 +1,6 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -export type DeployStep = 'auth' | 'repo' | 'deploying' | 'success' | 'error'; - export interface ProgressItem { id: string; label: string; @@ -10,95 +8,156 @@ export interface ProgressItem { error?: string; } +export type DeployStep = 'auth' | 'repo' | 'deploying' | 'success' | 'error'; + export interface DeployState { + // Auth token: string | null; username: string | null; + rememberToken: boolean; + + // Dialog isOpen: boolean; step: DeployStep; + + // Repo repoSlug: string; branchName: string; baseBranch: string; + + // Progress progress: ProgressItem[]; + + // Result prUrl: string | null; error: string | null; - rememberToken: boolean; - isValidatingToken: boolean; + + // Deploy in flight + isDeploying: boolean; } export interface DeployActions { openDialog: () => void; closeDialog: () => void; + setToken: (token: string, username: string) => void; + clearToken: () => void; + setRememberToken: (remember: boolean) => void; setStep: (step: DeployStep) => void; - setToken: (token: string | null) => void; - setUsername: (username: string | null) => void; setRepoSlug: (slug: string) => void; setBranchName: (name: string) => void; - setBaseBranch: (branch: string) => void; - setProgress: (progress: ProgressItem[]) => void; + setBaseBranch: (name: string) => void; + initProgress: () => void; updateProgress: (id: string, update: Partial) => void; - setPrUrl: (url: string | null) => void; - setError: (error: string | null) => void; - setRememberToken: (remember: boolean) => void; - setIsValidatingToken: (validating: boolean) => void; - reset: () => void; + setSuccess: (prUrl: string) => void; + setError: (error: string) => void; + setIsDeploying: (v: boolean) => void; + resetTransient: () => void; } -const initialState: DeployState = { - token: null, - username: null, +export type DeployStore = DeployState & DeployActions; + +const DEPLOY_STEPS: ProgressItem[] = [ + { id: 'verify', label: 'Verify repository access', status: 'pending' }, + { id: 'branch', label: 'Create branch', status: 'pending' }, + { id: 'upload-md', label: 'Upload workflow source', status: 'pending' }, + { id: 'upload-yml', label: 'Upload compiled YAML', status: 'pending' }, + { id: 'pr', label: 'Create pull request', status: 'pending' }, +]; + +const initialTransient = { isOpen: false, - step: 'auth', + step: 'auth' as DeployStep, repoSlug: '', branchName: '', baseBranch: 'main', - progress: [], + progress: DEPLOY_STEPS.map((s) => ({ ...s })), prUrl: null, error: null, - rememberToken: true, - isValidatingToken: false, + isDeploying: false, }; -export const useDeployStore = create()( +export const useDeployStore = create()( persist( (set) => ({ - ...initialState, + // Auth + token: null, + username: null, + rememberToken: false, - openDialog: () => set((s) => ({ - isOpen: true, - step: s.token && s.username ? 'repo' : 'auth', - error: null, - prUrl: null, - progress: [], - })), + // Transient + ...initialTransient, - closeDialog: () => set({ isOpen: false }), + openDialog: () => + set((state) => ({ + isOpen: true, + // If we have a saved token, skip to repo step + step: state.token ? 'repo' : 'auth', + progress: DEPLOY_STEPS.map((s) => ({ ...s })), + prUrl: null, + error: null, + isDeploying: false, + })), + + closeDialog: () => + set({ + isOpen: false, + progress: DEPLOY_STEPS.map((s) => ({ ...s })), + prUrl: null, + error: null, + isDeploying: false, + }), + + setToken: (token, username) => set({ token, username }), + + clearToken: () => + set({ token: null, username: null, rememberToken: false, step: 'auth' }), + + setRememberToken: (rememberToken) => set({ rememberToken }), setStep: (step) => set({ step }), - setToken: (token) => set({ token }), - setUsername: (username) => set({ username }), - setRepoSlug: (slug) => set({ repoSlug: slug }), - setBranchName: (name) => set({ branchName: name }), - setBaseBranch: (branch) => set({ baseBranch: branch }), - setProgress: (progress) => set({ progress }), + + setRepoSlug: (repoSlug) => set({ repoSlug }), + + setBranchName: (branchName) => set({ branchName }), + + setBaseBranch: (baseBranch) => set({ baseBranch }), + + initProgress: () => + set({ progress: DEPLOY_STEPS.map((s) => ({ ...s })) }), + updateProgress: (id, update) => - set((s) => ({ - progress: s.progress.map((p) => - p.id === id ? { ...p, ...update } : p + set((state) => ({ + progress: state.progress.map((p) => + p.id === id ? { ...p, ...update } : p, ), })), - setPrUrl: (url) => set({ prUrl: url }), - setError: (error) => set({ error }), - setRememberToken: (remember) => set({ rememberToken: remember }), - setIsValidatingToken: (validating) => set({ isValidatingToken: validating }), - reset: () => set({ ...initialState, isOpen: false }), + setSuccess: (prUrl) => set({ prUrl, step: 'success', isDeploying: false }), + + setError: (error) => set({ error, step: 'error', isDeploying: false }), + + setIsDeploying: (isDeploying) => set({ isDeploying }), + + resetTransient: () => + set((state) => ({ + ...initialTransient, + // Keep auth if remembered + step: state.token ? 'repo' : 'auth', + })), }), { - name: 'deploy-store', - partialize: (state) => - state.rememberToken - ? { token: state.token, username: state.username, rememberToken: state.rememberToken } - : { rememberToken: state.rememberToken }, - } - ) + name: 'gh-aw-deploy', + partialize: (state) => { + // Only persist token/username if rememberToken is true + if (state.rememberToken) { + return { + token: state.token, + username: state.username, + rememberToken: state.rememberToken, + }; + } + return {}; + }, + }, + ), ); diff --git a/docs/editor-app/src/styles/globals.css b/docs/editor-app/src/styles/globals.css index e9a114fede7..b6c0d533026 100644 --- a/docs/editor-app/src/styles/globals.css +++ b/docs/editor-app/src/styles/globals.css @@ -1071,3 +1071,58 @@ code, pre, .monospace { display: none; } } + +/* ============================================================ + Deploy Dialog Animations + ============================================================ */ +@keyframes deployOverlayIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes deployDialogIn { + from { + opacity: 0; + transform: translate(-50%, -48%); + } + to { + opacity: 1; + transform: translate(-50%, -50%); + } +} + +@keyframes deployCheckIn { + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes deployCheckScale { + from { + transform: scale(0.5); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes deployRingPulse { + 0% { + transform: scale(0.8); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + transform: scale(1.3); + opacity: 0; + } +} diff --git a/docs/editor-app/src/utils/deploy.ts b/docs/editor-app/src/utils/deploy.ts new file mode 100644 index 00000000000..8fa0f631015 --- /dev/null +++ b/docs/editor-app/src/utils/deploy.ts @@ -0,0 +1,162 @@ +import type { DeployStore } from '../stores/deployStore'; +import { + getRepo, + getDefaultBranchSha, + createBranch, + createOrUpdateFile, + createPullRequest, +} from './githubApi'; + +function escapeGitHubExpressions(text: string): string { + // Escape ${{ }} patterns to prevent injection in PR body + return text.replace(/\$\{\{/g, '`${{`').replace(/\}\}/g, '`}}`'); +} + +function sanitizeWorkflowName(name: string): string { + // Strip path traversal and restrict to safe filename characters + return name + .replace(/\.\./g, '') + .replace(/[/\\]/g, '') + .replace(/[^a-zA-Z0-9._-]/g, '-') + .replace(/^[.-]+/, '') // no leading dots or dashes + .slice(0, 100) // reasonable length limit + || 'workflow'; // fallback if empty after sanitization +} + +export async function runDeploy( + store: DeployStore, + markdown: string, + yaml: string, + workflowName: string, +): Promise { + const { token, repoSlug, branchName, baseBranch } = store; + if (!token) { + store.setError('No token available'); + return; + } + + const parts = repoSlug.split('/'); + if (parts.length !== 2) { + store.setError('Invalid repository slug'); + return; + } + const [owner, repo] = parts; + const safeName = sanitizeWorkflowName(workflowName); + const mdPath = `.github/workflows/${safeName}.md`; + const ymlPath = `.github/workflows/${safeName}.lock.yml`; + + try { + // Step 1: Verify repo access + store.updateProgress('verify', { status: 'running' }); + const repoData = await getRepo(token, owner, repo); + if (!repoData.permissions?.push) { + throw new Error( + `You don't have push permission to ${owner}/${repo}. Ensure your token has the 'repo' scope.`, + ); + } + store.updateProgress('verify', { status: 'done' }); + + // Step 2: Create branch + store.updateProgress('branch', { status: 'running' }); + const actualBase = baseBranch || repoData.default_branch; + const sha = await getDefaultBranchSha(token, owner, repo, actualBase); + try { + await createBranch(token, owner, repo, branchName, sha); + } catch (err) { + const e = err as Error & { status?: number }; + if (e.status === 422) { + throw new Error( + `Branch '${branchName}' already exists. Change the branch name or delete the existing branch on GitHub.`, + ); + } + throw err; + } + store.updateProgress('branch', { status: 'done' }); + + // Step 3: Upload .md file + store.updateProgress('upload-md', { status: 'running' }); + await createOrUpdateFile( + token, + owner, + repo, + mdPath, + markdown, + `Add agentic workflow source: ${safeName}`, + branchName, + ); + store.updateProgress('upload-md', { status: 'done' }); + + // Step 4: Upload .lock.yml file + store.updateProgress('upload-yml', { status: 'running' }); + await createOrUpdateFile( + token, + owner, + repo, + ymlPath, + yaml, + `Add compiled workflow: ${safeName}`, + branchName, + ); + store.updateProgress('upload-yml', { status: 'done' }); + + // Step 5: Create PR + store.updateProgress('pr', { status: 'running' }); + const safeDesc = escapeGitHubExpressions( + `This PR adds the **${safeName}** agentic workflow.`, + ); + const prBody = [ + `## Add Agentic Workflow: ${escapeGitHubExpressions(safeName)}`, + '', + safeDesc, + '', + '### Files', + `- \`${mdPath}\` -- Workflow source (edit this)`, + `- \`${ymlPath}\` -- Compiled output (auto-generated)`, + '', + '### Next Steps', + '1. Review the workflow configuration', + '2. Merge this PR to activate the workflow', + '', + '---', + '*Deployed from [gh-aw Visual Editor](https://mossaka.github.io/gh-aw-editor-visualizer/)*', + ].join('\n'); + + const pr = await createPullRequest( + token, + owner, + repo, + `Add Agentic Workflow: ${safeName}`, + branchName, + actualBase, + prBody, + ); + store.updateProgress('pr', { status: 'done' }); + store.setSuccess(pr.html_url); + } catch (err) { + const e = err as Error & { status?: number }; + // Mark the currently running step as error + const currentProgress = store.progress; + const runningStep = currentProgress.find((p) => p.status === 'running'); + if (runningStep) { + store.updateProgress(runningStep.id, { + status: 'error', + error: e.message, + }); + } + + // Build user-friendly error message + let msg = e.message || 'An unexpected error occurred'; + if (e.status === 401) { + msg = 'Your token is invalid or expired. Please enter a new one.'; + store.clearToken(); + } else if (e.status === 403) { + msg = `Your token doesn't have permission to push to this repository. Ensure your token has 'repo' and 'workflow' scopes.`; + } else if (e.status === 404) { + msg = `Repository '${owner}/${repo}' not found. Check the name and your token's access.`; + } else if (!e.status) { + msg = 'Network error -- check your internet connection and try again.'; + } + + store.setError(msg); + } +} diff --git a/docs/editor-app/src/utils/githubApi.ts b/docs/editor-app/src/utils/githubApi.ts index aac5554a642..668bf5581bf 100644 --- a/docs/editor-app/src/utils/githubApi.ts +++ b/docs/editor-app/src/utils/githubApi.ts @@ -1,48 +1,53 @@ const API_BASE = 'https://api.github.com'; -export class GitHubApiError extends Error { - constructor( - public status: number, - message: string, - ) { - super(message); - this.name = 'GitHubApiError'; - } +function headers(token: string): HeadersInit { + return { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + }; } -async function githubFetch( - token: string, - endpoint: string, - options?: RequestInit, -): Promise { - const res = await fetch(`${API_BASE}${endpoint}`, { - ...options, - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.github+json', - 'Content-Type': 'application/json', - ...options?.headers, - }, +export interface GitHubUser { + login: string; + avatar_url: string; +} + +export interface GitHubRepo { + default_branch: string; + permissions: { push: boolean }; +} + +export interface GitHubPR { + html_url: string; + number: number; +} + +async function ghFetch(url: string, token: string, init?: RequestInit): Promise { + const res = await fetch(url, { + ...init, + headers: { ...headers(token), ...(init?.headers || {}) }, }); if (!res.ok) { const body = await res.json().catch(() => ({})); - throw new GitHubApiError(res.status, body.message || res.statusText); + const msg = (body as { message?: string }).message || res.statusText; + const err = new Error(msg); + (err as Error & { status: number }).status = res.status; + throw err; } - return res.json(); + return res; } -export async function validateToken( - token: string, -): Promise<{ login: string; avatar_url: string }> { - return githubFetch(token, '/user'); +export async function validateToken(token: string): Promise { + const res = await ghFetch(`${API_BASE}/user`, token); + return res.json(); } -export async function getRepo( - token: string, - owner: string, - repo: string, -): Promise<{ default_branch: string; permissions: { push: boolean } }> { - return githubFetch(token, `/repos/${owner}/${repo}`); +export async function getRepo(token: string, owner: string, repo: string): Promise { + const res = await ghFetch( + `${API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, + token, + ); + return res.json(); } export async function getDefaultBranchSha( @@ -51,11 +56,13 @@ export async function getDefaultBranchSha( repo: string, branch: string, ): Promise { - const ref = await githubFetch( + // Branch paths with / must NOT be encoded — GitHub API expects literal slashes + const res = await ghFetch( + `${API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/ref/heads/${branch}`, token, - `/repos/${owner}/${repo}/git/ref/heads/${branch}`, ); - return ref.object.sha; + const data = await res.json(); + return (data as { object: { sha: string } }).object.sha; } export async function createBranch( @@ -65,13 +72,15 @@ export async function createBranch( branchName: string, sha: string, ): Promise { - await githubFetch(token, `/repos/${owner}/${repo}/git/refs`, { - method: 'POST', - body: JSON.stringify({ - ref: `refs/heads/${branchName}`, - sha, - }), - }); + await ghFetch( + `${API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/refs`, + token, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha }), + }, + ); } export async function createOrUpdateFile( @@ -83,16 +92,22 @@ export async function createOrUpdateFile( message: string, branch: string, ): Promise { - await githubFetch( + // Use TextEncoder for proper UTF-8 base64 encoding (no deprecated unescape) + const bytes = new TextEncoder().encode(content); + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + const b64 = btoa(binary); + + const encodedPath = path.split('/').map(encodeURIComponent).join('/'); + await ghFetch( + `${API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodedPath}`, token, - `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path}`, { method: 'PUT', - body: JSON.stringify({ - message, - content: btoa(unescape(encodeURIComponent(content))), - branch, - }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message, content: b64, branch }), }, ); } @@ -105,9 +120,15 @@ export async function createPullRequest( head: string, base: string, body: string, -): Promise<{ html_url: string; number: number }> { - return githubFetch(token, `/repos/${owner}/${repo}/pulls`, { - method: 'POST', - body: JSON.stringify({ title, head, base, body }), - }); +): Promise { + const res = await ghFetch( + `${API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, + token, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, head, base, body }), + }, + ); + return res.json(); } diff --git a/docs/editor-app/src/utils/validation.ts b/docs/editor-app/src/utils/validation.ts index 572a3a10675..dc12aee0614 100644 --- a/docs/editor-app/src/utils/validation.ts +++ b/docs/editor-app/src/utils/validation.ts @@ -25,7 +25,7 @@ function isValidCron(s: string): boolean { // Includes ecosystem identifiers required by strict mode const DOMAIN_KEYWORDS = [ 'defaults', 'all', 'none', - 'python', 'node', 'go', 'rust', 'ruby', 'java', 'docker', + 'python', 'node', 'go', 'rust', 'ruby', 'java', 'dotnet', 'elixir', 'haskell', 'swift', 'php', 'perl', ]; diff --git a/docs/public/editor/assets/CanvasWithProvider-16gt3nDF.js b/docs/public/editor/assets/CanvasWithProvider-16gt3nDF.js new file mode 100644 index 00000000000..15b1bca92c4 --- /dev/null +++ b/docs/public/editor/assets/CanvasWithProvider-16gt3nDF.js @@ -0,0 +1,3 @@ +import{r as p,j as e}from"./react-vendor-D4h_eraO.js";import{H as b,P as y,u as E,a as R,i as T,B,b as O,C as q,M as L,d as _,R as A}from"./flow-vendor-BeACmQQ2.js";import{c as W,u,F as z,C as P,S as G,b as N}from"./index-7l71UumV.js";import{B as M,a as F,W as D,S as H,G as $,L as U}from"./wrench-BtizfR87.js";import{L as V}from"./layout-template-DawlbNTY.js";import"./radix-vendor-B-4BPIkP.js";const Z=[["rect",{width:"8",height:"8",x:"3",y:"3",rx:"2",key:"by2w9f"}],["path",{d:"M7 11v4a2 2 0 0 0 2 2h4",key:"xkn7yn"}],["rect",{width:"8",height:"8",x:"13",y:"13",rx:"2",key:"1cgmvn"}]],Y=W("workflow",Z),J=p.memo(function({count:n}){return n===0?null:e.jsx("span",{style:K,children:n})}),K={display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:"18px",height:"18px",padding:"0 5px",borderRadius:"9px",fontSize:"11px",fontWeight:600,color:"#ffffff",backgroundColor:"#d4a72c",flexShrink:0},g=p.memo(function({type:n,icon:t,title:o,selected:i=!1,dimmed:r=!1,children:l}){const a=u(h=>h.errorNodeIds)??[],d=u(h=>h.validationErrors)??[],c=u(h=>h.lintResults)??[],w=a.includes(n),f=d.filter(h=>h.nodeId===n).length,k=c.filter(h=>h.nodeId===n).length,j=["workflow-node",`node-${n}`,i?"selected":"",r?"dimmed":"",w?"has-error":""].filter(Boolean).join(" ");return e.jsxs(e.Fragment,{children:[e.jsx(b,{type:"target",position:y.Top}),e.jsxs("div",{className:j,"data-tour-target":n,children:[e.jsxs("div",{className:"workflow-node__header",children:[e.jsx("div",{className:"workflow-node__icon",children:t}),e.jsx("div",{className:"workflow-node__title",children:o}),f>0&&e.jsx("span",{style:Q,children:f}),f===0&&e.jsx(J,{count:k})]}),e.jsx("div",{className:"workflow-node__divider"}),e.jsx("div",{className:"workflow-node__content",children:l})]}),e.jsx(b,{type:"source",position:y.Bottom})]})}),Q={display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:"18px",height:"18px",padding:"0 5px",borderRadius:"9px",fontSize:"11px",fontWeight:600,color:"#ffffff",backgroundColor:"#cf222e",marginLeft:"auto",flexShrink:0},X={issues:"Issues",pull_request:"Pull Request",issue_comment:"Comment",push:"Code Push",schedule:"Schedule",workflow_dispatch:"Manual Trigger",slash_command:"Slash Command",release:"Release",discussion:"Discussion",pull_request_review:"PR Review",pull_request_review_comment:"PR Review Comment",discussion_comment:"Discussion Comment"},ee=p.memo(function({data:n,selected:t}){const o=u(d=>d.trigger),r=u(d=>d.selectedNodeId)!==null&&!t,l=o.event?X[o.event]||o.event:"",a=o.activityTypes.length>0?o.activityTypes.join(", "):"";return e.jsx(g,{type:"trigger",icon:e.jsx(M,{size:18}),title:n.label,selected:t,dimmed:r,children:o.event?e.jsxs(e.Fragment,{children:[e.jsx("div",{children:l}),a&&e.jsx("div",{children:a}),o.skipBots&&e.jsx("div",{children:"Skip bots: Yes"})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to choose a trigger"})})}),se={claude:{name:"Claude",tagline:"Best for complex reasoning"},copilot:{name:"GitHub Copilot",tagline:"Great for GitHub integration"},codex:{name:"OpenAI Codex",tagline:"Good for code editing tasks"},custom:{name:"Custom",tagline:"Bring your own engine"}},te=p.memo(function({data:n,selected:t}){const o=u(a=>a.engine),r=u(a=>a.selectedNodeId)!==null&&!t,l=o.type?se[o.type]:null;return e.jsx(g,{type:"engine",icon:e.jsx(F,{size:18}),title:n.label,selected:t,dimmed:r,children:l?e.jsxs(e.Fragment,{children:[e.jsx("div",{style:{fontWeight:600},children:l.name}),e.jsx("div",{children:l.tagline})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to choose an AI"})})}),oe=p.memo(function({data:n,selected:t}){const o=u(l=>l.tools),r=u(l=>l.selectedNodeId)!==null&&!t;return e.jsx(g,{type:"tools",icon:e.jsx(D,{size:18}),title:n.label,selected:t,dimmed:r,children:o.length>0?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"workflow-node__chips",children:o.map(l=>e.jsx("span",{className:"workflow-node__chip",children:l},l))}),e.jsxs("div",{className:"workflow-node__summary",children:[o.length," tool",o.length!==1?"s":""," enabled"]})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to add tools"})})}),ne=p.memo(function({data:n,selected:t}){const o=u(c=>c.instructions),r=u(c=>c.selectedNodeId)!==null&&!t,l=o.split(` +`).filter(c=>c.trim()),a=l.slice(0,3).join(` +`),d=l.length>3;return e.jsx(g,{type:"instructions",icon:e.jsx(z,{size:18}),title:n.label,selected:t,dimmed:r,children:o?e.jsx(e.Fragment,{children:e.jsxs("div",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word"},children:[a,d&&"..."]})}):e.jsx("span",{className:"workflow-node__cta",children:"Click to write instructions"})})}),ie={"add-comment":"Post comments","create-issue":"Create issues","update-issue":"Edit issues","create-pull-request":"Create pull requests","submit-pull-request-review":"Submit PR reviews","add-labels":"Add labels","remove-labels":"Remove labels","add-reviewer":"Request reviewers","assign-to-user":"Assign to person","close-issue":"Close issues","close-pull-request":"Close pull requests","create-discussion":"Create discussions","update-release":"Edit releases","push-to-pull-request-branch":"Push code to PR","create-pull-request-review-comment":"Review code","dispatch-workflow":"Trigger workflows","upload-asset":"Upload files"},re=p.memo(function({data:n,selected:t}){const o=u(d=>d.safeOutputs),r=u(d=>d.selectedNodeId)!==null&&!t,l=Object.entries(o).filter(([,d])=>d?.enabled),a=l.length;return e.jsx(g,{type:"safeOutputs",icon:e.jsx(H,{size:18}),title:n.label,selected:t,dimmed:r,children:a>0?e.jsxs(e.Fragment,{children:[l.slice(0,4).map(([d])=>e.jsxs("div",{className:"workflow-node__check-item",children:[e.jsx(P,{size:14}),e.jsx("span",{children:ie[d]||d})]},d)),e.jsxs("div",{className:"workflow-node__summary",children:[a," action",a!==1?"s":""," enabled"]})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to enable outputs"})})}),le=p.memo(function({data:n,selected:t}){const o=u(c=>c.network),r=u(c=>c.selectedNodeId)!==null&&!t,l=o.allowed.length,a=o.blocked.length,d=l+a;return e.jsx(g,{type:"network",icon:e.jsx($,{size:18}),title:n.label,selected:t,dimmed:r,children:d>0?e.jsxs(e.Fragment,{children:[l>0&&e.jsxs("div",{children:[l," allowed domain",l!==1?"s":""]}),a>0&&e.jsxs("div",{children:[a," blocked domain",a!==1?"s":""]})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to configure network"})})}),de=p.memo(function({data:n,selected:t}){const o=u(c=>c.concurrency)??{group:""},i=u(c=>c.rateLimit)??{max:"",window:""},r=u(c=>c.platform)??"",a=u(c=>c.selectedNodeId)!==null&&!t,d=[];return r&&d.push(r),o.group&&d.push("Concurrency group set"),i.max&&d.push(`Rate limit: ${i.max}/${i.window||"?"}`),e.jsx(g,{type:"settings",icon:e.jsx(G,{size:18}),title:n.label,selected:t,dimmed:a,children:d.length>0?d.map((c,w)=>e.jsx("div",{children:c},w)):e.jsx("span",{className:"workflow-node__cta",children:"Click to configure settings"})})}),ae=p.memo(function({data:n,selected:t}){const i=u(r=>r.selectedNodeId)!==null&&!t;return e.jsx(g,{type:"steps",icon:e.jsx(U,{size:18}),title:n.label,selected:t,dimmed:i,children:e.jsx("span",{className:"workflow-node__cta",children:"Click to add custom steps"})})});function ce(){const s=N(i=>i.setSidebarTab),n=N(i=>i.sidebarOpen),t=N(i=>i.toggleSidebar),o=()=>{s("templates"),n||t()};return e.jsxs("div",{className:"canvas-empty-state",children:[e.jsx(Y,{className:"canvas-empty-state__icon",size:64}),e.jsx("h2",{className:"canvas-empty-state__title",children:"Build your workflow"}),e.jsx("p",{className:"canvas-empty-state__description",children:"Get started by choosing a template from the sidebar, or add blocks to build your workflow step by step."}),e.jsxs("button",{className:"canvas-empty-state__cta",onClick:o,children:[e.jsx(V,{size:14}),"Choose a template"]})]})}const ue={trigger:ee,engine:te,tools:oe,instructions:ne,safeOutputs:re,network:le,settings:de,steps:ae},C=260,v=120,me=[{id:"trigger",type:"trigger",label:"When this happens",description:"Choose what starts this workflow",isRequired:!0,isConfigured:s=>s.trigger.event!==""},{id:"engine",type:"engine",label:"AI Assistant",description:"Choose which AI runs this",isRequired:!0,isConfigured:s=>s.engine.type!==""},{id:"tools",type:"tools",label:"Agent Tools",description:"What tools the agent can use",isRequired:!1,isConfigured:s=>s.tools.length>0},{id:"instructions",type:"instructions",label:"Instructions",description:"Tell the agent what to do",isRequired:!0,isConfigured:s=>s.instructions.trim().length>0},{id:"safeOutputs",type:"safeOutputs",label:"What it can do",description:"Actions the agent can take",isRequired:!1,isConfigured:s=>Object.keys(s.safeOutputs).length>0},{id:"network",type:"network",label:"Network Access",description:"Allowed network domains",isRequired:!1,isConfigured:s=>s.network.allowed.length>0||s.network.blocked.length>0},{id:"settings",type:"settings",label:"Settings",description:"Concurrency, rate limits, platform",isRequired:!1,isConfigured:s=>s.platform!==""||(s.concurrency?.group??"")!==""||(s.concurrency?.cancelInProgress??!1)||(s.rateLimit?.max??"")!==""||(s.rateLimit?.window??"")!==""},{id:"steps",type:"steps",label:"Custom Steps",description:"Additional workflow steps",isRequired:!1,isConfigured:()=>!1}];function pe(s,n){const t=new _.graphlib.Graph;return t.setDefaultEdgeLabel(()=>({})),t.setGraph({rankdir:"TB",nodesep:40,ranksep:60}),s.forEach(i=>{t.setNode(i.id,{width:C,height:v})}),n.forEach(i=>{t.setEdge(i.source,i.target)}),_.layout(t),{nodes:s.map(i=>{const r=t.node(i.id);return{...i,position:{x:r.x-C/2,y:r.y-v/2}}}),edges:n}}function he(){const s=u(),n=u(m=>m.selectNode),t=N(m=>m.theme),o=t==="dark"||t==="auto"&&typeof window<"u"&&window.matchMedia("(prefers-color-scheme: dark)").matches,i=o?"dark":"light",r=p.useMemo(()=>me.filter(m=>m.isRequired||m.isConfigured(s)),[s]),l=p.useMemo(()=>r.map(m=>({id:m.id,type:m.type,position:{x:0,y:0},data:{label:m.label,type:m.type,description:m.description,isConfigured:m.isConfigured(s)}})),[r,s]),a=p.useMemo(()=>r.slice(1).map((m,x)=>({id:`e-${r[x].id}-${m.id}`,source:r[x].id,target:m.id,animated:!0,style:{stroke:"var(--borderColor-default, #d1d9e0)"}})),[r]),{nodes:d,edges:c}=p.useMemo(()=>pe(l,a),[l,a]),[w,f,k]=E(d),[j,,h]=R(c);p.useMemo(()=>{f(d)},[d,f]);const I=p.useCallback((m,x)=>{n(x.id)},[n]),S=p.useCallback(()=>{n(null)},[n]);return r.length===0?e.jsx(ce,{}):e.jsxs(T,{nodes:w,edges:j,onNodesChange:k,onEdgesChange:h,onNodeClick:I,onPaneClick:S,nodeTypes:ue,colorMode:i,fitView:!0,fitViewOptions:{padding:.2},minZoom:.3,maxZoom:1.5,proOptions:{hideAttribution:!0},children:[e.jsx(B,{variant:O.Dots,gap:16,size:1,color:o?"#30363d":void 0}),e.jsx(q,{position:"bottom-left"}),e.jsx(L,{position:"bottom-left",style:{marginBottom:50},nodeStrokeWidth:3,maskColor:o?"rgba(0, 0, 0, 0.6)":void 0,pannable:!0,zoomable:!0})]})}function je(){return e.jsx(A,{children:e.jsx(he,{})})}export{je as default}; diff --git a/docs/public/editor/assets/CanvasWithProvider-Dyo1Jspo.js b/docs/public/editor/assets/CanvasWithProvider-Dyo1Jspo.js deleted file mode 100644 index 14da4757809..00000000000 --- a/docs/public/editor/assets/CanvasWithProvider-Dyo1Jspo.js +++ /dev/null @@ -1,3 +0,0 @@ -import{r as p,j as e}from"./react-vendor-D4h_eraO.js";import{H as j,P as b,u as v,a as I,i as S,B as E,b as R,C as T,M as B,d as y,R as O}from"./flow-vendor-BeACmQQ2.js";import{c as q,u as m,F as L,S as A,b as k}from"./index-D2nmaYOD.js";import{B as W,a as z,W as P,C as G,S as F,G as M,L as D}from"./wrench-C08fmUNq.js";import{L as H}from"./layout-template-dsQmQ2fv.js";const $=[["rect",{width:"8",height:"8",x:"3",y:"3",rx:"2",key:"by2w9f"}],["path",{d:"M7 11v4a2 2 0 0 0 2 2h4",key:"xkn7yn"}],["rect",{width:"8",height:"8",x:"13",y:"13",rx:"2",key:"1cgmvn"}]],U=q("workflow",$),V=p.memo(function({count:o}){return o===0?null:e.jsx("span",{style:Z,children:o})}),Z={display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:"18px",height:"18px",padding:"0 5px",borderRadius:"9px",fontSize:"11px",fontWeight:600,color:"#ffffff",backgroundColor:"#d4a72c",flexShrink:0},g=p.memo(function({type:o,icon:s,title:n,selected:i=!1,dimmed:d=!1,children:r}){const c=m(a=>a.errorNodeIds)??[],l=m(a=>a.validationErrors)??[],u=m(a=>a.lintResults)??[],h=c.includes(o),f=l.filter(a=>a.nodeId===o).length,x=u.filter(a=>a.nodeId===o).length,N=["workflow-node",`node-${o}`,i?"selected":"",d?"dimmed":"",h?"has-error":""].filter(Boolean).join(" ");return e.jsxs(e.Fragment,{children:[e.jsx(j,{type:"target",position:b.Top}),e.jsxs("div",{className:N,"data-tour-target":o,children:[e.jsxs("div",{className:"workflow-node__header",children:[e.jsx("div",{className:"workflow-node__icon",children:s}),e.jsx("div",{className:"workflow-node__title",children:n}),f>0&&e.jsx("span",{style:Y,children:f}),f===0&&e.jsx(V,{count:x})]}),e.jsx("div",{className:"workflow-node__divider"}),e.jsx("div",{className:"workflow-node__content",children:r})]}),e.jsx(j,{type:"source",position:b.Bottom})]})}),Y={display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:"18px",height:"18px",padding:"0 5px",borderRadius:"9px",fontSize:"11px",fontWeight:600,color:"#ffffff",backgroundColor:"#cf222e",marginLeft:"auto",flexShrink:0},J={issues:"Issues",pull_request:"Pull Request",issue_comment:"Comment",push:"Code Push",schedule:"Schedule",workflow_dispatch:"Manual Trigger",slash_command:"Slash Command",release:"Release",discussion:"Discussion",pull_request_review:"PR Review",pull_request_review_comment:"PR Review Comment",discussion_comment:"Discussion Comment"},K=p.memo(function({data:o,selected:s}){const n=m(l=>l.trigger),d=m(l=>l.selectedNodeId)!==null&&!s,r=n.event?J[n.event]||n.event:"",c=n.activityTypes.length>0?n.activityTypes.join(", "):"";return e.jsx(g,{type:"trigger",icon:e.jsx(W,{size:18}),title:o.label,selected:s,dimmed:d,children:n.event?e.jsxs(e.Fragment,{children:[e.jsx("div",{children:r}),c&&e.jsx("div",{children:c}),n.skipBots&&e.jsx("div",{children:"Skip bots: Yes"})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to choose a trigger"})})}),Q={claude:{name:"Claude",tagline:"Best for complex reasoning"},copilot:{name:"GitHub Copilot",tagline:"Great for GitHub integration"},codex:{name:"OpenAI Codex",tagline:"Good for code editing tasks"},custom:{name:"Custom",tagline:"Bring your own engine"}},X=p.memo(function({data:o,selected:s}){const n=m(c=>c.engine),d=m(c=>c.selectedNodeId)!==null&&!s,r=n.type?Q[n.type]:null;return e.jsx(g,{type:"engine",icon:e.jsx(z,{size:18}),title:o.label,selected:s,dimmed:d,children:r?e.jsxs(e.Fragment,{children:[e.jsx("div",{style:{fontWeight:600},children:r.name}),e.jsx("div",{children:r.tagline})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to choose an AI"})})}),ee=p.memo(function({data:o,selected:s}){const n=m(r=>r.tools),d=m(r=>r.selectedNodeId)!==null&&!s;return e.jsx(g,{type:"tools",icon:e.jsx(P,{size:18}),title:o.label,selected:s,dimmed:d,children:n.length>0?e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"workflow-node__chips",children:n.map(r=>e.jsx("span",{className:"workflow-node__chip",children:r},r))}),e.jsxs("div",{className:"workflow-node__summary",children:[n.length," tool",n.length!==1?"s":""," enabled"]})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to add tools"})})}),se=p.memo(function({data:o,selected:s}){const n=m(u=>u.instructions),d=m(u=>u.selectedNodeId)!==null&&!s,r=n.split(` -`).filter(u=>u.trim()),c=r.slice(0,3).join(` -`),l=r.length>3;return e.jsx(g,{type:"instructions",icon:e.jsx(L,{size:18}),title:o.label,selected:s,dimmed:d,children:n?e.jsx(e.Fragment,{children:e.jsxs("div",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word"},children:[c,l&&"..."]})}):e.jsx("span",{className:"workflow-node__cta",children:"Click to write instructions"})})}),te={"add-comment":"Post comments","create-issue":"Create issues","update-issue":"Edit issues","create-pull-request":"Create pull requests","submit-pull-request-review":"Submit PR reviews","add-labels":"Add labels","remove-labels":"Remove labels","add-reviewer":"Request reviewers","assign-to-user":"Assign to person","close-issue":"Close issues","close-pull-request":"Close pull requests","create-discussion":"Create discussions","update-release":"Edit releases","push-to-pull-request-branch":"Push code to PR","create-pull-request-review-comment":"Review code","dispatch-workflow":"Trigger workflows","upload-asset":"Upload files"},oe=p.memo(function({data:o,selected:s}){const n=m(l=>l.safeOutputs),d=m(l=>l.selectedNodeId)!==null&&!s,r=Object.entries(n).filter(([,l])=>l?.enabled),c=r.length;return e.jsx(g,{type:"safeOutputs",icon:e.jsx(F,{size:18}),title:o.label,selected:s,dimmed:d,children:c>0?e.jsxs(e.Fragment,{children:[r.slice(0,4).map(([l])=>e.jsxs("div",{className:"workflow-node__check-item",children:[e.jsx(G,{size:14}),e.jsx("span",{children:te[l]||l})]},l)),e.jsxs("div",{className:"workflow-node__summary",children:[c," action",c!==1?"s":""," enabled"]})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to enable outputs"})})}),ne=p.memo(function({data:o,selected:s}){const n=m(u=>u.network),d=m(u=>u.selectedNodeId)!==null&&!s,r=n.allowed.length,c=n.blocked.length,l=r+c;return e.jsx(g,{type:"network",icon:e.jsx(M,{size:18}),title:o.label,selected:s,dimmed:d,children:l>0?e.jsxs(e.Fragment,{children:[r>0&&e.jsxs("div",{children:[r," allowed domain",r!==1?"s":""]}),c>0&&e.jsxs("div",{children:[c," blocked domain",c!==1?"s":""]})]}):e.jsx("span",{className:"workflow-node__cta",children:"Click to configure network"})})}),ie=p.memo(function({data:o,selected:s}){const n=m(u=>u.concurrency)??{group:""},i=m(u=>u.rateLimit)??{max:"",window:""},d=m(u=>u.platform)??"",c=m(u=>u.selectedNodeId)!==null&&!s,l=[];return d&&l.push(d),n.group&&l.push("Concurrency group set"),i.max&&l.push(`Rate limit: ${i.max}/${i.window||"?"}`),e.jsx(g,{type:"settings",icon:e.jsx(A,{size:18}),title:o.label,selected:s,dimmed:c,children:l.length>0?l.map((u,h)=>e.jsx("div",{children:u},h)):e.jsx("span",{className:"workflow-node__cta",children:"Click to configure settings"})})}),re=p.memo(function({data:o,selected:s}){const i=m(d=>d.selectedNodeId)!==null&&!s;return e.jsx(g,{type:"steps",icon:e.jsx(D,{size:18}),title:o.label,selected:s,dimmed:i,children:e.jsx("span",{className:"workflow-node__cta",children:"Click to add custom steps"})})});function le(){const t=k(i=>i.setSidebarTab),o=k(i=>i.sidebarOpen),s=k(i=>i.toggleSidebar),n=()=>{t("templates"),o||s()};return e.jsxs("div",{className:"canvas-empty-state",children:[e.jsx(U,{className:"canvas-empty-state__icon",size:64}),e.jsx("h2",{className:"canvas-empty-state__title",children:"Build your workflow"}),e.jsx("p",{className:"canvas-empty-state__description",children:"Get started by choosing a template from the sidebar, or add blocks to build your workflow step by step."}),e.jsxs("button",{className:"canvas-empty-state__cta",onClick:n,children:[e.jsx(H,{size:14}),"Choose a template"]})]})}const de={trigger:K,engine:X,tools:ee,instructions:se,safeOutputs:oe,network:ne,settings:ie,steps:re},_=260,C=120,ae=[{id:"trigger",type:"trigger",label:"When this happens",description:"Choose what starts this workflow",isRequired:!0,isConfigured:t=>t.trigger.event!==""},{id:"engine",type:"engine",label:"AI Assistant",description:"Choose which AI runs this",isRequired:!0,isConfigured:t=>t.engine.type!==""},{id:"tools",type:"tools",label:"Agent Tools",description:"What tools the agent can use",isRequired:!1,isConfigured:t=>t.tools.length>0},{id:"instructions",type:"instructions",label:"Instructions",description:"Tell the agent what to do",isRequired:!0,isConfigured:t=>t.instructions.trim().length>0},{id:"safeOutputs",type:"safeOutputs",label:"What it can do",description:"Actions the agent can take",isRequired:!1,isConfigured:t=>Object.keys(t.safeOutputs).length>0},{id:"network",type:"network",label:"Network Access",description:"Allowed network domains",isRequired:!1,isConfigured:t=>t.network.allowed.length>0||t.network.blocked.length>0},{id:"settings",type:"settings",label:"Settings",description:"Concurrency, rate limits, platform",isRequired:!1,isConfigured:t=>t.platform!==""||(t.concurrency?.group??"")!==""||(t.concurrency?.cancelInProgress??!1)||(t.rateLimit?.max??"")!==""||(t.rateLimit?.window??"")!==""},{id:"steps",type:"steps",label:"Custom Steps",description:"Additional workflow steps",isRequired:!1,isConfigured:()=>!1}];function ce(t,o){const s=new y.graphlib.Graph;return s.setDefaultEdgeLabel(()=>({})),s.setGraph({rankdir:"TB",nodesep:40,ranksep:60}),t.forEach(i=>{s.setNode(i.id,{width:_,height:C})}),o.forEach(i=>{s.setEdge(i.source,i.target)}),y.layout(s),{nodes:t.map(i=>{const d=s.node(i.id);return{...i,position:{x:d.x-_/2,y:d.y-C/2}}}),edges:o}}function ue(){const t=m(),o=m(a=>a.selectNode),s=p.useMemo(()=>ae.filter(a=>a.isRequired||a.isConfigured(t)),[t]),n=p.useMemo(()=>s.map(a=>({id:a.id,type:a.type,position:{x:0,y:0},data:{label:a.label,type:a.type,description:a.description,isConfigured:a.isConfigured(t)}})),[s,t]),i=p.useMemo(()=>s.slice(1).map((a,w)=>({id:`e-${s[w].id}-${a.id}`,source:s[w].id,target:a.id,animated:!0,style:{stroke:"var(--borderColor-default, #d1d9e0)"}})),[s]),{nodes:d,edges:r}=p.useMemo(()=>ce(n,i),[n,i]),[c,l,u]=v(d),[h,,f]=I(r);p.useMemo(()=>{l(d)},[d,l]);const x=p.useCallback((a,w)=>{o(w.id)},[o]),N=p.useCallback(()=>{o(null)},[o]);return s.length===0?e.jsx(le,{}):e.jsxs(S,{nodes:c,edges:h,onNodesChange:u,onEdgesChange:f,onNodeClick:x,onPaneClick:N,nodeTypes:de,fitView:!0,fitViewOptions:{padding:.2},minZoom:.3,maxZoom:1.5,proOptions:{hideAttribution:!0},children:[e.jsx(E,{variant:R.Dots,gap:16,size:1}),e.jsx(T,{position:"bottom-left"}),e.jsx(B,{position:"bottom-left",style:{marginBottom:50},nodeStrokeWidth:3,pannable:!0,zoomable:!0})]})}function we(){return e.jsx(O,{children:e.jsx(ue,{})})}export{we as default}; diff --git a/docs/public/editor/assets/EditorView-CjlFMWmw.js b/docs/public/editor/assets/EditorView-D7yKdvcD.js similarity index 51% rename from docs/public/editor/assets/EditorView-CjlFMWmw.js rename to docs/public/editor/assets/EditorView-D7yKdvcD.js index 7652dc6d886..4173d6f84e0 100644 --- a/docs/public/editor/assets/EditorView-CjlFMWmw.js +++ b/docs/public/editor/assets/EditorView-D7yKdvcD.js @@ -1,8 +1,8 @@ -import{r as d,j as e}from"./react-vendor-D4h_eraO.js";import{H as K,t as J}from"./prism-vendor-BQJQnrpB.js";import{u as A,d as Q,T as Z,e as ee,h as te}from"./index-D2nmaYOD.js";const t="https://github.github.com/gh-aw/reference",ne={key:"name",title:"Workflow Name",description:"A short name for the workflow. Shown in the GitHub Actions UI. Defaults to the filename if omitted.",type:"string",required:!1,examples:["name: triage-issues"],link:`${t}/frontmatter/`},se={key:"description",title:"Description",description:"Human-readable description of what the workflow does. Rendered as a comment in the compiled lock file.",type:"string",required:!1,examples:["description: Automatically triage new issues using AI"],link:`${t}/frontmatter/`},oe={key:"source",title:"Source",description:"Reference to a remote workflow source in owner/repo/path@ref format. Used for importing workflows from other repositories.",type:"string",required:!1,examples:["source: acme/workflows/.github/aw/triage.md@main"],link:`${t}/frontmatter/`},re={key:"labels",title:"Labels",description:"Array of string labels for categorizing the workflow. Useful for organizing and filtering workflows.",type:"string[]",required:!1,examples:[`labels: +import{r as u,j as e}from"./react-vendor-D4h_eraO.js";import{H as Z,t as D}from"./prism-vendor-BQJQnrpB.js";import{u as N,d as ee,T as te,b as ne,e as se,h as oe}from"./index-7l71UumV.js";import"./radix-vendor-B-4BPIkP.js";const t="https://github.github.com/gh-aw/reference",re={key:"name",title:"Workflow Name",description:"A short name for the workflow. Shown in the GitHub Actions UI. Defaults to the filename if omitted.",type:"string",required:!1,examples:["name: triage-issues"],link:`${t}/frontmatter/`},ie={key:"description",title:"Description",description:"Human-readable description of what the workflow does. Rendered as a comment in the compiled lock file.",type:"string",required:!1,examples:["description: Automatically triage new issues using AI"],link:`${t}/frontmatter/`},le={key:"source",title:"Source",description:"Reference to a remote workflow source in owner/repo/path@ref format. Used for importing workflows from other repositories.",type:"string",required:!1,examples:["source: acme/workflows/.github/aw/triage.md@main"],link:`${t}/frontmatter/`},ae={key:"labels",title:"Labels",description:"Array of string labels for categorizing the workflow. Useful for organizing and filtering workflows.",type:"string[]",required:!1,examples:[`labels: - triage - - issues`],link:`${t}/frontmatter/`},ie={key:"imports",title:"Imports",description:"Array of shared workflow specs to import. Shared components live in .github/workflows/shared/ and provide reusable instructions.",type:"string[]",required:!1,examples:[`imports: + - issues`],link:`${t}/frontmatter/`},ce={key:"imports",title:"Imports",description:"Array of shared workflow specs to import. Shared components live in .github/workflows/shared/ and provide reusable instructions.",type:"string[]",required:!1,examples:[`imports: - shared/common-tools.md - - shared/review-guidelines.md`],link:`${t}/frontmatter/`},le={key:"on",title:"Trigger Events",description:"Defines which GitHub events start this workflow. Supports issues, pull_request, issue_comment, discussion, schedule, slash_command, push, workflow_dispatch, and release.",type:"string | string[] | object",required:!0,examples:["on: issues",`on: + - shared/review-guidelines.md`],link:`${t}/frontmatter/`},de={key:"on",title:"Trigger Events",description:"Defines which GitHub events start this workflow. Supports issues, pull_request, issue_comment, discussion, schedule, slash_command, push, workflow_dispatch, and release.",type:"string | string[] | object",required:!0,examples:["on: issues",`on: - issues - pull_request`,`on: issues: @@ -20,7 +20,7 @@ import{r as d,j as e}from"./react-vendor-D4h_eraO.js";import{H as K,t as J}from" skip-bots: - dependabot[bot]`],link:`${t}/triggers/`},{key:"on.bots",title:"Allow Bots",description:"Allow specific bot accounts to trigger the workflow.",type:"string[]",required:!1,examples:[`on: bots: - - renovate[bot]`],link:`${t}/triggers/`}]},ae={key:"engine",title:"AI Engine",description:"Which AI model powers this workflow. Choose from copilot, claude, or codex. Each engine has different strengths.",type:"string | object",required:!0,examples:["engine: copilot",`engine: + - renovate[bot]`],link:`${t}/triggers/`}]},ue={key:"engine",title:"AI Engine",description:"Which AI model powers this workflow. Choose from copilot, claude, or codex. Each engine has different strengths.",type:"string | object",required:!0,examples:["engine: copilot",`engine: id: claude model: sonnet`],link:`${t}/engines/`,children:[{key:"engine.id",title:"Engine ID",description:"Identifier of the AI engine: copilot, claude, or codex.",type:"string",required:!0,examples:[`engine: id: claude`],link:`${t}/engines/`},{key:"engine.model",title:"Model Version",description:"Specific model version to use. Options depend on the engine (e.g. sonnet, opus for Claude).",type:"string",required:!1,examples:[`engine: @@ -32,10 +32,10 @@ import{r as d,j as e}from"./react-vendor-D4h_eraO.js";import{H as K,t as J}from" command: my-agent args: - --verbose - - --timeout=300`],link:`${t}/engines/`}]},ce={key:"permissions",title:"Permissions",description:"GitHub Actions permissions controlling what the AI can access. Use read-all or write-all for broad access, or set per-scope permissions.",type:"string | object",required:!1,examples:["permissions: read-all",`permissions: + - --timeout=300`],link:`${t}/engines/`}]},pe={key:"permissions",title:"Permissions",description:"GitHub Actions permissions controlling what the AI can access. Use read-all or write-all for broad access, or set per-scope permissions.",type:"string | object",required:!1,examples:["permissions: read-all",`permissions: contents: read issues: write - pull-requests: write`],link:`${t}/permissions/`},de={key:"tools",title:"Tools",description:"Capabilities and tools available to the AI agent. Each tool extends what the agent can do.",type:"string[] | object[]",required:!1,examples:[`tools: + pull-requests: write`],link:`${t}/permissions/`},fe={key:"tools",title:"Tools",description:"Capabilities and tools available to the AI agent. Each tool extends what the agent can do.",type:"string[] | object[]",required:!1,examples:[`tools: - github - bash - playwright`,`tools: @@ -60,7 +60,7 @@ import{r as d,j as e}from"./react-vendor-D4h_eraO.js";import{H as K,t as J}from" branch-prefix: memory/`],link:`${t}/tools/`},{key:"serena",title:"Serena Tool",description:"Advanced code intelligence with language-aware understanding. Provides semantic code analysis.",type:"object",required:!1,examples:[`tools: - serena: languages: typescript,python`],link:`${t}/tools/`},{key:"agentic-workflows",title:"Agentic Workflows Tool",description:"Inspect and analyze other agentic workflows in the repository.",type:"boolean",required:!1,examples:[`tools: - - agentic-workflows`],link:`${t}/tools/`}]},ue={key:"safe-outputs",title:"Safe Outputs",description:"Actions the AI agent is allowed to take on your behalf. The agent can only perform explicitly enabled outputs.",type:"string[]",required:!1,examples:[`safe-outputs: + - agentic-workflows`],link:`${t}/tools/`}]},me={key:"safe-outputs",title:"Safe Outputs",description:"Actions the AI agent is allowed to take on your behalf. The agent can only perform explicitly enabled outputs.",type:"string[]",required:!1,examples:[`safe-outputs: - add-comment - create-issue - add-labels`,`safe-outputs: @@ -88,7 +88,7 @@ import{r as d,j as e}from"./react-vendor-D4h_eraO.js";import{H as K,t as J}from" - create-discussion`],link:`${t}/safe-outputs/`},{key:"close-discussion",title:"Close Discussion",description:"Close discussions.",type:"boolean",required:!1,examples:[`safe-outputs: - close-discussion`],link:`${t}/safe-outputs/`},{key:"dispatch-workflow",title:"Trigger Other Workflows",description:"Start other workflows in the repository.",type:"boolean",required:!1,examples:[`safe-outputs: - dispatch-workflow`],link:`${t}/safe-outputs/`},{key:"upload-asset",title:"Upload Files",description:"Upload images, charts, or reports for persistent storage.",type:"boolean",required:!1,examples:[`safe-outputs: - - upload-asset`],link:`${t}/safe-outputs/`}]},pe={key:"network",title:"Network",description:"Control which domains the agent can access. By default only essential services (GitHub, AI providers) are allowed.",type:"object",required:!1,examples:[`network: + - upload-asset`],link:`${t}/safe-outputs/`}]},he={key:"network",title:"Network",description:"Control which domains the agent can access. By default only essential services (GitHub, AI providers) are allowed.",type:"object",required:!1,examples:[`network: allowed: - api.example.com - "*.npmjs.org"`,`network: @@ -98,13 +98,13 @@ import{r as d,j as e}from"./react-vendor-D4h_eraO.js";import{H as K,t as J}from" - api.example.com - pypi.org`],link:`${t}/network/`},{key:"network.blocked",title:"Blocked Domains",description:"Domains to explicitly block, even if they would otherwise be allowed.",type:"string[]",required:!1,examples:[`network: blocked: - - evil.com`],link:`${t}/network/`}]},fe={key:"timeout-minutes",title:"Timeout",description:"Maximum time in minutes the workflow is allowed to run before it stops automatically.",type:"number",required:!1,examples:["timeout-minutes: 30"],link:`${t}/frontmatter/`},me={key:"strict",title:"Strict Mode",description:"Enable enhanced security validation for the workflow. Adds extra checks during compilation.",type:"boolean",required:!1,examples:["strict: true"],link:`${t}/frontmatter/`},he={key:"concurrency",title:"Concurrency",description:"Control what happens when this workflow is triggered while already running. Define a group and whether to cancel in-progress runs.",type:"object",required:!1,examples:["concurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: true"],link:`${t}/frontmatter/`,children:[{key:"concurrency.group",title:"Concurrency Group",description:"An expression that groups workflow runs. Only one run per group can be active at a time.",type:"string",required:!0,examples:["concurrency:\n group: ${{ github.workflow }}-${{ github.ref }}"],link:`${t}/frontmatter/`},{key:"concurrency.cancel-in-progress",title:"Cancel In-Progress",description:"Cancel any running workflow in the same group when a new one starts.",type:"boolean",required:!1,examples:[`concurrency: + - evil.com`],link:`${t}/network/`}]},ge={key:"timeout-minutes",title:"Timeout",description:"Maximum time in minutes the workflow is allowed to run before it stops automatically.",type:"number",required:!1,examples:["timeout-minutes: 30"],link:`${t}/frontmatter/`},ye={key:"strict",title:"Strict Mode",description:"Enable enhanced security validation for the workflow. Adds extra checks during compilation.",type:"boolean",required:!1,examples:["strict: true"],link:`${t}/frontmatter/`},ke={key:"concurrency",title:"Concurrency",description:"Control what happens when this workflow is triggered while already running. Define a group and whether to cancel in-progress runs.",type:"object",required:!1,examples:["concurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: true"],link:`${t}/frontmatter/`,children:[{key:"concurrency.group",title:"Concurrency Group",description:"An expression that groups workflow runs. Only one run per group can be active at a time.",type:"string",required:!0,examples:["concurrency:\n group: ${{ github.workflow }}-${{ github.ref }}"],link:`${t}/frontmatter/`},{key:"concurrency.cancel-in-progress",title:"Cancel In-Progress",description:"Cancel any running workflow in the same group when a new one starts.",type:"boolean",required:!1,examples:[`concurrency: group: my-group - cancel-in-progress: true`],link:`${t}/frontmatter/`}]},ge={key:"runtimes",title:"Runtimes",description:"Override default runtime versions for languages used during the workflow (node, python, go, etc.).",type:"object",required:!1,examples:[`runtimes: + cancel-in-progress: true`],link:`${t}/frontmatter/`}]},xe={key:"runtimes",title:"Runtimes",description:"Override default runtime versions for languages used during the workflow (node, python, go, etc.).",type:"object",required:!1,examples:[`runtimes: node: "20" - python: "3.12"`],link:`${t}/frontmatter/`},ye={key:"features",title:"Features",description:"Feature flags to enable experimental or optional functionality in the workflow.",type:"object",required:!1,examples:[`features: - mcp-gateway: true`],link:`${t}/frontmatter/`},M=[{id:"general",title:"General",entries:[ne,se,oe,re,ie]},{id:"triggers",title:"Triggers",entries:[le]},{id:"engines",title:"Engine",entries:[ae]},{id:"permissions",title:"Permissions",entries:[ce]},{id:"tools",title:"Tools",entries:[de]},{id:"safe-outputs",title:"Safe Outputs",entries:[ue]},{id:"network",title:"Network",entries:[pe]},{id:"settings",title:"Settings",entries:[fe,me,he,ge,ye]}],z=new Map;function D(n){for(const a of n)z.set(a.key,a),a.children&&D(a.children)}for(const n of M)D(n.entries);function ke(n){return z.get(n)}function xe({activeDocKey:n,onScrollComplete:a}){const[o,i]=d.useState(""),c=d.useRef(null),l=d.useRef(new Map),r=d.useMemo(()=>{if(!o.trim())return M;const s=o.toLowerCase(),f=[];for(const x of M){const p=x.entries.filter(u=>G(u,s));p.length>0&&f.push({...x,entries:p})}return f},[o]);d.useEffect(()=>{if(!n)return;const s=l.current.get(n);if(s){s.scrollIntoView({behavior:"smooth",block:"start"}),s.classList.add("docs-entry--highlight");const f=setTimeout(()=>{s.classList.remove("docs-entry--highlight"),a?.()},1200);return()=>clearTimeout(f)}},[n,a]);const g=(s,f)=>{f?l.current.set(s,f):l.current.delete(s)};return e.jsxs("div",{style:k.container,children:[e.jsxs("div",{style:k.searchBox,children:[e.jsx("input",{type:"text",value:o,onChange:s=>i(s.target.value),placeholder:"Search fields...",style:k.searchInput}),o&&e.jsx("button",{onClick:()=>i(""),style:k.clearBtn,"aria-label":"Clear search",children:"x"})]}),!o&&e.jsxs("nav",{style:k.toc,children:[e.jsx("div",{style:k.tocTitle,children:"Contents"}),M.map(s=>e.jsx("a",{href:`#docs-section-${s.id}`,onClick:f=>{f.preventDefault(),c.current?.querySelector(`#docs-section-${s.id}`)?.scrollIntoView({behavior:"smooth",block:"start"})},style:k.tocLink,children:s.title},s.id))]}),e.jsxs("div",{ref:c,style:k.content,children:[r.length===0&&e.jsxs("div",{style:k.emptyState,children:['No fields match "',o,'"']}),r.map(s=>e.jsxs("div",{id:`docs-section-${s.id}`,style:k.section,children:[e.jsx("div",{style:k.sectionTitle,children:s.title}),s.entries.map(f=>e.jsx(Y,{entry:f,registerRef:g,activeKey:n},f.key))]},s.id))]})]})}function Y({entry:n,registerRef:a,activeKey:o,depth:i=0}){const c=o===n.key;return e.jsxs(e.Fragment,{children:[e.jsxs("div",{ref:l=>a(n.key,l),className:`docs-entry ${c?"docs-entry--highlight":""}`,style:{...k.entry,marginLeft:i*16,borderLeftColor:c?"var(--color-accent-fg, #0969da)":"transparent"},id:`docs-entry-${n.key}`,children:[e.jsxs("div",{style:k.entryHeader,children:[e.jsx("code",{style:k.entryKey,children:n.key}),e.jsx("span",{style:k.typeBadge,children:n.type}),n.required&&e.jsx("span",{style:k.requiredBadge,children:"required"})]}),e.jsx("div",{style:k.entryTitle,children:n.title}),e.jsx("div",{style:k.entryDesc,children:n.description}),n.examples.map((l,r)=>e.jsx("pre",{style:k.exampleBlock,children:l},r)),n.link&&e.jsx("a",{href:n.link,target:"_blank",rel:"noopener noreferrer",style:k.docsLink,children:"Full docs →"})]}),n.children?.map(l=>e.jsx(Y,{entry:l,registerRef:a,activeKey:o,depth:i+1},l.key))]})}function G(n,a){return!!(`${n.key} ${n.title} ${n.description}`.toLowerCase().includes(a)||n.children?.some(i=>G(i,a)))}const P='ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',k={container:{display:"flex",flexDirection:"column",height:"100%",overflow:"hidden"},searchBox:{position:"relative",padding:"8px 12px",borderBottom:"1px solid var(--color-border-default, #d0d7de)",flexShrink:0},searchInput:{width:"100%",padding:"6px 28px 6px 8px",fontSize:"12px",fontFamily:"inherit",border:"1px solid var(--color-border-default, #d0d7de)",borderRadius:"6px",background:"var(--color-bg-default, #fff)",color:"var(--color-fg-default, #1f2328)",outline:"none"},clearBtn:{position:"absolute",right:"18px",top:"50%",transform:"translateY(-50%)",border:"none",background:"none",cursor:"pointer",color:"var(--color-fg-muted, #656d76)",fontSize:"12px",lineHeight:1,padding:"2px 4px"},toc:{padding:"8px 12px",borderBottom:"1px solid var(--color-border-default, #d0d7de)",flexShrink:0},tocTitle:{fontSize:"11px",fontWeight:600,textTransform:"uppercase",letterSpacing:"0.5px",color:"var(--color-fg-muted, #656d76)",marginBottom:"4px"},tocLink:{display:"block",fontSize:"12px",color:"var(--color-accent-fg, #0969da)",textDecoration:"none",padding:"2px 0",cursor:"pointer"},content:{flex:1,overflowY:"auto",padding:"8px 12px 24px"},emptyState:{padding:"24px",color:"var(--color-fg-muted, #656d76)",fontSize:"13px",textAlign:"center"},section:{marginBottom:"20px"},sectionTitle:{fontSize:"12px",fontWeight:600,textTransform:"uppercase",letterSpacing:"0.5px",color:"var(--color-fg-muted, #656d76)",paddingBottom:"4px",borderBottom:"1px solid var(--color-border-muted, #d8dee4)",marginBottom:"8px"},entry:{padding:"10px 12px",marginBottom:"8px",borderRadius:"6px",border:"1px solid var(--color-border-default, #d0d7de)",borderLeft:"3px solid transparent",background:"var(--color-bg-default, #fff)",transition:"border-color 0.2s ease, background 0.2s ease"},entryHeader:{display:"flex",alignItems:"center",gap:"6px",marginBottom:"4px",flexWrap:"wrap"},entryKey:{fontSize:"13px",fontWeight:600,fontFamily:P,color:"var(--color-accent-fg, #0969da)"},typeBadge:{fontSize:"10px",fontWeight:500,fontFamily:P,padding:"1px 6px",borderRadius:"10px",background:"var(--color-bg-muted, #eaeef2)",color:"var(--color-fg-muted, #656d76)"},requiredBadge:{fontSize:"10px",fontWeight:600,padding:"1px 6px",borderRadius:"10px",background:"color-mix(in srgb, var(--color-danger-fg, #d1242f) 12%, transparent)",color:"var(--color-danger-fg, #d1242f)"},entryTitle:{fontSize:"13px",fontWeight:600,color:"var(--color-fg-default, #1f2328)",marginBottom:"2px"},entryDesc:{fontSize:"12px",color:"var(--color-fg-muted, #656d76)",lineHeight:1.5,marginBottom:"6px"},exampleBlock:{fontSize:"11px",fontFamily:P,lineHeight:1.5,padding:"6px 8px",borderRadius:"4px",background:"var(--color-bg-subtle, #f6f8fa)",border:"1px solid var(--color-border-muted, #d8dee4)",overflow:"auto",marginBottom:"4px",whiteSpace:"pre-wrap"},docsLink:{fontSize:"11px",color:"var(--color-accent-fg, #0969da)",textDecoration:"none",fontWeight:500}};function E(n){const a=[],o=/(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(\[[^\]]*\]\([^)]*\))/g;let i=0,c;for(;(c=o.exec(n))!==null;){c.index>i&&a.push({type:"plain",content:n.slice(i,c.index)});const l=c[0];if(c[1])a.push({type:"inline-code",content:l});else if(c[2])a.push({type:"bold",content:l});else if(c[3])a.push({type:"italic",content:l});else if(c[4]){const r=l.indexOf("]");a.push({type:"link-bracket",content:"["}),a.push({type:"link-text",content:l.slice(1,r)}),a.push({type:"link-bracket",content:"]"}),a.push({type:"link-paren",content:"("}),a.push({type:"link-url",content:l.slice(r+2,l.length-1)}),a.push({type:"link-paren",content:")"})}i=c.index+l.length}return iu.type==="plain"?{...u,type:"heading-text"}:u)),o.push(p);continue}const f=r.match(/^(\s*[-*+]\s)(.*)/);if(f){o.push([{type:"list-marker",content:f[1]},...E(f[2])]);continue}const x=r.match(/^(\s*\d+\.\s)(.*)/);if(x){o.push([{type:"list-marker",content:x[1]},...E(x[2])]);continue}if(/^\s*