@alwith/module-extension
v0.6.0
Published
Portable application extension host and native package storage
Readme
@alwith/module-extension
Reusable application extensions for ALwith desktop hosts. This package is separate from Codex plugins and skills. It contains no Desktop or U application imports and does not own agent sessions.
Delivery
Maintained packages use the 0.6.x release line. extensionApiVersion follows the package version; extensionApiCompatibilityVersions lists the earlier versions the current API still serves (0.5.4, unchanged in 0.6.0), and the default Tauri runtime accepts them. The optional native adapter and JavaScript peers use Tauri 3 alpha.
The npm package includes compiled JavaScript, declarations, schemas, the generated command bindings (/commands) and the native extension libraries. The Tauri plugin is the alwith-extension crate. Ordinary extension bundles contain only JavaScript and static assets. See LICENSE for redistribution terms.
[dependencies]
alwith-extension = "0.6"
tauri-plugin-dialog = "3.0.0-alpha.4"tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(alwith_extension::init(Config { development_directory, ..Default::default() }));Config { root, native_directory } overrides the default app_local_data_dir()/extensions root and the bundled resource_dir()/native libraries.
Grant alwith-extension:default only to participating windows in Tauri capabilities. Each such window creates one createTauriExtensionRuntime from /tauri, supplying application capability versions, actual service objects, author module mappings and an evaluator from /host. Call start() once per WebView; React StrictMode and page navigation are not instance ownership boundaries. Native destruction/navigation observation releases the old document's tokens. Do not release tokens merely because a window is hidden.
Main and settings windows can advertise the same application contributions while mounting different views. /react provides useHostSnapshot and ExtensionView; callers supply a container and error handler. Most applications use the default runtime and the management UI instead of assembling these parts. Commands and surfaces are explicit host UI outlets. There are no automatic global listeners or DOM prototype patches. Native capabilities are opt-in; no filesystem capability is registered automatically.
The root SDK and /host can be imported without DOM, React, or Tauri. /testing supplies in-memory data for independent hosts. /marketplace is an optional validated catalogue, not a second installer. /build is a Bun author tool. React and Tauri are optional peers; the selected adapter requires the corresponding peer.
Default Tauri runtime (0.5.2)
/tauri exports the runtime ALwith U uses, without U's product parts. A generated application only supplies its identity, language, toast and policies:
import { defineDefaultExtensionRuntime } from "@alwith/module-extension/tauri"
export const extensions = defineDefaultExtensionRuntime({
host: { id: "my-app", version },
language: () => i18n.language,
// The host toast; returns { hide, setMessage } and calls onClose when dismissed
notices: { show: (message, duration, onClose) => showToast(message, duration, onClose) },
// apiCompatibilityVersions, http: { allowUrl }, externalLinks: { allowUrl }, clipboard,
// capabilities, modules, contributions are optional
})
if (import.meta.hot) import.meta.hot.dispose(() => void extensions.dispose())createDefaultExtensionRuntime(options)composescreateCommonCapabilitiesover the native plugin (files, directories, binary HTTP, clipboard, notices),createTauriHttp,createTauriNotifications,createTauriExternalLinksandcreateTauriDialogs, then the host's owncapabilities. HTTP denies every URL unlesshttp.allowUrlallows it; external links allow every HTTP(S) URL unlessexternalLinks.allowUrlnarrows them; the clipboard defaults tonavigator.clipboard.writeText. All seven contribution kinds are supported at1.0.0(DEFAULT_CONTRIBUTIONS) unlesscontributionsreplaces them.- Extensions may
requirethe SDK,/dom,/plugin,/react(mountReactonly),react,react/jsx-runtime,react/jsx-dev-runtime(mapped onto the production runtime) andreact-dom/client, plus the host'smodules. React loads on the first evaluation, so/tauriitself does not import the React peer. defaultRuntimeOptions(options)returns the same runtime options without the Tauri transport, for hosts that build theExtensionRuntimethemselves.wrapEvaluate?: (evaluate) => evaluatereceives the standard CommonJS evaluator and returns the one the runtime uses, so a host can decorate specific extension factories (U binds yup-kb's Drive data this way); default: the standard evaluator unchanged.defineDefaultExtensionRuntime(options, afterStart?)returns{ getRuntime, start, dispose }: one runtime per WebView, created on first use;startiscreateExtensionStartup(getRuntime, afterStart)(refresh,start(), thenafterStart, shared by concurrent callers; a failed startup is not kept). The caller wiresdisposeto HMR disposal or window teardown.pickLocalExtension()invokesplugin:alwith-extension|pick_local. The native plugin still opens the folder dialog throughtauri-plugin-dialog, so the host registerstauri_plugin_dialog::init()beforealwith_extension::init(..)and grantsalwith-extension:default(which includesallow-pick-local) to participating windows. No JS dialog permission is needed for the picker;createTauriDialogsneedsdialog:allow-message./plugin'spluginHostCapabilityhas no default provider: class-based plugins need a host-suppliedPluginHostincapabilities.
/host exports the install flow the UI uses: installLocalExtension(runtime, path, { update?, source?, signal? }) installs or updates a picked folder, waits for every window to release the old package, enables new installations and reports activation; executeReload(runtime, id, signal) performs a coordinated reload; executeStagedInstall verifies a package the host staged itself.
React management UI (0.5.2)
/react ships U's extension management and outlets. Wrap them in ExtensionsHostProvider with the application's host:
import { ExtensionsHostProvider, ExtensionsSection } from "@alwith/module-extension/react"
import { pickLocalExtension } from "@alwith/module-extension/tauri"
<ExtensionsHostProvider
value={{
i18n: { language },
openLink,
reportError,
confirm: (message, title) => ask(message, { title, kind: "warning" }),
notify: (message) => toast.success(message)
}}
>
<ExtensionsSection
runtime={extensions.getRuntime()}
start={extensions.start}
pickLocal={pickLocalExtension}
onOpenSurface={openSurface}
/>
</ExtensionsHostProvider>ExtensionsSection/ExtensionsManager: search, install from a folder, enable/disable, update from a folder, reload, uninstall afterconfirm, pending transitions with cancel, errors and service retry, contributed pages per extension and each extension's settings views.settingsselects the compact settings-window variant.- Host hooks on
ExtensionsSection, all optional; omitted, the section behaves as above:install(runtime, { update?, onPhase })replacespickLocal+installLocalExtension(for example a host's own prepare/staging install). The section shows"preparing"first; the hook callsonPhase("installing")once a package is chosen and resolves anInstallResult, ornullwhen the user cancels. Withinstall,pickLocalmay be omitted.onEnabled(installation)runs after the enable switch, onceruntime.settled()resolved without an error for that extension (for example a first-use notice).reload(runtime, id, signal)replacesexecuteReloadand resolves"reloaded"or"updated"; the notice follows the outcome.onUninstalled(installation)runs after the uninstall transition completed in every window (grant or import cleanup). If the transition ends with the extension still installed, the hook is skipped and an error is reported.refresh()replacesruntime.refresh()for the service Retry button and window focus.renderIcon(installation)andnotices(installation)pass through toExtensionsManager.
ExtensionsManageralso takesrenderIcon?(installation) => ReactNode(replaces the manifest icon;undefined/nullkeeps it) andnotices?(installation) => ExtensionNotice[]({ id, content }, rendered under the row with a close button; a dismissal lasts until the installation's package revision changes).useExtensions(runtime, start?, onError?, { refresh? }?): snapshot and contributions as React state; runsstarton mount andrefresh(defaultruntime.refresh()) on window focus.- Outlets:
ExtensionNavigation(the leading rail's app buttons),ExtensionActions(topBar/navigationplacements),ExtensionStatusBar,ExtensionSettingsNavigation+ExtensionSettingsContent,ExtensionMount,ExtensionViewContent(local error and retry) andExtensionPage(a surface or settings page as a full page).ExtensionViewContentandExtensionMounttakeonOpen?(view) => boolean | Promise<boolean>, a gate that runs once per view id before asurfacesorsettingsPagesview renders (U's legacy consent), andonCancel?();falseor a rejection (reported throughonError) keeps the view closed.ExtensionPagetakesonOpenand closes itself throughonClosewhen the gate cancels.ExtensionIconandEXTENSION_ICONSresolve manifest and contribution icons;ExtensionSettingsControlspresents DOM-based extension settings through the host's select and switch controls. - Strings come from
extensionLocales(enandzh-CNcomplete, other U languages partial, falling back toen). A host with its own i18n passest; it then receives the keys prefixed withextensions.and must provide every key ofextensionLocales.en. extensionActions(installation)decides whether an installation offers update and uninstall; the default hides both forbundled:*sources.- The components use Tailwind classes and the shadcn tokens of the shared U presentation; the application's Tailwind setup must scan this package's
dist. The catalog layout stylesheet (react.css) is imported by the components themselves.
Host-owned parts
The package does not decide authorization or product behavior. The application keeps:
- Policy: which origins HTTP may reach, which links may open, which installations may be updated or removed, and any capability authorization.
- Bundled, legacy and imported extensions: seeding bundled packages after startup (
afterStart), legacy Desktop conversion, startup imports, grant cleanup after uninstall, and their notices and consent flows. - Product capabilities and modules: application-specific providers (for example a Drive bridge or wallpaper), the
PluginHostprovider, and a host UI kit offered to extensions throughmodules.
These plug into the package's flows through the host hooks above (wrapEvaluate, ExtensionsSection's install, onEnabled, reload, onUninstalled, refresh, renderIcon, notices, and the views' onOpen) rather than through a copy of the management UI.
Manifest format 3
{
"id": "my-extension",
"name": "My extension",
"version": "1.0.0",
"dependencies": { "@alwith/module-extension": "^0.6.0" },
"hosts": { "alwith-u": ">=0.1.1" }
}Optional display metadata: description and author are nonempty strings; authorUrl is an absolute HTTP(S) URL without credentials. icon accepts an HTTP(S) image URL or a local glyph ID: lucide:blocks, lucide:play, lucide:panel, lucide:settings, lucide:clock, lucide:chart, lucide:globe. Glyph IDs are exact lowercase names matching the existing contribution icon vocabulary. Unknown names are rejected at build/install. authorUrl cannot use glyph IDs. Omit unused fields, not null or blank strings.
parseManifestIcon(icon) returns { type: "image", url } or { type: "lucide", name } without React/DOM/Lucide dependencies. Hosts map glyphs to local components; U shares the map with its extension entrypoints. Remote images retain their existing error fallback. Local glyphs work while offline or disabled. Package-relative paths and data URLs remain unsupported. Metadata does not affect activation or permissions.
Optional locales translates only name and description, keeping all metadata in one manifest:
{
"id": "localized-example",
"name": "Notes",
"description": "Personal notes",
"version": "1.0.0",
"dependencies": { "@alwith/module-extension": "^0.6.0" },
"locales": {
"zh": { "name": "笔记", "description": "个人笔记" },
"zh-CN": { "name": "我的笔记" }
}
}resolveManifestText(manifest, language) is a pure helper independent of React, DOM, Tauri and i18next. Each field resolves by exact language, primary language, then the top-level default, ignoring language case. Missing descriptions remain absent; a translation may provide a description even when the default omits it. The manifest and author language keys remain unchanged.
The public package validates translations at build/install boundaries and persists them inside Installation.manifest.locales. Hosts supply their current language and refresh the display on language changes, including search, accessibility labels and management prompts. Disabled extensions work without activation. Tauri notification/dialog providers accept { language: () => currentLanguage }, evaluating each operation; omitted callbacks use default text. Extension page content remains the extension's responsibility.
Language keys require 2–8 ASCII letters followed by optional hyphen-separated 1–8 ASCII alphanumeric subtags (en, zh-CN, zh-Hant-TW, es-419). Case-insensitive duplicates are rejected. Each locale entry must contain at least one nonblank string name or description; unknown fields, null, arrays and empty entries are rejected. locales may be omitted or {}. No extra translation files, placeholders, default-locale field or host i18n dependency are needed.
The current snapshot uses locales exclusively. NLS files are ordinary assets with no special parsing; %key% strings remain literal. All author examples use extension version 1.0.0 and SDK range ^0.5.0. The U host constraint remains independent at >=0.1.1.
manifestVersion defaults to 3, entry to main.js, dataSchemaVersion to 1, and hosts to an empty unrestricted map. Only the SDK dependency is supported; this is a compatibility constraint, not an instruction to install npm packages. A nonempty hosts map is an allowlist of alternative host IDs with independent version ranges. extensionApiVersion is generated from this package's package.json.version before tests, type checking and distribution builds. Hosts pass apiVersion: extensionApiVersion and their own host: { id, version } to the runtime. Use parseManifest(raw) to normalize author JSON before passing it to a standalone JS host; native snapshots already contain defaults.
A host that has verified support for an earlier SDK API can supply additional concrete versions:
apiVersion: extensionApiVersion,
apiCompatibilityVersions: ["0.1.0-rc.20"]The SDK dependency must match the current version or one explicitly supported version. No older versions are accepted by default. The current version, every compatibility version and the requested range must all be valid; an alias cannot hide an invalid current version. Normal semver prerelease rules apply: 0.1.0-rc.20 satisfies ^0.1.0-rc.11, but not ^0.1.0-rc.21, ^0.1.0 or ^0.2.0. This declaration changes only SDK dependency matching. Manifest versions, host constraints, capabilities and native authorization remain independent. Installed packages, certificates, data and enable state are not rewritten.
There is no v2 compatibility path. Old fields (engines, capabilities, contributions, externals) are rejected. Rebuild old development extensions; existing v2 native state is not automatically converted or deleted. Back up development data before manually replacing an old store. Storage-layout migration is a separate mechanism and does not make v2 manifests compatible.
SDK and host versions are checked before module evaluation. Missing modules fail at require, missing capabilities fail when requested, and unsupported contributions fail on registration. Activation failure releases managed resources; later callback failures must be reported by the caller. Side effects already sent outside the process cannot be rolled back. The evaluator uses only the host's explicit author module map and still denies private host/tauri/testing entrypoints. Private capability authorization remains the host's responsibility.
dataSchemaVersion remains optional author metadata with default 1. It is a format marker, not JSON structure validation. Data format changes still require migration support; the current service rejects such updates. The maintained SDK package, runtime API and Rust crate use the 0.5.x release line.
Build an extension
import { buildExtension } from "@alwith/module-extension/build"
await buildExtension({ sourceDirectory: "./my-extension", outputDirectory: "./build/my-extension" })The source contains manifest.json, main.tsx and optional assets. The default export is a defineExtension(context => ({ onload, onunload })) factory. Register commands with context.command and views with context.view("settings" | "surfaces" | "statusBar" | "settingsPages", view). Start rendering and background work in a view's mount hook. Register cancellation listeners and all cleanup with the provided owner.
The tool produces one CommonJS entry (main.js by default), checks its imports, and emits a compact manifest without an externals list. It rejects unknown/dynamic requires, private host entrypoints, links and output overwrites. Only author SDK, DOM helpers, mountReact, shared React and JSX modules belong in the evaluator map. This execution model is for trusted same-context UI code, not a security sandbox.
examples/portable exercises commands, settings, a React surface and compare-and-swap data. Its manifest depends on @alwith/module-extension: ^0.5.0. examples/host is an independent two-window Tauri consumer; it installs the packed tarball and never imports the U or Desktop source trees.
State and errors
Native storage uses the application's local data directory under extensions/; hosts and development identifiers remain isolated. Native state selects an immutable package, installation identity and data generation. The service owns all writes; data writes require the expected revision and live instance token. Conflict, corruption, revoked instance and absent data are distinct results.
Installed layout: extensions/<extensionId>/packages/<packageRevision>/ contains the
read-only code/resources; extensions/<extensionId>/data/data.json holds mutable
configuration. A bounded .previous.json supports interrupted-write recovery.
extensions/state.json remains the shared authority for installation identity and
selected revisions. Existing package-layout v1 stores migrate on open, preserving the
original trees under .legacy-v1/; older binaries cannot open the new layout. Public
SDK/data APIs are unchanged. See native persistence in the plugin crate's documentation.
Activation awaits asynchronous hooks. Deactivation revokes new calls immediately, then awaits hooks and LIFO resource cleanup. Cleanup errors are aggregated. A hung hook remains hung; no timeout reports a false successful stop. Update, disable and uninstall commit only after every participating window releases its instance. Events carry full sequenced snapshots and a service identity; data changes notify subscribers without reloading code. Call refresh() on transport reconnection or window focus.
Use native plugin:alwith-extension|pick_local (commands.pickLocal()) to grant a user-selected directory to the calling window, then request installLocal or an update transition through the runtime. Package resources use a scoped read-only URI scheme with real URL directories for relative CSS assets; no home-directory asset scope is required.
Uninstall retains installation source and data. Reinstall from a different source is rejected. The current native installer supports local directories; it deliberately rejects data schema changes with migrationRequired. ZIP/network installation, explicit data migrations/rollback, data clearing and package garbage collection are not yet implemented. Immutable old content is retained. Do not present this initial package as completion of the architecture document's full stability phase.
Development and verification
From this directory in alwith-modules:
bun install
bun run test
bun run check
bunx tsc -p tsconfig.core.json
bun run build
bun pm pack
cargo test --manifest-path rust/Cargo.toml
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warningsThe plugin's Rust types are the source of src/generated/commands.ts (UPDATE_BINDINGS=1 cargo test --manifest-path rust/Cargo.toml); src/tauri/protocol.ts names its types by direction. The JSON schemas come from cargo run -p alwith-extension-impl --bin generate-protocol. Keep generated changes under review with their Rust source. Tests cover delayed activation, cancellation, cleanup failures, real bundled JSX, multiwindow CAS and stop barriers, content validation and storage failure recovery. Target-platform WebView behavior and power-loss behavior still require platform validation before a stable release or replacing Desktop.
Optional native capabilities
The root SDK exports httpCapability, notificationsCapability, externalLinksCapability and dialogsCapability, plus DOM-free contracts. Each capability is version 1.0.0. Extensions request these providers directly through context.capability(key). Missing providers throw; host authorization still applies. context.supportsCapability(key) checks provider availability, not authorization. No capability declarations belong in the manifest.
Hosts explicitly register providers from @alwith/module-extension/tauri/capabilities:
import { httpCapability, dialogsCapability } from "@alwith/module-extension"
import { createTauriHttp, createTauriDialogs } from "@alwith/module-extension/tauri/capabilities"
const capabilities = {
[httpCapability.id]: createTauriHttp({
allowUrl: (url, _extensionId) => url.origin === "https://example.com",
timeoutMs: 30_000,
maxResponseBytes: 8 * 1024 * 1024
}),
[dialogsCapability.id]: createTauriDialogs()
}
// Supply capabilities to createTauriExtensionRuntime's options.HTTP additionally requires the optional @tauri-apps/plugin-http JS peer (3.0.0-alpha.1+) and the host's matching Rust plugin. The HTTP peer loads only when a request is made. Other adapters use the existing Tauri API peer and await native commands directly. The host initializes the selected Rust plugins and configures native ACLs:
| Provider | Rust plugin | Permissions |
| -------------------------- | ------------------------------------ | -------------------------------------------------------------------- |
| createTauriHttp | tauri-plugin-http | http:default with explicit URL allow scopes |
| createTauriNotifications | tauri-plugin-notification | notification:default |
| createTauriExternalLinks | tauri-plugin-opener | opener:allow-open-url with allowed URL scopes, or opener:default |
| createTauriDialogs | tauri-plugin-dialog 3.0.0-alpha.1+ | dialog:allow-message |
The extension plugin is the alwith-extension crate; its implementation ships as a prebuilt library in this package. These adapters reuse host-linked native plugins; business extensions never gain a private Rust build. A non-Tauri host can implement the same contracts through a provider's create(binding) callback. Existing { version, value } capability registrations remain supported, but a shared value does not automatically acquire lifecycle guards: use a binding factory for native or stateful services.
Providers are cached per activation, receive the manifest identity and cancellation, and register one completion barrier with the owner. Retained methods reject after revocation, including after reactivation of the same extension. Every pending operation is joined before normal owner cleanup finishes. Rejections remain observable on the original call; disposal does not report the same operation error a second time.
HTTP accepts text request bodies and returns status, final URL, headers and a UTF-8 text body. The default response limit is 8 MiB of bytes, with a 30-second timeout including reading. HTTP errors such as 404 remain responses; transport/ACL/limit errors reject. Automatic redirects are disabled: a 3xx response requires a new explicit request, checked against host policy again. Only HTTP(S) URLs without embedded credentials are accepted; the adapter does not inject host login tokens or expose unsafe TLS/proxy options. To avoid implicit shared authentication, hosts MUST disable the HTTP plugin's cookies Cargo feature (including defaults enabled transitively). The plugin ignores JS credentials and otherwise shares a cookie jar between the application and every extension. Check the resolved feature graph; a host that needs automatic cookies must supply a separate isolated transport instead. A caller may pass a view's Cancellation to end work when the view closes. Instance revocation, caller cancellation and timeout propagate to the plugin's AbortSignal. The upstream HTTP plugin handles native resource cancellation; its JS API does not provide an independently awaited cancellation acknowledgement, so this is not a hard guarantee of native network quiescence.
Notification permission requests are explicit; send never opens a permission prompt. Permission values reflect the plugin's report, not independently verified OS authorization: the current desktop backend reports granted without an OS permission query. send resolves when the native command accepts the request, not when the OS delivers or displays it. IPC and returned native-command errors propagate, but the upstream desktop backend may discard later OS delivery errors. Notifications prefix their title with the extension name. Link opening accepts only HTTP(S) URLs allowed by host policy and offers no executable or file-path option. Dialogs expose message and confirmation only, using the extension name as the title; they do not grant filesystem access. Native dialogs, already submitted notifications and browser launches cannot be undone by cancellation. Revocation rejects late successful results, and an open dialog keeps cleanup pending until the user closes it.
The evaluator and capability facade are lifecycle/convention boundaries for trusted extensions, not isolation from other JavaScript in the same WebView. Tauri ACLs remain window-scoped. Filesystem access, subprocesses, credentials, application login, Runtime sessions and business-specific native services belong to separately authorized host providers.
For a host that does not need automatic HTTP cookies (including U), preserve the other default transport features explicitly:
tauri-plugin-http = { version = "3.0.0-alpha.1", default-features = false, features = ["rustls-tls", "http2", "charset", "system-proxy"] }Verify cargo tree -e features -i tauri-plugin-http contains neither cookies nor default. Cargo features unify across dependencies; setting default-features = false in only one dependency declaration is insufficient if another dependency re-enables cookies.
Host UI outlets
In addition to commands and surfaces, hosts can advertise these 1.0.0 contributions:
| Contribution | Registration | Host responsibility |
| --------------- | -------------------------------------- | -------------------------------------------------------------------- |
| topBar | context.action("topBar", action) | Render a compact toolbar action |
| navigation | context.action("navigation", action) | Render a navigation entry |
| statusBar | context.view("statusBar", view) | Mount a compact persistent widget |
| settingsPages | context.view("settingsPages", view) | Render a settings column entry and mount its full page when selected |
Actions have id, title, optional icon and order, and target: { type: "command" | "surface", id }. Both action and target IDs must be local nonempty ASCII IDs containing only letters, digits, ., _, -. The host prefixes both with the extension ID, so an action cannot name another extension's contribution. Register the referenced command or surface as well; registration order is free. Targets may disappear independently, so host UIs must disable or hide actions without a current target. Execute commands through their guarded run() function; route surface targets through the host's navigation.
HostSnapshot.actions publishes actions only for active instances; stop/failure removes them along with views. Existing consumers of commands/views keep working. Main and settings windows may advertise all supported application contributions but only mount their actual local outlets. Lifecycle compatibility does not imply a view is rendered in every window.
Presentation metadata is optional on views and actions. order must be finite (host default 0); icon names are blocks, play, panel, settings, clock, chart, globe. mountReact preserves both fields. The package owns no application layout or UI toolkit. Dimensions, ordering, scrolling, empty-state behavior and placement remain host decisions. Put timers and subscriptions in mount scope and release them on unmount.
The top-bar, navigation, status-bar, and settings-page examples require extension API ^0.1.0; build them along with the four existing entry examples using bun run build:entry-examples <new-output-directory>.
Bar alignment
ContributionPresentation.alignment accepts "left" | "center" | "right" for topBar actions and statusBar views. mountReact forwards it unchanged. Invalid values are rejected on registration. Navigation and settings/page outlets ignore alignment. Existing contributions remain compatible: U defaults omitted alignment to left for both top-bar actions and status-bar views. Explicit center/right alignment is preserved. Legacy ribbon actions retain the reviewed business icons (bar-chart-3, file-text, book-open, shield-check) and explicitly request left alignment.
U uses three equal-width groups in each bar's available space. Center is relative to that available space (after native controls, sidebar and version-info clearances), independent of side content width. Within each group, order is ascending from left to right, ties use the full contribution ID. A crowded group scrolls horizontally without displacing another group. Unoccupied toolbar space stays available for dragging the native window.
Alignment is included in SDK ^0.1.0. The top-bar and status-bar examples (extension version 1.0.0) demonstrate all three positions.
Contribution lifecycle and host helpers
context.supportsContribution(kind)checks the application's supported outlets. Revoked instances throw; unsupported outlets return false and attempts to register them throw. No contribution declarations belong in the manifest. This is application support, not whether the current window has mounted an outlet.- The disposer returned by
context.view()synchronously removes that registration and cancels all its mounts. Await it to wait for every mount's asynchronous cleanup. Other views and the extension remain active. A new registration with the same ID has a separate lifetime. /hostexportsresolveActionTarget(snapshot, action)for ownership-aware command/surface lookup andsortContributions(items)for default order/ID sorting without mutating the input. Layout, routing and disabled/hidden presentation remain host responsibilities.mountReact()catches component rendering failures inside the extension root.host.mount(id, container, cancellation?, onError?)reports them asViewRenderErrorwithextensionId,viewIdandcause; the instance diagnostic snapshot records the error without stopping unrelated views.ExtensionView.onErrorreceives these errors too. The host renders its own local error/retry UI; remounting recreates the root. Errors reported after cancellation are ignored. This does not catch arbitrary asynchronous tasks or event-handler errors; extension code must handle or report those explicitly through the mount context's optionalreportError.
The top-bar example requires SDK ^0.1.0 and checks whether the host supports topBar. Hosts without a top bar can still expose its required surface through their page navigation.
Class-based Plugin SDK
The optional @alwith/module-extension/plugin author entrypoint exports concrete
Plugin, Component, ItemView, WorkspaceLeaf, PluginSettingTab, Setting
and control classes. createPluginExtension(context, { manifest, styles?, create })
adapts a typed plugin to the same runtime, contribution registry and revisioned
context.data service as a factory extension. Hosts explicitly provide
pluginHostCapability (plugin.host); this is independent of the opt-in legacy
host policy. Authors using this entrypoint should declare SDK dependency
^0.1.8 or newer. SDK and host compatibility checks still occur before activation.
import { defineExtension } from "@alwith/module-extension"
import { createPluginExtension, Plugin } from "@alwith/module-extension/plugin"
class CounterPlugin extends Plugin {
async onload(): Promise<void> {
await this.saveData({ count: 0 })
this.addCommand({
id: "read",
name: "Read counter",
callback: async (): Promise<void> => {
const data = await this.loadData()
await this.app.chat.sendMessage(JSON.stringify(data))
}
})
}
}
export default defineExtension(context =>
createPluginExtension(context, {
manifest: context.manifest,
create(_api, app, manifest): Plugin {
return new CounterPlugin(app, manifest)
}
})
)The create callback receives bound constructors, the explicit PluginApp and
its manifest. api.Plugin can be constructed without arguments; public
Plugin(app, manifest) and WorkspaceLeaf(app, location?) also accept the
explicit app. ItemView(leaf) inherits its leaf's owner. Each activation retains
its own data queue, workspace, callbacks and cleanup; no mutable default app is
used. loadData() returns JSON or null, and saveData(value) uses the current
revision and manifest data schema version. Existing legacy bundles consume these
same implementations through their historical @alwith/extension binding.
For an idle empty leaf, setViewState creates the view and starts onOpen
synchronously before returning its promise. Initial state is applied as soon as
onOpen completes, before managed component loading adds further asynchronous
work. Replacements remain serialized and wait for the previous view's close and
cleanup. Callers should await the returned promise before delivering fetched
content to a view whose onOpen performs asynchronous initialization, and to
observe initialization failures and completion.
Legacy CommonJS compatibility
The optional @alwith/module-extension/legacy entrypoint provides
createLegacyExtension(context, { manifest, source, styles?, modules? }) and
legacyHostCapability. styles is an installed resource path. Hosts must
explicitly provide legacy.host; importing the main SDK does not install DOM
helpers or activate this compatibility layer.
This generic API does not recognize business extension IDs. Hosts can layer
optional application-specific compatibility policies over Plugin/Component lifetime, ItemView/WorkspaceLeaf,
workspace tabs, commands and top-bar actions, settings controls, Modal, Notice,
and requestUrl. The workspace surface retains view instances across outlet
mounts; settings is a settingsPages contribution. Unsupported external
CommonJS imports fail explicitly. Package-relative JS/JSON modules, directory
entries, caching and circular references are supported through modules.
app.vault.adapter.getResourcePath() and relative fetch map to activation-bound
package resources. registerVivariumSurface registers independent surfaces with local IDs
vivarium-<id>. The host chooses their page outlet; U uses its leading screen.
Mount cleanup owns surface-created leaves without adding them to workspace tabs.
The workspace surface remains available for older view-based entry points.
LegacyHost supplies fetch, restricted invoke, vault write/remove, notices,
language, external links, view navigation, and chat sending. Chat context must
be captured once per activation at a user interaction and remain immutable for
later asynchronous sends. The host enforces native command/path authorization.
Optional loadInitialData and acknowledgeInitialData migrate a legacy snapshot
only after the portable data write succeeds. Existing data subscriptions update
live settings objects without replacing references captured by business code;
removed keys are removed from the live object.
PluginHost.presentModal(modal, present) optionally controls when a compatibility modal is displayed. Call present() to show it; return a disposer to remove a queued presentation on close or unload. Repeated or stale presentation callbacks do not reopen a closed modal. Hosts without this hook display modals immediately. Host policies must preserve explicit consent and the modal callbacks.
Optional bindStorage(storage) receives {read, write, exists, resource} before
entry evaluation and returns a disposer. Hosts route direct legacy data.json
file aliases to this port so Plugin and file APIs share initialization and the
write queue. exists() distinguishes absent data from stored JSON null.
manifest.dir is a virtual own-extension directory; hosts map it without
writing back into the old application.
Native hosts use validate_manifest, Service::installed_package, and the
Tauri plugin::installed_package/plugin::snapshot wrappers instead of reading
private service state or storage layout. Package snapshots validate the enabled
installation, source, transition state, selected manifest and complete revision.
These host-only APIs do not replace activation tokens for extension-facing calls.
The evaluated module gets per-instance window/document facades. Timers, window and document listeners, directly appended body/head nodes, views, styles, and modals are released with the activation. Created elements remain native DOM objects. The finite DOM convenience methods are installed additively and idempotently on HTMLElement. These facades provide compatibility and resource ownership; they are not a security sandbox. Hosts should only evaluate reviewed, trusted sources, and review package-specific background behavior before admission. Session archive/export and unrestricted Tauri access are not part of this API.
Common host capabilities (0.1.5)
The SDK exports filesCapability, directoriesCapability, binaryHttpCapability,
clipboardCapability, and noticesCapability. Hosts register providers from
createCommonCapabilities (the /host export), injecting native command dispatch,
clipboard writing, and in-app notice presentation. Providers own their activation
lifecycle; revocation rejects further use and teardown waits for accepted native work.
Files use bytes (Uint8Array). Directory selection returns an opaque handle with
id, display name, and scoped files; the native host must persist and check grants
for each extension. Private root data.json is reserved for context.data. Neither
these providers nor the legacy facade constitute a JavaScript sandbox.
Rust filesystem supplies bounded, relative-path file operations. The optional
native-http feature supplies bounded binary HTTP without automatic redirects or
shared cookies. The host still authorizes installation identity, paths and URLs.
Native HTTP has no cancellation handle yet: abort suppresses delivery, teardown
waits, and transport timeouts bound execution. Legacy hosts can override command
names and reuse createScopedFileOperations, createBinaryHttp, and
createBinaryFetch without exposing old command syntax to modern extensions.
noticesCapability is an in-app message with hide and setMessage, distinct from
OS notificationsCapability. Closed notices cannot be revived and activation
cleanup hides them. Clipboard currently supports text writes only.
Chat drafts
With SDK 0.1.9 or newer, plugins can call await app.chat.setDraft(text) to fill
an empty composer for user review. This never sends a message. The host must
implement the optional PluginHost.setDraft method; unsupported hosts reject
the call. Existing app.chat.sendMessage(text) calls still send directly.
U targets the currently selected writable chat. Existing text, attachments, or file mentions cause draft placement to reject instead of overwriting user input. Select a chat first; the operation does not silently create or choose a conversation. Both methods return promises: handle rejection when subsequent plugin actions require successful delivery. Sending success means acceptance, not turn completion.
Coordinated reload (0.1.0-rc.11)
beginTransition with action: "reload" stops all instances without changing the selected package, data generation, source or persistent enabled intent. Disabled installations reject reload. The native barrier requires every lease to release before commit; cleanup failure keeps the transition pending. Canceling an uncommitted transition preserves the old selection.
A committed reload increments the installation's optional activationRevision. Runtime identity includes this value so a failed activation can be retried even when a window misses the pending snapshot. It is separate from package content and data revisions. Hosts must confirm transition completion and local activation rather than treating request acceptance as successful reload. This does not fetch packages, migrate data or provide automatic rollback.
