* feat(editor): implement Figma import functionality
- Added a new FigmaImportDialog component for importing .fig files.
- Integrated file processing and conversion logic to handle Figma files.
- Enhanced the editor layout to include a button for opening the Figma import dialog.
- Introduced page tabs for better navigation between document pages.
- Updated various components to utilize active page children for rendering and interactions.
- Added new dependencies: fzstd and kiwi-schema for improved functionality.
* fix(ai): harden Claude agent env variable resolution
- Filter empty strings and object values from settings.json env
- Validate ANTHROPIC_CUSTOM_HEADERS is valid JSON before passing
- Remove incorrect ANTHROPIC_CUSTOM_HEADERS auto-generation from auth token
* refactor(ai): remove silent fallback logic, surface errors in AI panel
- Remove direct-stream fallback in generateDesign, delegate to orchestrator
- Remove orchestrator planner fallback plan (buildFallbackPlanFromPrompt)
- Remove sub-agent retry logic, minimal mode retry, and placeholder insertion
- Throw on sub-agent error instead of silently degrading
- Call updateLastMessage in catch block so errors display immediately
- Clean up unused imports, functions, and console.log/warn statements
* fix(ai): add 30s API connect timeout for faster error detection
- Set maxRetries: 0 on Anthropic SDK to prevent retry delays on 429
- Add AbortController-based 30s timeout for initial API connection
- If the provider doesn't respond within 30s, abort and send SSE error
- Refactor streamViaAnthropicSDK with shared send() helper
* fix(editor): improve Figma import visual fidelity
- Extract image files from .fig ZIP archive and resolve by SHA-1 hash
- Map Figma textCase property (UPPER/LOWER/TITLE) to text content
- Convert ellipse nodes with image fills to image nodes
- Decode Figma vector paths and match icons via Lucide icon registry
- Fix z-order: move background rectangles behind content layers
- Set clipContent=true for Figma frames (default clip behavior)
- Unwrap single-artboard pages to use artboard as root frame
* fix(canvas): preserve clipPath during async image load and invalidate cache
- Copy clipPath from placeholder rect to FabricImage in onload callback
- Set obj.dirty=true after applying/removing clipPath so Fabric.js
re-renders with the updated clip region instead of using stale cache
* fix(ai): use page-aware empty frame detection and place new designs as siblings
- Fix isCanvasOnlyEmptyFrame() to use getActivePageChildren() instead of
document.children which is empty after page migration
- When canvas is empty, replace default frame (existing behavior restored)
- When canvas has content, add generated frame as top-level sibling
positioned to the right with 100px gap
- Track generationRootFrameId so height adjustment targets correct frame
- Add WIDTH SELECTION rule: app screens use 375x812 mobile dimensions
- Include selected node dimensions in AI context string
* feat(electron): implement application menu with native actions
- Added a native application menu for macOS and Windows, including File, Edit, View, and Window menus.
- Integrated menu actions for creating, opening, saving files, and editing operations like undo/redo.
- Enhanced the editor layout to handle Electron menu actions through a custom hook.
- Updated icon components in the toolbar and top bar for consistent styling.
- Refactored padding and layout components for improved structure and readability.
* fix(canvas): use page-aware children for selection depth resolution
getSelectableNodeIds() used doc.children which is empty under the
pages architecture. Switch to getActivePageChildren() so canvas
selection correctly syncs to the layers panel.
* fix(panels): adjust property panel sections for image nodes
Hide fill/stroke sections (not applicable to images) and show
corner radius section for image elements.
* feat(canvas): implement image corner radius via clipPath
FabricImage does not support rx/ry natively. Use a rounded-rect
clipPath instead, with objectCaching disabled to ensure immediate
re-render on radius changes.
* fix(canvas): use page-aware forcePageResync for canvas re-sync
The old pattern { ...doc, children: [...doc.children] } only touched
root-level children which are empty under pages architecture. Extract
forcePageResync() into canvas-sync-utils and replace all 7 occurrences
across canvas-object-modified, drag-into-layout, and layout-reorder.
* fix(canvas): preserve image corner radius clipPath during drag
Three issues caused corner radius loss on image drag:
1. Scale baking in object:modified changed FabricImage width from
natural to display dimensions, creating a clipPath size mismatch.
Exclude images from scale baking (like paths/polygons).
2. object:moving unconditionally cleared all clipPaths to avoid stale
frame clipping. Now only clears absolutePositioned (frame) clips,
preserving object-level clips like image corner radius.
3. Post-drag re-sync overwrote the corner radius clipPath with the
parent frame clip. Now skips frame clip when the object already
has its own non-absolutePositioned clipPath.
* feat(ai): enhance chat functionality with image attachments support
- Added support for image attachments in chat messages, allowing users to send images alongside text.
- Implemented temporary file handling for attachments, ensuring they are accessible to the AI processing.
- Updated chat message structure to include attachments, and modified the prompt building logic to reference these images.
- Enhanced the chat panel UI to allow users to select and preview images before sending.
- Introduced a new state management for pending attachments in the AI store.
- Refactored related components to accommodate the new attachment feature, improving user experience and interaction.
* feat(multi-page): introduce multi-page architecture and enhance agent settings UI
- Added multi-page architecture to the document structure, allowing for page CRUD actions such as add, remove, rename, reorder, and duplicate.
- Implemented `activePageId` state management in the canvas store for better page handling.
- Enhanced the top bar with an `AgentStatusButton` to display connected AI providers and their statuses.
- Updated the agent settings dialog to improve the UI for managing AI integrations, including better layout and connection indicators.
- Refactored related components for improved usability and visual consistency.
* docs(README): add contributors section with dynamic contributor image
* feat(ai): improve chat streaming and error handling
- Added functionality to hide the checklist when streaming stops with no completed items.
- Implemented abort controller management to handle user-initiated stops during chat generation.
- Enhanced error handling to prevent displaying errors for user-initiated stops while preserving partial content.
- Updated orchestration and design generation functions to support abort signals, improving responsiveness during streaming.
* fix(canvas): align z-order with layer panel convention (top = front)
Establish consistent z-order: children[0] = top of layer panel = frontmost
on canvas. This matches the standard design tool convention (Figma, Sketch).
- Reverse flattenNodes iteration so children[0] is added last to canvas
- Add z-order reconciliation via moveObjectTo after each sync pass
- Figma import: sort children descending so front items are at children[0]
- addNode defaults to prepend (index 0) so new items appear at top
- duplicateNode inserts clone above original (at idx instead of idx+1)
- drag-reparent inserts at index 0 (frontmost) instead of childCount
- Async image loading preserves z-order via insertAt instead of add
- Add FigmaImportLayoutMode toggle (preserve vs openpencil auto-layout)
* chore: resolve .gitignore merge conflict
* fix(canvas): clamp corner radius to half the element height
* chore: update version to 0.1.0 and modify mac artifact naming in electron-builder configuration
---------
Co-authored-by: Fini <fini.yang@gmail.com>
108 lines
2.9 KiB
TypeScript
108 lines
2.9 KiB
TypeScript
import { mkdirSync, readFileSync } from 'node:fs'
|
|
import { homedir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
type EnvLike = Record<string, string | undefined>
|
|
|
|
interface ClaudeSettings {
|
|
env?: Record<string, unknown>
|
|
}
|
|
|
|
function normalizeEnvValue(value: unknown): string | undefined {
|
|
if (value == null) return undefined
|
|
if (typeof value === 'string') {
|
|
// Filter out empty strings - they cause issues
|
|
if (value.trim() === '') return undefined
|
|
return value
|
|
}
|
|
// Skip non-string values (objects, arrays, numbers, booleans) to prevent
|
|
// invalid header errors like "Invalid header name: '{...}'"
|
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
return String(value)
|
|
}
|
|
// Skip object values - they would cause header errors
|
|
if (typeof value === 'object') {
|
|
return undefined
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function readClaudeSettingsEnv(): EnvLike {
|
|
try {
|
|
const path = join(homedir(), '.claude', 'settings.json')
|
|
const raw = readFileSync(path, 'utf-8')
|
|
const parsed = JSON.parse(raw) as ClaudeSettings
|
|
if (!parsed.env || typeof parsed.env !== 'object') return {}
|
|
|
|
const env: EnvLike = {}
|
|
for (const [key, value] of Object.entries(parsed.env)) {
|
|
const normalized = normalizeEnvValue(value)
|
|
if (normalized !== undefined) {
|
|
env[key] = normalized
|
|
}
|
|
}
|
|
return env
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate if a string is valid JSON (for ANTHROPIC_CUSTOM_HEADERS).
|
|
*/
|
|
function isValidJson(str: string): boolean {
|
|
try {
|
|
JSON.parse(str)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build env passed to Claude Agent SDK.
|
|
* Priority: current process env > ~/.claude/settings.json env.
|
|
*/
|
|
export function buildClaudeAgentEnv(): EnvLike {
|
|
const fromSettings = readClaudeSettingsEnv()
|
|
const fromProcess = process.env as EnvLike
|
|
|
|
const merged: EnvLike = {
|
|
...fromSettings,
|
|
...fromProcess,
|
|
}
|
|
|
|
// Validate ANTHROPIC_CUSTOM_HEADERS if it exists - must be valid JSON
|
|
// If invalid, delete it to prevent "Invalid header name" errors
|
|
if (merged.ANTHROPIC_CUSTOM_HEADERS) {
|
|
if (!isValidJson(merged.ANTHROPIC_CUSTOM_HEADERS)) {
|
|
delete merged.ANTHROPIC_CUSTOM_HEADERS
|
|
}
|
|
}
|
|
|
|
// Compatibility: use ANTHROPIC_AUTH_TOKEN as ANTHROPIC_API_KEY if no API key is set
|
|
const authToken = merged.ANTHROPIC_AUTH_TOKEN
|
|
if (authToken && !merged.ANTHROPIC_API_KEY) {
|
|
merged.ANTHROPIC_API_KEY = authToken
|
|
}
|
|
|
|
// Running inside Claude terminal can break nested Claude invocations.
|
|
delete merged.CLAUDECODE
|
|
|
|
return merged
|
|
}
|
|
|
|
/**
|
|
* Force Claude CLI debug output into a writable temp location.
|
|
* This avoids crashes in restricted environments where ~/.claude/debug is not writable.
|
|
*/
|
|
export function getClaudeAgentDebugFilePath(): string | undefined {
|
|
try {
|
|
const dir = join('/tmp', 'openpencil-claude-debug')
|
|
mkdirSync(dir, { recursive: true })
|
|
return join(dir, 'claude-agent.log')
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|