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.
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>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/.
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.
# 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- Tool-calling loop powered by Perchance's
ai-text-pluginwith 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) viaRunOptions.cwd; persistent disk:storageKeydefaults 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 tocwd: "/home/user";deleteFile/renameFileuse 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-callbackregisterPortalCallback - "Continue" mechanism for truncated responses — picks up where the LLM left off via
startWith
- 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
lastSavedHashtracking 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
onVfsChangeevents: 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-renamecustom 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
- 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
- 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
- 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
- IndexedDB (via
idbv8.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
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 | onChange → trackedWrite() → 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)
beforeunloadhandler blocks navigation when dirty files exist, with a 1-second race-timed flush attempt_persistingguard 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 destructivecreateFilewhich 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
- 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 resolution —
edit_filerequires exact old/new string replacement with duplicate detection
- Fully typed TypeScript strict mode (ES2022) with generic
ToolDefinition<TArgs>and runtimevalidateShape<T>()guards - Zustand vanilla store (
createStore+subscribeWithSelector) — central IDE state with Preact bridge viauseSyncExternalStore - 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.yamlallowing esbuild builds
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
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.
| 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 } |
| 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 } |
| 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 } |
| 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 } |
| 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 } |
| 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 } |
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) │
└─────────────────────────────────────────────────────┘
- 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
TextEncoderbyte-length / 4 with adaptive heuristics (divisor 3.0 if >15% code operators detected)
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.
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().
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) |
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 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(), andvfsRename()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
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 throughwriteFile()/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()excludesnode_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
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).
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) |
Vanilla Zustand store (src/store.ts) created with createStore and subscribeWithSelector middleware:
| 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) |
| 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.
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+) |
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.
Test suite uses Vitest v4 with jsdom environment:
pnpm exec vitest run # Single test run
pnpm exec vitest --watch # Vitest watch mode| 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 |
On first load without an API key, the Setup Screen wizard appears:
- Enter your Jina AI API key (free tier available)
- Key is saved to localStorage under
agent:jina_key - Agent panel loads with full functionality
- Change key later via Settings modal (gear icon in header)
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) |
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.
| 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 |
- LLM calls: All operations (agent loop, summarization, memory extraction) use
getAi()helper which resolveswindow[name]/window.root[name]/window.parent?.root?.[name]. Never callwindow.aidirectly. - 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 typedexecute(args: TArgs)instead of untypedRecord<string, any>. - Runtime validation: Zero-dependency
validateShape<T>()andisArrayOf()utilities for type-safe runtime checks withdbKvGetValidated(). - UI rendering: Preact renders into
document.bodywith semantic HTML and ARIA roles throughout. - CDN cache busting: Use immutable
@<COMMIT>references, never mutable@mainbranch 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: truncatedTextto 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/"]inpackage.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.
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 emittingBuild-time constants injected via esbuild define:
__VERSION__— frompackage.json__COMMIT__— from$COMMITenv var or"dev"__BUILD_TIME__— ISO timestamp of build
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
distitself) auto-runs the full pipeline — typecheck + test +update-cdn. - Pull requests on
mainrun typecheck + test only. workflow_dispatchprovides UI toggles for any combination of jobs.
update-cdn job (auto-runs on every push):
- Checkout the source branch.
pnpm install --frozen-lockfile.- Build with
COMMIT=<src-short-sha> node ./esbuild.config.mjs; guarddist/agent.jsexists. - On an orphan branch
dist-temp, commitdist/agent.js(H1). - Generate
IMPORT.mdpointing to the immutable@HASH_FULL/dist/agent.jsURL (full SHA, not short, for maximum jsDelivr caching durability). - Add a second commit on top of H1 with
IMPORT.md(H2 — child of H1). - Force-push
HEAD:distto 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).
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 |
MIT