V0.5.0 (#67)
* 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>
30
.githooks/pre-commit
Executable 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
|
||||||
22
.github/workflows/build-electron.yml
vendored
|
|
@ -58,7 +58,7 @@ jobs:
|
||||||
run: bun run mcp:compile
|
run: bun run mcp:compile
|
||||||
|
|
||||||
- name: Build Electron app
|
- 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:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
|
@ -72,8 +72,8 @@ jobs:
|
||||||
- name: Rename arm64 update metadata
|
- name: Rename arm64 update metadata
|
||||||
if: matrix.platform == 'mac-arm64'
|
if: matrix.platform == 'mac-arm64'
|
||||||
run: |
|
run: |
|
||||||
if [ -f dist-electron/latest-mac.yml ]; then
|
if [ -f out/release/latest-mac.yml ]; then
|
||||||
mv dist-electron/latest-mac.yml dist-electron/latest-mac-arm64.yml
|
mv out/release/latest-mac.yml out/release/latest-mac-arm64.yml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
|
|
@ -81,14 +81,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: electron-${{ matrix.platform }}
|
name: electron-${{ matrix.platform }}
|
||||||
path: |
|
path: |
|
||||||
dist-electron/*.dmg
|
out/release/*.dmg
|
||||||
dist-electron/*.zip
|
out/release/*.zip
|
||||||
dist-electron/*.exe
|
out/release/*.exe
|
||||||
dist-electron/*.AppImage
|
out/release/*.AppImage
|
||||||
dist-electron/*.deb
|
out/release/*.deb
|
||||||
dist-electron/latest*.yml
|
out/release/latest*.yml
|
||||||
dist-electron/*.blockmap
|
out/release/*.blockmap
|
||||||
!dist-electron/builder-debug.yml
|
!out/release/builder-debug.yml
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
|
|
||||||
2
.github/workflows/ci.yml
vendored
|
|
@ -51,5 +51,5 @@ jobs:
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: web-build
|
name: web-build
|
||||||
path: .output/
|
path: out/web/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
|
||||||
3
.github/workflows/docker.yml
vendored
|
|
@ -36,6 +36,9 @@ jobs:
|
||||||
- variant: copilot
|
- variant: copilot
|
||||||
target: with-copilot
|
target: with-copilot
|
||||||
suffix: '-copilot'
|
suffix: '-copilot'
|
||||||
|
- variant: gemini
|
||||||
|
target: with-gemini
|
||||||
|
suffix: '-gemini'
|
||||||
- variant: full
|
- variant: full
|
||||||
target: full
|
target: full
|
||||||
suffix: '-full'
|
suffix: '-full'
|
||||||
|
|
|
||||||
10
.gitignore
vendored
|
|
@ -1,7 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
*.local
|
||||||
count.txt
|
count.txt
|
||||||
.env
|
.env
|
||||||
|
|
@ -12,7 +10,11 @@ count.txt
|
||||||
.vinxi
|
.vinxi
|
||||||
__unconfig*
|
__unconfig*
|
||||||
todos.json
|
todos.json
|
||||||
|
.openpencil-tmp/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
electron-dist/
|
electron-dist/
|
||||||
dist-electron/
|
dist-electron/
|
||||||
.openpencil-tmp/
|
|
||||||
scripts/figma-*
|
|
||||||
|
|
|
||||||
339
CLAUDE.md
|
|
@ -1,6 +1,7 @@
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
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
|
## 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`
|
- **Run a single test:** `bun --bun vitest run path/to/test.ts`
|
||||||
- **Type check:** `npx tsc --noEmit`
|
- **Type check:** `npx tsc --noEmit`
|
||||||
- **Install dependencies:** `bun install`
|
- **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 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)
|
- **Electron build:** `bun run electron:build` (full web build + compile + electron-builder package)
|
||||||
|
|
||||||
## Architecture
|
## 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).
|
**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)
|
└── 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
|
### 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)
|
- **`$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
|
- `resolveNodeForCanvas()` resolves `$refs` on-the-fly before CanvasKit rendering
|
||||||
- Code generators output `var(--name)` for `$ref` values
|
- Code generators output `var(--name)` for `$ref` values
|
||||||
- Multiple theme axes supported (e.g. Theme-1 with Light/Dark, Theme-2 with Compact/Comfortable)
|
- 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
|
### 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.
|
- **Single-shot**: `batch_design` or `insert_node` — one call
|
||||||
|
- **Layered**: `design_skeleton` → `design_content` × N → `design_refine` — phased generation with focused context
|
||||||
**Layered** (new): Break generation into phases, each with focused context and per-section post-processing:
|
- **Segmented prompts**: `get_design_prompt(section=...)` loads focused subsets (schema, layout, roles, icons, etc.)
|
||||||
|
|
||||||
```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
|
|
||||||
|
|
||||||
### Path Aliases
|
### 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
|
### 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`.
|
Tailwind CSS v4 imported via `apps/web/src/styles.css`. UI primitives from shadcn/ui. Icons from `lucide-react`.
|
||||||
|
|
||||||
### 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`.
|
|
||||||
|
|
||||||
### CI / CD
|
### CI / CD
|
||||||
|
|
||||||
- **`.github/workflows/ci.yml`** — Push/PR: type check (`tsc --noEmit`), tests (`vitest`), web build
|
- **`.github/workflows/ci.yml`** — Push/PR: type check, tests, 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/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
|
## Code Style
|
||||||
|
|
||||||
|
|
@ -357,34 +115,13 @@ Tailwind CSS v4 imported via `src/styles.css`. UI primitives from shadcn/ui (`sr
|
||||||
|
|
||||||
## Git Commit Convention
|
## Git Commit Convention
|
||||||
|
|
||||||
Use [Conventional Commits](https://www.conventionalcommits.org/) format:
|
Use [Conventional Commits](https://www.conventionalcommits.org/) format: `<type>(<scope>): <subject>`
|
||||||
|
|
||||||
```
|
**Types:** `feat`, `fix`, `refactor`, `perf`, `style`, `docs`, `test`, `chore`
|
||||||
<type>(<scope>): <subject>
|
|
||||||
|
|
||||||
<body>
|
**Scopes:** `editor`, `canvas`, `panels`, `history`, `ai`, `codegen`, `store`, `types`, `variables`, `figma`, `mcp`, `electron`, `renderer`, `sdk`
|
||||||
```
|
|
||||||
|
|
||||||
### Type
|
**Rules:** Subject in English, lowercase start, no period, imperative mood. Body is optional; explain **why** not what. One commit per change.
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
11
Dockerfile
|
|
@ -58,12 +58,21 @@ ENV NODE_ENV=production NITRO_HOST=0.0.0.0 NITRO_PORT=3000
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["bun", "run", "./.output/server/index.mjs"]
|
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 ──
|
# ── Full: all CLI tools ──
|
||||||
FROM oven/bun:1 AS full
|
FROM oven/bun:1 AS full
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/.output ./.output
|
COPY --from=builder /app/.output ./.output
|
||||||
COPY --from=builder /app/package.json ./
|
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
|
ENV NODE_ENV=production NITRO_HOST=0.0.0.0 NITRO_PORT=3000
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["bun", "run", "./.output/server/index.mjs"]
|
CMD ["bun", "run", "./.output/server/index.mjs"]
|
||||||
|
|
|
||||||
70
README.de.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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
|
> **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:
|
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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | Alle CLI-Tools |
|
| `openpencil-full:latest` | ~1 GB | Alle CLI-Tools |
|
||||||
|
|
||||||
**Ausführen (nur Web):**
|
**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
|
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
|
```bash
|
||||||
# Schritt 1 — Login (einmalig)
|
# Schritt 1 — Login (einmalig)
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | In den Agenteneinstellungen verbinden (`Cmd+,`) |
|
| **Codex CLI** | In den Agenteneinstellungen verbinden (`Cmd+,`) |
|
||||||
| **OpenCode** | In den Agenteneinstellungen verbinden (`Cmd+,`) |
|
| **OpenCode** | In den Agenteneinstellungen verbinden (`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login` dann 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.
|
**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 |
|
| **Frontend** | React 19 · TanStack Start · Tailwind CSS v4 · shadcn/ui |
|
||||||
| **Canvas** | CanvasKit/Skia (WASM, GPU-beschleunigt) |
|
| **Canvas** | CanvasKit/Skia (WASM, GPU-beschleunigt) |
|
||||||
| **Zustand** | Zustand v5 |
|
| **State** | Zustand v5 |
|
||||||
| **Server** | Nitro |
|
| **Server** | Nitro |
|
||||||
| **Desktop** | Electron 35 |
|
| **Desktop** | Electron 35 |
|
||||||
| **KI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
| **KI** | Anthropic SDK · Claude Agent SDK · OpenCode SDK · Copilot SDK |
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## Projektstruktur
|
## Projektstruktur
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia-Engine — Zeichnen, Synchronisierung, Layout, Hilfslinien, Stiftwerkzeug
|
├── apps/
|
||||||
components/ React-UI — Editor, Panels, gemeinsame Dialoge, Icons
|
│ ├── web/ TanStack Start Web-App
|
||||||
services/ai/ KI-Chat, Orchestrierer, Designgenerierung, Streaming
|
│ │ ├── src/
|
||||||
services/figma/ Figma-.fig-Binär-Importpipeline
|
│ │ │ ├── canvas/ CanvasKit/Skia-Engine — Zeichnen, Sync, Layout
|
||||||
services/codegen React+Tailwind- und HTML+CSS-Codegeneratoren
|
│ │ │ ├── components/ React-UI — Editor, Panels, gemeinsame Dialoge, Icons
|
||||||
stores/ Zustand — Canvas, Dokument, Seiten, Verlauf, KI, Einstellungen
|
│ │ │ ├── services/ai/ KI-Chat, Orchestrierer, Designgenerierung, Streaming
|
||||||
variables/ Design-Token-Auflösung und Referenzverwaltung
|
│ │ │ ├── stores/ Zustand — Canvas, Dokument, Seiten, Verlauf, KI
|
||||||
mcp/ MCP-Server-Tools für externe CLI-Integration
|
│ │ │ ├── mcp/ MCP-Server-Tools für externe CLI-Integration
|
||||||
uikit/ Wiederverwendbares Komponenten-Kit-System
|
│ │ │ ├── hooks/ Tastaturkürzel, Datei-Drop, Figma-Paste
|
||||||
server/
|
│ │ │ └── uikit/ Wiederverwendbares Komponenten-Kit-System
|
||||||
api/ai/ Nitro-API — Streaming-Chat, Generierung, Validierung
|
│ │ └── server/
|
||||||
utils/ Claude CLI, OpenCode, Codex, Copilot-Client-Wrapper
|
│ │ ├── api/ai/ Nitro-API — Streaming-Chat, Generierung, Validierung
|
||||||
electron/
|
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot-Wrapper
|
||||||
main.ts Fenster, Nitro-Fork, natives Menü, Auto-Updater
|
│ └── desktop/ Electron-Desktop-App
|
||||||
preload.ts IPC-Brücke
|
│ ├── 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
|
## Tastaturkürzel
|
||||||
|
|
@ -266,6 +278,7 @@ bun --bun run dev # Entwicklungsserver (Port 3000)
|
||||||
bun --bun run build # Produktions-Build
|
bun --bun run build # Produktions-Build
|
||||||
bun --bun run test # Tests ausführen (Vitest)
|
bun --bun run test # Tests ausführen (Vitest)
|
||||||
npx tsc --noEmit # Typprüfung
|
npx tsc --noEmit # Typprüfung
|
||||||
|
bun run bump <version> # Version über alle package.json synchronisieren
|
||||||
bun run electron:dev # Electron-Entwicklung
|
bun run electron:dev # Electron-Entwicklung
|
||||||
bun run electron:build # Electron-Paketierung
|
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.
|
Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetails und Code-Stil.
|
||||||
|
|
||||||
1. Forken und klonen
|
1. Forken und klonen
|
||||||
2. Branch erstellen: `git checkout -b feat/my-feature`
|
2. Versionssynchronisierung einrichten: `git config core.hooksPath .githooks`
|
||||||
3. Prüfungen ausführen: `npx tsc --noEmit && bun --bun run test`
|
3. Branch erstellen: `git checkout -b feat/my-feature`
|
||||||
4. Mit [Conventional Commits](https://www.conventionalcommits.org/) committen: `feat(canvas): add rotation snapping`
|
4. Prüfungen ausführen: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Pull Request gegen `main` öffnen
|
5. Mit [Conventional Commits](https://www.conventionalcommits.org/) committen: `feat(canvas): add rotation snapping`
|
||||||
|
6. Pull Request gegen `main` öffnen
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|
@ -290,6 +304,7 @@ Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetail
|
||||||
- [x] Figma-`.fig`-Import
|
- [x] Figma-`.fig`-Import
|
||||||
- [x] Boolesche Operationen (Vereinigung, Subtraktion, Schnittmenge)
|
- [x] Boolesche Operationen (Vereinigung, Subtraktion, Schnittmenge)
|
||||||
- [x] Multi-Modell-Fähigkeitsprofile
|
- [x] Multi-Modell-Fähigkeitsprofile
|
||||||
|
- [x] Monorepo-Umstrukturierung mit wiederverwendbaren Paketen
|
||||||
- [ ] Kollaboratives Bearbeiten
|
- [ ] Kollaboratives Bearbeiten
|
||||||
- [ ] Plugin-System
|
- [ ] Plugin-System
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ Beiträge sind willkommen! Siehe [CLAUDE.md](./CLAUDE.md) für Architekturdetail
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Unserem Discord beitreten</strong>
|
||||||
</a>
|
</a>
|
||||||
— Fragen stellen, Designs teilen, Funktionen vorschlagen.
|
— Fragen stellen, Designs teilen, Funktionen vorschlagen.
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
78
README.es.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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
|
> **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:
|
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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | Todas las herramientas CLI |
|
| `openpencil-full:latest` | ~1 GB | Todas las herramientas CLI |
|
||||||
|
|
||||||
**Ejecutar (solo web):**
|
**Ejecutar (solo web):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | Conectar en Configuración de Agente (`Cmd+,`) |
|
| **Codex CLI** | Conectar en Configuración de Agente (`Cmd+,`) |
|
||||||
| **OpenCode** | 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+,`) |
|
| **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.
|
**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
|
## Estructura del Proyecto
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ Motor CanvasKit/Skia — dibujo, sincronización, diseño, guías, herramienta pluma
|
├── apps/
|
||||||
components/ Interfaz React — editor, paneles, diálogos compartidos, iconos
|
│ ├── web/ Aplicación web TanStack Start
|
||||||
services/ai/ Chat de IA, orquestador, generación de diseño, transmisión
|
│ │ ├── src/
|
||||||
services/figma/ Pipeline de importación binaria de Figma .fig
|
│ │ │ ├── canvas/ Motor CanvasKit/Skia — dibujo, sincronización, diseño
|
||||||
services/codegen Generadores de código React+Tailwind y HTML+CSS
|
│ │ │ ├── components/ Interfaz React — editor, paneles, diálogos compartidos, iconos
|
||||||
stores/ Zustand — lienzo, documento, páginas, historial, IA, configuración
|
│ │ │ ├── services/ai/ Chat de IA, orquestador, generación de diseño, transmisión
|
||||||
variables/ Resolución de tokens de diseño y gestión de referencias
|
│ │ │ ├── stores/ Zustand — lienzo, documento, páginas, historial, IA
|
||||||
mcp/ Herramientas del servidor MCP para integración con CLI externas
|
│ │ │ ├── mcp/ Herramientas del servidor MCP para integración con CLI externas
|
||||||
uikit/ Sistema de kit de componentes reutilizables
|
│ │ │ ├── hooks/ Atajos de teclado, soltar archivos, pegado de Figma
|
||||||
server/
|
│ │ │ └── uikit/ Sistema de kit de componentes reutilizables
|
||||||
api/ai/ API Nitro — chat en streaming, generación, validación
|
│ │ └── server/
|
||||||
utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
|
│ │ ├── api/ai/ API Nitro — chat en streaming, generación, validación
|
||||||
electron/
|
│ │ └── utils/ Wrappers de Claude CLI, OpenCode, Codex, Copilot
|
||||||
main.ts Ventana, fork Nitro, menú nativo, actualizador automático
|
│ └── desktop/ Aplicación de escritorio Electron
|
||||||
preload.ts Puente IPC
|
│ ├── 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
|
## 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 build # Compilación de producción
|
||||||
bun --bun run test # Ejecutar pruebas (Vitest)
|
bun --bun run test # Ejecutar pruebas (Vitest)
|
||||||
npx tsc --noEmit # Verificación de tipos
|
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:dev # Desarrollo con Electron
|
||||||
bun run electron:build # Empaquetado de 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.
|
¡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
|
1. Haz fork y clona el repositorio
|
||||||
2. Crea una rama: `git checkout -b feat/my-feature`
|
2. Configura la sincronización de versión: `git config core.hooksPath .githooks`
|
||||||
3. Ejecuta las verificaciones: `npx tsc --noEmit && bun --bun run test`
|
3. Crea una rama: `git checkout -b feat/my-feature`
|
||||||
4. Haz commit con [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
4. Ejecuta las verificaciones: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Abre un PR contra `main`
|
5. Haz commit con [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
||||||
|
6. Abre un PR contra `main`
|
||||||
|
|
||||||
## Hoja de Ruta
|
## Hoja de Ruta
|
||||||
|
|
||||||
|
|
@ -290,6 +304,7 @@ bun run electron:build # Empaquetado de Electron
|
||||||
- [x] Importación de Figma `.fig`
|
- [x] Importación de Figma `.fig`
|
||||||
- [x] Operaciones booleanas (unión, sustracción, intersección)
|
- [x] Operaciones booleanas (unión, sustracción, intersección)
|
||||||
- [x] Perfiles de capacidad multimodelo
|
- [x] Perfiles de capacidad multimodelo
|
||||||
|
- [x] Reestructuración en monorepo con paquetes reutilizables
|
||||||
- [ ] Edición colaborativa
|
- [ ] Edición colaborativa
|
||||||
- [ ] Sistema de plugins
|
- [ ] 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" />
|
<img src="https://contrib.rocks/image?repo=ZSeven-W/openpencil" alt="Contributors" />
|
||||||
</a>
|
</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
|
## Star History
|
||||||
|
|
||||||
|
|
@ -310,12 +332,6 @@ bun run electron:build # Empaquetado de Electron
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Comunidad
|
## Licencia
|
||||||
|
|
||||||
<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.
|
|
||||||
|
|
||||||
|
|
||||||
|
[MIT](./LICENSE) — Copyright (c) 2026 ZSeven-W
|
||||||
|
|
|
||||||
80
README.fr.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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
|
> **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 :
|
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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 Go | Tous les outils CLI |
|
| `openpencil-full:latest` | ~1 Go | Tous les outils CLI |
|
||||||
|
|
||||||
**Exécuter (web uniquement) :**
|
**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+,`) |
|
| **Codex CLI** | Connecter dans les Paramètres de l'agent (`Cmd+,`) |
|
||||||
| **OpenCode** | 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+,`) |
|
| **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.
|
**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**
|
||||||
- Serveur MCP intégré — installation en un clic dans les CLI Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot
|
- 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
|
- 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é
|
- **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.)
|
- **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
|
## Structure du projet
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ Moteur CanvasKit/Skia — dessin, sync, mise en page, guides, outil plume
|
├── apps/
|
||||||
components/ Interface React — éditeur, panneaux, boîtes de dialogue partagées, icônes
|
│ ├── web/ Application web TanStack Start
|
||||||
services/ai/ Chat IA, orchestrateur, génération de design, streaming
|
│ │ ├── src/
|
||||||
services/figma/ Pipeline d'import binaire Figma .fig
|
│ │ │ ├── canvas/ Moteur CanvasKit/Skia — dessin, sync, mise en page
|
||||||
services/codegen Générateurs de code React+Tailwind et HTML+CSS
|
│ │ │ ├── components/ Interface React — éditeur, panneaux, boîtes de dialogue partagées, icônes
|
||||||
stores/ Zustand — canevas, document, pages, historique, IA, paramètres
|
│ │ │ ├── services/ai/ Chat IA, orchestrateur, génération de design, streaming
|
||||||
variables/ Résolution des tokens de design et gestion des références
|
│ │ │ ├── stores/ Zustand — canevas, document, pages, historique, IA
|
||||||
mcp/ Outils serveur MCP pour l'intégration CLI externe
|
│ │ │ ├── mcp/ Outils serveur MCP pour l'intégration CLI externe
|
||||||
uikit/ Système de kits de composants réutilisables
|
│ │ │ ├── hooks/ Raccourcis clavier, dépôt de fichiers, collage Figma
|
||||||
server/
|
│ │ │ └── uikit/ Système de kits de composants réutilisables
|
||||||
api/ai/ API Nitro — chat en streaming, génération, validation
|
│ │ └── server/
|
||||||
utils/ Enveloppes client Claude CLI, OpenCode, Codex, Copilot
|
│ │ ├── api/ai/ API Nitro — chat en streaming, génération, validation
|
||||||
electron/
|
│ │ └── utils/ Enveloppes Claude CLI, OpenCode, Codex, Copilot
|
||||||
main.ts Fenêtre, fork Nitro, menu natif, mise à jour automatique
|
│ └── desktop/ Application de bureau Electron
|
||||||
preload.ts Pont IPC
|
│ ├── 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
|
## 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 build # Build de production
|
||||||
bun --bun run test # Lancer les tests (Vitest)
|
bun --bun run test # Lancer les tests (Vitest)
|
||||||
npx tsc --noEmit # Vérification des types
|
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:dev # Développement Electron
|
||||||
bun run electron:build # Packaging 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.
|
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
|
1. Forker et cloner
|
||||||
2. Créer une branche : `git checkout -b feat/my-feature`
|
2. Configurer la synchronisation de version : `git config core.hooksPath .githooks`
|
||||||
3. Exécuter les vérifications : `npx tsc --noEmit && bun --bun run test`
|
3. Créer une branche : `git checkout -b feat/my-feature`
|
||||||
4. Commiter avec [Conventional Commits](https://www.conventionalcommits.org/) : `feat(canvas): add rotation snapping`
|
4. Exécuter les vérifications : `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Ouvrir une PR contre `main`
|
5. Commiter avec [Conventional Commits](https://www.conventionalcommits.org/) : `feat(canvas): add rotation snapping`
|
||||||
|
6. Ouvrir une PR contre `main`
|
||||||
|
|
||||||
## Feuille de route
|
## Feuille de route
|
||||||
|
|
||||||
|
|
@ -290,6 +304,7 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
|
||||||
- [x] Import Figma `.fig`
|
- [x] Import Figma `.fig`
|
||||||
- [x] Opérations booléennes (union, soustraction, intersection)
|
- [x] Opérations booléennes (union, soustraction, intersection)
|
||||||
- [x] Profils de capacités multi-modèles
|
- [x] Profils de capacités multi-modèles
|
||||||
|
- [x] Restructuration en monorepo avec packages réutilisables
|
||||||
- [ ] Édition collaborative
|
- [ ] Édition collaborative
|
||||||
- [ ] Système de plugins
|
- [ ] 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" />
|
<img src="https://contrib.rocks/image?repo=ZSeven-W/openpencil" alt="Contributors" />
|
||||||
</a>
|
</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
|
## Star History
|
||||||
|
|
||||||
|
|
@ -310,12 +332,6 @@ Les contributions sont les bienvenues ! Consultez [CLAUDE.md](./CLAUDE.md) pour
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Communauté
|
## Licence
|
||||||
|
|
||||||
<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.
|
|
||||||
|
|
||||||
|
|
||||||
|
[MIT](./LICENSE) — Copyright (c) 2026 ZSeven-W
|
||||||
|
|
|
||||||
62
README.hi.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<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
|
> **पूर्वापेक्षाएँ:** [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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | सभी CLI टूल |
|
| `openpencil-full:latest` | ~1 GB | सभी CLI टूल |
|
||||||
|
|
||||||
**चलाएँ (केवल वेब):**
|
**चलाएँ (केवल वेब):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
|
| **Codex CLI** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
|
||||||
| **OpenCode** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
|
| **OpenCode** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login` फिर एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
|
| **GitHub Copilot** | `copilot login` फिर एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
|
||||||
|
| **Gemini CLI** | एजेंट सेटिंग्स में कनेक्ट करें (`Cmd+,`) |
|
||||||
|
|
||||||
**मॉडल क्षमता प्रोफ़ाइल** — प्रत्येक मॉडल टियर के अनुसार प्रॉम्प्ट, थिंकिंग मोड और टाइमआउट को स्वचालित रूप से अनुकूलित करता है। फुल-टियर मॉडल (Claude) को पूर्ण प्रॉम्प्ट मिलते हैं; स्टैंडर्ड-टियर (GPT-4o, Gemini, DeepSeek) में थिंकिंग अक्षम होती है; बेसिक-टियर (MiniMax, Qwen, Llama, Mistral) को अधिकतम विश्वसनीयता के लिए सरलीकृत नेस्टेड-JSON प्रॉम्प्ट मिलते हैं।
|
**मॉडल क्षमता प्रोफ़ाइल** — प्रत्येक मॉडल टियर के अनुसार प्रॉम्प्ट, थिंकिंग मोड और टाइमआउट को स्वचालित रूप से अनुकूलित करता है। फुल-टियर मॉडल (Claude) को पूर्ण प्रॉम्प्ट मिलते हैं; स्टैंडर्ड-टियर (GPT-4o, Gemini, DeepSeek) में थिंकिंग अक्षम होती है; बेसिक-टियर (MiniMax, Qwen, Llama, Mistral) को अधिकतम विश्वसनीयता के लिए सरलीकृत नेस्टेड-JSON प्रॉम्प्ट मिलते हैं।
|
||||||
|
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## प्रोजेक्ट संरचना
|
## प्रोजेक्ट संरचना
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia इंजन — ड्रॉइंग, सिंक, लेआउट, गाइड, पेन टूल
|
├── apps/
|
||||||
components/ React UI — एडिटर, पैनल, शेयर्ड डायलॉग, आइकन
|
│ ├── web/ TanStack Start वेब ऐप
|
||||||
services/ai/ AI चैट, ऑर्केस्ट्रेटर, डिज़ाइन जनरेशन, स्ट्रीमिंग
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig बाइनरी इम्पोर्ट पाइपलाइन
|
│ │ │ ├── canvas/ CanvasKit/Skia इंजन — ड्रॉइंग, सिंक, लेआउट
|
||||||
services/codegen React+Tailwind और HTML+CSS कोड जनरेटर
|
│ │ │ ├── components/ React UI — एडिटर, पैनल, शेयर्ड डायलॉग, आइकन
|
||||||
stores/ Zustand — कैनवास, दस्तावेज़, पेज, हिस्ट्री, AI, सेटिंग्स
|
│ │ │ ├── services/ai/ AI चैट, ऑर्केस्ट्रेटर, डिज़ाइन जनरेशन, स्ट्रीमिंग
|
||||||
variables/ डिज़ाइन टोकन रिज़ॉल्यूशन और रेफ़रेंस मैनेजमेंट
|
│ │ │ ├── stores/ Zustand — कैनवास, दस्तावेज़, पेज, हिस्ट्री, AI
|
||||||
mcp/ बाहरी CLI इंटीग्रेशन के लिए MCP सर्वर टूल
|
│ │ │ ├── mcp/ बाहरी CLI इंटीग्रेशन के लिए MCP सर्वर टूल
|
||||||
uikit/ पुन: उपयोगी कम्पोनेंट किट सिस्टम
|
│ │ │ ├── hooks/ कीबोर्ड शॉर्टकट, फ़ाइल ड्रॉप, Figma पेस्ट
|
||||||
server/
|
│ │ │ └── uikit/ पुन: उपयोगी कम्पोनेंट किट सिस्टम
|
||||||
api/ai/ Nitro API — स्ट्रीमिंग चैट, जनरेशन, वैलिडेशन
|
│ │ └── server/
|
||||||
utils/ Claude CLI, OpenCode, Codex, Copilot क्लाइंट रैपर
|
│ │ ├── api/ai/ Nitro API — स्ट्रीमिंग चैट, जनरेशन, वैलिडेशन
|
||||||
electron/
|
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot रैपर
|
||||||
main.ts विंडो, Nitro फ़ोर्क, नेटिव मेनू, ऑटो-अपडेटर
|
│ └── desktop/ Electron डेस्कटॉप ऐप
|
||||||
preload.ts IPC ब्रिज
|
│ ├── 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 build # प्रोडक्शन बिल्ड
|
||||||
bun --bun run test # टेस्ट चलाएँ (Vitest)
|
bun --bun run test # टेस्ट चलाएँ (Vitest)
|
||||||
npx tsc --noEmit # टाइप चेक
|
npx tsc --noEmit # टाइप चेक
|
||||||
|
bun run bump <version> # सभी package.json में वर्शन सिंक करें
|
||||||
bun run electron:dev # Electron डेव
|
bun run electron:dev # Electron डेव
|
||||||
bun run electron:build # Electron पैकेज
|
bun run electron:build # Electron पैकेज
|
||||||
```
|
```
|
||||||
|
|
@ -275,10 +288,11 @@ bun run electron:build # Electron पैकेज
|
||||||
योगदान का स्वागत है! आर्किटेक्चर विवरण और कोड स्टाइल के लिए [CLAUDE.md](./CLAUDE.md) देखें।
|
योगदान का स्वागत है! आर्किटेक्चर विवरण और कोड स्टाइल के लिए [CLAUDE.md](./CLAUDE.md) देखें।
|
||||||
|
|
||||||
1. फ़ोर्क और क्लोन करें
|
1. फ़ोर्क और क्लोन करें
|
||||||
2. ब्रांच बनाएँ: `git checkout -b feat/my-feature`
|
2. वर्शन सिंक सेटअप करें: `git config core.hooksPath .githooks`
|
||||||
3. चेक चलाएँ: `npx tsc --noEmit && bun --bun run test`
|
3. ब्रांच बनाएँ: `git checkout -b feat/my-feature`
|
||||||
4. [Conventional Commits](https://www.conventionalcommits.org/) के साथ कमिट करें: `feat(canvas): add rotation snapping`
|
4. चेक चलाएँ: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. `main` के विरुद्ध PR खोलें
|
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] Figma `.fig` इम्पोर्ट
|
||||||
- [x] बूलियन ऑपरेशन (यूनियन, सबट्रैक्ट, इंटरसेक्ट)
|
- [x] बूलियन ऑपरेशन (यूनियन, सबट्रैक्ट, इंटरसेक्ट)
|
||||||
- [x] मल्टी-मॉडल क्षमता प्रोफ़ाइल
|
- [x] मल्टी-मॉडल क्षमता प्रोफ़ाइल
|
||||||
|
- [x] पुन: उपयोगी पैकेज के साथ मोनोरेपो पुनर्गठन
|
||||||
- [ ] सहयोगी संपादन
|
- [ ] सहयोगी संपादन
|
||||||
- [ ] प्लगइन सिस्टम
|
- [ ] प्लगइन सिस्टम
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ bun run electron:build # Electron पैकेज
|
||||||
## समुदाय
|
## समुदाय
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> हमारे Discord में शामिल हों</strong>
|
||||||
</a>
|
</a>
|
||||||
— प्रश्न पूछें, डिज़ाइन साझा करें, सुविधाएँ सुझाएँ।
|
— प्रश्न पूछें, डिज़ाइन साझा करें, सुविधाएँ सुझाएँ।
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
64
README.id.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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
|
> **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:
|
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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | Semua alat CLI |
|
| `openpencil-full:latest` | ~1 GB | Semua alat CLI |
|
||||||
|
|
||||||
**Jalankan (hanya web):**
|
**Jalankan (hanya web):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | Hubungkan di Pengaturan Agen (`Cmd+,`) |
|
| **Codex CLI** | Hubungkan di Pengaturan Agen (`Cmd+,`) |
|
||||||
| **OpenCode** | Hubungkan di Pengaturan Agen (`Cmd+,`) |
|
| **OpenCode** | Hubungkan di Pengaturan Agen (`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login` lalu 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.
|
**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
|
## Struktur Proyek
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ Mesin CanvasKit/Skia — menggambar, sinkronisasi, tata letak, panduan, alat pen
|
├── apps/
|
||||||
components/ UI React — editor, panel, dialog bersama, ikon
|
│ ├── web/ Aplikasi web TanStack Start
|
||||||
services/ai/ Chat AI, orkestrator, pembuatan desain, streaming
|
│ │ ├── src/
|
||||||
services/figma/ Pipeline impor biner Figma .fig
|
│ │ │ ├── canvas/ Mesin CanvasKit/Skia — menggambar, sinkronisasi, tata letak
|
||||||
services/codegen Generator kode React+Tailwind dan HTML+CSS
|
│ │ │ ├── components/ UI React — editor, panel, dialog bersama, ikon
|
||||||
stores/ Zustand — kanvas, dokumen, halaman, riwayat, AI, pengaturan
|
│ │ │ ├── services/ai/ Chat AI, orkestrator, pembuatan desain, streaming
|
||||||
variables/ Resolusi token desain dan manajemen referensi
|
│ │ │ ├── stores/ Zustand — kanvas, dokumen, halaman, riwayat, AI
|
||||||
mcp/ Alat server MCP untuk integrasi CLI eksternal
|
│ │ │ ├── mcp/ Alat server MCP untuk integrasi CLI eksternal
|
||||||
uikit/ Sistem kit komponen yang dapat digunakan ulang
|
│ │ │ ├── hooks/ Pintasan keyboard, seret file, tempel Figma
|
||||||
server/
|
│ │ │ └── uikit/ Sistem kit komponen yang dapat digunakan ulang
|
||||||
api/ai/ Nitro API — chat streaming, pembuatan, validasi
|
│ │ └── server/
|
||||||
utils/ Pembungkus klien Claude CLI, OpenCode, Codex, Copilot
|
│ │ ├── api/ai/ Nitro API — chat streaming, pembuatan, validasi
|
||||||
electron/
|
│ │ └── utils/ Pembungkus Claude CLI, OpenCode, Codex, Copilot
|
||||||
main.ts Jendela, fork Nitro, menu native, pembaruan otomatis
|
│ └── desktop/ Aplikasi desktop Electron
|
||||||
preload.ts Jembatan IPC
|
│ ├── 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
|
## Pintasan Keyboard
|
||||||
|
|
@ -266,6 +278,7 @@ bun --bun run dev # Server pengembangan (port 3000)
|
||||||
bun --bun run build # Build produksi
|
bun --bun run build # Build produksi
|
||||||
bun --bun run test # Jalankan pengujian (Vitest)
|
bun --bun run test # Jalankan pengujian (Vitest)
|
||||||
npx tsc --noEmit # Pemeriksaan tipe
|
npx tsc --noEmit # Pemeriksaan tipe
|
||||||
|
bun run bump <version> # Sinkronisasi versi di semua package.json
|
||||||
bun run electron:dev # Pengembangan Electron
|
bun run electron:dev # Pengembangan Electron
|
||||||
bun run electron:build # Paket 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.
|
Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitektur dan gaya kode.
|
||||||
|
|
||||||
1. Fork dan clone
|
1. Fork dan clone
|
||||||
2. Buat cabang: `git checkout -b feat/my-feature`
|
2. Atur sinkronisasi versi: `git config core.hooksPath .githooks`
|
||||||
3. Jalankan pemeriksaan: `npx tsc --noEmit && bun --bun run test`
|
3. Buat cabang: `git checkout -b feat/my-feature`
|
||||||
4. Commit dengan [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
4. Jalankan pemeriksaan: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Buka PR ke `main`
|
5. Commit dengan [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
||||||
|
6. Buka PR ke `main`
|
||||||
|
|
||||||
## Peta Jalan
|
## Peta Jalan
|
||||||
|
|
||||||
|
|
@ -290,6 +304,7 @@ Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitekt
|
||||||
- [x] Impor Figma `.fig`
|
- [x] Impor Figma `.fig`
|
||||||
- [x] Operasi boolean (gabung, kurangi, potong)
|
- [x] Operasi boolean (gabung, kurangi, potong)
|
||||||
- [x] Profil kemampuan multi-model
|
- [x] Profil kemampuan multi-model
|
||||||
|
- [x] Restrukturisasi monorepo dengan paket yang dapat digunakan ulang
|
||||||
- [ ] Pengeditan kolaboratif
|
- [ ] Pengeditan kolaboratif
|
||||||
- [ ] Sistem plugin
|
- [ ] Sistem plugin
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ Kontribusi sangat disambut! Lihat [CLAUDE.md](./CLAUDE.md) untuk detail arsitekt
|
||||||
## Komunitas
|
## Komunitas
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Bergabung dengan Discord kami</strong>
|
||||||
</a>
|
</a>
|
||||||
— Ajukan pertanyaan, bagikan desain, sarankan fitur.
|
— Ajukan pertanyaan, bagikan desain, sarankan fitur.
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
64
README.ja.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<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
|
> **前提条件:** [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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | すべての CLI ツール |
|
| `openpencil-full:latest` | ~1 GB | すべての CLI ツール |
|
||||||
|
|
||||||
**実行(Web のみ):**
|
**実行(Web のみ):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | エージェント設定で接続(`Cmd+,`) |
|
| **Codex CLI** | エージェント設定で接続(`Cmd+,`) |
|
||||||
| **OpenCode** | エージェント設定で接続(`Cmd+,`) |
|
| **OpenCode** | エージェント設定で接続(`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login` 後、エージェント設定で接続(`Cmd+,`) |
|
| **GitHub Copilot** | `copilot login` 後、エージェント設定で接続(`Cmd+,`) |
|
||||||
|
| **Gemini CLI** | エージェント設定で接続(`Cmd+,`) |
|
||||||
|
|
||||||
**モデル能力プロファイル** — モデルの階層に応じてプロンプト、シンキングモード、タイムアウトを自動適応。フル階層モデル(Claude)には完全なプロンプト、標準階層(GPT-4o、Gemini、DeepSeek)ではシンキングを無効化、ベーシック階層(MiniMax、Qwen、Llama、Mistral)には最大限の信頼性のために簡略化されたネスト JSON プロンプトを使用。
|
**モデル能力プロファイル** — モデルの階層に応じてプロンプト、シンキングモード、タイムアウトを自動適応。フル階層モデル(Claude)には完全なプロンプト、標準階層(GPT-4o、Gemini、DeepSeek)ではシンキングを無効化、ベーシック階層(MiniMax、Qwen、Llama、Mistral)には最大限の信頼性のために簡略化されたネスト JSON プロンプトを使用。
|
||||||
|
|
||||||
|
|
@ -194,7 +196,7 @@ docker build --target full -t openpencil-full .
|
||||||
|
|
||||||
**デザインシステム**
|
**デザインシステム**
|
||||||
- デザイン変数 — カラー・数値・文字列トークン、`$variable` 参照付き
|
- デザイン変数 — カラー・数値・文字列トークン、`$variable` 参照付き
|
||||||
- マルチテーマサポート — 複数のテーマ軸、各軸に複数バリアント(ライト/ダーク、コンパクト/コンフォータブル)
|
- マルチテーマサポート — 複数のテーマ軸、各軸に複数バリアント(Light/Dark、Compact/Comfortable)
|
||||||
- コンポーネントシステム — インスタンスとオーバーライドを持つ再利用可能なコンポーネント
|
- コンポーネントシステム — インスタンスとオーバーライドを持つ再利用可能なコンポーネント
|
||||||
- CSS 同期 — カスタムプロパティの自動生成、コード出力に `var(--name)` を使用
|
- CSS 同期 — カスタムプロパティの自動生成、コード出力に `var(--name)` を使用
|
||||||
|
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## プロジェクト構成
|
## プロジェクト構成
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia エンジン — 描画、同期、レイアウト、ガイド、ペンツール
|
├── apps/
|
||||||
components/ React UI — エディター、パネル、共有ダイアログ、アイコン
|
│ ├── web/ TanStack Start Web アプリ
|
||||||
services/ai/ AI チャット、オーケストレーター、デザイン生成、ストリーミング
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig バイナリインポートパイプライン
|
│ │ │ ├── canvas/ CanvasKit/Skia エンジン — 描画、同期、レイアウト
|
||||||
services/codegen React+Tailwind および HTML+CSS コードジェネレーター
|
│ │ │ ├── components/ React UI — エディター、パネル、共有ダイアログ、アイコン
|
||||||
stores/ Zustand — キャンバス、ドキュメント、ページ、履歴、AI、設定
|
│ │ │ ├── services/ai/ AI チャット、オーケストレーター、デザイン生成、ストリーミング
|
||||||
variables/ デザイントークンの解決とリファレンス管理
|
│ │ │ ├── stores/ Zustand — キャンバス、ドキュメント、ページ、履歴、AI
|
||||||
mcp/ 外部 CLI 統合用 MCP サーバーツール
|
│ │ │ ├── mcp/ 外部 CLI 統合用 MCP サーバーツール
|
||||||
uikit/ 再利用可能なコンポーネントキットシステム
|
│ │ │ ├── hooks/ キーボードショートカット、ファイルドロップ、Figma ペースト
|
||||||
server/
|
│ │ │ └── uikit/ 再利用可能なコンポーネントキットシステム
|
||||||
api/ai/ Nitro API — ストリーミングチャット、生成、バリデーション
|
│ │ └── server/
|
||||||
utils/ Claude CLI、OpenCode、Codex、Copilot クライアントラッパー
|
│ │ ├── api/ai/ Nitro API — ストリーミングチャット、生成、バリデーション
|
||||||
electron/
|
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot ラッパー
|
||||||
main.ts ウィンドウ、Nitro フォーク、ネイティブメニュー、自動アップデーター
|
│ └── desktop/ Electron デスクトップアプリ
|
||||||
preload.ts IPC ブリッジ
|
│ ├── 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 build # 本番ビルド
|
||||||
bun --bun run test # テストの実行(Vitest)
|
bun --bun run test # テストの実行(Vitest)
|
||||||
npx tsc --noEmit # 型チェック
|
npx tsc --noEmit # 型チェック
|
||||||
|
bun run bump <version> # すべての package.json のバージョンを同期
|
||||||
bun run electron:dev # Electron 開発モード
|
bun run electron:dev # Electron 開発モード
|
||||||
bun run electron:build # Electron パッケージング
|
bun run electron:build # Electron パッケージング
|
||||||
```
|
```
|
||||||
|
|
@ -275,10 +288,11 @@ bun run electron:build # Electron パッケージング
|
||||||
コントリビューションを歓迎します!アーキテクチャの詳細とコードスタイルについては [CLAUDE.md](./CLAUDE.md) をご覧ください。
|
コントリビューションを歓迎します!アーキテクチャの詳細とコードスタイルについては [CLAUDE.md](./CLAUDE.md) をご覧ください。
|
||||||
|
|
||||||
1. フォークしてクローン
|
1. フォークしてクローン
|
||||||
2. ブランチを作成:`git checkout -b feat/my-feature`
|
2. バージョン同期を設定:`git config core.hooksPath .githooks`
|
||||||
3. チェックを実行:`npx tsc --noEmit && bun --bun run test`
|
3. ブランチを作成:`git checkout -b feat/my-feature`
|
||||||
4. [Conventional Commits](https://www.conventionalcommits.org/) 形式でコミット:`feat(canvas): add rotation snapping`
|
4. チェックを実行:`npx tsc --noEmit && bun --bun run test`
|
||||||
5. `main` ブランチに PR を作成
|
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] Figma `.fig` インポート
|
||||||
- [x] ブーリアン演算(合体、型抜き、交差)
|
- [x] ブーリアン演算(合体、型抜き、交差)
|
||||||
- [x] マルチモデル能力プロファイル
|
- [x] マルチモデル能力プロファイル
|
||||||
|
- [x] 再利用可能なパッケージによるモノレポ構成
|
||||||
- [ ] 共同編集
|
- [ ] 共同編集
|
||||||
- [ ] プラグインシステム
|
- [ ] プラグインシステム
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ bun run electron:build # Electron パッケージング
|
||||||
## コミュニティ
|
## コミュニティ
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Discord に参加する</strong>
|
||||||
</a>
|
</a>
|
||||||
— 質問、デザインの共有、機能のリクエストはこちら。
|
— 質問、デザインの共有、機能のリクエストはこちら。
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
86
README.ko.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<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
|
> **필수 조건:** [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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | 모든 CLI 도구 |
|
| `openpencil-full:latest` | ~1 GB | 모든 CLI 도구 |
|
||||||
|
|
||||||
**실행 (웹만):**
|
**실행 (웹만):**
|
||||||
|
|
@ -155,8 +156,8 @@ docker build --target full -t openpencil-full .
|
||||||
|
|
||||||
**프롬프트에서 UI로**
|
**프롬프트에서 UI로**
|
||||||
- **텍스트-투-디자인** — 페이지를 설명하면 스트리밍 애니메이션으로 실시간으로 캔버스에 생성
|
- **텍스트-투-디자인** — 페이지를 설명하면 스트리밍 애니메이션으로 실시간으로 캔버스에 생성
|
||||||
- **오케스트레이터** — 복잡한 페이지를 공간적 서브태스크로 분해하여 병렬 생성 지원
|
- **오케스트레이터** — 복잡한 페이지를 공간적 서브태스크로 분해하여 병렬 생성
|
||||||
- **디자인 수정** — 요소를 선택하고 자연어로 변경 내용을 설명
|
- **디자인 수정** — 요소를 선택하고 자연어로 변경 사항을 설명
|
||||||
- **비전 입력** — 스크린샷이나 목업을 참조로 첨부하여 디자인
|
- **비전 입력** — 스크린샷이나 목업을 참조로 첨부하여 디자인
|
||||||
|
|
||||||
**멀티 에이전트 지원**
|
**멀티 에이전트 지원**
|
||||||
|
|
@ -167,15 +168,16 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | 에이전트 설정에서 연결 (`Cmd+,`) |
|
| **Codex CLI** | 에이전트 설정에서 연결 (`Cmd+,`) |
|
||||||
| **OpenCode** | 에이전트 설정에서 연결 (`Cmd+,`) |
|
| **OpenCode** | 에이전트 설정에서 연결 (`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login` 후 에이전트 설정에서 연결 (`Cmd+,`) |
|
| **GitHub Copilot** | `copilot login` 후 에이전트 설정에서 연결 (`Cmd+,`) |
|
||||||
|
| **Gemini CLI** | 에이전트 설정에서 연결 (`Cmd+,`) |
|
||||||
|
|
||||||
**모델 역량 프로파일** — 모델 티어에 따라 프롬프트, 사고 모드, 타임아웃을 자동 조정합니다. 풀 티어 모델(Claude)은 완전한 프롬프트를 받고, 스탠다드 티어(GPT-4o, Gemini, DeepSeek)는 사고 모드를 비활성화하며, 베이직 티어(MiniMax, Qwen, Llama, Mistral)는 최대 안정성을 위해 단순화된 중첩 JSON 프롬프트를 받습니다.
|
**모델 역량 프로파일** — 모델 티어에 따라 프롬프트, 사고 모드, 타임아웃을 자동 조정합니다. 풀 티어 모델(Claude)은 완전한 프롬프트를 받고, 스탠다드 티어(GPT-4o, Gemini, DeepSeek)는 사고 모드를 비활성화하며, 베이직 티어(MiniMax, Qwen, Llama, Mistral)는 최대 안정성을 위해 단순화된 중첩 JSON 프롬프트를 받습니다.
|
||||||
|
|
||||||
**MCP 서버**
|
**MCP 서버**
|
||||||
- 내장 MCP 서버 — Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLI에 원클릭 설치
|
- 내장 MCP 서버 — Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLI에 원클릭 설치
|
||||||
- Node.js 자동 감지 — 설치되지 않은 경우 HTTP 전송 모드로 자동 대체하고 MCP HTTP 서버를 자동 시작
|
- Node.js 자동 감지 — 설치되지 않은 경우 HTTP 전송 모드로 자동 대체하고 MCP HTTP 서버를 자동 시작
|
||||||
- 터미널에서 디자인 자동화: MCP 호환 에이전트를 통해 `.op` 파일 읽기, 생성, 편집 가능
|
- 터미널에서 디자인 자동화: MCP 호환 에이전트를 통해 `.op` 파일 읽기, 생성, 편집
|
||||||
- **계층적 디자인 워크플로** — `design_skeleton` → `design_content` → `design_refine`으로 더 높은 충실도의 멀티 섹션 디자인
|
- **계층적 디자인 워크플로** — `design_skeleton` → `design_content` → `design_refine`으로 더 높은 충실도의 멀티 섹션 디자인
|
||||||
- **세그먼트 프롬프트 검색** — 필요한 디자인 지식만 로드 (스키마, 레이아웃, 역할, 아이콘, 계획 등)
|
- **세그먼트 프롬프트 검색** — 필요한 디자인 지식만 로드 (schema, layout, roles, icons, planning 등)
|
||||||
- 멀티 페이지 지원 — MCP 도구를 통해 페이지 생성, 이름 변경, 순서 변경, 복제
|
- 멀티 페이지 지원 — 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)
|
- 아이콘 피커(Iconify)와 이미지 가져오기(PNG/JPEG/SVG/WebP/GIF)
|
||||||
- 오토 레이아웃 — 수직/수평 방향, 갭·패딩·justify·align 지원
|
- 오토 레이아웃 — 수직/수평 방향, gap, padding, justify, align 지원
|
||||||
- 탭 내비게이션이 있는 멀티 페이지 문서
|
- 탭 내비게이션이 있는 멀티 페이지 문서
|
||||||
|
|
||||||
**디자인 시스템**
|
**디자인 시스템**
|
||||||
- 디자인 변수 — 컬러·숫자·문자열 토큰, `$variable` 참조 지원
|
- 디자인 변수 — 컬러, 숫자, 문자열 토큰, `$variable` 참조 지원
|
||||||
- 멀티 테마 지원 — 여러 테마 축, 각 축에 여러 변형(라이트/다크, 컴팩트/컴포터블)
|
- 멀티 테마 지원 — 여러 테마 축, 각 축에 변형(Light/Dark, Compact/Comfortable)
|
||||||
- 컴포넌트 시스템 — 인스턴스와 오버라이드를 가진 재사용 가능한 컴포넌트
|
- 컴포넌트 시스템 — 인스턴스와 오버라이드를 가진 재사용 가능한 컴포넌트
|
||||||
- CSS 동기화 — 커스텀 프로퍼티 자동 생성, 코드 출력에 `var(--name)` 사용
|
- CSS 동기화 — 커스텀 프로퍼티 자동 생성, 코드 출력에 `var(--name)` 사용
|
||||||
|
|
||||||
|
|
@ -202,7 +204,7 @@ docker build --target full -t openpencil-full .
|
||||||
- 레이아웃, 채우기, 선, 효과, 텍스트, 이미지, 벡터를 유지하며 `.fig` 파일 가져오기
|
- 레이아웃, 채우기, 선, 효과, 텍스트, 이미지, 벡터를 유지하며 `.fig` 파일 가져오기
|
||||||
|
|
||||||
**데스크톱 앱**
|
**데스크톱 앱**
|
||||||
- Electron을 통한 네이티브 macOS·Windows·Linux 지원
|
- Electron을 통한 네이티브 macOS, Windows, Linux 지원
|
||||||
- `.op` 파일 연결 — 더블 클릭으로 열기, 단일 인스턴스 잠금
|
- `.op` 파일 연결 — 더블 클릭으로 열기, 단일 인스턴스 잠금
|
||||||
- GitHub Releases에서 자동 업데이트
|
- GitHub Releases에서 자동 업데이트
|
||||||
- 네이티브 애플리케이션 메뉴와 파일 다이얼로그
|
- 네이티브 애플리케이션 메뉴와 파일 다이얼로그
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## 프로젝트 구조
|
## 프로젝트 구조
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia 엔진 — 드로잉, 동기화, 레이아웃, 가이드, 펜 툴
|
├── apps/
|
||||||
components/ React UI — 에디터, 패널, 공유 다이얼로그, 아이콘
|
│ ├── web/ TanStack Start 웹 앱
|
||||||
services/ai/ AI 채팅, 오케스트레이터, 디자인 생성, 스트리밍
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig 바이너리 가져오기 파이프라인
|
│ │ │ ├── canvas/ CanvasKit/Skia 엔진 — 드로잉, 동기화, 레이아웃
|
||||||
services/codegen React+Tailwind 및 HTML+CSS 코드 생성기
|
│ │ │ ├── components/ React UI — 에디터, 패널, 공유 다이얼로그, 아이콘
|
||||||
stores/ Zustand — 캔버스, 문서, 페이지, 히스토리, AI, 설정
|
│ │ │ ├── services/ai/ AI 채팅, 오케스트레이터, 디자인 생성, 스트리밍
|
||||||
variables/ 디자인 토큰 해석 및 참조 관리
|
│ │ │ ├── stores/ Zustand — 캔버스, 문서, 페이지, 히스토리, AI
|
||||||
mcp/ 외부 CLI 통합용 MCP 서버 툴
|
│ │ │ ├── mcp/ 외부 CLI 통합용 MCP 서버 도구
|
||||||
uikit/ 재사용 가능한 컴포넌트 킷 시스템
|
│ │ │ ├── hooks/ 키보드 단축키, 파일 드롭, Figma 붙여넣기
|
||||||
server/
|
│ │ │ └── uikit/ 재사용 가능한 컴포넌트 킷 시스템
|
||||||
api/ai/ Nitro API — 스트리밍 채팅, 생성, 유효성 검사
|
│ │ └── server/
|
||||||
utils/ Claude CLI, OpenCode, Codex, Copilot 클라이언트 래퍼
|
│ │ ├── api/ai/ Nitro API — 스트리밍 채팅, 생성, 유효성 검사
|
||||||
electron/
|
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot 래퍼
|
||||||
main.ts 윈도우, Nitro 포크, 네이티브 메뉴, 자동 업데이터
|
│ └── desktop/ Electron 데스크톱 앱
|
||||||
preload.ts IPC 브리지
|
│ ├── 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 build # 프로덕션 빌드
|
||||||
bun --bun run test # 테스트 실행 (Vitest)
|
bun --bun run test # 테스트 실행 (Vitest)
|
||||||
npx tsc --noEmit # 타입 검사
|
npx tsc --noEmit # 타입 검사
|
||||||
|
bun run bump <version> # 모든 package.json에 버전 동기화
|
||||||
bun run electron:dev # Electron 개발 모드
|
bun run electron:dev # Electron 개발 모드
|
||||||
bun run electron:build # Electron 패키징
|
bun run electron:build # Electron 패키징
|
||||||
```
|
```
|
||||||
|
|
@ -275,21 +288,23 @@ bun run electron:build # Electron 패키징
|
||||||
기여를 환영합니다! 아키텍처 세부 정보와 코드 스타일은 [CLAUDE.md](./CLAUDE.md)를 참고하세요.
|
기여를 환영합니다! 아키텍처 세부 정보와 코드 스타일은 [CLAUDE.md](./CLAUDE.md)를 참고하세요.
|
||||||
|
|
||||||
1. 포크 후 클론
|
1. 포크 후 클론
|
||||||
2. 브랜치 생성: `git checkout -b feat/my-feature`
|
2. 버전 동기화 설정: `git config core.hooksPath .githooks`
|
||||||
3. 검사 실행: `npx tsc --noEmit && bun --bun run test`
|
3. 브랜치 생성: `git checkout -b feat/my-feature`
|
||||||
4. [Conventional Commits](https://www.conventionalcommits.org/) 형식으로 커밋: `feat(canvas): add rotation snapping`
|
4. 검사 실행: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. `main` 브랜치에 PR 생성
|
5. [Conventional Commits](https://www.conventionalcommits.org/) 형식으로 커밋: `feat(canvas): add rotation snapping`
|
||||||
|
6. `main` 브랜치에 PR 생성
|
||||||
|
|
||||||
## 로드맵
|
## 로드맵
|
||||||
|
|
||||||
- [x] CSS 동기화가 있는 디자인 변수 & 토큰
|
- [x] CSS 동기화가 있는 디자인 변수 & 토큰
|
||||||
- [x] 컴포넌트 시스템(인스턴스 & 오버라이드)
|
- [x] 컴포넌트 시스템 (인스턴스 & 오버라이드)
|
||||||
- [x] 오케스트레이터를 통한 AI 디자인 생성
|
- [x] 오케스트레이터를 통한 AI 디자인 생성
|
||||||
- [x] 계층적 디자인 워크플로가 포함된 MCP 서버 통합
|
- [x] 계층적 디자인 워크플로가 포함된 MCP 서버 통합
|
||||||
- [x] 멀티 페이지 지원
|
- [x] 멀티 페이지 지원
|
||||||
- [x] Figma `.fig` 가져오기
|
- [x] Figma `.fig` 가져오기
|
||||||
- [x] 불리언 연산(결합, 빼기, 교차)
|
- [x] 불리언 연산 (합치기, 빼기, 교차)
|
||||||
- [x] 멀티 모델 역량 프로파일
|
- [x] 멀티 모델 역량 프로파일
|
||||||
|
- [x] 재사용 가능한 패키지를 포함한 모노레포 구조 변경
|
||||||
- [ ] 공동 편집
|
- [ ] 공동 편집
|
||||||
- [ ] 플러그인 시스템
|
- [ ] 플러그인 시스템
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ bun run electron:build # Electron 패키징
|
||||||
## 커뮤니티
|
## 커뮤니티
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Discord에 참여하기</strong>
|
||||||
</a>
|
</a>
|
||||||
— 질문하기, 디자인 공유, 기능 제안.
|
— 질문하기, 디자인 공유, 기능 제안.
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
59
README.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | All CLI tools |
|
| `openpencil-full:latest` | ~1 GB | All CLI tools |
|
||||||
|
|
||||||
**Run (web only):**
|
**Run (web only):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | Connect in Agent Settings (`Cmd+,`) |
|
| **Codex CLI** | Connect in Agent Settings (`Cmd+,`) |
|
||||||
| **OpenCode** | Connect in Agent Settings (`Cmd+,`) |
|
| **OpenCode** | Connect in Agent Settings (`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login` then 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.
|
**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
|
## Project Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia engine — drawing, sync, layout, guides, pen tool
|
├── apps/
|
||||||
components/ React UI — editor, panels, shared dialogs, icons
|
│ ├── web/ TanStack Start web app
|
||||||
services/ai/ AI chat, orchestrator, design generation, streaming
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig binary import pipeline
|
│ │ │ ├── canvas/ CanvasKit/Skia engine — drawing, sync, layout
|
||||||
services/codegen Multi-platform code generators (React, HTML, Vue, Svelte, Flutter, SwiftUI, Compose, React Native)
|
│ │ │ ├── components/ React UI — editor, panels, shared dialogs, icons
|
||||||
stores/ Zustand — canvas, document, pages, history, AI, settings
|
│ │ │ ├── services/ai/ AI chat, orchestrator, design generation, streaming
|
||||||
variables/ Design token resolution and reference management
|
│ │ │ ├── stores/ Zustand — canvas, document, pages, history, AI
|
||||||
mcp/ MCP server tools for external CLI integration
|
│ │ │ ├── mcp/ MCP server tools for external CLI integration
|
||||||
uikit/ Reusable component kit system
|
│ │ │ ├── hooks/ Keyboard shortcuts, file drop, Figma paste
|
||||||
server/
|
│ │ │ └── uikit/ Reusable component kit system
|
||||||
api/ai/ Nitro API — streaming chat, generation, validation
|
│ │ └── server/
|
||||||
utils/ Claude CLI, OpenCode, Codex, Copilot client wrappers
|
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
|
||||||
electron/
|
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
|
||||||
main.ts Window, Nitro fork, native menu, auto-updater
|
│ └── desktop/ Electron desktop app
|
||||||
preload.ts IPC bridge
|
│ ├── 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
|
## Keyboard Shortcuts
|
||||||
|
|
@ -266,6 +278,7 @@ bun --bun run dev # Dev server (port 3000)
|
||||||
bun --bun run build # Production build
|
bun --bun run build # Production build
|
||||||
bun --bun run test # Run tests (Vitest)
|
bun --bun run test # Run tests (Vitest)
|
||||||
npx tsc --noEmit # Type check
|
npx tsc --noEmit # Type check
|
||||||
|
bun run bump <version> # Sync version across all package.json
|
||||||
bun run electron:dev # Electron dev
|
bun run electron:dev # Electron dev
|
||||||
bun run electron:build # Electron package
|
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.
|
Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details and code style.
|
||||||
|
|
||||||
1. Fork and clone
|
1. Fork and clone
|
||||||
2. Create a branch: `git checkout -b feat/my-feature`
|
2. Set up version sync: `git config core.hooksPath .githooks`
|
||||||
3. Run checks: `npx tsc --noEmit && bun --bun run test`
|
3. Create a branch: `git checkout -b feat/my-feature`
|
||||||
4. Commit with [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
4. Run checks: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Open a PR against `main`
|
5. Commit with [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
||||||
|
6. Open a PR against `main`
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|
@ -290,6 +304,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
|
||||||
- [x] Figma `.fig` import
|
- [x] Figma `.fig` import
|
||||||
- [x] Boolean operations (union, subtract, intersect)
|
- [x] Boolean operations (union, subtract, intersect)
|
||||||
- [x] Multi-model capability profiles
|
- [x] Multi-model capability profiles
|
||||||
|
- [x] Monorepo restructure with reusable packages
|
||||||
- [ ] Collaborative editing
|
- [ ] Collaborative editing
|
||||||
- [ ] Plugin system
|
- [ ] Plugin system
|
||||||
|
|
||||||
|
|
@ -302,7 +317,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Join our Discord</strong>
|
||||||
</a>
|
</a>
|
||||||
— Ask questions, share designs, suggest features.
|
— Ask questions, share designs, suggest features.
|
||||||
|
|
|
||||||
64
README.pt.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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
|
> **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:
|
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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | Todas as ferramentas CLI |
|
| `openpencil-full:latest` | ~1 GB | Todas as ferramentas CLI |
|
||||||
|
|
||||||
**Executar (apenas web):**
|
**Executar (apenas web):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | Conectar nas Configurações do Agente (`Cmd+,`) |
|
| **Codex CLI** | Conectar nas Configurações do Agente (`Cmd+,`) |
|
||||||
| **OpenCode** | 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+,`) |
|
| **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.
|
**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
|
## Estrutura do Projeto
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ Motor CanvasKit/Skia — desenho, sincronização, layout, guias, ferramenta caneta
|
├── apps/
|
||||||
components/ UI React — editor, painéis, diálogos compartilhados, ícones
|
│ ├── web/ Aplicação web TanStack Start
|
||||||
services/ai/ Chat IA, orquestrador, geração de design, streaming
|
│ │ ├── src/
|
||||||
services/figma/ Pipeline de importação binária do Figma .fig
|
│ │ │ ├── canvas/ Motor CanvasKit/Skia — desenho, sincronização, layout
|
||||||
services/codegen Geradores de código React+Tailwind e HTML+CSS
|
│ │ │ ├── components/ UI React — editor, painéis, diálogos compartilhados, ícones
|
||||||
stores/ Zustand — canvas, documento, páginas, histórico, IA, configurações
|
│ │ │ ├── services/ai/ Chat IA, orquestrador, geração de design, streaming
|
||||||
variables/ Resolução de tokens de design e gerenciamento de referências
|
│ │ │ ├── stores/ Zustand — canvas, documento, páginas, histórico, IA
|
||||||
mcp/ Ferramentas do servidor MCP para integração com CLI externo
|
│ │ │ ├── mcp/ Ferramentas do servidor MCP para integração com CLI externo
|
||||||
uikit/ Sistema de kit de componentes reutilizáveis
|
│ │ │ ├── hooks/ Atalhos de teclado, soltar arquivos, colar do Figma
|
||||||
server/
|
│ │ │ └── uikit/ Sistema de kit de componentes reutilizáveis
|
||||||
api/ai/ API Nitro — chat em streaming, geração, validação
|
│ │ └── server/
|
||||||
utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
|
│ │ ├── api/ai/ API Nitro — chat em streaming, geração, validação
|
||||||
electron/
|
│ │ └── utils/ Wrappers de cliente Claude CLI, OpenCode, Codex, Copilot
|
||||||
main.ts Janela, fork do Nitro, menu nativo, atualizador automático
|
│ └── desktop/ Aplicativo desktop Electron
|
||||||
preload.ts Ponte IPC
|
│ ├── 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
|
## 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 build # Build de produção
|
||||||
bun --bun run test # Executar testes (Vitest)
|
bun --bun run test # Executar testes (Vitest)
|
||||||
npx tsc --noEmit # Verificação de tipos
|
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:dev # Desenvolvimento com Electron
|
||||||
bun run electron:build # Empacotamento do 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.
|
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
|
1. Faça fork e clone
|
||||||
2. Crie uma branch: `git checkout -b feat/my-feature`
|
2. Configure a sincronização de versão: `git config core.hooksPath .githooks`
|
||||||
3. Execute as verificações: `npx tsc --noEmit && bun --bun run test`
|
3. Crie uma branch: `git checkout -b feat/my-feature`
|
||||||
4. Faça commit com [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
4. Execute as verificações: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Abra um PR contra `main`
|
5. Faça commit com [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
||||||
|
6. Abra um PR contra `main`
|
||||||
|
|
||||||
## Roadmap
|
## 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] Importação do Figma `.fig`
|
||||||
- [x] Operações booleanas (união, subtração, interseção)
|
- [x] Operações booleanas (união, subtração, interseção)
|
||||||
- [x] Perfis de capacidade multi-modelo
|
- [x] Perfis de capacidade multi-modelo
|
||||||
|
- [x] Reestruturação em monorepo com pacotes reutilizáveis
|
||||||
- [ ] Edição colaborativa
|
- [ ] Edição colaborativa
|
||||||
- [ ] Sistema de plugins
|
- [ ] Sistema de plugins
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ Contribuições são bem-vindas! Consulte o [CLAUDE.md](./CLAUDE.md) para detalh
|
||||||
## Comunidade
|
## Comunidade
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Entre no nosso Discord</strong>
|
||||||
</a>
|
</a>
|
||||||
— Faça perguntas, compartilhe designs, sugira funcionalidades.
|
— Faça perguntas, compartilhe designs, sugira funcionalidades.
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
64
README.ru.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
@ -102,7 +102,7 @@ bun run electron:dev
|
||||||
|
|
||||||
> **Требования:** [Bun](https://bun.sh/) >= 1.0 и [Node.js](https://nodejs.org/) >= 18
|
> **Требования:** [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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 ГБ | Все CLI-инструменты |
|
| `openpencil-full:latest` | ~1 ГБ | Все CLI-инструменты |
|
||||||
|
|
||||||
**Запуск (только веб):**
|
**Запуск (только веб):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | Подключить в настройках агента (`Cmd+,`) |
|
| **Codex CLI** | Подключить в настройках агента (`Cmd+,`) |
|
||||||
| **OpenCode** | Подключить в настройках агента (`Cmd+,`) |
|
| **OpenCode** | Подключить в настройках агента (`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login`, затем подключить в настройках агента (`Cmd+,`) |
|
| **GitHub Copilot** | `copilot login`, затем подключить в настройках агента (`Cmd+,`) |
|
||||||
|
| **Gemini CLI** | Подключить в настройках агента (`Cmd+,`) |
|
||||||
|
|
||||||
**Профили возможностей моделей** — автоматически адаптирует промпты, режим thinking и таймауты для каждого уровня моделей. Модели полного уровня (Claude) получают полные промпты; стандартного уровня (GPT-4o, Gemini, DeepSeek) — с отключённым thinking; базового уровня (MiniMax, Qwen, Llama, Mistral) — упрощённые промпты с вложенным JSON для максимальной надёжности.
|
**Профили возможностей моделей** — автоматически адаптирует промпты, режим thinking и таймауты для каждого уровня моделей. Модели полного уровня (Claude) получают полные промпты; стандартного уровня (GPT-4o, Gemini, DeepSeek) — с отключённым thinking; базового уровня (MiniMax, Qwen, Llama, Mistral) — упрощённые промпты с вложенным JSON для максимальной надёжности.
|
||||||
|
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ Движок CanvasKit/Skia — рисование, синхронизация, раскладка, направляющие, инструмент пера
|
├── apps/
|
||||||
components/ React UI — редактор, панели, общие диалоги, иконки
|
│ ├── web/ Веб-приложение TanStack Start
|
||||||
services/ai/ AI-чат, оркестратор, генерация дизайна, стриминг
|
│ │ ├── src/
|
||||||
services/figma/ Пайплайн бинарного импорта Figma .fig
|
│ │ │ ├── canvas/ Движок CanvasKit/Skia — рисование, синхронизация, раскладка
|
||||||
services/codegen Генераторы кода React+Tailwind и HTML+CSS
|
│ │ │ ├── components/ React UI — редактор, панели, общие диалоги, иконки
|
||||||
stores/ Zustand — холст, документ, страницы, история, AI, настройки
|
│ │ │ ├── services/ai/ AI-чат, оркестратор, генерация дизайна, стриминг
|
||||||
variables/ Разрешение дизайн-токенов и управление ссылками
|
│ │ │ ├── stores/ Zustand — холст, документ, страницы, история, AI
|
||||||
mcp/ Инструменты MCP-сервера для интеграции с внешними CLI
|
│ │ │ ├── mcp/ Инструменты MCP-сервера для интеграции с внешними CLI
|
||||||
uikit/ Система переиспользуемых наборов компонентов
|
│ │ │ ├── hooks/ Горячие клавиши, перетаскивание файлов, вставка из Figma
|
||||||
server/
|
│ │ │ └── uikit/ Система переиспользуемых наборов компонентов
|
||||||
api/ai/ Nitro API — стриминговый чат, генерация, валидация
|
│ │ └── server/
|
||||||
utils/ Обёртки клиентов Claude CLI, OpenCode, Codex, Copilot
|
│ │ ├── api/ai/ Nitro API — стриминговый чат, генерация, валидация
|
||||||
electron/
|
│ │ └── utils/ Обёртки клиентов Claude CLI, OpenCode, Codex, Copilot
|
||||||
main.ts Окно, форк Nitro, нативное меню, автообновление
|
│ └── desktop/ Десктопное приложение Electron
|
||||||
preload.ts IPC-мост
|
│ ├── 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 build # Сборка для продакшена
|
||||||
bun --bun run test # Запустить тесты (Vitest)
|
bun --bun run test # Запустить тесты (Vitest)
|
||||||
npx tsc --noEmit # Проверка типов
|
npx tsc --noEmit # Проверка типов
|
||||||
|
bun run bump <version> # Синхронизация версий во всех package.json
|
||||||
bun run electron:dev # Разработка Electron
|
bun run electron:dev # Разработка Electron
|
||||||
bun run electron:build # Упаковка Electron
|
bun run electron:build # Упаковка Electron
|
||||||
```
|
```
|
||||||
|
|
@ -275,10 +288,11 @@ bun run electron:build # Упаковка Electron
|
||||||
Мы приветствуем вклад в проект! Подробности об архитектуре и стиле кода смотрите в [CLAUDE.md](./CLAUDE.md).
|
Мы приветствуем вклад в проект! Подробности об архитектуре и стиле кода смотрите в [CLAUDE.md](./CLAUDE.md).
|
||||||
|
|
||||||
1. Сделайте форк и клонируйте репозиторий
|
1. Сделайте форк и клонируйте репозиторий
|
||||||
2. Создайте ветку: `git checkout -b feat/my-feature`
|
2. Настройте синхронизацию версий: `git config core.hooksPath .githooks`
|
||||||
3. Запустите проверки: `npx tsc --noEmit && bun --bun run test`
|
3. Создайте ветку: `git checkout -b feat/my-feature`
|
||||||
4. Сделайте коммит в формате [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
4. Запустите проверки: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Откройте PR в ветку `main`
|
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] Импорт Figma `.fig`
|
||||||
- [x] Булевы операции (объединение, вычитание, пересечение)
|
- [x] Булевы операции (объединение, вычитание, пересечение)
|
||||||
- [x] Мультимодельные профили возможностей
|
- [x] Мультимодельные профили возможностей
|
||||||
|
- [x] Реструктуризация в монорепозиторий с переиспользуемыми пакетами
|
||||||
- [ ] Совместное редактирование
|
- [ ] Совместное редактирование
|
||||||
- [ ] Система плагинов
|
- [ ] Система плагинов
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ bun run electron:build # Упаковка Electron
|
||||||
## Сообщество
|
## Сообщество
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Присоединяйтесь к нашему Discord</strong>
|
||||||
</a>
|
</a>
|
||||||
— Задавайте вопросы, делитесь дизайнами, предлагайте функции.
|
— Задавайте вопросы, делитесь дизайнами, предлагайте функции.
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
68
README.th.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
@ -102,7 +102,7 @@ bun run electron:dev
|
||||||
|
|
||||||
> **ข้อกำหนดเบื้องต้น:** [Bun](https://bun.sh/) >= 1.0 และ [Node.js](https://nodejs.org/) >= 18
|
> **ข้อกำหนดเบื้องต้น:** [Bun](https://bun.sh/) >= 1.0 และ [Node.js](https://nodejs.org/) >= 18
|
||||||
|
|
||||||
### การติดตั้งด้วย Docker
|
### Docker
|
||||||
|
|
||||||
มี image หลายรูปแบบให้เลือก — เลือกแบบที่เหมาะกับความต้องการของคุณ:
|
มี image หลายรูปแบบให้เลือก — เลือกแบบที่เหมาะกับความต้องการของคุณ:
|
||||||
|
|
||||||
|
|
@ -113,6 +113,7 @@ bun run electron:dev
|
||||||
| `openpencil-codex:latest` | — | + Codex CLI |
|
| `openpencil-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | เครื่องมือ CLI ทั้งหมด |
|
| `openpencil-full:latest` | ~1 GB | เครื่องมือ CLI ทั้งหมด |
|
||||||
|
|
||||||
**รัน (เว็บเท่านั้น):**
|
**รัน (เว็บเท่านั้น):**
|
||||||
|
|
@ -167,13 +168,14 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | เชื่อมต่อใน Agent Settings (`Cmd+,`) |
|
| **Codex CLI** | เชื่อมต่อใน Agent Settings (`Cmd+,`) |
|
||||||
| **OpenCode** | เชื่อมต่อใน Agent Settings (`Cmd+,`) |
|
| **OpenCode** | เชื่อมต่อใน Agent Settings (`Cmd+,`) |
|
||||||
| **GitHub Copilot** | `copilot login` จากนั้นเชื่อมต่อใน 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 ที่ย่อลงเพื่อความเสถียรสูงสุด
|
**โปรไฟล์ความสามารถของโมเดล** — ปรับ prompt, โหมด thinking และ timeout ตามระดับโมเดลโดยอัตโนมัติ โมเดลระดับเต็ม (Claude) ได้ prompt ครบถ้วน; โมเดลระดับมาตรฐาน (GPT-4o, Gemini, DeepSeek) ปิด thinking; โมเดลระดับพื้นฐาน (MiniMax, Qwen, Llama, Mistral) ได้ prompt แบบ nested-JSON ที่ย่อลงเพื่อความเสถียรสูงสุด
|
||||||
|
|
||||||
**MCP Server**
|
**MCP Server**
|
||||||
- MCP Server ในตัว — ติดตั้งได้ด้วยคลิกเดียวใน Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLIs
|
- MCP Server ในตัว — ติดตั้งได้ด้วยคลิกเดียวใน Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLIs
|
||||||
- ตรวจจับ Node.js อัตโนมัติ — หากไม่ได้ติดตั้ง จะสำรองไปใช้ HTTP transport โดยอัตโนมัติและเริ่ม MCP HTTP เซิร์ฟเวอร์
|
- ตรวจจับ Node.js อัตโนมัติ — หากไม่ได้ติดตั้ง จะสำรองไปใช้ HTTP transport และเริ่ม MCP HTTP server โดยอัตโนมัติ
|
||||||
- การทำ Design Automation จาก Terminal: อ่าน สร้าง และแก้ไขไฟล์ `.op` ผ่าน agent ที่รองรับ MCP
|
- การทำ Design automation จาก terminal: อ่าน สร้าง และแก้ไขไฟล์ `.op` ผ่าน agent ที่รองรับ MCP
|
||||||
- **Layered design workflow** — `design_skeleton` → `design_content` → `design_refine` สำหรับดีไซน์หลายส่วนที่มีความละเอียดสูงขึ้น
|
- **Layered design workflow** — `design_skeleton` → `design_content` → `design_refine` สำหรับดีไซน์หลายส่วนที่มีความละเอียดสูงขึ้น
|
||||||
- **Segmented prompt retrieval** — โหลดเฉพาะความรู้ด้านดีไซน์ที่ต้องการ (schema, layout, roles, icons, planning ฯลฯ)
|
- **Segmented prompt retrieval** — โหลดเฉพาะความรู้ด้านดีไซน์ที่ต้องการ (schema, layout, roles, icons, planning ฯลฯ)
|
||||||
- รองรับหลายหน้า — สร้าง เปลี่ยนชื่อ เรียงลำดับ และทำซ้ำหน้าผ่าน MCP tools
|
- รองรับหลายหน้า — สร้าง เปลี่ยนชื่อ เรียงลำดับ และทำซ้ำหน้าผ่าน MCP tools
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## โครงสร้างโปรเจกต์
|
## โครงสร้างโปรเจกต์
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia engine — การวาด, sync, layout, guides, pen tool
|
├── apps/
|
||||||
components/ React UI — editor, panels, shared dialogs, icons
|
│ ├── web/ TanStack Start web app
|
||||||
services/ai/ AI chat, orchestrator, การสร้างดีไซน์, streaming
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig binary import pipeline
|
│ │ │ ├── canvas/ CanvasKit/Skia engine — การวาด, sync, layout
|
||||||
services/codegen React+Tailwind และ HTML+CSS code generators
|
│ │ │ ├── components/ React UI — editor, panels, shared dialogs, icons
|
||||||
stores/ Zustand — canvas, document, pages, history, AI, settings
|
│ │ │ ├── services/ai/ AI chat, orchestrator, การสร้างดีไซน์, streaming
|
||||||
variables/ การแก้ไข design token และการจัดการ reference
|
│ │ │ ├── stores/ Zustand — canvas, document, pages, history, AI
|
||||||
mcp/ MCP server tools สำหรับการเชื่อมต่อ CLI ภายนอก
|
│ │ │ ├── mcp/ MCP server tools สำหรับการเชื่อมต่อ CLI ภายนอก
|
||||||
uikit/ ระบบ component kit ที่นำกลับมาใช้ใหม่ได้
|
│ │ │ ├── hooks/ Keyboard shortcuts, file drop, Figma paste
|
||||||
server/
|
│ │ │ └── uikit/ ระบบ component kit ที่นำกลับมาใช้ใหม่ได้
|
||||||
api/ai/ Nitro API — streaming chat, generation, validation
|
│ │ └── server/
|
||||||
utils/ Claude CLI, OpenCode, Codex, Copilot client wrappers
|
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
|
||||||
electron/
|
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
|
||||||
main.ts Window, Nitro fork, native menu, auto-updater
|
│ └── desktop/ Electron desktop app
|
||||||
preload.ts IPC bridge
|
│ ├── 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 build # Production build
|
||||||
bun --bun run test # รันการทดสอบ (Vitest)
|
bun --bun run test # รันการทดสอบ (Vitest)
|
||||||
npx tsc --noEmit # ตรวจสอบ type
|
npx tsc --noEmit # ตรวจสอบ type
|
||||||
|
bun run bump <version> # Sync version ในทุก package.json
|
||||||
bun run electron:dev # Electron dev
|
bun run electron:dev # Electron dev
|
||||||
bun run electron:build # Electron package
|
bun run electron:build # Electron package
|
||||||
```
|
```
|
||||||
|
|
@ -275,10 +288,11 @@ bun run electron:build # Electron package
|
||||||
ยินดีต้อนรับการมีส่วนร่วมทุกรูปแบบ! ดู [CLAUDE.md](./CLAUDE.md) สำหรับรายละเอียดสถาปัตยกรรมและรูปแบบโค้ด
|
ยินดีต้อนรับการมีส่วนร่วมทุกรูปแบบ! ดู [CLAUDE.md](./CLAUDE.md) สำหรับรายละเอียดสถาปัตยกรรมและรูปแบบโค้ด
|
||||||
|
|
||||||
1. Fork และ clone
|
1. Fork และ clone
|
||||||
2. สร้าง branch: `git checkout -b feat/my-feature`
|
2. ตั้งค่า version sync: `git config core.hooksPath .githooks`
|
||||||
3. รันการตรวจสอบ: `npx tsc --noEmit && bun --bun run test`
|
3. สร้าง branch: `git checkout -b feat/my-feature`
|
||||||
4. Commit ด้วย [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
4. รันการตรวจสอบ: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. เปิด PR เข้า `main`
|
5. Commit ด้วย [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
||||||
|
6. เปิด PR เข้า `main`
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|
@ -290,6 +304,7 @@ bun run electron:build # Electron package
|
||||||
- [x] นำเข้า Figma `.fig`
|
- [x] นำเข้า Figma `.fig`
|
||||||
- [x] Boolean operations (union, subtract, intersect)
|
- [x] Boolean operations (union, subtract, intersect)
|
||||||
- [x] โปรไฟล์ความสามารถหลายโมเดล
|
- [x] โปรไฟล์ความสามารถหลายโมเดล
|
||||||
|
- [x] ปรับโครงสร้างเป็น monorepo พร้อม package ที่นำกลับมาใช้ใหม่ได้
|
||||||
- [ ] การแก้ไขร่วมกัน
|
- [ ] การแก้ไขร่วมกัน
|
||||||
- [ ] ระบบปลั๊กอิน
|
- [ ] ระบบปลั๊กอิน
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ bun run electron:build # Electron package
|
||||||
## ชุมชน
|
## ชุมชน
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> เข้าร่วม Discord ของเรา</strong>
|
||||||
</a>
|
</a>
|
||||||
— ถามคำถาม แชร์ดีไซน์ เสนอฟีเจอร์
|
— ถามคำถาม แชร์ดีไซน์ เสนอฟีเจอร์
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
70
README.tr.md
|
|
@ -1,16 +1,16 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>Dunyanin ilk acik kaynakli AI-yerel vektor tasarim araci.</strong><br />
|
<strong>Dünyanın ilk açık kaynaklı AI-yerel vektör tasarım aracı.</strong><br />
|
||||||
<sub>Eszamanli Ajan Ekipleri • Kod Olarak Tasarim • Yerlesik MCP Sunucusu • Coklu Model Zekasi</sub>
|
<sub>Eşzamanlı Ajan Ekipleri • Kod Olarak Tasarım • Yerleşik MCP Sunucusu • Çoklu Model Zekası</sub>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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
|
> **Ö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:
|
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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | Tüm CLI araçları |
|
| `openpencil-full:latest` | ~1 GB | Tüm CLI araçları |
|
||||||
|
|
||||||
**Çalıştır (yalnızca web):**
|
**Ç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+,`) |
|
| **Codex CLI** | Ajan Ayarlarından bağlanın (`Cmd+,`) |
|
||||||
| **OpenCode** | 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+,`) |
|
| **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.
|
**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 |
|
| **Ö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 |
|
| **Durum Yönetimi** | Zustand v5 |
|
||||||
| **Sunucu** | Nitro |
|
| **Sunucu** | Nitro |
|
||||||
| **Masaüstü** | Electron 35 |
|
| **Masaüstü** | Electron 35 |
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## Proje Yapısı
|
## Proje Yapısı
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia motoru — çizim, senkronizasyon, düzen, kılavuzlar, kalem aracı
|
├── apps/
|
||||||
components/ React UI — editör, paneller, paylaşılan iletişim kutuları, simgeler
|
│ ├── web/ TanStack Start web uygulaması
|
||||||
services/ai/ AI sohbet, orkestratör, tasarım üretimi, akış
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig ikili içe aktarma ardışık düzeni
|
│ │ │ ├── canvas/ CanvasKit/Skia motoru — çizim, senkronizasyon, düzen
|
||||||
services/codegen React+Tailwind ve HTML+CSS kod üreticileri
|
│ │ │ ├── components/ React UI — editör, paneller, paylaşılan iletişim kutuları, simgeler
|
||||||
stores/ Zustand — kanvas, belge, sayfalar, geçmiş, AI, ayarlar
|
│ │ │ ├── services/ai/ AI sohbet, orkestratör, tasarım üretimi, akış
|
||||||
variables/ Tasarım token çözümleme ve referans yönetimi
|
│ │ │ ├── stores/ Zustand — kanvas, belge, sayfalar, geçmiş, AI
|
||||||
mcp/ Harici CLI entegrasyonu için MCP sunucu araçları
|
│ │ │ ├── mcp/ Harici CLI entegrasyonu için MCP sunucu araçları
|
||||||
uikit/ Yeniden kullanılabilir bileşen kiti sistemi
|
│ │ │ ├── hooks/ Klavye kısayolları, dosya bırakma, Figma yapıştırma
|
||||||
server/
|
│ │ │ └── uikit/ Yeniden kullanılabilir bileşen kiti sistemi
|
||||||
api/ai/ Nitro API — akış sohbet, üretim, doğrulama
|
│ │ └── server/
|
||||||
utils/ Claude CLI, OpenCode, Codex, Copilot istemci sarmalayıcıları
|
│ │ ├── api/ai/ Nitro API — akış sohbet, üretim, doğrulama
|
||||||
electron/
|
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot sarmalayıcıları
|
||||||
main.ts Pencere, Nitro çatallanması, yerel menü, otomatik güncelleyici
|
│ └── desktop/ Electron masaüstü uygulaması
|
||||||
preload.ts IPC köprüsü
|
│ ├── 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ı
|
## 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 build # Üretim derlemesi
|
||||||
bun --bun run test # Testleri çalıştır (Vitest)
|
bun --bun run test # Testleri çalıştır (Vitest)
|
||||||
npx tsc --noEmit # Tür denetimi
|
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:dev # Electron geliştirme modu
|
||||||
bun run electron:build # Electron paketleme
|
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.
|
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
|
1. Fork'layın ve klonlayın
|
||||||
2. Dal oluşturun: `git checkout -b feat/my-feature`
|
2. Sürüm eşitlemeyi ayarlayın: `git config core.hooksPath .githooks`
|
||||||
3. Kontrolleri çalıştırın: `npx tsc --noEmit && bun --bun run test`
|
3. Dal oluşturun: `git checkout -b feat/my-feature`
|
||||||
4. [Conventional Commits](https://www.conventionalcommits.org/) formatıyla commit yapın: `feat(canvas): add rotation snapping`
|
4. Kontrolleri çalıştırın: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. `main` dalına PR açın
|
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ı
|
## 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] Figma `.fig` içe aktarma
|
||||||
- [x] Boolean işlemler (birleştirme, çıkarma, kesişim)
|
- [x] Boolean işlemler (birleştirme, çıkarma, kesişim)
|
||||||
- [x] Çoklu model yetenek profilleri
|
- [x] Çoklu model yetenek profilleri
|
||||||
|
- [x] Yeniden kullanılabilir paketlerle monorepo yapılandırması
|
||||||
- [ ] Ortak düzenleme
|
- [ ] Ortak düzenleme
|
||||||
- [ ] Eklenti sistemi
|
- [ ] Eklenti sistemi
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ Katkılarınızı bekliyoruz! Mimari ayrıntılar ve kod stili için [CLAUDE.md]
|
||||||
## Topluluk
|
## Topluluk
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Discord'umuza katılın</strong>
|
||||||
</a>
|
</a>
|
||||||
— Soru sorun, tasarımlarınızı paylaşın, özellik önerin.
|
— Soru sorun, tasarımlarınızı paylaşın, özellik önerin.
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
64
README.vi.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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
|
> **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:
|
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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | Tất cả công cụ CLI |
|
| `openpencil-full:latest` | ~1 GB | Tất cả công cụ CLI |
|
||||||
|
|
||||||
**Chạy (chỉ web):**
|
**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+,`) |
|
| **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+,`) |
|
| **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+,`) |
|
| **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.
|
**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
|
## Cấu trúc dự án
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia engine — vẽ, đồng bộ, layout, hướng dẫn, công cụ bút
|
├── apps/
|
||||||
components/ React UI — editor, panels, hộp thoại dùng chung, icons
|
│ ├── web/ Ứng dụng web TanStack Start
|
||||||
services/ai/ AI chat, orchestrator, tạo thiết kế, streaming
|
│ │ ├── src/
|
||||||
services/figma/ Pipeline nhập binary Figma .fig
|
│ │ │ ├── canvas/ Engine CanvasKit/Skia — vẽ, đồng bộ, layout
|
||||||
services/codegen Bộ tạo mã React+Tailwind và HTML+CSS
|
│ │ │ ├── components/ React UI — editor, panels, hộp thoại dùng chung, icons
|
||||||
stores/ Zustand — canvas, document, pages, history, AI, settings
|
│ │ │ ├── services/ai/ AI chat, orchestrator, tạo thiết kế, streaming
|
||||||
variables/ Giải quyết token thiết kế và quản lý tham chiếu
|
│ │ │ ├── stores/ Zustand — canvas, document, pages, history, AI
|
||||||
mcp/ Công cụ máy chủ MCP để tích hợp CLI bên ngoài
|
│ │ │ ├── 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
|
│ │ │ ├── hooks/ Phím tắt, kéo thả tệp, dán từ Figma
|
||||||
server/
|
│ │ │ └── uikit/ Hệ thống kit component có thể tái sử dụng
|
||||||
api/ai/ Nitro API — streaming chat, generation, validation
|
│ │ └── server/
|
||||||
utils/ Claude CLI, OpenCode, Codex, Copilot client wrappers
|
│ │ ├── api/ai/ Nitro API — streaming chat, generation, validation
|
||||||
electron/
|
│ │ └── utils/ Claude CLI, OpenCode, Codex, Copilot wrappers
|
||||||
main.ts Cửa sổ, Nitro fork, menu gốc, auto-updater
|
│ └── desktop/ Ứng dụng desktop Electron
|
||||||
preload.ts IPC bridge
|
│ ├── 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
|
## 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 build # Build production
|
||||||
bun --bun run test # Chạy kiểm thử (Vitest)
|
bun --bun run test # Chạy kiểm thử (Vitest)
|
||||||
npx tsc --noEmit # Kiểm tra kiểu
|
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:dev # Electron dev
|
||||||
bun run electron:build # Đóng gói Electron
|
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ã.
|
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
|
1. Fork và clone
|
||||||
2. Tạo branch: `git checkout -b feat/my-feature`
|
2. Thiết lập đồng bộ phiên bản: `git config core.hooksPath .githooks`
|
||||||
3. Chạy kiểm tra: `npx tsc --noEmit && bun --bun run test`
|
3. Tạo branch: `git checkout -b feat/my-feature`
|
||||||
4. Commit theo [Conventional Commits](https://www.conventionalcommits.org/): `feat(canvas): add rotation snapping`
|
4. Chạy kiểm tra: `npx tsc --noEmit && bun --bun run test`
|
||||||
5. Mở PR vào nhánh `main`
|
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
|
## 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] Nhập Figma `.fig`
|
||||||
- [x] Phép toán Boolean (hợp nhất, trừ, giao)
|
- [x] Phép toán Boolean (hợp nhất, trừ, giao)
|
||||||
- [x] Hồ sơ năng lực đa mô hình
|
- [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
|
- [ ] Chỉnh sửa cộng tác
|
||||||
- [ ] Hệ thống plugin
|
- [ ] 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
|
## Cộng đồng
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> Tham gia Discord của chúng tôi</strong>
|
||||||
</a>
|
</a>
|
||||||
— Đặt câu hỏi, chia sẻ thiết kế, đề xuất tính năng.
|
— Đặt câu hỏi, chia sẻ thiết kế, đề xuất tính năng.
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
@ -102,7 +102,7 @@ bun run electron:dev
|
||||||
|
|
||||||
> **前置條件:** [Bun](https://bun.sh/) >= 1.0 以及 [Node.js](https://nodejs.org/) >= 18
|
> **前置條件:** [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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | 所有 CLI 工具 |
|
| `openpencil-full:latest` | ~1 GB | 所有 CLI 工具 |
|
||||||
|
|
||||||
**執行(僅 Web):**
|
**執行(僅 Web):**
|
||||||
|
|
@ -167,6 +168,7 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | 在 Agent 設定中連接(`Cmd+,`) |
|
| **Codex CLI** | 在 Agent 設定中連接(`Cmd+,`) |
|
||||||
| **OpenCode** | 在 Agent 設定中連接(`Cmd+,`) |
|
| **OpenCode** | 在 Agent 設定中連接(`Cmd+,`) |
|
||||||
| **GitHub Copilot** | 執行 `copilot login` 後在 Agent 設定中連接(`Cmd+,`) |
|
| **GitHub Copilot** | 執行 `copilot login` 後在 Agent 設定中連接(`Cmd+,`) |
|
||||||
|
| **Gemini CLI** | 在 Agent 設定中連接(`Cmd+,`) |
|
||||||
|
|
||||||
**模型能力設定檔** — 自動依據模型層級調整提示詞、思考模式和逾時設定。完整層級模型(Claude)獲得完整提示詞;標準層級(GPT-4o、Gemini、DeepSeek)停用思考模式;基礎層級(MiniMax、Qwen、Llama、Mistral)獲得精簡巢狀 JSON 提示詞,確保最大可靠性。
|
**模型能力設定檔** — 自動依據模型層級調整提示詞、思考模式和逾時設定。完整層級模型(Claude)獲得完整提示詞;標準層級(GPT-4o、Gemini、DeepSeek)停用思考模式;基礎層級(MiniMax、Qwen、Llama、Mistral)獲得精簡巢狀 JSON 提示詞,確保最大可靠性。
|
||||||
|
|
||||||
|
|
@ -223,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## 專案結構
|
## 專案結構
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia 引擎 — 繪圖、同步、版面配置、參考線、鋼筆工具
|
├── apps/
|
||||||
components/ React UI — 編輯器、面板、共用對話框、圖示
|
│ ├── web/ TanStack Start Web 應用程式
|
||||||
services/ai/ AI 聊天、編排器、設計生成、串流處理
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig 二進位檔案匯入管線
|
│ │ │ ├── canvas/ CanvasKit/Skia 引擎 — 繪圖、同步、版面配置
|
||||||
services/codegen React+Tailwind 和 HTML+CSS 程式碼生成器
|
│ │ │ ├── components/ React UI — 編輯器、面板、共用對話框、圖示
|
||||||
stores/ Zustand — 畫布、文件、頁面、歷程、AI、設定
|
│ │ │ ├── services/ai/ AI 聊天、編排器、設計生成、串流處理
|
||||||
variables/ 設計令牌解析與參照管理
|
│ │ │ ├── stores/ Zustand — 畫布、文件、頁面、歷程、AI
|
||||||
mcp/ 供外部 CLI 整合使用的 MCP 伺服器工具
|
│ │ │ ├── mcp/ 供外部 CLI 整合使用的 MCP 伺服器工具
|
||||||
uikit/ 可重複使用元件套件系統
|
│ │ │ ├── hooks/ 鍵盤快捷鍵、檔案拖放、Figma 貼上
|
||||||
server/
|
│ │ │ └── uikit/ 可重複使用元件套件系統
|
||||||
api/ai/ Nitro API — 串流聊天、生成、驗證
|
│ │ └── server/
|
||||||
utils/ Claude CLI、OpenCode、Codex、Copilot 客戶端封裝
|
│ │ ├── api/ai/ Nitro API — 串流聊天、生成、驗證
|
||||||
electron/
|
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客戶端封裝
|
||||||
main.ts 視窗、Nitro 子處理序、原生選單、自動更新
|
│ └── desktop/ Electron 桌面應用程式
|
||||||
preload.ts IPC 橋接
|
│ ├── 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 build # 正式版建置
|
||||||
bun --bun run test # 執行測試(Vitest)
|
bun --bun run test # 執行測試(Vitest)
|
||||||
npx tsc --noEmit # 型別檢查
|
npx tsc --noEmit # 型別檢查
|
||||||
|
bun run bump <version> # 在所有 package.json 間同步版本號
|
||||||
bun run electron:dev # Electron 開發模式
|
bun run electron:dev # Electron 開發模式
|
||||||
bun run electron:build # Electron 封裝
|
bun run electron:build # Electron 封裝
|
||||||
```
|
```
|
||||||
|
|
@ -275,10 +288,11 @@ bun run electron:build # Electron 封裝
|
||||||
歡迎貢獻!請查閱 [CLAUDE.md](./CLAUDE.md) 了解架構細節和程式碼風格。
|
歡迎貢獻!請查閱 [CLAUDE.md](./CLAUDE.md) 了解架構細節和程式碼風格。
|
||||||
|
|
||||||
1. Fork 並複製存放庫
|
1. Fork 並複製存放庫
|
||||||
2. 建立分支:`git checkout -b feat/my-feature`
|
2. 設定版本同步:`git config core.hooksPath .githooks`
|
||||||
3. 執行檢查:`npx tsc --noEmit && bun --bun run test`
|
3. 建立分支:`git checkout -b feat/my-feature`
|
||||||
4. 使用 [Conventional Commits](https://www.conventionalcommits.org/) 提交:`feat(canvas): add rotation snapping`
|
4. 執行檢查:`npx tsc --noEmit && bun --bun run test`
|
||||||
5. 向 `main` 分支發起 PR
|
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] Figma `.fig` 匯入
|
||||||
- [x] 布林運算(聯集、減去、交集)
|
- [x] 布林運算(聯集、減去、交集)
|
||||||
- [x] 多模型能力設定檔
|
- [x] 多模型能力設定檔
|
||||||
|
- [x] Monorepo 重構,支援可重複使用套件
|
||||||
- [ ] 協同編輯
|
- [ ] 協同編輯
|
||||||
- [ ] 外掛程式系統
|
- [ ] 外掛程式系統
|
||||||
|
|
||||||
|
|
@ -302,12 +317,11 @@ bun run electron:build # Electron 封裝
|
||||||
## 社群
|
## 社群
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> 加入我們的 Discord</strong>
|
||||||
</a>
|
</a>
|
||||||
— 提問、分享設計、提出功能建議。
|
— 提問、分享設計、提出功能建議。
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
75
README.zh.md
|
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./electron/icon.png" alt="OpenPencil" width="120" />
|
<img src="./apps/desktop/build/icon.png" alt="OpenPencil" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">OpenPencil</h1>
|
<h1 align="center">OpenPencil</h1>
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
|
|
||||||
### 🧠 多模型智能
|
### 🧠 多模型智能
|
||||||
|
|
||||||
自动适配每个模型的能力。Claude 获得完整提示词和思考模式;GPT-4o/Gemini 关闭思考模式;小模型(MiniMax、通义千问、Llama)使用简化提示词以确保输出可靠性。
|
自动适配每个模型的能力。Claude 获得完整提示词和思考模式;GPT-4o/Gemini 关闭思考模式;小模型(MiniMax、Qwen、Llama)使用简化提示词以确保输出可靠性。
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%">
|
<td width="50%">
|
||||||
|
|
@ -102,9 +102,9 @@ bun run electron:dev
|
||||||
|
|
||||||
> **前置条件:** [Bun](https://bun.sh/) >= 1.0 以及 [Node.js](https://nodejs.org/) >= 18
|
> **前置条件:** [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-codex:latest` | — | + Codex CLI |
|
||||||
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
| `openpencil-opencode:latest` | — | + OpenCode CLI |
|
||||||
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
| `openpencil-copilot:latest` | — | + GitHub Copilot CLI |
|
||||||
|
| `openpencil-gemini:latest` | — | + Gemini CLI |
|
||||||
| `openpencil-full:latest` | ~1 GB | 全部 CLI 工具 |
|
| `openpencil-full:latest` | ~1 GB | 全部 CLI 工具 |
|
||||||
|
|
||||||
**运行(仅 Web):**
|
**运行(仅 Web):**
|
||||||
|
|
@ -123,7 +124,7 @@ docker run -d -p 3000:3000 ghcr.io/zseven-w/openpencil:latest
|
||||||
|
|
||||||
**运行 AI CLI(以 Claude Code 为例):**
|
**运行 AI CLI(以 Claude Code 为例):**
|
||||||
|
|
||||||
AI 聊天依赖 Claude CLI 的 OAuth 登录,使用 Docker volume 持久化登录状态:
|
AI 聊天依赖 Claude CLI 的 OAuth 登录。使用 Docker volume 持久化登录会话:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 第一步 — 登录(仅需一次)
|
# 第一步 — 登录(仅需一次)
|
||||||
|
|
@ -167,8 +168,9 @@ docker build --target full -t openpencil-full .
|
||||||
| **Codex CLI** | 在 Agent 设置中连接(`Cmd+,`) |
|
| **Codex CLI** | 在 Agent 设置中连接(`Cmd+,`) |
|
||||||
| **OpenCode** | 在 Agent 设置中连接(`Cmd+,`) |
|
| **OpenCode** | 在 Agent 设置中连接(`Cmd+,`) |
|
||||||
| **GitHub Copilot** | 运行 `copilot login` 后在 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 服务器**
|
||||||
- 内置 MCP 服务器 — 一键安装到 Claude Code / Codex / Gemini / OpenCode / Kiro / Copilot CLI
|
- 内置 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
|
- React + Tailwind CSS、HTML + CSS、CSS Variables
|
||||||
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
|
- Vue、Svelte、Flutter、SwiftUI、Jetpack Compose、React Native
|
||||||
- 从设计令牌生成 CSS Variables
|
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
|
|
@ -224,22 +225,32 @@ docker build --target full -t openpencil-full .
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
openpencil/
|
||||||
canvas/ CanvasKit/Skia 引擎 — 绘图、同步、布局、参考线、钢笔工具
|
├── apps/
|
||||||
components/ React UI — 编辑器、面板、共享对话框、图标
|
│ ├── web/ TanStack Start Web 应用
|
||||||
services/ai/ AI 聊天、编排器、设计生成、流式处理
|
│ │ ├── src/
|
||||||
services/figma/ Figma .fig 二进制文件导入流水线
|
│ │ │ ├── canvas/ CanvasKit/Skia 引擎 — 绘图、同步、布局
|
||||||
services/codegen 多平台代码生成器(React、HTML、Vue、Svelte、Flutter、SwiftUI、Compose、React Native)
|
│ │ │ ├── components/ React UI — 编辑器、面板、共享对话框、图标
|
||||||
stores/ Zustand — 画布、文档、页面、历史、AI、设置
|
│ │ │ ├── services/ai/ AI 聊天、编排器、设计生成、流式处理
|
||||||
variables/ 设计令牌解析与引用管理
|
│ │ │ ├── stores/ Zustand — 画布、文档、页面、历史、AI
|
||||||
mcp/ 供外部 CLI 集成使用的 MCP 服务器工具
|
│ │ │ ├── mcp/ 供外部 CLI 集成使用的 MCP 服务器工具
|
||||||
uikit/ 可复用组件套件系统
|
│ │ │ ├── hooks/ 键盘快捷键、文件拖放、Figma 粘贴
|
||||||
server/
|
│ │ │ └── uikit/ 可复用组件套件系统
|
||||||
api/ai/ Nitro API — 流式聊天、生成、验证
|
│ │ └── server/
|
||||||
utils/ Claude CLI、OpenCode、Codex、Copilot 客户端封装
|
│ │ ├── api/ai/ Nitro API — 流式聊天、生成、验证
|
||||||
electron/
|
│ │ └── utils/ Claude CLI、OpenCode、Codex、Copilot 客户端封装
|
||||||
main.ts 窗口、Nitro 子进程、原生菜单、自动更新
|
│ └── desktop/ Electron 桌面应用
|
||||||
preload.ts IPC 桥接
|
│ ├── 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 build # 生产构建
|
||||||
bun --bun run test # 运行测试(Vitest)
|
bun --bun run test # 运行测试(Vitest)
|
||||||
npx tsc --noEmit # 类型检查
|
npx tsc --noEmit # 类型检查
|
||||||
|
bun run bump <version> # 同步所有 package.json 的版本号
|
||||||
bun run electron:dev # Electron 开发模式
|
bun run electron:dev # Electron 开发模式
|
||||||
bun run electron:build # Electron 打包
|
bun run electron:build # Electron 打包
|
||||||
```
|
```
|
||||||
|
|
@ -276,10 +288,11 @@ bun run electron:build # Electron 打包
|
||||||
欢迎贡献!请查阅 [CLAUDE.md](./CLAUDE.md) 了解架构细节和代码风格。
|
欢迎贡献!请查阅 [CLAUDE.md](./CLAUDE.md) 了解架构细节和代码风格。
|
||||||
|
|
||||||
1. Fork 并克隆仓库
|
1. Fork 并克隆仓库
|
||||||
2. 创建分支:`git checkout -b feat/my-feature`
|
2. 设置版本同步:`git config core.hooksPath .githooks`
|
||||||
3. 运行检查:`npx tsc --noEmit && bun --bun run test`
|
3. 创建分支:`git checkout -b feat/my-feature`
|
||||||
4. 使用 [Conventional Commits](https://www.conventionalcommits.org/) 提交:`feat(canvas): add rotation snapping`
|
4. 运行检查:`npx tsc --noEmit && bun --bun run test`
|
||||||
5. 向 `main` 分支发起 PR
|
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] Figma `.fig` 导入
|
||||||
- [x] 布尔运算(合并、减去、相交)
|
- [x] 布尔运算(合并、减去、相交)
|
||||||
- [x] 多模型能力配置
|
- [x] 多模型能力配置
|
||||||
|
- [x] Monorepo 重构与可复用包
|
||||||
- [ ] 协同编辑
|
- [ ] 协同编辑
|
||||||
- [ ] 插件系统
|
- [ ] 插件系统
|
||||||
|
|
||||||
|
|
@ -303,16 +317,11 @@ bun run electron:build # Electron 打包
|
||||||
## 社区
|
## 社区
|
||||||
|
|
||||||
<a href="https://discord.gg/h9Fmyy6pVh">
|
<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>
|
<strong> 加入我们的 Discord</strong>
|
||||||
</a>
|
</a>
|
||||||
— 提问、分享设计、提出功能建议。
|
— 提问、分享设计、提出功能建议。
|
||||||
|
|
||||||
**飞书交流群**
|
|
||||||
|
|
||||||
<img src="./screenshot/557517811-62010928-d91a-4223-bc10-9ee7a4fbf043.jpg" alt="飞书交流群" width="240" />
|
|
||||||
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
<a href="https://star-history.com/#ZSeven-W/openpencil&Date">
|
||||||
|
|
|
||||||
37
apps/desktop/CLAUDE.md
Normal 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.
|
||||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 544 KiB |
|
|
@ -11,7 +11,8 @@ import { spawn, execSync, type ChildProcess } from 'node:child_process'
|
||||||
import { build } from 'esbuild'
|
import { build } from 'esbuild'
|
||||||
import { join } from 'node:path'
|
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
|
const VITE_DEV_PORT = 3000
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -42,7 +43,7 @@ async function compileElectron(): Promise<void> {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
external: ['electron'],
|
external: ['electron'],
|
||||||
target: 'node20',
|
target: 'node20',
|
||||||
outdir: join(ROOT, 'electron-dist'),
|
outdir: join(ROOT, 'out', 'desktop'),
|
||||||
outExtension: { '.js': '.cjs' },
|
outExtension: { '.js': '.cjs' },
|
||||||
format: 'cjs' as const,
|
format: 'cjs' as const,
|
||||||
}
|
}
|
||||||
|
|
@ -50,11 +51,11 @@ async function compileElectron(): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
build({
|
build({
|
||||||
...common,
|
...common,
|
||||||
entryPoints: [join(ROOT, 'electron', 'main.ts')],
|
entryPoints: [join(DESKTOP_DIR, 'main.ts')],
|
||||||
}),
|
}),
|
||||||
build({
|
build({
|
||||||
...common,
|
...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
|
// Ensure cleanup on exit
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (process.platform === 'win32' && vite.pid) {
|
if (process.platform === 'win32' && vite.pid) {
|
||||||
// SIGTERM is unreliable on Windows; use taskkill for proper tree-kill
|
|
||||||
try {
|
try {
|
||||||
execSync(`taskkill /pid ${vite.pid} /T /F`, { stdio: 'ignore' })
|
execSync(`taskkill /pid ${vite.pid} /T /F`, { stdio: 'ignore' })
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
@ -102,10 +102,11 @@ async function main(): Promise<void> {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
target: 'node20',
|
target: 'node20',
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
entryPoints: [join(ROOT, 'src', 'mcp', 'server.ts')],
|
entryPoints: [join(ROOT, 'apps', 'web', 'src', 'mcp', 'server.ts')],
|
||||||
outfile: join(ROOT, 'dist', 'mcp-server.cjs'),
|
outfile: join(ROOT, 'out', 'mcp-server.cjs'),
|
||||||
alias: { '@': join(ROOT, 'src') },
|
alias: { '@': join(ROOT, 'apps', 'web', 'src') },
|
||||||
define: { 'import.meta.env': '{}' },
|
define: { 'import.meta.env': '{}' },
|
||||||
|
external: ['canvas', 'paper'],
|
||||||
})
|
})
|
||||||
console.log('[electron-dev] MCP server compiled')
|
console.log('[electron-dev] MCP server compiled')
|
||||||
|
|
||||||
|
|
@ -114,7 +115,7 @@ async function main(): Promise<void> {
|
||||||
// 4. Launch Electron
|
// 4. Launch Electron
|
||||||
console.log('[electron-dev] Starting Electron...')
|
console.log('[electron-dev] Starting Electron...')
|
||||||
const electronBin = join(ROOT, 'node_modules', '.bin', '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,
|
cwd: ROOT,
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: { ...process.env },
|
env: { ...process.env },
|
||||||
|
|
@ -3,24 +3,27 @@ productName: OpenPencil
|
||||||
copyright: Copyright (c) 2024-2026 OpenPencil contributors
|
copyright: Copyright (c) 2024-2026 OpenPencil contributors
|
||||||
|
|
||||||
directories:
|
directories:
|
||||||
output: dist-electron
|
output: out/release
|
||||||
buildResources: build
|
buildResources: apps/desktop/build
|
||||||
|
|
||||||
files:
|
files:
|
||||||
- electron-dist/**/*
|
- from: out/desktop
|
||||||
|
to: .
|
||||||
|
filter:
|
||||||
|
- "**/*"
|
||||||
- "!node_modules"
|
- "!node_modules"
|
||||||
|
|
||||||
extraResources:
|
extraResources:
|
||||||
- from: .output/server
|
- from: out/web/server
|
||||||
to: server
|
to: server
|
||||||
- from: .output/public
|
- from: out/web/public
|
||||||
to: public
|
to: public
|
||||||
- from: dist/mcp-server.cjs
|
- from: out/mcp-server.cjs
|
||||||
to: mcp-server.cjs
|
to: mcp-server.cjs
|
||||||
|
|
||||||
mac:
|
mac:
|
||||||
category: public.app-category.graphics-design
|
category: public.app-category.graphics-design
|
||||||
icon: build/icon.icns
|
icon: apps/desktop/build/icon.icns
|
||||||
artifactName: "${productName}-${version}-${arch}-mac.${ext}"
|
artifactName: "${productName}-${version}-${arch}-mac.${ext}"
|
||||||
target:
|
target:
|
||||||
- dmg
|
- dmg
|
||||||
|
|
@ -33,7 +36,7 @@ dmg:
|
||||||
title: "${productName} ${version}"
|
title: "${productName} ${version}"
|
||||||
|
|
||||||
win:
|
win:
|
||||||
icon: build/icon.ico
|
icon: apps/desktop/build/icon.ico
|
||||||
artifactName: "${productName}-${version}-${arch}-win.${ext}"
|
artifactName: "${productName}-${version}-${arch}-win.${ext}"
|
||||||
target:
|
target:
|
||||||
- nsis
|
- nsis
|
||||||
|
|
@ -48,7 +51,7 @@ nsis:
|
||||||
createStartMenuShortcut: true
|
createStartMenuShortcut: true
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
icon: build/icon.png
|
icon: apps/desktop/build/icon.png
|
||||||
category: Graphics
|
category: Graphics
|
||||||
artifactName: "${productName}-${version}-${arch}-linux.${ext}"
|
artifactName: "${productName}-${version}-${arch}-linux.${ext}"
|
||||||
desktop:
|
desktop:
|
||||||
|
|
@ -72,6 +75,6 @@ fileAssociations:
|
||||||
description: OpenPencil Design File
|
description: OpenPencil Design File
|
||||||
mimeType: application/x-openpencil
|
mimeType: application/x-openpencil
|
||||||
role: Editor
|
role: Editor
|
||||||
icon: build/icon
|
icon: apps/desktop/build/icon
|
||||||
|
|
||||||
asar: true
|
asar: true
|
||||||
163
apps/desktop/ipc-handlers.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { execSync } from 'node:child_process'
|
import { execSync } from 'node:child_process'
|
||||||
import { fork, type ChildProcess } from 'node:child_process'
|
import { fork, type ChildProcess } from 'node:child_process'
|
||||||
import { createServer } from 'node:net'
|
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 { homedir } from 'node:os'
|
||||||
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises'
|
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises'
|
||||||
|
|
||||||
|
|
@ -33,16 +33,12 @@ import {
|
||||||
import {
|
import {
|
||||||
setupAutoUpdater,
|
setupAutoUpdater,
|
||||||
broadcastUpdaterState,
|
broadcastUpdaterState,
|
||||||
getUpdaterState,
|
|
||||||
setUpdaterState,
|
setUpdaterState,
|
||||||
checkForAppUpdates,
|
|
||||||
clearUpdateTimer,
|
clearUpdateTimer,
|
||||||
startUpdateTimer,
|
|
||||||
quitAndInstall,
|
|
||||||
getAutoUpdateEnabled,
|
|
||||||
setAutoUpdateEnabled,
|
setAutoUpdateEnabled,
|
||||||
} from './auto-updater'
|
} 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 mainWindow: BrowserWindow | null = null
|
||||||
let nitroProcess: ChildProcess | null = null
|
let nitroProcess: ChildProcess | null = null
|
||||||
|
|
@ -448,143 +444,91 @@ function createWindow(): void {
|
||||||
mainWindow.webContents.openDevTools({ mode: 'detach' })
|
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.on('closed', () => {
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// IPC: native file dialogs & updater
|
// IPC: native file dialogs & updater (extracted to ipc-handlers.ts)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function setupIPC(): void {
|
function initIPC(): void {
|
||||||
ipcMain.handle('dialog:openFile', async () => {
|
setupIPC({
|
||||||
if (!mainWindow) return null
|
getMainWindow: () => mainWindow,
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
getPendingFilePath: () => pendingFilePath,
|
||||||
title: 'Open .op file',
|
clearPendingFilePath: () => { pendingFilePath = null },
|
||||||
filters: [{ name: 'OpenPencil Files', extensions: ['op', 'pen'] }],
|
prefsCache,
|
||||||
properties: ['openFile'],
|
schedulePrefsWrite,
|
||||||
})
|
writeAppSettings,
|
||||||
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -651,7 +595,7 @@ app.on('ready', async () => {
|
||||||
await initLogger(app.getPath('userData'))
|
await initLogger(app.getPath('userData'))
|
||||||
fixPath()
|
fixPath()
|
||||||
await loadPrefs()
|
await loadPrefs()
|
||||||
setupIPC()
|
initIPC()
|
||||||
buildAppMenu()
|
buildAppMenu()
|
||||||
|
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
6
apps/desktop/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@zseven-w/desktop",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ export interface ElectronAPI {
|
||||||
getPreferences: () => Promise<Record<string, string>>
|
getPreferences: () => Promise<Record<string, string>>
|
||||||
setPreference: (key: string, value: string) => Promise<void>
|
setPreference: (key: string, value: string) => Promise<void>
|
||||||
removePreference: (key: string) => Promise<void>
|
removePreference: (key: string) => Promise<void>
|
||||||
|
confirmClose: () => void
|
||||||
updater: {
|
updater: {
|
||||||
getState: () => Promise<UpdaterState>
|
getState: () => Promise<UpdaterState>
|
||||||
checkForUpdates: () => Promise<UpdaterState>
|
checkForUpdates: () => Promise<UpdaterState>
|
||||||
|
|
@ -90,6 +91,8 @@ const api: ElectronAPI = {
|
||||||
|
|
||||||
getPendingFile: () => ipcRenderer.invoke('file:getPending'),
|
getPendingFile: () => ipcRenderer.invoke('file:getPending'),
|
||||||
|
|
||||||
|
confirmClose: () => ipcRenderer.send('window:confirmClose'),
|
||||||
|
|
||||||
getLogDir: () => ipcRenderer.invoke('log:getDir'),
|
getLogDir: () => ipcRenderer.invoke('log:getDir'),
|
||||||
|
|
||||||
updater: {
|
updater: {
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"outDir": "../electron-dist",
|
"outDir": "../../out/desktop",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
116
apps/web/CLAUDE.md
Normal 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
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@zseven-w/web",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 544 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
16
apps/web/public/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,7 @@ interface ChatBody {
|
||||||
system: string
|
system: string
|
||||||
messages: Array<{ role: 'user' | 'assistant'; content: string; attachments?: ChatAttachmentWire[] }>
|
messages: Array<{ role: 'user' | 'assistant'; content: string; attachments?: ChatAttachmentWire[] }>
|
||||||
model?: string
|
model?: string
|
||||||
provider?: 'anthropic' | 'openai' | 'opencode' | 'copilot'
|
provider?: 'anthropic' | 'openai' | 'opencode' | 'copilot' | 'gemini'
|
||||||
thinkingMode?: 'adaptive' | 'disabled' | 'enabled'
|
thinkingMode?: 'adaptive' | 'disabled' | 'enabled'
|
||||||
thinkingBudgetTokens?: number
|
thinkingBudgetTokens?: number
|
||||||
effort?: 'low' | 'medium' | 'high' | 'max'
|
effort?: 'low' | 'medium' | 'high' | 'max'
|
||||||
|
|
@ -93,7 +93,7 @@ export default defineEventHandler(async (event) => {
|
||||||
setResponseHeaders(event, { 'Content-Type': 'application/json' })
|
setResponseHeaders(event, { 'Content-Type': 'application/json' })
|
||||||
return { error: 'Missing model. Model fallback is disabled.' }
|
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' })
|
setResponseHeaders(event, { 'Content-Type': 'application/json' })
|
||||||
return { error: 'Missing or unsupported provider. Provider fallback is disabled.' }
|
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 === 'anthropic') return streamViaAgentSDK(body, body.model)
|
||||||
if (body.provider === 'opencode') return streamViaOpenCode(body, body.model)
|
if (body.provider === 'opencode') return streamViaOpenCode(body, body.model)
|
||||||
if (body.provider === 'copilot') return streamViaCopilot(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)
|
return streamViaCodex(body, body.model)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -720,6 +721,61 @@ function mapCopilotReasoningEffort(
|
||||||
return effort
|
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) */
|
/** Stream via GitHub Copilot SDK (@github/copilot-sdk) */
|
||||||
function streamViaCopilot(body: ChatBody, model?: string) {
|
function streamViaCopilot(body: ChatBody, model?: string) {
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
|
|
@ -39,7 +39,7 @@ function buildExecCmd(binPath: string, args: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectBody {
|
interface ConnectBody {
|
||||||
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot'
|
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot' | 'gemini-cli'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectResult {
|
interface ConnectResult {
|
||||||
|
|
@ -82,6 +82,10 @@ export default defineEventHandler(async (event) => {
|
||||||
return connectCopilot()
|
return connectCopilot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.agent === 'gemini-cli') {
|
||||||
|
return connectGeminiCli()
|
||||||
|
}
|
||||||
|
|
||||||
return { connected: false, models: [], error: `Unknown agent: ${body.agent}` } satisfies ConnectResult
|
return { connected: false, models: [], error: `Unknown agent: ${body.agent}` } satisfies ConnectResult
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -667,3 +671,174 @@ function friendlyOpenCodeError(raw: string): string {
|
||||||
}
|
}
|
||||||
return raw
|
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
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ interface GenerateBody {
|
||||||
system: string
|
system: string
|
||||||
message: string
|
message: string
|
||||||
model?: string
|
model?: string
|
||||||
provider?: 'anthropic' | 'openai' | 'opencode'
|
provider?: 'anthropic' | 'openai' | 'opencode' | 'gemini'
|
||||||
thinkingMode?: 'adaptive' | 'disabled' | 'enabled'
|
thinkingMode?: 'adaptive' | 'disabled' | 'enabled'
|
||||||
thinkingBudgetTokens?: number
|
thinkingBudgetTokens?: number
|
||||||
effort?: 'low' | 'medium' | 'high' | 'max'
|
effort?: 'low' | 'medium' | 'high' | 'max'
|
||||||
|
|
@ -48,6 +48,9 @@ export default defineEventHandler(async (event) => {
|
||||||
if (body.provider === 'openai') {
|
if (body.provider === 'openai') {
|
||||||
return generateViaCodex(body, body.model)
|
return generateViaCodex(body, body.model)
|
||||||
}
|
}
|
||||||
|
if (body.provider === 'gemini') {
|
||||||
|
return generateViaGemini(body, body.model)
|
||||||
|
}
|
||||||
return { error: 'Missing or unsupported provider. Provider fallback is disabled.' }
|
return { error: 'Missing or unsupported provider. Provider fallback is disabled.' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -261,3 +264,15 @@ async function generateViaOpenCode(body: GenerateBody, model?: string): Promise<
|
||||||
releaseOpencodeServer(ocServer)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
311
apps/web/server/api/ai/image-generate.ts
Normal 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',
|
||||||
|
})
|
||||||
|
}
|
||||||
276
apps/web/server/api/ai/image-search.ts
Normal 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
|
||||||
|
})
|
||||||
104
apps/web/server/api/ai/image-service-test.ts
Normal 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 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -2,7 +2,7 @@ import { defineEventHandler, readBody, setResponseHeaders } from 'h3'
|
||||||
import { execSync } from 'node:child_process'
|
import { execSync } from 'node:child_process'
|
||||||
|
|
||||||
interface InstallBody {
|
interface InstallBody {
|
||||||
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot'
|
agent: 'claude-code' | 'codex-cli' | 'opencode' | 'copilot' | 'gemini-cli'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InstallResult {
|
interface InstallResult {
|
||||||
|
|
@ -17,6 +17,7 @@ const BINARY_MAP: Record<string, string> = {
|
||||||
'codex-cli': 'codex',
|
'codex-cli': 'codex',
|
||||||
'opencode': 'opencode',
|
'opencode': 'opencode',
|
||||||
'copilot': 'copilot',
|
'copilot': 'copilot',
|
||||||
|
'gemini-cli': 'gemini',
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkBinary(binary: string): boolean {
|
function checkBinary(binary: string): boolean {
|
||||||
|
|
@ -65,6 +66,11 @@ function getInstallInfo(agent: string): { command: string; docsUrl: string } {
|
||||||
: 'See documentation',
|
: 'See documentation',
|
||||||
docsUrl: 'https://docs.github.com/copilot/how-tos/copilot-cli',
|
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:
|
default:
|
||||||
return { command: '', docsUrl: '' }
|
return { command: '', docsUrl: '' }
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +123,8 @@ async function tryAutoInstall(agent: string, binary: string): Promise<InstallRes
|
||||||
return tryOpenCodeInstall(binary)
|
return tryOpenCodeInstall(binary)
|
||||||
case 'copilot':
|
case 'copilot':
|
||||||
return tryCopilotInstall(binary)
|
return tryCopilotInstall(binary)
|
||||||
|
case 'gemini-cli':
|
||||||
|
return tryNpmInstall('@anthropic-ai/gemini-cli', binary)
|
||||||
default:
|
default:
|
||||||
return { success: false, error: 'Unknown agent' }
|
return { success: false, error: 'Unknown agent' }
|
||||||
}
|
}
|
||||||