* docs: add image search & generation design spec and implementation plan

- Spec: dual-source image search (Openverse + Wikimedia), multi-provider image generation
- Plan: 16 tasks covering types, server endpoints, settings UI, property panel, auto-search pipeline, MCP integration

* feat(types): add image service types and imagePrompt to ImageNode

* feat(server): add image service API key validation endpoint

Adds POST /api/ai/image-service-test that validates credentials for
openverse (client_credentials), openai/custom (Bearer + /v1/models),
gemini (API key + v1beta/models), and replicate (Bearer + /v1/models).

* feat(server): add multi-provider image generation endpoint

* feat(server): add dual-source image search endpoint (Openverse + Wikimedia)

POST /api/ai/image-search searches freely-licensed images via Openverse
with automatic fallback to Wikimedia Commons on 429 rate-limit responses.
Supports optional OAuth credentials for authenticated Openverse requests.

* feat(store): add imageSearchStatuses to canvas store for runtime status tracking

* feat(store): add image generation config and Openverse OAuth to agent settings

* feat(editor): add Images tab to agent settings dialog

Adds Popover primitive, ImagesPage component with Image Search (Openverse OAuth, test) and Image Generation (provider select, API key, model, base URL) sections, and wires them into the settings dialog sidebar.

* feat(panels): add image search popover with Openverse/Wikimedia results grid

* feat(panels): add image generate popover with multi-provider support

* feat(panels): add Search and Generate buttons to image property section

* feat(ai): update prompts to use imagePrompt instead of src for image nodes

* feat(ai): add auto-search pipeline with Openverse/Wikimedia fallback

* feat(ai): trigger auto image search after design generation completes

* feat(mcp): implement G() operation for image search in batch design DSL

Adds the G(parent, mode, prompt) operation to batch_design DSL that creates
an image node and optionally fetches a real image URL via the image-search
API when mode is "search". Converts executeLine to async to support the
network call.

* feat(mcp): auto-fill images after design refinement in layered pipeline

* feat(ai): split imageSearchQuery and imagePrompt for search vs generation

- ImageNode now has both imageSearchQuery (short keywords for search)
  and imagePrompt (long description for AI image generation)
- AI prompts instruct LLM to generate both fields
- Search pipeline and popovers use imageSearchQuery
- Generate popover uses imagePrompt
- Server-side simplifySearchQuery kept as fallback for manual input

* fix(ai): hook auto image search into orchestrator completion path

The primary generation path uses executeOrchestration -> insertStreamingNode,
not applyNodesToCanvas/animateNodesToCanvas. Added scanAndFillImages call
to orchestrator.ts after all sub-agents complete. Added debug logging.
Removed plan/spec docs from git.

* style(editor): remove provider names from image search ready status

* fix(panels): clean up image gen error display and settings UI

- Parse API error response to show concise message instead of raw JSON
- Limit error text to 2 lines with line-clamp
- Fix image gen test button sending wrong service name
- Inline Image Search ready indicator with section header
- Remove debug logging from image search pipeline

* style(panels): allow up to 4 lines for image gen error message

* fix: avoid 1-frame delay when resizing canvas (#60)

rAF callbacks run before ResizeObserver in the same frame.
Scheduling render in ResizeObserver via rAF defers it to the next frame.

Invoke render() synchronously to leverage ResizeObserver's pre-paint timing
and ensure immediate visual update.

* feat(electron): implement desktop application structure and auto-updater

- Introduced a new Electron desktop application with a structured directory for apps and packages.
- Added auto-updater functionality to manage application updates seamlessly.
- Created a comprehensive menu system for the desktop app.
- Implemented logging capabilities for better debugging and error tracking.
- Configured build settings for various platforms (macOS, Windows, Linux) using electron-builder.
- Established TypeScript configurations for both the desktop and web applications.
- Integrated Vite for the web application with support for React and Tailwind CSS.
- Added icons and assets for the desktop application.

* chore: update package versions to 0.5.0 across all package.json files and add pre-commit hook for version synchronization

- Bumped version to 0.5.0 in package.json files for the main project, desktop app, web app, and all packages.
- Introduced a pre-commit hook to automatically sync version numbers from branch names to all package.json files.

* chore: update package versions to 0.5.0 and refactor Skia components

- Bumped version to 0.5.0 in bun.lock and all relevant package.json files.
- Refactored Skia components to utilize shared functionality from @zseven-w/pen-renderer, including image loading, hit testing, and path utilities.
- Removed redundant code and improved modularity by re-exporting necessary functions and classes from the renderer package.

* fix(panels): handle string fill values in icon nodes (#61)

AI-generated icon/path nodes may have fill stored as a raw string
instead of a PenFill[] array, causing "Cannot use 'in' operator"
crash when selecting the node in the property panel.

* chore: update documentation and project structure for monorepo organization

- Added a new version bump command to synchronize all package.json files.
- Updated the project structure to reflect a monorepo setup with organized workspaces for apps and packages.
- Enhanced README files in multiple languages to include the new structure and commands.
- Adjusted image paths in documentation to point to the correct locations for the desktop application.

* feat(ai): incremental image search and improved image generation prompts

- Refactor image search from batch post-generation to incremental queue:
  enqueueImageForSearch() triggers as each image node is inserted during
  streaming, so images appear progressively instead of all at once after
  generation completes. scanAndFillImages() remains as a final sweep.
- Update imagePrompt guidance to avoid "transparent background" and
  similar phrases that many models cannot reliably produce.
- Pass node width/height from image panel to generation endpoint for
  aspect-ratio-aware output (Gemini aspect ratio mapping, OpenAI size
  selection, Replicate dimensions).

* feat(ai): multi-profile image generation config and cleaner error messages

- Support multiple image generation profiles with active selection;
  first configured profile becomes default. Old single-config migrated
  automatically on hydrate.
- Fix Gemini aspect ratio: move to generationConfig.imageConfig per API spec.
- Extract clean error messages from provider JSON responses (Gemini
  error.message, OpenAI error.message, Replicate detail) instead of
  returning raw JSON text.
- Remove destructive client-side regex that mangled error display.

* feat(design-md): integrate design system panel and functionality

- Added a new DesignMdPanel component for managing design system specifications.
- Implemented functionality to toggle the design system panel in the editor layout and toolbar.
- Introduced new commands for importing, exporting, and auto-generating design.md content.
- Updated AI chat handlers to utilize design.md data for enhanced design generation.
- Enhanced localization support for design system features across multiple languages.

* perf(canvas): skip draw calls for nodes outside the viewport (#64)

Add viewport culling in render() to avoid issuing CanvasKit draw calls
  for off-screen nodes. A 64px screen-space buffer is kept around the
  viewport edges so nearby nodes are pre-rendered, preventing pop-in
  during fast panning.

* feat(utils): enhance Windows process spawning for CLI scripts

- Updated the buildSpawnClaudeCodeProcess function to handle .cmd and .ps1 scripts appropriately.
- Implemented PowerShell invocation for .ps1 files and ensured safe defaults for .cmd and .exe files.
- Improved handling of command execution to avoid limitations of cmd.exe.

* feat(ai): add support for Gemini CLI integration

- Extended the AI provider options to include 'gemini' across various components and APIs.
- Implemented functions for generating, validating, and connecting to the Gemini CLI.
- Added Gemini-specific error handling and model fetching logic.
- Updated UI components to display Gemini as a selectable provider with appropriate icons and labels.
- Enhanced localization support for Gemini-related features in multiple languages.

* feat(editor): warn before closing with unsaved changes

Intercept window/tab close when isDirty is true:
- Electron: native dialog with Save / Don't Save / Cancel
- Web: beforeunload handler + confirm on New/Open actions
- i18n: close-confirm strings for all 15 locales

* feat(ipc): extract IPC handlers to a dedicated module

- Moved IPC dialog handling and updater functions from main.ts to ipc-handlers.ts for better organization and maintainability.
- Implemented file open/save dialogs, theme setting, and preferences management through IPC.
- Enhanced updater functionality with state management and auto-update settings.
- Improved code structure by separating concerns, making it easier to manage IPC-related logic.

* feat(docs): update CLAUDE documentation and add new files for desktop and web apps

- Enhanced CLAUDE.md with detailed module documentation references for `packages/` and `apps/`.
- Updated `pen-core` description to include clone utilities in `pen-core`.
- Added new documentation files for the desktop and web applications, outlining their structure, components, and functionalities.
- Included IPC handler details in the desktop app documentation for better clarity on file dialogs and theme synchronization.

* feat(docker): add Gemini CLI support and update documentation

- Introduced a new Docker build stage for the Gemini CLI, allowing users to install and run it.
- Updated the Dockerfile to include the installation of the Gemini CLI alongside existing CLI tools.
- Enhanced README files in multiple languages to document the new `openpencil-gemini` image variant.
- Added Gemini CLI connection instructions to the main README for better user guidance.

* feat(docs): add Gemini CLI connection instructions to multiple language READMEs

- Updated README files in German, Spanish, French, Hindi, Indonesian, Japanese, Korean, Portuguese, Russian, Thai, Turkish, Vietnamese, and both Traditional and Simplified Chinese to include connection instructions for the Gemini CLI.
- Enhanced documentation to improve user guidance for connecting the Gemini CLI in agent settings.

* perf(renderer): replace count-based text cache limits with memory-based eviction (#66)

previous limits (PARA_CACHE_MAX=200, TEXT_CACHE_MAX=300) were too small
  for scenes with many nodes, causing constant cache churn and paragraph
  rebuilds every frame, which dropped FPS significantly during canvas pan.

  - switch to byte-budget limits (64 MB paragraphs, 256 MB bitmaps)
  - bitmap size measured exactly as cw*ch*4; paragraph WASM heap estimated
    as content.length*64+4096
  - eviction uses Map insertion order (FIFO) instead of a separate string[]
    array, replacing O(n) array.shift() with O(1) Map.entries().next()
  - evict before insert so the budget check includes the incoming entry

---------

Co-authored-by: Fini <fini.yang@gmail.com>
Co-authored-by: leinaldo <60176594+leinaldo@users.noreply.github.com>
This commit is contained in:
Kayshen Xu 2026-03-22 09:44:04 +08:00 committed by GitHub
parent b05ebb944a
commit 03d4433693
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
525 changed files with 14488 additions and 5246 deletions

30
.githooks/pre-commit Executable file
View file

@ -0,0 +1,30 @@
#!/bin/sh
#
# Pre-commit hook: read version from branch name (e.g. v0.5.0)
# and sync to all package.json files
#
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Extract version from branch name like v0.5.0, v1.2.3, release/v2.0.0
VERSION=$(echo "$BRANCH" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
if [ -z "$VERSION" ]; then
exit 0
fi
CHANGED=0
for f in package.json apps/*/package.json packages/*/package.json; do
if [ -f "$f" ]; then
CURRENT=$(jq -r '.version' "$f")
if [ "$CURRENT" != "$VERSION" ]; then
jq --arg v "$VERSION" '.version=$v' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
git add "$f"
CHANGED=1
fi
fi
done
if [ "$CHANGED" = "1" ]; then
echo "[version-sync] branch $BRANCH → $VERSION"
fi

View file

@ -58,7 +58,7 @@ jobs:
run: bun run mcp:compile
- name: Build Electron app
run: npx electron-builder --config electron-builder.yml ${{ matrix.build_args }} --publish never
run: npx electron-builder --config apps/desktop/electron-builder.yml ${{ matrix.build_args }} --publish never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.CSC_LINK }}
@ -72,8 +72,8 @@ jobs:
- name: Rename arm64 update metadata
if: matrix.platform == 'mac-arm64'
run: |
if [ -f dist-electron/latest-mac.yml ]; then
mv dist-electron/latest-mac.yml dist-electron/latest-mac-arm64.yml
if [ -f out/release/latest-mac.yml ]; then
mv out/release/latest-mac.yml out/release/latest-mac-arm64.yml
fi
- name: Upload artifacts
@ -81,14 +81,14 @@ jobs:
with:
name: electron-${{ matrix.platform }}
path: |
dist-electron/*.dmg
dist-electron/*.zip
dist-electron/*.exe
dist-electron/*.AppImage
dist-electron/*.deb
dist-electron/latest*.yml
dist-electron/*.blockmap
!dist-electron/builder-debug.yml
out/release/*.dmg
out/release/*.zip
out/release/*.exe
out/release/*.AppImage
out/release/*.deb
out/release/latest*.yml
out/release/*.blockmap
!out/release/builder-debug.yml
retention-days: 30
release:

View file

@ -51,5 +51,5 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: web-build
path: .output/
path: out/web/
retention-days: 7

View file

@ -36,6 +36,9 @@ jobs:
- variant: copilot
target: with-copilot
suffix: '-copilot'
- variant: gemini
target: with-gemini
suffix: '-gemini'
- variant: full
target: full
suffix: '-full'

10
.gitignore vendored
View file

@ -1,7 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
count.txt
.env
@ -12,7 +10,11 @@ count.txt
.vinxi
__unconfig*
todos.json
.openpencil-tmp/
# Build outputs
out/
dist/
dist-ssr/
electron-dist/
dist-electron/
.openpencil-tmp/
scripts/figma-*

339
CLAUDE.md
View file

@ -1,6 +1,7 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Detailed module docs are in `packages/CLAUDE.md`, `apps/web/CLAUDE.md`, and `apps/desktop/CLAUDE.md` — loaded automatically when working in those directories.
## Commands
@ -11,13 +12,29 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Run a single test:** `bun --bun vitest run path/to/test.ts`
- **Type check:** `npx tsc --noEmit`
- **Install dependencies:** `bun install`
- **Bump version:** `bun run bump <version>` (syncs all package.json files)
- **Electron dev:** `bun run electron:dev` (starts Vite + Electron together)
- **Electron compile:** `bun run electron:compile` (esbuild electron/ to electron-dist/)
- **Electron compile:** `bun run electron:compile` (esbuild electron/ to out/desktop/)
- **Electron build:** `bun run electron:build` (full web build + compile + electron-builder package)
## Architecture
OpenPencil is an open-source vector design tool (alternative to Pencil.dev) with a Design-as-Code philosophy. Built as a **TanStack Start** full-stack React application with Bun runtime. Server API powered by **Nitro**. Also ships as an **Electron** desktop app for macOS, Windows, and Linux.
OpenPencil is an open-source vector design tool (alternative to Pencil.dev) with a Design-as-Code philosophy. Organized as a **Bun monorepo** with workspaces:
```text
openpencil/
├── apps/
│ ├── web/ TanStack Start full-stack React app (Vite + Nitro)
│ └── desktop/ Electron desktop app (macOS, Windows, Linux)
├── packages/
│ ├── pen-types/ Type definitions for PenDocument model
│ ├── pen-core/ Document tree ops, layout engine, variables, boolean ops, clone utilities
│ ├── pen-codegen/ Multi-platform code generators
│ ├── pen-figma/ Figma .fig file parser and converter
│ ├── pen-renderer/ Standalone CanvasKit/Skia renderer
│ └── pen-sdk/ Umbrella SDK (re-exports all packages)
└── .githooks/ Pre-commit version sync from branch name
```
**Key technologies:** React 19, CanvasKit/Skia WASM (canvas engine), Paper.js (boolean path operations), Zustand v5 (state management), TanStack Router (file-based routing), Tailwind CSS v4, shadcn/ui (UI primitives), Vite 7, Nitro (server), Electron 35 (desktop), TypeScript (strict mode).
@ -53,299 +70,40 @@ PenDocument
└── children: PenNode[] (default/single-page fallback)
```
- `document-store-pages.ts` — page CRUD actions: `addPage`, `removePage`, `renamePage`, `reorderPage`, `duplicatePage`
- `canvas-store.ts``activePageId` state, `setActivePageId` action
- `canvas-sync-utils.ts``forcePageResync()` triggers page-aware canvas re-sync
- `page-tabs.tsx` — tab bar UI for multi-page navigation with context menu
### Design Variables Architecture
```text
PenDocument (source of truth)
├── variables: Record<string, VariableDefinition> ($color-1, $spacing-md, ...)
├── themes: Record<string, string[]> ({Theme-1: ["Default","Dark"]})
└── children: PenNode[] (nodes with $variable refs)
┌──────────┴──────────┐
▼ ▼
Canvas Sync Code Generation
resolveNodeForCanvas() $ref → var(--name)
$ref → concrete value CSS Variables block
```
- **`$variable` references are preserved** in the document store (e.g. `$color-1` in fill color)
- `normalize-pen-file.ts` does NOT resolve `$refs` — only fixes format issues
- `resolveNodeForCanvas()` resolves `$refs` on-the-fly before CanvasKit rendering
- Code generators output `var(--name)` for `$ref` values
- Multiple theme axes supported (e.g. Theme-1 with Light/Dark, Theme-2 with Compact/Comfortable)
- Each theme axis has variants; variables can have per-variant values (`ThemedValue[]`)
### MCP Layered Design Workflow
External LLMs (Claude Code, Codex, Gemini CLI, etc.) can generate designs via MCP using two approaches:
External LLMs (Claude Code, Codex, Gemini CLI, etc.) can generate designs via MCP:
**Single-shot** (existing): `batch_design` or `insert_node` — generate entire design in one call. Simple but lower fidelity for complex designs.
**Layered** (new): Break generation into phases, each with focused context and per-section post-processing:
```text
get_design_prompt(section="planning") → Load planning-specific guidelines
design_skeleton(rootFrame, sections) → Create root + section frames
│ Returns: section IDs, content width,
│ per-section guidelines, suggested roles
design_content(sectionId, children) ×N → Populate each section independently
│ Runs: role resolution, icon resolution,
│ sanitization per section
design_refine(rootId) → Full-tree validation + auto-fixes
Returns: fix report, layout snapshot
```
**`get_design_prompt` segmented retrieval**: Instead of loading the full ~8K char prompt at once, external LLMs can request focused subsets:
- `schema` — PenNode types, fill/stroke format
- `layout` — Flexbox layout engine rules
- `roles` — Semantic role listing with defaults
- `text` — Typography, CJK, copywriting rules
- `style` — Visual style policy, palette
- `icons` — Available icon names + usage
- `examples` — Design examples
- `guidelines` — General design tips
- `planning` — Design type detection, section decomposition, style guide template, layered workflow guide
**`design_skeleton` section guidelines**: The tool generates context-specific content guidelines for each section based on its name/role (nav → navbar layout tips, hero → headline sizing, form → input width rules, etc.), reducing per-call cognitive load.
### Key Modules
- **`src/canvas/`** — Canvas engine (33 files + `skia/` subdir with 11 files):
- **`skia/`** — CanvasKit/Skia WASM renderer (primary canvas engine):
- `skia-canvas.tsx` — Main canvas React component (SkiaCanvas), mouse/keyboard event handling, drawing tools, select/drag/resize/rotate, marquee, pen tool, text editing overlay
- `skia-engine.ts` — Core rendering engine: `SkiaEngine` class, `syncFromDocument()`, viewport transform, node flattening with layout resolution, `SpatialIndex` integration, zoom/pan, dirty-flag rendering loop
- `skia-renderer.ts` — GPU-accelerated draw calls: rectangles, ellipses, text, paths, images, frames, groups, selection handles, guides, agent indicators
- `skia-init.ts` — CanvasKit WASM loader with CDN fallback
- `skia-hit-test.ts``SpatialIndex` for spatial queries: `hitTest()`, `searchRect()`, R-tree backed
- `skia-viewport.ts` — Viewport math: `screenToScene`, `sceneToScreen`, `viewportMatrix`, `zoomToPoint`
- `skia-paint-utils.ts` — Color parsing, gradient creation, text line wrapping for CanvasKit
- `skia-path-utils.ts` — SVG path `d` string to CanvasKit Path conversion
- `skia-image-loader.ts` — Async image loading and caching for CanvasKit
- `skia-overlays.ts` — Selection overlays, hover highlights, dimension labels
- `skia-pen-tool.ts` — Pen tool implementation for Skia: anchor points, control handles, path building
- **Legacy Fabric.js files** (retained for `zoomToFitContent` utility, pending removal):
- `fabric-canvas.tsx`, `use-fabric-canvas.ts`, `canvas-object-factory.ts`, `canvas-object-sync.ts`, `canvas-object-modified.ts`, `canvas-controls.ts`
- **Shared modules** (used by both Skia and legacy code):
- `canvas-sync-lock.ts` — Prevents circular sync loops
- `canvas-sync-utils.ts``forcePageResync()` utility for page-aware canvas re-sync
- `canvas-constants.ts` — Default colors, zoom limits, stroke widths
- `canvas-node-creator.ts``createNodeForTool`, `isDrawingTool` helpers
- `canvas-layout-engine.ts` — Auto-layout computation: `resolvePadding`, `getNodeWidth/Height`, `computeLayoutPositions`, `Padding` interface
- `canvas-text-measure.ts` — Text width/height estimation, CJK detection, `parseSizing`
- `canvas-zoom-cache.ts` — Bitmap caching during zoom/pan for performance
- `use-canvas-sync.ts` — PenDocument ↔ canvas sync, node flattening, variable resolution via `resolveNodeForCanvas()`
- `use-canvas-events.ts` — Drawing events, shape creation
- `use-canvas-viewport.ts` — Wheel zoom, space+drag panning, tool cursor switching
- `use-canvas-selection.ts` — Selection sync between canvas and store
- `use-canvas-hover.ts` — Hover state management
- `use-canvas-guides.ts` / `guide-utils.ts` — Smart alignment guides with snapping
- `pen-tool.ts` — Bezier pen tool (Fabric-based, Skia version in `skia/skia-pen-tool.ts`)
- `parent-child-transform.ts` — Propagates parent transforms to children proportionally
- `use-dimension-label.ts` / `use-frame-labels.ts` / `use-entered-frame-overlay.ts` / `use-layout-indicator.ts` — Visual overlays
- `insertion-indicator.ts` / `drag-into-layout.ts` / `drag-reparent.ts` / `layout-reorder.ts` — Drag-and-drop support
- `selection-context.ts` — Multi-select context management
- `agent-indicator.ts` / `use-agent-indicators.ts` — Agent visual indicators on canvas during concurrent AI generation
- **`src/variables/`** — Design variables system (2 files):
- `resolve-variables.ts` — Core resolution utilities: `resolveVariableRef`, `resolveNodeForCanvas`, `getDefaultTheme`, `isVariableRef`; resolves `$variable` references to concrete values for canvas rendering with circular reference guards
- `replace-refs.ts``replaceVariableRefsInTree`: recursively walk node tree to replace/resolve `$refs` when renaming or deleting variables (covers opacity, gap, padding, fills, strokes, effects, text)
- **`src/stores/`** — Zustand stores (9 files):
- `canvas-store.ts` — UI/tool/selection/viewport/clipboard/interaction state, `variablesPanelOpen` toggle, `activePageId`, `figmaImportDialogOpen`
- `document-store.ts` — PenDocument tree CRUD: `addNode`, `updateNode`, `removeNode`, `moveNode`, `reorderNode`, `duplicateNode`, `groupNodes`, `ungroupNode`, `toggleVisibility`, `toggleLock`, `scaleDescendantsInStore`, `rotateDescendantsInStore`, `getNodeById`, `getParentOf`, `getFlatNodes`, `isDescendantOf`; Variable CRUD: `setVariable`, `removeVariable`, `renameVariable`, `setThemes` (all with history support)
- `document-store-pages.ts` — Page actions extracted from document-store: `addPage`, `removePage`, `renamePage`, `reorderPage`, `duplicatePage`
- `document-tree-utils.ts` — Pure tree helpers extracted from document-store: `findNodeInTree`, `findParentInTree`, `removeNodeFromTree`, `updateNodeInTree`, `flattenNodes`, `insertNodeInTree`, `isDescendantOf`, `getNodeBounds`, `findClearX`, `scaleChildrenInPlace`, `rotateChildrenInPlace`, `createEmptyDocument`, `DEFAULT_FRAME_ID`
- `history-store.ts` — Undo/redo (max 300 states), batch mode for grouped operations
- `ai-store.ts` — Chat messages, streaming state, generated code, model selection, `pendingAttachments` for image uploads
- `agent-settings-store.ts` — AI provider config (Anthropic/OpenAI/OpenCode/Copilot), MCP CLI integrations (Claude Code, Codex CLI, Gemini CLI, OpenCode CLI, Kiro CLI, Copilot CLI), localStorage persistence
- `uikit-store.ts` — UIKit management: imported kits, component browser state (search, category filters), localStorage persistence
- `theme-preset-store.ts` — Theme preset management and persistence
- **`src/types/`** — Type system (9 files):
- `pen.ts` — PenDocument/PenNode (frame, group, rectangle, ellipse, line, polygon, path, text, image, ref), ContainerProps, `PenPage`; `PenDocument.variables`, `PenDocument.themes`, `PenDocument.pages`
- `canvas.ts` — ToolType (select, frame, rectangle, ellipse, line, polygon, path, text, hand), ViewportState, SelectionState, CanvasInteraction
- `styles.ts` — PenFill (solid, linear_gradient, radial_gradient), PenStroke, PenEffect (shadow, blur), BlendMode, StyledTextSegment
- `variables.ts``VariableDefinition` (type + value), `ThemedValue` (value per theme), `VariableValue`
- `uikit.ts` — UIKit, KitComponent, ComponentCategory types for reusable component organization and browsing
- `agent-settings.ts` — AI provider config types (`AIProviderType`: anthropic/openai/opencode/copilot, `AIProviderConfig`, `MCPCliIntegration`, `GroupedModel`)
- `electron.d.ts` — Electron IPC bridge types (file dialogs, save operations, updater: `UpdaterState`/`UpdaterStatus`, `getState`/`checkForUpdates`/`quitAndInstall`/`onStateChange`)
- `theme-preset.ts` — Type definitions for theme presets
- `opencode-sdk.d.ts` — Type declarations for @opencode-ai/sdk
- **`src/components/editor/`** — Editor UI (9 files): editor-layout, toolbar (with variables panel toggle), boolean-toolbar (contextual floating toolbar for union/subtract/intersect, shown when 2+ compatible shapes selected), tool-button, shape-tool-dropdown (rectangle/ellipse/line/path + icon picker + image import), top-bar (with `AgentStatusButton`), status-bar, page-tabs (multi-page navigation with context menu), update-ready-banner (Electron auto-updater notification)
- **`src/components/panels/`** — Panels (29 files):
- `layer-panel.tsx` / `layer-item.tsx` / `layer-context-menu.tsx` — Tree view with drag-and-drop reordering and drop-into-children (above/below/inside), visibility/lock toggles, context menu, rename
- `property-panel.tsx` — Unified property panel
- `fill-section.tsx` — Solid + gradient fill, variable picker integration for color binding
- `stroke-section.tsx` — Stroke color/width/dash, variable picker for stroke color binding
- `corner-radius-section.tsx` — Unified or 4-point corner radius
- `size-section.tsx` — Position, size, rotation
- `text-section.tsx` — Font, size, weight, spacing, alignment
- `text-layout-section.tsx` — Text node layout controls (auto/fixed-width/fixed-height modes, fill width/height)
- `icon-section.tsx` — Icon property panel section: current icon name, library dropdown, icon picker
- `effects-section.tsx` — Shadow and blur
- `export-section.tsx` — Per-layer export to PNG/SVG with scale options (1x/2x/3x)
- `layout-section.tsx` — Auto-layout (none/vertical/horizontal), gap, padding, justify, align; variable picker for gap/padding binding
- `layout-padding-section.tsx` — Extracted padding controls: single/axis/T-R-B-L modes with popover mode switcher
- `appearance-section.tsx` — Opacity, visibility, lock, flip; variable picker for opacity binding
- `ai-chat-panel.tsx` / `chat-message.tsx` — AI chat with markdown, design block collapse, apply design, image attachment upload (paperclip button, preview strip, 5MB/4-image limit)
- `ai-chat-handlers.ts``useChatHandlers` hook, `isDesignRequest`, `buildContextString` helpers extracted from ai-chat-panel
- `ai-chat-checklist.tsx``FixedChecklist` component for AI generation progress display
- `code-panel.tsx` — Code generation output (React/Tailwind, HTML/CSS, CSS Variables, Vue, Svelte, Flutter, SwiftUI, Compose, React Native)
- `image-section.tsx` — Image property panel section
- `node-preview-svg.tsx` — Node SVG preview component
- `right-panel.tsx` — Right panel container
- `component-browser-panel.tsx` / `component-browser-grid.tsx` / `component-browser-card.tsx` — Resizable floating panel for browsing, importing, and inserting UIKit components with category tabs and search
- `variables-panel.tsx` — Design variables management: theme axes as tabs, variant columns, resizable floating panel, add/rename/delete themes and variants
- `variable-row.tsx` — Individual variable row: type icon, editable name, per-theme-variant value cells (color picker, number input, text input), context menu
- **`src/components/shared/`** — Reusable UI (10 files): ColorPicker, NumberInput, SectionHeader, ExportDialog, SaveDialog, AgentSettingsDialog, IconPickerDialog, VariablePicker, FigmaImportDialog, LanguageSelector
- **`src/components/icons/`** — Provider/brand logos: ClaudeLogo, OpenAILogo, OpenCodeLogo, CopilotLogo, FigmaLogo
- **`src/components/ui/`** — shadcn/ui primitives: Button, Select, Separator, Slider, Switch, Toggle, Tooltip
- **`src/services/ai/`** — AI services (31 files + `role-definitions/` subdir with 9 files):
- `ai-service.ts` — Main AI chat API wrapper, model negotiation, provider selection
- `ai-prompts.ts` — System prompts for design generation, context building
- `ai-types.ts``ChatMessage` (with `attachments?: ChatAttachment[]`), `ChatAttachment` (id, name, mediaType, data, size), `AIDesignRequest`, `OrchestratorPlan`, streaming response types
- `ai-runtime-config.ts` — Configuration constants for AI timeouts, thinking modes, effort levels, prompt length limits
- `model-profiles.ts` — Model capability profiles: adapts thinking mode, effort, timeouts, prompt complexity per model tier (full/standard/basic)
- `agent-identity.ts` — Agent identity assignment (color, name) for concurrent multi-agent generation
- `design-generator.ts` — Top-level `generateDesign`/`generateDesignModification` with orchestrator fallback, re-exports from design-parser and design-canvas-ops
- `design-parser.ts` — Pure JSON/JSONL parsing: `extractJsonFromResponse`, `extractStreamingNodes`, `parseJsonlToTree`, node validation and scoring
- `design-canvas-ops.ts` — Canvas mutation operations: `insertStreamingNode`, `applyNodesToCanvas`, `upsertNodesToCanvas`, `animateNodesToCanvas`, generation state management, sanitization and heuristics
- `design-node-sanitization.ts` — Node cloning and merging utilities: `deepCloneNode`, `setNodeChildren`, `mergeNodeForProgressiveUpsert` (extracted from design-canvas-ops)
- `design-animation.ts` — Fade-in animation coordination for generated design nodes
- `design-validation.ts` — Post-generation screenshot validation using vision API to detect and auto-fix visual issues
- `design-validation-fixes.ts` — Auto-fix strategies for validation-detected issues
- `design-pre-validation.ts` — Pre-validation heuristics before vision API call
- `design-screenshot.ts` — Canvas screenshot capture for validation pipeline
- `design-type-presets.ts` — Design type detection and section presets (landing page, dashboard, mobile, etc.)
- `design-code-generator.ts` — AI-powered code generation from design nodes
- `design-code-prompts.ts` — Prompts for design-to-code generation
- `design-system-generator.ts` — Design system token extraction from generated designs
- `design-system-prompts.ts` — Prompts for design system generation
- `html-renderer.ts` — PenNode tree to HTML rendering for validation screenshots
- `visual-ref-orchestrator.ts` — Visual reference-based orchestration for image-guided generation
- `generation-utils.ts` — Pure utilities for text measurement, size/padding parsing, phone placeholder generation, color extraction
- `icon-resolver.ts` — Auto-resolves AI-generated icon path nodes by name to verified Lucide SVG paths
- `role-resolver.ts` — Registry-based system for applying role-specific defaults (button padding, card gaps) and tree post-pass fixes
- `role-definitions/` — Modular role definition files: index, content, display, interactive, layout, navigation, media, typography, table
- `orchestrator.ts` — Orchestrator entry point: `executeOrchestration`, `callOrchestrator`, plan parsing
- `orchestrator-sub-agent.ts` — Sub-agent execution: `executeSubAgentsSequentially`, `executeSubAgent`, prompt building, retry/fallback logic
- `orchestrator-progress.ts``emitProgress`, `buildFinalStepTags` for streaming progress updates
- `orchestrator-prompts.ts` — Ultra-lightweight orchestrator prompt for spatial decomposition
- `orchestrator-prompt-optimizer.ts` — Prompt preparation, compression, timeout calculation, fallback plan generation
- `context-optimizer.ts` — Chat history trimming, sliding window to prevent unbounded context growth
- **`src/services/figma/`** — Figma `.fig` file import pipeline (14 files):
- `fig-parser.ts` — Binary `.fig` file parser
- `figma-types.ts` — Figma internal type definitions
- `figma-node-mapper.ts` — Maps Figma nodes to PenNodes
- `figma-fill-mapper.ts` — Converts Figma fills to PenFill
- `figma-stroke-mapper.ts` — Converts Figma strokes to PenStroke
- `figma-effect-mapper.ts` — Converts Figma effects to PenEffect
- `figma-layout-mapper.ts` — Maps Figma auto-layout to PenNode layout props
- `figma-text-mapper.ts` — Converts Figma text styles
- `figma-vector-decoder.ts` — Decodes Figma vector geometry
- `figma-color-utils.ts` — Color space conversion utilities
- `figma-image-resolver.ts` — Resolves image blob references
- `figma-clipboard.ts` — Figma clipboard paste handling
- `figma-node-converters.ts` — Figma node conversion utilities
- `figma-tree-builder.ts` — Figma document tree building
- **`src/services/codegen/`** — Multi-platform code generators (9 files, output `var(--name)` for `$variable` refs):
- `react-generator.ts` — React + Tailwind CSS
- `html-generator.ts` — HTML + CSS
- `css-variables-generator.ts` — CSS Variables from design tokens
- `vue-generator.ts` — Vue 3 + CSS
- `svelte-generator.ts` — Svelte + CSS
- `flutter-generator.ts` — Flutter/Dart
- `swiftui-generator.ts` — SwiftUI
- `compose-generator.ts` — Android Jetpack Compose
- `react-native-generator.ts` — React Native
- **`src/hooks/`** — Hooks (5 files):
- `use-keyboard-shortcuts.ts` — Global keyboard event handling: tools, clipboard, undo/redo, save, select all, delete, arrow nudge, z-order, boolean operations (Cmd+Alt+U/S/I)
- `use-electron-menu.ts` — Electron native menu IPC listener: dispatches menu actions (new, open, save, save-as, undo, redo, etc.) to Zustand stores; also handles `onOpenFile` for `.op` file association
- `use-figma-paste.ts` — Handle Figma clipboard paste into canvas
- `use-file-drop.ts` — Handle file drag-and-drop onto canvas
- `use-mcp-sync.ts` — MCP live canvas synchronization hook
- **`src/lib/`** — Utility functions (`utils.ts` with `cn()` for class merging)
- **`src/uikit/`** — UI kit system (3 files + `kits/` subdir):
- `built-in-registry.ts` — Default built-in UIKit with standard UI components
- `kit-import-export.ts` — Import/export UIKits from .pen files with variable reference collection
- `kit-utils.ts` — UIKit utilities: extract components from documents, find reusable nodes, deep clone
- `kits/` — Default kit data: `default-kit.ts`, `default-kit-meta.ts`
- **`src/mcp/`** — MCP server integration (2 files + `tools/` and `utils/` subdirs):
- `server.ts` — MCP server entry point, tool registration (stdio + HTTP modes)
- `document-manager.ts` — MCP utility for reading, writing, and caching PenDocuments from disk; live canvas sync via Nitro API
- `tools/` — Individual MCP tool implementations:
- Core: `open-document.ts`, `batch-get.ts`, `get-selection.ts`, `batch-design.ts` (DSL operations), `node-crud.ts` (insert/update/delete/move/copy/replace)
- Layout: `snapshot-layout.ts`, `find-empty-space.ts`, `import-svg.ts`
- Variables: `variables.ts`, `theme-presets.ts`
- Pages: `pages.ts` (add/remove/rename/reorder/duplicate)
- Layered design: `design-prompt.ts` (segmented retrieval), `design-skeleton.ts`, `design-content.ts`, `design-refine.ts`, `layered-design-defs.ts`
- `utils/` — Shared utilities: `id.ts`, `node-operations.ts` (page-aware `getDocChildren`/`setDocChildren`), `sanitize.ts`, `svg-node-parser.ts`
- **`src/utils/`** — File operations (save/open .pen), export (PNG/SVG), node clone, pen file normalization (format fixes only, preserves `$variable` refs), SVG parser (import SVG to editable PenNodes), syntax highlight, boolean operations (union/subtract/intersect via Paper.js), `app-storage.ts` (application storage/persistence), `arc-path.ts` (SVG arc utilities), `theme-preset-io.ts` (theme preset file I/O)
- **`src/constants/`** — Application constants: `app.ts` (MCP default port, app-level config)
- **`src/i18n/`** — Internationalization setup and locale files
- **`server/api/ai/`** — Nitro server API (8 files): `chat.ts` (streaming SSE with thinking state, multimodal image attachments per provider), `generate.ts` (non-streaming generation), `connect-agent.ts` (Claude Code/Codex CLI/OpenCode/Copilot connection), `models.ts` (model definitions), `validate.ts` (vision-based post-generation validation), `mcp-install.ts` (MCP server install/uninstall into CLI tool configs; auto-detects `node` availability — if missing, falls back to HTTP URL config `{ type: "http", url }` and auto-starts the MCP HTTP server), `install-agent.ts` (agent installation endpoint), `icon.ts` (icon name → SVG path resolution via local Iconify sets). Supports Anthropic API key or Claude Agent SDK (local OAuth) as dual providers
- **`server/utils/`** — Server utilities (7 files):
- `resolve-claude-cli.ts` — Resolves standalone `claude` binary path (handles Nitro bundling issues with SDK's `import.meta.url`)
- `resolve-claude-agent-env.ts` — Builds Claude Agent SDK environment: merges `~/.claude/settings.json` env, validates `ANTHROPIC_CUSTOM_HEADERS`, handles auth token compat
- `opencode-client.ts` — Shared OpenCode client manager, reuses server on port 4096 with random port fallback
- `codex-client.ts` — Codex CLI client wrapper with JSON streaming, thinking mode support, timeout handling, optional `imageFiles` for vision queries
- `copilot-client.ts` — Resolves standalone `copilot` binary path to avoid Bun's `node:sqlite` incompatibility with bundled CLI
- `mcp-server-manager.ts` — MCP HTTP server lifecycle management: spawn/track/stop detached process, PID file persistence
- `mcp-sync-state.ts` — In-memory MCP state: current document/selection, SSE broadcast to connected clients
### CanvasKit/Skia Architecture
- **GPU-accelerated WASM rendering** — CanvasKit (Skia compiled to WASM) renders all canvas content via WebGL surface
- **SkiaEngine class** (`skia-engine.ts`) is the core: owns the render loop, viewport transforms, node flattening, and `SpatialIndex` for hit testing
- **Dirty-flag rendering**`markDirty()` schedules a `requestAnimationFrame` redraw; no continuous rendering loop
- **Node flattening**`syncFromDocument()` walks the PenDocument tree, resolves auto-layout positions via `canvas-layout-engine.ts`, and produces flat `RenderNode[]` with absolute coordinates
- **SpatialIndex** (`skia-hit-test.ts`) — R-tree backed spatial queries for `hitTest()` (click) and `searchRect()` (marquee selection)
- **Coordinate conversion**`screenToScene()` / `sceneToScreen()` in `skia-viewport.ts` handle viewport ↔ scene transforms
- **Bitmap caching**`canvas-zoom-cache.ts` caches rendered frames during zoom/pan for smoother interaction
- **Event handling** — all mouse/keyboard events are handled directly in `skia-canvas.tsx` (no Fabric abstractions)
- **Parent-child transforms** — nodes are flattened to absolute coordinates; `parent-child-transform.ts` propagates transforms to descendants during drag/scale/rotate
- **Legacy Fabric.js**`fabric` package remains as a dependency for `zoomToFitContent` utility; canvas rendering no longer uses Fabric
### Routing
File-based routing via TanStack Router. Routes in `src/routes/`, auto-generated tree in `src/routeTree.gen.ts` (do not edit).
- `/` — Landing page
- `/editor` — Main design editor
- **Single-shot**: `batch_design` or `insert_node` — one call
- **Layered**: `design_skeleton``design_content` × N → `design_refine` — phased generation with focused context
- **Segmented prompts**: `get_design_prompt(section=...)` loads focused subsets (schema, layout, roles, icons, etc.)
### Path Aliases
`@/*` maps to `./src/*` (configured in `tsconfig.json` and `vite.config.ts`).
`@/*` maps to `./src/*` (configured in `apps/web/tsconfig.json` and `apps/web/vite.config.ts`).
### Styling
Tailwind CSS v4 imported via `src/styles.css`. UI primitives from shadcn/ui (`src/components/ui/`). Icons from `lucide-react`. shadcn/ui config in `components.json`.
### Electron Desktop App
- **`electron/main.ts`** — Main process: window creation, Nitro server fork, IPC for native file dialogs, `.op` file association handling (`open-file` event on macOS, CLI args + single-instance lock on Windows/Linux)
- **`electron/preload.ts`** — Context bridge for renderer ↔ main IPC (file dialogs, menu actions, updater state, `onOpenFile`/`readFile` for file association)
- **`electron/app-menu.ts`** — Native application menu configuration (File, Edit, View, Help)
- **`electron/auto-updater.ts`** — Auto-updater implementation: checks GitHub Releases on startup and periodically
- **`electron/constants.ts`** — Electron-specific constants
- **`electron/logger.ts`** — Main process logging
- **`electron-builder.yml`** — Packaging config: macOS (dmg/zip), Windows (nsis/portable), Linux (AppImage/deb), `.op` file association (`fileAssociations`)
- **`scripts/electron-dev.ts`** — Dev workflow: starts Vite → waits for port 3000 → compiles electron/ with esbuild → launches Electron
- Build flow: `BUILD_TARGET=electron bun run build``bun run electron:compile``npx electron-builder`
- In production, Nitro server is forked as a child process on a random port; Electron loads `http://127.0.0.1:{port}/editor`
- Auto-updater checks GitHub Releases on startup and every hour; `update-ready-banner.tsx` shows download progress and "Restart & Install" prompt
- **File association:** `.op` files are registered as OpenPencil documents via `fileAssociations` in `electron-builder.yml`. On macOS the `open-file` app event handles double-click/drag; on Windows/Linux `requestSingleInstanceLock` + `second-instance` event forwards CLI args to the existing window. Pending file paths are queued until the renderer is ready, then sent via `file:open` IPC channel. The renderer (`use-electron-menu.ts`) listens via `onOpenFile`, reads the file through `file:read` IPC, and calls `loadDocument`.
Tailwind CSS v4 imported via `apps/web/src/styles.css`. UI primitives from shadcn/ui. Icons from `lucide-react`.
### CI / CD
- **`.github/workflows/ci.yml`** — Push/PR: type check (`tsc --noEmit`), tests (`vitest`), web build
- **`.github/workflows/build-electron.yml`** — Tag push (`v*`) or manual: builds Electron for macOS, Windows, Linux in parallel, creates draft GitHub Release with all artifacts
- **`.github/workflows/ci.yml`** — Push/PR: type check, tests, web build
- **`.github/workflows/build-electron.yml`** — Tag push (`v*`) or manual: builds Electron for all platforms, creates draft GitHub Release
- **`.github/workflows/docker.yml`** — Docker image build and push
### Version Sync
- **Pre-commit hook** (`.githooks/pre-commit`): extracts version from branch name (e.g. `v0.5.0``0.5.0`) and syncs to all `package.json` files
- **Manual bump:** `bun run bump <version>` to set a specific version across all workspaces
- Requires `git config core.hooksPath .githooks` (one-time setup per clone)
## Code Style
@ -357,34 +115,13 @@ Tailwind CSS v4 imported via `src/styles.css`. UI primitives from shadcn/ui (`sr
## Git Commit Convention
Use [Conventional Commits](https://www.conventionalcommits.org/) format:
Use [Conventional Commits](https://www.conventionalcommits.org/) format: `<type>(<scope>): <subject>`
```
<type>(<scope>): <subject>
**Types:** `feat`, `fix`, `refactor`, `perf`, `style`, `docs`, `test`, `chore`
<body>
```
**Scopes:** `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`, `renderer`, `sdk`
### Type
- `feat` — New feature
- `fix` — Bug fix
- `refactor` — Refactoring (no behavior change)
- `perf` — Performance optimization
- `style` — Code formatting (no logic change)
- `docs` — Documentation
- `test` — Tests
- `chore` — Build / tooling / dependency changes
### Scope
By module: `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`.
### Rules
- Subject in English, lowercase start, no period, imperative mood (e.g. `add`, `fix`, `remove`).
- Body is optional; explain **why** not what.
- One commit per change. Do not mix unrelated changes in a single commit.
**Rules:** Subject in English, lowercase start, no period, imperative mood. Body is optional; explain **why** not what. One commit per change.
## License

View file

@ -58,12 +58,21 @@ ENV NODE_ENV=production NITRO_HOST=0.0.0.0 NITRO_PORT=3000
EXPOSE 3000
CMD ["bun", "run", "./.output/server/index.mjs"]
FROM oven/bun:1 AS with-gemini
WORKDIR /app
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/package.json ./
RUN bun install -g @anthropic-ai/gemini-cli
ENV NODE_ENV=production NITRO_HOST=0.0.0.0 NITRO_PORT=3000
EXPOSE 3000
CMD ["bun", "run", "./.output/server/index.mjs"]
# ── Full: all CLI tools ──
FROM oven/bun:1 AS full
WORKDIR /app
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/package.json ./
RUN bun install -g @anthropic-ai/claude-code @openai/codex opencode-ai @github/copilot
RUN bun install -g @anthropic-ai/claude-code @openai/codex opencode-ai @github/copilot @anthropic-ai/gemini-cli
ENV NODE_ENV=production NITRO_HOST=0.0.0.0 NITRO_PORT=3000
EXPOSE 3000
CMD ["bun", "run", "./.output/server/index.mjs"]

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md"><b>Deutsch</b></a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Voraussetzungen:** [Bun](https://bun.sh/) >= 1.0 und [Node.js](https://nodejs.org/) >= 18
### Docker-Bereitstellung
### Docker
Mehrere Image-Varianten sind verfügbar — wählen Sie die passende für Ihre Anforderungen:
@ -113,6 +113,7 @@ Mehrere Image-Varianten sind verfügbar — wählen Sie die passende für Ihre A
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | Alle CLI-Tools |
**Ausführen (nur Web):**
@ -121,9 +122,9 @@ Mehrere Image-Varianten sind verfügbar — wählen Sie die passende für Ihre A
docker run -d -p 3000:3000 ghcr.io/zseven-w/openpencil:latest
```
**Mit AI CLI ausführen (z.B. Claude Code):**
**Mit KI-CLI ausführen (z.B. Claude Code):**
Der AI-Chat basiert auf Claude CLI OAuth-Login. Verwenden Sie ein Docker-Volume, um die Login-Sitzung beizubehalten:
Der KI-Chat basiert auf Claude CLI OAuth-Login. Verwenden Sie ein Docker-Volume, um die Login-Sitzung beizubehalten:
```bash
# Schritt 1 — Login (einmalig)
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | In den Agenteneinstellungen verbinden (`Cmd+,`) |
| **OpenCode** | In den Agenteneinstellungen verbinden (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` dann in den Agenteneinstellungen verbinden (`Cmd+,`) |
| **Gemini CLI** | In den Agenteneinstellungen verbinden (`Cmd+,`) |
**Modell-Fähigkeitsprofile** — passt Prompts, Thinking-Modus und Timeouts automatisch pro Modellstufe an. Modelle der Vollstufe (Claude) erhalten vollständige Prompts; Standardstufe (GPT-4o, Gemini, DeepSeek) deaktiviert Thinking; Basisstufe (MiniMax, Qwen, Llama, Mistral) erhält vereinfachte verschachtelte JSON-Prompts für maximale Zuverlässigkeit.
@ -213,7 +215,7 @@ docker build --target full -t openpencil-full .
| --- | --- |
| **Frontend** | React 19 · TanStack Start · Tailwind CSS v4 · shadcn/ui |
| **Canvas** | CanvasKit/Skia (WASM, GPU-beschleunigt) |
| **Zustand** | Zustand v5 |
| **State** | Zustand v5 |
| **Server** | Nitro |
| **Desktop** | Electron 35 |
| **KI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Projektstruktur
```text
src/
canvas/ CanvasKit/Skia-Engine — Zeichnen, Synchronisierung, Layout, Hilfslinien, Stiftwerkzeug
components/ React-UI — Editor, Panels, gemeinsame Dialoge, Icons
services/ai/ KI-Chat, Orchestrierer, Designgenerierung, Streaming
services/figma/ Figma-.fig-Binär-Importpipeline
services/codegen React+Tailwind- und HTML+CSS-Codegeneratoren
stores/ Zustand — Canvas, Dokument, Seiten, Verlauf, KI, Einstellungen
variables/ Design-Token-Auflösung und Referenzverwaltung
mcp/ MCP-Server-Tools für externe CLI-Integration
uikit/ Wiederverwendbares Komponenten-Kit-System
server/
api/ai/ Nitro-API — Streaming-Chat, Generierung, Validierung
utils/ Claude CLI, OpenCode, Codex, Copilot-Client-Wrapper
electron/
main.ts Fenster, Nitro-Fork, natives Menü, Auto-Updater
preload.ts IPC-Brücke
openpencil/
├── apps/
│ ├── web/ TanStack Start Web-App
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia-Engine — Zeichnen, Sync, Layout
│ │ │ ├── components/ React-UI — Editor, Panels, gemeinsame Dialoge, Icons
│ │ │ ├── services/ai/ KI-Chat, Orchestrierer, Designgenerierung, Streaming
│ │ │ ├── stores/ Zustand — Canvas, Dokument, Seiten, Verlauf, KI
│ │ │ ├── mcp/ MCP-Server-Tools für externe CLI-Integration
│ │ │ ├── hooks/ Tastaturkürzel, Datei-Drop, Figma-Paste
│ │ │ └── uikit/ Wiederverwendbares Komponenten-Kit-System
│ │ └── server/
│ │ ├── api/ai/ Nitro-API — Streaming-Chat, Generierung, Validierung
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot-Wrapper
│ └── desktop/ Electron-Desktop-App
│ ├── main.ts Fenster, Nitro-Fork, natives Menü, Auto-Updater
│ ├── ipc-handlers.ts Native Dateidialoge, Theme-Sync, Einstellungen-IPC
│ └── preload.ts IPC-Brücke
├── packages/
│ ├── pen-types/ Typdefinitionen für das PenDocument-Modell
│ ├── pen-core/ Dokumentbaum-Operationen, Layout-Engine, Variablen
│ ├── pen-codegen/ Codegeneratoren (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Figma-.fig-Datei-Parser und -Konverter
│ ├── pen-renderer/ Eigenständiger CanvasKit/Skia-Renderer
│ └── pen-sdk/ Umbrella-SDK (re-exportiert alle Pakete)
└── .githooks/ Pre-Commit-Versionssynchronisierung vom Branch-Namen
```
## Tastaturkürzel
@ -266,6 +278,7 @@ bun --bun run dev # Entwicklungsserver (Port 3000)
bun --bun run build # Produktions-Build
bun --bun run test # Tests ausführen (Vitest)
npx tsc --noEmit # Typprüfung
bun run bump <version> # Version über alle package.json synchronisieren
bun run electron:dev # Electron-Entwicklung
bun run electron:build # Electron-Paketierung
```
@ -275,10 +288,11 @@ bun run electron:build # Electron-Paketierung
Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetails und Code-Stil.
1. Forken und klonen
2. Branch erstellen: `git checkout -b feat/my-feature`
3. Prüfungen ausführen: `npx tsc --noEmit && bun --bun run test`
4. Mit [Conventional Commits](https://www.conventionalcommits.org/) committen: `feat(canvas): add rotation snapping`
5. Pull Request gegen `main` öffnen
2. Versionssynchronisierung einrichten: `git config core.hooksPath .githooks`
3. Branch erstellen: `git checkout -b feat/my-feature`
4. Prüfungen ausführen: `npx tsc --noEmit && bun --bun run test`
5. Mit [Conventional Commits](https://www.conventionalcommits.org/) committen: `feat(canvas): add rotation snapping`
6. Pull Request gegen `main` öffnen
## Roadmap
@ -290,6 +304,7 @@ Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetail
- [x] Figma-`.fig`-Import
- [x] Boolesche Operationen (Vereinigung, Subtraktion, Schnittmenge)
- [x] Multi-Modell-Fähigkeitsprofile
- [x] Monorepo-Umstrukturierung mit wiederverwendbaren Paketen
- [ ] Kollaboratives Bearbeiten
- [ ] Plugin-System
@ -302,12 +317,11 @@ Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetail
## Community
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Unserem Discord beitreten</strong>
</a>
— Fragen stellen, Designs teilen, Funktionen vorschlagen.
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md"><b>Español</b></a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Requisitos previos:** [Bun](https://bun.sh/) >= 1.0 y [Node.js](https://nodejs.org/) >= 18
### Despliegue con Docker
### Docker
Hay varias variantes de imagen disponibles — elige la que se ajuste a tus necesidades:
@ -113,6 +113,7 @@ Hay varias variantes de imagen disponibles — elige la que se ajuste a tus nece
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | Todas las herramientas CLI |
**Ejecutar (solo web):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Conectar en Configuración de Agente (`Cmd+,`) |
| **OpenCode** | Conectar en Configuración de Agente (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` y luego conectar en Configuración de Agente (`Cmd+,`) |
| **Gemini CLI** | Conectar en Configuración de Agente (`Cmd+,`) |
**Perfiles de Capacidad de Modelos** — adapta automáticamente los prompts, el modo de pensamiento y los tiempos de espera según el nivel del modelo. Los modelos de nivel completo (Claude) reciben prompts completos; los de nivel estándar (GPT-4o, Gemini, DeepSeek) desactivan el pensamiento; los de nivel básico (MiniMax, Qwen, Llama, Mistral) reciben prompts simplificados de JSON anidado para máxima fiabilidad.
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Estructura del Proyecto
```text
src/
canvas/ Motor CanvasKit/Skia — dibujo, sincronización, diseño, guías, herramienta pluma
components/ Interfaz React — editor, paneles, diálogos compartidos, iconos
services/ai/ Chat de IA, orquestador, generación de diseño, transmisión
services/figma/ Pipeline de importación binaria de Figma .fig
services/codegen Generadores de código React+Tailwind y HTML+CSS
stores/ Zustand — lienzo, documento, páginas, historial, IA, configuración
variables/ Resolución de tokens de diseño y gestión de referencias
mcp/ Herramientas del servidor MCP para integración con CLI externas
uikit/ Sistema de kit de componentes reutilizables
server/
api/ai/ API Nitro — chat en streaming, generación, validación
utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
electron/
main.ts Ventana, fork Nitro, menú nativo, actualizador automático
preload.ts Puente IPC
openpencil/
├── apps/
│ ├── web/ Aplicación web TanStack Start
│ │ ├── src/
│ │ │ ├── canvas/ Motor CanvasKit/Skia — dibujo, sincronización, diseño
│ │ │ ├── components/ Interfaz React — editor, paneles, diálogos compartidos, iconos
│ │ │ ├── services/ai/ Chat de IA, orquestador, generación de diseño, transmisión
│ │ │ ├── stores/ Zustand — lienzo, documento, páginas, historial, IA
│ │ │ ├── mcp/ Herramientas del servidor MCP para integración con CLI externas
│ │ │ ├── hooks/ Atajos de teclado, soltar archivos, pegado de Figma
│ │ │ └── uikit/ Sistema de kit de componentes reutilizables
│ │ └── server/
│ │ ├── api/ai/ API Nitro — chat en streaming, generación, validación
│ │ └── utils/ Wrappers de Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Aplicación de escritorio Electron
│ ├── main.ts Ventana, fork Nitro, menú nativo, actualizador automático
│ ├── ipc-handlers.ts Diálogos de archivos nativos, sincronización de tema, preferencias IPC
│ └── preload.ts Puente IPC
├── packages/
│ ├── pen-types/ Definiciones de tipos para el modelo PenDocument
│ ├── pen-core/ Operaciones de árbol del documento, motor de diseño, variables
│ ├── pen-codegen/ Generadores de código (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Parser y conversor de archivos .fig de Figma
│ ├── pen-renderer/ Renderizador independiente CanvasKit/Skia
│ └── pen-sdk/ SDK global (reexporta todos los paquetes)
└── .githooks/ Sincronización de versión pre-commit desde nombre de rama
```
## Atajos de Teclado
@ -266,6 +278,7 @@ bun --bun run dev # Servidor de desarrollo (puerto 3000)
bun --bun run build # Compilación de producción
bun --bun run test # Ejecutar pruebas (Vitest)
npx tsc --noEmit # Verificación de tipos
bun run bump <version> # Sincronizar versión en todos los package.json
bun run electron:dev # Desarrollo con Electron
bun run electron:build # Empaquetado de Electron
```
@ -275,10 +288,11 @@ bun run electron:build # Empaquetado de Electron
¡Las contribuciones son bienvenidas! Consulta [CLAUDE.md](./CLAUDE.md) para detalles sobre la arquitectura y el estilo de código.
1. Haz fork y clona el repositorio
2. Crea una rama: `git checkout -b feat/my-feature`
3. Ejecuta las verificaciones: `npx tsc --noEmit && bun --bun run test`
4. Haz commit con [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
5. Abre un PR contra `main`
2. Configura la sincronización de versión: `git config core.hooksPath .githooks`
3. Crea una rama: `git checkout -b feat/my-feature`
4. Ejecuta las verificaciones: `npx tsc --noEmit && bun --bun run test`
5. Haz commit con [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
6. Abre un PR contra `main`
## Hoja de Ruta
@ -290,6 +304,7 @@ bun run electron:build # Empaquetado de Electron
- [x] Importación de Figma `.fig`
- [x] Operaciones booleanas (unión, sustracción, intersección)
- [x] Perfiles de capacidad multimodelo
- [x] Reestructuración en monorepo con paquetes reutilizables
- [ ] Edición colaborativa
- [ ] Sistema de plugins
@ -299,6 +314,13 @@ bun run electron:build # Empaquetado de Electron
<img src="https://contrib.rocks/image?repo=ZSeven-W/openpencil" alt="Contributors" />
</a>
## Comunidad
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Únete a nuestro Discord</strong>
</a>
— Haz preguntas, comparte diseños y sugiere funciones.
## Star History
@ -310,12 +332,6 @@ bun run electron:build # Empaquetado de Electron
</picture>
</a>
## Comunidad
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<strong> Únete a nuestro Discord</strong>
</a>
— Haz preguntas, comparte diseños y sugiere funciones.
## Licencia
[MIT](./LICENSE) — Copyright (c) 2026 ZSeven-W

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md"><b>Français</b></a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Prérequis :** [Bun](https://bun.sh/) >= 1.0 et [Node.js](https://nodejs.org/) >= 18
### Déploiement Docker
### Docker
Plusieurs variantes d'images sont disponibles — choisissez celle qui correspond à vos besoins :
@ -113,6 +113,7 @@ Plusieurs variantes d'images sont disponibles — choisissez celle qui correspon
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 Go | Tous les outils CLI |
**Exécuter (web uniquement) :**
@ -167,12 +168,13 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Connecter dans les Paramètres de l'agent (`Cmd+,`) |
| **OpenCode** | Connecter dans les Paramètres de l'agent (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` puis connecter dans les Paramètres de l'agent (`Cmd+,`) |
| **Gemini CLI** | Connecter dans les Paramètres de l'agent (`Cmd+,`) |
**Profils de capacités des modèles** — adapte automatiquement les prompts, le mode de réflexion et les délais d'attente par niveau de modèle. Les modèles de niveau complet (Claude) reçoivent des prompts complets ; le niveau standard (GPT-4o, Gemini, DeepSeek) désactive la réflexion ; le niveau basique (MiniMax, Qwen, Llama, Mistral) reçoit des prompts JSON imbriqués simplifiés pour une fiabilité maximale.
**Serveur MCP**
- Serveur MCP intégré — installation en un clic dans les CLI Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot
- Détection automatique de Node.js — si non installé, bascule automatiquement vers le transport HTTP et démarre le serveur MCP HTTP
- Détection automatique de Node.js — si non installé, bascule vers le transport HTTP et démarre automatiquement le serveur MCP HTTP
- Automatisation du design depuis le terminal : lire, créer et modifier des fichiers `.op` via tout agent compatible MCP
- **Workflow de design en couches**`design_skeleton``design_content``design_refine` pour des designs multi-sections de plus haute fidélité
- **Récupération segmentée des prompts** — chargez uniquement les connaissances de design nécessaires (schéma, layout, rôles, icônes, planification, etc.)
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Structure du projet
```text
src/
canvas/ Moteur CanvasKit/Skia — dessin, sync, mise en page, guides, outil plume
components/ Interface React — éditeur, panneaux, boîtes de dialogue partagées, icônes
services/ai/ Chat IA, orchestrateur, génération de design, streaming
services/figma/ Pipeline d'import binaire Figma .fig
services/codegen Générateurs de code React+Tailwind et HTML+CSS
stores/ Zustand — canevas, document, pages, historique, IA, paramètres
variables/ Résolution des tokens de design et gestion des références
mcp/ Outils serveur MCP pour l'intégration CLI externe
uikit/ Système de kits de composants réutilisables
server/
api/ai/ API Nitro — chat en streaming, génération, validation
utils/ Enveloppes client Claude CLI, OpenCode, Codex, Copilot
electron/
main.ts Fenêtre, fork Nitro, menu natif, mise à jour automatique
preload.ts Pont IPC
openpencil/
├── apps/
│ ├── web/ Application web TanStack Start
│ │ ├── src/
│ │ │ ├── canvas/ Moteur CanvasKit/Skia — dessin, sync, mise en page
│ │ │ ├── components/ Interface React — éditeur, panneaux, boîtes de dialogue partagées, icônes
│ │ │ ├── services/ai/ Chat IA, orchestrateur, génération de design, streaming
│ │ │ ├── stores/ Zustand — canevas, document, pages, historique, IA
│ │ │ ├── mcp/ Outils serveur MCP pour l'intégration CLI externe
│ │ │ ├── hooks/ Raccourcis clavier, dépôt de fichiers, collage Figma
│ │ │ └── uikit/ Système de kits de composants réutilisables
│ │ └── server/
│ │ ├── api/ai/ API Nitro — chat en streaming, génération, validation
│ │ └── utils/ Enveloppes Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Application de bureau Electron
│ ├── main.ts Fenêtre, fork Nitro, menu natif, mise à jour automatique
│ ├── ipc-handlers.ts Dialogues fichiers natifs, sync thème, préférences IPC
│ └── preload.ts Pont IPC
├── packages/
│ ├── pen-types/ Définitions de types pour le modèle PenDocument
│ ├── pen-core/ Opérations sur l'arbre du document, moteur de mise en page, variables
│ ├── pen-codegen/ Générateurs de code (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Parseur et convertisseur de fichiers Figma .fig
│ ├── pen-renderer/ Moteur de rendu CanvasKit/Skia autonome
│ └── pen-sdk/ SDK parapluie (réexporte tous les packages)
└── .githooks/ Synchronisation de version pre-commit depuis le nom de branche
```
## Raccourcis clavier
@ -266,6 +278,7 @@ bun --bun run dev # Serveur de développement (port 3000)
bun --bun run build # Build de production
bun --bun run test # Lancer les tests (Vitest)
npx tsc --noEmit # Vérification des types
bun run bump <version> # Synchroniser la version dans tous les package.json
bun run electron:dev # Développement Electron
bun run electron:build # Packaging Electron
```
@ -275,10 +288,11 @@ bun run electron:build # Packaging Electron
Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour les détails d'architecture et le style de code.
1. Forker et cloner
2. Créer une branche : `git checkout -b feat/my-feature`
3. Exécuter les vérifications : `npx tsc --noEmit && bun --bun run test`
4. Commiter avec [Conventional Commits](https://www.conventionalcommits.org/) : `feat(canvas): add rotation snapping`
5. Ouvrir une PR contre `main`
2. Configurer la synchronisation de version : `git config core.hooksPath .githooks`
3. Créer une branche : `git checkout -b feat/my-feature`
4. Exécuter les vérifications : `npx tsc --noEmit && bun --bun run test`
5. Commiter avec [Conventional Commits](https://www.conventionalcommits.org/) : `feat(canvas): add rotation snapping`
6. Ouvrir une PR contre `main`
## Feuille de route
@ -290,6 +304,7 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
- [x] Import Figma `.fig`
- [x] Opérations booléennes (union, soustraction, intersection)
- [x] Profils de capacités multi-modèles
- [x] Restructuration en monorepo avec packages réutilisables
- [ ] Édition collaborative
- [ ] Système de plugins
@ -299,6 +314,13 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
<img src="https://contrib.rocks/image?repo=ZSeven-W/openpencil" alt="Contributors" />
</a>
## Communauté
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Rejoindre notre Discord</strong>
</a>
— Posez des questions, partagez vos designs, suggérez des fonctionnalités.
## Star History
@ -310,12 +332,6 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
</picture>
</a>
## Communauté
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<strong> Rejoindre notre Discord</strong>
</a>
— Posez des questions, partagez vos designs, suggérez des fonctionnalités.
## Licence
[MIT](./LICENSE) — Copyright (c) 2026 ZSeven-W

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -102,7 +102,7 @@ bun run electron:dev
> **पूर्वापेक्षाएँ:** [Bun](https://bun.sh/) >= 1.0 और [Node.js](https://nodejs.org/) >= 18
### Docker डिप्लॉयमेंट
### Docker
कई इमेज वेरिएंट उपलब्ध हैं — अपनी ज़रूरत के अनुसार चुनें:
@ -113,6 +113,7 @@ bun run electron:dev
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | सभी CLI टूल |
**चलाएँ (केवल वेब):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
| **OpenCode** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` फिर एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
| **Gemini CLI** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
**मॉडल क्षमता प्रोफ़ाइल** — प्रत्येक मॉडल टियर के अनुसार प्रॉम्प्ट, थिंकिंग मोड और टाइमआउट को स्वचालित रूप से अनुकूलित करता है। फुल-टियर मॉडल (Claude) को पूर्ण प्रॉम्प्ट मिलते हैं; स्टैंडर्ड-टियर (GPT-4o, Gemini, DeepSeek) में थिंकिंग अक्षम होती है; बेसिक-टियर (MiniMax, Qwen, Llama, Mistral) को अधिकतम विश्वसनीयता के लिए सरलीकृत नेस्टेड-JSON प्रॉम्प्ट मिलते हैं।
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## प्रोजेक्ट संरचना
```text
src/
canvas/ CanvasKit/Skia इंजन — ड्रॉइंग, सिंक, लेआउट, गाइड, पेन टूल
components/ React UI — एडिटर, पैनल, शेयर्ड डायलॉग, आइकन
services/ai/ AI चैट, ऑर्केस्ट्रेटर, डिज़ाइन जनरेशन, स्ट्रीमिंग
services/figma/ Figma .fig बाइनरी इम्पोर्ट पाइपलाइन
services/codegen React+Tailwind और HTML+CSS कोड जनरेटर
stores/ Zustand — कैनवास, दस्तावेज़, पेज, हिस्ट्री, AI, सेटिंग्स
variables/ डिज़ाइन टोकन रिज़ॉल्यूशन और रेफ़रेंस मैनेजमेंट
mcp/ बाहरी CLI इंटीग्रेशन के लिए MCP सर्वर टूल
uikit/ पुन: उपयोगी कम्पोनेंट किट सिस्टम
server/
api/ai/ Nitro API — स्ट्रीमिंग चैट, जनरेशन, वैलिडेशन
utils/ Claude CLI, OpenCode, Codex, Copilot क्लाइंट रैपर
electron/
main.ts विंडो, Nitro फ़ोर्क, नेटिव मेनू, ऑटो-अपडेटर
preload.ts IPC ब्रिज
openpencil/
├── apps/
│ ├── web/ TanStack Start वेब ऐप
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia इंजन — ड्रॉइंग, सिंक, लेआउट
│ │ │ ├── components/ React UI — एडिटर, पैनल, शेयर्ड डायलॉग, आइकन
│ │ │ ├── services/ai/ AI चैट, ऑर्केस्ट्रेटर, डिज़ाइन जनरेशन, स्ट्रीमिंग
│ │ │ ├── stores/ Zustand — कैनवास, दस्तावेज़, पेज, हिस्ट्री, AI
│ │ │ ├── mcp/ बाहरी CLI इंटीग्रेशन के लिए MCP सर्वर टूल
│ │ │ ├── hooks/ कीबोर्ड शॉर्टकट, फ़ाइल ड्रॉप, Figma पेस्ट
│ │ │ └── uikit/ पुन: उपयोगी कम्पोनेंट किट सिस्टम
│ │ └── server/
│ │ ├── api/ai/ Nitro API — स्ट्रीमिंग चैट, जनरेशन, वैलिडेशन
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot रैपर
│ └── desktop/ Electron डेस्कटॉप ऐप
│ ├── main.ts विंडो, Nitro फ़ोर्क, नेटिव मेनू, ऑटो-अपडेटर
│ ├── ipc-handlers.ts नेटिव फ़ाइल डायलॉग, थीम सिंक, प्राथमिकताएँ IPC
│ └── preload.ts IPC ब्रिज
├── packages/
│ ├── pen-types/ PenDocument मॉडल के लिए टाइप परिभाषाएँ
│ ├── pen-core/ दस्तावेज़ ट्री ऑपरेशन, लेआउट इंजन, वेरिएबल
│ ├── pen-codegen/ कोड जनरेटर (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Figma .fig फ़ाइल पार्सर और कनवर्टर
│ ├── pen-renderer/ स्टैंडअलोन CanvasKit/Skia रेंडरर
│ └── pen-sdk/ अम्ब्रेला SDK (सभी पैकेज री-एक्सपोर्ट)
└── .githooks/ ब्रांच नाम से प्री-कमिट वर्शन सिंक
```
## कीबोर्ड शॉर्टकट
@ -266,6 +278,7 @@ bun --bun run dev # डेव सर्वर (पोर्ट 3000)
bun --bun run build # प्रोडक्शन बिल्ड
bun --bun run test # टेस्ट चलाएँ (Vitest)
npx tsc --noEmit # टाइप चेक
bun run bump <version> # सभी package.json में वर्शन सिंक करें
bun run electron:dev # Electron डेव
bun run electron:build # Electron पैकेज
```
@ -275,10 +288,11 @@ bun run electron:build # Electron पैकेज
योगदान का स्वागत है! आर्किटेक्चर विवरण और कोड स्टाइल के लिए [CLAUDE.md](./CLAUDE.md) देखें।
1. फ़ोर्क और क्लोन करें
2. ब्रांच बनाएँ: `git checkout -b feat/my-feature`
3. चेक चलाएँ: `npx tsc --noEmit && bun --bun run test`
4. [Conventional Commits](https://www.conventionalcommits.org/) के साथ कमिट करें: `feat(canvas): add rotation snapping`
5. `main` के विरुद्ध PR खोलें
2. वर्शन सिंक सेटअप करें: `git config core.hooksPath .githooks`
3. ब्रांच बनाएँ: `git checkout -b feat/my-feature`
4. चेक चलाएँ: `npx tsc --noEmit && bun --bun run test`
5. [Conventional Commits](https://www.conventionalcommits.org/) के साथ कमिट करें: `feat(canvas): add rotation snapping`
6. `main` के विरुद्ध PR खोलें
## रोडमैप
@ -290,6 +304,7 @@ bun run electron:build # Electron पैकेज
- [x] Figma `.fig` इम्पोर्ट
- [x] बूलियन ऑपरेशन (यूनियन, सबट्रैक्ट, इंटरसेक्ट)
- [x] मल्टी-मॉडल क्षमता प्रोफ़ाइल
- [x] पुन: उपयोगी पैकेज के साथ मोनोरेपो पुनर्गठन
- [ ] सहयोगी संपादन
- [ ] प्लगइन सिस्टम
@ -302,12 +317,11 @@ bun run electron:build # Electron पैकेज
## समुदाय
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> हमारे Discord में शामिल हों</strong>
</a>
— प्रश्न पूछें, डिज़ाइन साझा करें, सुविधाएँ सुझाएँ।
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md"><b>Bahasa Indonesia</b></a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Prasyarat:** [Bun](https://bun.sh/) >= 1.0 dan [Node.js](https://nodejs.org/) >= 18
### Deployment Docker
### Docker
Tersedia beberapa varian image — pilih yang sesuai kebutuhan Anda:
@ -113,6 +113,7 @@ Tersedia beberapa varian image — pilih yang sesuai kebutuhan Anda:
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | Semua alat CLI |
**Jalankan (hanya web):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Hubungkan di Pengaturan Agen (`Cmd+,`) |
| **OpenCode** | Hubungkan di Pengaturan Agen (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` lalu hubungkan di Pengaturan Agen (`Cmd+,`) |
| **Gemini CLI** | Hubungkan di Pengaturan Agen (`Cmd+,`) |
**Profil Kemampuan Model** — secara otomatis menyesuaikan prompt, mode thinking, dan timeout per tingkatan model. Model tingkat penuh (Claude) mendapat prompt lengkap; tingkat standar (GPT-4o, Gemini, DeepSeek) menonaktifkan thinking; tingkat dasar (MiniMax, Qwen, Llama, Mistral) mendapat prompt JSON bertingkat yang disederhanakan untuk keandalan maksimum.
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Struktur Proyek
```text
src/
canvas/ Mesin CanvasKit/Skia — menggambar, sinkronisasi, tata letak, panduan, alat pen
components/ UI React — editor, panel, dialog bersama, ikon
services/ai/ Chat AI, orkestrator, pembuatan desain, streaming
services/figma/ Pipeline impor biner Figma .fig
services/codegen Generator kode React+Tailwind dan HTML+CSS
stores/ Zustand — kanvas, dokumen, halaman, riwayat, AI, pengaturan
variables/ Resolusi token desain dan manajemen referensi
mcp/ Alat server MCP untuk integrasi CLI eksternal
uikit/ Sistem kit komponen yang dapat digunakan ulang
server/
api/ai/ Nitro API — chat streaming, pembuatan, validasi
utils/ Pembungkus klien Claude CLI, OpenCode, Codex, Copilot
electron/
main.ts Jendela, fork Nitro, menu native, pembaruan otomatis
preload.ts Jembatan IPC
openpencil/
├── apps/
│ ├── web/ Aplikasi web TanStack Start
│ │ ├── src/
│ │ │ ├── canvas/ Mesin CanvasKit/Skia — menggambar, sinkronisasi, tata letak
│ │ │ ├── components/ UI React — editor, panel, dialog bersama, ikon
│ │ │ ├── services/ai/ Chat AI, orkestrator, pembuatan desain, streaming
│ │ │ ├── stores/ Zustand — kanvas, dokumen, halaman, riwayat, AI
│ │ │ ├── mcp/ Alat server MCP untuk integrasi CLI eksternal
│ │ │ ├── hooks/ Pintasan keyboard, seret file, tempel Figma
│ │ │ └── uikit/ Sistem kit komponen yang dapat digunakan ulang
│ │ └── server/
│ │ ├── api/ai/ Nitro API — chat streaming, pembuatan, validasi
│ │ └── utils/ Pembungkus Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Aplikasi desktop Electron
│ ├── main.ts Jendela, fork Nitro, menu native, pembaruan otomatis
│ ├── ipc-handlers.ts Dialog file native, sinkronisasi tema, preferensi IPC
│ └── preload.ts Jembatan IPC
├── packages/
│ ├── pen-types/ Definisi tipe untuk model PenDocument
│ ├── pen-core/ Operasi pohon dokumen, mesin tata letak, variabel
│ ├── pen-codegen/ Generator kode (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Parser dan konverter file Figma .fig
│ ├── pen-renderer/ Renderer CanvasKit/Skia mandiri
│ └── pen-sdk/ SDK payung (re-ekspor semua paket)
└── .githooks/ Pre-commit sinkronisasi versi dari nama branch
```
## Pintasan Keyboard
@ -266,6 +278,7 @@ bun --bun run dev # Server pengembangan (port 3000)
bun --bun run build # Build produksi
bun --bun run test # Jalankan pengujian (Vitest)
npx tsc --noEmit # Pemeriksaan tipe
bun run bump <version> # Sinkronisasi versi di semua package.json
bun run electron:dev # Pengembangan Electron
bun run electron:build # Paket Electron
```
@ -275,10 +288,11 @@ bun run electron:build # Paket Electron
Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitektur dan gaya kode.
1. Fork dan clone
2. Buat cabang: `git checkout -b feat/my-feature`
3. Jalankan pemeriksaan: `npx tsc --noEmit && bun --bun run test`
4. Commit dengan [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
5. Buka PR ke `main`
2. Atur sinkronisasi versi: `git config core.hooksPath .githooks`
3. Buat cabang: `git checkout -b feat/my-feature`
4. Jalankan pemeriksaan: `npx tsc --noEmit && bun --bun run test`
5. Commit dengan [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
6. Buka PR ke `main`
## Peta Jalan
@ -290,6 +304,7 @@ Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitekt
- [x] Impor Figma `.fig`
- [x] Operasi boolean (gabung, kurangi, potong)
- [x] Profil kemampuan multi-model
- [x] Restrukturisasi monorepo dengan paket yang dapat digunakan ulang
- [ ] Pengeditan kolaboratif
- [ ] Sistem plugin
@ -302,12 +317,11 @@ Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitekt
## Komunitas
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Bergabung dengan Discord kami</strong>
</a>
— Ajukan pertanyaan, bagikan desain, sarankan fitur.
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -102,7 +102,7 @@ bun run electron:dev
> **前提条件:** [Bun](https://bun.sh/) >= 1.0 および [Node.js](https://nodejs.org/) >= 18
### Docker デプロイ
### Docker
複数のイメージバリアントが利用可能です — ニーズに合ったものを選択してください:
@ -113,6 +113,7 @@ bun run electron:dev
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | すべての CLI ツール |
**実行Web のみ):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | エージェント設定で接続(`Cmd+,` |
| **OpenCode** | エージェント設定で接続(`Cmd+,` |
| **GitHub Copilot** | `copilot login` 後、エージェント設定で接続(`Cmd+,` |
| **Gemini CLI** | エージェント設定で接続(`Cmd+,` |
**モデル能力プロファイル** — モデルの階層に応じてプロンプト、シンキングモード、タイムアウトを自動適応。フル階層モデルClaudeには完全なプロンプト、標準階層GPT-4o、Gemini、DeepSeekではシンキングを無効化、ベーシック階層MiniMax、Qwen、Llama、Mistralには最大限の信頼性のために簡略化されたネスト JSON プロンプトを使用。
@ -194,7 +196,7 @@ docker build --target full -t openpencil-full .
**デザインシステム**
- デザイン変数 — カラー・数値・文字列トークン、`$variable` 参照付き
- マルチテーマサポート — 複数のテーマ軸、各軸に複数バリアント(ライト/ダーク、コンパクト/コンフォータブル
- マルチテーマサポート — 複数のテーマ軸、各軸に複数バリアント(Light/Dark、Compact/Comfortable
- コンポーネントシステム — インスタンスとオーバーライドを持つ再利用可能なコンポーネント
- CSS 同期 — カスタムプロパティの自動生成、コード出力に `var(--name)` を使用
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## プロジェクト構成
```text
src/
canvas/ CanvasKit/Skia エンジン — 描画、同期、レイアウト、ガイド、ペンツール
components/ React UI — エディター、パネル、共有ダイアログ、アイコン
services/ai/ AI チャット、オーケストレーター、デザイン生成、ストリーミング
services/figma/ Figma .fig バイナリインポートパイプライン
services/codegen React+Tailwind および HTML+CSS コードジェネレーター
stores/ Zustand — キャンバス、ドキュメント、ページ、履歴、AI、設定
variables/ デザイントークンの解決とリファレンス管理
mcp/ 外部 CLI 統合用 MCP サーバーツール
uikit/ 再利用可能なコンポーネントキットシステム
server/
api/ai/ Nitro API — ストリーミングチャット、生成、バリデーション
utils/ Claude CLI、OpenCode、Codex、Copilot クライアントラッパー
electron/
main.ts ウィンドウ、Nitro フォーク、ネイティブメニュー、自動アップデーター
preload.ts IPC ブリッジ
openpencil/
├── apps/
│ ├── web/ TanStack Start Web アプリ
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia エンジン — 描画、同期、レイアウト
│ │ │ ├── components/ React UI — エディター、パネル、共有ダイアログ、アイコン
│ │ │ ├── services/ai/ AI チャット、オーケストレーター、デザイン生成、ストリーミング
│ │ │ ├── stores/ Zustand — キャンバス、ドキュメント、ページ、履歴、AI
│ │ │ ├── mcp/ 外部 CLI 統合用 MCP サーバーツール
│ │ │ ├── hooks/ キーボードショートカット、ファイルドロップ、Figma ペースト
│ │ │ └── uikit/ 再利用可能なコンポーネントキットシステム
│ │ └── server/
│ │ ├── api/ai/ Nitro API — ストリーミングチャット、生成、バリデーション
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot ラッパー
│ └── desktop/ Electron デスクトップアプリ
│ ├── main.ts ウィンドウ、Nitro フォーク、ネイティブメニュー、自動アップデーター
│ ├── ipc-handlers.ts ネイティブファイルダイアログ、テーマ同期、設定 IPC
│ └── preload.ts IPC ブリッジ
├── packages/
│ ├── pen-types/ PenDocument モデルの型定義
│ ├── pen-core/ ドキュメントツリー操作、レイアウトエンジン、変数
│ ├── pen-codegen/ コードジェネレーターReact、HTML、Vue、Flutter、...
│ ├── pen-figma/ Figma .fig ファイルパーサーとコンバーター
│ ├── pen-renderer/ スタンドアロン CanvasKit/Skia レンダラー
│ └── pen-sdk/ アンブレラ SDK全パッケージの再エクスポート
└── .githooks/ ブランチ名からのプレコミットバージョン同期
```
## キーボードショートカット
@ -266,6 +278,7 @@ bun --bun run dev # 開発サーバー(ポート 3000
bun --bun run build # 本番ビルド
bun --bun run test # テストの実行Vitest
npx tsc --noEmit # 型チェック
bun run bump <version> # すべての package.json のバージョンを同期
bun run electron:dev # Electron 開発モード
bun run electron:build # Electron パッケージング
```
@ -275,10 +288,11 @@ bun run electron:build # Electron パッケージング
コントリビューションを歓迎します!アーキテクチャの詳細とコードスタイルについては [CLAUDE.md](./CLAUDE.md) をご覧ください。
1. フォークしてクローン
2. ブランチを作成:`git checkout -b feat/my-feature`
3. チェックを実行:`npx tsc --noEmit && bun --bun run test`
4. [Conventional Commits](https://www.conventionalcommits.org/) 形式でコミット:`feat(canvas): add rotation snapping`
5. `main` ブランチに PR を作成
2. バージョン同期を設定:`git config core.hooksPath .githooks`
3. ブランチを作成:`git checkout -b feat/my-feature`
4. チェックを実行:`npx tsc --noEmit && bun --bun run test`
5. [Conventional Commits](https://www.conventionalcommits.org/) 形式でコミット:`feat(canvas): add rotation snapping`
6. `main` ブランチに PR を作成
## ロードマップ
@ -290,6 +304,7 @@ bun run electron:build # Electron パッケージング
- [x] Figma `.fig` インポート
- [x] ブーリアン演算(合体、型抜き、交差)
- [x] マルチモデル能力プロファイル
- [x] 再利用可能なパッケージによるモノレポ構成
- [ ] 共同編集
- [ ] プラグインシステム
@ -302,12 +317,11 @@ bun run electron:build # Electron パッケージング
## コミュニティ
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Discord に参加する</strong>
</a>
— 質問、デザインの共有、機能のリクエストはこちら。
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -102,7 +102,7 @@ bun run electron:dev
> **필수 조건:** [Bun](https://bun.sh/) >= 1.0 및 [Node.js](https://nodejs.org/) >= 18
### Docker 배포
### Docker
여러 이미지 변형을 사용할 수 있습니다 — 필요에 맞는 것을 선택하세요:
@ -113,6 +113,7 @@ bun run electron:dev
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | 모든 CLI 도구 |
**실행 (웹만):**
@ -155,8 +156,8 @@ docker build --target full -t openpencil-full .
**프롬프트에서 UI로**
- **텍스트-투-디자인** — 페이지를 설명하면 스트리밍 애니메이션으로 실시간으로 캔버스에 생성
- **오케스트레이터** — 복잡한 페이지를 공간적 서브태스크로 분해하여 병렬 생성 지원
- **디자인 수정** — 요소를 선택하고 자연어로 변경 내용을 설명
- **오케스트레이터** — 복잡한 페이지를 공간적 서브태스크로 분해하여 병렬 생성
- **디자인 수정** — 요소를 선택하고 자연어로 변경 사항을 설명
- **비전 입력** — 스크린샷이나 목업을 참조로 첨부하여 디자인
**멀티 에이전트 지원**
@ -167,15 +168,16 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | 에이전트 설정에서 연결 (`Cmd+,`) |
| **OpenCode** | 에이전트 설정에서 연결 (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` 후 에이전트 설정에서 연결 (`Cmd+,`) |
| **Gemini CLI** | 에이전트 설정에서 연결 (`Cmd+,`) |
**모델 역량 프로파일** — 모델 티어에 따라 프롬프트, 사고 모드, 타임아웃을 자동 조정합니다. 풀 티어 모델(Claude)은 완전한 프롬프트를 받고, 스탠다드 티어(GPT-4o, Gemini, DeepSeek)는 사고 모드를 비활성화하며, 베이직 티어(MiniMax, Qwen, Llama, Mistral)는 최대 안정성을 위해 단순화된 중첩 JSON 프롬프트를 받습니다.
**MCP 서버**
- 내장 MCP 서버 — Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLI에 원클릭 설치
- Node.js 자동 감지 — 설치되지 않은 경우 HTTP 전송 모드로 자동 대체하고 MCP HTTP 서버를 자동 시작
- 터미널에서 디자인 자동화: MCP 호환 에이전트를 통해 `.op` 파일 읽기, 생성, 편집 가능
- 터미널에서 디자인 자동화: MCP 호환 에이전트를 통해 `.op` 파일 읽기, 생성, 편집
- **계층적 디자인 워크플로**`design_skeleton``design_content``design_refine`으로 더 높은 충실도의 멀티 섹션 디자인
- **세그먼트 프롬프트 검색** — 필요한 디자인 지식만 로드 (스키마, 레이아웃, 역할, 아이콘, 계획 등)
- **세그먼트 프롬프트 검색** — 필요한 디자인 지식만 로드 (schema, layout, roles, icons, planning 등)
- 멀티 페이지 지원 — MCP 도구를 통해 페이지 생성, 이름 변경, 순서 변경, 복제
**코드 생성**
@ -186,15 +188,15 @@ docker build --target full -t openpencil-full .
**캔버스 & 드로잉**
- 팬, 줌, 스마트 정렬 가이드, 스냅 지원의 무한 캔버스
- 사각형, 타원, 직선, 다각형, 펜(베지어), Frame, 텍스트
- 불리언 연산 — 합치기, 빼기, 교차 (컨텍스트 도구 모음)
- Rectangle, Ellipse, Line, Polygon, Pen(Bezier), Frame, Text
- 불리언 연산 — 합치기, 빼기, 교차 (컨텍스트 툴바)
- 아이콘 피커(Iconify)와 이미지 가져오기(PNG/JPEG/SVG/WebP/GIF)
- 오토 레이아웃 — 수직/수평 방향, 갭·패딩·justify·align 지원
- 오토 레이아웃 — 수직/수평 방향, gap, padding, justify, align 지원
- 탭 내비게이션이 있는 멀티 페이지 문서
**디자인 시스템**
- 디자인 변수 — 컬러·숫자·문자열 토큰, `$variable` 참조 지원
- 멀티 테마 지원 — 여러 테마 축, 각 축에 여러 변형(라이트/다크, 컴팩트/컴포터블)
- 디자인 변수 — 컬러, 숫자, 문자열 토큰, `$variable` 참조 지원
- 멀티 테마 지원 — 여러 테마 축, 각 축에 변형(Light/Dark, Compact/Comfortable)
- 컴포넌트 시스템 — 인스턴스와 오버라이드를 가진 재사용 가능한 컴포넌트
- CSS 동기화 — 커스텀 프로퍼티 자동 생성, 코드 출력에 `var(--name)` 사용
@ -202,7 +204,7 @@ docker build --target full -t openpencil-full .
- 레이아웃, 채우기, 선, 효과, 텍스트, 이미지, 벡터를 유지하며 `.fig` 파일 가져오기
**데스크톱 앱**
- Electron을 통한 네이티브 macOS·Windows·Linux 지원
- Electron을 통한 네이티브 macOS, Windows, Linux 지원
- `.op` 파일 연결 — 더블 클릭으로 열기, 단일 인스턴스 잠금
- GitHub Releases에서 자동 업데이트
- 네이티브 애플리케이션 메뉴와 파일 다이얼로그
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## 프로젝트 구조
```text
src/
canvas/ CanvasKit/Skia 엔진 — 드로잉, 동기화, 레이아웃, 가이드, 펜 툴
components/ React UI — 에디터, 패널, 공유 다이얼로그, 아이콘
services/ai/ AI 채팅, 오케스트레이터, 디자인 생성, 스트리밍
services/figma/ Figma .fig 바이너리 가져오기 파이프라인
services/codegen React+Tailwind 및 HTML+CSS 코드 생성기
stores/ Zustand — 캔버스, 문서, 페이지, 히스토리, AI, 설정
variables/ 디자인 토큰 해석 및 참조 관리
mcp/ 외부 CLI 통합용 MCP 서버 툴
uikit/ 재사용 가능한 컴포넌트 킷 시스템
server/
api/ai/ Nitro API — 스트리밍 채팅, 생성, 유효성 검사
utils/ Claude CLI, OpenCode, Codex, Copilot 클라이언트 래퍼
electron/
main.ts 윈도우, Nitro 포크, 네이티브 메뉴, 자동 업데이터
preload.ts IPC 브리지
openpencil/
├── apps/
│ ├── web/ TanStack Start 웹 앱
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia 엔진 — 드로잉, 동기화, 레이아웃
│ │ │ ├── components/ React UI — 에디터, 패널, 공유 다이얼로그, 아이콘
│ │ │ ├── services/ai/ AI 채팅, 오케스트레이터, 디자인 생성, 스트리밍
│ │ │ ├── stores/ Zustand — 캔버스, 문서, 페이지, 히스토리, AI
│ │ │ ├── mcp/ 외부 CLI 통합용 MCP 서버 도구
│ │ │ ├── hooks/ 키보드 단축키, 파일 드롭, Figma 붙여넣기
│ │ │ └── uikit/ 재사용 가능한 컴포넌트 킷 시스템
│ │ └── server/
│ │ ├── api/ai/ Nitro API — 스트리밍 채팅, 생성, 유효성 검사
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot 래퍼
│ └── desktop/ Electron 데스크톱 앱
│ ├── main.ts 윈도우, Nitro 포크, 네이티브 메뉴, 자동 업데이터
│ ├── ipc-handlers.ts 네이티브 파일 대화상자, 테마 동기화, 환경설정 IPC
│ └── preload.ts IPC 브리지
├── packages/
│ ├── pen-types/ PenDocument 모델 타입 정의
│ ├── pen-core/ 문서 트리 연산, 레이아웃 엔진, 변수
│ ├── pen-codegen/ 코드 생성기 (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Figma .fig 파일 파서 및 변환기
│ ├── pen-renderer/ 독립형 CanvasKit/Skia 렌더러
│ └── pen-sdk/ 통합 SDK (모든 패키지 재export)
└── .githooks/ 브랜치 이름에서 버전 동기화를 위한 pre-commit
```
## 키보드 단축키
@ -266,6 +278,7 @@ bun --bun run dev # 개발 서버 (포트 3000)
bun --bun run build # 프로덕션 빌드
bun --bun run test # 테스트 실행 (Vitest)
npx tsc --noEmit # 타입 검사
bun run bump <version> # 모든 package.json에 버전 동기화
bun run electron:dev # Electron 개발 모드
bun run electron:build # Electron 패키징
```
@ -275,21 +288,23 @@ bun run electron:build # Electron 패키징
기여를 환영합니다! 아키텍처 세부 정보와 코드 스타일은 [CLAUDE.md](./CLAUDE.md)를 참고하세요.
1. 포크 후 클론
2. 브랜치 생성: `git checkout -b feat/my-feature`
3. 검사 실행: `npx tsc --noEmit && bun --bun run test`
4. [Conventional Commits](https://www.conventionalcommits.org/) 형식으로 커밋: `feat(canvas): add rotation snapping`
5. `main` 브랜치에 PR 생성
2. 버전 동기화 설정: `git config core.hooksPath .githooks`
3. 브랜치 생성: `git checkout -b feat/my-feature`
4. 검사 실행: `npx tsc --noEmit && bun --bun run test`
5. [Conventional Commits](https://www.conventionalcommits.org/) 형식으로 커밋: `feat(canvas): add rotation snapping`
6. `main` 브랜치에 PR 생성
## 로드맵
- [x] CSS 동기화가 있는 디자인 변수 & 토큰
- [x] 컴포넌트 시스템(인스턴스 & 오버라이드)
- [x] 컴포넌트 시스템 (인스턴스 & 오버라이드)
- [x] 오케스트레이터를 통한 AI 디자인 생성
- [x] 계층적 디자인 워크플로가 포함된 MCP 서버 통합
- [x] 멀티 페이지 지원
- [x] Figma `.fig` 가져오기
- [x] 불리언 연산(결합, 빼기, 교차)
- [x] 불리언 연산 (합치기, 빼기, 교차)
- [x] 멀티 모델 역량 프로파일
- [x] 재사용 가능한 패키지를 포함한 모노레포 구조 변경
- [ ] 공동 편집
- [ ] 플러그인 시스템
@ -302,12 +317,11 @@ bun run electron:build # Electron 패키징
## 커뮤니티
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Discord에 참여하기</strong>
</a>
— 질문하기, 디자인 공유, 기능 제안.
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -113,6 +113,7 @@ Multiple image variants are available — pick the one that fits your needs:
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | All CLI tools |
**Run (web only):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Connect in Agent Settings (`Cmd+,`) |
| **OpenCode** | Connect in Agent Settings (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` then connect in Agent Settings (`Cmd+,`) |
| **Gemini CLI** | Connect in Agent Settings (`Cmd+,`) |
**Model Capability Profiles** — automatically adapts prompts, thinking mode, and timeouts per model tier. Full-tier models (Claude) get complete prompts; standard-tier (GPT-4o, Gemini, DeepSeek) disable thinking; basic-tier (MiniMax, Qwen, Llama, Mistral) get simplified nested-JSON prompts for maximum reliability.
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Project Structure
```text
src/
canvas/ CanvasKit/Skia engine — drawing, sync, layout, guides, pen tool
components/ React UI — editor, panels, shared dialogs, icons
services/ai/ AI chat, orchestrator, design generation, streaming
services/figma/ Figma .fig binary import pipeline
services/codegen Multi-platform code generators (React, HTML, Vue, Svelte, Flutter, SwiftUI, Compose, React Native)
stores/ Zustand — canvas, document, pages, history, AI, settings
variables/ Design token resolution and reference management
mcp/ MCP server tools for external CLI integration
uikit/ Reusable component kit system
server/
api/ai/ Nitro API — streaming chat, generation, validation
utils/ Claude CLI, OpenCode, Codex, Copilot client wrappers
electron/
main.ts Window, Nitro fork, native menu, auto-updater
preload.ts IPC bridge
openpencil/
├── apps/
│ ├── web/ TanStack Start web app
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia engine — drawing, sync, layout
│ │ │ ├── components/ React UI — editor, panels, shared dialogs, icons
│ │ │ ├── services/ai/ AI chat, orchestrator, design generation, streaming
│ │ │ ├── stores/ Zustand — canvas, document, pages, history, AI
│ │ │ ├── mcp/ MCP server tools for external CLI integration
│ │ │ ├── hooks/ Keyboard shortcuts, file drop, Figma paste
│ │ │ └── uikit/ Reusable component kit system
│ │ └── server/
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
│ └── desktop/ Electron desktop app
│ ├── main.ts Window, Nitro fork, native menu, auto-updater
│ ├── ipc-handlers.ts Native file dialogs, theme sync, prefs IPC
│ └── preload.ts IPC bridge
├── packages/
│ ├── pen-types/ Type definitions for PenDocument model
│ ├── pen-core/ Document tree ops, layout engine, variables
│ ├── pen-codegen/ Code generators (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Figma .fig file parser and converter
│ ├── pen-renderer/ Standalone CanvasKit/Skia renderer
│ └── pen-sdk/ Umbrella SDK (re-exports all packages)
└── .githooks/ Pre-commit version sync from branch name
```
## Keyboard Shortcuts
@ -266,6 +278,7 @@ bun --bun run dev # Dev server (port 3000)
bun --bun run build # Production build
bun --bun run test # Run tests (Vitest)
npx tsc --noEmit # Type check
bun run bump <version> # Sync version across all package.json
bun run electron:dev # Electron dev
bun run electron:build # Electron package
```
@ -275,10 +288,11 @@ bun run electron:build # Electron package
Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details and code style.
1. Fork and clone
2. Create a branch: `git checkout -b feat/my-feature`
3. Run checks: `npx tsc --noEmit && bun --bun run test`
4. Commit with [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
5. Open a PR against `main`
2. Set up version sync: `git config core.hooksPath .githooks`
3. Create a branch: `git checkout -b feat/my-feature`
4. Run checks: `npx tsc --noEmit && bun --bun run test`
5. Commit with [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
6. Open a PR against `main`
## Roadmap
@ -290,6 +304,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
- [x] Figma `.fig` import
- [x] Boolean operations (union, subtract, intersect)
- [x] Multi-model capability profiles
- [x] Monorepo restructure with reusable packages
- [ ] Collaborative editing
- [ ] Plugin system
@ -302,7 +317,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
## Community
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Join our Discord</strong>
</a>
— Ask questions, share designs, suggest features.

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md"><b>Português</b></a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Pré-requisitos:** [Bun](https://bun.sh/) >= 1.0 e [Node.js](https://nodejs.org/) >= 18
### Implantação com Docker
### Docker
Várias variantes de imagem estão disponíveis — escolha a que se adequa às suas necessidades:
@ -113,6 +113,7 @@ Várias variantes de imagem estão disponíveis — escolha a que se adequa às
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | Todas as ferramentas CLI |
**Executar (apenas web):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Conectar nas Configurações do Agente (`Cmd+,`) |
| **OpenCode** | Conectar nas Configurações do Agente (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` e depois conectar nas Configurações do Agente (`Cmd+,`) |
| **Gemini CLI** | Conectar nas Configurações do Agente (`Cmd+,`) |
**Perfis de Capacidade de Modelo** — adapta automaticamente prompts, modo de thinking e timeouts por nível de modelo. Modelos de nível completo (Claude) recebem prompts completos; nível padrão (GPT-4o, Gemini, DeepSeek) desativam thinking; nível básico (MiniMax, Qwen, Llama, Mistral) recebem prompts simplificados de JSON aninhado para máxima confiabilidade.
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Estrutura do Projeto
```text
src/
canvas/ Motor CanvasKit/Skia — desenho, sincronização, layout, guias, ferramenta caneta
components/ UI React — editor, painéis, diálogos compartilhados, ícones
services/ai/ Chat IA, orquestrador, geração de design, streaming
services/figma/ Pipeline de importação binária do Figma .fig
services/codegen Geradores de código React+Tailwind e HTML+CSS
stores/ Zustand — canvas, documento, páginas, histórico, IA, configurações
variables/ Resolução de tokens de design e gerenciamento de referências
mcp/ Ferramentas do servidor MCP para integração com CLI externo
uikit/ Sistema de kit de componentes reutilizáveis
server/
api/ai/ API Nitro — chat em streaming, geração, validação
utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
electron/
main.ts Janela, fork do Nitro, menu nativo, atualizador automático
preload.ts Ponte IPC
openpencil/
├── apps/
│ ├── web/ Aplicação web TanStack Start
│ │ ├── src/
│ │ │ ├── canvas/ Motor CanvasKit/Skia — desenho, sincronização, layout
│ │ │ ├── components/ UI React — editor, painéis, diálogos compartilhados, ícones
│ │ │ ├── services/ai/ Chat IA, orquestrador, geração de design, streaming
│ │ │ ├── stores/ Zustand — canvas, documento, páginas, histórico, IA
│ │ │ ├── mcp/ Ferramentas do servidor MCP para integração com CLI externo
│ │ │ ├── hooks/ Atalhos de teclado, soltar arquivos, colar do Figma
│ │ │ └── uikit/ Sistema de kit de componentes reutilizáveis
│ │ └── server/
│ │ ├── api/ai/ API Nitro — chat em streaming, geração, validação
│ │ └── utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Aplicativo desktop Electron
│ ├── main.ts Janela, fork do Nitro, menu nativo, atualizador automático
│ ├── ipc-handlers.ts Diálogos de arquivo nativos, sincronização de tema, preferências IPC
│ └── preload.ts Ponte IPC
├── packages/
│ ├── pen-types/ Definições de tipos para o modelo PenDocument
│ ├── pen-core/ Operações de árvore de documento, motor de layout, variáveis
│ ├── pen-codegen/ Geradores de código (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Parser e conversor de arquivos .fig do Figma
│ ├── pen-renderer/ Renderizador CanvasKit/Skia independente
│ └── pen-sdk/ SDK guarda-chuva (re-exporta todos os pacotes)
└── .githooks/ Sincronização de versão no pre-commit a partir do nome da branch
```
## Atalhos de Teclado
@ -266,6 +278,7 @@ bun --bun run dev # Servidor de desenvolvimento (porta 3000)
bun --bun run build # Build de produção
bun --bun run test # Executar testes (Vitest)
npx tsc --noEmit # Verificação de tipos
bun run bump <version> # Sincronizar versão em todos os package.json
bun run electron:dev # Desenvolvimento com Electron
bun run electron:build # Empacotamento do Electron
```
@ -275,10 +288,11 @@ bun run electron:build # Empacotamento do Electron
Contribuições são bem-vindas! Consulte o [CLAUDE.md](./CLAUDE.md) para detalhes de arquitetura e estilo de código.
1. Faça fork e clone
2. Crie uma branch: `git checkout -b feat/my-feature`
3. Execute as verificações: `npx tsc --noEmit && bun --bun run test`
4. Faça commit com [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
5. Abra um PR contra `main`
2. Configure a sincronização de versão: `git config core.hooksPath .githooks`
3. Crie uma branch: `git checkout -b feat/my-feature`
4. Execute as verificações: `npx tsc --noEmit && bun --bun run test`
5. Faça commit com [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
6. Abra um PR contra `main`
## Roadmap
@ -290,6 +304,7 @@ Contribuições são bem-vindas! Consulte o [CLAUDE.md](./CLAUDE.md) para detalh
- [x] Importação do Figma `.fig`
- [x] Operações booleanas (união, subtração, interseção)
- [x] Perfis de capacidade multi-modelo
- [x] Reestruturação em monorepo com pacotes reutilizáveis
- [ ] Edição colaborativa
- [ ] Sistema de plugins
@ -302,12 +317,11 @@ Contribuições são bem-vindas! Consulte o [CLAUDE.md](./CLAUDE.md) para detalh
## Comunidade
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Entre no nosso Discord</strong>
</a>
— Faça perguntas, compartilhe designs, sugira funcionalidades.
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md"><b>Русский</b></a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Требования:** [Bun](https://bun.sh/) >= 1.0 и [Node.js](https://nodejs.org/) >= 18
### Развёртывание через Docker
### Docker
Доступно несколько вариантов образов — выберите подходящий для ваших нужд:
@ -113,6 +113,7 @@ bun run electron:dev
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 ГБ | Все CLI-инструменты |
**Запуск (только веб):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Подключить в настройках агента (`Cmd+,`) |
| **OpenCode** | Подключить в настройках агента (`Cmd+,`) |
| **GitHub Copilot** | `copilot login`, затем подключить в настройках агента (`Cmd+,`) |
| **Gemini CLI** | Подключить в настройках агента (`Cmd+,`) |
**Профили возможностей моделей** — автоматически адаптирует промпты, режим thinking и таймауты для каждого уровня моделей. Модели полного уровня (Claude) получают полные промпты; стандартного уровня (GPT-4o, Gemini, DeepSeek) — с отключённым thinking; базового уровня (MiniMax, Qwen, Llama, Mistral) — упрощённые промпты с вложенным JSON для максимальной надёжности.
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Структура проекта
```text
src/
canvas/ Движок CanvasKit/Skia — рисование, синхронизация, раскладка, направляющие, инструмент пера
components/ React UI — редактор, панели, общие диалоги, иконки
services/ai/ AI-чат, оркестратор, генерация дизайна, стриминг
services/figma/ Пайплайн бинарного импорта Figma .fig
services/codegen Генераторы кода React+Tailwind и HTML+CSS
stores/ Zustand — холст, документ, страницы, история, AI, настройки
variables/ Разрешение дизайн-токенов и управление ссылками
mcp/ Инструменты MCP-сервера для интеграции с внешними CLI
uikit/ Система переиспользуемых наборов компонентов
server/
api/ai/ Nitro API — стриминговый чат, генерация, валидация
utils/ Обёртки клиентов Claude CLI, OpenCode, Codex, Copilot
electron/
main.ts Окно, форк Nitro, нативное меню, автообновление
preload.ts IPC-мост
openpencil/
├── apps/
│ ├── web/ Веб-приложение TanStack Start
│ │ ├── src/
│ │ │ ├── canvas/ Движок CanvasKit/Skia — рисование, синхронизация, раскладка
│ │ │ ├── components/ React UI — редактор, панели, общие диалоги, иконки
│ │ │ ├── services/ai/ AI-чат, оркестратор, генерация дизайна, стриминг
│ │ │ ├── stores/ Zustand — холст, документ, страницы, история, AI
│ │ │ ├── mcp/ Инструменты MCP-сервера для интеграции с внешними CLI
│ │ │ ├── hooks/ Горячие клавиши, перетаскивание файлов, вставка из Figma
│ │ │ └── uikit/ Система переиспользуемых наборов компонентов
│ │ └── server/
│ │ ├── api/ai/ Nitro API — стриминговый чат, генерация, валидация
│ │ └── utils/ Обёртки клиентов Claude CLI, OpenCode, Codex, Copilot
│ └── desktop/ Десктопное приложение Electron
│ ├── main.ts Окно, форк Nitro, нативное меню, автообновление
│ ├── ipc-handlers.ts Нативные файловые диалоги, синхронизация темы, настройки IPC
│ └── preload.ts IPC-мост
├── packages/
│ ├── pen-types/ Определения типов для модели PenDocument
│ ├── pen-core/ Операции с деревом документа, движок раскладки, переменные
│ ├── pen-codegen/ Генераторы кода (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Парсер и конвертер файлов Figma .fig
│ ├── pen-renderer/ Автономный рендерер CanvasKit/Skia
│ └── pen-sdk/ Зонтичный SDK (реэкспортирует все пакеты)
└── .githooks/ Pre-commit синхронизация версий из имени ветки
```
## Горячие клавиши
@ -266,6 +278,7 @@ bun --bun run dev # Сервер разработки (порт 3000)
bun --bun run build # Сборка для продакшена
bun --bun run test # Запустить тесты (Vitest)
npx tsc --noEmit # Проверка типов
bun run bump <version> # Синхронизация версий во всех package.json
bun run electron:dev # Разработка Electron
bun run electron:build # Упаковка Electron
```
@ -275,10 +288,11 @@ bun run electron:build # Упаковка Electron
Мы приветствуем вклад в проект! Подробности об архитектуре и стиле кода смотрите в [CLAUDE.md](./CLAUDE.md).
1. Сделайте форк и клонируйте репозиторий
2. Создайте ветку: `git checkout -b feat/my-feature`
3. Запустите проверки: `npx tsc --noEmit && bun --bun run test`
4. Сделайте коммит в формате [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
5. Откройте PR в ветку `main`
2. Настройте синхронизацию версий: `git config core.hooksPath .githooks`
3. Создайте ветку: `git checkout -b feat/my-feature`
4. Запустите проверки: `npx tsc --noEmit && bun --bun run test`
5. Сделайте коммит в формате [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
6. Откройте PR в ветку `main`
## Дорожная карта
@ -290,6 +304,7 @@ bun run electron:build # Упаковка Electron
- [x] Импорт Figma `.fig`
- [x] Булевы операции (объединение, вычитание, пересечение)
- [x] Мультимодельные профили возможностей
- [x] Реструктуризация в монорепозиторий с переиспользуемыми пакетами
- [ ] Совместное редактирование
- [ ] Система плагинов
@ -302,12 +317,11 @@ bun run electron:build # Упаковка Electron
## Сообщество
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Присоединяйтесь к нашему Discord</strong>
</a>
— Задавайте вопросы, делитесь дизайнами, предлагайте функции.
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <b>ไทย</b> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **ข้อกำหนดเบื้องต้น:** [Bun](https://bun.sh/) >= 1.0 และ [Node.js](https://nodejs.org/) >= 18
### การติดตั้งด้วย Docker
### Docker
มี image หลายรูปแบบให้เลือก — เลือกแบบที่เหมาะกับความต้องการของคุณ:
@ -113,6 +113,7 @@ bun run electron:dev
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | เครื่องมือ CLI ทั้งหมด |
**รัน (เว็บเท่านั้น):**
@ -167,13 +168,14 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | เชื่อมต่อใน Agent Settings (`Cmd+,`) |
| **OpenCode** | เชื่อมต่อใน Agent Settings (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` จากนั้นเชื่อมต่อใน Agent Settings (`Cmd+,`) |
| **Gemini CLI** | เชื่อมต่อใน Agent Settings (`Cmd+,`) |
**โปรไฟล์ความสามารถของโมเดล** — ปรับ prompt, โหมด thinking และ timeout ตามระดับโมเดลโดยอัตโนมัติ โมเดลระดับเต็ม (Claude) ได้ prompt ครบถ้วน; โมเดลระดับมาตรฐาน (GPT-4o, Gemini, DeepSeek) ปิด thinking; โมเดลระดับพื้นฐาน (MiniMax, Qwen, Llama, Mistral) ได้ prompt แบบ nested-JSON ที่ย่อลงเพื่อความเสถียรสูงสุด
**MCP Server**
- MCP Server ในตัว — ติดตั้งได้ด้วยคลิกเดียวใน Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLIs
- ตรวจจับ Node.js อัตโนมัติ — หากไม่ได้ติดตั้ง จะสำรองไปใช้ HTTP transport โดยอัตโนมัติและเริ่ม MCP HTTP เซิร์ฟเวอร์
- การทำ Design Automation จาก Terminal: อ่าน สร้าง และแก้ไขไฟล์ `.op` ผ่าน agent ที่รองรับ MCP
- ตรวจจับ Node.js อัตโนมัติ — หากไม่ได้ติดตั้ง จะสำรองไปใช้ HTTP transport และเริ่ม MCP HTTP server โดยอัตโนมัติ
- การทำ Design automation จาก terminal: อ่าน สร้าง และแก้ไขไฟล์ `.op` ผ่าน agent ที่รองรับ MCP
- **Layered design workflow**`design_skeleton``design_content``design_refine` สำหรับดีไซน์หลายส่วนที่มีความละเอียดสูงขึ้น
- **Segmented prompt retrieval** — โหลดเฉพาะความรู้ด้านดีไซน์ที่ต้องการ (schema, layout, roles, icons, planning ฯลฯ)
- รองรับหลายหน้า — สร้าง เปลี่ยนชื่อ เรียงลำดับ และทำซ้ำหน้าผ่าน MCP tools
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## โครงสร้างโปรเจกต์
```text
src/
canvas/ CanvasKit/Skia engine — การวาด, sync, layout, guides, pen tool
components/ React UI — editor, panels, shared dialogs, icons
services/ai/ AI chat, orchestrator, การสร้างดีไซน์, streaming
services/figma/ Figma .fig binary import pipeline
services/codegen React+Tailwind และ HTML+CSS code generators
stores/ Zustand — canvas, document, pages, history, AI, settings
variables/ การแก้ไข design token และการจัดการ reference
mcp/ MCP server tools สำหรับการเชื่อมต่อ CLI ภายนอก
uikit/ ระบบ component kit ที่นำกลับมาใช้ใหม่ได้
server/
api/ai/ Nitro API — streaming chat, generation, validation
utils/ Claude CLI, OpenCode, Codex, Copilot client wrappers
electron/
main.ts Window, Nitro fork, native menu, auto-updater
preload.ts IPC bridge
openpencil/
├── apps/
│ ├── web/ TanStack Start web app
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia engine — การวาด, sync, layout
│ │ │ ├── components/ React UI — editor, panels, shared dialogs, icons
│ │ │ ├── services/ai/ AI chat, orchestrator, การสร้างดีไซน์, streaming
│ │ │ ├── stores/ Zustand — canvas, document, pages, history, AI
│ │ │ ├── mcp/ MCP server tools สำหรับการเชื่อมต่อ CLI ภายนอก
│ │ │ ├── hooks/ Keyboard shortcuts, file drop, Figma paste
│ │ │ └── uikit/ ระบบ component kit ที่นำกลับมาใช้ใหม่ได้
│ │ └── server/
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
│ └── desktop/ Electron desktop app
│ ├── main.ts Window, Nitro fork, native menu, auto-updater
│ ├── ipc-handlers.ts ไดอะล็อกไฟล์เนทีฟ, ซิงค์ธีม, การตั้งค่า IPC
│ └── preload.ts IPC bridge
├── packages/
│ ├── pen-types/ Type definitions สำหรับ PenDocument model
│ ├── pen-core/ Document tree ops, layout engine, variables
│ ├── pen-codegen/ Code generators (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Figma .fig file parser และ converter
│ ├── pen-renderer/ Standalone CanvasKit/Skia renderer
│ └── pen-sdk/ Umbrella SDK (re-exports ทุก package)
└── .githooks/ Pre-commit version sync จาก branch name
```
## คีย์ลัด
@ -266,6 +278,7 @@ bun --bun run dev # Dev server (port 3000)
bun --bun run build # Production build
bun --bun run test # รันการทดสอบ (Vitest)
npx tsc --noEmit # ตรวจสอบ type
bun run bump <version> # Sync version ในทุก package.json
bun run electron:dev # Electron dev
bun run electron:build # Electron package
```
@ -275,10 +288,11 @@ bun run electron:build # Electron package
ยินดีต้อนรับการมีส่วนร่วมทุกรูปแบบ! ดู [CLAUDE.md](./CLAUDE.md) สำหรับรายละเอียดสถาปัตยกรรมและรูปแบบโค้ด
1. Fork และ clone
2. สร้าง branch: `git checkout -b feat/my-feature`
3. รันการตรวจสอบ: `npx tsc --noEmit && bun --bun run test`
4. Commit ด้วย [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
5. เปิด PR เข้า `main`
2. ตั้งค่า version sync: `git config core.hooksPath .githooks`
3. สร้าง branch: `git checkout -b feat/my-feature`
4. รันการตรวจสอบ: `npx tsc --noEmit && bun --bun run test`
5. Commit ด้วย [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
6. เปิด PR เข้า `main`
## Roadmap
@ -290,6 +304,7 @@ bun run electron:build # Electron package
- [x] นำเข้า Figma `.fig`
- [x] Boolean operations (union, subtract, intersect)
- [x] โปรไฟล์ความสามารถหลายโมเดล
- [x] ปรับโครงสร้างเป็น monorepo พร้อม package ที่นำกลับมาใช้ใหม่ได้
- [ ] การแก้ไขร่วมกัน
- [ ] ระบบปลั๊กอิน
@ -302,12 +317,11 @@ bun run electron:build # Electron package
## ชุมชน
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> เข้าร่วม Discord ของเรา</strong>
</a>
— ถามคำถาม แชร์ดีไซน์ เสนอฟีเจอร์
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,16 +1,16 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
<p align="center">
<strong>Dunyanin ilk acik kaynakli AI-yerel vektor tasarim araci.</strong><br />
<sub>Eszamanli Ajan Ekipleri &bull; Kod Olarak Tasarim &bull; Yerlesik MCP Sunucusu &bull; Coklu Model Zekasi</sub>
<strong>Dünyanın ilk açık kaynaklı AI-yerel vektör tasarım aracı.</strong><br />
<sub>Eşzamanlı Ajan Ekipleri &bull; Kod Olarak Tasarım &bull; Yerleşik MCP Sunucusu &bull; Çoklu Model Zekası</sub>
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <b>Türkçe</b> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Ön koşullar:** [Bun](https://bun.sh/) >= 1.0 ve [Node.js](https://nodejs.org/) >= 18
### Docker ile Dağıtım
### Docker
Birden fazla görüntü varyantı mevcuttur — ihtiyaçlarınıza uygun olanı seçin:
@ -113,6 +113,7 @@ Birden fazla görüntü varyantı mevcuttur — ihtiyaçlarınıza uygun olanı
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | Tüm CLI araçları |
**Çalıştır (yalnızca web):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Ajan Ayarlarından bağlanın (`Cmd+,`) |
| **OpenCode** | Ajan Ayarlarından bağlanın (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` ardından Ajan Ayarlarından bağlanın (`Cmd+,`) |
| **Gemini CLI** | Ajan Ayarlarından bağlanın (`Cmd+,`) |
**Model Yetenek Profilleri** — promptları, düşünme modunu ve zaman aşımlarını model katmanına göre otomatik olarak uyarlar. Tam katman modeller (Claude) eksiksiz promptlar alır; standart katman (GPT-4o, Gemini, DeepSeek) düşünme modunu devre dışı bırakır; temel katman (MiniMax, Qwen, Llama, Mistral) maksimum güvenilirlik için basitleştirilmiş iç içe JSON promptları alır.
@ -212,7 +214,7 @@ docker build --target full -t openpencil-full .
| | |
| --- | --- |
| **Ön Uç** | React 19 · TanStack Start · Tailwind CSS v4 · shadcn/ui |
| **Kanvas** | CanvasKit/Skia (WASM, GPU hizlandirmali) |
| **Kanvas** | CanvasKit/Skia (WASM, GPU hızlandırmalı) |
| **Durum Yönetimi** | Zustand v5 |
| **Sunucu** | Nitro |
| **Masaüstü** | Electron 35 |
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Proje Yapısı
```text
src/
canvas/ CanvasKit/Skia motoru — çizim, senkronizasyon, düzen, kılavuzlar, kalem aracı
components/ React UI — editör, paneller, paylaşılan iletişim kutuları, simgeler
services/ai/ AI sohbet, orkestratör, tasarım üretimi, akış
services/figma/ Figma .fig ikili içe aktarma ardışık düzeni
services/codegen React+Tailwind ve HTML+CSS kod üreticileri
stores/ Zustand — kanvas, belge, sayfalar, geçmiş, AI, ayarlar
variables/ Tasarım token çözümleme ve referans yönetimi
mcp/ Harici CLI entegrasyonu için MCP sunucu araçları
uikit/ Yeniden kullanılabilir bileşen kiti sistemi
server/
api/ai/ Nitro API — akış sohbet, üretim, doğrulama
utils/ Claude CLI, OpenCode, Codex, Copilot istemci sarmalayıcıları
electron/
main.ts Pencere, Nitro çatallanması, yerel menü, otomatik güncelleyici
preload.ts IPC köprüsü
openpencil/
├── apps/
│ ├── web/ TanStack Start web uygulaması
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia motoru — çizim, senkronizasyon, düzen
│ │ │ ├── components/ React UI — editör, paneller, paylaşılan iletişim kutuları, simgeler
│ │ │ ├── services/ai/ AI sohbet, orkestratör, tasarım üretimi, akış
│ │ │ ├── stores/ Zustand — kanvas, belge, sayfalar, geçmiş, AI
│ │ │ ├── mcp/ Harici CLI entegrasyonu için MCP sunucu araçları
│ │ │ ├── hooks/ Klavye kısayolları, dosya bırakma, Figma yapıştırma
│ │ │ └── uikit/ Yeniden kullanılabilir bileşen kiti sistemi
│ │ └── server/
│ │ ├── api/ai/ Nitro API — akış sohbet, üretim, doğrulama
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot sarmalayıcıları
│ └── desktop/ Electron masaüstü uygulaması
│ ├── main.ts Pencere, Nitro çatallanması, yerel menü, otomatik güncelleyici
│ ├── ipc-handlers.ts Yerel dosya diyalogları, tema senkronizasyonu, tercihler IPC
│ └── preload.ts IPC köprüsü
├── packages/
│ ├── pen-types/ PenDocument modeli için tür tanımları
│ ├── pen-core/ Belge ağacı işlemleri, düzen motoru, değişkenler
│ ├── pen-codegen/ Kod oluşturucular (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Figma .fig dosya ayrıştırıcı ve dönüştürücü
│ ├── pen-renderer/ Bağımsız CanvasKit/Skia işleyici
│ └── pen-sdk/ Şemsiye SDK (tüm paketleri yeniden dışa aktarır)
└── .githooks/ Dal adından ön-commit sürüm eşitleme
```
## Klavye Kısayolları
@ -266,6 +278,7 @@ bun --bun run dev # Geliştirme sunucusu (port 3000)
bun --bun run build # Üretim derlemesi
bun --bun run test # Testleri çalıştır (Vitest)
npx tsc --noEmit # Tür denetimi
bun run bump <version> # Tüm package.json dosyalarında sürümü eşitle
bun run electron:dev # Electron geliştirme modu
bun run electron:build # Electron paketleme
```
@ -275,10 +288,11 @@ bun run electron:build # Electron paketleme
Katkılarınızı bekliyoruz! Mimari ayrıntılar ve kod stili için [CLAUDE.md](./CLAUDE.md) dosyasına bakın.
1. Fork'layın ve klonlayın
2. Dal oluşturun: `git checkout -b feat/my-feature`
3. Kontrolleri çalıştırın: `npx tsc --noEmit && bun --bun run test`
4. [Conventional Commits](https://www.conventionalcommits.org/) formatıyla commit yapın: `feat(canvas): add rotation snapping`
5. `main` dalına PR açın
2. Sürüm eşitlemeyi ayarlayın: `git config core.hooksPath .githooks`
3. Dal oluşturun: `git checkout -b feat/my-feature`
4. Kontrolleri çalıştırın: `npx tsc --noEmit && bun --bun run test`
5. [Conventional Commits](https://www.conventionalcommits.org/) formatıyla commit yapın: `feat(canvas): add rotation snapping`
6. `main` dalına PR açın
## Yol Haritası
@ -290,6 +304,7 @@ Katkılarınızı bekliyoruz! Mimari ayrıntılar ve kod stili için [CLAUDE.md]
- [x] Figma `.fig` içe aktarma
- [x] Boolean işlemler (birleştirme, çıkarma, kesişim)
- [x] Çoklu model yetenek profilleri
- [x] Yeniden kullanılabilir paketlerle monorepo yapılandırması
- [ ] Ortak düzenleme
- [ ] Eklenti sistemi
@ -302,12 +317,11 @@ Katkılarınızı bekliyoruz! Mimari ayrıntılar ve kod stili için [CLAUDE.md]
## Topluluk
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Discord'umuza katılın</strong>
</a>
— Soru sorun, tasarımlarınızı paylaşın, özellik önerin.
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <b>Tiếng Việt</b> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **Yêu cầu:** [Bun](https://bun.sh/) >= 1.0 và [Node.js](https://nodejs.org/) >= 18
### Triển khai bằng Docker
### Docker
Có nhiều biến thể image khác nhau — chọn loại phù hợp với nhu cầu của bạn:
@ -113,6 +113,7 @@ Có nhiều biến thể image khác nhau — chọn loại phù hợp với nhu
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | Tất cả công cụ CLI |
**Chạy (chỉ web):**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | Kết nối trong Cài đặt tác nhân (`Cmd+,`) |
| **OpenCode** | Kết nối trong Cài đặt tác nhân (`Cmd+,`) |
| **GitHub Copilot** | `copilot login` rồi kết nối trong Cài đặt tác nhân (`Cmd+,`) |
| **Gemini CLI** | Kết nối trong Cài đặt tác nhân (`Cmd+,`) |
**Hồ sơ Năng lực Mô hình** — tự động thích ứng prompt, chế độ thinking và thời gian chờ theo từng cấp mô hình. Mô hình cấp đầy đủ (Claude) nhận prompt hoàn chỉnh; cấp tiêu chuẩn (GPT-4o, Gemini, DeepSeek) tắt thinking; cấp cơ bản (MiniMax, Qwen, Llama, Mistral) nhận prompt JSON lồng nhau đơn giản hóa để đảm bảo độ tin cậy tối đa.
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## Cấu trúc dự án
```text
src/
canvas/ CanvasKit/Skia engine — vẽ, đồng bộ, layout, hướng dẫn, công cụ bút
components/ React UI — editor, panels, hộp thoại dùng chung, icons
services/ai/ AI chat, orchestrator, tạo thiết kế, streaming
services/figma/ Pipeline nhập binary Figma .fig
services/codegen Bộ tạo mã React+Tailwind và HTML+CSS
stores/ Zustand — canvas, document, pages, history, AI, settings
variables/ Giải quyết token thiết kế và quản lý tham chiếu
mcp/ Công cụ máy chủ MCP để tích hợp CLI bên ngoài
uikit/ Hệ thống kit component có thể tái sử dụng
server/
api/ai/ Nitro API — streaming chat, generation, validation
utils/ Claude CLI, OpenCode, Codex, Copilot client wrappers
electron/
main.ts Cửa sổ, Nitro fork, menu gốc, auto-updater
preload.ts IPC bridge
openpencil/
├── apps/
│ ├── web/ Ứng dụng web TanStack Start
│ │ ├── src/
│ │ │ ├── canvas/ Engine CanvasKit/Skia — vẽ, đồng bộ, layout
│ │ │ ├── components/ React UI — editor, panels, hộp thoại dùng chung, icons
│ │ │ ├── services/ai/ AI chat, orchestrator, tạo thiết kế, streaming
│ │ │ ├── stores/ Zustand — canvas, document, pages, history, AI
│ │ │ ├── mcp/ Công cụ máy chủ MCP để tích hợp CLI bên ngoài
│ │ │ ├── hooks/ Phím tắt, kéo thả tệp, dán từ Figma
│ │ │ └── uikit/ Hệ thống kit component có thể tái sử dụng
│ │ └── server/
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
│ └── desktop/ Ứng dụng desktop Electron
│ ├── main.ts Cửa sổ, Nitro fork, menu gốc, auto-updater
│ ├── ipc-handlers.ts Hộp thoại file gốc, đồng bộ theme, tùy chọn IPC
│ └── preload.ts IPC bridge
├── packages/
│ ├── pen-types/ Định nghĩa kiểu cho mô hình PenDocument
│ ├── pen-core/ Thao tác cây tài liệu, layout engine, biến
│ ├── pen-codegen/ Bộ tạo mã (React, HTML, Vue, Flutter, ...)
│ ├── pen-figma/ Trình phân tích và chuyển đổi tệp Figma .fig
│ ├── pen-renderer/ Bộ dựng hình CanvasKit/Skia độc lập
│ └── pen-sdk/ SDK tổng hợp (tái xuất tất cả các gói)
└── .githooks/ Pre-commit đồng bộ phiên bản từ tên nhánh
```
## Phím tắt
@ -266,6 +278,7 @@ bun --bun run dev # Máy chủ phát triển (cổng 3000)
bun --bun run build # Build production
bun --bun run test # Chạy kiểm thử (Vitest)
npx tsc --noEmit # Kiểm tra kiểu
bun run bump <version> # Đồng bộ phiên bản trên tất cả package.json
bun run electron:dev # Electron dev
bun run electron:build # Đóng gói Electron
```
@ -275,10 +288,11 @@ bun run electron:build # Đóng gói Electron
Chào mừng đóng góp! Xem [CLAUDE.md](./CLAUDE.md) để biết chi tiết về kiến trúc và phong cách mã.
1. Fork và clone
2. Tạo branch: `git checkout -b feat/my-feature`
3. Chạy kiểm tra: `npx tsc --noEmit && bun --bun run test`
4. Commit theo [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
5. Mở PR vào nhánh `main`
2. Thiết lập đồng bộ phiên bản: `git config core.hooksPath .githooks`
3. Tạo branch: `git checkout -b feat/my-feature`
4. Chạy kiểm tra: `npx tsc --noEmit && bun --bun run test`
5. Commit theo [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
6. Mở PR vào nhánh `main`
## Lộ trình
@ -290,6 +304,7 @@ Chào mừng đóng góp! Xem [CLAUDE.md](./CLAUDE.md) để biết chi tiết v
- [x] Nhập Figma `.fig`
- [x] Phép toán Boolean (hợp nhất, trừ, giao)
- [x] Hồ sơ năng lực đa mô hình
- [x] Tái cấu trúc monorepo với các gói tái sử dụng
- [ ] Chỉnh sửa cộng tác
- [ ] Hệ thống plugin
@ -302,12 +317,11 @@ Chào mừng đóng góp! Xem [CLAUDE.md](./CLAUDE.md) để biết chi tiết v
## Cộng đồng
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> Tham gia Discord của chúng tôi</strong>
</a>
— Đặt câu hỏi, chia sẻ thiết kế, đề xuất tính năng.
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="./README.md">English</a> · <a href="./README.zh.md">简体中文</a> · <b>繁體中文</b> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
<a href="./README.md"><b>English</b></a> · <a href="./README.zh.md">简体中文</a> · <a href="./README.zh-TW.md">繁體中文</a> · <a href="./README.ja.md">日本語</a> · <a href="./README.ko.md">한국어</a> · <a href="./README.fr.md">Français</a> · <a href="./README.es.md">Español</a> · <a href="./README.de.md">Deutsch</a> · <a href="./README.pt.md">Português</a> · <a href="./README.ru.md">Русский</a> · <a href="./README.hi.md">हिन्दी</a> · <a href="./README.tr.md">Türkçe</a> · <a href="./README.th.md">ไทย</a> · <a href="./README.vi.md">Tiếng Việt</a> · <a href="./README.id.md">Bahasa Indonesia</a>
</p>
<p align="center">
@ -102,7 +102,7 @@ bun run electron:dev
> **前置條件:** [Bun](https://bun.sh/) >= 1.0 以及 [Node.js](https://nodejs.org/) >= 18
### Docker 部署
### Docker
提供多種映像檔變體 — 選擇適合您需求的版本:
@ -113,6 +113,7 @@ bun run electron:dev
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | 所有 CLI 工具 |
**執行(僅 Web**
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | 在 Agent 設定中連接(`Cmd+,` |
| **OpenCode** | 在 Agent 設定中連接(`Cmd+,` |
| **GitHub Copilot** | 執行 `copilot login` 後在 Agent 設定中連接(`Cmd+,` |
| **Gemini CLI** | 在 Agent 設定中連接(`Cmd+,` |
**模型能力設定檔** — 自動依據模型層級調整提示詞、思考模式和逾時設定。完整層級模型Claude獲得完整提示詞標準層級GPT-4o、Gemini、DeepSeek停用思考模式基礎層級MiniMax、Qwen、Llama、Mistral獲得精簡巢狀 JSON 提示詞,確保最大可靠性。
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
## 專案結構
```text
src/
canvas/ CanvasKit/Skia 引擎 — 繪圖、同步、版面配置、參考線、鋼筆工具
components/ React UI — 編輯器、面板、共用對話框、圖示
services/ai/ AI 聊天、編排器、設計生成、串流處理
services/figma/ Figma .fig 二進位檔案匯入管線
services/codegen React+Tailwind 和 HTML+CSS 程式碼生成器
stores/ Zustand — 畫布、文件、頁面、歷程、AI、設定
variables/ 設計令牌解析與參照管理
mcp/ 供外部 CLI 整合使用的 MCP 伺服器工具
uikit/ 可重複使用元件套件系統
server/
api/ai/ Nitro API — 串流聊天、生成、驗證
utils/ Claude CLI、OpenCode、Codex、Copilot 客戶端封裝
electron/
main.ts 視窗、Nitro 子處理序、原生選單、自動更新
preload.ts IPC 橋接
openpencil/
├── apps/
│ ├── web/ TanStack Start Web 應用程式
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia 引擎 — 繪圖、同步、版面配置
│ │ │ ├── components/ React UI — 編輯器、面板、共用對話框、圖示
│ │ │ ├── services/ai/ AI 聊天、編排器、設計生成、串流處理
│ │ │ ├── stores/ Zustand — 畫布、文件、頁面、歷程、AI
│ │ │ ├── mcp/ 供外部 CLI 整合使用的 MCP 伺服器工具
│ │ │ ├── hooks/ 鍵盤快捷鍵、檔案拖放、Figma 貼上
│ │ │ └── uikit/ 可重複使用元件套件系統
│ │ └── server/
│ │ ├── api/ai/ Nitro API — 串流聊天、生成、驗證
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客戶端封裝
│ └── desktop/ Electron 桌面應用程式
│ ├── main.ts 視窗、Nitro 子處理序、原生選單、自動更新
│ ├── ipc-handlers.ts 原生檔案對話框、主題同步、偏好設定 IPC
│ └── preload.ts IPC 橋接
├── packages/
│ ├── pen-types/ PenDocument 模型型別定義
│ ├── pen-core/ 文件樹操作、版面配置引擎、變數
│ ├── pen-codegen/ 程式碼生成器React、HTML、Vue、Flutter...
│ ├── pen-figma/ Figma .fig 檔案解析器與轉換器
│ ├── pen-renderer/ 獨立 CanvasKit/Skia 渲染器
│ └── pen-sdk/ 整合 SDK重新匯出所有套件
└── .githooks/ Pre-commit 版本號同步(從分支名稱)
```
## 鍵盤快捷鍵
@ -266,6 +278,7 @@ bun --bun run dev # 開發伺服器(連接埠 3000
bun --bun run build # 正式版建置
bun --bun run test # 執行測試Vitest
npx tsc --noEmit # 型別檢查
bun run bump <version> # 在所有 package.json 間同步版本號
bun run electron:dev # Electron 開發模式
bun run electron:build # Electron 封裝
```
@ -275,10 +288,11 @@ bun run electron:build # Electron 封裝
歡迎貢獻!請查閱 [CLAUDE.md](./CLAUDE.md) 了解架構細節和程式碼風格。
1. Fork 並複製存放庫
2. 建立分支:`git checkout -b feat/my-feature`
3. 執行檢查:`npx tsc --noEmit && bun --bun run test`
4. 使用 [Conventional Commits](https://www.conventionalcommits.org/) 提交:`feat(canvas): add rotation snapping`
5. 向 `main` 分支發起 PR
2. 設定版本同步:`git config core.hooksPath .githooks`
3. 建立分支:`git checkout -b feat/my-feature`
4. 執行檢查:`npx tsc --noEmit && bun --bun run test`
5. 使用 [Conventional Commits](https://www.conventionalcommits.org/) 提交:`feat(canvas): add rotation snapping`
6. 向 `main` 分支發起 PR
## 路線圖
@ -290,6 +304,7 @@ bun run electron:build # Electron 封裝
- [x] Figma `.fig` 匯入
- [x] 布林運算(聯集、減去、交集)
- [x] 多模型能力設定檔
- [x] Monorepo 重構,支援可重複使用套件
- [ ] 協同編輯
- [ ] 外掛程式系統
@ -302,12 +317,11 @@ bun run electron:build # Electron 封裝
## 社群
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> 加入我們的 Discord</strong>
</a>
— 提問、分享設計、提出功能建議。
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
</p>
<h1 align="center">OpenPencil</h1>
@ -55,7 +55,7 @@
### 🧠 多模型智能
自动适配每个模型的能力。Claude 获得完整提示词和思考模式GPT-4o/Gemini 关闭思考模式小模型MiniMax、通义千问、Llama使用简化提示词以确保输出可靠性。
自动适配每个模型的能力。Claude 获得完整提示词和思考模式GPT-4o/Gemini 关闭思考模式小模型MiniMax、Qwen、Llama使用简化提示词以确保输出可靠性。
</td>
<td width="50%">
@ -102,9 +102,9 @@ bun run electron:dev
> **前置条件:** [Bun](https://bun.sh/) >= 1.0 以及 [Node.js](https://nodejs.org/) >= 18
### Docker 部署
### Docker
提供多个镜像变体按需选择:
提供多个镜像变体按需选择:
| 镜像 | 大小 | 包含 |
| --- | --- | --- |
@ -113,6 +113,7 @@ bun run electron:dev
| `openpencil-codex:latest` | — | + Codex CLI |
| `openpencil-opencode:latest` | — | + OpenCode CLI |
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
| `openpencil-gemini:latest` | — | + Gemini CLI |
| `openpencil-full:latest` | ~1 GB | 全部 CLI 工具 |
**运行(仅 Web**
@ -123,7 +124,7 @@ docker run -d -p 3000:3000 ghcr.io/zseven-w/openpencil:latest
**运行 AI CLI以 Claude Code 为例):**
AI 聊天依赖 Claude CLI 的 OAuth 登录,使用 Docker volume 持久化登录状态
AI 聊天依赖 Claude CLI 的 OAuth 登录。使用 Docker volume 持久化登录会话
```bash
# 第一步 — 登录(仅需一次)
@ -167,8 +168,9 @@ docker build --target full -t openpencil-full .
| **Codex CLI** | 在 Agent 设置中连接(`Cmd+,` |
| **OpenCode** | 在 Agent 设置中连接(`Cmd+,` |
| **GitHub Copilot** | 运行 `copilot login` 后在 Agent 设置中连接(`Cmd+,` |
| **Gemini CLI** | 在 Agent 设置中连接(`Cmd+,` |
**模型能力配置** — 自动根据模型层级适配提示词、思考模式和超时时间。完整层级模型Claude获得完整提示词标准层级模型GPT-4o、Gemini、DeepSeek关闭思考模式基础层级模型MiniMax、通义千问、Llama、Mistral使用简化的嵌套 JSON 提示词以确保最大可靠性。
**模型能力配置** — 自动根据模型层级适配提示词、思考模式和超时时间。完整层级模型Claude获得完整提示词标准层级模型GPT-4o、Gemini、DeepSeek关闭思考模式基础层级模型MiniMax、Qwen、Llama、Mistral使用简化的嵌套 JSON 提示词以确保最大可靠性。
**MCP 服务器**
- 内置 MCP 服务器 — 一键安装到 Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLI
@ -181,7 +183,6 @@ docker build --target full -t openpencil-full .
**代码生成**
- React + Tailwind CSS、HTML + CSS、CSS Variables
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
- 从设计令牌生成 CSS Variables
## 功能特性
@ -224,22 +225,32 @@ docker build --target full -t openpencil-full .
## 项目结构
```text
src/
canvas/ CanvasKit/Skia 引擎 — 绘图、同步、布局、参考线、钢笔工具
components/ React UI — 编辑器、面板、共享对话框、图标
services/ai/ AI 聊天、编排器、设计生成、流式处理
services/figma/ Figma .fig 二进制文件导入流水线
services/codegen 多平台代码生成器React、HTML、Vue、Svelte、Flutter、SwiftUI、Compose、React Native
stores/ Zustand — 画布、文档、页面、历史、AI、设置
variables/ 设计令牌解析与引用管理
mcp/ 供外部 CLI 集成使用的 MCP 服务器工具
uikit/ 可复用组件套件系统
server/
api/ai/ Nitro API — 流式聊天、生成、验证
utils/ Claude CLI、OpenCode、Codex、Copilot 客户端封装
electron/
main.ts 窗口、Nitro 子进程、原生菜单、自动更新
preload.ts IPC 桥接
openpencil/
├── apps/
│ ├── web/ TanStack Start Web 应用
│ │ ├── src/
│ │ │ ├── canvas/ CanvasKit/Skia 引擎 — 绘图、同步、布局
│ │ │ ├── components/ React UI — 编辑器、面板、共享对话框、图标
│ │ │ ├── services/ai/ AI 聊天、编排器、设计生成、流式处理
│ │ │ ├── stores/ Zustand — 画布、文档、页面、历史、AI
│ │ │ ├── mcp/ 供外部 CLI 集成使用的 MCP 服务器工具
│ │ │ ├── hooks/ 键盘快捷键、文件拖放、Figma 粘贴
│ │ │ └── uikit/ 可复用组件套件系统
│ │ └── server/
│ │ ├── api/ai/ Nitro API — 流式聊天、生成、验证
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客户端封装
│ └── desktop/ Electron 桌面应用
│ ├── main.ts 窗口、Nitro 子进程、原生菜单、自动更新
│ ├── ipc-handlers.ts 原生文件对话框、主题同步、偏好设置 IPC
│ └── preload.ts IPC 桥接
├── packages/
│ ├── pen-types/ PenDocument 模型类型定义
│ ├── pen-core/ 文档树操作、布局引擎、变量
│ ├── pen-codegen/ 代码生成器React、HTML、Vue、Flutter 等)
│ ├── pen-figma/ Figma .fig 文件解析与转换
│ ├── pen-renderer/ 独立 CanvasKit/Skia 渲染器
│ └── pen-sdk/ 聚合 SDK重新导出所有包
└── .githooks/ 预提交钩子:从分支名同步版本号
```
## 键盘快捷键
@ -267,6 +278,7 @@ bun --bun run dev # 开发服务器(端口 3000
bun --bun run build # 生产构建
bun --bun run test # 运行测试Vitest
npx tsc --noEmit # 类型检查
bun run bump <version> # 同步所有 package.json 的版本号
bun run electron:dev # Electron 开发模式
bun run electron:build # Electron 打包
```
@ -276,10 +288,11 @@ bun run electron:build # Electron 打包
欢迎贡献!请查阅 [CLAUDE.md](./CLAUDE.md) 了解架构细节和代码风格。
1. Fork 并克隆仓库
2. 创建分支:`git checkout -b feat/my-feature`
3. 运行检查:`npx tsc --noEmit && bun --bun run test`
4. 使用 [Conventional Commits](https://www.conventionalcommits.org/) 提交:`feat(canvas): add rotation snapping`
5. 向 `main` 分支发起 PR
2. 设置版本同步:`git config core.hooksPath .githooks`
3. 创建分支:`git checkout -b feat/my-feature`
4. 运行检查:`npx tsc --noEmit && bun --bun run test`
5. 使用 [Conventional Commits](https://www.conventionalcommits.org/) 提交:`feat(canvas): add rotation snapping`
6. 向 `main` 分支发起 PR
## 路线图
@ -291,6 +304,7 @@ bun run electron:build # Electron 打包
- [x] Figma `.fig` 导入
- [x] 布尔运算(合并、减去、相交)
- [x] 多模型能力配置
- [x] Monorepo 重构与可复用包
- [ ] 协同编辑
- [ ] 插件系统
@ -303,16 +317,11 @@ bun run electron:build # Electron 打包
## 社区
<a href="https://discord.gg/h9Fmyy6pVh">
<img src="./public/logo-discord.svg" alt="Discord" width="16" />
<img src="./apps/web/public/logo-discord.svg" alt="Discord" width="16" />
<strong> 加入我们的 Discord</strong>
</a>
— 提问、分享设计、提出功能建议。
**飞书交流群**
<img src="./screenshot/557517811-62010928-d91a-4223-bc10-9ee7a4fbf043.jpg" alt="飞书交流群" width="240" />
## Star History
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">

37
apps/desktop/CLAUDE.md Normal file
View file

@ -0,0 +1,37 @@
# Desktop App
Electron desktop app for macOS, Windows, and Linux.
## Files
- **`main.ts`** — Main process: window creation, Nitro server fork, preferences, `.op` file association handling (`open-file` event on macOS, CLI args + single-instance lock on Windows/Linux)
- **`ipc-handlers.ts`** — IPC handler setup: native file dialogs (`dialog:openFile`, `dialog:saveFile`, `dialog:saveToPath`), theme sync for title bar overlay, renderer preferences (replaces origin-scoped localStorage), auto-updater IPC
- **`preload.ts`** — Context bridge for renderer <-> main IPC (file dialogs, menu actions, updater state, `onOpenFile`/`readFile` for file association)
- **`app-menu.ts`** — Native application menu configuration (File, Edit, View, Help)
- **`auto-updater.ts`** — Auto-updater: checks GitHub Releases on startup and periodically
- **`constants.ts`** — Electron-specific constants (port, window dimensions, platform padding)
- **`logger.ts`** — Main process logging
- **`dev.ts`** — Dev workflow: starts Vite -> waits for port 3000 -> compiles MCP -> compiles Electron -> launches Electron
## Build Flow
```text
BUILD_TARGET=electron bun run build
-> bun run electron:compile (esbuild electron/ to out/desktop/)
-> bun run mcp:compile
-> npx electron-builder --config apps/desktop/electron-builder.yml
```
- **`electron-builder.yml`** — Packaging config: macOS (dmg/zip), Windows (nsis/portable), Linux (AppImage/deb), `.op` file association
- **`build/`** — Platform icons (.icns, .ico, .png)
- In production, Nitro server is forked as a child process on a random port; Electron loads `http://127.0.0.1:{port}/editor`
## File Association
`.op` files are registered as OpenPencil documents via `fileAssociations` in `electron-builder.yml`:
- macOS: `open-file` app event handles double-click/drag
- Windows/Linux: `requestSingleInstanceLock` + `second-instance` event forwards CLI args to existing window
## Auto-updater
Checks GitHub Releases on startup and every hour. `update-ready-banner.tsx` (in web app) shows download progress and "Restart & Install" prompt.

View file

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View file

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 544 KiB

View file

@ -11,7 +11,8 @@ import { spawn, execSync, type ChildProcess } from 'node:child_process'
import { build } from 'esbuild'
import { join } from 'node:path'
const ROOT = join(import.meta.dirname, '..')
const DESKTOP_DIR = import.meta.dirname
const ROOT = join(DESKTOP_DIR, '..', '..')
const VITE_DEV_PORT = 3000
// ---------------------------------------------------------------------------
@ -42,7 +43,7 @@ async function compileElectron(): Promise<void> {
sourcemap: true,
external: ['electron'],
target: 'node20',
outdir: join(ROOT, 'electron-dist'),
outdir: join(ROOT, 'out', 'desktop'),
outExtension: { '.js': '.cjs' },
format: 'cjs' as const,
}
@ -50,11 +51,11 @@ async function compileElectron(): Promise<void> {
await Promise.all([
build({
...common,
entryPoints: [join(ROOT, 'electron', 'main.ts')],
entryPoints: [join(DESKTOP_DIR, 'main.ts')],
}),
build({
...common,
entryPoints: [join(ROOT, 'electron', 'preload.ts')],
entryPoints: [join(DESKTOP_DIR, 'preload.ts')],
}),
])
@ -77,7 +78,6 @@ async function main(): Promise<void> {
// Ensure cleanup on exit
const cleanup = () => {
if (process.platform === 'win32' && vite.pid) {
// SIGTERM is unreliable on Windows; use taskkill for proper tree-kill
try {
execSync(`taskkill /pid ${vite.pid} /T /F`, { stdio: 'ignore' })
} catch { /* ignore */ }
@ -102,10 +102,11 @@ async function main(): Promise<void> {
sourcemap: true,
target: 'node20',
format: 'cjs',
entryPoints: [join(ROOT, 'src', 'mcp', 'server.ts')],
outfile: join(ROOT, 'dist', 'mcp-server.cjs'),
alias: { '@': join(ROOT, 'src') },
entryPoints: [join(ROOT, 'apps', 'web', 'src', 'mcp', 'server.ts')],
outfile: join(ROOT, 'out', 'mcp-server.cjs'),
alias: { '@': join(ROOT, 'apps', 'web', 'src') },
define: { 'import.meta.env': '{}' },
external: ['canvas', 'paper'],
})
console.log('[electron-dev] MCP server compiled')
@ -114,7 +115,7 @@ async function main(): Promise<void> {
// 4. Launch Electron
console.log('[electron-dev] Starting Electron...')
const electronBin = join(ROOT, 'node_modules', '.bin', 'electron')
const electron = spawn(electronBin, [join(ROOT, 'electron-dist', 'main.cjs')], {
const electron = spawn(electronBin, [join(ROOT, 'out', 'desktop', 'main.cjs')], {
cwd: ROOT,
stdio: 'inherit',
env: { ...process.env },

View file

@ -3,24 +3,27 @@ productName: OpenPencil
copyright: Copyright (c) 2024-2026 OpenPencil contributors
directories:
output: dist-electron
buildResources: build
output: out/release
buildResources: apps/desktop/build
files:
- electron-dist/**/*
- from: out/desktop
to: .
filter:
- "**/*"
- "!node_modules"
extraResources:
- from: .output/server
- from: out/web/server
to: server
- from: .output/public
- from: out/web/public
to: public
- from: dist/mcp-server.cjs
- from: out/mcp-server.cjs
to: mcp-server.cjs
mac:
category: public.app-category.graphics-design
icon: build/icon.icns
icon: apps/desktop/build/icon.icns
artifactName: "${productName}-${version}-${arch}-mac.${ext}"
target:
- dmg
@ -33,7 +36,7 @@ dmg:
title: "${productName} ${version}"
win:
icon: build/icon.ico
icon: apps/desktop/build/icon.ico
artifactName: "${productName}-${version}-${arch}-win.${ext}"
target:
- nsis
@ -48,7 +51,7 @@ nsis:
createStartMenuShortcut: true
linux:
icon: build/icon.png
icon: apps/desktop/build/icon.png
category: Graphics
artifactName: "${productName}-${version}-${arch}-linux.${ext}"
desktop:
@ -72,6 +75,6 @@ fileAssociations:
description: OpenPencil Design File
mimeType: application/x-openpencil
role: Editor
icon: build/icon
icon: apps/desktop/build/icon
asar: true

View file

@ -0,0 +1,163 @@
import {
ipcMain,
dialog,
type BrowserWindow,
} from 'electron'
import { resolve, extname, sep } from 'node:path'
import { readFile, writeFile } from 'node:fs/promises'
import { app } from 'electron'
import {
getUpdaterState,
checkForAppUpdates,
quitAndInstall,
getAutoUpdateEnabled,
setAutoUpdateEnabled,
setUpdaterState,
clearUpdateTimer,
startUpdateTimer,
} from './auto-updater'
import { getLogDir } from './logger'
interface IpcDeps {
getMainWindow: () => BrowserWindow | null
getPendingFilePath: () => string | null
clearPendingFilePath: () => void
prefsCache: Record<string, string>
schedulePrefsWrite: () => void
writeAppSettings: (patch: { autoUpdate?: boolean }) => Promise<void>
}
export function setupIPC(deps: IpcDeps): void {
const { getMainWindow, getPendingFilePath, clearPendingFilePath, prefsCache, schedulePrefsWrite, writeAppSettings } = deps
ipcMain.handle('dialog:openFile', async () => {
const mainWindow = getMainWindow()
if (!mainWindow) return null
const result = await dialog.showOpenDialog(mainWindow, {
title: 'Open .op file',
filters: [{ name: 'OpenPencil Files', extensions: ['op', 'pen'] }],
properties: ['openFile'],
})
if (result.canceled || result.filePaths.length === 0) return null
const filePath = result.filePaths[0]
const content = await readFile(filePath, 'utf-8')
return { filePath, content }
})
ipcMain.handle(
'dialog:saveFile',
async (_event, payload: { content: string; defaultPath?: string }) => {
const mainWindow = getMainWindow()
if (!mainWindow) return null
const result = await dialog.showSaveDialog(mainWindow, {
title: 'Save .op file',
defaultPath: payload.defaultPath,
filters: [{ name: 'OpenPencil Files', extensions: ['op'] }],
})
if (result.canceled || !result.filePath) return null
await writeFile(result.filePath, payload.content, 'utf-8')
return result.filePath
},
)
ipcMain.handle(
'dialog:saveToPath',
async (_event, payload: { filePath: string; content: string }) => {
const resolved = resolve(payload.filePath)
if (resolved.includes('\0')) {
throw new Error('Invalid file path')
}
const ext = extname(resolved).toLowerCase()
if (ext !== '.op' && ext !== '.pen') {
throw new Error('Only .op and .pen file extensions are allowed')
}
const allowedRoots = [app.getPath('home'), app.getPath('temp')]
const inAllowedDir = allowedRoots.some(
(root) => resolved === root || resolved.startsWith(root + sep),
)
if (!inAllowedDir) {
throw new Error('File path must be within the user home or temp directory')
}
await writeFile(resolved, payload.content, 'utf-8')
return resolved
},
)
ipcMain.handle('file:getPending', () => {
const filePath = getPendingFilePath()
if (filePath) {
clearPendingFilePath()
return filePath
}
return null
})
ipcMain.handle('file:read', async (_event, filePath: string) => {
const resolved = resolve(filePath)
const ext = extname(resolved).toLowerCase()
if (ext !== '.op' && ext !== '.pen') return null
try {
const content = await readFile(resolved, 'utf-8')
return { filePath: resolved, content }
} catch {
return null
}
})
// Theme sync for Windows/Linux title bar overlay
ipcMain.handle(
'theme:set',
(_event, theme: 'dark' | 'light', colors?: { bg: string; fg: string }) => {
const mainWindow = getMainWindow()
if (!mainWindow || mainWindow.isDestroyed()) return
const isWinOrLinux = process.platform === 'win32' || process.platform === 'linux'
if (!isWinOrLinux) return
const isLinux = process.platform === 'linux'
const fallbackBg = theme === 'dark' ? '#111' : '#fff'
const fallbackFg = theme === 'dark' ? '#d4d4d8' : '#3f3f46'
mainWindow.setTitleBarOverlay({
color: isLinux ? (colors?.bg || fallbackBg) : 'rgba(0,0,0,0)',
symbolColor: colors?.fg || fallbackFg,
})
},
)
// Renderer preferences
ipcMain.handle('prefs:getAll', () => ({ ...prefsCache }))
ipcMain.handle('prefs:set', (_event, key: string, value: string) => {
prefsCache[key] = value
schedulePrefsWrite()
})
ipcMain.handle('prefs:remove', (_event, key: string) => {
delete prefsCache[key]
schedulePrefsWrite()
})
ipcMain.handle('log:getDir', () => getLogDir())
// Updater IPC
ipcMain.handle('updater:getState', () => getUpdaterState())
ipcMain.handle('updater:checkForUpdates', async () => {
await checkForAppUpdates(true)
return getUpdaterState()
})
ipcMain.handle('updater:quitAndInstall', () => quitAndInstall())
ipcMain.handle('updater:getAutoCheck', () => getAutoUpdateEnabled())
ipcMain.handle('updater:setAutoCheck', async (_event, enabled: boolean) => {
setAutoUpdateEnabled(enabled)
await writeAppSettings({ autoUpdate: enabled })
if (enabled) {
startUpdateTimer()
setUpdaterState({ status: 'idle' })
} else {
clearUpdateTimer()
setUpdaterState({ status: 'disabled' })
}
return enabled
})
}

View file

@ -8,7 +8,7 @@ import {
import { execSync } from 'node:child_process'
import { fork, type ChildProcess } from 'node:child_process'
import { createServer } from 'node:net'
import { join, resolve, extname, sep } from 'node:path'
import { join, extname } from 'node:path'
import { homedir } from 'node:os'
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises'
@ -33,16 +33,12 @@ import {
import {
setupAutoUpdater,
broadcastUpdaterState,
getUpdaterState,
setUpdaterState,
checkForAppUpdates,
clearUpdateTimer,
startUpdateTimer,
quitAndInstall,
getAutoUpdateEnabled,
setAutoUpdateEnabled,
} from './auto-updater'
import { initLogger, log, getLogDir } from './logger'
import { initLogger, log } from './logger'
import { setupIPC } from './ipc-handlers'
let mainWindow: BrowserWindow | null = null
let nitroProcess: ChildProcess | null = null
@ -448,143 +444,91 @@ function createWindow(): void {
mainWindow.webContents.openDevTools({ mode: 'detach' })
}
// ---------------------------------------------------------------------------
// Unsaved-changes close confirmation
// ---------------------------------------------------------------------------
let forceClose = false
mainWindow.on('close', (event) => {
if (forceClose || !mainWindow || mainWindow.isDestroyed()) return
// preventDefault must be called synchronously — check dirty state after
event.preventDefault()
const win = mainWindow
win.webContents
.executeJavaScript(
'({ dirty: window.__documentIsDirty === true,' +
' message: typeof window.__i18nT === "function" ? window.__i18nT("topbar.closeConfirmMessage") : "",' +
' detail: typeof window.__i18nT === "function" ? window.__i18nT("topbar.closeConfirmDetail") : "",' +
' save: typeof window.__i18nT === "function" ? window.__i18nT("common.save") : "",' +
' dontSave: typeof window.__i18nT === "function" ? window.__i18nT("topbar.dontSave") : "",' +
' cancel: typeof window.__i18nT === "function" ? window.__i18nT("common.cancel") : "" })',
)
.then((result: { dirty: boolean; message: string; detail: string; save: string; dontSave: string; cancel: string }) => {
if (!result.dirty) {
// Not dirty — allow close
forceClose = true
win.close()
return
}
// Show native save dialog with i18n strings
return dialog
.showMessageBox(win, {
type: 'question',
buttons: [
result.save || 'Save',
result.dontSave || "Don't Save",
result.cancel || 'Cancel',
],
defaultId: 0,
cancelId: 2,
message: result.message || 'Do you want to save changes before closing?',
detail: result.detail || 'Your changes will be lost if you don\'t save them.',
})
.then(({ response }) => {
if (response === 0) {
// Save — tell renderer to save then confirm close
win.webContents.send('menu:action', 'save-and-close')
} else if (response === 1) {
// Don't Save — force close
forceClose = true
win.close()
}
// Cancel (response === 2) — do nothing, window stays open
})
})
.catch(() => {
// Page not loaded or crashed — allow close
forceClose = true
win.close()
})
})
// Renderer confirms save completed → force close
ipcMain.on('window:confirmClose', () => {
forceClose = true
mainWindow?.close()
})
mainWindow.on('closed', () => {
mainWindow = null
})
}
// ---------------------------------------------------------------------------
// IPC: native file dialogs & updater
// IPC: native file dialogs & updater (extracted to ipc-handlers.ts)
// ---------------------------------------------------------------------------
function setupIPC(): void {
ipcMain.handle('dialog:openFile', async () => {
if (!mainWindow) return null
const result = await dialog.showOpenDialog(mainWindow, {
title: 'Open .op file',
filters: [{ name: 'OpenPencil Files', extensions: ['op', 'pen'] }],
properties: ['openFile'],
})
if (result.canceled || result.filePaths.length === 0) return null
const filePath = result.filePaths[0]
const content = await readFile(filePath, 'utf-8')
return { filePath, content }
})
ipcMain.handle(
'dialog:saveFile',
async (_event, payload: { content: string; defaultPath?: string }) => {
if (!mainWindow) return null
const result = await dialog.showSaveDialog(mainWindow, {
title: 'Save .op file',
defaultPath: payload.defaultPath,
filters: [{ name: 'OpenPencil Files', extensions: ['op'] }],
})
if (result.canceled || !result.filePath) return null
await writeFile(result.filePath, payload.content, 'utf-8')
return result.filePath
},
)
ipcMain.handle(
'dialog:saveToPath',
async (_event, payload: { filePath: string; content: string }) => {
const resolved = resolve(payload.filePath)
if (resolved.includes('\0')) {
throw new Error('Invalid file path')
}
const ext = extname(resolved).toLowerCase()
if (ext !== '.op' && ext !== '.pen') {
throw new Error('Only .op and .pen file extensions are allowed')
}
// Directory allowlist: only allow writes under user home or OS temp
const allowedRoots = [app.getPath('home'), app.getPath('temp')]
const inAllowedDir = allowedRoots.some(
(root) => resolved === root || resolved.startsWith(root + sep),
)
if (!inAllowedDir) {
throw new Error('File path must be within the user home or temp directory')
}
await writeFile(resolved, payload.content, 'utf-8')
return resolved
},
)
ipcMain.handle('file:getPending', () => {
if (pendingFilePath) {
const filePath = pendingFilePath
pendingFilePath = null
return filePath
}
return null
})
ipcMain.handle('file:read', async (_event, filePath: string) => {
const resolved = resolve(filePath)
const ext = extname(resolved).toLowerCase()
if (ext !== '.op' && ext !== '.pen') return null
try {
const content = await readFile(resolved, 'utf-8')
return { filePath: resolved, content }
} catch {
return null
}
})
// Theme sync for Windows/Linux title bar overlay
ipcMain.handle(
'theme:set',
(_event, theme: 'dark' | 'light', colors?: { bg: string; fg: string }) => {
if (!mainWindow || mainWindow.isDestroyed()) return
const isWinOrLinux = process.platform === 'win32' || process.platform === 'linux'
if (!isWinOrLinux) return
const isLinux = process.platform === 'linux'
const fallbackBg = theme === 'dark' ? '#111' : '#fff'
const fallbackFg = theme === 'dark' ? '#d4d4d8' : '#3f3f46'
mainWindow.setTitleBarOverlay({
// Windows supports transparent overlay; Linux uses actual CSS card color
color: isLinux ? (colors?.bg || fallbackBg) : 'rgba(0,0,0,0)',
symbolColor: colors?.fg || fallbackFg,
})
},
)
// Generic renderer preferences (replaces localStorage which is origin-scoped
// and lost when Nitro server restarts on a different random port)
ipcMain.handle('prefs:getAll', () => ({ ...prefsCache }))
ipcMain.handle('prefs:set', (_event, key: string, value: string) => {
prefsCache[key] = value
schedulePrefsWrite()
})
ipcMain.handle('prefs:remove', (_event, key: string) => {
delete prefsCache[key]
schedulePrefsWrite()
})
ipcMain.handle('log:getDir', () => getLogDir())
ipcMain.handle('updater:getState', () => getUpdaterState())
ipcMain.handle('updater:checkForUpdates', async () => {
await checkForAppUpdates(true)
return getUpdaterState()
})
ipcMain.handle('updater:quitAndInstall', () => quitAndInstall())
ipcMain.handle('updater:getAutoCheck', () => getAutoUpdateEnabled())
ipcMain.handle('updater:setAutoCheck', async (_event, enabled: boolean) => {
setAutoUpdateEnabled(enabled)
await writeAppSettings({ autoUpdate: enabled })
if (enabled) {
startUpdateTimer()
setUpdaterState({ status: 'idle' })
} else {
clearUpdateTimer()
setUpdaterState({ status: 'disabled' })
}
return enabled
function initIPC(): void {
setupIPC({
getMainWindow: () => mainWindow,
getPendingFilePath: () => pendingFilePath,
clearPendingFilePath: () => { pendingFilePath = null },
prefsCache,
schedulePrefsWrite,
writeAppSettings,
})
}
@ -651,7 +595,7 @@ app.on('ready', async () => {
await initLogger(app.getPath('userData'))
fixPath()
await loadPrefs()
setupIPC()
initIPC()
buildAppMenu()
if (!isDev) {

View file

@ -0,0 +1,6 @@
{
"name": "@zseven-w/desktop",
"version": "0.5.0",
"private": true,
"type": "module"
}

View file

@ -36,6 +36,7 @@ export interface ElectronAPI {
getPreferences: () => Promise<Record<string, string>>
setPreference: (key: string, value: string) => Promise<void>
removePreference: (key: string) => Promise<void>
confirmClose: () => void
updater: {
getState: () => Promise<UpdaterState>
checkForUpdates: () => Promise<UpdaterState>
@ -90,6 +91,8 @@ const api: ElectronAPI = {
getPendingFile: () => ipcRenderer.invoke('file:getPending'),
confirmClose: () => ipcRenderer.send('window:confirmClose'),
getLogDir: () => ipcRenderer.invoke('log:getDir'),
updater: {

View file

@ -3,7 +3,7 @@
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "../electron-dist",
"outDir": "../../out/desktop",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,

116
apps/web/CLAUDE.md Normal file
View file

@ -0,0 +1,116 @@
# Web App
TanStack Start full-stack React app (Vite + Nitro). Routes in `src/routes/`, auto-generated tree in `src/routeTree.gen.ts` (do not edit).
- `/` — Landing page
- `/editor` — Main design editor
## Canvas Engine (`src/canvas/`)
14 files + `skia/` subdir with 14 files.
### CanvasKit/Skia Architecture
- **GPU-accelerated WASM rendering** — CanvasKit (Skia compiled to WASM) renders all canvas content via WebGL surface
- **SkiaEngine class** (`skia-engine.ts`) is the core: owns the render loop, viewport transforms, node flattening, and `SpatialIndex` for hit testing
- **Dirty-flag rendering**`markDirty()` schedules a `requestAnimationFrame` redraw; no continuous rendering loop
- **Node flattening**`syncFromDocument()` walks the PenDocument tree, resolves auto-layout positions via layout engine, and produces flat `RenderNode[]` with absolute coordinates
- **SpatialIndex** (`skia-hit-test.ts`) — R-tree backed spatial queries for `hitTest()` (click) and `searchRect()` (marquee selection)
- **Coordinate conversion**`screenToScene()` / `sceneToScreen()` in `skia-viewport.ts` handle viewport ↔ scene transforms
- **Event handling** — mouse/keyboard events managed by `SkiaInteractionManager` (`skia-interaction.ts`); hit testing for resize/rotate/arc handles in `skia-hit-handlers.ts`; `skia-canvas.tsx` is the React component (lifecycle, sync, rendering)
- **Parent-child transforms** — nodes are flattened to absolute coordinates; transforms propagate to descendants during drag/scale/rotate
### `skia/` Files
- `skia-canvas.tsx` — React component: lifecycle, sync effects, wheel zoom, text editing overlay; delegates interaction to `SkiaInteractionManager`
- `skia-interaction.ts``SkiaInteractionManager` class: all mouse/keyboard interaction state and handlers (select, drag, resize, rotate, draw, marquee, pen tool, arc editing, hover cursor)
- `skia-hit-handlers.ts` — Hit test functions: `hitTestHandle` (resize), `hitTestRotation` (rotation zone), `hitTestArcHandle` (ellipse arc)
- `skia-engine.ts` — Core rendering engine: `SkiaEngine` class, `syncFromDocument()`, viewport, node flattening, zoom/pan, dirty-flag loop
- `skia-renderer.ts` — GPU draw calls: shapes, text, paths, images, selection handles, guides, agent indicators
- `skia-init.ts` — CanvasKit WASM loader with CDN fallback
- `skia-hit-test.ts``SpatialIndex` R-tree for spatial queries
- `skia-viewport.ts` — Viewport math
- `skia-paint-utils.ts` — Color parsing, gradient creation, text line wrapping
- `skia-path-utils.ts` — SVG path to CanvasKit Path conversion
- `skia-image-loader.ts` — Async image loading and caching
- `skia-overlays.ts` — Selection overlays, hover highlights, dimension labels
- `skia-pen-tool.ts` — Pen tool: anchor points, control handles, path building
- `skia-font-manager.ts` — Font management
### Shared Canvas Modules
- `canvas-sync-lock.ts` — Prevents circular sync loops
- `canvas-sync-utils.ts``forcePageResync()` utility
- `canvas-constants.ts` — Default colors, zoom limits, stroke widths
- `canvas-node-creator.ts``createNodeForTool`, `isDrawingTool`
- `canvas-layout-engine.ts` — Auto-layout (delegates to `@zseven-w/pen-core`)
- `canvas-text-measure.ts` — Text width/height estimation, CJK detection
- `font-utils.ts`, `node-helpers.ts` — Re-exports from pen-core
- `insertion-indicator.ts`, `selection-context.ts`, `agent-indicator.ts`, `use-layout-indicator.ts`, `skia-engine-ref.ts`
## Zustand Stores (`src/stores/`)
- `canvas-store.ts` — UI/tool/selection/viewport/clipboard/interaction state, `activePageId`
- `document-store.ts` — PenDocument tree CRUD, variable CRUD, component management (all with history)
- `document-store-pages.ts` — Page actions: add, remove, rename, reorder, duplicate
- `document-tree-utils.ts` — Re-exports tree helpers and clone utilities from `@zseven-w/pen-core`
- `history-store.ts` — Undo/redo (max 300 states), batch mode
- `ai-store.ts` — Chat messages, streaming state, model selection
- `agent-settings-store.ts` — AI provider config, MCP CLI integrations, localStorage persistence
- `uikit-store.ts` — UIKit management
- `theme-preset-store.ts` — Theme preset management
## Components (`src/components/`)
- **`editor/`** — Editor UI: editor-layout, toolbar, boolean-toolbar, tool-button, shape-tool-dropdown, top-bar, status-bar, page-tabs, update-ready-banner
- **`panels/`** — 32 files: layer panel, property panel, fill/stroke/corner/size/text/effects/export/layout/appearance sections, AI chat panel, code panel, component browser, variables panel
- **`shared/`** — Reusable UI: ColorPicker, NumberInput, SectionHeader, ExportDialog, SaveDialog, AgentSettingsDialog, IconPickerDialog, VariablePicker, FigmaImportDialog, FontPicker, LanguageSelector
- **`icons/`** — Provider/brand logos
- **`ui/`** — shadcn/ui primitives
## AI Services (`src/services/ai/`)
35 files + `role-definitions/` + `design-principles/` subdirs:
- `ai-service.ts` — Main AI chat API wrapper, model negotiation, provider selection
- `ai-prompts.ts` — System prompts for design generation
- `ai-types.ts` — ChatMessage, ChatAttachment, AIDesignRequest, OrchestratorPlan
- `model-profiles.ts` — Adapts thinking mode, effort, timeouts per model tier
- `design-generator.ts` — Top-level `generateDesign`/`generateDesignModification`
- `design-parser.ts` — JSON/JSONL parsing
- `design-canvas-ops.ts` — Canvas mutation operations
- `design-node-sanitization.ts` — Node merging (re-exports `deepCloneNode` from pen-core)
- `design-validation.ts` / `design-pre-validation.ts` / `design-validation-fixes.ts` — Post-generation validation
- `icon-resolver.ts` — Auto-resolves icon names to Lucide SVG paths
- `orchestrator.ts` / `orchestrator-sub-agent.ts` / `orchestrator-prompts.ts` — Spatial decomposition orchestrator
- `context-optimizer.ts` — Chat history trimming
## Hooks (`src/hooks/`)
- `use-keyboard-shortcuts.ts` — Global keyboard: tools, clipboard, undo/redo, save, z-order, boolean ops
- `use-electron-menu.ts` — Electron native menu IPC listener
- `use-figma-paste.ts` — Figma clipboard paste
- `use-file-drop.ts` — File drag-and-drop
- `use-mcp-sync.ts` — MCP live canvas sync
- `use-system-fonts.ts` — System font detection
## MCP Server (`src/mcp/`)
- `server.ts` — MCP server entry point, tool registration (stdio + HTTP modes)
- `document-manager.ts` — Document read/write/cache; live canvas sync via Nitro API
- `tools/` — Core (open-document, batch-get, get-selection, batch-design, node-crud), Layout (snapshot-layout, find-empty-space, import-svg), Variables, Pages, Layered design (design-prompt, design-skeleton, design-content, design-refine)
- `utils/``id.ts`, `node-operations.ts` (re-exports `cloneNodeWithNewIds` from pen-core), `sanitize.ts`, `svg-node-parser.ts`
## UIKit (`src/uikit/`)
- `built-in-registry.ts` — Default built-in UIKit
- `kit-import-export.ts` — Import/export UIKits from .pen files
- `kit-utils.ts` — Extract components, find reusable nodes (re-exports `deepCloneNode` from pen-core)
## Utilities (`src/utils/`)
File operations: save/open .pen, export PNG/SVG, node clone (re-exports `cloneNodesWithNewIds` from pen-core), pen file normalization, SVG parser, syntax highlight, boolean operations, `app-storage.ts`, `arc-path.ts`, `theme-preset-io.ts`, `id.ts`
## Server API (`server/`)
- **`api/ai/`** — Nitro API (11 files): streaming chat, generation, agent connection, validation, MCP install, icon resolution, image generation/search. Supports Anthropic API key or Claude Agent SDK (local OAuth)
- **`utils/`** — Server utilities: Claude CLI resolver, OpenCode/Codex/Copilot clients, MCP server manager, sync state, server logger

6
apps/web/package.json Normal file
View file

@ -0,0 +1,6 @@
{
"name": "@zseven-w/web",
"version": "0.5.0",
"private": true,
"type": "module"
}

View file

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 544 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,16 @@
{
"short_name": "OpenPencil",
"name": "OpenPencil",
"description": "Open-source vector design tool with Design-as-Code philosophy",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#000000"
}

View file

@ -31,7 +31,7 @@ interface ChatBody {
system: string
messages: Array<{ role: 'user' | 'assistant'; content: string; attachments?: ChatAttachmentWire[] }>
model?: string
provider?: 'anthropic' | 'openai' | 'opencode' | 'copilot'
provider?: 'anthropic' | 'openai' | 'opencode' | 'copilot' | 'gemini'
thinkingMode?: 'adaptive' | 'disabled' | 'enabled'
thinkingBudgetTokens?: number
effort?: 'low' | 'medium' | 'high' | 'max'
@ -93,7 +93,7 @@ export default defineEventHandler(async (event) => {
setResponseHeaders(event, { 'Content-Type': 'application/json' })
return { error: 'Missing model. Model fallback is disabled.' }
}
if (body.provider !== 'anthropic' && body.provider !== 'openai' && body.provider !== 'opencode' && body.provider !== 'copilot') {
if (body.provider !== 'anthropic' && body.provider !== 'openai' && body.provider !== 'opencode' && body.provider !== 'copilot' && body.provider !== 'gemini') {
setResponseHeaders(event, { 'Content-Type': 'application/json' })
return { error: 'Missing or unsupported provider. Provider fallback is disabled.' }
}
@ -107,6 +107,7 @@ export default defineEventHandler(async (event) => {
if (body.provider === 'anthropic') return streamViaAgentSDK(body, body.model)
if (body.provider === 'opencode') return streamViaOpenCode(body, body.model)
if (body.provider === 'copilot') return streamViaCopilot(body, body.model)
if (body.provider === 'gemini') return streamViaGemini(body, body.model)
return streamViaCodex(body, body.model)
})
@ -720,6 +721,61 @@ function mapCopilotReasoningEffort(
return effort
}
/** Stream via Gemini CLI (`gemini -p -o stream-json`) — CLI handles its own auth */
function streamViaGemini(body: ChatBody, model?: string) {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
const pingTimer = setInterval(() => {
try {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'ping', content: '' })}\n\n`))
} catch { /* stream already closed */ }
}, KEEPALIVE_INTERVAL_MS)
try {
const { streamGeminiExec } = await import('../../utils/gemini-client')
// Build prompt from messages
const lastUserMsg = [...body.messages].reverse().find((m) => m.role === 'user')
const prompt = lastUserMsg?.content ?? ''
const { stream: geminiStream } = streamGeminiExec(prompt, {
model,
systemPrompt: body.system,
})
for await (const event of geminiStream) {
clearInterval(pingTimer)
if (event.type === 'text') {
const data = JSON.stringify({ type: 'text', content: event.content })
try {
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
} catch { /* stream closed */ }
} else if (event.type === 'error') {
const data = JSON.stringify({ type: 'error', content: event.content })
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
}
// 'done' is handled after loop
}
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'done', content: '' })}\n\n`),
)
} catch (error) {
const content = error instanceof Error ? error.message : 'Unknown error'
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'error', content })}\n\n`),
)
} finally {
clearInterval(pingTimer)
controller.close()
}
},
})
return new Response(stream)
}
/** Stream via GitHub Copilot SDK (@github/copilot-sdk) */
function streamViaCopilot(body: ChatBody, model?: string) {
const stream = new ReadableStream({

View file

@ -39,7 +39,7 @@ function buildExecCmd(binPath: string, args: string): string {
}
interface ConnectBody {
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot'
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot' | 'gemini-cli'
}
interface ConnectResult {
@ -82,6 +82,10 @@ export default defineEventHandler(async (event) => {
return connectCopilot()
}
if (body.agent === 'gemini-cli') {
return connectGeminiCli()
}
return { connected: false, models: [], error: `Unknown agent: ${body.agent}` } satisfies ConnectResult
})
@ -667,3 +671,174 @@ function friendlyOpenCodeError(raw: string): string {
}
return raw
}
/** Fallback model list when dynamic fetch fails */
const FALLBACK_GEMINI_MODELS: GroupedModel[] = [
{ value: 'gemini-3-pro-preview', displayName: 'Gemini 3 Pro', description: 'Most capable', provider: 'gemini' },
{ value: 'gemini-3-flash-preview', displayName: 'Gemini 3 Flash', description: 'Fast + capable', provider: 'gemini' },
{ value: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', description: 'Thinking model', provider: 'gemini' },
{ value: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', description: 'Fast + thinking', provider: 'gemini' },
{ value: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash', description: 'Fast model', provider: 'gemini' },
]
/** Fetch available models from Gemini API using local auth credentials */
async function fetchGeminiModels(): Promise<GroupedModel[]> {
const { readFile } = await import('node:fs/promises')
const { homedir } = await import('node:os')
const { join } = await import('node:path')
// Build auth header — try API key first, then OAuth token
let authUrl: (base: string) => string
let headers: Record<string, string> = {}
const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
if (envKey) {
authUrl = (base) => `${base}?key=${envKey}`
} else {
// Read OAuth token
const oauthPath = join(homedir(), '.gemini', 'oauth_creds.json')
const raw = await readFile(oauthPath, 'utf-8')
const creds = JSON.parse(raw) as { access_token?: string; expiry_date?: number }
if (!creds.access_token) throw new Error('No access token')
if (creds.expiry_date && Date.now() > creds.expiry_date - 60_000) throw new Error('Token expired')
authUrl = (base) => base
headers = { Authorization: `Bearer ${creds.access_token}` }
}
const res = await fetch(authUrl('https://generativelanguage.googleapis.com/v1beta/models'), { headers })
if (!res.ok) throw new Error(`API ${res.status}`)
const data = await res.json() as {
models?: Array<{
name?: string
displayName?: string
description?: string
supportedGenerationMethods?: string[]
}>
}
const models: GroupedModel[] = []
const seen = new Set<string>()
for (const m of data.models ?? []) {
// Only include models that support generateContent (text generation)
if (!m.supportedGenerationMethods?.includes('generateContent')) continue
const id = m.name?.replace('models/', '') ?? ''
if (!id || seen.has(id)) continue
// Skip embedding, AQA, and legacy models
if (/embed|aqa|^chat-bison|^text-bison|^gemini-1\.0/i.test(id)) continue
seen.add(id)
models.push({
value: id,
displayName: m.displayName ?? id,
description: m.description?.slice(0, 60) ?? '',
provider: 'gemini' as const,
})
}
// Sort: gemini-3 first, then 2.5, then others
models.sort((a, b) => {
const order = (v: string) => {
if (v.includes('gemini-3')) return 0
if (v.includes('gemini-2.5-pro')) return 1
if (v.includes('gemini-2.5-flash')) return 2
if (v.includes('gemini-2.0')) return 3
return 4
}
return order(a.value) - order(b.value)
})
return models
}
/** Connect to Gemini CLI and return available models. */
async function connectGeminiCli(): Promise<ConnectResult> {
serverLog.info('[connect-agent] connecting to Gemini CLI...')
try {
const { resolveGeminiCli } = await import('../../utils/resolve-gemini-cli')
const binPath = resolveGeminiCli()
serverLog.info(`[connect-agent] resolved gemini path: ${binPath ?? 'NOT FOUND'}`)
if (!binPath) {
return { connected: false, models: [], notInstalled: true, error: 'Gemini CLI not found' }
}
// Verify binary responds
const { execSync } = await import('node:child_process')
const versionCmd = buildExecCmd(binPath, '--version')
try {
const ver = execSync(`${versionCmd} 2>&1`, { encoding: 'utf-8', timeout: 10000 }).trim()
serverLog.info(`[connect-agent] gemini version: ${ver}`)
} catch (err) {
serverLog.error(`[connect-agent] gemini --version failed: ${err instanceof Error ? err.message : err}`)
return { connected: false, models: [], error: 'Gemini CLI not responding' }
}
// Dynamically fetch models, fallback to hardcoded list
let models: GroupedModel[]
try {
models = await fetchGeminiModels()
serverLog.info(`[connect-agent] gemini: fetched ${models.length} models from API`)
} catch (err) {
serverLog.info(`[connect-agent] gemini: model fetch failed (${err instanceof Error ? err.message : err}), using fallback`)
models = FALLBACK_GEMINI_MODELS
}
const geminiInfo = await buildGeminiConnectionInfo()
const warning = models.length === 0 ? 'No models found. Try running "gemini" once to authenticate.' : undefined
if (models.length === 0) models = FALLBACK_GEMINI_MODELS
serverLog.info(`[connect-agent] gemini connected, ${models.length} models`)
return { connected: true, models, warning, ...geminiInfo }
} catch (error) {
const raw = error instanceof Error ? error.message : 'Failed to connect'
serverLog.error(`[connect-agent] gemini connection error: ${raw}`)
return { connected: false, models: [], error: friendlyGeminiError(raw) }
}
}
/** Build Gemini CLI connection info from local config files */
async function buildGeminiConnectionInfo(): Promise<{ connectionInfo: string; hintPath?: string }> {
const { readFile } = await import('node:fs/promises')
const { homedir } = await import('node:os')
const { join } = await import('node:path')
const hp = configPath('~/.gemini/settings.json', '%USERPROFILE%\\.gemini\\settings.json')
// Check env for API key
const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
if (envKey) {
const masked = envKey.length > 12 ? `${envKey.slice(0, 8)}...` : '***'
return { connectionInfo: `Connected via API key (${masked})`, hintPath: hp }
}
// Check OAuth creds (Gemini CLI login)
try {
const oauthPath = join(homedir(), '.gemini', 'oauth_creds.json')
await readFile(oauthPath, 'utf-8') // Check existence
// Try to get account email
try {
const accountsPath = join(homedir(), '.gemini', 'google_accounts.json')
const accountsRaw = await readFile(accountsPath, 'utf-8')
const accounts = JSON.parse(accountsRaw) as { active?: string }
if (accounts.active) {
return { connectionInfo: `Connected via Google (${accounts.active})`, hintPath: hp }
}
} catch { /* no accounts file */ }
return { connectionInfo: 'Connected via Google OAuth', hintPath: hp }
} catch { /* no OAuth creds */ }
return { connectionInfo: 'Connected via Gemini CLI', hintPath: hp }
}
/** Map Gemini CLI errors to user-friendly messages */
function friendlyGeminiError(raw: string): string {
if (/not found|ENOENT/i.test(raw)) {
return 'Gemini CLI not found. Install it with: npm install -g @anthropic-ai/gemini-cli'
}
if (/not authenticated|authenticate|auth|login/i.test(raw)) {
return 'Not authenticated. Run "gemini" in your terminal first to set up authentication.'
}
if (/timed?\s*out/i.test(raw)) {
return 'Connection timed out. Please try again.'
}
return raw
}

View file

@ -12,7 +12,7 @@ interface GenerateBody {
system: string
message: string
model?: string
provider?: 'anthropic' | 'openai' | 'opencode'
provider?: 'anthropic' | 'openai' | 'opencode' | 'gemini'
thinkingMode?: 'adaptive' | 'disabled' | 'enabled'
thinkingBudgetTokens?: number
effort?: 'low' | 'medium' | 'high' | 'max'
@ -48,6 +48,9 @@ export default defineEventHandler(async (event) => {
if (body.provider === 'openai') {
return generateViaCodex(body, body.model)
}
if (body.provider === 'gemini') {
return generateViaGemini(body, body.model)
}
return { error: 'Missing or unsupported provider. Provider fallback is disabled.' }
})
@ -261,3 +264,15 @@ async function generateViaOpenCode(body: GenerateBody, model?: string): Promise<
releaseOpencodeServer(ocServer)
}
}
/** Generate via Gemini CLI (`gemini -p -o json`) — CLI handles its own auth */
async function generateViaGemini(body: GenerateBody, model?: string): Promise<{ text?: string; error?: string }> {
const { runGeminiExec } = await import('../../utils/gemini-client')
return runGeminiExec(body.message, {
model,
systemPrompt: body.system,
thinkingMode: body.thinkingMode,
thinkingBudgetTokens: body.thinkingBudgetTokens,
effort: body.effort,
})
}

View file

@ -0,0 +1,311 @@
import { defineEventHandler, readBody, setResponseHeaders, createError } from 'h3'
interface ImageGenerateBody {
prompt: string
provider: 'openai' | 'custom' | 'gemini' | 'replicate'
model: string
apiKey: string
baseUrl?: string
width?: number
height?: number
}
/**
* POST /api/ai/image-generate
*
* Multi-provider image generation endpoint.
* Supports OpenAI (dall-e-3/dall-e-2), Gemini (imagen), and Replicate.
* Returns { url: string } either a remote URL or a base64 data URL.
*/
export default defineEventHandler(async (event) => {
setResponseHeaders(event, { 'Content-Type': 'application/json' })
const body = await readBody<ImageGenerateBody>(event)
if (!body?.prompt?.trim()) {
throw createError({ statusCode: 400, message: 'Missing required field: prompt' })
}
if (!body?.provider) {
throw createError({ statusCode: 400, message: 'Missing required field: provider' })
}
if (!body?.apiKey?.trim()) {
throw createError({ statusCode: 400, message: 'Missing required field: apiKey' })
}
const { prompt, provider, model, apiKey, baseUrl, width, height } = body
if (provider === 'openai' || provider === 'custom') {
return await generateOpenAI({ prompt, model, apiKey, baseUrl, width, height })
}
if (provider === 'gemini') {
return await generateGemini({ prompt, model, apiKey, baseUrl, width, height })
}
if (provider === 'replicate') {
return await generateReplicate({ prompt, model, apiKey, baseUrl, width, height })
}
throw createError({ statusCode: 400, message: `Unsupported provider: ${provider}` })
})
// ---------------------------------------------------------------------------
// Size mapping
// ---------------------------------------------------------------------------
function mapToOpenAISize(w?: number, h?: number): string {
if (!w || !h) return '1024x1024'
const ratio = w / h
if (ratio > 1.3) return '1792x1024'
if (ratio < 0.77) return '1024x1792'
return '1024x1024'
}
// ---------------------------------------------------------------------------
// OpenAI / custom OpenAI-compatible provider
// ---------------------------------------------------------------------------
async function generateOpenAI(opts: {
prompt: string
model: string
apiKey: string
baseUrl?: string
width?: number
height?: number
}): Promise<{ url: string }> {
const { prompt, model, apiKey, baseUrl, width, height } = opts
const size = mapToOpenAISize(width, height)
const endpoint = `${baseUrl ?? 'https://api.openai.com'}/v1/images/generations`
let res: Response
try {
res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ model, prompt, n: 1, size, response_format: 'url' }),
})
} catch (err) {
throw createError({ statusCode: 502, message: `OpenAI request failed: ${String(err)}` })
}
if (!res.ok) {
const text = await res.text().catch(() => '')
let msg = `OpenAI returned ${res.status}`
try {
const errJson = JSON.parse(text) as { error?: { message?: string } }
if (errJson.error?.message) msg = errJson.error.message
} catch {
if (text) msg += `: ${text.slice(0, 150)}`
}
throw createError({ statusCode: 502, message: msg })
}
const data = (await res.json()) as { data?: { url?: string }[] }
const url = data?.data?.[0]?.url
if (!url) {
throw createError({ statusCode: 502, message: 'OpenAI response missing image URL' })
}
return { url }
}
// ---------------------------------------------------------------------------
// Gemini image generation
// ---------------------------------------------------------------------------
function mapToGeminiAspectRatio(w?: number, h?: number): string | undefined {
if (!w || !h) return undefined
const ratio = w / h
if (ratio > 1.6) return '16:9'
if (ratio > 1.3) return '4:3'
if (ratio < 0.625) return '9:16'
if (ratio < 0.77) return '3:4'
return '1:1'
}
async function generateGemini(opts: {
prompt: string
model: string
apiKey: string
baseUrl?: string
width?: number
height?: number
}): Promise<{ url: string }> {
const { prompt, model, apiKey, baseUrl, width, height } = opts
const base = baseUrl ?? 'https://generativelanguage.googleapis.com'
const endpoint = `${base}/v1beta/models/${model}:generateContent?key=${apiKey}`
const generationConfig: Record<string, unknown> = { responseModalities: ['TEXT', 'IMAGE'] }
const aspectRatio = mapToGeminiAspectRatio(width, height)
if (aspectRatio) {
generationConfig.imageConfig = { aspectRatio }
}
let res: Response
try {
res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig,
}),
})
} catch (err) {
throw createError({ statusCode: 502, message: `Gemini request failed: ${String(err)}` })
}
if (!res.ok) {
const text = await res.text().catch(() => '')
let msg = `Gemini returned ${res.status}`
try {
const errJson = JSON.parse(text) as { error?: { message?: string } }
if (errJson.error?.message) msg = errJson.error.message
} catch {
if (text) msg += `: ${text.slice(0, 150)}`
}
throw createError({ statusCode: 502, message: msg })
}
const data = (await res.json()) as {
candidates?: {
content?: {
parts?: {
inlineData?: { mimeType?: string; data?: string }
text?: string
}[]
}
}[]
}
const parts = data?.candidates?.[0]?.content?.parts ?? []
const imagePart = parts.find((p) => p.inlineData?.mimeType?.startsWith('image/'))
if (!imagePart?.inlineData?.data || !imagePart.inlineData.mimeType) {
throw createError({ statusCode: 502, message: 'Gemini response missing inline image data' })
}
const { mimeType, data: base64data } = imagePart.inlineData
return { url: `data:${mimeType};base64,${base64data}` }
}
// ---------------------------------------------------------------------------
// Replicate
// ---------------------------------------------------------------------------
async function generateReplicate(opts: {
prompt: string
model: string
apiKey: string
baseUrl?: string
width?: number
height?: number
}): Promise<{ url: string }> {
const { prompt, model, apiKey, baseUrl, width, height } = opts
const base = baseUrl ?? 'https://api.replicate.com'
// Start prediction
let createRes: Response
try {
const input: Record<string, unknown> = { prompt }
if (width) input.width = width
if (height) input.height = height
createRes = await fetch(`${base}/v1/predictions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ model, input }),
})
} catch (err) {
throw createError({ statusCode: 502, message: `Replicate request failed: ${String(err)}` })
}
if (!createRes.ok) {
const text = await createRes.text().catch(() => '')
let msg = `Replicate returned ${createRes.status}`
try {
const errJson = JSON.parse(text) as { detail?: string }
if (errJson.detail) msg = errJson.detail
} catch {
if (text) msg += `: ${text.slice(0, 150)}`
}
throw createError({ statusCode: 502, message: msg })
}
const prediction = (await createRes.json()) as { id?: string; status?: string }
const predictionId = prediction?.id
if (!predictionId) {
throw createError({ statusCode: 502, message: 'Replicate response missing prediction ID' })
}
// Poll until succeeded or failed (max 120s, polling every 2s)
const maxAttempts = 60
for (let attempt = 0; attempt < maxAttempts; attempt++) {
await new Promise((resolve) => setTimeout(resolve, 2000))
let pollRes: Response
try {
pollRes = await fetch(`${base}/v1/predictions/${predictionId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
} catch (err) {
throw createError({
statusCode: 502,
message: `Replicate poll request failed: ${String(err)}`,
})
}
if (!pollRes.ok) {
const text = await pollRes.text().catch(() => '')
throw createError({
statusCode: 502,
message: `Replicate poll returned ${pollRes.status}: ${text.slice(0, 200)}`,
})
}
const status = (await pollRes.json()) as {
id?: string
status?: string
output?: string | string[]
error?: string
}
if (status.status === 'succeeded') {
const output = status.output
if (Array.isArray(output)) {
const first = output[0]
if (!first) {
throw createError({
statusCode: 502,
message: 'Replicate succeeded but output array is empty',
})
}
return { url: first }
}
if (typeof output === 'string') {
return { url: output }
}
throw createError({ statusCode: 502, message: 'Replicate succeeded but output is missing' })
}
if (status.status === 'failed' || status.status === 'canceled') {
throw createError({
statusCode: 502,
message: `Replicate prediction ${status.status}: ${status.error ?? 'unknown error'}`,
})
}
// Still starting/processing — keep polling
}
throw createError({
statusCode: 502,
message: 'Replicate prediction timed out after 120 seconds',
})
}

View file

@ -0,0 +1,276 @@
import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
import type { ImageSearchResult, ImageSearchResponse } from '../../../src/types/image-service'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface OpenverseImageResult {
id: string
url: string
thumbnail: string
width: number
height: number
license: string
license_version: string
attribution: string
}
interface OpenverseSearchResponse {
results: OpenverseImageResult[]
}
interface WikimediaImageInfo {
url: string
thumburl: string
width: number
height: number
mime: string
extmetadata?: {
LicenseShortName?: { value: string }
}
}
interface WikimediaPage {
pageid: number
title: string
imageinfo?: WikimediaImageInfo[]
}
interface WikimediaQueryResponse {
query?: {
pages?: Record<string, WikimediaPage>
}
}
// ---------------------------------------------------------------------------
// OAuth token cache
// ---------------------------------------------------------------------------
let cachedToken: string | null = null
let tokenExpiresAt = 0
async function getOpenverseToken(clientId: string, clientSecret: string): Promise<string | null> {
const now = Date.now()
if (cachedToken && now < tokenExpiresAt) {
return cachedToken
}
try {
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
})
const res = await fetch('https://api.openverse.org/v1/auth_tokens/token/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
if (!res.ok) return null
const data = (await res.json()) as { access_token: string; expires_in: number }
cachedToken = data.access_token
// Refresh 60 seconds before expiry
tokenExpiresAt = now + (data.expires_in - 60) * 1000
return cachedToken
} catch {
return null
}
}
// ---------------------------------------------------------------------------
// Query simplification — convert verbose AI prompts to search keywords
// ---------------------------------------------------------------------------
const STOP_WORDS = new Set([
'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
'of', 'with', 'by', 'from', 'is', 'are', 'was', 'were', 'be', 'been',
'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
'could', 'should', 'may', 'might', 'shall', 'can', 'that', 'this',
'these', 'those', 'it', 'its', 'very', 'really', 'just', 'also',
'about', 'above', 'after', 'before', 'between', 'into', 'through',
'during', 'each', 'some', 'such', 'no', 'not', 'only', 'same', 'so',
'than', 'too', 'up', 'out', 'if', 'then', 'once', 'here', 'there',
'when', 'where', 'how', 'all', 'both', 'few', 'more', 'most', 'other',
'any', 'as', 'while', 'using', 'showing', 'featuring', 'looking',
'style', 'styled', 'inspired', 'based',
])
/**
* Simplify a verbose image generation prompt into 2-4 search keywords.
* "delicious burger with fries and fresh vegetables" "burger fries vegetables"
* "modern office workspace with natural lighting" "modern office workspace"
*/
export function simplifySearchQuery(prompt: string): string {
const words = prompt
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, ' ')
.split(/\s+/)
.filter((w) => w.length > 2 && !STOP_WORDS.has(w))
// Take up to 4 keywords
const keywords = words.slice(0, 4)
return keywords.join(' ') || prompt.slice(0, 30)
}
// ---------------------------------------------------------------------------
// Mapping helpers (exported for testing)
// ---------------------------------------------------------------------------
export function mapOpenverseResult(r: OpenverseImageResult): ImageSearchResult {
return {
id: r.id,
url: r.url,
thumbUrl: r.thumbnail,
width: r.width,
height: r.height,
source: 'openverse',
license: `${r.license} ${r.license_version}`.trim(),
attribution: r.attribution,
}
}
export function mapWikimediaPages(
pages: Record<string, WikimediaPage>,
): ImageSearchResult[] {
const results: ImageSearchResult[] = []
for (const page of Object.values(pages)) {
const info = page.imageinfo?.[0]
if (!info) continue
results.push({
id: String(page.pageid),
url: info.url,
thumbUrl: info.thumburl ?? info.url,
width: info.width,
height: info.height,
source: 'wikimedia',
license: info.extmetadata?.LicenseShortName?.value ?? '',
})
}
return results
}
// ---------------------------------------------------------------------------
// Source fetchers
// ---------------------------------------------------------------------------
async function fetchFromOpenverse(
query: string,
count: number,
aspectRatio: string | undefined,
clientId: string | undefined,
clientSecret: string | undefined,
): Promise<ImageSearchResult[] | null> {
const url = new URL('https://api.openverse.org/v1/images/')
url.searchParams.set('q', query)
url.searchParams.set('page_size', String(count))
if (aspectRatio) {
url.searchParams.set('aspect_ratio', aspectRatio)
}
const headers: Record<string, string> = {}
if (clientId && clientSecret) {
const token = await getOpenverseToken(clientId, clientSecret)
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
const res = await fetch(url.toString(), { headers })
if (res.status === 429) {
// Rate limited — signal fallback
return null
}
if (!res.ok) {
return null
}
const data = (await res.json()) as OpenverseSearchResponse
return (data.results ?? []).map(mapOpenverseResult)
}
async function fetchFromWikimedia(
query: string,
count: number,
): Promise<ImageSearchResult[]> {
const url = new URL('https://commons.wikimedia.org/w/api.php')
url.searchParams.set('action', 'query')
url.searchParams.set('generator', 'search')
url.searchParams.set('gsrsearch', query)
url.searchParams.set('gsrnamespace', '6')
url.searchParams.set('gsrlimit', String(count))
url.searchParams.set('prop', 'imageinfo')
url.searchParams.set('iiprop', 'url|size|mime|extmetadata')
url.searchParams.set('iiurlwidth', '800')
url.searchParams.set('format', 'json')
url.searchParams.set('origin', '*')
const res = await fetch(url.toString())
if (!res.ok) return []
const data = (await res.json()) as WikimediaQueryResponse
const pages = data.query?.pages
if (!pages) return []
return mapWikimediaPages(pages)
}
// ---------------------------------------------------------------------------
// Endpoint
// ---------------------------------------------------------------------------
/**
* POST /api/ai/image-search
*
* Searches for freely-licensed images.
* Primary source: Openverse. Falls back to Wikimedia Commons on 429.
*
* Body: { query, count?, aspectRatio?, openverseClientId?, openverseClientSecret? }
*/
export default defineEventHandler(async (event) => {
setResponseHeaders(event, { 'Content-Type': 'application/json' })
const body = await readBody(event) as {
query?: string
count?: number
aspectRatio?: string
openverseClientId?: string
openverseClientSecret?: string
}
const rawQuery = body?.query?.trim() ?? ''
if (!rawQuery) {
return { error: 'Missing required field: query' }
}
// Simplify verbose AI prompts into search-friendly keywords
const query = simplifySearchQuery(rawQuery)
const count = Math.min(Math.max(Number(body?.count ?? 10), 1), 50)
const aspectRatio = body?.aspectRatio
const clientId = body?.openverseClientId
const clientSecret = body?.openverseClientSecret
// Try Openverse first
const openverseResults = await fetchFromOpenverse(
query,
count,
aspectRatio,
clientId,
clientSecret,
)
if (openverseResults !== null) {
return {
results: openverseResults,
source: 'openverse',
} satisfies ImageSearchResponse
}
// Openverse returned 429 or failed — fall back to Wikimedia
const wikimediaResults = await fetchFromWikimedia(query, count)
return {
results: wikimediaResults,
source: 'wikimedia',
} satisfies ImageSearchResponse
})

View file

@ -0,0 +1,104 @@
import { defineEventHandler, readBody } from 'h3'
interface ImageServiceTestRequest {
service: string
apiKey?: string
model?: string
baseUrl?: string
clientId?: string
clientSecret?: string
}
interface ImageServiceTestResponse {
valid: boolean
error?: string
}
/**
* POST /api/ai/image-service-test
*
* Validates API keys for image generation services.
* Returns { valid: boolean, error?: string }
*/
export default defineEventHandler(async (event): Promise<ImageServiceTestResponse> => {
const body = await readBody<ImageServiceTestRequest>(event)
const { service, apiKey, baseUrl, clientId, clientSecret } = body ?? {}
if (!service) {
return { valid: false, error: 'Missing required field: service' }
}
try {
switch (service) {
case 'openverse': {
if (!clientId || !clientSecret) {
return { valid: false, error: 'Openverse requires clientId and clientSecret' }
}
const formData = new URLSearchParams()
formData.set('grant_type', 'client_credentials')
formData.set('client_id', clientId)
formData.set('client_secret', clientSecret)
const res = await fetch('https://api.openverse.org/v1/auth_tokens/token/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString(),
})
if (!res.ok) {
const text = await res.text().catch(() => '')
return { valid: false, error: `Openverse auth failed (${res.status}): ${text}` }
}
return { valid: true }
}
case 'openai':
case 'custom': {
if (!apiKey) {
return { valid: false, error: 'Missing required field: apiKey' }
}
const origin = baseUrl ?? 'https://api.openai.com'
const res = await fetch(`${origin}/v1/models`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
if (!res.ok) {
const text = await res.text().catch(() => '')
return { valid: false, error: `Models request failed (${res.status}): ${text}` }
}
return { valid: true }
}
case 'gemini': {
if (!apiKey) {
return { valid: false, error: 'Missing required field: apiKey' }
}
const origin = baseUrl ?? 'https://generativelanguage.googleapis.com'
const res = await fetch(`${origin}/v1beta/models?key=${encodeURIComponent(apiKey)}`)
if (!res.ok) {
const text = await res.text().catch(() => '')
return { valid: false, error: `Gemini models request failed (${res.status}): ${text}` }
}
return { valid: true }
}
case 'replicate': {
if (!apiKey) {
return { valid: false, error: 'Missing required field: apiKey' }
}
const origin = baseUrl ?? 'https://api.replicate.com'
const res = await fetch(`${origin}/v1/models`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
if (!res.ok) {
const text = await res.text().catch(() => '')
return { valid: false, error: `Replicate models request failed (${res.status}): ${text}` }
}
return { valid: true }
}
default:
return { valid: false, error: `Unknown service: ${service}` }
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
return { valid: false, error: message }
}
})

View file

@ -2,7 +2,7 @@ import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
import { execSync } from 'node:child_process'
interface InstallBody {
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot'
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot' | 'gemini-cli'
}
interface InstallResult {
@ -17,6 +17,7 @@ const BINARY_MAP: Record<string, string> = {
'codex-cli': 'codex',
'opencode': 'opencode',
'copilot': 'copilot',
'gemini-cli': 'gemini',
}
function checkBinary(binary: string): boolean {
@ -65,6 +66,11 @@ function getInstallInfo(agent: string): { command: string; docsUrl: string } {
: 'See documentation',
docsUrl: 'https://docs.github.com/copilot/how-tos/copilot-cli',
}
case 'gemini-cli':
return {
command: 'npm install -g @anthropic-ai/gemini-cli',
docsUrl: 'https://github.com/anthropics/gemini-cli',
}
default:
return { command: '', docsUrl: '' }
}
@ -117,6 +123,8 @@ async function tryAutoInstall(agent: string, binary: string): Promise<InstallRes
return tryOpenCodeInstall(binary)
case 'copilot':
return tryCopilotInstall(binary)
case 'gemini-cli':
return tryNpmInstall('@anthropic-ai/gemini-cli', binary)
default:
return { success: false, error: 'Unknown agent' }
}

Some files were not shown because too many files have changed in this diff Show more