npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/app-ide-react

v1.19.10

Published

React IDE components for molecule.dev workspace

Readme

@molecule/app-ide-react

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.

@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().

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

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
}

ActivityCardProps

Props for the inline {@link 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
}

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
}

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, {@link 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 {@link 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 {@link 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
}

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 {@link ChatEventCardAction} with code: true.

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

ChatMessageItemProps

Properties for the chat message item component.

interface ChatMessageItemProps {
  message: ChatMessage
  className?: string
}

ChatPanelProps

Props for the {@link 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 {@link 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 {@link 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: {@link ChatPanelProps.conversationId} /
   * {@link ChatPanelProps.chatKey} / {@link ChatPanelProps.onConversationId} for
   * the conversation, and {@link ChatPanelProps.openShareSignal} /
   * {@link 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 {@link 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
  /** Changing this opens the in-chat `/report` modal (host-driven). Overrides the built-in header's bug-report button signal. */
  openReportSignal?: number
  /** Changing this opens the in-chat `/settings` view (host-driven). Overrides the built-in header's settings button signal. */
  openSettingsSignal?: number
  /**
   * When provided, the `/model` picker shows an "Add or manage your own
   * models…" row at the bottom of the list; choosing it closes the picker and
   * invokes this callback (the host opens its own custom-provider management
   * surface). Omit to hide the row — the shared package stays host-agnostic.
   */
  onManageCustomModels?: (context?: {
    /**
     * The mode a "Use this model" action should target — the `/model` picker's
     * scoped mode when it has one, else the LIVE conversation mode (discovery
     * runs in plan mode, so it maps to `'plan'`).
     */
    mode?: 'plan' | 'execute'
  }) => void
  /**
   * Bump to make the panel re-read its persisted model/settings state — e.g.
   * after the host's custom-provider surface sets the chat model via "Use".
   * The panel loads that state once on mount otherwise.
   */
  modelSelectionSignal?: number
  /**
   * Whether the current user may write SHARED project state from the chat
   * surface — send/run Synthase, change the model, toggle autofix/mode, run a
   * write-command, etc. A read-only project VIEWER passes `false`: the server
   * 403s every such write, so the UI disables the composer + those controls and
   * default-denies non-`viewerSafe` slash commands, showing a read-only note
   * instead of a dead control. Defaults to `true` (a single-user/owner IDE and
   * every non-collaborator host is unaffected). Read-only reads, view-only
   * surfaces (`/settings`, `/help`, `/cost`) and per-user preferences stay
   * available.
   */
  canEdit?: boolean
  /**
   * Whether the current user may MANAGE public share links — the `/share`
   * command, the header share button, and the ShareModal's create/revoke
   * controls. Distinct from {@link canEdit} because hosts commonly gate share
   * minting ABOVE editor (molecule.dev mints/lists/revokes at admin+): an
   * editor with `canEdit` true but `canShare` false gets no `/share` surface
   * instead of a modal whose every request 403s. Defaults to `canEdit !==
   * false` (back-compat: hosts that gate the modal themselves see no change).
   */
  canShare?: boolean
  /** Spinner/busy indicator node to show for in-chat loading states (e.g. the "designing" indicator). Falls back to a built-in dots animation. */
  spinner?: ReactNode
  /** Path of the currently focused file in the editor (shown first in @ picker). */
  activeFile?: string | null
  /** Paths of all open editor tabs (shown after active file in @ picker). */
  openTabs?: string[]
  /** Incremented to trigger a git status refresh (e.g. after file create/rename/delete). */
  gitStatusTick?: number
  /** Message to auto-send (e.g. from "Fix with AI"). Sent when pendingMessageKey changes. */
  pendingMessage?: string
  /** Incremented to trigger sending pendingMessage. */
  pendingMessageKey?: number
  /** When true, the pending message is sent on the user's behalf (e.g. the post-boot build kickoff) and is NOT shown as a user bubble — phase markers convey what's happening instead. */
  pendingMessageSuppressUser?: boolean
  /**
   * When true, the pending message was directly requested by the user (e.g. the
   * editor's or broken-preview overlay's "Fix with AI" button) rather than
   * dispatched autonomously (preview-health / preview-error auto-fix). A user
   * Stop suppresses autonomous automatic sends until the user re-engages; a
   * user-initiated pending message IS that re-engagement, so it always sends.
   */
  pendingMessageUserInitiated?: boolean
  /** File path the user just edited in the editor — triggers auto-deletion of queued autofix messages. */
  userEditedFile?: string
  /** Incremented to trigger the user-edit check (same path may be edited multiple times). */
  userEditedFileKey?: number
  /**
   * Whether the current user is anonymous. The shared IDE no longer renders any
   * built-in sign-up/guest card itself — guest reminders now arrive as a `custom`
   * stream event the host registers via {@link registerCustomEventCard}, and upgrade
   * call-to-actions come from {@link ChatPanelProps.buildUpgradeCta}, and the host's own
   * `buildUpgradeCta` closure decides whether an anonymous user should sign up vs. upgrade.
   *
   * It IS read for one thing: a limit error the backend raised for an anonymous caller
   * (`requiresSignup`) is dropped once this is explicitly `false` — the viewer signed in
   * mid-session (the in-IDE auth modal never navigates, so the panel keeps running), and a
   * "create a free account for more" banner with dead-end Sign up / Log in buttons is stale
   * the moment they have an account. Leave it `undefined` and nothing is suppressed.
   */
  isAnonymous?: boolean
  /** When true, user has a paid plan and can use all models (drives locked-model display). */
  isPro?: boolean
  /**
   * Retained for call-site compatibility. The periodic "sign up to keep your work"
   * reminder is no longer generated client-side — the host's backend decides when to
   * emit it as a `guest_reminder` `custom` stream event (so it can be suppressed during
   * discovery server-side). This prop no longer drives any built-in behavior.
   * @deprecated Guest reminders moved to the host-emitted `custom` event + registry.
   */
  suppressGuestReminder?: boolean
  /**
   * Builds the call-to-action button(s) shown when the chat surfaces an upgrade /
   * sign-in nudge — a locked model the user can't select, or a usage/resource limit
   * the backend reported. The shared IDE owns NO pricing or auth routes, so the host
   * supplies the button(s) here (e.g. its own `/pricing` or `/signup`). Return
   * `null`/`undefined` (the default) to render the nudge text with no button.
   * `requiresSignup`, when set, is the backend's flag that the user must sign up
   * rather than upgrade an existing plan; when unset the host's own auth state decides.
   *
   * The rest of the context is the backend's own description of the limit that
   * fired, forwarded verbatim so the host never has to guess the right button:
   *
   * - `limitType` — the RULE the backend enforced (e.g. `usage_balance`,
   *   `spend_cap`, `ai_cost`, `max_tool_loops`).
   * - `billingAction` — the REMEDY the backend resolved (e.g. `sign_up`,
   *   `upgrade`, `add_payment_method`, `add_funds`, `raise_spend_cap`,
   *   `enable_extra_usage`, `contact_support`, `owner_only`, `none`). Prefer this
   *   over `limitType` when both are present: one limit can need different
   *   buttons per account (an empty balance is "Add funds" with a card on file
   *   and "Add a payment method" without one), and `none` means there is nothing
   *   the user can do — render no button.
   * - `upgradeTier` — the plan an upgrade would move to, explicitly `null` when
   *   there is no higher tier, so a top-tier user is never sent to a plans page
   *   with nothing to sell them.
   *
   * Both vocabularies belong to the host's API, so they are plain strings here —
   * the shared IDE never interprets them. Every field is optional and may be
   * absent (a backend that sends none of them keeps the old behavior), so the
   * host must have a fallback.
   */
  buildUpgradeCta?: (context: {
    requiresSignup?: boolean
    limitType?: string
    billingAction?: string
    upgradeTier?: string | null
  }) => ChatEventCardAction | ChatEventCardAction[] | null | undefined
  /**
   * Optional app-specific section appended to the `/help` output — e.g. a plan /
   * upgrade blurb. The shared IDE has no pricing or plan copy, so the host supplies
   * the (already-localized) lines plus any call-to-action. Return `null` (the default)
   * to append nothing.
   */
  buildHelpUpgradeSection?: () =>
    { lines: string[]; action?: ChatEventCardAction | ChatEventCardAction[] } | null | undefined
  /**
   * The signed-in user's profile avatar (SOC1) — an inline `data:image/*` URI or
   * an `http(s)` URL — rendered beside their own messages in the chat timeline.
   * The host passes whatever value its user metadata holds; the shared IDE gates
   * it (`resolveUserAvatar`) so only a safe, renderable source reaches the DOM and
   * falls back to a generic icon otherwise. Omit it (the default) to always show
   * the icon.
   */
  userAvatar?: string | null
  /**
   * Display name of the AI coding agent, interpolated into all shared chat copy
   * that refers to it (the stalled-stream notice, sound-event descriptions, the
   * `/help` body, tips, `/settings` and command descriptions, the `/scripts`
   * empty state). The shared IDE owns NO product branding, so the host passes its
   * own agent brand name. Defaults to the neutral `'the assistant'`
   * (`DEFAULT_AGENT_NAME` from `@molecule/app-react`) so the package alone never
   * names a specific product.
   */
  agentName?: string
  /**
   * Display name of the host product / IDE, interpolated into shared chat copy
   * that refers to the product (the `/help` intro, the report-confirmation and
   * report-modal subheading, the command-menu version line). The host passes its
   * own product brand name; defaults to the neutral `'the IDE'`
   * (`DEFAULT_PRODUCT_NAME` from `@molecule/app-react`).
   */
  productName?: string
  /**
   * The host's current app/build version (e.g. `'0.1.0'`), shown in the `/version`
   * command's menu description and its output. The shared IDE has no build version
   * of its own, so when omitted it falls back to the package default constant.
   */
  version?: string
  /**
   * Host-specific slash commands to MERGE into the command menu, `/help`, and the
   * keyboard dispatcher, on top of the shared {@link COMMANDS} registry. For
   * commands the host handles itself (server-side intercepts or the agent), so
   * they show up in the menu instead of being invisible. Selecting one fills the
   * input with `/<id> ` (so the user can add arguments) and sending it routes to
   * the host's own handler — the shared package never dispatches these.
   *
   * The host owns keeping this list in sync with its handlers; molecule.dev, for
   * example, fetches it from `GET /ai/commands`. Ids must not collide with a
   * shared command id. Optional — omit for the plain shared command set.
   */
  extraCommands?: readonly CommandDef[]
  /**
   * URL the command-menu "Report a problem" link points at (the host's own issue
   * tracker / feedback page). The shared IDE owns no product URLs, so when this
   * is omitted (the default) the link is not rendered. The in-chat `/report`
   * modal — which POSTs to the project's own backend — is unaffected.
   */
  feedbackUrl?: string
  /**
   * Lists the project's tests for the `/test` browser. Omit it (the default)
   * and `/test` has nothing to show — the shared IDE owns no test-discovery
   * route of its own.
   *
   * molecule.dev implements it over `GET /projects/:id/tests`. It is called
   * on every `/test` invocation and once more when a run finishes, so a spec
   * the agent just wrote shows up the next time the browser is opened.
   */
  listTests?: () => Promise<TestList>
  /**
   * Runs the selected tests, streaming {@link TestRunEvent}s back as they
   * happen. Required alongside {@link ChatPanelProps.listTests} for the
   * browser's run controls to work.
   *
   * The returned handle's `cancel()` must stop the run (molecule.dev aborts the
   * SSE request, and the server kills the process tree on disconnect). The host
   * is responsible for running the e2e specs through the preview bond chain —
   * in a sandbox that means `npx playwright test` with
   * `@molecule/app-e2e-preview` as the browser, so the spec drives the live
   * preview rather than a browser binary that is not installed there.
   *
   * The run is owned by `ChatPanel`, not by the card, so it keeps streaming
   * while the browser is closed and is still there when it is re-opened.
   */
  runTests?: (selection: TestSelection, onEvent: (event: TestRunEvent) => void) => TestRunHandle
  /**
   * Whether this viewer may RUN tests. `false` still lets them open `/test`
   * and read what the project tests (the platform serves the listing to
   * viewers) but disables every run control and shows why. Defaults to
   * `canEdit !== false`.
   */
  canRunTests?: boolean
  /**
   * Whether the environment the tests run in is up — a running sandbox.
   * `false` makes `/test` say to start the project instead of listing an empty
   * browser or letting a Run click fail. Defaults to `true`.
   */
  testsAvailable?: boolean
  /**
   * Skip the executor's tool call that is running right now, without ending the
   * turn — the call comes back marked as skipped and the turn carries on. The
   * host owns the request; omitting this renders no Skip control on tool calls.
   * Resolving `false` means nothing was in flight, which is a benign race.
   */
  skipToolCall?: (toolCallId: string) => void | Promise<boolean | void>
  className?: string
}

ChatTimestampProps

Props for {@link 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'
}

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
}

ChatUserIdentity

Identity of the user whose avatar or name was clicked in the chat timeline, passed to {@link 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 {@link 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
}

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 {@link 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
}

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
}

CommandCategory

A command category with its display label.

interface CommandCategory {
  /** Stable category key referenced by {@link 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 {@link 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[]
}

CommandPaletteProps

Properties for the command palette.

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

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
}

DeviceFrameSelectorProps

Properties for device frame selector.

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

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
}

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
}

IconProps

Props for {@link 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
}

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
}

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
}

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
}

PreviewPanelProps

Props for the {@link 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 {@link 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 {@link 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 ({@link 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
}

PreviewStuckReport

Structured report passed to {@link 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 {@link 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
}

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
}

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
}

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
}

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
}

ResizeHandleProps

Properties for resize handle.

interface ResizeHandleProps {
  onResize: (delta: number) => void
  direction?: 'horizontal' | 'vertical'
  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 {@link 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
}

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
}

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 }>
}

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 {@link 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
}

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
}

SharePayload

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

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

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
}

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
}

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
}

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
}

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
}

TestFailure

One failing test and the output that explains it.

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

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[]
}

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
}

TestList

What {@link ChatPanelProps.listTests} resolves with.

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

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
}

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>
}

TestRunners

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

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

TestsCardProps

Props for {@link 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
}

TestSelection

What the bar asks the host to run.

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

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 {@link MAX_OUTPUT_LINES}. */
  output: string[]
  /** Per-id outcome from this run (and from earlier runs, until re-