Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · Runtime bundles and localization

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

Runtime bundles and localization

ElowenUiRuntime.api accepts PluginApiRequestInit. For a JSON write, call runtime.api(path, { method: 'PATCH', json: value }); the host serializes the value, sets application/json, preserves other request options and headers, and uses its existing authentication, deadlines and restart recovery. JSON writes without a method default to POST. Omitting json preserves raw RequestInit behavior, including FormData uploads and ordinary GET reads; the types exclude supplying both json and body. Responses remain unknown and the consumer must validate its own domain response. Serialization does not authorize a request or replay an unconfirmed write. The cronjob schedule preview is a real JSON consumer after migration.

The bundled MCP register uses host utils.interpolate(template, values) for row labels, tool counts, reconnect summaries and removal confirmations. Its runtime declaration uses ElowenUiRuntime['utils']['interpolate'], which UI API 55 publishes; the registration itself requires UI API 58 (plugins/mcp/web-src/index.tsx:18, plugins/mcp/elowen-plugin.json:52), so the bundle names the later API its own members need. Each template receives all values in one call; unknown placeholders remain literal and inserted values are not reparsed. The helper performs no I/O. Older hosts reject the bundle through the existing version gate; there is no local interpolation fallback.

Runtime plugin web bundles

Human-facing plugin state registers once through ctx.registerSurfaceFact(surface, resolve): panelVisibility, navBadge, brainStatus, projectIndicators or readiness. The surface selects the existing callback and response shape in src/plugins/surfaceFacts.ts; resolveSurfaceFacts retains account grants, web-admin classification, provider order and Project timeouts. Visibility and badges remain unique per plugin, and readiness remains instance-wide. A panelVisibility resolver may return its panel selection or a Promise of it; the host awaits it before listing panels and hides that plugin's account and Project panels if it throws or rejects. A navBadge resolver may likewise return its count or a Promise of it; the listing awaits it, and a value that is neither null nor a finite number is logged as plugin.nav_badge_failed and drops only that badge. Use local database or config reads only, never external requests, because every listing waits for the probe. The OneDrive plugin awaits its local Microsoft identity lookup and returns { project: [] } when linked is false, or null to keep the declared panels when linked. An absent resolver or a null result leaves all declared panels visible. For example, Changelog registers navBadge for each account's unread count. Missing registration yields no fact, and each surface retains its own failure behavior. The call arrived with shared API 10 and core 0.29.99; every bundled manifest now declares requiresSharedApi 14. Browser slots and listing responses are unchanged.

Plugin UI is discovered from the authenticated /api/plugins/ui listing. The listing supplies the plugin name, compatibility version, navigation and settings registrations, localized strings, bundle URL, CSS URL, and optional layout and presentation declarations. web.presentation: "overlay" keeps the normal /p/<plugin> navigation address but mounts the page through the same PageOverlay used by Settings and Account. Shell navigation updates that address in place so the current page stays mounted beneath the modal; on a hard load the canonical plugin page stays empty while the shell mounts the same modal.

web/lib/pluginUi.tsx loads an approved bundle at runtime with a same-origin module script. It does not use a static bundler import because the URL is runtime data. The host installs window.ElowenUiRuntime, which provides the host React and JSX runtimes, curated components, hooks, authenticated API access, utilities, and SPA navigation. The bundle registers pages and settings through window.__elowenRegisterPluginUi.

Plugin source bundles are built by scripts/build-plugins-web.mjs. A plugin with web-src/index.tsx, index.ts, index.jsx, or index.js is bundled to its web/index.js and receives generated CSS. Plugins without web-src/ keep their checked-in bundle. The root npm run build runs this step before copying plugin files into the distribution. The shared elowen-plugin-ui-kit/build helper owns browser emit settings through an explicit inline esbuild tsconfig, independent of the core or plugin checkout's compiler configuration. It emits ES2022 ESM with automatic JSX and host React shims; ESM is inherently strict, so it does not inject ambient alwaysStrict directives into CommonJS shims. Both the bundled Terminal UI and registry browser UIs use this helper, and build errors propagate to their drift gates.

Plugin code must use elowen-plugin-ui-kit and must not import web/. The host UI API version is the one PLUGIN_UI_API_VERSION declares in packages/plugin-ui-kit/index.js, mirrored by the host in web/lib/pluginUi.tsx and enforced in lockstep by the web typecheck. Each member in packages/plugin-ui-kit/index.d.ts is annotated with the API number that added it. The host checks the declared version before loading a bundle, catches bundle failures with PluginErrorBoundary, and shows an explicit unavailable or incompatible state. Plugin pages use document or workbench layout as declared by the manifest. A page reached directly receives host framing unless the registration lists the section's id in ownsPageFrame.

API 30 adds hooks.useInvalidatePluginUi(), which returns a callback refetching the plugin listing the main navigation is built from, so the count a plugin's registerSurfaceFact('navBadge', fn) probe reports moves right after the plugin's own change; with the listing unmounted it only marks it stale. API 26 adds hooks.useInvalidateConversationLinks(), which returns a callback invalidating the host's canonical conversation-link queries in both mine and all scopes. It uses the shared QueryClient once; if neither query is mounted, it only marks cached data stale and makes no request. The Cronjob plugin uses it after schedule changes without depending on a private host query key. The same coordinated API cut removes host useCronJobs, useSaveCronJob, useDeleteCronJob, utils.isValidSchedule, and the cron methods on utils.elowenClient; Cronjob owns their replacement queries, mutations and schedule validation. No plugin mounted means no Cronjob browser behavior, and older bundles using the removed members must be updated before this host API is published.

API 27 publishes the telemetry rail grid (RailSection, RailList, RailRow, RailDot) and ShimmerText, described under "Bounded shared UI". RailMeter and RailSectionHead were published too until API 45 withdrew them unused. The bundled MCP, Subagent and Terminal sections and the registry Todo and LSP sections require it.

API 28 publishes the checklist on that grid (TaskList, TaskRow), described under "Bounded shared UI". It only adds. The registry Todo plugin's rail section and card require it; a Todo bundle built for API 27 keeps rendering its own rows.

API 65 publishes RailMenu, MarkStrip and HoverCard plus utils.formatSpeed and utils.apiErrorMessage, described under "Published telemetry primitives". They exist because a rail row is a thing with its own controls — a stop button, a mark that names its model, a brief behind a name — which a menu of actions cannot carry. The bundled Subagent rail is the consumer; a bundle built for an older API keeps rendering its own rows.

API 67 publishes utils.workflowElapsedSeconds, the run elapsed the workflow inspectors show, so the bundled Subagent rail's workflow row counts the same clock instead of deriving its own.

API 66 adds RailRow's note, the row's second line under the name. A row that carries a status note then keeps the name whole and gives the note a line of its own, and the caller insets the note so that it sits under the name. The bundled Subagent rail is the consumer; a bundle built for an older API keeps rendering its own rows.

Plugin registrations can contribute pages, Account sections, User panels, Project panels, and settings sections. The plugin owns its domain state and authenticated plugin API. The host provides shared localization, query infrastructure, autosave contracts, navigation, overlays, and UI primitives.

Sandbox registers through the public PluginUiRegistration and types its Project contribution with PluginProjectRowsHook, PluginProjectRowStatus and PluginProjectRowAction; its browser runtime declares only the members it consumes and retains AssertPublished for local component names. plugins/sandbox/web-src/environmentPresentation.ts owns the seven environment states used by both the register and ProjectEnvironmentSettings: icon name/component, tone, drawer color class and busy flag. This lookup adds no reads or subscriptions and preserves the existing classes, including danger as text-destructive. Before a register mutation, Sandbox requires the current generation from its existing usage map. A confirmation surviving a 401/403 usage refetch reports the existing localized Project-access refusal without persisting an intent or sending a mutation when that generation is absent. The drawer consumes the required runtime projection from the current project overview directly, including an explicit null runtime name or readiness; it does not synthesize an older response. Without a Sandbox contribution, the host adds no environment state or lifecycle actions.

Three runtime members replace local copies a plugin would otherwise carry, so a plugin UI should use them instead of bundling its own:

  • ElowenUiRuntime.C.ConfirmDialog for any destructive or discarding confirmation. It renders in place inside the host overlay stack with the host focus return and backdrop rules; a plugin-local confirm modal would diverge from both.
  • utils.formatBytes(bytes, locale?) for a human-readable size with the host's binary units, so byte labels agree across the host and plugins. Pass the app locale for localized digits, such as 1,5 GB in Czech and Slovak; omission keeps the compact English rendering. Sandbox resource cards and history share this helper.
  • utils.renderMarkdown(source) for sanitized HTML from Markdown. It is the same parser and sanitizer the host uses for chat and assets; a plugin that injects its result needs no parser or sanitizer of its own.

The registry editor plugin is the reference consumer of all three: its discard and delete confirmations, its binary preview and status bar sizes, and its Markdown preview.

utils.localDateTime(timestamp, locale?, seconds?) is the UI API 44 formatter for ISO 8601 timestamps in the viewer's local time zone; core API and host-store timestamps are ISO 8601 UTC. Pass the application's locale from hooks.useTranslation() and false as the third argument for minute precision: utils.localDateTime(date.toISOString(), locale, false). Seconds default to enabled; an unparseable string is returned unchanged. Numeric timestamps must be converted to ISO by the caller, which also owns its empty and invalid-value labels. The formatter performs no network request or subscription. Without a formatter call there is no formatting work; hosts lacking API 44 do not supply this utility. Teams identity timestamps, OneDrive mirror status and Sandbox snapshots use this existing host boundary.

utils.paginateItems(items, page, pageSize) returns the clamped page and its visible items with the same rule the host registers use. The bundled MCP server register is its consumer. An older host without it leaves the name absent, so a bundle that needs it must require a host that ships it.

Remote CLI pairing and downloads

Pairing, the install command and device revocation are core browser surfaces, not a plugin page: see "Account > Computers" in docs/modules/web/accounts.md.

The paired target's process adapter is shared by the core executor and the Sandbox runner through createLocalProcessSession(startOperation, request, signal?) from elowen-plugin-shared/localExecution. It reconstructs stdout, stderr, cwd, stdin and the final exit from the canonical process results. Sandbox metadata, account leases and turn admission remain with each control owner. The daemon passes a request closure over the admitted socket, so releasing the parent turn does not redirect or end an admitted background process; disconnect or replacement still terminalizes that process with its original typed loss. Cancelling aborts that same request and waits for its exit or failure. A process-stdin rejection with code process_not_found means the executor has already removed the exited process: the adapter ignores only that late-input race for both writes and EOF, and closed retains the actual exit result. This lets consumers such as Terminal background processes close stdin without an error listener for an already exited process. All other stdin failures still reach the write/end callback and emit the Writable's error event; consumers that supply input must handle those events, as with a local child.

Internationalization

Core web copy is stored in the locale dictionaries under web/lib/i18n/dictionaries/. LanguageProvider in web/lib/i18n/context.tsx exposes the active locale and dictionary through useTranslation. The locale is mirrored to a cookie so the server can render the first response in the selected language, and to local storage for client persistence. The daemon also stores the account locale in the locale user setting. The web client synchronizes that setting through /auth/me/locale, while daemon-generated alert and push text reads the stored value directly rather than the browser cookie. Use dictionary keys and interpolate; do not hard-code user-facing strings in feature components.

Plugin strings come from the plugin UI listing. The manifest is the canonical English source for the plugin label, description, navigation and web.strings; i18n/cs.json and i18n/sk.json contain the corresponding overrides. A plugin with a manifest label also supplies a top-level label in each override. The host merges manifest English with the active locale's web.strings and exposes the result to the bundle. Manifest string keys are checked by contract and language validation tests.

Development, build, and tests

web/next.config.ts keeps Turbopack's root at web/, which makes Next emit .next/standalone/server.js where scripts/build-web-bundle.mjs expects it. Values from src/ or packages/ therefore never bundle into the browser: shared rules reach the web as byte-identical mirrors in web/lib/, pinned by tests/contract/webMirrorContracts.test.ts (see components). Typecheck and Vitest resolve cross-root imports anyway, so only npm run build:web proves the boundary.

Install root and web dependencies independently:

npm ci
npm ci --prefix web

Run the daemon and web development server in separate terminals:

npm run serve
npm --prefix web run dev

Build the web artifact from the repository root:

npm run build:web

For a direct Next.js build:

npm --prefix web run build

The web package scripts also provide:

npm --prefix web test
npm --prefix web run e2e:smoke
npm --prefix web run e2e
npm --prefix web run typecheck:e2e

The fake daemon's web/tests/e2e/fake-daemon/pluginMetadata.ts owns lazy, process-cached manifest and complete locale discovery from bundled plugins followed by E2E_PLUGIN_DIRS. Call pluginMetadata() to obtain ordered candidates, including duplicate directory names across roots. handlers/pluginRegistry.ts selects the first readable manifest for settings and projects only label, description, user-config label and field translations. realPlugins.ts independently selects the first usable in-directory browser bundle and applies the full browser locale data; an earlier manifest without a built bundle must not hide a later usable bundle. Missing or malformed JSON files are skipped, absent locale directories remain undefined for settings, and browser UI stays opt-in through seed.realPlugins(). Discovery costs one manifest/locale read per candidate per process; asset hashing and loading remain with the browser projection. web/tests/lib/pluginMetadata.test.ts characterizes root precedence, locale projections and the shared cache without starting the E2E servers.

Plugin-settings acceptance uses requiredPluginSectionHref from web/tests/e2e/fixtures/pluginSettingsContract.ts to resolve contributed-section addresses through the host's pluginNav functions and manifest placement. Delayed API fixtures hold all initial reads until the test releases them, then switch to the retry phase explicitly; development remounts do not count as user retries. Error assertions are scoped to the owning form or section because the shell also owns an assertive live region. collectPluginErrors accepts cancellation only for opt-in read methods, paths and exact queries with net::ERR_ABORTED; Sandbox's abort-aware desktop overview and read-only usage batch POST also assert successful replacement snapshots and the batch request's project ids. Other transport failures and every unexpected HTTP error still fail acceptance.

Use focused Vitest tests for changed components and libraries. For routing, authentication, streaming, responsive behavior, or plugin loading, run the relevant Playwright coverage as well. Before handing off a web change, run the focused tests, npm run build:web, and any affected contract or language checks. For a broader repository check, npm run check covers lint, dead-code, dependency, daemon typecheck, and language validation; it does not replace the web build or web test suite.

Owning code

  • web/lib/pluginUi.tsx: loads approved bundles, installs window.ElowenUiRuntime and publishes the components, hooks and utilities.
  • web/lib/pluginChatUi.tsx: renders the chat plugin slots, including the chat dock host.
  • web/lib/i18n/context.tsx and web/lib/i18n/dictionaries/: core locale context and dictionaries (cs, sk, en).
  • scripts/build-plugins-web.mjs and packages/plugin-ui-kit/build.js: the browser bundle build for plugins/*/web-src/.
  • packages/plugin-ui-kit/index.d.ts: the public UI API contract and version.
  • web/tests/lib/pluginUi.test.ts and web/tests/lib/pluginMetadata.test.ts: contract tests for the runtime surface and plugin metadata.
  • web/tests/e2e/fake-daemon/pluginMetadata.ts: manifest and locale discovery for end-to-end tests.

Published UI timestamp helpers

ElowenUiRuntime.utils now types the functions already published by the host: compactElapsed(ms: number): string and parseTs(iso?: string | null): number | null. Minimal use: const at = runtime.utils.parseTs(reply.createdAt); return at === null ? null : runtime.utils.compactElapsed(Date.now() - at);

compactElapsed uses the host's single-unit seconds/minutes/hours/days ladder and clamps negative elapsed time to zero seconds. parseTs parses ISO 8601 timestamps and returns null for absent or invalid input. These are local pure helpers with no requests, timers or registration cost. The host publishes them through ensurePluginUiRuntime; an unused helper has no effect. BrowserAccount replyAge is a real existing consumer. After the first containing core release its declarations may require both functions, but the null branch for an invalid timestamp remains. No runtime implementation or UI API version changes in this producer cut.

Plugin cards in Settings

web/modules/settings/PluginsSection.tsx keeps one module-local PluginCardFrame shell that every card on the page composes: the installed PluginCard, the registry MarketplaceCard of the Available view and the RemovedCard of a soft-removed bundled plugin. Supply name, title, version, the optional source, description, dependencies, counts (tools, skills, platforms), the caller's lead, trail and optional details, plus busy, running, note, onOpen, openLabel and onContextMenu. The frame is a CardGridItem marked data-plugin-card, exposes busy state through aria-busy and a dimmed card, draws the PluginIcon with a running dot, the name with PluginVersion, DependencyPills and the ProvidesBadges counts, and wraps the trail under the identity on a narrow card. With onOpen the card surface opens the plugin for the pointer and the name becomes the keyboard's one open control; every other control stops its click from reaching the surface. MarketplaceCard supplies registry identity, the install button and queued-operation progress; RemovedCard supplies the localized installed identity and the Restore button, which stays disabled while pending and restores the plugin disabled. Callers own the action handlers and disabled semantics; the frame adds no requests, lifecycle policy or plugin-facing UI API.

Explicit Skills mutation hook owners

The web runtime's useCreatePluginSkill, useUpdatePluginSkill and useDeletePluginSkill mutation inputs require owner: an account ID, 'instance', or null for the current account. SkillsPicker obtains const remove = hooks.useDeletePluginSkill() during render and later calls remove.mutateAsync({ name: pending.name, owner: pending.owner }) to delete the selected skill. A mutation acts only when mutate or mutateAsync is called; an omitted owner is a TypeScript error and has no inferred default. These hooks forward the existing payloads, invalidate the plugin-skills catalog, and preserve update rollback and name-plus-owner optimistic matching. Server authorization and revision checks remain authoritative; the hooks grant no permissions.

The underlying browser client requires SkillOwner in all three mutations: createPluginSkill({ name, description, content, owner }), updatePluginSkill(name, patch, owner), and deletePluginSkill(name, owner). SkillOwner is number | 'instance' | null; null is serialized as owner=me. Owner stays in the query, outside create/edit payloads. Minimal use:

await elowenClient.createPluginSkill({
  name: 'report-guide',
  description: 'Use for reports.',
  content: 'Check the source.',
  owner: null,
});

An unused mutation hook makes no mutation request. The registry Skills plugin's SkillsSettings uses these hooks with its selected owner; its SkillsPicker and CLI deletion picker also choose one explicitly. The registry runtime declarations and test-host hooks use the same required selection. See the Skills HTTP ownership contract.

Published page filter fields

PageFilterField is a type exported by elowen-plugin-ui-kit. Minimal use is import type { PageFilterField } from 'elowen-plugin-ui-kit'. Every field has id, label, control and optional hint; active: true also requires activeLabel and onReset, while active: false forbids those properties. This keeps active-chip and reset information paired with the active state. Core PageFilters and PageToolbar consume the published union directly; MemoryView, MarkdownAssetEditor and PluginsSection use it for their existing filter rows. A consumer needing the active subtype uses Extract<PageFilterField, { active: true }>. The former local PageFilters definition/export is removed. It is declaration-only, adds no runtime work or UI API version, and unused consumers have no effect. Registry/licensing adopters remain tied to their actual containing release and caller cut.

Changelog plugin presentation

Changelog declares complete authenticated API patterns through provides.apiRoutes and registerApiRoute: user-level GET entries, GET entries/:version, POST seen and GET asset/:version/:file, plus admin-only GET readers and POST unread/:version. The readers report covers all visible releases; the reset targets one shipped version. Handlers read the host's once-decoded req.params with an empty req.path. Missing parameters, malformed escapes and undeclared suffixes return 404 before any handler runs. Asset handlers retain their plain-segment allowlist, raster-image extension allowlist and directory containment validation after decoding.

The changelog plugin exposes GET /plugins/changelog/api/entries as { entries: EntrySummary[] }, with a server-computed unread flag on each visible entry. GET /plugins/changelog/api/entries/ returns the localized detail and its unread flag. POST /plugins/changelog/api/seen returns {} after recording the newest visible version; it neither returns nor reads back the internal marker. All three routes require user-level access. ChangelogPage reads the unread flags and posts seen once per visit when unread entries exist, then invalidates the plugin navigation query. Unknown locales or missing translations use English. With no authenticated account, unread flags are false and no marker is written; with no visible entries, seen acknowledges without writing. Stored versions only advance during ordinary reading, including when the shipped notes are older than the stored version. The listing contains metadata only; bodies are fetched per entry. When the plugin is unavailable, its registered routes and navigation contribution are absent.

The bundled What's new page receives only applicable content from Changelog's per-account API. Plugin-scoped sections carry installed-plugin identity from host.stores().pluginsRead.list(), the same discovery catalog as Settings. PluginItem uses the host's sanitized Markdown, the browser DOM parser to extract the section title, and the existing shadcn-backed muted Badge beside it. The pill shows only the plugin's localized name, never an icon: plugin icon.svg files are colored app tiles that some plugins lack, and a monochrome mask flattens a tile into a solid square, so icons made the pills inconsistent. UI API 54 still publishes components.AdaptiveBrandMark for other consumers. Changelog declares requiresApiVersion: 55 so an older host refuses the bundle before mounting. Untagged content has no pill; an unavailable plugin item never reaches this renderer. Details load only when an entry opens.

Locale and secret configuration owners

src/shared/locale.ts owns LOCALES, Locale, DEFAULT_LOCALE and validation; web/lib/i18n/locale.ts is its importless byte-identical browser mirror. isLocale accepts only cs/sk/en. LanguageProvider consumes this policy directly; serverPrefetch.readLocale calls sanitizeLocale on the parsed cookie. Missing/invalid choices yield English. The public i18n index keeps the live Locale type and client helpers, without re-exporting the unused default value. Value consumers import the pure owner. Account reconciliation still uses /auth/me/locale; validation itself performs no request.

Plugin detail and instance/account configuration responses retain masked config, secretsSet and revision fields, plus secretsUnreadable when a stored secret cannot be decrypted; those routes still answer and can replace or clear it. Core materializes declared PluginSecretVault values before applyConfigPatch/maskedConfigView. Blank/absent fields preserve secrets, null clears them, stale revisions return masked conflicts, and revisionless account writes retry concurrent changes. Vault/JSON revisions commit atomically; failure requests no restart. See encrypted configuration. Plugin listing and detail add credentials: { keys, corrupt } for Settings → Access; the corrupt keys come from the vault's corruption index, never from a decrypting read (see Access section).

Registry runtime consumers

Registry browser bundles import AutoSaveStatusProps and UseAutoSaveStatus type-only from elowen-plugin-ui-kit. The host owns their implementation; no registry forwarding module is required. BrowserAccount consumes the existing utils.formatBytes(bytes, document.documentElement.lang), parseTs and compactElapsed contracts. Byte sizes use the shared 1024 ladder through TB, localized decimal digits and no grouping; nonpositive or nonfinite sizes display 0 B. The Browser runtime requires these published helpers and omits a reply age only for an invalid timestamp.

Plugin registration loading is owned by usePluginUiRegistrations in web/lib/pluginUi.tsx. The single-entry hook delegates to the same engine and retains injected loading for production-equivalent tests. Bundle identity includes plugin name, JavaScript URL and CSS URL. Identity changes synchronously withdraw the previous registration; abandoned loads cannot publish a late result. Listing strings remain locale-specific without reloading an unchanged bundle. Boolean slot discovery uses the narrow pluginSlotEntries helper; operationDock retains its object contract and desktop-only policy.

Registry plugin tests use a neutral ModelIcon glyph when they only need icon presence. Production model branding remains owned by the core; no production icon API exists solely for a test fixture.