@alwith/module-chat
v0.6.13
Published
Desktop-first shared chat implementation. Distributed under the proprietary license included in this package.
Readme
@alwith/module-chat
Desktop-first shared chat implementation. Distributed under the proprietary license included in this package.
Desktop and U share turn virtualization, Desktop's Markdown renderer, activity groups, work sections, plans, user messages, assistant turn layout and edited-file cards. Both apps also consume the composer, completion menu, model/effort/permission selectors, attachments, approval components, search interaction and turn navigation with audio. U maps existing API state to presentation blocks; it does not maintain a second session state. Extraction is not yet a complete product acceptance: source-to-rendered search mapping and end-to-end visual/selection/scroll verification remain outstanding.
Ownership
async-question-card renders controlled, nonmodal Codex questions with choices,
free text, submission and skip actions. Hosts retain unsent drafts by session and
question ID, send the reply through the original session, and derive accepted
answers from Codex messages. History and completed turns remain read-only; the
component does not own a conversation store or create permission requests.
| Package owns | Application supplies | | --- | --- | | Message layout, Markdown, activity and tool presentation | Message projection, theme, translations, file/artifact opening and business-specific cards | | Composer, attachments, completion and selectors | Controlled draft, available commands/files/models/options, submission and error handling | | Approval and elicitation interaction | Original request identity, protocol response, replay and product-specific actions | | Virtual list, search UI and turn navigation | Session identity, viewport, source matches, reveal callback and navigation preferences | | Session timeline: turn grouping, history placeholders, retry, follow and anchor scrolling | Client, host capabilities, product slots and the scrolling element |
Application navigation, settings, login pages, update UI and general-purpose product controls are not part of this package. Runtime owns execution; @alwith/api owns protocol folding. Applications must not reconstruct run state from the chat presentation.
Import @alwith/module-chat/markdown.css for Markdown styling and include the package source in Tailwind's @source scan. With Vite, exclude the package from dependency pre-bundling, because Markdown runs in Web Workers created with new URL(…, import.meta.url), and pre-bundle the CommonJS shims its Base UI components import:
optimizeDeps: {
exclude: ["@alwith/module-chat"],
include: ["use-sync-external-store/shim", "use-sync-external-store/shim/with-selector"]
}Local linked development requires React deduplication in the app bundler; React is a peer, never bundled into chat. Bun component tests alias linked React to the real consumer React exports without installing a global DOM.
Run bun install at the workspace root, then bun check --no-script --cwd packages/chat and bun run test:chat. Consumer DOM tests live in both apps and cover the shared implementations through their real app adapters.
ACP session folding stays in @alwith/api. This package owns chat presentation and interaction, not Agent processes or a second session database.
session-timeline exports SessionTimeline, the whole conversation stream of one session: turns
grouped from the @alwith/api session, Codex activity, plans, edited files, async questions,
capacity retry, sparse native history and the navigation rail, in the virtual list, following the
stream until the user scrolls away. The application passes the session, a client (the
@nyssance/codex-acp-v2/client CodexClient, or anything with its store and history and retry
requests) and a SessionTimelineHost: the ActivityHost plus theme, and optionally file saving,
diff opening, file icons, tool relabelling and error reporting. Product content goes into slots:
leading, afterTurn, answerActions, turnFooter, renderUserMessage and insert. navigation
turns the rail on with the application's audio; anchorNewTurn anchors a new prompt at the top
instead of following the bottom; Viewport replaces the scrolling element. Key the component by
session. This entry needs @nyssance/codex-acp-v2 installed; it is an optional peer for the rest. turns exports the grouping for search sources and diagnostics, and timeline-registry
lets search reveal a turn in the mounted timeline. resource-content renders a file, link or image the way the timeline does, for an application's Markdown inside it; outside the timeline, wrap it in TimelineProvider. The update scheduler moved to @alwith/api/client.
conversation exports ConversationView, one session's whole chat surface: the timeline, the
restoring, detached, stopped and read-only states, inline permission and URL requests, the form
dialog, the diff dialog and the Composer. The application passes the session, a
ConversationClient (CodexClient satisfies it) and a ConversationHost: the timeline host plus
optional long-paste files (writeTextAttachment) and titles for its own tools (permissionTitle,
toolTitle). Product content goes into slots: header, overlay (search, previews), timeline
(the SessionTimeline slots), composer and children (application dialogs). Errors go to
host.reportError; a ReportedError carries a description under its message. Key the view by
session.
The Composer keeps a draft per session (text, images, mentions) for the life of the page and
exposes it through drafts, exportDraft, importDraft and setComposerDraft for other windows
and extensions. Its footer has slash commands, PermissionModeMenu, UsageMeter, ModelMenu and
send, steer and stop. Hosts add tools before the send button through actions and adapters inside
the form through children; both read the composer through useComposer() (session, readiness,
the draft port, the interrupt hook for dictation and the receipt observer for hands-free turns).
modelMenu supplies the catalog policy (group order, decorations, short labels, a header action);
modelSelector replaces the menu, such as before a draft session exists. ModelMenu and
PermissionModeMenu write through client.setConfig unless the host passes onConfigChange, and
ActionCard answers through client.respond unless it passes onRespond. Outside a
ConversationView, wrap these in ConversationProvider.
DraftProjectPicker is U's project capsule for an empty draft, meant for the composer's
inputHeader: the draft's folder name or "Select project", the recent projects (the listed
sessions' directories, newest first, or recentProjects), the host's native folder dialog
(host.chooseFolder, the desktop shell's chooseFolder; without it only recent projects are
offered) and the way out of a project. The Runtime's chat directory, learned through
client.refreshProjectContext(true), never shows as a project; onChange(null) means a draft
outside one. The application owns what a change does: U re-prepares its draft, the default
template starts a new draft session in the chosen directory (client.newSession(cwd, { draft:
true })) and discards the empty one.
session-list exports the sidebar's session list: project groups with five rows each and
"Show all", loading rows and retry, SessionRow with rename, fork or restore, archive, delete and
stop, the SessionInfoCard hover card, ArchivedSessions and the hooks behind them
(useActivityPreview, useProjectSessions). SessionListProvider takes the host and client and
owns the delete confirmation; slots add the application's row actions and group headers. Import
@alwith/module-chat/session-list.css once.
The same entry has U's Activity panel and the tabs in front of it. ActivityList shows the sessions
the Runtime runs, from its run-state registry: the selected session (and whatever the host reports
visible through isForeground) flat at the top, the rest under a collapsible Background group that
stays even when empty; rows carry the state dot, the reply preview, listening ports (opened through
host.openLink), running subagent tasks beneath their session, the stop button and Cmd/Ctrl+1–9
jumps with their hints. Rows keep the order they were first seen in; the Runtime stays the authority
for membership, so unsent drafts show only while client.state.drafts owns them. onOpen lets a
host open a row elsewhere (U: focus the window that owns it) and accessory marks it. NavigationTabs
is the pill tab bar (NavigationMenuIconButton rows, the sliding selection, the attention dot);
useNavigationView keeps the shown panel per client for the life of the application and
useActivityNotification(view) says when the Activity tab needs its dot; sessionNavigationStore(client)
reads the same state outside React and resetSessionNavigation(client) forgets it (a sign-out). Like SessionList, the
list renders no scroll container: mount it inside the application's scrolling element.
NavigationHeader is U's sidebar head: the application name as a drag region, its 32px icon
actions with tooltips and shortcut badges (shortcut in Tauri accelerator syntax, shown through
displayShortcut: "⌘N" on macOS, "Ctrl+N" elsewhere) and, as children, the menu rows beneath it
(NavigationItemButton with MENU_HIGHLIGHT). AccountMenu is the account row at the bottom:
the avatar and name opening the menu above it with the identity header, the application's
AccountMenuGroups of AccountMenuItems, AccountLanguageMenu over the application's language
list, AccountMenuSeparator between groups, and Sign out, whose failure goes to host.reportError.
HelpMenu is the question-mark button beside it, opening a menu as wide as the footer row with the
application's AccountMenuItems (U: keyboard shortcuts and the user guide). Both read strings and
error reporting from the session-list host, so mount them inside SessionListProvider.
ShortcutKey, ShortcutHint and ShortcutTooltip are the badge, label-plus-badge and tooltip the
header uses. Groups come from the @nyssance/codex-acp-v2/client
projection: Codex projects by name, then quick chats (the home directory, ~/Documents/Codex and
~/Documents/ALwith, on the Runtime's machine), then one group per remaining folder. The list waits
for the client's project context and never guesses quick chats from local path patterns, so this
entry needs @nyssance/codex-acp-v2 installed.
plugins is U's Toolbox: the Codex plugin and skill catalog with the installed plugins in its own
sidebar. PluginsPage({ cwd, active, onOpenCatalog?, extensions? }) loads the catalogs for the
current directory and the known conversation projects once it is active and the client's agent
reports the plugin store (hasPluginStore), and reloads them on client.onSkillsChanged. The
Plugins view lists local marketplaces and the remote catalog's featured plugins (search widens to
the whole catalog), installs, pages sections of 48, adds and removes marketplaces and upgrades them
all; the sidebar lists the installed plugins newest first and opens PluginDetailPage (information
strip, capabilities, MCP servers, skills, apps, hooks, enable switch, uninstall). The Skills view
groups skills by scope (Personal, System, Admin, one tab per repository) with the enable switch and
SkillDocument, which reads SKILL.md through the host and renders it like a chat reply.
extensions adds the application's own extension entry above Plugins, taking over the content area
while active. PluginsProvider({ host, client }) supplies both: the client is the CodexClient
members the page uses (PluginsClient), the store lives per client (pluginsStore(client),
usePluginsStore(selector)), and the host supplies t, i18n, theme, openLink and the
optional desktop pieces: assetUrl (icons and screenshots from local paths), readFile, openPath,
revealPath and copyText (the skill document and its actions; without readFile the catalog
description shows and the actions stay hidden), notify for what U showed as a success toast and
reportError for failures (an operation that fails reports, the page's own load error shows inline
with Retry). Import @alwith/module-chat/plugins.css once; PluginIconDefinitions is mounted by the
page. Strings are plugins.*, with plugins.close, plugins.copy and plugins.extensions for the
application words the page needs. createPluginsStore(api, host?) and installParams are the
store behind the page for hosts that drive it outside React.
ConversationView mounts the find bar itself (search defaults to true): ConversationSearch
feeds the package's ChatSearch with the session's turns through findCodexSearchSourceMatches
(mounted or not, including tool input and terminal output), reveals turns through the timeline
registry, loads the rest of the history before counting, and opens on Cmd/Ctrl+F or through the
host's subscribeSearch (the desktop shell's onDesktopMenu("find", open)) or
openConversationSearch(sessionId) (U's actions menu); shouldOpenSearch(event) lets a host keep
the key for its own editor. Strings are
conversation.search.*; the highlight colors come with the component.
search provides Desktop's complete find bar, DOM matching, highlights and navigation. The host supplies its viewport, labels and native-menu subscription; virtualized callers also supply source matches and an async turn-reveal callback. Key the component by session identity to discard the previous search on a session switch. Highlight ownership is per mounted search, and cancelled navigation cannot repaint after close. Source projection stays with the application's message model; the search component never reads a private store.
search-source shares Unicode-aware literal matching and original UTF-16 offsets with DOM matching. This does not make raw tool fields, Markdown source or folded content equivalent to rendered text: the current per-turn occurrence index still requires a complete source-to-rendered mapping. Do not treat the current search counter as proof that every indexed match can be revealed. Automatic expansion of folded matches has not been agreed.
permission-view supplies Desktop's approval list, sorting, keyboard selection and optional input-anchored portal. Without an anchor it renders inline. Applications supply request identity, details and the response callback; Desktop replay/vibemon actions and U's ACP pending-request routing remain in their hosts. Inline callers can select autoFocus="if-idle" to avoid taking focus from a draft.
choice-elicitation-form preserves Desktop's choice questions, Other inputs, previews and notes. schema-elicitation-form handles standard schema fields, including text, numbers and booleans. Applications answer the original pending request; the package does not send protocol messages. The latter reuses the API's property guards; local unpublished development resolves @alwith/api through the workspace root dependency and override.
url-elicitation-card provides U's inline pending-link presentation. The host opens the supplied URL and handles decline/cancel; opening a browser does not synthesize an acceptance response. Desktop's separate native URL-consent and authorization flow is unchanged, not replaced by this card.
Tool results use Desktop defaults; optional Output/Patch/image actions preserve inline terminal, diff and media behavior. The package includes TerminalOutput and PatchView implementations.
For i18next, wrap the application resources once with withChatResources(resources) from @alwith/module-chat/locales. This adds the alwithChat namespace without mutating application resources or choosing its active/fallback language. ActivityHost receives the translator for that namespace. Existing Desktop translations are included; unavailable entries follow the application i18n fallback policy.
The composer form comes from @alwith/module-chat/composer, with the controller/provider in composer-context and the textarea in composer-textarea. It owns file selection, drag/drop, image limits and preparation, and clears the draft after successful submission. Supply onSubmit and onSubmitError; application commands and local file identity are optional host policies. Desktop enables its bash border explicitly. Controls remain internal Desktop-derived primitives, not a general UI component kit. Locally linked apps deduplicate @base-ui/react as well as React to keep primitive contexts and hooks on the consumer instance.
composer-attachments provides the attachment list and chips, with Desktop's hover preview, removal and zoomable image dialog. The chip takes a translator for its three input labels. image-preview-dialog exposes the same preview for other chat image entry points; zoom, viewport fitting and pointer-centered scrolling remain package-owned.
completion-menu owns keyboard selection, focus, insertion and the anchored/inline menu; the host supplies completion data and async file search. model-select, effort-slider and permission-mode-select are controlled: callbacks request a change, while the host supplies the authoritative selected value. Provider catalogs, subscriptions, billing and settings navigation remain application policy.
navigation-rail owns Desktop's turn markers, preview, observation and scrubbing, and imports its CSS. The host projects turns, releases scroll following before reveal, supplies the virtualizer callback and injects navigation audio actions. navigation-audio and navigation-instruments share Desktop's sample cache, unlock lifecycle, instrument catalog and playback scheduling. Preferences and native diagnostics remain in the application. Linked consumer tests that mock smplr must resolve the same module instance as the package; Desktop deduplicates it alongside React and Base UI. Automated tests do not establish audible playback in a packaged application.
Collections (收藏)
@alwith/module-chat/collections is ALwith U's Collections tab and "Favorite" row action, together with the
collection model (Collection, CollectionData, CollectionAction, applyCollectionAction,
initialCollectionData, migrateCollections, DEFAULT_COLLECTION_ID, getCollectionName). There is no separate
collection package; @alwith/[email protected] remains on npm only for existing consumers.
Collections store organization preferences only (membership, order, name, emoji, collapsed). The agent remains the
session authority: titles, archive state and existence come from the session client of the surrounding
SessionListProvider, which the panel and the row action must be mounted inside. Strings are collection.* in the
alwithChat namespace (withChatResources); U has translated them for English and Simplified Chinese, other
languages fall back to English.
| Package owns | Application supplies |
| --- | --- |
| Collections tab: groups, emoji, drag-and-drop, rename/clear/delete dialogs, ⌘1–⌘9 | Persistence port, the scrolling element |
| "Favorite" hover-card action and collection picker | Session rows (renderRow), opening a chat, starting a new chat in a collection |
| Serialized saves; a failed save leaves the visible state unchanged | Emojibase data URL (optional), the sidebar tabs around the panel |
Persistence port
interface CollectionPersistence {
load(): Promise<unknown> // null/undefined: nothing saved yet
save(data: { collections; order }): Promise<void>
subscribe?(listener: () => void): (() => void) | Promise<() => void>
}
const collections = createCollectionStore(persistence, { reportError })dispatch(action | actions)applies the actions to the current state, callssaveonce and only then shows the result. Calls are serialized. A rejectedsave(disk full, a newer revision in another window) rejects that dispatch and leaves the visible state unchanged; the next user action proceeds normally. Nothing is retried.- The initial
loadruns at creation (ready). Until it succeeds every dispatch rejects with its error, so a value that failed to load is never overwritten with defaults.reload()loads again;subscribetriggers it for changes made elsewhere. A saved value of another shape is an error, never silently replaced.sessionMetais not persisted.
U wires its settings store (collections.data, revisioned writes). A create-alwith template wires
@alwith/module-store:
const settings = await openStore("settings")
const collections = createCollectionStore(
{
load: () => settings.get("collections.data").then((value) => value ?? null),
save: (data) => settings.set("collections.data", data),
subscribe: (listener) => settings.onKeyChange("collections.data", listener)
},
{ reportError: (error) => toast.error(String(error)) }
)Host
Collections read the surrounding SessionListProvider host (t, i18n, reportError). CollectionHostProvider
is optional and overrides or adds fields:
isMac?: boolean— ⌘ instead of Ctrl for 1–9; default: detected from the platform.emojibaseUrl?: string— base of the Emojibase data the emoji picker loads ({url}/{en|zh}/data.json). U serves a copy at/emojibase; without it frimousse fetches from its CDN, so desktop apps that must work offline serve the data themselves.
Mounting the 收藏 tab
The sidebar's tabs are, in order, 收藏 / 聊天 / 活动 (U adds Drive after them):
const tabs = [
{ key: "collections", title: t("collection.title"), icon: <StarIcon /> },
{ key: "sessions", title: t("sidebar.chats"), icon: <FolderIcon /> },
{ key: "activity", title: t("sidebar.activity"), icon: <ActivityIcon /> }
]
<SessionListProvider host={sessionListHost} client={client}>
<CollectionProvider store={collections}>
{view === "collections" ? (
<CollectionPanel
Scroll={SidebarScroll} // the chats tab's scrolling element; default: an overflow div
renderRow={renderRow}
onSelect={openSession}
onNewChat={(collectionId) => startChat({ collectionId })}
/>
) : …}
</CollectionProvider>
</SessionListProvider>renderRow(sessionRow, onHoverCardOpenChange, collectionId?) renders SessionRow with the row actions below.
onNewChat starts a draft; once the agent assigns the session id, the host saves it with
dispatch({ type: "add", collectionId, sessionId }).
Row action
Put the "Favorite" action into every session row's hover card (SessionList's rowActions, and renderRow
above). Inside a collection, also offer "Remove from group":
function SessionRowActions({ sessionRow, collectionId }: { sessionRow: SessionSummary; collectionId?: string }) {
const { t } = useSessionListHost()
const dispatch = useDispatchCollection()
const report = useReportCollection()
return (
<>
<AddToCollectionAction sessionId={sessionRow.sessionId} allowRemove={collectionId === undefined} />
{collectionId !== undefined && (
<HoverInfoAction
icon={<FolderMinusIcon />}
label={t("collection.removeFromGroup")}
onClick={() => report(dispatch({ type: "remove", collectionId, sessionId: sessionRow.sessionId }))}
/>
)}
</>
)
}CollectionPicker is the same menu behind a star button (favorite toggles the default group on click and opens
the menu on hover), for chat headers and project popovers.
License and redistribution
See LICENSE and third-party notices. ALwith-owned code is proprietary, not open source. You may build, customize, host, charge for and distribute your own applications under the included license, without per-application registration. This does not grant hosted-service access or rights to separately distributed Rust libraries or Runtime binaries. The npm artifact includes compiled/minified JS, declarations and necessary assets, not the private TS/TSX implementation or source maps. Minification is not encryption.
Default session list
The shipped session list owns its 30px row height and hover styling. Hosts do not need U-specific CSS variables. To use another layout, implement a replacement list against the session client rather than configuring the default list through additional style options.
Session status menu
SessionStateMenu from @alwith/module-chat/session-list is the compact four-dot
toolbar menu: needs attention (red), running (yellow), completed unread (blue),
and idle (green). Supply current Runtime sessions, the existing chat translator
t, and onSelect(session). Put it beside other toolbar actions in the same row.
The host supplies the visible session scope and excludes unused reusable drafts.
Historical sessions without Runtime state do not count as idle. Opening the menu
does not acknowledge unread sessions, stop agents, or create a second registry.
onSelect uses the application's existing session navigation path.
The component is engine-neutral and does not require a Codex client or provider.
Standard ACP child sessions appear in the shared SessionActions shelf, including
nested transcripts, advertised cancellation, and permissions or forms from every
descendant. Expanding a child reads its existing stream; it never opens another
agent session. ConversationView includes this shelf automatically; custom hosts
should mount one SessionActions for the root conversation. Requires
@nyssance/codex-acp-v2 0.7.47 or later.
Settings window: Codex account
@alwith/module-chat/settings is U's Codex box in the models section. The settings window has no agent connection of its own, so the main window serves its connection over window events: serveSettingsBridge(codexSettingsBridge(client), { listen, emitTo }) in the main window, createSettingsBridgeClient({ listen, emitTo }) once in the settings window. CodexAccountCard({ bridge }) renders inside a ConversationProvider (host strings, action cards): identity and plan limits with sign-out, LoginCard with the agent's auth methods when signed out, and the agent's session-less requests. The strings are connection.*, usage.* and the section label settings.provider in the chat namespace. ConnectionNotice from ./conversation is U's failed/dropped connection alert with reconnect for the main window.
Model providers
ProviderSection from @alwith/module-chat/settings is U's models section: the Codex box (CodexAccountCard), the ALwith account models when the host has an ALwith account, then provider pills for the built-in providers (DeepSeek, OpenRouter, Qwen with its regions, xAI) and custom OpenAI Responses providers, each with API key, optional API base URL, connection test and removal. A saved key puts that provider's models into every chat's model picker; nothing in ~/.codex changes.
Native plugin. The keys never reach the webview. They live in the package's Tauri 3 plugin alwith-chat on crates.io (runtime name alwith-chat):
alwith-chat = "0.6"Register the plugin with the application's Runtime access. An application without ALwith account integration uses init; it loads no native library and the account commands answer unavailable:
.plugin(alwith_chat::init(alwith_chat::runtime_fn(|app| {
app.state::<RuntimeState>().current()
})))An application with ALwith account integration uses init_with_account(runtime, development_directory). The ALwith account gateway (identity generations, revocation, the account catalog and request admission) is the private native chat library shipped in this package under native/<os>-<cpu>/; the application stages it with its other module libraries, and the plugin loads it from the bundled native resources (debug builds: development_directory). Revocation is ordered before every later request: feed each webview Runtime request through check(app, method, params) before sending it (run requests where requires_admission is true under the admission lock), pass lifecycle and model-selection results to observe(app, agent, session, result, Some(generation)) when confirms_model is true, report realtime requests with observe_realtime, and feed every Runtime event to observe_event.
Local session history and usage analytics are part of the same private library. Register the plugin with init_with_library(runtime, development_directory) (or init_with_account) and grant alwith-chat:sessions to the window that shows them: sessions_list (by source; Claude and ALwith through a metadata cache in the application cache directory's session-index, Codex directly), sessions_count_types and sessions_persona_evidence. The same set holds the usage commands of the usage-analytics extension, which keep the names and shapes it always called: get_model_pricing (the bundled models.dev table, per million tokens), scan_session_aggregates({ range }) and word_cloud({ range, sources }) (range today, week, days7, days30, month, or everything for all), daily_token_usage({ startDate, endDate }) (YYYY-MM-DD, endDate null for one day, at most 31 days), and export_week_sessions() and download_usage_json({ fileName, data }), which ask where to save with the official dialog plugin (register tauri_plugin_dialog::init()) and return null when the user cancels. Their errors are strings. File paths are accepted only below the session directories of the home directory. With init they answer unavailable. Rust hosts without a webview (Runtime services, alwith-dev) use the same operations through alwith_chat::sessions after alwith_native_client::configure_host or ALWITH_NATIVE_DIRECTORY, including SessionIndex, scan_all, aggregate_one, daily_token_usage, export_week, extract_session_meta and line access to large session files (sessions::lines).
runtime_fn gives the plugin its own admission lock. An application that orders session requests itself (U) implements RuntimeAccess with current and its admission lock instead. Grant alwith-chat:default to the windows that show the section (read, save, save custom, remove custom, test, apply, and read_skill_document); grant alwith-chat:managed-providers only to the main window of an application with ALwith account integration. The plugin keeps runtime-providers.json in the application data directory (U's file and format, private 0600, atomic replacement) and emits providers:changed with the public snapshot. Typed bindings of every command and both events are @alwith/module-chat/commands; errors are { code, message } (pending while affected sessions are busy).
Model catalogs. provider-catalog.json and the bundled Codex model catalogs are compiled into the crate from its assets/ (published to npm as assets/ too); the plugin writes the catalogs to model-catalogs/ in the application data directory when it loads and points each provider at its file. Applications stage nothing. A Codex bootstrap that merges them before Codex starts (U's CODEX_ACP_MODEL_CATALOGS) gets the paths from bundled_model_catalogs(app).
Applying to Codex. After every Runtime (re)start the host calls Providers::reset and schedule_apply(app); in the webview, applyProvidersWhenIdle(port) after each connect, before restoring model choices. Changes that affect busy sessions stay pending and are applied when the host reports a session idle (schedule_apply); the plugin repeats nothing on its own, and a failed application is shown as failed with an explicit Retry. Auxiliary agents (U's Gamepad AI) are configured with prepare_agent(app, agent_id) after the host validated the caller.
Webview. createProvidersPort() and createManagedProvidersPort() are built on the generated commands and events (@alwith/module-chat/commands, peer @tauri-apps/api); readSkillDocument(path, roots) likewise. useProviders(port, onError) is the newest snapshot; resolveSessionModel, providerGroups, gatewayModelId and isAccountModel give the model picker its groups and the default selection.
const port = createProvidersPort()
<ConversationProvider host={host} client={client}>
<ProviderSection port={port} bridge={bridge} group={SettingGroup} label={SettingLabel} />
</ConversationProvider>group and label are the desktop shell's SettingGroup / SettingLabel. Strings are the chat namespace's provider.*; failures go to host.reportError and confirmations to host.notify. Include the package in Tailwind's @source scan.
What the host owns. The ALwith platform account: U passes refreshAccountProviders (binds the signed-in account with createManagedProvidersPort().bind, then refresh(generation, accessToken) with a fresh token, and clear() on sign-out) and openTopUp (the account's top-up page); onRevoked reports sessions whose account model went away. Without refreshAccountProviders the account box is not rendered and the account commands are never called. ModelAliasSettings({ providers, store }) edits display names of account models in the host's settings store (ModelAliasStore).
Catalog data. The provider catalog and the Codex model catalogs are public package files: @alwith/module-chat/assets/provider-catalog.json and @alwith/module-chat/assets/catalogs/<provider>/models.json (scripts and tests read them from there; catalogs/custom/sources.json records where the custom catalog's capabilities were checked). A provider entry with "apiType": "anthropic" is an Anthropic Messages endpoint: the plugin registers it with that protocol, the adapter translates it for Codex (codex-acp-v2 0.7.63), and the connection test asks /v1/messages with x-api-key. In the webview, customModelEffort(model) gives a custom provider model's reasoning default from the bundled catalog, and presentModel(apiId) / MODEL_PRESENTATION the display names of account and provider models the catalogs name differently.
Windows native network diagnostics
Windows HTTP/SSE and WebSocket requests share the reqwest transport with native TLS (Schannel) and the same per-URL system proxy decision. A failed connection is never retried through another route or backend.
The native transport logs [alwith-native-network] JSON records for HTTP/SSE and WebSocket requests. transportBackend identifies the selected HTTP client (reqwest), separately from tlsBackend.
errorChain contains up to eight causes, each with a redacted message and debug capped at 256 characters.
errorKind: "tls" distinguishes native TLS failures from TCP connection failures. Schannel can hide its inner
IO error from source() traversal; its Windows error code remains available in the cause's debug text even
when osError is null.
proxyConfig records configuration presence only. proxySteps retains the user/machine lookup, PAC,
autodetection, manual proxy and bypass decisions, including WinHTTP failure codes. The transport uses the
Windows per-URL decision; environment proxy presence is recorded as ignored, without logging values.
A local HTTP 502 with NATIVE_NETWORK_CONNECT_FAILED indicates transport failure, not an upstream HTTP 502.
Use the matching request's error chain and proxy steps to identify the failing connection or TLS operation.
Skill documents
readSkillDocument(path, roots?) from @alwith/module-chat/plugins reads a skill's SKILL.md through the same plugin, for PluginsHost.readFile. The plugin reads only UTF-8 files whose canonical path lies under $CODEX_HOME (or ~/.codex), ~/.agents, or the .codex / .agents directory of a project directory passed in roots; links that leave those directories are refused.
Application client and adapter staging
stageEmojiData(directory) from @alwith/module-chat/stage copies the bundled Emojibase data (en, zh) the collection emoji picker loads; serve it at the host's emojibaseUrl (U: public/emojibase).
Applications import protocol clients and session projections from @alwith/module-chat/client, conversation import utilities from @alwith/module-chat/import, and build-time stageChatAdapter(directory, licenses) from @alwith/module-chat/stage. The chat package owns the adapter dependency and resolves it relative to its own installation; applications must not declare it separately or reach into a hoisted node_modules path. Staging preserves the published adapter bytes and includes both license notices. Runtime remains responsible for process ownership.
Existing Claude login (Bun host only)
readClaudeLogin() and requireClaudeAccessToken() from @alwith/module-chat/oauth
read the existing Claude Code / alwith-cli subscription login. They honor
CLAUDE_CODE_OAUTH_TOKEN and CLAUDE_CONFIG_DIR; macOS uses the account-scoped
Claude Code-credentials Keychain item (with the original directory hash for a
custom configuration directory), and Windows uses .claude/.credentials.json.
On macOS the file store is read only when the exact Keychain item is absent, never
on access denial, malformed data, or a locked Keychain. An existing Keychain
record without OAuth is authoritative and means there is no subscription login.
This entry is for a trusted Bun host or adapter, not a webview. It returns the
access token only to its caller and does not return refresh tokens. Never serialize
the returned record into UI state, logs, telemetry, or provider preferences. It
does not start a login, persist a second credential store, refresh, or log out the
owning application. Reads observe rotation and logout in the original store;
requireClaudeAccessToken() rejects a token expiring within 60 seconds, with an
instruction to refresh in the owning CLI. readClaudeLogin() returns null for
an absent login and rejects storage errors. Custom OAuth endpoints and descriptor
credentials are not supported by this reader.
The local OAuth translation candidate uses an explicit
X-Alwith-Anthropic-Auth-Mode: oauth header on an Anthropic gateway. It preserves
opencodex's OAuth request framing and permits credentials only at
https://api.anthropic.com. API-key routing remains the default. This is not yet
wired into the provider settings UI or a released adapter dependency. A successful
inference response does not establish which subscription or usage-credit bucket
was charged.
