Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 20. októbra 2026

Prechádzať dokumentáciu · Browser bundle and runtime

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Developer reference

Browser bundle and runtime

UI API 66 adds RailRow.note, the row's second line under the name: the lead and the figure column stay on the first line, the note runs one line deep beneath the name, and the caller insets it so it sits under the name rather than under whatever the label carries before it. A bundle calling it declares requiresApiVersion: 66 in its manifest and browser registration.

UI API 65 publishes components.RailMenu, components.MarkStrip and components.HoverCard plus utils.formatSpeed and utils.apiErrorMessage. RailMenu is the rail's popover — a panel of rail rows opened by the caller's own trigger, so the collapsed rail's instrument keeps its shape and its accessible name while the panel holds rows that carry their own buttons. MarkStrip is a row of small marks that each name themselves on hover. HoverCard opens after 600 ms of pointer rest, bounds and scrolls its body and never moves focus. utils.formatSpeed is the ONE rendering of a measured generation rate (tokens per second) and utils.apiErrorMessage the host's error shaping. The bundled Subagent rail is the consumer of all three components and of both helpers.

UI API 64 publishes components.ConnectionStatus, the one presentation of a connection's separate transport and model retry lanes: it counts down only a fresh locally owned schedule and says unknown or stale observations as such. The same API requires desktop ticket expiresAt and an AbortSignal in both ticket callbacks.

UI API 63 publishes components.TerminalSurface, the shared xterm screen, together with the dock additions that let a source draw its own window: PluginDockOperation.icon and .caption, PluginOperationDockSource.view and hooks.useOperationDock().open(id, plugin, { show: true }). Mount the surface for one session with its sessionKey, the plugin route that mints a one-shot ticket and the plugin's WebSocket path; it owns the terminal, the socket, the resume offsets, the copy/paste keys and the appearance settings. A window that ENDS a session calls components.TerminalSurface.release(sessionKey) (its own X, or an acknowledgement of an exited row); one that merely minimizes does not, which is what keeps the shell alive while the reader looks elsewhere. The full prop contract, the states, the frame protocol and the caps are in the terminal surface, and the dock's view rules in the operation dock. A bundle calling any of them requires UI API 63 in both its manifest and browser registration. Dock and Project card-action icons use the host's closed, case-sensitive lucide names, for example Terminal, not terminal; unknown names render the puzzle fallback. The Terminal plugin omits the optional dock caption, keeping the project in the bubble tooltip and accessible name through the operation title. Its own bare window retains Modal.title for accessibility without a visible caption. A token-bordered, rounded frame surrounds the padded terminal surface; two shared ghost icon buttons with white minus and X glyphs float 8px inside its top-right corner on every width, without adding clearance to the even 16px terminal padding. They have no circular outline or resting background; shared hover, keyboard focus and hit targets remain intact. While the surface connects or reconnects, core renders a centered spinner and, once retry progress exists, the shared connection status with its retry action; plugins do not import or copy that block.

UI API 62 publishes utils.projectName(project) alongside components.ProjectIcon. Use this pair for every Project name and icon, including dropdown options and selected summaries. Computer Projects use the paired device's computer.label and the laptop glyph; host and managed Projects retain their slug and stored icon or folder glyph. PluginUiProject.computer describes the existing /projects projection. Pass the complete Project to the icon, never a reconstructed slug-only row. Both helpers are synchronous; only an existing stored icon triggers the shared cached image read. The Editor's administrator picker is a consumer. Sites resolves its Project ids through the existing shared hooks.useProjects() catalog before rendering this pair; a Project outside that account's catalog has no identity row, including on a shared Site. The host query cache shares the single catalog read. Text-only clients use core's packages/plugin-shared/projectChoice.mjs.

A bundle calling projectName requires UI API 62 in both its manifest and browser registration. Older hosts refuse mounting it through the existing version gate.

Browser bundle contract

UI API 61 exposes components.ResourceHistoryModal with ResourceHistoryModalProps. Use it for a resource ring's stored history: pass the initial metric, description, close callback and a useHistory hook returning validated data, loading/error state, retention bounds and retry. An optional scope slot holds your Project picker. The host owns the range picker, coloured inline metric checkbox pills, empty/error states and chart; keep authorization, account-scoped query keys and polling inside your hook. Sandbox is the plugin consumer. Closing the frame unmounts its reader. A host without API 61 refuses a bundle requiring this component.

For a single-choice mode or filter, use components.Segmented with options, value, onChange and aria-label. Each option supports a decorative icon or iconContent, count and disabled. The host renders a bordered control well and a raised active pill using the current skin's opaque sidebar-accent/sidebar-accent-foreground pair, matching the active navigation row without hover. Only enabled inactive options receive a faint accent hover wash. Labels remain whole and the track scrolls horizontally by default, with native touch swipe, hidden scrollbar, selected-option reveal and keyboard radio semantics. Do not wrap or shrink its options in plugin CSS. variant="line" is reserved for distinct record-section navigation; collection-view filters use the default presentation. Existing runtime Segmented consumers inherit the change without rebuilding or a new API version, including Stats scope and grouping controls.

UI API 57 publishes exact ChoiceFieldProps, ProjectFilterPillsProps, SelectionSummaryProps and SummaryChipProps from the kit. Managed selections have one summary card; the unused summary/chip variant="line" is removed. Registry browser tests load the selected candidate's actual choice, project-filter and summary components rather than copying retired presentation branches. UI API 57 also publishes the existing components.DataTableSelectCell with its discriminated header/row props. Pass header for the current-page select-all, or both rowId and label for a row. Its enclosing DataTable owns the current-page ids, selection and change callback; without that selection context the cell renders nothing. The host's Memory register is the consumer; a card register ticks cards with CardSelectCheckbox and clears them through SelectionActionBar (API 25), as MCP's does. Consumers requiring the new component must declare API 57 in their manifest and registration. Selection performs only local set updates until the caller invokes a bulk action.

Every single-choice field uses the anchored Select surface, regardless of catalog length; ChoiceField.picker and ProjectFilterPills.variant no longer exist. Editor renders <C.ProjectFilterPills value={projectId ?? 'all'} onChange={setProject} includeAll={false} />. The host prepares the project catalog through its existing query and omits the filter when fewer than two choices exist. Callers own selected values and persistence; no additional requests or storage are introduced. Update consumers in the same release as the host because a maximum API version check cannot protect a bundle that still passes a removed property.

Catalog groups

components.MarkdownAssetEditor groups the current page with the existing ownership.scopes predicates, labels and icons. Skills supplies personal, instance, bundled and plugin scopes; personal entries start open, the others closed. Without scopes, custom agent definitions start open and built-ins closed. Do not call instance-wide definitions personal. Scope filters need not be exhaustive: unmatched entries retain their source group. Empty groups are omitted, and search or explicit scope/source filters reveal matches. The component preserves its pager, forms, bulk actions and caller-owned permissions.

For MCP's default-ordered register the equivalent composition is <C.SettingsGroup title={s.scopePersonal} icon={UserCog} collapsible defaultOpen>{cards}</C.SettingsGroup>; its instance group uses defaultOpen={false}. Do not pass storageKey: list expansion lasts only for the mount. The composition performs no requests or persistence and does nothing without a mounted catalog. This changes presentation only and adds no runtime members.

UI API 58 publishes components.SettingsValue, SettingsState, SettingsList, SettingsItem and SliderWithReading beside SettingsGroup, SettingsRow and StatusDot: the content primitives of every window and of the Project drawer, identical to Settings. A web.project, web.user or web.settings panel returns SettingsGroups (never its own page wrapper), each holding SettingsRows with a description and a right-aligned control, or a SettingsList of SettingsItems for records; a read-only value is SettingsValue and an empty or failed read is SettingsState. State is a StatusDot plus a word; colour means state only. PropertyGroup, PropertyRow and DetailBlock are no longer published. They need no requests and set requiresApiVersion: 58. Details: seam catalog, UI API 58. Inside the Project and User drawers the host draws these same components in its compact drawer presentation (a group's title above one hairline-divided box, labels in one column with the value after them, list items and states as lines of that box); a panel passes the same props and needs no change or check of its own.

UI API 59 publishes hooks.useProjectCommits(id, { limit, anchor, enabled }): the Project's commit log read in ANCHORED pages of the host's own query cache. The result is react-query's infinite query — data.pages of { commits, hasMore, anchor }, hasNextPage, isFetchingNextPage, fetchNextPage() and refetch() — so a panel can unfold a history in place and ask for the next page as the end of the list comes into view. anchor is the commit the WHOLE walk starts from and the caller names it (the head a hooks.useProjectGit snapshot reported), because every page, including a first page re-read after a failure, is asked for from that same commit: that is what stops a reload from shifting the window onto a newer history and showing one commit twice. null lets the daemon anchor at the current HEAD, for a caller with no snapshot. The read carries no new request shape or polling; one call is one page. A bundle using it declares requiresApiVersion: 59 in its manifest and its registration, since an older host has no such hook to call.

UI API 56 publishes utils.uploadProjectFile(file, { projectId, directory }, { signal, onProgress }). For example, Editor passes its selected relative Project directory and renders durable byte progress with the existing host Progress component. The helper returns a stored { name, path, relative } or { failed: true, reason?, cancelled? }. It opens /api/brain/uploads/sessions, streams Blob parts, recovers from dropped connections using the server's offset and commits the verified size. Part retries use the host's authenticated finite request policy; only validated durable byte progress renews an episode, and open/commit are never retried. See upload ownership. Abort the signal to cancel and discard staging. There are no file type or total size limits; occupied names gain suffixes. Accounts must hold current Project write access, rechecked at part and commit; directories cannot leave the Project. Open sessions expire after idle time and are lost on restart. Without a call it performs no work; memory is bounded by the part and guest chunk, with no inference cost.

The implementation stays in web/lib/projectUploads.ts inside the Next bundle root. Chat calls it directly with { sessionId }, retaining its conversation-owned attachment layout, and plugins call the host runtime rather than importing web/ or bundling a copy. UI-kit imports are type-only.

The current browser runtime contract is API 66, the value of PLUGIN_UI_API_VERSION in packages/plugin-ui-kit/index.d.ts and web/lib/pluginUi.tsx. The runtime publishes no LiveTail component and no generic mcpServers config editor; the terminal and MCP plugins own their live management surfaces. The host exposes the runtime as window.ElowenUiRuntime. It also defines window.__elowenRegisterPluginUi(plugin, registration) (web/lib/pluginUi.tsx), which the bundle calls to register its components.

A bundle declares requiresApiVersion in the registration object. It loads when that requirement is no newer than the host runtime. API 26 deliberately removes the cron-specific browser hooks and schedule parser: Cronjob owns its own requests and validates schedules through its server preview. The maximum-version check does not shield an older bundle from removals on a newer host; update installed bundles in the same release.

The runtime provides:

  • apiVersion.
  • The host's react, reactDom, and jsxRuntime instances.
  • Curated components, including shared layout, dialog, icon, project, and autosave components. API 19 publishes SectionDeck and DeckNavigation with their typed DeckNavGroup, DeckNavItem, and DeckNavMatch records. API 22 publishes Textarea. API 24 publishes CardGrid and CardGridItem, the host's card register: layout="list" is one full-width card per row, for entries read as a line (a name and the sentence saying what it is for), with the name as the one open control. A plugin's short register draws the same cards as the built-in ones instead of a table approximating them. API 25 publishes SelectionActionBar and CardSelectCheckbox, the floating bulk-action bar and the per-card select checkbox extracted from the memory register, so a plugin's card register ticks and clears the same selection: the count line, the action buttons and the X clear button in one bar, with activation that never opens the entry underneath. API 27 publishes the chat rail grid: RailSection, RailList, RailRow, RailDot and ShimmerText (see docs/WEB.md, "Bounded shared UI"). API 28 publishes the checklist TaskList and its row TaskRow (see docs/WEB.md), so a plugin's task list draws the host's rows, menu and strings. Plugin pages get the host's closing instance-name wordmark automatically: the shell renders it after every page, so a bundle renders none of its own.
  • Curated hooks, including useAutoSaveStatus, usePluginConfigDraft, and useToast. useToast returns the same object for old bundles using either { toast } or feedback.toast(...). API 29 adds promise and dismissKey (typed in the kit as PluginToast); bundles narrow them as optional on older hosts and feature-detect them. Promise feedback propagates the operation result and rejection. Supply curated localized text, not exception strings. Omit success feedback where the UI already confirms the action. The host's usePluginConfigDraft already raises the trailing "changes saved" confirmation, so consumers of that draft must not announce the same save a second time.
  • Curated pure utils. API 21 requires a viewer locale for buildUsageSummary, so a plugin cannot render English-only figures. API 31 publishes the shared workflowLabel and workflowCounts utilities. API 34 publishes utils.uncollectedChildResult(run) and utils.railProcessVisible(process). The bundled Subagent and Terminal rails require API 34 and filter their supplied rows through these helpers before rendering either density. Running children and pending result deliveries stay visible; only running detached processes qualify. Terminal foreground handles remain in the snapshot for their tool calls but do not own rail space. Missing rows produce no section; without either plugin its rail slot is absent. Each predicate is constant-time, filtering is linear, and neither helper performs I/O or changes the snapshot. Hosts below API 34 refuse requiring bundles before mount. API 32 publishes utils.cardTasks(cards) and utils.cardTasksAddressable(rows) from the host's existing web/lib/railTasks.ts. Todo calls the first with [card], then the all-or-nothing id guard before rendering interactive rows. It preserves structured labels, owner, unresolved blockers and measured times, without parsing glued text or fetching a task route. No Todo card yields an empty array; missing task ids fail the guard. Work is linear in the supplied cards and Todo rows and creates no network request. Todo's rail slot receives TaskRailData: already-projected, addressable rows from the host, passed directly to TaskList rather than reconstructed as session tasks. The host normalizes incoming plugin cards at the emit boundary; the slot payload is internal host data, not a second untrusted wire boundary. No registered Todo surface means the normal host fallback remains in charge.
  • API 23 publishes two utils a plugin surface should not carry its own copy of. utils.formatUsd(value, locale, decimals = 2) is the ONE localized USD rendering: a null amount means nothing reported a price and renders an em dash, never $0.00, so an unpriced bucket is never presented as free. The host renders every usage figure at 4 decimals and passes that explicitly; 2 is the default for a bundle's own amount. utils.impersonateUser(userId, landing?) hands the reader into another account's own session through the host's identity transition, the same elowen:auth-transition protocol every account-scoped surface on the page listens for, including the host's in-flight guard, which refuses a second transition while one is running. It resolves once the new identity is the one in the cookie, and rejects with the reason core gave, such as forbidden, having rolled the transition back first. The optional landing is where the shell opens as the new account, such as "/account?cat=cli"; without one it goes to /dash. It must be a path in THIS app, and the host decides that, not the caller: an absolute URL, a protocol-relative one, a relative one, a javascript: string, anything containing a backslash, and anything that normalises to a leading // are all refused before the switch starts, with landing must be a path in this app. Pass the path a reader should arrive on, not a URL you assembled from host state. Do not re-declare the transition event, its storage key or the request around it in a bundle; do not offer a stop-impersonation control from a plugin, since the host shell owns leaving an impersonated session. And do not print a price with your own Intl.NumberFormat call, which is how an unpriced amount becomes $0.00.
  • Authenticated same-origin api(path, init).
  • SPA navigate(href).

A plugin page whose manifest sets presentation: "overlay" opens as a page overlay, and its components receive surface: "deck". An overlay plugin with peer sections composes the same frame as Settings and Account from the published components. The host bounds the modal body; SectionDeck fills it and owns the one vertical content scroller. Do not wrap the deck in another scrolling or shrink-preventing container.

const { SectionDeck, DeckNavigation } = window.ElowenUiRuntime!.components;

function ChatbotDeck() {
  const [active, setActive] = React.useState("bots");
  const groups = [{
    id: "chatbots",
    items: [
      { id: "bots", label: "Bots", icon: Bot, current: active === "bots", onActivate: () => setActive("bots") },
      { id: "statistics", label: "Statistics", icon: ChartNoAxesColumn, current: active === "statistics", onActivate: () => setActive("statistics") }
    ]
  }];

  return (
    <SectionDeck
      testId="chatbot-deck"
      contentLabel={groups[0].items.find((item) => item.current)!.label}
      navigation={(layout, className) => (
        <DeckNavigation
          label="Chatbot sections"
          groups={groups}
          layout={layout}
          testId="chatbot-navigation"
          emptyLabel="No sections"
          className={className}
        />
      )}
    >
      {active === "bots" ? <Bots /> : <Statistics />}
    </SectionDeck>
  );
}

A bundle using these API 19 components sets both manifest web.requiresApiVersion: 19 and registration requiresApiVersion: 19. New bundles should target the current contract, API 66.

Build browser sources with the elowen-plugin-ui-kit package and the plugin web build command. The bundle must use the host React instance and must not ship another React, query client, or import from the host web/ application. If the prebuilt host CSS does not contain a utility the plugin needs, ship css in the manifest. The host serves bundle and stylesheet URLs with content hashes.

Design tokens live in web/app/styles/tokens.css and are mirrored for plugins in packages/plugin-ui-kit/theme.css; a contract test enforces the mirror. A plugin stylesheet is compiled against the mirror, so never rename or remove a token — add beside it. A renamed token silently paints the stale fallback instead of the host's live value.

Typography comes from the same tokens and nowhere else. Use the role classes (text-page-title, text-section-title, text-card-title, text-ui, text-meta, text-small, text-figure, text-body) or var(--text-<role>) in a stylesheet; each role carries its size, line height and weight, so a bundle reads at the size of the Settings window and needs no font-medium. Do not write Tailwind's text-xs, text-sm, text-base or text-lg, an arbitrary text-[13px], a raw font-size or a font-family. Use font-mono only for real code: code blocks, paths, hashes, ids, log output and command lines; numbers stay sans with tabular-nums. Nothing is smaller than text-small (12px). The 11px text-caption and 10px text-tiny were retired in October 2026; a bundle built before then keeps the kit's old fallback size until it is rebuilt. A plugin that needs a size the roles do not give asks for a new role in the host's tokens.css rather than inventing one. web/tests/styles/typeScaleGuard.test.ts holds the bundled plugins to this.

Text links have one look, the host's: the surrounding text colour, always underlined, the brand colour on hover. Put the host class inline-link on an <a> or a <Link>, and inline-links on a container of rendered Markdown so every anchor in it gets the look. Do not write underline, underline-offset-*, hover:underline or a text-primary link colour of your own. The classes live in the host stylesheet, so they need no UI API version; on an older host they do nothing and the link renders as plain text.

A text ACTION is not a link (owner, 13 Oct 2026): it is C.Button with variant="ghost" — no underline, quiet at rest, the accent wash on hover — and size="sm" where it sits inside a line or a row. Retry buttons, disclosure toggles, Show all, Choose icon and Map repository are the controls the host and the registry plugins draw this way. Never put .inline-link on a <button>.

Register UI components like this:

window.__elowenRegisterPluginUi?.("my-plugin", {
  requiresApiVersion: 66,
  pages: { "": RootPage },
  account: { connection: ConnectionPanel },
  user: { details: UserPanel },
  project: { status: ProjectPanel },
  settings: { settings: SettingsPanel }
});

The registration object accepts operationDock, pages, account, accountChip, user, userStatus, project, projectRows, projectEditIcon, dashboardMetrics, chatPickers, chatCards, chatRailSections, chatDock, historyBranches, settings, and ownsPageFrame. Page keys are slash-joined route patterns; "" is the root page. Page components receive plugin name, captured params, remaining path segments, surface, and optional save-state reporting. An account section is placed in the Account menu by its manifest entry's order (see the manifest guide); the registration only supplies its component. User and Project panels receive the selected host DTO and a panel id. The one chatDock component receives the conversation's Project, its session, the live narration and the pending-input notice.

operationDock is a PluginOperationDockSource that lists the plugin's long-running operations in the background operations dock. It implements list, get, acknowledge and decodeEvent for the event kind named by eventKind. The host owns account filtering, query lifetime and the event stream, and the plugin validates its own wire rows.

These contribution slots are real runtime seams, not plugin-name switches. projectRows receives the bounded Project projection. dashboardMetrics receives the dashboard's documented metric props. chatPickers are local browser choosers keyed by command and do not create a model turn. chatCards render opaque BrainCard ids before the read-only generic StaticCard, drawing the card the daemon pushed for the session on screen rather than rebuilding it from a route. chatRailSections render expanded and compact variants while the host owns rail geometry and SSE; in all three, sessionId is the conversation the plugin's routes may act on and is null while a delegated child or a read-only transcript is shown; an expanded section draws its head and rows with the published rail grid (API 27) rather than its own markup. historyBranches render a branch below a conversation row inside a named branch region, while the host owns pagination, grouping, search, and authorization. Contributions return standalone semantic list/disclosure content, not table-row or table-cell fragments. The parent/items/expanded/toggle/open props remain unchanged; the generic fallback uses the same list-compatible mount. Cronjob's matching renderer is Cronjob 0.6.49 and requires core 0.30.7; update core and Cronjob together before using the new list host. There is no table/list compatibility flag or legacy renderer. Their open(target) callback uses session:<percent-encoded-id>:<0|1>; Cronjob encodes its run's session id with encodeURIComponent and sets 1 only for a continuable run. Subagent uses session:<percent-encoded-id> without a flag. Both views of the unified core list decode these targets once through parseBrainSessionTarget in web/lib/pluginChatUi.tsx, with no I/O; malformed or non-session targets do not navigate. Normal history branches preserve the continuation flag. A target without a flag, as Subagent sends, opens a read-only delegated transcript. Daemon authorization remains authoritative. An absent renderer keeps the existing generic navigation. chatDock is mounted once per chat surface for the conversation on screen, independent of the loaded turns. A missing or failed optional slot is omitted or uses its stated generic fallback; it is never represented by a successful empty component.

The optional cron.conversationLinks(input) control method contributes job metadata for the core-authorized conversation ids. Core rechecks job visibility and transcript ownership in src/api/routes/brainConversationLinks.ts; it derives the navigation prefix from the current registry's actual cron owner, using /p/<encoded-owner>?job=<encoded-job-id>. Plugin-supplied URLs are ignored. The registry Cronjob is the current consumer, but the package name is not fixed in core. Absent control, absent method or missing account grant answers unavailable; a failed read answers error, and an empty successful read remains available. The core sub-agent branch keeps its own independent status. This is one display-only read per listing and creates no job, schedule or transcript mutation.

A settings component is mounted in the settings deck and can also be served as a standalone plugin page. Add its id to ownsPageFrame only when the bundle renders the complete page frame and owns its own save indicator.