@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.tsJSDoc, 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/reactAPI
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-