Skip to content

Repository files navigation

perchance-ide

A standalone AI-powered Integrated Development Environment that runs entirely within Perchance generators. Combines an LLM-driven agent with a full-featured code editor (CodeMirror 6), virtual file system, Python execution environment (Pyodide WebAssembly), and a project documentation subagent — all bundled into a single JavaScript file (~430KB minified) served via jsDelivr CDN.

Quick Start

1. Create a Perchance Generator

Create a new generator on Perchance and add an HTML panel:

<!-- HTML Panel -->
<script>
  import("https://cdn.jsdelivr.net/gh/Fahell/perchance-ide@<COMMIT>/dist/agent.js");
</script>

2. Configure the List Panel

Add the ai-text-plugin to your list panel:

agentAi = {import:ai-text-plugin}

Replace <COMMIT> with the latest commit hash (auto-published to the dist branch's IMPORT.md on every push via the CI pipeline). See the generator templates for reference implementations and explore the other generator examples in ai-text-plugin/, continue-generator/, super-fetch-plugin/, and other-coder-perchance-generator/.

3. Web Search Setup (Optional)

On first launch, you'll be prompted to enter a Jina AI API key (free tier available). This enables web search and page scraping capabilities. The key is stored locally in your browser and never sent anywhere else.

Development Workflow

# Install dependencies
pnpm install

# Development mode (esbuild watch + source maps)
pnpm dev

# Single-shot production build → dist/agent.js (CI normally does this)
node ./esbuild.config.mjs

# Run test suite (Vitest)
pnpm exec vitest run

# Type check (strict)
tsc --noEmit

# Vitest watch mode
pnpm exec vitest --watch

Features

AI Agent & Tools

  • Tool-calling loop powered by Perchance's ai-text-plugin with up to 8 iterations, timeout handling (5min), cancellation (AbortController), and repetition detection
  • Web search via Jina AI API with in-memory TTL cache (5 min) and FIFO eviction
  • Page scraping to fetch and parse web content as markdown with retry + exponential backoff
  • Context management tools: BM25-lite keyword search + index-based message retrieval
  • Virtual File System (VFS) operations: read, write, edit (exact string replacement), list tree, search (name+content), delete, rename — all with change tracking via FNV-1a hashing
  • Python execution via Pyodide WebAssembly (v314.0.2) in a dedicated Web Worker — keeps main thread responsive during load and execution; automatic VFS↔MEMFS bidirectional sync via message passing with incremental change propagation (no full snapshot on repeated executions)
  • Package installation via micropip (numpy, pandas, requests, etc.)
  • Rate limiting per tool via sliding window algorithm
  • Node.js execution via BrowserPod v2.12.1 (loaded from CDN, pinned to version tag) — npm install, run_node_script, execute_npm_command; requires a BrowserPod API key (console.browserpod.io); conditionally booted at startup when enabled in settings; all operations run in the project root (/home/user) via RunOptions.cwd; persistent disk: storageKey defaults to "agent-perchance" so the same Pod disk is resumed across page reloads
  • Shell Tools via BrowserPod — run_shell_command (whitelisted Bash, Git, system utilities), run_git_command (safe Git operations), start_http_server (HTTP portal with public URL); auto-enabled when Node.js tools are active; bidirectional VFS↔Pod sync with reconciliation (stale files deleted, orphaned VFS files removed); all operations default to cwd: "/home/user"; deleteFile/renameFile use direct exec (no shell wrapping) for injection safety
  • Interactive Terminal — xterm.js-based terminal panel (managed internally by BrowserPod, no separate instance) for live shell sessions; toggleable via [term] button in the editor column footer; resizable (drag handle, 100-600px) with close button; auto-syncs files back to VFS when panel is hidden; supports multiple simultaneous HTTP portals via multi-callback registerPortalCallback
  • "Continue" mechanism for truncated responses — picks up where the LLM left off via startWith

Code Editor

  • CodeMirror 6-based tabbed editor with syntax highlighting for JS/TS/JSX/TSX, HTML, CSS, JSON, Markdown, Python, XML, YAML
  • Professional dark theme — One Dark Pro-inspired color scheme with full syntax highlighting (keywords, strings, types, variables, tags, attributes), styled autocomplete popup, matching bracket highlighting, and fold placeholders
  • Autocomplete — language-aware completions for JavaScript, TypeScript, JSX, TSX, CSS, and HTML (via @codemirror/autocomplete)
  • Linter integration — real-time error/warning diagnostics for JS, TS, JSON, CSS, and HTML (via @codemirror/lint)
  • Hover tooltips — contextual information on hover over keywords, APIs, and tokens
  • Indent guides — vertical lines at each tab stop for visual alignment
  • VS Code-inspired keymap — common editor shortcuts (duplicate line, comment toggle, move lines, indent/outdent, etc.)
  • Undo/redo stacks per file with state persistence; external content reloads are excluded from undo history to avoid confusing Ctrl+Z behavior
  • Auto-save to VFS (debounced 200ms) with lastSavedHash tracking to detect external changes; auto-save is enabled by default
  • Breadcrumbs bar — shows cursor position in the syntax tree (function/class/module hierarchy) with clickable breadcrumbs
  • Status bar — shows line/column position, selection length, total lines, and file save status
  • Live VFS sync — editor subscribes to onVfsChange events: reloads content on external edits (hash-diff guarded), closes tabs on deletes, updates refs on renames
  • Conflict resolution — detects when an external edit (agent tool, Pod sync) modifies a file with unsaved changes; shows a banner with "Keep mine" / "Accept theirs" options
  • Flush-before-rename — editor buffer is saved via editor:flush-before-rename custom event before file rename operations, preventing stale content
  • Emmet support for HTML/CSS expansion
  • Settings: font size, tab size, word wrap — all persisted to localStorage

Panels & UI

  • 3-column layout: chat sidebar | code editor + terminal panel | right panel (file explorer / outline / HTML preview / Python output)
  • Preview Panel — live HTML rendering via sandboxed iframe (srcdoc + allow-scripts)
  • Output Panel — persistent history of Python executions (last 20) with expandable cards and copy-to-clipboard
  • File Search — Ctrl+P fuzzy search across all VFS files with real-time scoring
  • Diff View — unified line-level diff with collapsible unchanged regions (Myers algorithm)
  • Context Viewer — token budget visualization across hot/warm/cold tiers
  • Scroll-to-bottom FAB for chat messages

Project Documentation (Mapper Agent)

  • Automatic documentation subagent that maintains structured summaries per-project in /<project>/_map/ after every file change
  • Tracks all VFS mutations via hash-based change detection (FNV-1a)
  • Uses its own lightweight tool loop (5 tools: read_file, write_file, edit_file, rename_file, delete_file)
  • Coalesces rapid successive edits before dispatching
  • Index file (index.md) is auto-generated deterministically — mapper focuses on individual file summaries
  • Summary format includes Purpose field and absolute VFS paths with pipe-separated dependency descriptions

Internationalization

  • 5 languages: English (en), Português (pt-BR), Español (es), 日本語 (ja), 中文 (zh)
  • Full translation dictionaries for settings, setup wizard, panel UI, tool cards, context viewer
  • Automatic browser locale detection with localStorage persistence

Storage & Persistence

  • IndexedDB (via idb v8.0.3) — 3 object stores: messages (auto-increment, timestamp index), kv (memories, summaries, chunks), files (VFS path-keyed)
  • localStorage — small config (API key, locale, editor settings) with legacy migration
  • Incremental persist — after initial full save, only modified files are written to IndexedDB (see Saving & Conflict Resolution)
  • Project export/import via JSON serialization (src/utils/vfs-io.ts) with sanitized paths

Saving & Conflict Resolution

The IDE uses a three-layer storage architecture with bidirectional synchronization:

Editor (CodeMirror) ←→ VFS (in-memory) ←→ IndexedDB (persist)
                          ↕
                     BrowserPod (Pod FS)

Six save triggers feed into a centralized debounced persist:

Trigger Mechanism Debounce
Agent write_file / edit_file tool trackedWrite()scheduleVfsPersist() 2000ms (500ms when auto-save on)
Agent delete_file / rename_file tool trackedDelete() / trackedRename()scheduleVfsPersist() 2000ms
Editor auto-save onChangetrackedWrite()scheduleVfsPersist() 200ms
Tab switch flushVfsPersist() (immediate) Immediate
Ctrl+S flushVfsPersist() (immediate) Immediate
Page unload / tab close beforeunload handler → flushVfsPersist() (with 1s timeout) Immediate

Change detection uses FNV-1a hashing (~5μs per 10KB) to determine whether file content actually changed before emitting events or persisting.

Conflict resolution prevents data loss when the editor buffer diverges from the VFS:

  • If the editor buffer is clean (no unsaved changes) and VFS content changes externally (agent tool, Pod sync), the editor reloads the VFS content transparently. The reload is excluded from undo history to avoid confusing Ctrl+Z behavior.
  • If the editor buffer is dirty (unsaved changes) and VFS content changes externally, a conflict banner appears at the top of the editor:
    • "Keep mine" — overwrites VFS with editor buffer content and persists
    • "Accept theirs" — discards editor changes and reloads from VFS (excluded from undo history)

Data loss prevention:

  • Auto-save is enabled by default (can be disabled in settings)
  • beforeunload handler blocks navigation when dirty files exist, with a 1-second race-timed flush attempt
  • _persisting guard prevents concurrent IndexedDB writes
  • Incremental persist only writes dirty files, reducing the window for data loss during crashes
  • BrowserPod write pre-check reads existing file content before createFile() — skips if already in sync (avoids destructive createFile which wipes existing content)
  • BrowserPod sync merge preserves files written by real-time subscriptions when bulk-syncing

Limitations:

  • CM6 undo history is per-file and not persisted across page reloads
  • No VFS snapshot is associated with conversation checkpoints
  • localStorage quota errors (settings, API key) are silently caught — check browser dev tools if settings don't persist

Agent Loop Internals

  • Dynamic system prompt built from enabled tool categories (web, context, VFS, terminal, node — each toggleable in settings)
  • Tool call parsing via flat XML tags with CDATA sections — <tool_call name="..."><param><![CDATA[value]]></param></tool_call> — using depth-aware tag matching and auto-closing-tag repair
  • Repetition detection — warns at 3 consecutive identical calls, interrupts at 5
  • Token estimation heuristic: UTF-8 byte length / 4 (or / 3 for code-heavy content)
  • Parallel execution — multiple <tool_call> blocks in one response run simultaneously
  • Conflict resolutionedit_file requires exact old/new string replacement with duplicate detection

Developer Experience

  • Fully typed TypeScript strict mode (ES2022) with generic ToolDefinition<TArgs> and runtime validateShape<T>() guards
  • Zustand vanilla store (createStore + subscribeWithSelector) — central IDE state with Preact bridge via useSyncExternalStore
  • Zero external dependencies for validation, diff, retry, and rate-limiting utilities
  • Unit tested with Vitest + jsdom (80+ tests: VFS, storage, context estimation, tool call parsing, retry, truncation, web cache)
  • Custom pnpm-workspace.yaml allowing esbuild builds

Architecture

src/
├── index.ts                  # Entry — bootstrap, env check, API key setup, agent orchestration
├── agent-loop.ts             # Core loop — tool call detection, continuation, repetition guard
├── context-manager.ts        # Token estimation, summarization, chunked summary storage
├── db.ts                     # IndexedDB (idb v8) — messages, kv, files stores
├── memory.ts                 # Persistent memory extraction (1-3 facts per exchange)
├── message-store.ts          # In-memory message cache + async IndexedDB persistence
├── storage.ts                # localStorage wrapper with legacy migration
├── store.ts                  # Zustand vanilla store — IDE-wide state (files, settings, messages, UI, conflictedFiles, persist debounce adjustment)
├── types.ts                  # Perchance ai-text-plugin types + getAi() resolver (checks window[name], window.root[name], window.parent?.root?.[name])
├── mapper-agent.ts           # Subagent — auto-maintains /<project>/_map/ documentation
├── mapper-dispatcher.ts      # Listens to VFS events, coalesces, dispatches per-project mapper
├── vfs.ts                    # Virtual File System — path operations, tree, snapshot, inline change events (write/delete/rename) for real-time sync subscribers
├── vfs-events.ts             # Hash-based (FNV-1a) change tracking + event emitter
├── vfs-persist.ts            # Debounced IndexedDB persistence (dynamic debounce: 500ms auto-save / 2000ms tools) with incremental dirty-path tracking, _persisting concurrency guard, and flush API
│
├── agent/
│   ├── prompt-builder.ts     # Dynamic system prompt from enabled tools + project state
│   ├── tool-call-parser.ts   # Flat XML CDATA parser, response cleaner, closing-tag fixer
│   ├── repetition-detector.ts # Fingerprint-based loop prevention
│   └── timeout-helpers.ts    # AbortSignal composition, withTimeout, aiCallWithSignal
│
├── browserpod/
│   ├── browserpod.d.ts       # Type declarations for @leaningtech/browserpod@2.12.1 (pinned CDN version) — BootConfig, RunOptions, Terminal, Process, BinaryFile, overloaded createFile/openFile, ArrayBuffer-based onOutput
│   └── manager.ts            # BrowserPod singleton — Node.js runtime lifecycle, VFS sync (merge-based cache, write pre-check to avoid destructive createFile), run(RunOptions with cwd/env/echo), Pod file management (delete/rename/list with recursive mkdir), multi-callback portal support, VFS change subscription for real-time sync; persistent storage by default (storageKey: "agent-perchance")
│
├── tools/
│   ├── index.ts              # Registry — ToolDefinition<TArgs>, categories, rate limiters
│   ├── context-tools.ts      # search_history (BM25-lite, trilingual stopwords) + get_messages
│   ├── node-tools.ts         # Node.js tools (npm install, node script, npm command) via BrowserPod
│   ├── shell-tools.ts        # Shell tools (run_shell_command, run_git_command, start_http_server) + Pod→VFS bidirectional sync with source file filtering
│   ├── sync-utils.ts         # Shared VFS↔BrowserPod sync utilities — VFS→Pod push (with optional deletion reconciliation), Pod→VFS write with hash tracking and persist
│   ├── vfs-tools.ts          # read/write/edit/list/search/delete/rename — diff-cache integration
│   ├── terminal-tools.ts     # run_python, execute_script, install_package
│   └── web-search.ts         # Jina AI search + scrape with TTL cache
│
├── utils/
│   ├── validate.ts           # Zero-dep runtime guards: validateShape<T>(), isArrayOf()
│   ├── truncate.ts           # Smart truncation (chars or lines) with ellipsis
│   ├── retry.ts              # Exponential backoff with full jitter + AbortSignal
│   ├── rate-limiter.ts       # Sliding window rate limiter
│   ├── diff.ts               # Myers diff algorithm (line-level, ~80 lines core)
│   ├── diff-cache.ts         # Before/after cache for tool call diff views (max 50 entries)
│   └── vfs-io.ts             # Project export/import JSON serialization with path sanitization
│
├── terminal/
│   ├── pyodide.ts            # Pyodide Web Worker bridge — delegates to PyodideWorkerManager; incremental VFS sync
│   └── pyodide.test.ts       # Pyodide bridge tests
│
├── workers/
│   ├── pyodide-manager.ts     # Worker lifecycle, request/response correlation, retry with backoff, incremental VFS sync
│   └── pyodide-worker-code.ts # Inline worker code as string (Blob URL) — init, runPython, installPackage, syncFiles, incremental sync
│
├── editor/
│   ├── index.ts              # CM6 factory — basicSetup, theme, keymap, change listener, linter selector, autocomplete selector, cursor tracker
│   ├── theme.ts              # Professional One Dark Pro-inspired dark theme with full syntax highlighting (keywords, strings, types, tags, etc.), autocomplete/fold/tooltip styling
│   ├── langs.ts              # Ext→LanguageSupport map (JS/TS/JSX/TSX/HTML/CSS/JSON/MD/Python/XML/YAML)
│   ├── outline.ts            # Lezer syntax tree → OutlineSymbol[] (JS/CSS/HTML)
│   ├── autocomplete.ts       # Language-aware autocomplete sources for JS/TS/JSX/TSX/CSS/HTML
│   ├── breadcrumbs.ts        # Lezer syntax tree → Breadcrumb[] for cursor-position hierarchy
│   ├── hover.ts              # Hover tooltip plugin — contextual info on keywords, APIs, tokens
│   ├── indent-guides.ts      # Vertical indent guide lines at each tab stop
│   ├── keymap.ts             # VS Code-inspired keymap (duplicate line, comment toggle, move lines, indent/outdent, etc.)
│   ├── lint.ts               # Linter extensions for JS/TS/JSON/CSS/HTML using @codemirror/lint
│   ├── emmet.ts              # Emmet CM6 plugin integration
│   ├── emmet-langs.ts        # Emmet syntax mapping
│   └── view-store.ts         # Active EditorView tracker
│
├── i18n/
│   ├── dict.ts               # 5-locale translation dictionaries + locale labels
│   └── index.ts              # t() function, browser detection, persistence
│
└── ui/
    ├── index.ts              # Entry — renderPanel() / renderSetup() with ErrorBoundary
    ├── AgentPanel.tsx        # 3-column layout, modals, keyboard shortcuts, state subscription
    ├── ChatMessages.tsx      # Message list with AutoScroll
    ├── MessageList.tsx       # Scrollable container (role="log", aria-live="polite")
    ├── UserMessage.tsx       # User message bubble
    ├── AgentMessage.tsx      # Agent response + tool call cards
    ├── ResponseText.tsx      # Markdown renderer with expand/collapse
    ├── ToolCallCard.tsx      # Collapsible tool call with args, result, diff view
    ├── ThinkingIndicator.tsx # Animated dots
    ├── ScrollFAB.tsx         # Floating scroll-to-bottom button
    ├── Header.tsx            # Version, commit, FAQ trigger
    ├── Footer.tsx            # Input bar, settings/context buttons (terminal toggle moved to EditorFooter)
    ├── Modal.tsx             # Accessible modal — focus trap, Escape close
    ├── SettingsModal.tsx     # API key, language, auto-save, tool toggles
    ├── ContextViewer.tsx     # Token budget, summary, tier visualization, memories
    ├── FaqModal.tsx          # FAQ with project links
    ├── CodeEditor.tsx        # CM6 tabbed editor — auto-save, dirty tracking, conflict detection (banner with Keep/Accept), VFS change subscription for external edits, flush-before-rename event handling, breadcrumbs bar, EditorStatusBar
    ├── BreadcrumbsBar.tsx    # Cursor-position breadcrumbs (function/class/module hierarchy) with Lezer syntax tree
    ├── EditorStatusBar.tsx   # Line/column/selection status bar with file save indicator
    ├── DiffView.tsx          # Unified diff with collapsible unchanged regions
    ├── FileSearchModal.tsx   # Ctrl+P fuzzy file search
    ├── PreviewPanel.tsx      # Live HTML preview via sandboxed iframe
    ├── OutputPanel.tsx       # Python execution history with copy
    ├── RightPanel.tsx        # Tab container (files/outline/preview/output)
    ├── TerminalPanel.tsx     # Interactive terminal via BrowserPod (no own xterm instance); resizable, close button, Pod→VFS sync on hide
    ├── EditorFooter.tsx      # Editor column footer with terminal toggle button and file info
    ├── SetupScreen.tsx       # First-run API key wizard
    ├── ErrorBoundary.tsx     # Preact error boundary with retry
    ├── theme.ts              # Design tokens — colors, fonts
    ├── markdown.ts           # Minimal markdown→HTML
    ├── animations.ts         # @keyframes definitions
    ├── formatRelativeTime.ts # Human-readable timestamps
    └── types.ts              # PanelMessage, ToolCallEntry, AgentStatus
    └── hooks/
        └── useKeyboardShortcuts.ts  # Global shortcut bindings

Agent Tools

The agent has access to the following tools, exposed through a generic ToolDefinition<TArgs> interface with typed parameters and runtime validation. Each tool can be individually enabled/disabled in settings, has an optional rate limit, and a configurable timeout.

Web & Information Tools

Tool Description Parameters
web_search Search the web via Jina AI (cached 5 min, max 3 retries) { query: string }
scrape_url Fetch full page content as markdown (cached 5 min) { url: string, maxChars?: number }

Context Management Tools

Tool Description Parameters
search_history BM25-lite keyword search across conversation history (EN/PT/ES stopwords) { query: string }
get_messages Retrieve raw messages by position or count { count?: number, from?: number, to?: number }

Virtual File System Tools

Tool Description Parameters
read_file Read file contents from VFS (max 5000 chars) { path: string }
write_file Create or overwrite a file (auto-dir, diff-cache, dirty-tracking) { path: string, content: string }
edit_file Replace exact text (safer than write_file for partial edits) { file_path: string, old_string: string, new_string: string }
list_files Show project tree with 📁/📄 icons { dir?: string }
search_files Search files by name or content (case-insensitive, max 20 results) { query: string, maxResults?: number }
delete_file Delete a file or folder recursively { path: string }
rename_file Rename or move a file/folder { oldPath: string, newPath: string }

Python Execution Tools

Tool Description Parameters
run_python Execute Python via Pyodide (VFS auto-synced) { code: string }
execute_script Run a .py file from VFS { path: string }
install_package Install via micropip (numpy, pandas, etc.) { pkgName: string }

Node.js Tools (BrowserPod)

Tool Description Parameters
run_npm_install Install npm packages (or from package.json) { packages?: string }
run_node_script Execute a Node.js script file in the BrowserPod env { path: string, args?: string }
execute_npm_command Run an arbitrary npm command (test, build, start...) { command: string }

Shell Tools (BrowserPod)

Tool Description Parameters
run_shell_command Execute safe Bash commands (whitelist-enforced; ls, cat, grep, curl, mkdir, cp, mv, rm, node, npm, git, etc.) { command: string }
run_git_command Native Git operations (status, log, diff, add, commit, branch, checkout); push/fetch/remote/config blocked { args: string }
start_http_server Start an HTTP server and get a public portal URL via BrowserPod sandbox { command: string, port?: number }

Context Management Architecture

The agent employs a 3-tier context architecture to efficiently manage conversation history within token budgets:

┌─────────────────────────────────────────────────────┐
│  HOT — Always in prompt (~1200 tokens)              │
│  Last 5 messages + rolling summary + key facts      │
├─────────────────────────────────────────────────────┤
│  WARM — Accessible via search_history tool          │
│  Chunked summaries of older conversation blocks     │
│  BM25-lite keyword search across all history        │
├─────────────────────────────────────────────────────┤
│  COLD — Accessible via get_messages tool            │
│  Full raw message history                           │
│  Index-based retrieval (by position or count)       │
└─────────────────────────────────────────────────────┘

Key Mechanisms

  • Automatic summarization triggers when conversation exceeds ~6K token budget
  • Summary condensation merges old+new summaries when combined budget exceeded
  • Chunked summaries stored in IndexedDB with message range ranges
  • Memory extraction runs asynchronously after each exchange, capturing timeless facts (max 20 stored)
  • Context tools enable the agent to self-retrieve historical information when users reference earlier conversation
  • ContextViewer modal provides visual feedback on token usage, tier distribution, chunk summaries, and extracted memories
  • Token estimation uses TextEncoder byte-length / 4 with adaptive heuristics (divisor 3.0 if >15% code operators detected)

Storage Layers

localStorage (Configuration)

Via src/storage.ts — synchronous storage for small user settings with agent: prefix:

Function Description
storageGet<T>(key) Get value by key (preserves null)
storageSet<T>(key, value) Set value by key
storageDel(key) Delete a key
storageHas(key) Check if key exists
storageKeys() List all keys
storageClear() Remove all data

Stored values: API key (agent:jina_key), panel mode, UI locale, input enabled state.

IndexedDB (Bulk Data)

Via src/db.ts — powered by idb v8 with two object stores:

Store Key Type Contents
messages Auto-increment id Chat message history (indexed by timestamp)
kv String key Generic key-value: memories, summaries, chunks, VFS

Messages API: dbAddMessage(), dbGetAllMessages(), dbGetLastN(n), dbGetMessageCount(), dbClearMessages(), dbGetMessagesByRange(from, to?)

Key-Value API: dbKvGet<T>(), dbKvGetValidated<T>() (with runtime type guard), dbKvSet(), dbKvDel(), dbKvClear(), dbKvKeys()

VFS (files) API: dbSaveVfs(entries), dbLoadVfs() — full replace on save, path-keyed retrieval.

Both stores share a single lazy-initialized IndexedDB connection via getDb().

Message Store

Custom message store (src/message-store.ts) combining in-memory cache with async IndexedDB persistence:

Function Description
initMessageStore() Load persisted messages from IndexedDB (async)
addMessage(msg) Append message and persist to IndexedDB (async)
getMessages() Get all messages from cache (sync)
getLastN(n) Get last N messages from cache (sync)
getMessageCount() Total message count (sync)
clearMessages() Clear cache and IndexedDB (async)

VFS Persistence

Centralized debounced persistence via src/vfs-persist.ts:

Function Description
scheduleVfsPersist() Debounced write to IndexedDB (2s timeout) — resets on each call
flushVfsPersist() Immediate write, cancels pending debounce
cancelScheduledPersist() Cancel pending save without flushing

VFS Events & Change Tracking

VFS mutations emit events through two complementary systems:

1. Inline Change Events (src/vfs.ts) — lightweight, no-hash emitter built directly into VFS operations:

  • Event types: write, delete, rename
  • Subscriber pattern: onVfsChange(listener) returns unsubscribe function
  • Fires synchronously on every vfsWrite(), vfsDeleteTree(), and vfsRename() call
  • Used by BrowserPod for real-time VFS→Pod sync (editor changes propagate immediately)
  • Content sanitization: vfsWrite() strips UTF-8 BOM and leading invisible whitespace to prevent downstream parse failures

2. Hash-Based Change Tracking (src/vfs-events.ts) — wraps VFS mutations with FNV-1a fingerprinting:

  • FNV-1a 32-bit hash computed on every write (~5μs per 10KB)
  • Event types: created, modified, deleted, renamed
  • Subscriber pattern: onVfsChange(listener) returns unsubscribe function
  • Hash persistence to IndexedDB for cross-session change detection
  • Used by the Mapper Agent to trigger documentation updates

VFS↔BrowserPod Synchronization Architecture

The VFS and BrowserPod runtime maintain bidirectional sync through multiple mechanisms:

VFS → Pod (Push):

Path When Mechanism
syncVfsToPod() Before each shell/git/node command (sync-utils.ts) Bulk write ALL VFS files to Pod; optionally reconcile deletions (shell tools only)
subscribeToVfsChanges() After Pod boot (manager.ts) Real-time: single file write/delete/rename on each VFS mutation via onVfsChange event
reconnect() On WebSocket disconnection (manager.ts) Bulk re-sync cached files from lastSyncedFiles

Pod → VFS (Pull):

Path When Mechanism
pullProjectFilesFromPod() After each shell/git command (shell-tools.ts) Allowlist-filtered bulk pull via find; orphan reconciliation (only for previously-synced files)
pullMetadataFromPod() After execute_npm_command (node-tools.ts) Pulls only package.json and package-lock.json using tracked writes with persist

Key design decisions:

  • Real-time sync via subscribeToVfsChanges() propagates editor changes immediately through writeFile()/deleteFile()/renameFile()
  • Bulk sync before commands ensures the Pod has the latest VFS state
  • Pod→VFS pull uses trackedWrite() (hash tracking) + scheduleVfsPersist() so pulled files survive page reloads
  • Orphan deletion in pull only targets files previously synced to Pod (not all VFS files), preventing accidental deletion of files created while Pod was offline
  • listFiles() excludes node_modules, .git, .npm, and __pycache__ from traversal to avoid performance issues
  • On Pod boot, an initial syncVfsToPod(false) pushes all existing VFS files so the Pod starts with the project intact

VFS Path Convention: PROJECT_ROOT

Since v0.1.0, the VFS uses PROJECT_ROOT = "/home/user" instead of bare / for POSIX compatibility with the BrowserPod Node.js runtime. All project files live under /home/user/... — paths like /src/index.ts are no longer valid. The root / and /home are protected from deletion. Tool descriptions in the system prompt dynamically reference PROJECT_ROOT (e.g., /home/user/src/index.ts).

Virtual File System I/O (Export/Import)

src/utils/vfs-io.ts provides project serialization:

Function Description
serializeProject() Export all VFS files as JSON manifest
deserializeProject(json) Import JSON manifest with path sanitization (rejects .. traversal)

State Management

Vanilla Zustand store (src/store.ts) created with createStore and subscribeWithSelector middleware:

State Slices

Slice Key Fields Description
Files activeFile, files[] Open file tabs and active selection
Editor editorView, settingsVersion Active EditorView ref, triggers recreation on settings change
Layout panelMode, sidebarVisible 3-column layout configuration
Settings settings Locale, fontSize, wordWrap, tabSize, autoSave, tool toggles (5)
Status isProcessing, statusMessage Processing indicator state
BrowserPod browserPodStatus, browserPodError Node.js runtime (BrowserPod) loading state (idle/loading/ready/error)
Terminal terminalOpen, activePortals[] Interactive terminal panel visibility + active HTTP portal URLs
Pyodide pyodideStatus, pyodideError Python runtime loading state
VFS vfsVersion Incremented on file writes for preview reactivity
Output outputs[] Python execution history (last 20 entries)
Messages messages[], agentStatus Panel chat messages and agent status (idle/thinking/searching/scraping/responding)

Key Actions

Action Description
openFile(path, name, language) Open tab or switch active (dedup)
closeFile(path) Close tab, select next available
setTerminalOpen(open) Show/hide interactive terminal panel
addPortal(portal) Register an active HTTP portal URL
clearPortals() Clear all tracked HTTP portals
renameFile(oldPath, newPath) Rename + trackedRename events + VFS persist
addUserMessage(content) Append user message with timestamp
addToolCall(name, args) Create tool call entry, return ID for updates
appendAgentResponse(response) Set final agent response text
appendToLastAgentResponse(text) Append to current agent response (for "continue")
setRightPanelTab(tab) Switch between files/outline/preview/output
addOutput(entry) Add Python output entry (auto-truncate to 20)
bumpVfsVersion() Trigger preview re-render
updateSettings(partial) Merge partial settings + increment settingsVersion

The store is runtime-only without built-in persistence. Preact components subscribe via useSyncExternalStore.

Accessibility

All UI components follow ARIA patterns for screen reader compatibility and keyboard navigation:

Component ARIA Implementation
Modal role="dialog", aria-modal="true", focus trap, Escape close, focus restoration
SettingsModal toggles role="switch", aria-checked, keyboard activation
MessageList role="log", aria-live="polite" for dynamic updates
ContextViewer budget bar role="progressbar", aria-valuenow/min/max
RightPanel tabs role="tablist" / role="tab", aria-selected
RightPanel tree role="tree" / role="treeitem", aria-expanded
FileSearchModal Keyboard navigation (arrows, Enter, Escape)
All interactive controls Semantic <button> elements with accessible labels
Color contrast textMuted: #757575 on black background (WCAG AA for 18pt+)

Internationalization

Supports 5 languages with automatic locale detection and persistence:

Locale Code Label
English en English
Portuguese pt-BR Português
Spanish es Español
Japanese ja 日本語
Chinese zh 中文

Translation dictionaries are stored in src/i18n/dict.ts with the t(key, locale?) function providing dot-notation access. Fallback chain: locale dict → en dict → raw key.

Testing

Test suite uses Vitest v4 with jsdom environment:

pnpm exec vitest run          # Single test run
pnpm exec vitest --watch      # Vitest watch mode

Test Coverage

Test File Coverage Area
src/vfs.test.ts VFS CRUD, normalization, tree, rename, delete, edge cases (~40 tests)
src/storage.test.ts localStorage get/set/del/has/keys/clear operations
src/context-manager.test.ts Token estimation (encoding + code heuristic)
src/agent-loop.test.ts Tool call extraction regex, response cleaning
src/utils/truncate.test.ts String truncation (character and line modes)
src/utils/retry.test.ts Error classification, retry with exponential backoff
src/terminal/pyodide.test.ts Pyodide bridge utilities
tests/tools/web-search-cache.test.ts Web search + scrape TTL cache, eviction, expiration

Configuration & Setup

Initial Setup

On first load without an API key, the Setup Screen wizard appears:

  1. Enter your Jina AI API key (free tier available)
  2. Key is saved to localStorage under agent:jina_key
  3. Agent panel loads with full functionality
  4. Change key later via Settings modal (gear icon in header)

Settings

Accessible via gear icon in header or Ctrl+,:

Setting Description
Jina API Key Web search and page scraping API key (validated on save)
Language UI locale selection (en, pt-BR, es, ja, zh)
Auto Save Auto-save files on change in editor (default: off)
Web Tools Enable/disable web_search and scrape_url tools
Context Tools Enable/disable search_history and get_messages tools
File Tools Enable/disable all VFS tools (read, write, edit, etc.)
Python Tools Enable/disable run_python, execute_script, install_package
Node.js Tools Enable/disable npm/node tools and shell tools via BrowserPod
BrowserPod API Key API key for BrowserPod Node.js runtime (console.browserpod.io)

Browser Requirements (Production)

When deploying the agent on a standalone page (outside Perchance), BrowserPod requires cross-origin isolation headers for SharedArrayBuffer support. The server must send these HTTP headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Why: BrowserPod's Node.js runtime depends on SharedArrayBuffer for WebAssembly execution. Without these headers, BrowserPod.boot() will fail silently. Most cloud hosting platforms (Vercel, Netlify, Cloudflare Pages) support configuring these headers.

On Perchance: The perchance.org platform sets these headers automatically — no action needed.

The manager logs a diagnostic warning at boot time if crossOriginIsolated is false.

Keyboard Shortcuts

Shortcut Action
Ctrl+, Open Settings
Ctrl+I Open Context Viewer
Ctrl+P File search
Ctrl+L Focus chat input
Ctrl+` Toggle terminal panel
Escape Close modals / blur editor

Critical Development Patterns

  • LLM calls: All operations (agent loop, summarization, memory extraction) use getAi() helper which resolves window[name] / window.root[name] / window.parent?.root?.[name]. Never call window.ai directly.
  • Tool calls: AI outputs <tool_call name="..."><param><![CDATA[value]]></param></tool_call> XML → runtime parses via depth-aware tag matching with CDATA extraction, executes tool, feeds result back into next LLM iteration. Multiple <tool_call> blocks in one response run in parallel.
  • Tool interface: Generic ToolDefinition<TArgs> with typed execute(args: TArgs) instead of untyped Record<string, any>.
  • Runtime validation: Zero-dependency validateShape<T>() and isArrayOf() utilities for type-safe runtime checks with dbKvGetValidated().
  • UI rendering: Preact renders into document.body with semantic HTML and ARIA roles throughout.
  • CDN cache busting: Use immutable @<COMMIT> references, never mutable @main branch tags.
  • Message flow: User message → handleSendMessage()addMessage() (cache + IndexedDB) → buildContext() (token-aware) → formatMemories()agentLoop() (up to 8 iterations, with continuation for truncation) → extractMemories() (async background).
  • Cancellation: User can cancel an in-progress agent response via AbortController. The LLM call is stopped via aiResult.stop(), and any running tool executions are aborted.
  • "Continue" mechanism: When agent response is truncated (>~1000 tokens), the panel shows a "Continue" button that re-calls the LLM with startWith: truncatedText to pick up where it left off.
  • Mapper Agent: After the main agent finishes, per-project VFS changes are coalesced and dispatched to a lightweight subagent that auto-maintains documentation in /<project>/_map/. This subagent runs with clean context (no history) and has its own internal tool loop (read_file, write_file, edit_file, rename_file, delete_file). Uses the same CDATA-based flat XML tool call format as the main agent.
  • Shell bidirectional sync: After or executes, files and directories created on the BrowserPod are auto-synced with the VFS. This includes real-time propagation of VFS changes and a filtered bulk pull from the Pod back to the VFS with non-destructive orphan reconciliation. See Sync Architecture (docs/sync.md) for technical details.
  • Project structure rule: The agent's system prompt instructs it to place source code under dedicated directories (src/, app/, lib/), declare source boundaries via "files": ["src/"] in package.json, and never mix runtime artifacts with source files. Only recognized source extensions and well-known config names are synced back to the IDE VFS.
  • Retry policy: All external API calls (Jina AI) use exponential backoff with full jitter (AWS-recommended). Retryable errors: network errors (TypeError), HTTP 429, 5xx. Non-retryable: 4xx (except 429), AbortError.

Build & Deployment

Built with esbuild (v0.28) into a single minified ESM file:

pnpm dev                       # Development build: source maps + watch mode (only local npm script)
node ./esbuild.config.mjs      # Single-shot production build → dist/agent.js
tsc --noEmit                   # TypeScript strict type check without emitting

Build-time constants injected via esbuild define:

  • __VERSION__ — from package.json
  • __COMMIT__ — from $COMMIT env var or "dev"
  • __BUILD_TIME__ — ISO timestamp of build

CI/CD Pipeline

Deployment is fully automated through GitHub Actions (.github/workflows/ci.yml). There is no local pnpm deploy step — release workflows are CI-exclusive. The legacy deploy.sh was deleted; all heavy operations (build, typecheck, test, publish) moved to a single workflow with per-stage jobs and job-level concurrency.

Trigger conditions:

  • Every push to any branch (except dist itself) auto-runs the full pipeline — typecheck + test + update-cdn.
  • Pull requests on main run typecheck + test only.
  • workflow_dispatch provides UI toggles for any combination of jobs.

update-cdn job (auto-runs on every push):

  1. Checkout the source branch.
  2. pnpm install --frozen-lockfile.
  3. Build with COMMIT=<src-short-sha> node ./esbuild.config.mjs; guard dist/agent.js exists.
  4. On an orphan branch dist-temp, commit dist/agent.js (H1).
  5. Generate IMPORT.md pointing to the immutable @HASH_FULL/dist/agent.js URL (full SHA, not short, for maximum jsDelivr caching durability).
  6. Add a second commit on top of H1 with IMPORT.md (H2 — child of H1).
  7. Force-push HEAD:dist to origin.

Why two commits instead of amend? H1 is an ancestor of dist's HEAD (via H2), which means H1 is permanently retained in dist's history. The legacy deploy.sh instead used git commit --amend + force-push, creating H1 as an orphan relative to the final amended commit — GitHub's GC could eventually purge it, permanently breaking the jsDelivr @H1/dist/agent.js URL referenced by every Perchance generator.

Branches:

Branch Purpose Lifecycle
main Source code history (clean, no deploy artifacts) Updated by merged PRs + direct pushes
dist CDN snapshots: dist/agent.js + IMPORT.md Force-replaced on every push to source

CDN URL: https://cdn.jsdelivr.net/gh/Fahell/perchance-ide@<COMMIT>/dist/agent.js

Latest committed value lives in dist's IMPORT.md (auto-updated by CI on every push).

Generator Templates

The project includes reference generators in these directories:

Directory Description
generator/ Main template — minimal HTML panel that imports the agent bundle + list panel with agentAi = {import:ai-text-plugin}
ai-text-plugin/ The official ai-text-plugin generator — complete HTML panel with documentation, usage examples, and list panel with all $output code
continue-generator/ AI Text Continue — interactive text continuation tool with instruction injection, paragraph continuation, and localStorage persistence
super-fetch-plugin/ Super Fetch — CORS-proxied fetch plugin for Perchance with automatic CDN bypass and fallback logic
other-coder-perchance-generator/ UFO AI Code Analyzer — Monaco Editor-based code analysis tool with language selector, auto-complete, and integrated chat

License

MIT

About

IDE com IA integrada ao Perchance

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages