@molecule/app-ide-react

Feature · ide · App (browser) · v1.21.2 · Apache-2.0

React IDE components — chat, editor, preview, file-explorer panels for an AI-powered workspace

npm install @molecule/app-ide-react

npm · Source on GitHub

How it works

@molecule/app-ide-react is a ready-made ide feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.

import { ChatPanel, EditorPanel, PreviewPanel, WorkspaceLayout } from '@molecule/app-ide-react'

;<WorkspaceLayout>
  <ChatPanel
    projectId="proj_abc123"
    onFileOpen={(path) => console.log('open', path)}
    onFileChange={(path, content) => console.log('changed', path, content.length)}
    onReadyToBuild={() => console.log('boot sandbox')}
  />
  <EditorPanel
    onActiveFileChange={(path) => console.log('active', path)}
    onFixWithAI={(req) => console.log('fix', req)}
  />
  <PreviewPanel onPreviewError={(errs) => console.error(errs)} />
</WorkspaceLayout>

Works with: @molecule/app-ai-chat, @molecule/app-ai-models, @molecule/app-ai-voice, @molecule/app-code-editor, @molecule/app-country-flags, @molecule/app-i18n, @molecule/app-icons, @molecule/app-ide, @molecule/app-live-preview, @molecule/app-logger, @molecule/app-react, @molecule/app-storage, @molecule/app-ui, @molecule/app-ui-react

Reference

@molecule/app-ide-react — React components for an AI-powered IDE workspace: WorkspaceLayout (resizable panel row), ChatPanel (streaming AI chat with tool-call cards, @ file mentions, / commands), EditorPanel (tabbed Monaco editor), PreviewPanel (live-preview iframe with device frames + crash/blank recovery), FileExplorer, CommandPalette, QuickOpen, TabBar, plus registerCustomEventCard() for app-specific chat cards and useKeyboardShortcuts().

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { ChatPanel, EditorPanel, PreviewPanel, WorkspaceLayout } from '@molecule/app-ide-react'

;<WorkspaceLayout>
  <ChatPanel
    projectId="proj_abc123"
    onFileOpen={(path) => console.log('open', path)}
    onFileChange={(path, content) => console.log('changed', path, content.length)}
    onReadyToBuild={() => console.log('boot sandbox')}
  />
  <EditorPanel
    onActiveFileChange={(path) => console.log('active', path)}
    onFixWithAI={(req) => console.log('fix', req)}
  />
  <PreviewPanel onPreviewError={(errs) => console.error(errs)} />
</WorkspaceLayout>

Type

feature

Installation

npm install @molecule/app-ide-react @molecule/app-ai-chat @molecule/app-ai-models @molecule/app-ai-voice @molecule/app-code-editor @molecule/app-country-flags @molecule/app-i18n @molecule/app-icons @molecule/app-ide @molecule/app-live-preview @molecule/app-logger @molecule/app-react @molecule/app-storage @molecule/app-ui @molecule/app-ui-react material-file-icons react react-dom
npm install -D @types/react

API

Interfaces

CommandCategory

A command category with its display label.

interface CommandCategory {
  /** Stable category key referenced by `CommandDef.category`. */
  key: CommandCategoryKey
  /** Human-readable category heading (English default; wrapped in `t()` at render). */
  label: string
}

CommandDef

Metadata describing a single slash command.

interface CommandDef {
  /** Command id (the part after the slash, e.g. `'help'`). */
  id: string
  /** Display label including the leading slash (e.g. `'/help'`). */
  label: string
  /**
   * Short description shown in the menu and in `/help` (English default). May
   * contain the `{{agentName}}` interpolation token, filled in by the render
   * sites (command menu, `/help`, `/settings` card) from the host's agent
   * identity (neutral default: "the assistant").
   */
  description: string
  /** Category this command is grouped under. */
  category: CommandCategoryKey
  /**
   * Argument syntax for commands that take options, shown in the `/settings`
   * command reference (English default). `[…]` = optional, `<…>` = required.
   * Omit for commands that take no arguments.
   */
  usage?: string
  /**
   * Alternate short forms (WITHOUT the slash) that also invoke this command —
   * e.g. `['t']` lets `/t <message>` invoke `/teamsay`. Matched by the chat
   * input's command dispatch alongside `id`.
   */
  aliases?: string[]
  /**
   * True for a human-only side-channel command handled by the HOST's server
   * (e.g. a team-chat note the agent never sees): the raw `/command` text is
   * sent to the backend, but NO optimistic user bubble is rendered — the server
   * emits the canonical `message` stream event (see the `ChatStreamEvent`
   * union), which IS the visible message, persisted and fanned out live to
   * every project member.
   */
  sideChannel?: boolean
  /**
   * True for commands a read-only project VIEWER may run — the ones that only
   * read, open a view-only surface, or set a per-user/per-device preference
   * (`/cost`, `/settings`, `/help`, `/mic`, …). Everything else writes shared
   * project state or triggers a Synthase turn (which the server 403s for a
   * viewer), so it is **default-denied** for viewers: omitting this flag means a
   * viewer cannot run the command, which is the safe default for any command
   * added later. Host side-channel commands (`sideChannel`, e.g. `/teamsay`) are
   * team communication, not project mutation — they set this so viewers can talk.
   */
  viewerSafe?: boolean
}

CommandGroup

A category paired with the commands that belong to it.

interface CommandGroup {
  /** The category metadata (key + label). */
  category: CommandCategory
  /** Commands in this category, in registry order. */
  commands: CommandDef[]
}

ActivityCardProps

Props for the inline ActivityCard.

interface ActivityCardProps {
  /** The captured activity to render. */
  activity: Activity
  /** Called when the card is clicked — should open the Activity panel filtered to this activity. */
  onActivityClick?: (activity: Activity) => void
}

ChatTimestampProps

Props for ChatTimestamp.

interface ChatTimestampProps {
  /** Epoch milliseconds of the message or event. */
  timestamp: number
  /**
   * `inline` sits inside an existing header row (the user message's name line);
   * `line` is its own row above a reply or card, aligned with that item's content.
   */
  variant?: 'inline' | 'line'
  /** Horizontal alignment for the `line` variant (centered notices center their time). */
  align?: 'left' | 'center'
}

IconProps

Props for Icon. Extends SVGProps so callers can forward any SVG/HTML attribute (data-mol-id, aria-*, role, event handlers, style) to the root <svg> without the component enumerating them.

interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'width' | 'height' | 'viewBox' | 'fill'> {
  /** Name of the glyph to look up in the bonded icon set (e.g. `'sync'`). */
  name: IconName
  /** Width and height of the rendered SVG in pixels. Defaults to 16. */
  size?: number
  /** Class name forwarded to the root `<svg>`. */
  className?: string
}

TestsCardProps

Props for TestsCard.

interface TestsCardProps {
  /** Every discovered test (unfiltered). */
  tests: TestItem[]
  /** How the listing went. */
  status: TestsStatus
  /** The run this card is showing — owned by the panel, so it survives a close. */
  run: TestsRunState
  /** Seeds the search box from `/test <query>`. */
  initialQuery: string
  /** Whether this viewer may run tests at all (a viewer may not). */
  canRun: boolean
  /** Runs a selection. */
  onRun: (selection: TestSelection) => void
  /** Stops the run in flight — the whole run, every remaining file. */
  onCancel: () => void
  /**
   * Skips the command the run is on right now, WITHOUT ending it: that file
   * comes back `Skipped` and the run moves to the next one. Omitted by a host
   * that does not serve skipping — then no Skip control is rendered.
   */
  onSkipCurrent?: () => void
  /**
   * Hands the failing tests to the agent as ONE chat message — a real turn it
   * answers, exactly like the editor’s “Fix with AI”.
   */
  onFix: (failures: TestFailure[]) => void
  /**
   * Why fixing is unavailable (a viewer, a turn already streaming), already
   * translated by the host — `null` when it is available. The card states the
   * reason on the disabled button rather than letting a click do nothing.
   */
  fixDisabledReason: string | null
  /** Light theme (drives the same row border + field inset the sibling cards use). */
  isLight: boolean
  /** Chrome-less inside the command overlay; full card chrome in the timeline. */
  embedded?: boolean
}

UserAvatarProps

Props for UserAvatar.

interface UserAvatarProps {
  /**
   * The signed-in user's avatar — an inline `data:image/*` URI or an `http(s)`
   * URL from their profile metadata. Unsafe / unset / oversized values are
   * ignored (see `resolveUserAvatar`) and the generic icon is shown.
   */
  userAvatar?: string | null
  /**
   * The message author's display name, for a TEAMMATE's message. When set and
   * there is no renderable avatar, the fallback tile shows the name's initial
   * (e.g. "T" for Test) instead of the generic `user` glyph — so a teammate
   * without a profile picture is still identifiable at a glance and is NEVER
   * shown with the viewer's own avatar. Also used as the accessible label /
   * image alt in place of the solo "You".
   */
  name?: string
  /** Diameter of the avatar in pixels. Defaults to 24. */
  size?: number
  /**
   * Optional click handler. When supplied, the avatar becomes an interactive
   * button (pointer cursor, hover/focus ring, keyboard- and screen-reader
   * accessible) that opens the user's profile — the host decides what to show.
   * When omitted (the default) the avatar renders exactly as before: a static,
   * non-interactive image/icon, so existing call sites are unaffected.
   */
  onClick?: () => void
}

Activity

A single captured activity. Mirrors the SSE activity.activity payload; the REST list endpoint additionally returns payload and result for the expanded detail view.

interface Activity {
  id: string
  type: ActivityType
  status: ActivityStatus
  recipient?: string
  summary?: string
  /** ISO 8601 timestamp. */
  timestamp: string
  /** Full captured payload — only present on the REST detail response (dev only). */
  payload?: unknown
  /** Provider result / synthetic success record — only present on the REST detail response. */
  result?: unknown
}

AutoCommitState

The countdown's state.

intervalSeconds is the configured cadence (0 = disabled). remaining is the live count: a positive number while counting down, 0 at the instant a commit is due, and null while disabled or paused (after a commit, awaiting the next file change to re-arm).

interface AutoCommitState {
  /** Configured countdown length in seconds; `0` when auto-commit is off. */
  intervalSeconds: number
  /** Seconds left until the next auto-commit; `null` when disabled or paused. */
  remaining: number | null
}

ReportFormState

The report modal's form state.

interface ReportFormState {
  /** Short summary / issue title. */
  title: string
  /** Detailed description of the problem or request. */
  description: string
  /** Optional reproduction steps (free text). */
  steps: string
  /** Whether to attach the recent conversation to the report. */
  includeChat: boolean
}

ClientInfo

Client-side diagnostics attached to a report so triage can see the running environment without asking the user. Every field is optional — only what could be read in the current environment is present (see collectClientInfo).

interface ClientInfo {
  /** The running build version. */
  appVersion?: string
  /** `navigator.userAgent` (browser + OS). */
  userAgent?: string
  /** `navigator.platform`. */
  platform?: string
  /** `navigator.language`. */
  language?: string
  /** Inner viewport size, `${innerWidth}×${innerHeight}`. */
  viewport?: string
  /** Physical screen size, `${screen.width}×${screen.height}`. */
  screen?: string
  /** Active theme — `'light'` or `'dark'`. */
  theme?: string
  /** The current page URL (`window.location.href`). */
  url?: string
}

ReportPayload

The POST /projects/:id/report request body.

interface ReportPayload {
  /** Short summary / issue title. */
  title: string
  /** Detailed description. */
  description: string
  /** Reproduction steps — omitted entirely when blank. */
  steps?: string
  /** Whether the backend should attach the recent conversation. */
  includeChat: boolean
  /** Client diagnostics — omitted entirely when none could be collected. */
  clientInfo?: ClientInfo
}

ReportResult

The POST /projects/:id/report response.

interface ReportResult {
  /** Whether the report was recorded. */
  ok: boolean
  /** Link to the created issue, when one was filed. */
  url?: string
  /** The persisted DB row id. */
  id?: string
}

SharePayload

The POST /projects/:projectId/shares request body.

interface SharePayload {
  /** Role granted to anyone who opens the link. */
  role: ShareRole
}

ShareLinkResult

The POST /projects/:projectId/shares response — the created public link. Mirrors the relevant fields of the @molecule/api-resource-share ShareLink.

interface ShareLinkResult {
  /** The link's unique id (used by the revoke route). */
  id?: string
  /** Opaque slug embedded in the public URL. */
  slug: string
  /** The role this link grants. */
  role: ShareRole
  /**
   * A fully-qualified share URL, when the backend supplies one. Preferred over
   * client-side construction so the canonical origin (e.g. a custom domain)
   * wins over the current page origin.
   */
  url?: string
  /** When the link expires, if ever (ISO 8601); absent/`null` = no expiry. */
  expiresAt?: string | null
  /** When the link was revoked (ISO 8601); absent/`null` = still active. */
  revokedAt?: string | null
  /** When the link was created (ISO 8601). */
  createdAt?: string
}

ChatTimestampSlot

A timeline item that can carry a timestamp, in render order.

interface ChatTimestampSlot {
  /** The timeline item's id. */
  id: string
  /** Epoch milliseconds of the item. */
  timestamp: number
  /**
   * Shown even with timestamps turned off — the user's own message headers (the
   * time already fits in the header row) and critical events (a failure, a
   * blocked connection, a limit that stopped work).
   */
  alwaysShown: boolean
}

DeviceDimensions

Per-frame iframe sizing. width/height are the PORTRAIT CSS sizes; a fixed-frame (rotatable) device swaps them in landscape. '100%' width with a null height means "fluid" — fill the available preview area (responsive / desktop have no fixed frame to rotate).

interface DeviceDimensions {
  /** Portrait CSS width (e.g. `'768px'`, or `'100%'` for a fluid frame). */
  readonly width: string
  /** Portrait CSS height in px (e.g. `'1024px'`), or `null` to fill the area. */
  readonly height: string | null
  /** Whether the frame has a fixed size that can be rotated portrait ⇄ landscape. */
  readonly rotatable: boolean
}

TestCaseRef

The one test a file is on right now, named the way its runner names it.

interface TestCaseRef {
  /** The runner's test title. */
  title: string
  /** Its enclosing group (`describe`), when it has one. */
  describe?: string
}

TestCaseProgress

What one FILE is doing while it runs: which test is on screen, and how its own tests have gone so far. Discarded the moment the file reports a result — the row's verdict pill takes over from there.

interface TestCaseProgress {
  /** The test running right now, or `null` between tests. */
  current: TestCaseRef | null
  passed: number
  failed: number
  skipped: number
}

TestGroup

One rendered group of rows: a kind within a project directory.

interface TestGroup {
  kind: TestKind
  workspace: TestWorkspace
  /** The directory's name for the heading; `null` for the workspace root. */
  label: string | null
  items: TestItem[]
}

TestResultEntry

What one test file ended as in the last run.

interface TestResultEntry {
  status: TestStatus
  durationMs?: number
  passed: number
  failed: number
  skipped: number
  /** The runner's output, kept for a FAILURE so the row can keep showing it. */
  output?: string
}

TestsRunState

Everything the card knows about the run it is showing.

interface TestsRunState {
  runId: string | null
  running: boolean
  /** The ids this run covers, in run order. */
  queued: string[]
  /** The id whose output is streaming right now, when the host says which. */
  currentId: string | null
  /** The live output lines, newest last, capped at `MAX_OUTPUT_LINES`. */
  output: string[]
  /** Per-id outcome from this run (and from earlier runs, until re-run). */
  results: Record<string, TestResultEntry>
  /**
   * Per-id LIVE per-test progress, for the files that are running right now.
   * An entry exists only between a file's first `case` event and its `result`.
   */
  cases: Record<string, TestCaseProgress>
  /** A skip was asked for and the run has not answered it yet. */
  skipPending: boolean
  /**
   * Skipping is not on offer: the host wired no `skipCurrent`, or it answered
   * that there was nothing left to skip. The control is dropped — never
   * replaced by an error the person cannot act on.
   */
  skipUnavailable: boolean
  /** How the run ended, once it has. */
  outcome: TestRunOutcome | null
  /** A run-level failure message (timeout, transport error) shown in the card. */
  error: string | null
  startedAt: number | null
  durationMs: number | null
}

TestFailure

One failing test and the output that explains it.

interface TestFailure {
  item: TestItem
  output?: string | undefined
}

ChatEventCardAction

A single call-to-action on a chat card: a labelled link (href) and/or click handler. The app supplies any route/copy — the shared package never hardcodes one.

interface ChatEventCardAction {
  /** Button label (already localized by the app). */
  label: string
  /** Link target. App-owned — e.g. the host's own pricing/auth route. */
  href?: string
  /** Click handler (alternative to, or alongside, `href`). */
  onClick?: () => void
  /**
   * Render the action's label as inline monospace code — a command/identifier like
   * `/report` or a skill name — so it stands out from prose while staying clickable.
   */
  code?: boolean
  /**
   * Semantic design-system button color. When set, the card renders this action
   * as a real `cm.button` (the ClassMap's standard tinted button, same as every
   * other button in the app) in this color, so a CTA looks identical wherever it
   * appears — e.g. an app can keep "Sign up" `primary` and "Log in" `success`
   * across its auth page, banners, and chat cards. When omitted, the card's
   * legacy accent-outline treatment applies. The app owns the semantics; the
   * shared package just passes the color through to the ClassMap.
   */
  color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'
}

ChatEventCardCode

A non-interactive inline monospace code span in a card body — a command or identifier the prose refers to (/report, a skill name) that should read as code but isn't clickable. For a clickable command, use ChatEventCardAction with code: true.

interface ChatEventCardCode {
  /** The code text, rendered monospaced/tinted. */
  code: string
}

ChatEventCard

A chat system card: a short message with an optional action (or actions). Mirrors the system-card shape ChatPanel renders for upgrade prompts, guest reminders, etc.

interface ChatEventCard {
  /** The card's text. */
  text: string
  /** An optional action button (or buttons): a link (`href`) and/or a click handler. */
  action?: ChatEventCardAction | ChatEventCardAction[]
  /**
   * Composable inline body for a `tone` (tip) card: an ordered list of segments rendered
   * in sequence — plain strings as text, `ChatEventCardAction`s as inline underlined
   * links — so prose and links interleave freely (e.g. text → an inline link → a trailing
   * period). When set, the renderer uses this INSTEAD of `text` + appended `action`s, so a
   * link can sit mid-sentence rather than only at the end. Segments carry their own spacing
   * (no auto-space is inserted between them). Keep `text` populated with a plain-text
   * equivalent for accessibility / non-toned consumers. Only honored when `tone` is set.
   */
  content?: ChatEventCardSegment[]
  /**
   * When true, ChatPanel renders the card as a stand-out tip box rather than muted inline
   * text. Prefer setting `ChatEventCard.tone` (which implies emphasis AND picks the
   * accent colour + icon); `emphasized` without a `tone` falls back to the neutral `info`
   * tone. The app opts in; the shared package never infers emphasis from a card's copy.
   */
  emphasized?: boolean
  /**
   * The card's tip TONE — picks its accent colour + default icon so every notice card
   * shares ONE consistent box (icon + tinted body + a uniform 1px border + actions),
   * differing only by colour/icon per kind:
   * - `info` — blue, info glyph (neutral notice)
   * - `gold` — amber, lightbulb (an honest tip / onboarding note)
   * - `upgrade` — amber, clock (a plan/limit/budget nudge)
   * - `success` — green, check (a completed action, e.g. a saved script)
   * - `signup` — primary, sign-in (an auth nudge)
   *
   * Setting `tone` implies emphasis. Cards that supply composable `ChatEventCard.content`
   * render their inline links in the box; cards that supply `action`(s) render them as a
   * consistent row of accent buttons. Omit `tone` (and `emphasized`) for a plain muted line.
   */
  tone?: 'info' | 'gold' | 'upgrade' | 'success' | 'signup'
  /**
   * Optional icon-name override (a `@molecule/app-icons` glyph) — defaults to the tone's
   * icon. Use only a name that exists in the bonded set (`getIcon` throws otherwise);
   * sets with extra glyphs register them via `CustomIconNames` augmentation.
   */
  icon?: IconName
  /**
   * The limit this card already explains, named by the same `limitType` the backend puts
   * on its limit errors (e.g. `'ai_cost'`). A limit is hit ONCE but can surface twice —
   * as this persisted card (recorded when the turn was interrupted) and again as the live
   * limit banner when the NEXT send is refused — which reads as two cards saying the same
   * thing. When a live error carries the same `limitType`, ChatPanel hides this card for
   * as long as that banner is up, so exactly one surface states the limit; the card
   * returns as soon as the error clears. The banner is never the one suppressed: it is
   * the only feedback the refused send gets, and its message can be more specific than
   * the card's (a platform-capacity refusal shares `limitType` with a personal-budget
   * one). The app owns the identifier; the shared package only matches it.
   */
  coversLimitType?: string
  /**
   * Marks the card as a critical event — something failed, was blocked, or stopped
   * the work (a refused deploy, a blocked outbound connection). A critical card
   * keeps its timestamp even while the viewer has timestamps turned off. The app
   * decides; the shared package never infers criticality from a card's tone or copy.
   */
  critical?: boolean
}

SettingMeta

Canonical, value-free metadata for a single user-controllable setting.

interface SettingMeta {
  /** Stable id (also the i18n key suffix, e.g. `'effort'`). */
  id: SettingKey
  /** Human-readable label (English default; wrapped in `t()` at render). */
  label: string
  /**
   * One-line explanation of what the setting does (English default). May
   * contain the `{{agentName}}` interpolation token, filled in at render from
   * the host's agent identity (neutral default: "the assistant").
   */
  description: string
  /**
   * The slash command that edits this setting client-side. Drives the inline
   * "Edit" affordance and cross-links the setting to its command. Omitted only
   * for read-only settings.
   */
  editCommand?: CommandId
  /**
   * The exact slash-command input to prefill when editing, for settings whose
   * bare `SettingMeta.editCommand` is not specific enough — e.g. the
   * per-mode model rows both run the `model` command but must scope it to a
   * mode (`/model --plan`, `/model --execute`). Omit when running the bare
   * command suffices.
   */
  editInput?: string
}

WorkspaceLayoutProps

Properties for workspace layout.

interface WorkspaceLayoutProps {
  children: ReactNode
  className?: string
}

IdeClientAction

A non-mutating UI action the AI agent asks the IDE to perform — reload or navigate the live preview, open a file in the editor, or drive the preview's interaction bridge (preview_ui). Delivered via the client_action chat-stream event (and, for preview_ui, also via the host's collab socket so a mid-build tab reload can't orphan it).

interface IdeClientAction {
  action: 'reload_preview' | 'navigate_preview' | 'open_file' | 'preview_ui'
  /** navigate_preview: a URL path (e.g. "/dashboard"). open_file: a file path. */
  path?: string
  /** preview_ui: correlates the command with its ui-result round-trip. */
  requestId?: string
  /** preview_ui: the interaction the preview bridge should perform. */
  command?: 'snapshot' | 'click' | 'fill' | 'select' | 'waitFor'
  /** preview_ui: the `data-mol-id` of the target element (preferred). */
  molId?: string
  /** preview_ui: CSS-selector fallback when no molId is available. */
  selector?: string
  /** preview_ui: visible-label match for apps whose elements carry no molId. */
  text?: string
  /** preview_ui: value to set for fill/select. */
  value?: string
}

ChatUserIdentity

Identity of the user whose avatar or name was clicked in the chat timeline, passed to ChatPanelProps.onProfileClick so the host can open that user's profile.

The identity is always the clicked MESSAGE AUTHOR's — a teammate's click carries their persisted author fields, never the viewer's. An author-less message (the local optimistic echo, legacy solo rows) is the signed-in user's own, so it carries ChatPanelProps.currentUserId + the viewer's avatar. The host compares id against the signed-in user to pick the surface: own → editable profile, someone else → view-only. A missing id means "the signed-in user" (author-less rows in a host that passes no currentUserId).

interface ChatUserIdentity {
  /** The clicked user's stable id (a persisted `author.id`), when known. */
  id?: string
  /** The clicked user's display name (a persisted `author.name`), when known. */
  name?: string
  /** The clicked user's avatar value (data-URI / URL), if any. */
  avatar?: string | null
}

TestItem

One test file the host discovered in the project.

interface TestItem {
  /** Stable id, unique across workspaces — used as the row key and to select by. */
  id: string
  /** Path relative to its workspace, e.g. `e2e/home.spec.ts`. */
  file: string
  kind: TestKind
  workspace: TestWorkspace
  /**
   * The group's project name, ready to render: the same path as `workspace`,
   * or `null` for the workspace root, which has no name of its own. Hosts that
   * omit it get the path itself (and the translated "Project" for the root).
   */
  workspaceLabel?: string | null
  /** Human label for the row; the bar falls back to the file path without one. */
  title?: string
}

TestRunners

Which runner drives each kind in one workspace (null = none installed).

interface TestRunners {
  e2e: string | null
  unit: string | null
}

TestList

What ChatPanelProps.listTests resolves with.

interface TestList {
  tests: TestItem[]
  /**
   * Per project directory (keyed like `TestItem.workspace`), the runners
   * the host found. Only directories that hold a listed test appear.
   */
  runners: Record<string, TestRunners>
}

TestSelection

What the bar asks the host to run.

interface TestSelection {
  /** Specific `TestItem.id`s. Takes precedence over `kind`. */
  ids?: string[]
  /** Everything of this kind when no `ids` are given. */
  kind?: TestKind | 'all'
}

TestRunHandle

Handle to a run in flight, so the bar can stop it.

interface TestRunHandle {
  /** Stop the run — the host aborts its stream, which cancels the work. */
  cancel(): void
  /**
   * Skip the command the run is on right now WITHOUT ending the run: the files
   * that command owned come back as `result`s with `status: 'skipped'`, and the
   * run moves to the next command.
   *
   * Optional, because a host may not serve it. Resolving `false` means there
   * was nothing to skip (the run had already moved on) — a benign race the card
   * answers by dropping the control, never by showing an error.
   */
  skipCurrent?(): void | Promise<boolean | void>
}

ChatPanelProps

Props for the ChatPanel component — the IDE chat surface plus the callbacks the host app uses to react to AI activity (file changes, boot, client actions, etc.).

interface ChatPanelProps {
  projectId: string
  endpoint?: string
  /** If provided, auto-send this message once on mount (e.g., prompt from landing page). */
  initialMessage?: string
  /** Called after the initial message has been sent — used to clear router state. */
  onInitialMessageSent?: () => void
  /** Called to open a file as a preview tab. `opts.focus === false` opens it quietly (no pane switch) — e.g. a saved plan or a system-initiated open while the user is busy. */
  onFileOpen?: (path: string, opts?: { focus?: boolean }) => void
  /** Called when a filename in a tool call is double-clicked — should pin the tab. */
  onFileDoubleClick?: (path: string) => void
  /** Called when a file in the uncommitted list is clicked for diff view. */
  onFileDiff?: (path: string, diff?: { original: string; modified: string }) => void
  /** Called to undo/redo a file change — writes the given content to the file path. */
  onFileRevert?: (path: string, content: string) => Promise<void>
  /** Called when the AI creates or modifies a file — should refresh the editor if the file is open. */
  onFileChange?: (path: string, content: string) => void
  /** Called when a file is removed from disk (e.g. reverting an untracked file). */
  onFileDeleted?: (path: string) => void
  /** Called after a successful commit — should refresh file explorer git status. */
  onCommit?: () => void
  /** Called when an inline activity card is clicked — should open the Activity panel filtered to this activity. */
  onActivityClick?: (activity: ActivityFromCard) => void
  /**
   * Reports a chat timeline item that threw during render, caught by that item's
   * error boundary. The item degrades to an inline notice either way; this is how the
   * host gets the crash into its telemetry instead of it being visible only to the
   * one user who hit it.
   */
  onRenderError?: (error: Error, info: ErrorInfo) => void
  /**
   * Called when a user avatar or author name in the chat timeline is clicked —
   * the host opens THAT user's profile (e.g. molecule.dev's profile modal).
   * Receives the clicked author's `ChatUserIdentity`: a teammate's click
   * carries their persisted author fields, the signed-in user's own click
   * carries their own — the host compares `id` against the signed-in user to
   * decide editable-own vs view-only-teammate. Omit it (the default) to render
   * the avatars/names non-interactive (static, exactly as before). Only human
   * authors are clickable — the molecule glyph on auto-sent messages is not.
   */
  onProfileClick?: (user: ChatUserIdentity) => void
  /**
   * The signed-in user's id (a message's persisted `author.id`). Used to stamp
   * the identity handed to `onProfileClick` for AUTHOR-LESS messages (the
   * local optimistic echo, legacy solo rows), which are always the signed-in
   * user's own. Omit in a solo host — those clicks then carry no `id`, which
   * hosts treat as "the signed-in user".
   */
  currentUserId?: string
  /** Called when the server signals (via the `ready_to_build` stream event) that discovery is complete and the sandbox should boot. */
  onReadyToBuild?: () => void
  /**
   * True while the plan has finished streaming but the sandbox is still booting
   * (after `ready_to_build`, before the post-boot build kickoff). When set and no
   * message is actively streaming, the chat shows a "waiting for the development
   * environment" indicator so the conversation doesn't appear to silently stall.
   */
  awaitingSandboxBoot?: boolean
  /** Called when the agent requests a UI action via the `client_action` stream event (reload/navigate the preview, open a file). */
  onClientAction?: (action: IdeClientAction) => void
  /** Called on each stream `done` — host uses it to keep the boot view up until the parallel during-boot plan stream finishes. */
  onTurnComplete?: () => void
  /**
   * Called whenever the chat's loading state changes — true when a turn (plan or build) is in
   * progress, false when idle. The host uses this as the authoritative "the agent is actively
   * building" signal to drive the preview's "Building your app…" overlay, so a half-built /
   * blank preview during a long build always shows progress instead of a bare white screen.
   */
  onLoadingChange?: (loading: boolean) => void
  /**
   * Navigates the live preview to a route path. Wired so a `[label](/route)` markdown link in
   * an assistant message (e.g. the agent's "your app is ready" handoff) jumps the preview to
   * that page on click. User-initiated, so the host should navigate unconditionally (it is not
   * the rate-limited agent `navigate_preview` action).
   */
  onNavigatePreview?: (path: string) => void
  /**
   * Called on mount with a handler the parent invokes to deliver a broadcast chat event
   * from another project member (the push channel); called with null on unmount.
   */
  onRegisterPushHandler?: (
    handler: ((conversationId: string, event: ChatStreamEvent) => void) | null,
  ) => void
  /**
   * Called on mount with a function that reloads chat history and converges the
   * panel on the persisted transcript; called with null on unmount. The host
   * should invoke it whenever its chat push channel (re)connects: a broadcast
   * sent while that socket was down (a teammate's team note against a
   * backgrounded tab or a slept laptop) is otherwise lost until a page
   * lifecycle event happens to fire. Cheap when nothing changed — an identical
   * transcript is never re-applied.
   */
  onRegisterHistoryReconcile?: (reconcile: (() => Promise<void>) | null) => void
  /** Changing this value submits the current input draft — used to send a prefilled prompt after the prompt→chat morph docks. */
  autoSubmitSignal?: number
  /** Seeds the input with this text on mount (prompt→chat morph), so the chat input shows the prompt before it is sent. */
  initialInputValue?: string
  /** Hide the conversation-selector header (e.g. during discovery, before any history is worth showing). */
  hideConversationMenu?: boolean
  /**
   * Whether to render the built-in conversation header (the picker + searchable
   * history dropdown, the share / bug-report / settings buttons, and the
   * new-chat "+"). Defaults to `true` (the package owns that chrome). Pass
   * `false` to operate **headless** — the host renders those controls itself
   * (e.g. molecule.dev's Workspace top bar) and drives the chat through the
   * controlled props below: `ChatPanelProps.conversationId` /
   * `ChatPanelProps.chatKey` / `ChatPanelProps.onConversationId` for
   * the conversation, and `ChatPanelProps.openShareSignal` /
   * `ChatPanelProps.openReportSignal` to open the in-chat modals.
   */
  renderConversationHeader?: boolean
  /**
   * Host-controlled active conversation id (headless mode). Drives the chat
   * endpoint's `?conversationId=`. When `undefined` (the default) the panel owns
   * the active conversation internally (localStorage-backed). `null` is a valid
   * controlled value meaning "no conversation yet".
   */
  conversationId?: string | null
  /**
   * Host-controlled remount key for the inner chat (headless mode). Changing it
   * remounts the conversation timeline (a new chat or a switch); the backend
   * assigning an id mid-stream must NOT change it (that would drop in-flight
   * messages). Falls back to the internal key when omitted.
   */
  chatKey?: string
  /**
   * Called whenever the active conversation id changes — the backend assigns one
   * mid-stream and the host needs it to keep its own picker in sync WITHOUT
   * remounting (do not change `ChatPanelProps.chatKey` in response).
   */
  onConversationId?: (id: string | null) => void
  /** Changing this opens the in-chat `/share` modal (host-driven, e.g. a top-bar share button). Overrides the built-in header's share button signal. */
  openShareSignal?: number
// … truncated — the full declaration is in node_modules/@molecule/app-ide-react/dist/types.d.ts

EditorPanelProps

Properties for the editor panel component.

interface EditorPanelProps {
  className?: string
  /**
   * Render the editor read-only (Monaco `readOnly`) — for read-only project
   * VIEWERS, whose file writes the server rejects anyway. Typing is blocked at
   * the editor, so a viewer never composes an edit that cannot be saved.
   */
  readOnly?: boolean
  /** Called whenever the active file changes (tab switch, file open, file close). */
  onActiveFileChange?: (path: string | null) => void
  /** Called once after the editor is fully mounted and ready to accept files. */
  onEditorReady?: () => void
  /** Called whenever the open tab list changes (file opened or closed). */
  onTabsChange?: (paths: string[]) => void
  /** Maps file path to git status for coloring tab filenames. */
  fileStatuses?: Record<string, string>
  /** Path of the file currently being formatted, for visual indicator. */
  formattingFile?: string | null
  /** Path of the file with an active save debounce countdown. */
  countdownFile?: string | null
  /** Incremented each keystroke to restart the countdown animation. */
  countdownKey?: number
  /** Estimated format duration in ms (rolling average, default 2000). */
  formatEstimate?: number
  /** Called when the user triggers "Fix with AI" from the editor's lightbulb or context menu. */
  onFixWithAI?: (request: FixWithAIRequest) => void
  /** Override double-click on a tab. Return `true` to skip the default pin behavior. */
  onTabDoubleClick?: (path: string) => boolean
}

PreviewStuckReport

Structured report passed to PreviewPanelProps.onPreviewStuck when the preview gives up. Carries the failure class + the route it happened on so the host can compose an actionable, agent-fixable message instead of a bare "preview is stuck".

interface PreviewStuckReport {
  /** The failure class — what left the preview unable to show the running app. */
  reason: PreviewStuckReason
  /** The preview's current location (route) when the failure was detected, if known. */
  url?: string
}

PreviewUiCommand

A live-preview interaction the host asks the panel to perform inside the iframe, so an AI agent can verify a feature end-to-end by DRIVING the app the user is watching (no headless browser). The panel just relays it to the iframe's interaction bridge — generic, so it carries no host/API specifics.

interface PreviewUiCommand {
  /** Correlates this command with its result; the host round-trips on it. */
  id: string
  /** `snapshot` the interactive UI, or act on an element. */
  action: 'snapshot' | 'click' | 'fill' | 'select' | 'waitFor'
  /** `data-mol-id` of the target element (preferred over selector). */
  molId?: string
  /** CSS-selector fallback when no molId is available. */
  selector?: string
  /** Visible-label match — targets apps whose elements carry no `data-mol-id`. */
  text?: string
  /** Value to set for `fill` / `select`. */
  value?: string
  /**
   * `snapshot` only — the moment the host pointed the preview at a new URL (`Date.now()`).
   * Only a document that loaded at or after it may answer, so a navigation snapshot can never
   * come from the OUTGOING page still sitting in the iframe. Set it ONLY when a new document
   * is genuinely loading (the URL actually changed) — otherwise nothing can satisfy it and the
   * command goes unanswered. Omit for a plain read.
   */
  minLoadedAt?: number
}

PreviewUiResult

The preview interaction bridge's reply to a PreviewUiCommand.

interface PreviewUiResult {
  ok: boolean
  /** Interactive-element list + url/title from the preview (present on a snapshot / success). */
  snapshot?: unknown
  found?: boolean
  error?: string
  /**
   * Failed network requests from the last ~10s (method, url, status, bounded response body),
   * captured in-page — so a click that 4xx'd explains itself in the same result.
   */
  recentNetworkErrors?: string[]
  /**
   * Present when the bridge gave up on a settle budget instead of observing the page settle —
   * it names what was still pending (document parsing, an empty root, an unreached route,
   * in-flight requests). The snapshot may be incomplete, so a race stays distinguishable from
   * a genuinely broken page.
   */
  stillSettling?: string
}

PreviewPanelProps

Props for the PreviewPanel — the live app preview (iframe + device frame + URL bar).

interface PreviewPanelProps {
  /** Custom loading indicator shown while the dev server is starting. */
  loadingIndicator?: ReactNode
  /**
   * The current UI command the host wants performed in the preview iframe (AI-driven
   * end-to-end verification). The panel posts it to the iframe's interaction bridge when it
   * CHANGES (keyed on `id`, so each new command fires exactly once). The panel only relays it;
   * the host owns what to send and what to do with the result.
   */
  uiCommand?: PreviewUiCommand | null
  /** Called when the iframe replies to a `PreviewUiCommand`, keyed by the command `id`. */
  onUiResult?: (id: string, result: PreviewUiResult) => void
  /** Custom loading indicator shown when the dev server restarts mid-session. Falls back to loadingIndicator if not provided. */
  restartingIndicator?: ReactNode
  /** Called when the preview iframe reports runtime JS errors. */
  onPreviewError?: (
    errors: Array<{ message: string; source?: string; line?: number; column?: number }>,
  ) => void
  /** Incremented when AI edits files. Triggers an iframe reload only when the preview is broken. */
  fileChangeTick?: number
  /**
   * Active-build hint (e.g. a basename like `GuestMenu.tsx`) the host sets while the
   * AI is editing files. When non-null the overlay is forced on — covering the
   * blank-white iframe reload a build triggers — and shows "Updating `<hint>`…" so
   * the user sees what's being worked on. Null when no build edit is in flight.
   */
  buildingHint?: string | null
  /**
   * Whether the AI agent is actively building right now (a chat turn is in progress).
   * The host derives this from the chat's loading state. While true, the preview keeps a
   * "Building your app…" status overlay up whenever the app has NOT confirmed it rendered
   * content (no `molecule:ready`) — so a half-built / blank / white iframe during a long
   * build always shows progress instead of a bare white screen. A confirmed render still
   * reveals the live app (HMR updates stay visible), so this never hides a working preview.
   */
  isBuilding?: boolean
  /**
   * Timestamp (ms since epoch) of when the preview's backing server/sandbox was last
   * woken from sleep or restarted, or 0/undefined when it never was. While this is
   * recent, the panel treats the preview like a fresh cold boot: the dev server behind
   * it is restarting and recompiling, so a document that reloads to blank (or a
   * transient error page that never runs the bridge) is EXPECTED for a while and must
   * NOT trip the fast "preview is blank" accusation — the honest starting/loading
   * status stays up, and only the generous never-rendered ceiling can accuse. A real
   * render (`molecule:ready`) clears the patience immediately, so a healthy wake
   * reveals as fast as ever.
   */
  wakeAt?: number
  /**
   * Called when the preview gives up showing the running app — after exhausting reload
   * recovery, at the absolute readiness ceiling, OR when the heartbeat watchdog detects a
   * frozen (locked-thread) app. Receives a `PreviewStuckReport` (failure class +
   * route) so the host can drive recovery UI AND hand the agent an actionable, targeted
   * fix request. The argument is optional for backward compatibility with no-arg callers.
   */
  onPreviewStuck?: (report?: PreviewStuckReport) => void
  /**
   * Escalation hook: restart the BACKING dev server. The panel calls this at most
   * once per broken episode (long cooldown, never during a build) when client-side
   * recovery is provably useless: the server answers probes, at least one automatic
   * reload already ran, and the app has still never confirmed a render for minutes.
   * Document reloads cannot heal that class — e.g. a poisoned immutable module cache
   * keyed to the dev server's version hash (observed live 2026-08-31); only a server
   * restart mints new module URLs. Return (or resolve) `false` to DECLINE (wrong
   * role, hidden tab, sandbox not running, a turn active) — the panel then burns no
   * budget and may ask again later; any other result counts as "restart initiated".
   * Omit the prop to disable escalation entirely (the default for generic hosts).
   */
  onRestartBackend?: () => Promise<boolean | void> | boolean | void
  /**
   * Called when the preview's render verdict changes (`PreviewRenderState`) — and
   * with the current location so the host can report WHERE. The host forwards this to the
   * server so Synthase's post-loop verification can confirm the app actually rendered (not
   * just that it compiled + served) before calling a build done.
   */
  onRenderState?: (state: PreviewRenderState, url?: string) => void
  className?: string
}

TabBarProps

Properties for tab bar.

interface TabBarProps {
  tabs: EditorTab[]
  activeFile: string | null
  onSelect: (path: string) => void
  onClose: (path: string) => void
  onDoubleClick?: (path: string) => void
  /** Maps file path to git status for coloring tab filenames. */
  fileStatuses?: Record<string, string>
  className?: string
}

FileExplorerProps

Properties for file explorer.

interface FileExplorerProps {
  /**
   * Read-only mode (a project viewer): the context menu omits every mutating
   * entry. Pair with omitting the mutation handlers (onRename/onDelete/…) so
   * keyboard shortcuts and drag-moves no-op too.
   */
  readOnly?: boolean
  files: FileNode[]
  onFileSelect: (path: string) => void
  onFileDoubleClick?: (path: string) => void
  onDirExpand?: (path: string) => void
  /** Called when the user chooses "Rename" from the context menu. */
  onRename?: (path: string) => void
  /** Called when the user chooses "Delete" from the context menu. */
  onDelete?: (path: string) => void
  /** Called when the user deletes multiple selected files/folders via context menu or keyboard. */
  onDeleteMultiple?: (paths: string[]) => void
  /** Called when the user moves files via drag-and-drop or cut+paste. */
  onMoveFiles?: (moves: Array<{ oldPath: string; newPath: string }>) => void
  /** Called when the user chooses "New File" from the context menu. */
  onNewFile?: (dirPath: string) => void
  /** Called when the user chooses "New Folder" from the context menu. */
  onNewFolder?: (dirPath: string) => void
  /** Called when the user chooses "Collapse All" from the context menu. */
  onCollapseAll?: () => void
  className?: string
  /** localStorage key for persisting expand/collapse state across reloads. */
  persistKey?: string
  /** Path of the currently active file — highlighted in the tree. */
  activeFile?: string | null
  /** Maps file path to git status — used to color directory names by highest-priority child status. */
  fileStatuses?: Record<string, string>
}

FileNode

File Node interface.

interface FileNode {
  name: string
  path: string
  type: 'file' | 'directory'
  children?: FileNode[]
  isDimmed?: boolean
  gitStatus?: 'modified' | 'added' | 'deleted' | 'untracked'
  /** If this entry is a symlink, the target it points to. */
  symlinkTarget?: string
}

ResizeHandleProps

Properties for resize handle.

interface ResizeHandleProps {
  onResize: (delta: number) => void
  direction?: 'horizontal' | 'vertical'
  className?: string
}

DeviceFrameSelectorProps

Properties for device frame selector.

interface DeviceFrameSelectorProps {
  current: DeviceFrame
  onChange: (device: DeviceFrame) => void
  className?: string
}

ToolCallCardProps

Properties for tool call card.

interface ToolCallCardProps {
  id: string
  name: string
  input?: unknown
  output?: unknown
  status: 'pending' | 'running' | 'done' | 'error'
  /** Snapshot of original/modified file content captured at tool-call time. */
  fileDiff?: { original: string; modified: string }
  /** Externally controlled undo state — when true, the card displays as undone. */
  isUndone?: boolean
  /** Called when the undo/redo button is toggled on this tool call. */
  onUndoToggle?: (id: string, undone: boolean) => void
  /** Called when a filename in the card is clicked — should open the file as a preview tab. */
  onFileOpen?: (path: string) => void
  /** Called when a filename in the card is double-clicked — should pin the tab. */
  onFileDoubleClick?: (path: string) => void
  /** Called when a file-changing card is clicked — should open the file diff in the editor. */
  onFileDiff?: (path: string, diff?: { original: string; modified: string }) => void
  /** Called to undo/redo a file change — writes the given content to the file path. */
  onFileRevert?: (path: string, content: string) => Promise<void>
  /** Called when the user responds to an `ask_user` tool call (clicks an option or submits free text). */
  onAskUserResponse?: (response: string) => void
  /**
   * Skip this tool call while it is RUNNING, without ending the turn: the call
   * comes back as skipped, the model is told it did not run, and the turn
   * carries on. Omitted by a host that does not serve it — then no control is
   * rendered at all. Resolving `false` means nothing was in flight (the call
   * had already finished), which returns the button to its resting state rather
   * than reporting an error.
   */
  onSkip?: (id: string) => void | Promise<boolean | void>
  /**
   * Why skipping is unavailable to THIS viewer, already translated by the host
   * — `null` when it is available. The control stays visible and disabled with
   * the reason on it, rather than vanishing without explanation.
   */
  skipDisabledReason?: string | null
  className?: string
}

ChatMessageItemProps

Properties for the chat message item component.

interface ChatMessageItemProps {
  message: ChatMessage
  className?: string
}

KeyboardShortcut

A keyboard shortcut definition.

interface KeyboardShortcut {
  /** Key combo string, e.g. `"mod+p"`, `"mod+shift+f"`. `mod` = Cmd (Mac) / Ctrl (others). */
  keys: string
  /** Handler invoked when the shortcut fires. */
  handler: () => void
  /** If true, fires even when an `<input>` / `<textarea>` is focused. */
  allowInInput?: boolean
  /** If true, fires even when the Monaco editor is focused. */
  allowInEditor?: boolean
  /** Human-readable label for display in the command palette. */
  label?: string
}

SidebarTabsProps

Properties for the sidebar tab switcher.

interface SidebarTabsProps {
  /** Currently active sidebar tab. */
  activeTab: 'files' | 'search'
  /** Called when the user switches tabs. */
  onTabChange: (tab: 'files' | 'search') => void
  /** Tab content rendered below the tab buttons. */
  children: ReactNode
  className?: string
}

SearchPanelProps

Properties for the search-in-files panel.

interface SearchPanelProps {
  /** Project ID used for API calls. */
  projectId: string
  /** Called when the user clicks a search result. */
  onResultClick?: (path: string, line: number) => void
  className?: string
  /**
   * The project's excluded directory names (VS Code `search.exclude`
   * semantics). Displayed and editable in the panel; the backend applies the
   * SAME set server-side to every search surface (panel + AI tools), so this
   * prop is display/edit state — searches don't send it per query. When
   * omitted, the panel shows `DEFAULT_SEARCH_EXCLUDED_DIRS`.
   */
  excludedDirs?: string[]
  /** Persist an edited excluded-dir set (the host owns storage). */
  onExcludedDirsChange?: (dirs: string[]) => void
  /**
   * Read-only mode (a project viewer): search stays fully usable, but the
   * Replace toggle/inputs/buttons are hidden (bulk writes are editor work).
   * Pair with omitting onExcludedDirsChange so exclude edits never persist.
   */
  readOnly?: boolean
}

SearchResult

A single file's search results.

interface SearchResult {
  /** Relative file path. */
  file: string
  /** Matching lines within the file. */
  matches: Array<{ line: number; content: string }>
}

SearchResponse

Response from the search API endpoint.

interface SearchResponse {
  /** The search pattern used. */
  pattern: string
  /** Grouped results by file. */
  results: SearchResult[]
  /** Total number of matches across all files. */
  totalCount: number
  /** Whether results were truncated. */
  truncated: boolean
}

QuickPickerItem

An item in the quick picker list.

interface QuickPickerItem {
  /** Unique identifier. */
  id: string
  /** Primary label. */
  label: string
  /** Secondary text shown beside the label. */
  detail?: string
  /** Optional icon element. */
  icon?: ReactNode
}

QuickPickerProps

Properties for the reusable quick picker overlay.

interface QuickPickerProps {
  /** Items to display and filter. */
  items: QuickPickerItem[]
  /** Placeholder text for the search input. */
  placeholder?: string
  /** Called when the user selects an item. */
  onSelect: (item: QuickPickerItem) => void
  /** Called when the user dismisses the picker (Escape or backdrop click). */
  onDismiss: () => void
  /** Show a loading indicator. */
  loading?: boolean
  /** Pre-fill the search input. */
  initialQuery?: string
  className?: string
}

QuickOpenProps

Properties for the quick-open file finder.

interface QuickOpenProps {
  /** Project ID used for API calls. */
  projectId: string
  /** Called when the user selects a file. */
  onFileOpen: (path: string) => void
  /** Called when the picker is dismissed. */
  onDismiss: () => void
}

Command

A command available in the command palette.

interface Command {
  /** Unique identifier. */
  id: string
  /** Display label. */
  label: string
  /** Keyboard shortcut hint (e.g. "Cmd+P"). */
  shortcut?: string
  /** Handler invoked when the command is executed. */
  execute: () => void
  /** Category prefix (e.g. "View", "File"). */
  category?: string
}

CommandPaletteProps

Properties for the command palette.

interface CommandPaletteProps {
  /** Available commands. */
  commands: Command[]
  /** Called when the palette is dismissed. */
  onDismiss: () => void
}

ShortcutEntry

A shortcut entry for display in the keyboard shortcuts panel.

interface ShortcutEntry {
  /** Human-readable label describing the action. */
  label: string
  /** Display string for the key combo (e.g. "⌘P", "⌘⇧F"). */
  keys: string
  /** Optional grouping category. */
  category?: string
  /** Handler invoked when the row is clicked. */
  execute?: () => void
}

KeyboardShortcutsPanelProps

Properties for the keyboard shortcuts reference panel.

interface KeyboardShortcutsPanelProps {
  /** List of shortcuts to display. */
  shortcuts: ShortcutEntry[]
  /** Called when the panel is dismissed. */
  onDismiss: () => void
}

Types

CommandCategoryKey

Category keys used to group commands in the menu and in /help.

type CommandCategoryKey = 'context' | 'code' | 'collaborate' | 'model' | 'settings' | 'support'

CommandId

Union of all command ids (loosely string, since CommandDef.id is a string).

type CommandId = CommandDef['id']

TestsStatus

Discovery status for the tests list — mirrors ScriptsCard's.

type TestsStatus = 'loading' | 'ready' | 'error' | 'unavailable'

ActivityType

Channel categories a captured activity can belong to.

type ActivityType = 'email' | 'sms' | 'push' | 'webhook' | 'channel'

ActivityStatus

Lifecycle status of a captured activity.

type ActivityStatus = 'captured' | 'sent' | 'delivered' | 'failed'

AutoCommitAction

Actions the countdown reducer accepts.

  • set — apply a /autocommit <seconds> command (seconds <= 0 disables); arms AND starts a fresh countdown (an explicit, just-now user choice).
  • hydrate — restore a cadence persisted on the project (e.g. on reload), enabled but PAUSED (seconds <= 0 disables). Unlike set, it does NOT start counting down — the countdown only re-arms on the next file change — so reopening a project never auto-commits a tree the user hasn't touched.
  • reset — a file changed; restart the full countdown (no-op when disabled).
  • tick — one second elapsed; decrement toward zero (no-op when paused).
  • fired — a commit was just dispatched; pause until the next file change.
type AutoCommitAction =
  | { type: 'set'; seconds: number }
  | { type: 'hydrate'; seconds: number }
  | { type: 'reset' }
  | { type: 'tick' }
  | { type: 'fired' }

ShareRole

A role granted by a share link.

type ShareRole = (typeof SHARE_ROLES)[number]

ShareCommand

The parsed result of a /share command:

  • create — POST a link at a valid role (/share, defaulting to viewer, or /share <role> with a recognized role).
  • invalid — an unrecognized role argument was given (the caller shows usage).
type ShareCommand = { kind: 'create'; role: ShareRole } | { kind: 'invalid'; arg: string }

TimestampsCommand

Parsed /timestamps invocation: toggle, explicit on/off, or an unrecognized argument.

type TimestampsCommand = { action: 'toggle' | 'on' | 'off' } | { action: 'invalid' }

DeviceOrientation

Preview-only iframe orientation. Portrait is the natural orientation of a fixed-frame device; landscape swaps its width/height. This is a visual preview concern that lives in PreviewPanel's local state — it is NOT part of the live-preview core state.

type DeviceOrientation = 'portrait' | 'landscape'

ChatEventCardSegment

One inline segment of a card's composable body: literal text, an inline monospace ChatEventCardCode span, or a labelled link/action (ChatEventCardAction). See ChatEventCard.content.

type ChatEventCardSegment = string | ChatEventCardCode | ChatEventCardAction

ChatEventCardFactory

Turns a custom event's data payload into a chat card, or returns null to render nothing for that event.

type ChatEventCardFactory = (data: Record<string, unknown> | undefined) => ChatEventCard | null

SettingKey

Stable ids for each user-controllable setting (also the i18n key suffix).

type SettingKey =
  | 'model'
  | 'planModel'
  | 'executeModel'
  | 'commitModel'
  | 'compactModel'
  | 'mode'
  | 'effort'
  | 'maxLoops'
  | 'autoFix'
  | 'autoCommit'
  | 'hooks'
  | 'autoApproveCommands'
  | 'sounds'
  | 'timestamps'

TestWorkspace

Which project directory a test file belongs to: the directory that OWNS the file (the nearest ancestor holding the runner's config or a package.json), as a path relative to the workspace root — app, api, my-app/app, packages/web — or . for the workspace root itself. Nothing guarantees app/ or api/ at the root: the executor names its project directory, so the value is a path, not an enum.

type TestWorkspace = 'app' | 'api' | '.' | (string & {})

TestKind

What a test file is: an end-to-end spec driven against the LIVE PREVIEW (the @molecule/app-e2e-preview bond every scaffolded app carries), or a plain unit test run by the project's own runner.

type TestKind = 'e2e' | 'unit'

TestStatus

How one test file ended.

type TestStatus = 'passed' | 'failed' | 'skipped'

TestCaseStatus

How one TEST inside a file is doing: the three verdicts a file can end with, plus running — the state a file never reports, because a file is only ever seen finished.

type TestCaseStatus = TestStatus | 'running'

TestRunOutcome

How a whole run ended. skipped-by-user is a run that otherwise finished but had at least one command skipped from the card — deliberately its OWN outcome rather than completed plus a count, so nothing can read a run with skipped work in it as a clean pass. A real failure still wins: error, timeout and cancelled take precedence over it.

type TestRunOutcome = 'completed' | 'cancelled' | 'timeout' | 'error' | 'skipped-by-user'

TestRunEvent

One event from a run in progress. The host streams these to the bar (over SSE in molecule.dev) in the order the run produces them: one start, then interleaved output/case/result, then exactly one done.

type TestRunEvent =
  | { type: 'start'; runId: string; ids: string[]; startedAt?: string }
  | { type: 'output'; id?: string; stream?: 'stdout' | 'stderr'; chunk: string }
  /**
   * One TEST within a file — the runner's own title and group, so the person
   * watching a five-minute spec run sees WHICH test is on screen instead of a
   * spinner. `id` is the same file id `result` uses.
   */
  | {
      type: 'case'
      id: string
      title: string
      describe?: string
      status: TestCaseStatus
      durationMs?: number
    }
  | {
      type: 'result'
      id: string
      status: TestStatus
      durationMs?: number
      passed?: number
      failed?: number
      skipped?: number
      /** The runner's output for a FAILURE, which the bar keeps visible. */
      output?: string
    }
  | {
      type: 'done'
      runId?: string
      outcome: TestRunOutcome
      passed?: number
      failed?: number
      skipped?: number
      durationMs?: number
      error?: string
    }

PreviewStuckReason

Why the preview could not show the running app — the failure CLASS the host hands to its AI agent so a fix can be targeted (and so the agent isn't told "it's broken" with no hint of how). Distinct from a JS error (onPreviewError): these are states the iframe itself can't report once it's in them.

type PreviewStuckReason =
  // Heartbeats stopped after a render — the app's main thread is locked (an infinite
  // loop / runaway render). The iframe can post nothing else once frozen, so only the
  // host's heartbeat-silence watchdog can detect it.
  | 'frozen'
  // Repeated reload/remount cycles never produced a confirmed render — the document
  // loads but the app never mounts (e.g. a route that throws on every attempt).
  | 'load-failed'
  // The absolute readiness ceiling elapsed with no confirmed render and no active
  // build — a catch-all backstop so the preview can never spin forever.
  | 'load-timeout'

PreviewRenderState

The preview's live render verdict, derived from the iframe bridges — the one preview fact the server can't observe itself (it has no browser). The host forwards it to the server so the post-loop verification won't pass a build while the app isn't actually rendering ("compiles + serves" ≠ "renders").

  • rendered — the app drew content (molecule:ready).
  • blank — loaded but showed nothing (gave up / #root empty after settling).
  • frozen — rendered then locked up (heartbeats stopped).
  • loading — still loading / not yet determined.
type PreviewRenderState = 'rendered' | 'blank' | 'frozen' | 'loading'

Functions

groupCommandsByCategory(commands, categories)

Groups commands under their categories, preserving category and command order and dropping empty categories. Used by the /settings command reference (and any other view that lists commands by section) so the grouping stays in sync with the registry automatically.

function groupCommandsByCategory(
  commands?: readonly CommandDef[],
  categories?: readonly CommandCategory[],
): CommandGroup[]
  • commands — Command registry to group (defaults to COMMANDS).
  • categories — Ordered categories (defaults to COMMAND_CATEGORIES).

Returns: One CommandGroup per non-empty category, in category order.

matchesSideChannelCommand(defs, text)

True when text invokes a side-channel command (CommandDef.sideChannel) from defs, matched against the leading /token by command id or alias (case-insensitive). Used by the chat input's dispatch AND the auto-send (initialMessage) path so a side-channel send never renders an optimistic "/command …" echo — the server's emitted message stream event is the one visible message.

function matchesSideChannelCommand(defs: readonly CommandDef[] | undefined, text: string): boolean
  • defs — The command registry to consult (e.g. shared ∪ host commands).
  • text — The raw outgoing message text.

Returns: Whether the text invokes a side-channel command.

ActivityCard(props)

Compact, clickable inline card for a single captured activity.

function ActivityCard({ activity, onActivityClick }: ActivityCardProps): JSX.Element
  • props — Component props.

Returns: The rendered activity card element.

ChatPanel(props)

AI chat panel with conversation history dropdown and Claude Code-style tool display.

function ChatPanel({
  projectId,
  endpoint,
  initialMessage,
  onInitialMessageSent,
  activeFile,
  openTabs,
  onFileOpen,
  onFileDoubleClick,
  onFileDiff,
  onFileRevert,
  onFileChange,
  onFileDeleted,
  onCommit,
  onActivityClick,
  onRenderError,
  onProfileClick,
  currentUserId,
  onReadyToBuild,
  awaitingSandboxBoot,
  onClientAction,
  onTurnComplete,
  onLoadingChange,
  onNavigatePreview,
  onRegisterPushHandler,
  onRegisterHistoryReconcile,
  autoSubmitSignal,
  initialInputValue,
  hideConversationMenu,
  renderConversationHeader = true,
  conversationId: controlledConversationId,
  chatKey: controlledChatKey,
  onConversationId: controlledOnConversationId,
  openShareSignal: controlledShareSignal,
  openReportSignal: controlledReportSignal,
  openSettingsSignal: controlledSettingsSignal,
  onManageCustomModels,
  gitStatusTick,
  pendingMessage,
  pendingMessageKey,
  pendingMessageSuppressUser,
  pendingMessageUserInitiated,
  userEditedFile,
  userEditedFileKey,
  isPro,
  isAnonymous,
  canEdit = true,
  canShare,
  shareLinksAllowed,
  shareLinksRefreshSignal,
  shareModalContent,
  buildUpgradeCta,
  buildHelpUpgradeSection,
  userAvatar,
  agentName,
  productName,
  version,
  extraCommands,
  feedbackUrl,
  listTests,
  runTests,
  canRunTests,
  testsAvailable,
  skipToolCall,
  className,
}: ChatPanelProps): JSX.Element
  • props — Component props (see MessageItemProps).

Returns: The rendered chat panel element.

ChatTimestamp(props)

A chat item's timestamp.

function ChatTimestamp({
  timestamp,
  variant = 'inline',
  align = 'left',
}: ChatTimestampProps): JSX.Element | null
  • props — ChatTimestampProps.

Returns: The rendered timestamp, or null for an invalid time.

CommandPalette(props)

Command Palette overlay.

function CommandPalette({ commands, onDismiss }: CommandPaletteProps): JSX.Element
  • props — Component props.

Returns: The command palette element.

DeviceFrameSelector(props)

A dropdown that selects the preview device frame and hosts the Rotate + Open-in-new-tab actions.

function DeviceFrameSelector({
  current,
  onChange,
  className,
  canRotate,
  rotated,
  onRotate,
  onOpenExternal,
}: DeviceFrameSelectorWithActionsProps): JSX.Element
  • props — Component props (see DeviceFrameSelectorWithActionsProps).

Returns: The rendered device-frame selector element.

EditorPanel(props)

Code editor panel with tab bar and Monaco integration.

function EditorPanel({
  className,
  readOnly,
  onActiveFileChange,
  onEditorReady,
  onTabsChange,
  fileStatuses,
  formattingFile,
  countdownFile,
  countdownKey,
  formatEstimate = 2000,
  onFixWithAI,
  onTabDoubleClick,
}: EditorPanelProps): JSX.Element
  • props — Component props.

Returns: The rendered editor panel element.

FileExplorer(props)

Tree-view file explorer component with multi-select, keyboard navigation, and drag-and-drop.

function FileExplorer({
  files,
  onFileSelect,
  onFileDoubleClick,
  onDirExpand,
  readOnly,
  onRename,
  onDelete,
  onDeleteMultiple,
  onMoveFiles,
  onNewFile,
  onNewFolder,
  onCollapseAll,
  className,
  persistKey,
  activeFile,
  fileStatuses,
}: FileExplorerProps): JSX.Element
  • props — Component props (see FileTreeItemProps).

Returns: The rendered file explorer element.

Icon(props)

Renders the named glyph from the bonded icon set.

function Icon({ name, size = 16, className, ...rest }: IconProps): JSX.Element
  • props — IconProps.

Returns: An <svg> element rendering the named glyph.

KeyboardShortcutsPanel(props)

Keyboard shortcuts reference panel.

function KeyboardShortcutsPanel({ shortcuts, onDismiss }: KeyboardShortcutsPanelProps): JSX.Element
  • props — Component props.

Returns: The keyboard shortcuts panel element.

PreviewPanel(props)

Live preview panel with iframe, device frame selector, and URL bar.

function PreviewPanel({
  loadingIndicator,
  restartingIndicator,
  className,
  onPreviewError,
  onPreviewStuck,
  onRestartBackend,
  onRenderState,
  uiCommand,
  onUiResult,
  fileChangeTick,
  buildingHint,
  isBuilding,
  wakeAt,
}: PreviewPanelProps): JSX.Element
  • props — Component props.

Returns: The rendered preview panel element.

QuickOpen(props)

Quick Open file finder overlay.

function QuickOpen({ projectId, onFileOpen, onDismiss }: QuickOpenProps): JSX.Element
  • props — Component props.

Returns: The quick open element.

QuickPicker(props)

Quick picker overlay with keyboard navigation.

function QuickPicker({
  items,
  placeholder,
  onSelect,
  onDismiss,
  loading,
  initialQuery,
  className,
}: QuickPickerProps): JSX.Element
  • props — Component props.

Returns: The quick picker element.

ReportModal(props)

The bug-report / feedback modal opened by /report, /bug, and the header bug-report button.

function ReportModal({
  projectId,
  conversationId,
  initialTitle,
  onClose,
  onSubmitted,
  productName = DEFAULT_PRODUCT_NAME,
  appVersion,
}: {
  projectId: string
  conversationId?: string | null
  initialTitle?: string
  onClose: () => void
  onSubmitted: (result: ReportResult) => void
  productName?: string
  appVersion?: string
}): JSX.Element
  • props — Component props.

Returns: The rendered report modal.

ResizeHandle(props)

Draggable handle for resizing adjacent panels. Uses Pointer Events so it works with mouse, touch, and pen (an iPad drag resizes just like a desktop drag); a wide invisible grab zone wraps a thin visible line that brightens to the primary color on hover/drag for a clear affordance. Arrow keys nudge the split for keyboard users.

function ResizeHandle({
  onResize,
  direction = 'horizontal',
  className,
}: ResizeHandleProps): JSX.Element
  • props — Component props.

Returns: The rendered resize handle element.

SearchPanel(props)

Search-in-files panel for the IDE sidebar with find-and-replace support.

function SearchPanel({
  projectId,
  onResultClick,
  className,
  excludedDirs,
  onExcludedDirsChange,
  readOnly,
}: SearchPanelProps): JSX.Element
  • props — Component props.

Returns: The search panel element.

ShareLinkManager(props)

The reusable public-link manager. See the module doc for behavior.

function ShareLinkManager({
  projectId,
  canManage = true,
  canCreate = canManage,
  canRevoke = canManage,
  roles = [DEFAULT_SHARE_ROLE],
  initialRole = DEFAULT_SHARE_ROLE,
  refreshSignal,
  onCreated,
  onRevoked,
}: {
  projectId: string
  canManage?: boolean
  canCreate?: boolean
  canRevoke?: boolean
  roles?: readonly ShareRole[]
  initialRole?: ShareRole
  refreshSignal?: number
  onCreated?: (result: ShareLinkResult) => void
  onRevoked?: (id: string) => void
}): JSX.Element
  • props — Component props.

Returns: The rendered manager.

ShareModal(props)

The share-link modal opened by /share, /share <role>, and the header share button.

function ShareModal({
  projectId,
  initialRole = DEFAULT_SHARE_ROLE,
  roles = [DEFAULT_SHARE_ROLE],
  canManage = true,
  canCreate = canManage,
  canRevoke = canManage,
  showLinks = true,
  refreshSignal,
  children,
  onClose,
  onCreated,
}: {
  projectId: string
  initialRole?: ShareRole
  roles?: readonly ShareRole[]
  canManage?: boolean
  canCreate?: boolean
  canRevoke?: boolean
  showLinks?: boolean
  refreshSignal?: number
  children?: ReactNode
  onClose: () => void
  onCreated?: (result: ShareLinkResult) => void
}): JSX.Element
  • props — Component props.

Returns: The rendered share modal.

SidebarTabs(props)

Sidebar tab strip with file explorer and search icons.

function SidebarTabs({ activeTab, onTabChange, children, className }: SidebarTabsProps): JSX.Element
  • props — Component props.

Returns: The sidebar tabs element.

TabBar(props)

Horizontally scrollable tab bar for open editor files.

function TabBar({
  tabs,
  activeFile,
  onSelect,
  onClose,
  onDoubleClick,
  fileStatuses,
  className,
}: TabBarProps): JSX.Element | null
  • props — Component props (see TabItemProps).

Returns: The rendered tab bar element, or null if no tabs are open.

TestsCard(props)

The tests browser shown by /test.

function TestsCard({
  tests,
  status,
  run,
  initialQuery,
  canRun,
  onRun,
  onCancel,
  onSkipCurrent,
  onFix,
  fixDisabledReason,
  isLight,
  embedded,
}: TestsCardProps): JSX.Element
  • props — See TestsCardProps.

Returns: The rendered tests card.

UserAvatar(props)

Renders the user's avatar image, or the generic user icon when there is no safe, renderable avatar. When UserAvatarProps.onClick is provided the visual is wrapped in an accessible button so the avatar can open the user's profile.

function UserAvatar({ userAvatar, name, size = 24, onClick }: UserAvatarProps): JSX.Element
  • props — UserAvatarProps.

Returns: The avatar image or icon fallback, optionally wrapped in a button.

WorkspaceLayout(props)

Top-level workspace layout that arranges child panels in a row with draggable vertical dividers between them. Each divider resizes its left panel; the new size is written back through resizePanel, so it persists in workspace state.

function WorkspaceLayout({ children, className }: WorkspaceLayoutProps): JSX.Element
  • props — Component props.

Returns: The rendered workspace layout element.

activityIconName(type)

Returns the bonded-icon-set glyph NAME for an activity type — pass it to <Icon name={…} /> to render the themed SVG. Unknown/future types (which activityFromEvent normalizes to webhook) reuse the link glyph rather than risk a getIcon throw.

function activityIconName(type: ActivityType): IconName
  • type — The activity channel type.

Returns: The icon-set glyph name for the type.

activityTypeLabel(type)

Human-readable, translated label for a channel type (used as filter-tab labels).

function activityTypeLabel(type: ActivityType): string
  • type — The activity channel type.

Returns: The translated channel label.

activityStatusLabel(status)

Human-readable, translated label for a status (shown in the status pill).

function activityStatusLabel(status: ActivityStatus): string
  • status — The activity status.

Returns: The translated status label.

activityStatusColors(status)

Resolves the status-pill colors for a given status. Uses RGBA literals (not ClassMap classes) because these semantic status hues are not part of the surface/text token set — the same approach VerificationBadge takes for its pass/fail coloring.

function activityStatusColors(status: ActivityStatus): { fg: string; bg: string }
  • status — The activity status.

Returns: An object with fg (text) and bg (background) CSS color strings.

activitySummaryLine(activity)

Builds the one-line summary shown on the inline card: the activity's own summary, with the recipient appended after an arrow when present (e.g. Welcome email → user@example.com). Falls back to a translated, type-specific default when no summary was captured.

function activitySummaryLine(activity: Pick<Activity, 'type' | 'recipient' | 'summary'>): string
  • activity — The activity to summarize.

Returns: The single-line summary string.

activityFromEvent(raw)

Maps a raw SSE activity event payload into a normalized Activity. Tolerates missing optional fields and supplies an id/timestamp if absent.

function activityFromEvent(raw: {
  id?: string
  type?: string
  status?: string
  recipient?: string
  summary?: string
  timestamp?: string
}): Activity
  • raw — The activity field from the SSE event.
  • raw.id — Activity id; generated if absent.
  • raw.type — Channel type; defaults to webhook if absent.
  • raw.status — Lifecycle status; defaults to captured if absent.
  • raw.recipient — Optional recipient.
  • raw.summary — Optional short summary.
  • raw.timestamp — ISO timestamp; defaults to now if absent.

Returns: A normalized Activity object.

filterActivitiesByType(activities, type)

Filters a list of activities by channel type. null (the "All" tab) returns every activity unchanged.

function filterActivitiesByType(activities: Activity[], type: ActivityType | null): Activity[]
  • activities — The activities to filter.
  • type — The channel type to keep, or null for all.

Returns: The filtered list (a new array unless type is null).

isAutoCommitCountdownVisible(state)

Whether the countdown is in its visible window — armed and within the final AUTO_COMMIT_COUNTDOWN_VISIBLE_SECONDS seconds — i.e. the commit button should render the live "Auto-commit in Ns" label instead of "Commit".

function isAutoCommitCountdownVisible(state: AutoCommitState): boolean
  • state — The countdown state.

Returns: true when armed with at most the visible-window seconds left.

resolveAutoCommitSeconds(saved)

Resolves the persisted project.settings.autoCommitSeconds value to the effective cadence. A finite number is an explicit user choice: positive values floor to whole seconds, non-positive values mean "the user turned auto-commit off" (0). Anything else — missing, never configured, or invalid — falls back to DEFAULT_AUTO_COMMIT_SECONDS, because auto-commit is on by default.

function resolveAutoCommitSeconds(saved: unknown): number
  • saved — The raw persisted setting value.

Returns: The effective cadence in whole seconds (0 = off).

autoCommitReducer(state, action)

Pure reducer for the auto-commit countdown. Deterministic and side-effect free: the component performs the actual commit when isAutoCommitDue becomes true, then dispatches fired.

function autoCommitReducer(state: AutoCommitState, action: AutoCommitAction): AutoCommitState
  • state — The current countdown state.
  • action — The action to apply.

Returns: The next countdown state.

isAutoCommitEnabled(state)

Whether auto-commit is enabled (a positive cadence is configured), regardless of whether it is currently counting down or paused.

function isAutoCommitEnabled(state: AutoCommitState): boolean
  • state — The countdown state.

Returns: true when intervalSeconds > 0.

isAutoCommitArmed(state)

Whether the countdown is actively armed (counting down), i.e. a 1s tick interval should be running. False when disabled or paused after a commit.

function isAutoCommitArmed(state: AutoCommitState): boolean
  • state — The countdown state.

Returns: true when remaining is a number.

isAutoCommitDue(state)

Whether a commit is due this instant (the countdown has reached zero). The component reacts to this by running /commit and dispatching fired.

function isAutoCommitDue(state: AutoCommitState): boolean
  • state — The countdown state.

Returns: true when remaining === 0.

parseAutoCommitCommand(input)

Parses an /autocommit [seconds] command.

Returns { seconds: null } when /autocommit is typed with no argument (the caller shows usage/current state), { seconds: n } for a non-negative integer argument (0 cancels), or null when the input is not the command.

function parseAutoCommitCommand(input: string): { seconds: number | null } | null
  • input — The raw chat input.

Returns: The parsed command, or null when it is not /autocommit.

formatAutoCommitBadge(state)

Formats the countdown's remaining seconds for the compact badge (e.g. "12s"). Returns '' when there is nothing to show (disabled or paused).

function formatAutoCommitBadge(state: AutoCommitState): string
  • state — The countdown state.

Returns: The badge label, or ''.

parseReportCommand(input)

Parses a /report [title] or /bug [title] command. Returns the (possibly empty) trimmed title used to seed the modal when the input is one of those commands, else null.

function parseReportCommand(input: string): { title: string } | null
  • input — The raw chat input.

Returns: { title } when it's a /report or /bug command, else null.

collectClientInfo(opts)

Collects client-side diagnostics for a report. Reads navigator (userAgent/platform/language), window (inner viewport size, screen size, location href), plus the caller-supplied app version and theme. Every access is guarded (typeof window/navigator !== 'undefined' and per-property presence) so it is SSR-safe and never throws — it returns only the fields it could actually read, so the result may be partial or (in a headless environment) empty.

function collectClientInfo(opts?: { appVersion?: string; theme?: string }): ClientInfo
  • opts — Caller-supplied context.
  • opts.appVersion — The running build version, if known.
  • opts.theme — The active theme ('light' | 'dark'), if known.

Returns: The populated subset of ClientInfo.

buildReportPayload(form, clientInfo)

Builds the POST /projects/:id/report body from the form state, trimming all text fields and omitting steps entirely when it is blank. When clientInfo is supplied and non-empty, it is attached; an undefined or empty diagnostics object is omitted from the payload.

function buildReportPayload(form: ReportFormState, clientInfo?: ClientInfo): ReportPayload
  • form — The report form state.
  • clientInfo — Optional client diagnostics from collectClientInfo.

Returns: The normalized report payload.

isReportFormValid(form)

Whether the report form has the minimum required fields: a non-empty title and a non-empty description.

function isReportFormValid(form: ReportFormState): boolean
  • form — The report form state.

Returns: true when the form can be submitted.

formatReportConfirmation(result)

Builds the confirmation message shown after a report is submitted. Returns the English defaults; the component passes these through t() at render. The success defaultValue may contain the {{productName}} interpolation token, which the caller fills in from the host's product identity (neutral default: "the IDE"). When the result is not ok, returns the failure message.

function formatReportConfirmation(result: ReportResult): { key: string; defaultValue: string }
  • result — The POST /projects/:id/report response.

Returns: { key, defaultValue } for the confirmation/failure message.

isShareRole(value)

Type guard for a valid ShareRole (case-insensitive callers should lower-case first).

function isShareRole(value: string): boolean
  • value — The candidate value.

Returns: true when value is one of the SHARE_ROLES.

parseShareCommand(input)

Parses a /share [role] command. Bare /share creates a link at the default viewer role; /share <role> uses the named role when valid (any case); an unrecognized role is reported as invalid so the caller can show usage. Returns null when the input is not the /share command.

function parseShareCommand(input: string): ShareCommand | null
  • input — The raw chat input.

Returns: The parsed ShareCommand, or null.

buildSharePayload(role)

Builds the POST /projects/:projectId/shares body for a role.

function buildSharePayload(role?: 'viewer' | 'commenter' | 'editor' | 'owner'): SharePayload
  • role — The role to grant (defaults to DEFAULT_SHARE_ROLE).

Returns: The normalized share payload.

buildShareUrl(result, origin)

Resolves the copyable public URL for a created share link. Prefers the backend-supplied url (canonical origin / custom domain); otherwise builds <origin>/share/<slug> from the given origin, tolerating a trailing slash.

function buildShareUrl(result: ShareLinkResult, origin: string): string
  • result — The created link from the share endpoint.
  • origin — The current page origin (e.g. window.location.origin).

Returns: The absolute, copyable share URL.

parseChatTimestampsVisible(raw)

Parses the stored preference. Only an explicit 'true' shows every timestamp, so a missing or unreadable value keeps the default (off — only the user's own message headers and critical events carry a time; see planChatTimestamps).

function parseChatTimestampsVisible(raw: string | null): boolean
  • raw — The raw localStorage value.

Returns: Whether timestamps are visible on every item.

getChatTimestampsVisible()

Reads this device's preference.

function getChatTimestampsVisible(): boolean

Returns: Whether chat timestamps are visible on every item (default false).

resolveChatLocale(getLocale)

Resolves the active i18n locale, falling back to the browser's when no i18n provider is bonded.

function resolveChatLocale(getLocale: () => string): string
  • getLocale — The i18n accessor (throws when no provider is bonded).

Returns: A BCP 47 locale.

planChatTimestamps(slots, options)

Decides which timeline items actually render their timestamp. An item is a candidate when timestamps are on, or when its slot is ChatTimestampSlot.alwaysShown. A candidate renders only when its label differs from the last RENDERED label, so a run of items that would all read "3 minutes ago" shows it once, at the top of the run. Re-run on every minute tick: labels move with the clock, so the grouping does too.

function planChatTimestamps(
  slots: readonly (ChatTimestampSlot | null)[],
  options: { visible: boolean; now: number; locale: string },
): Set<string>
  • slots — One entry per rendered timeline item, in order (null = no timestamp).
  • options — The visibility preference, the current time, and the locale.
  • options.visible — Whether timestamps are turned on for every item.
  • options.now — Epoch milliseconds to measure from.
  • options.locale — BCP 47 locale for the labels.

Returns: The ids of the items whose timestamp renders.

setChatTimestampsVisible(visible)

Persists this device's preference and notifies every mounted chat in this tab.

function setChatTimestampsVisible(visible: boolean): void
  • visible — Whether chat timestamps should be shown.

parseTimestampsCommand(input)

Parses /timestamps [on | off]. show/hide are accepted as synonyms.

function parseTimestampsCommand(input: string): TimestampsCommand | null
  • input — The trimmed composer text.

Returns: The parsed command, or null when the text is not /timestamps.

formatChatRelativeTime(ms, now, locale)

Short, localized, MINUTE-precise time for a chat item: "now" and "5 minutes ago" within the hour, then the clock time ("9:25 AM") for earlier today, then the date and time ("Sep 12, 9:25 AM", with the year when it differs).

Minute precision at every age is load-bearing: planChatTimestamps collapses runs of identical labels, so a coarse label ("1 hour ago", "3 days ago") would collapse a whole hour or day of history into one timestamp. Localized by Intl, so it needs no translation keys.

function formatChatRelativeTime(ms: number, now: number, locale: string): string
  • ms — Epoch milliseconds of the item.
  • now — Epoch milliseconds to measure from.
  • locale — BCP 47 locale (e.g. 'en', 'fr').

Returns: The formatted label.

formatChatFullTime(ms, locale)

Exact, localized date and time for a chat item's tooltip and <time dateTime> companion (e.g. "Sep 13, 2026, 2:05 PM").

function formatChatFullTime(ms: number, locale: string): string
  • ms — Epoch milliseconds of the item.
  • locale — BCP 47 locale.

Returns: The formatted date and time.

isDeviceRotatable(device)

Whether a device frame can be rotated (true only for fixed-frame devices — tablet and mobile). Responsive and desktop are full-width with nothing to rotate, so the "Rotate" control is enabled only "where possible".

function isDeviceRotatable(device: DeviceFrame): boolean
  • device — The device frame.

Returns: true if the frame has a fixed size that can be rotated.

resolveDeviceSize(device, orientation)

Resolves the concrete iframe width/height for a device frame at a given orientation. Fluid frames (responsive/desktop) ignore orientation and fill the preview area (width from dims, height: '100%'). Fixed frames render at their pixel size in portrait and at the swapped size in landscape.

function resolveDeviceSize(
  device: DeviceFrame,
  orientation: DeviceOrientation,
): { width: string; height: string }
  • device — The device frame.
  • orientation — The preview orientation.

Returns: The CSS width and height to apply to the preview iframe.

deviceIconName(device)

Returns the icon-set glyph name for a device frame.

function deviceIconName(device: DeviceFrame): string
  • device — The device frame.

Returns: The icon name registered in the bonded icon set.

groupTests(tests)

Group tests for display: by kind (end-to-end first), then by project directory (whatever directories are present — app, my-app/app, packages/web, the root last), with the files sorted inside each group.

function groupTests(tests: readonly TestItem[]): TestGroup[]
  • tests — Every discovered test.

Returns: The groups, in display order. Empty groups are never produced.

countByKind(tests)

How many tests there are of each kind.

function countByKind(tests: readonly TestItem[]): Record<TestKind, number>
  • tests — Every discovered test.

Returns: The per-kind counts.

summarizeResults(tests, results)

The tallies of the last run, over the tests that are still listed. Counted per FILE (one row, one verdict) so the collapsed summary matches the rows.

function summarizeResults(
  tests: readonly TestItem[],
  results: Record<string, TestResultEntry>,
): { passed: number; failed: number; skipped: number; reported: number }
  • tests — The currently listed tests.
  • results — The per-id outcomes.

Returns: Passed/failed/skipped file counts.

applyTestRunEvent(state, event)

Fold one streamed event into the run state.

Deliberately total: an event for an id the card no longer lists is recorded anyway (a re-list may be in flight), a case that arrives out of order (after its file already reported a verdict) is dropped rather than resurrecting a finished row, and an unknown event type leaves the state untouched rather than throwing inside a stream handler.

function applyTestRunEvent(state: TestsRunState, event: TestRunEvent): TestsRunState
  • state — The current state.
  • event — The event just received.

Returns: The next state (a new object whenever anything changed).

requestSkip(state)

Record that the person asked to skip what is running. The state only says a request is OUT — the run's own result for the skipped file is what actually moves the row, so nothing here can make a skipped file read as anything else.

function requestSkip(state: TestsRunState): TestsRunState
  • state — The current state.

Returns: The next state, or the same one when there is nothing to skip.

markSkipUnavailable(state)

Record that skipping is not on offer — either the host wired no skipCurrent or it answered that the run had already moved on. The control is dropped; the person is not shown an error about a race they did not cause.

function markSkipUnavailable(state: TestsRunState): TestsRunState
  • state — The current state.

Returns: The next state.

canSkipRun(state)

Whether the host can be asked to skip right now.

function canSkipRun(state: TestsRunState): boolean
  • state — The run state.

Returns: True when a Skip control belongs on screen.

currentRunRowId(state)

Which row the Skip acts on: the one the stream named as current, or — when it has not named one yet — the single row still awaiting a verdict, because with exactly one candidate there is nothing to be ambiguous about.

function currentRunRowId(state: TestsRunState): string | null
  • state — The run state.

Returns: The row's id, or null when the run is not on an identifiable row.

testCaseLabel(current)

The one-line name of the test a file is on: its group and its title, or just its title when the runner gave it no group.

function testCaseLabel(current: TestCaseRef | null | undefined): string
  • current — The running test, or null.

Returns: The label, or `` when nothing is named.

failRun(state, message)

The state after a run that never produced a done event — the host's stream died, or its request failed before the server could answer.

function failRun(state: TestsRunState, message: string): TestsRunState
  • state — The current state.
  • message — What to show in the card.

Returns: The next state, no longer running.

isRowRunning(state, id)

Whether a row should render as "running": the run is live and either the host named this row as current, or it is in the queue and has no verdict yet.

function isRowRunning(state: TestsRunState, id: string): boolean
  • state — The run state.
  • id — The row's test id.

Returns: True when the row is part of the live run and still undecided.

testRowLabel(item)

The label for a row: the host's title when it gave one, else the file path.

function testRowLabel(item: TestItem): string
  • item — The test.

Returns: The label to render.

filterTests(tests, query)

Filters tests by a free-text query, matching (case-insensitively) against the file path, the row title, and the project directory. A blank query returns every test in input order — the same contract as filterScripts.

function filterTests(tests: readonly TestItem[], query: string): TestItem[]
  • tests — The tests to filter.
  • query — The search query.

Returns: The matching tests, in input order.

parseTestCommand(input)

Parses a /test [query | all] command (/tests is the registered alias).

all is the one argument that ACTS: it runs everything immediately. Every other argument only filters the list, exactly the way /scripts <query> seeds the scripts browser — running ONE test by name is what the per-row Run button is for, and giving the argument a second meaning would make /test <thing> sometimes list and sometimes execute.

function parseTestCommand(input: string): { query: string; runAll: boolean } | null
  • input — The raw chat input.

Returns: { query, runAll } when it is a /test command, else null.

buildTestFixMessage(failures)

Compose the ONE user message the card's “Fix with Synthase” action sends.

It is a real turn the executor answers, so it reads like something a person would type: which test failed, where it lives, what the runner said, and to re-run it. This is a PROMPT, not UI copy — it is not translated, exactly like the auto-fix loop’s own Fix these issues: message.

function buildTestFixMessage(failures: readonly TestFailure[]): string
  • failures — The failing tests, in the order the card lists them.

Returns: The message, or `` when there is nothing to fix.

failedTests(tests, results)

Every listed test whose last verdict was a failure, with its output — what the card's fix actions send.

function failedTests(
  tests: readonly TestItem[],
  results: Record<string, TestResultEntry>,
): TestFailure[]
  • tests — The currently listed (and filtered) tests.
  • results — The per-id outcomes.

Returns: The failures, in list order.

resolveUserAvatar(avatar)

Resolve a stored avatar value to a safe <img> src, or null to fall back to the generic icon.

function resolveUserAvatar(avatar?: string | null): string | null
  • avatar — The avatar value from user metadata (data-URI, URL, or absent).

Returns: A safe image src string, or null when there is no renderable avatar.

hasRenderableAvatar(avatar)

Whether a stored avatar value is renderable (vs. needing the icon fallback).

function hasRenderableAvatar(avatar?: string | null): boolean
  • avatar — The avatar value from user metadata.

Returns: true when resolveUserAvatar would return a src.

clampPanelSize(currentSize, deltaPx, containerWidth, min, max)

Clamp a panel's new size after a pixel drag delta.

function clampPanelSize(
  currentSize: number,
  deltaPx: number,
  containerWidth: number,
  min?: number,
  max?: number,
): number
  • currentSize — The panel's current size as a percentage.
  • deltaPx — The drag delta in pixels (positive = grow the left panel).
  • containerWidth — The layout container width in pixels.
  • min — Minimum allowed percentage. Defaults to MIN_PANEL_PERCENT.
  • max — Maximum allowed percentage. Defaults to MAX_PANEL_PERCENT.

Returns: The new size as a percentage, clamped to [min, max].

livePanelSize(layout, panelConfigs, index)

The live size (percentage) of the visible panel at index, read from layout.sizes keyed by the panel's position group (where resizePanel writes), falling back to the panel's defaultSize, then to an equal split.

function livePanelSize(layout: WorkspaceLayout, panelConfigs: PanelConfig[], index: number): number
  • layout — The current workspace layout.
  • panelConfigs — The visible panel configs, in render order.
  • index — The index of the panel within panelConfigs.

Returns: The panel size as a percentage.

registerCustomEventCard(name, factory)

Register a renderer for a custom chat-stream event. Consuming apps call this at startup so their own { type: 'custom', name } events surface as chat cards — keeping app-specific events out of the core ai-chat union and this package's ChatPanel. Re-registering the same name overwrites the previous factory.

function registerCustomEventCard(name: string, factory: ChatEventCardFactory): void
  • name — The custom event name to handle (matches the emitted event's name).
  • factory — Builds the card from the event's data (or returns null to skip).

getCustomEventCardFactory(name)

Resolve the registered card factory for a custom event name, if any.

function getCustomEventCardFactory(name: string): ChatEventCardFactory | undefined
  • name — The custom event name.

Returns: The registered factory, or undefined if none is registered.

useChatTimestampsVisible()

Whether chat timestamps are turned on for every item on this device. Re-renders when /timestamps, a host settings toggle, or another tab changes it.

function useChatTimestampsVisible(): boolean

Returns: true when every item's timestamp may render (default false).

useMinuteNow()

The current time, refreshed once a minute for every subscriber at once.

function useMinuteNow(): number

Returns: Epoch milliseconds as of the last minute tick.

serializeEvent(e, isMac)

Serialize a KeyboardEvent into a normalized combo string. Format: modifier keys in order mod+ctrl+alt+shift+key (lowercase). mod maps to Meta on Mac, Control elsewhere.

function serializeEvent(e: KeyboardEvent, isMac?: boolean): string
  • e — The keyboard event.
  • isMac — Override platform detection (for testing).

Returns: Serialized combo string.

normalizeKeys(keys)

Normalize a shortcut definition string into the same format produced by serializeEvent. Accepts mod+shift+f style strings (case-insensitive).

function normalizeKeys(keys: string): string
  • keys — Raw shortcut string.

Returns: Normalized lowercase combo string.

isInputFocused(e)

Returns true when the event target is an interactive input element where shortcuts should be suppressed by default.

function isInputFocused(e: KeyboardEvent): boolean
  • e — The keyboard event.

Returns: Whether an input-like element is focused.

isMonacoFocused(e)

Returns true when focus is inside a Monaco editor instance.

function isMonacoFocused(e: KeyboardEvent): boolean
  • e — The keyboard event.

Returns: Whether the target is inside .monaco-editor.

useKeyboardShortcuts(shortcuts)

Registers global keyboard shortcuts on the document.

function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]): void
  • shortcuts — Array of shortcut definitions. Callers should wrap the array in useMemo to avoid unnecessary re-subscriptions.

useMediaQueryMatch(query)

Subscribes to a CSS media query. SSR-safe: reports false until mounted in a browser.

function useMediaQueryMatch(query: string): boolean
  • query — A media query string, e.g. '(max-width: 767px)'.

Returns: Whether the query currently matches.

useNarrowViewport()

Whether the viewport is phone-width (below 768px). Use for layout-density decisions: bigger type, taller rows, dvh-capped popovers.

function useNarrowViewport(): boolean

Returns: True on phone-width viewports.

useCoarsePointer()

Whether the primary pointer is coarse (touch). Use for interaction decisions: ≥44px touch targets, always-visible controls that desktop reveals on hover (hover does not exist on touch).

function useCoarsePointer(): boolean

Returns: True on touch-first devices.

Constants

COMMAND_CATEGORIES

Ordered list of command categories used as section headings.

const COMMAND_CATEGORIES: readonly CommandCategory[]

COMMANDS

Every available slash command, grouped by category order. This is the authoritative list — the menu, key handling, /help, AND the system prompt's "Available Commands" section all read from it.

const COMMANDS: readonly CommandDef[]

ToolCallCard

Compact tool-call row with status dot, label, summary, and expandable detail pane.

const ToolCallCard: MemoExoticComponent<
  ({
    id,
    name,
    input,
    output,
    status,
    fileDiff,
    isUndone: isUndoneProp,
    onUndoToggle,
    onFileOpen,
    onFileDoubleClick,
    onFileDiff,
    onFileRevert,
    onAskUserResponse,
    onSkip,
    skipDisabledReason,
    className,
  }: ToolCallCardProps) => JSX.Element | null
>
  • props — Component props.

Returns: The rendered tool call card element.

ACTIVITY_TYPES

All channel types, in the order their filter tabs should appear.

const ACTIVITY_TYPES: ActivityType[]

AUTO_COMMIT_DISABLED

The disabled (off) countdown state — the reducer's initial value.

const AUTO_COMMIT_DISABLED: AutoCommitState

DEFAULT_AUTO_COMMIT_SECONDS

The default auto-commit cadence in seconds — auto-commit is ON by default. Projects that have never persisted autoCommitSeconds hydrate to this cadence (paused, arming on the first file change); only an explicit 0 (the user cancelled) keeps auto-commit off.

The value is the TOTAL debounce after the last file change (once the agent's turn has finished): a short quiet period, then the final AUTO_COMMIT_COUNTDOWN_VISIBLE_SECONDS seconds render as a live countdown on the commit button. 5s = 2s quiet + 3s visible countdown.

const DEFAULT_AUTO_COMMIT_SECONDS: 5

AUTO_COMMIT_COUNTDOWN_VISIBLE_SECONDS

How many final seconds of the countdown are DISPLAYED as a live countdown. Until the countdown drops to this threshold, the commit-bar button stays a plain green "Commit" (a bare "12s" pill told users nothing); the countdown label only takes over for these last seconds, right before the auto-commit fires. Cadences at or below the threshold show the countdown the whole time.

const AUTO_COMMIT_COUNTDOWN_VISIBLE_SECONDS: 3

EMPTY_REPORT_FORM

An empty report form (the modal's initial state).

const EMPTY_REPORT_FORM: ReportFormState

SHARE_ROLES

Share roles, ordered least- to most-privileged. Mirrors the @molecule/api-resource-share SHARE_ROLES contract; duplicated here (rather than imported) because an app-* package may not import an api-* package across the stack boundary.

const SHARE_ROLES: readonly ['viewer', 'commenter', 'editor', 'owner']

DEFAULT_SHARE_ROLE

The default (and safest) role when /share is run with no argument.

const DEFAULT_SHARE_ROLE: 'viewer' | 'commenter' | 'editor' | 'owner'

SHARE_ROLE_LABELS

Short English labels per role (the component wraps these in t() at render).

const SHARE_ROLE_LABELS: Record<'viewer' | 'commenter' | 'editor' | 'owner', string>

CHAT_TIMESTAMPS_STORAGE_KEY

localStorage key holding 'true' / 'false'. Absent = the default (hidden).

const CHAT_TIMESTAMPS_STORAGE_KEY: 'mol_chat_show_timestamps'

CHAT_TIMESTAMPS_EVENT

Window event dispatched (same tab) whenever the preference changes.

const CHAT_TIMESTAMPS_EVENT: 'mol:chat-timestamps-changed'

DEVICE_FRAMES

The device frames the selector dropdown lists, in display order. none = responsive (no fixed frame, full width).

const DEVICE_FRAMES: readonly DeviceFrame[]

DEVICE_META

Per-frame display metadata: the icon-set glyph name and the i18n label (key + English default) shown in the dropdown trigger + menu items.

const DEVICE_META: Record<
  DeviceFrame,
  { readonly icon: IconName; readonly labelKey: string; readonly label: string }
>

DEVICE_DIMENSIONS

The fixed pixel frame each device renders the preview iframe at. none (responsive) and desktop are fluid — full width, full height, nothing to rotate. tablet (768×1024) and mobile (375×667) are fixed frames that can be rotated to landscape (swapping width/height).

const DEVICE_DIMENSIONS: Record<DeviceFrame, DeviceDimensions>

MAX_OUTPUT_LINES

Live output lines retained — enough to read, bounded so a chatty run cannot grow forever.

const MAX_OUTPUT_LINES: 400

EMPTY_RUN_STATE

A run that has not started.

const EMPTY_RUN_STATE: TestsRunState

MAX_FIX_OUTPUT_CHARS

Output kept per failure when several are batched into one fix message.

const MAX_FIX_OUTPUT_CHARS: 2000

MAX_FIX_MESSAGE_OUTPUT_CHARS

Total output kept across a batched fix message, so one click cannot send a novel.

const MAX_FIX_MESSAGE_OUTPUT_CHARS: 12000

MAX_AVATAR_SRC_LENGTH

Maximum avatar source length we will render inline (~256 KB data-URI).

const MAX_AVATAR_SRC_LENGTH: 262144

MIN_PANEL_PERCENT

Smallest a panel may shrink to, as a percentage of the container.

const MIN_PANEL_PERCENT: 10

MAX_PANEL_PERCENT

Largest a panel may grow to, as a percentage of the container.

const MAX_PANEL_PERCENT: 80

IS_MAC

Detect macOS / iOS for Cmd vs Ctrl.

const IS_MAC: boolean

NARROW_VIEWPORT_QUERY

The phone-width media query — matches viewports below 768px, mirroring the host workspace's single-pane mobile breakpoint.

const NARROW_VIEWPORT_QUERY: '(max-width: 767px)'

COARSE_POINTER_QUERY

The touch-first media query — matches when the PRIMARY pointer is coarse (finger), i.e. phones/tablets, regardless of viewport width.

const COARSE_POINTER_QUERY: '(pointer: coarse)'

SETTINGS

Every user-controllable setting, in display order. The authoritative list — the /settings view (via buildSettingsList) and the system prompt both read from it, so they cannot drift. The default model and the per-mode planModel / executeModel rows are kept distinct (rather than collapsed into one "Model" row); effort, autoCommit, and hooks are each listed alongside (or in hooks' case, instead of) the command the panel references — so a user's actual configuration is never understated and the panel never shows a command for a setting it hides.

const SETTINGS: readonly SettingMeta[]

DEFAULT_SEARCH_EXCLUDED_DIRS

Default search-excluded directory names shown when the host supplies none — VS Code's search.exclude/files.exclude defaults plus the platform's vendored/build dirs. MUST stay in sync with the API-side list in @molecule/api-ai-tools (utilities.ts) — the cross-stack import boundary forces the duplication.

const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
  'node_modules',
  'bower_components',
  '.git',
  '.svn',
  '.hg',
  'CVS',
  'dist',
  '.next',
  '.vite',
  'molecule',
]

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-ai-chat ^1.0.1
  • @molecule/app-ai-models ^1.0.1
  • @molecule/app-ai-voice ^1.0.1
  • @molecule/app-code-editor ^1.0.1
  • @molecule/app-country-flags ^1.0.1
  • @molecule/app-i18n ^1.0.1
  • @molecule/app-icons ^1.0.1
  • @molecule/app-ide ^1.0.1
  • @molecule/app-live-preview ^1.0.1
  • @molecule/app-logger ^1.0.1
  • @molecule/app-react ^1.0.1
  • @molecule/app-storage ^1.0.1
  • @molecule/app-ui ^1.0.1
  • @molecule/app-ui-react ^1.0.1
  • react ^18.0.0 || ^19.0.0
  • react-dom ^18.0.0 || ^19.0.0

Runtime Dependencies

  • @molecule/app-ai-chat

  • @molecule/app-ai-models

  • @molecule/app-ai-voice

  • @molecule/app-code-editor

  • @molecule/app-country-flags

  • @molecule/app-i18n

  • @molecule/app-icons

  • @molecule/app-ide

  • @molecule/app-live-preview

  • @molecule/app-logger

  • @molecule/app-react

  • @molecule/app-storage

  • @molecule/app-ui

  • @molecule/app-ui-react

  • material-file-icons

  • react

  • react-dom

  • The panels are hook-driven and THROW without provider wiring: wrap the tree in @molecule/app-react's WorkspaceProvider, EditorProvider, ChatProvider, and PreviewProvider (or the umbrella MoleculeProvider), each wired to a bond — typically @molecule/app-ide-default (workspace/layout state + persistence), @molecule/app-code-editor-monaco, @molecule/app-ai-chat-http (streaming chat endpoint), and @molecule/app-live-preview-iframe. ChatPanel additionally needs the bonded HTTP client (useHttpClient) and I18nProvider; getClassMap() needs a ClassMap bond.

  • WorkspaceLayout takes panels as CHILDREN and matches them in order to the workspace provider's panel configs (extra configs are ignored, so rendering 2 children against the default 3-panel layout works). Panel sizes persist through the workspace provider's resizePanel.

  • App-specific chat/stream events do NOT belong in this package — emit { type: 'custom', name, data } events and register a card with registerCustomEventCard(name, factory) in the consuming app.

  • ReportModal and ShareModal are exported so a HOST can mount them itself. They need only projectId + the bonded HTTP client — no workspace/editor/preview provider, no chat, no running sandbox — because they POST to /projects/:id/report and /projects/:id/shares on the platform API. Do NOT route a host's "report a bug" / "share" button through ChatPanel's openReportSignal / openShareSignal alone: those props are observed inside the panel, so in any phase where the host does not mount a ChatPanel (a hibernating sandbox, an error screen) the button silently does nothing. Mount the modal directly in those phases. ChatPanel renders its own copies for /report and /share; a host that mounts them too must gate on whether the panel is mounted so the modal never opens twice.

  • ReportModal does NOT show its own success state — it calls onSubmitted and closes, so a host that mounts it MUST surface the confirmation itself (formatReportConfirmation(result) returns the i18n key + default copy, and result.url is the filed issue). ShareModal does show the created link inline, but only until it is closed.

  • Chat timestamps are a PER-DEVICE preference in localStorage, OFF by default — not a ChatPanel prop or project setting. Off, only the user's own message headers and critical events show a time; on, every message and event card does. Labels are minute-precise at every age ("3 minutes ago", then "9:25 AM", then "Sep 12, 9:25 AM") and advance each minute; a run of items with the same label shows it once, at the top, so only same-minute runs collapse. A host settings screen toggles it with setChatTimestampsVisible(bool) and reads it with useChatTimestampsVisible(); /timestamps [on | off] writes the same value.

  • A custom card is a critical event only when its factory sets critical: true (a failure, a blocked action, a limit that stopped work) — the package never infers it from tone or copy.

  • ChatPanel owns no pricing or auth routes, so every limit/upgrade button comes from the host's buildUpgradeCta(context). The context forwards the backend's whole description of the refusal: requiresSignup, limitType (the rule that fired), billingAction (the remedy it resolved — e.g. add_funds, add_payment_method, raise_spend_cap, upgrade, none) and upgradeTier (null = no higher plan). Branch on billingAction first; every field is optional, so keep a fallback. A live limit banner and a recorded card that declares the same coversLimitType state one fact, so the card steps aside while the banner is up — which only reads as one consistent message if the banner's button is resolved from the same billingAction the card used.

  • /test lists and runs the project's tests (TestsCard, opened by the /test command — /tests is a registered alias — in the same closeable overlay as /scripts and /skills, and renderable in the timeline as the tests system card). It owns no routes: pass listTests and runTests and the command works; omit either and it has nothing to run. runTests(selection, onEvent) returns a handle whose cancel() must really stop the run, and it must deliver exactly one done event however the run ends — including when the request never opened — or the card spins forever. canRunTests (a viewer: false) and testsAvailable (the environment is up) each disable the run controls and state their own reason in the card. The RUN is owned by ChatPanel, not the card, so it keeps streaming while the overlay is closed. /test all runs everything; any other argument only filters the list.

  • End-to-end specs are meant to run against the LIVE PREVIEW. The host should drive them through @molecule/app-e2e-preview (what every mlcl create app already bonds in e2e/bonds.ts), not a browser binary — in a sandbox there is none. That bond drives the page the person is actually looking at, so a preview must be open somewhere or the driver waits and fails; the card says so next to the end-to-end group.

  • Chat markdown links are scheme-allowlisted. MarkdownContent renders [label](href) with a leading-slash or bare path as a preview-route button and only http(s):/mailto: (plus //host) as real anchors; every other scheme (javascript:, data:, …) renders as inert text. Model output is prompt-injectable, so never loosen this to render raw schemes — the markdown bonds' isSafeUrl keeps the same policy.

  • Text routes through t('ide.*') — @molecule/app-locales-ide supplies translations.

Translations

Translation strings are provided by @molecule/app-locales-ide.