High-level design notes for @codeam/ide. Pairs with the per-phase roadmap docs in docs/roadmap/.
┌──────────────────────────────────────────────────────┐
│ Consumer app (CodeAgent Mobile, your app, …) │
│ - Owns the backend (REST / SSE / WebSocket / local) │
│ - Provides adapters that implement the contracts │
│ - Mounts the IDE UI components inside its routes │
└──────────────────────────────────────────────────────┘
▲
│ adapters in
│
┌──────────────────────────────────────────────────────┐
│ @codeam/ide-web │ @codeam/ide-native │
│ React DOM UI │ React Native UI │
│ │
│ FileViewerProvider, FileViewerHost, (Phase 2: │
│ FileTreeSidebar, SourceControlPanel, SearchPanel, │
│ TerminalPanel, SettingsDrawer, ExtensionMarketplace) │
└──────────────────────────────────────────────────────┘
▲
│ imports types from
│
┌──────────────────────────────────────────────────────┐
│ @codeam/ide-core │
│ - Adapter interfaces │
│ FileFetcher, GitProvider, FileTreeProvider, │
│ SearchProvider, TerminalProvider, SettingsStore │
│ - Payload types │
│ - Pure helpers (detectLanguage) │
│ - Zero runtime dependencies on a UI framework │
└──────────────────────────────────────────────────────┘
The dependency direction is strictly downward. @codeam/ide-core knows nothing about React. @codeam/ide-web and @codeam/ide-native know about React but not about each other. Consumer apps know about everything and own the wiring.
-
Backend-agnostic by construction. No UI component imports anything from a specific backend (CodeAgent CLI, GitHub Codespaces, etc.). The contract is the API. Consumers swap backends without touching the library.
-
Stable identity for adapters. All components key their
useEffectlifecycles off adapter identity. Adapter factories must return the same instance for the same id — module-levelMapcache oruseMemoon the consumer side. This is documented in every consumer-facing README to avoid the flicker class of bugs. -
Same surface on web and native. When a component exists in both
@codeam/ide-weband@codeam/ide-native, the props are identical and the behaviour matches. Cross-platform consumers can swap the import path without touching call sites. -
No tooling lock-in for consumers. No required Tailwind config, no required babel plugin, no required webpack loader. The packages ship with inline styles or framework-native equivalents (StyleSheet on RN). Themes are JSON.
-
Monaco loaded lazily. Bundling 5+ MB of editor code into a consumer's main bundle is a non-starter. Web uses
@monaco-editor/react's on-demand loader; native fetches Monaco from a CDN inside a WebView on first open. Phase 2 adds amonacoLoaderprop for self-hosted assets. -
Phase-gated feature delivery. Phase 1 ships file viewer + the adapter contracts. Phases 2/3/4 add features incrementally — each phase has explicit acceptance criteria so we never claim "done" without measuring it.
codeagent-mobile-ide/
├── packages/
│ ├── core/ # @codeam/ide-core
│ ├── web/ # @codeam/ide-web
│ └── native/ # @codeam/ide-native
├── examples/ # consumer examples — runnable, used as smoke tests
├── docs/
│ ├── ARCHITECTURE.md # this file
│ └── roadmap/
│ ├── phase-2-features.md
│ ├── phase-3-extensions.md
│ └── phase-4-gtm.md
├── .github/workflows/ # CI + Release (tag-triggered)
├── .husky/ # pre-commit / commit-msg hooks
├── cliff.toml # git-cliff config — Conventional Commits → CHANGELOG sections
├── eslint.config.mjs # ESLint v9 flat config (shared by all packages)
├── tsconfig.base.json # shared compiler options
├── commitlint.config.mjs # Conventional Commits with library-specific scopes
└── package.json # workspace root
Each package owns:
- Its own
package.json(independent publishable unit) - Its own
tsconfig.json(extends../../tsconfig.base.json) - Its own
tsup.config.ts(build config — ESM + CJS + d.ts) - Its own
README.md(per-package npm landing page)
tsupfor builds. Fast (esbuild under the hood), produces ESM + CJS +.d.tsin one pass, plays nice with monorepos.vitestfor tests. Lighter than Jest, faster startup, native ESM support — good fit for a library.- ESLint v9 flat config at the repo root, shared by all packages.
- Prettier for formatting. Standard config: 100 col, 2 spaces, single quotes, trailing commas.
husky+lint-stagedruns ESLint on staged files at commit time.commitlintenforces Conventional Commits with the scope list incommitlint.config.mjs.git-cliffgenerates per-package CHANGELOG entries at release time from the Conventional Commits between tags. Same configuration shape as thecodeagent-mobile-clientsrepo, so anyone reading either changelog sees an identical format. All three packages share a fixed version line — a singlevX.Y.Ztag publishes all three.
Runs on every push and PR to main:
commitlint— PR commit messagesnpm run lintnpm run typechecknpm run build(all packages)npm test
Triggered by pushing a tag matching v*.*.*. Topology:
setup— validates the tag and exposes the version as a job outputpublish-core— patchespackages/core/package.jsonversion, builds, publishes to npmpublish-web+publish-native— run in parallel after core; each patches its own version + the@codeam/ide-coredependency pin, builds, publishesrelease— runsgit-cliffagainst the commits between the previous and current tag, prepends the notes to each package'sCHANGELOG.md, commits back tomainwithchore(changelog): notes for vX.Y.Z [skip ci], and creates a GitHub Release using the same notes
Secrets required (set in GitHub repo settings → Secrets):
NPM_TOKEN— npmjs.com automation token with publish scope on the@codeamorg
A pre-release tag (vX.Y.Z-rc.N) follows the same path but the GitHub Release is marked prerelease: true.
The library lives in this repo (public). Consumer apps live elsewhere:
codeagent-mobile(private) — the mobile + landing + backend, depends on@codeam/ide-web+@codeam/ide-nativecodeagent-mobile-clients(public) — the CLI + VS Code/JetBrains plugins, depends on@codeam/ide-corefor the adapter contracts
When the adapter contract changes:
- Land the contract change here as a
minorbump (ormajorif it's breaking) - Update consumers to the new version
- If the change is breaking, give consumers a deprecation window via overload-compatible additions before removing the old surface
Adapter contracts are public API. Treat changes accordingly.
Every IDE surface depends on at most one adapter. The adapter is a small interface — no inheritance, no class hierarchies, just an object with the methods declared in the contract.
// You write this:
const fetcher: FileFetcher = {
label: 'session-abc',
canWrite: true,
read: async (path) => { ... },
write: async (path, content) => { ... },
};
// We render this:
<FileViewerProvider fetcher={fetcher}>
<FileViewerHost />
</FileViewerProvider>Same model for GitProvider, FileTreeProvider, SearchProvider, TerminalProvider. Each component receives only the adapter it needs — no "god object" with every capability. This makes testing trivial (mock the adapter) and lets consumers ship without a feature they haven't implemented yet (skip the adapter, the component renders an empty state).
- Adoption. Drop into any React-based app with a 5-line wiring.
- Iteration. Each phase adds adapter contracts + UI surfaces independently — no big-bang releases.
- Compatibility. VS Code's mental model is preserved (Monaco editor, VS Code-format manifest, Open VSX marketplace) so users and extension authors don't need to learn new concepts.
- No-React consumers. Phase 1+ targets React only. Webcomponent wrapper is possible later but not in scope.
- Local-only file systems. All flows assume an adapter that can fail / be slow. Local file access is just one possible adapter.
- Desktop-class extensions. Phase 3 supports the most-used VS Code extension APIs, not all 200+. Extensions requiring
child_process, debugger UI, or notebook view are out of scope for v1.