sdk-console-architecture
The architecture decisions behind the TypeScript SDK packages (@stigmer/react-sdk, @stigmer/design-tokens, @stigmer/client) and the client apps that are thin shells over them, each with its reason, plus the theme-token reference. Use when adding or changing a hook, a component, a token, a streaming view, or any wiring between a client app and an SDK component.
- License 许可
- Apache-2.0
- 所属仓库
- stigmer/stigmer
- 最近更新
- 2026-11-13
- Stars
- ★ 9
- Forks
- 6
01 前言 Front Matter
- name
- sdk-console-architecture
- paths
- [ "sdk/react/**", "sdk/theme/**", "client-apps/web/src/**", "client-apps/desktop/src/**" ]
- description
- The architecture decisions behind the TypeScript SDK packages (@stigmer/react-sdk, @stigmer/design-tokens, @stigmer/client) and the client apps that are thin shells over them, each with its reason, plus the theme-token reference. Use when adding or changing a hook, a component, a token, a streaming view, or any wiring between a client app and an SDK component.
02 资源文件 1
- 参考文档references/theme-tokens.md5.0 KB
03 SKILL.md 正文
SDK and console architecture
Stigmer is a platform for platforms. Every line of UI code answers one question:
would a platform builder embedding Stigmer in their product need this? If yes,
it belongs in the SDK packages. The client apps (client-apps/web,
client-apps/desktop) are reference implementations; the SDK packages are the
product. The package guides (sdk/AGENTS.md, client-apps/AGENTS.md) carry the
one-line laws; this skill carries the decisions behind them, with the reason
each was taken, so that a change which bends one knows what it is bending.
The layered architecture
| Package | Role | Allowed dependencies |
|---|---|---|
@stigmer/client |
Typed API clients generated from the protobuf contract | @stigmer/apis |
@stigmer/react-sdk |
Data hooks, behaviour hooks, styled components | @stigmer/client, @stigmer/design-tokens, react |
@stigmer/design-tokens |
--stgm-* tokens, presets, the cn() utility |
none beyond clsx and tailwind-merge |
client-apps/web |
The Next.js console: routing, app shell, page layout | the SDK packages, next/* |
client-apps/desktop |
The Tauri app: Vite, React Router, native integrations | the SDK packages, @tauri-apps/* |
Dependencies flow downward only. The SDK never imports from a client app; client
apps never import from each other. sdk-import-boundaries in
tools/eslint-plugin-stigmer/rules/ enforces the direction.
The decisions
Each decision is named; cite it by name in code comments and reviews.
SDK first. Build every feature in @stigmer/react-sdk first (data hook,
then behaviour hook, then styled component) and consume it from a client app
second. When placement is uncertain, default to the SDK: extraction later is
harder than inclusion now.
Thin shells. Page files are routes and layout only, with no domain logic. A page file that grows past a screen of layout and imports is carrying logic that belongs in the SDK.
Headless first. Three layers, each independently importable: data hooks (API
data through @stigmer/client), behaviour hooks (interaction logic and state
machines), styled components (hooks composed with @stigmer/design-tokens
tokens). A platform builder adopts at the layer that fits: full control with
hooks only, partial control with hooks and their own rendering, zero effort with
the styled components. useSession() without <SessionViewer /> must be a
valid import, and the hooks layer is an entry of its own,
@stigmer/react-sdk/headless (sdk/react/src/headless.ts), which the root
re-exports and nothing that draws may enter.
No framework dependencies in the SDK. @stigmer/react-sdk has no next/*,
no @tauri-apps/*, no routing, no app-shell auth, no framework assumption; it
runs in any React environment. Navigation and image optimisation are the
consumer's responsibility, reached through callback props. The "use client"
directive is not a framework import: it is part of the React Server Components
specification and a no-op string for bundlers that do not know it.
Theme tokens only. Every visual property flows through --stgm-* tokens,
scoped to the .stgm container and @layer stgm, using the token family the
rendering context calls for. The reference is
references/theme-tokens.md.
Error messages are the interface. Every error states what happened, why, and what to do. A hook used outside its provider throws a message naming the provider to add; a data hook returns a typed error state; a styled component renders meaningful error UI, never a blank screen.
Generated types are the source of truth. @stigmer/apis feeds
@stigmer/client, which feeds @stigmer/react-sdk. Never hand-write a type
that duplicates a generated one; never raw-fetch beside a generated client.
Derived types (Pick, Omit, UI extensions) are fine.
One provider. StigmerProvider and useStigmer() are the only injection
point. The five-minute integration is: install, create a client, wrap in the
provider, import a component. SDK hooks use useStigmer() and nothing else; a
second provider needs a written reason.
The streaming pipeline. Real-time views ride one pipeline: a stream source,
the StreamController state machine (idle, connecting, streaming,
reconnecting, complete, error;
sdk/react-core/src/internal/stream-controller.ts), request-animation-frame
coalescing (buffer the latest, commit at most once per frame), structural
sharing (walk the tree by natural keys and keep unchanged references),
startTransition, useSyncExternalStore, and React.memo on the leaves. Never
useState(snapshot) for a high-frequency stream: it re-renders the whole tree
on every frame. Completion is phase-driven (a terminal run phase or an explicit
signal), never inferred from the stream ending.
Reference stability is architectural. A hook that returns an object literal
wraps it in useMemo; a callback's dependencies name the specific method used
(conv.sendFollowUp, not conv). These are not micro-optimisations; they are
what makes React.memo downstream work at all.
Opt-in behaviour changes. A new behaviour that changes DOM structure, scroll
behaviour, rendering strategy or bundle dependencies is opt-in through a prop
with a backward-compatible default (virtualized?: boolean defaults to
false). Automatic threshold switching mid-session is forbidden: it remounts,
loses state and resets scroll.
The one exception is a redesign the maintainer rules for every host, shipped as
a breaking (!) change whose body says so. The session viewer's plain-line
steps (stigmer/stigmer#3046) are one: a working turn's quiet stretch of steps
folds into one line once every step in it returned (never one still running or
one that failed), and a finished turn leads with its answer and folds its
working under "How it got there", for every reader. A host gets the new
presentation with the release; there is no prop for the old one.
Dependency licence policy. The SDK packages use only MIT- or Apache-2.0-compatible dependencies. A commercial-licence dependency is isolated to a client app; a dependency that imposes obligations on SDK consumers is a blocker.
Lazy loading for heavy dependencies. A library most screens never use is
imported through React.lazy or import(), so it is downloaded only on use:
react-virtuoso (sdk/react/src/internal/VirtualizedThread.tsx) and CodeMirror
(YamlEditor in sdk/react/src/manifest/YamlEditor.tsx is the loading
boundary, CodeMirrorYamlEditor.tsx the implementation). Lazy is not optional:
a bundler resolves every import it can reach, import() included, so a package
the kit names in any import is a dependency, never an optional peer. A library
too large to install for every host (mermaid) is not named by the kit at all:
the host passes its import (StigmerProvider's loadMermaid). An app with only
the required peers builds the kit with Vite and webpack
(test/install/smoke-libs.mjs).
Cache across mounts, reset by key. key={id} remounts are the correct React
pattern for a clean state reset; useFetch consumers pass a cacheKey for
cross-mount persistence (instant render of previously fetched data, refetch in
the background). The two coexist: the cache gives useFetch memory, the key
gives hooks clean resets.
Animation accessibility. All animation respects prefers-reduced-motion
(sdk/react/src/internal/motion-preference.ts), using a 0.01ms duration
rather than 0ms so animationEnd and transitionEnd callbacks still fire.
Class-based animation, not @starting-style, wherever virtualization recycles
DOM nodes, because @starting-style fires on every insertion including a
recycled mount. Animation CSS lives in @layer stgm with --stgm-motion-*
properties.
Client-app parity. Every client app consuming the same SDK component wires
its props identically, allowing only platform differences such as routing or
native APIs. When a fix or a feature changes how one client wires a component,
every other consumer of that component is reviewed and updated in the same
changeset. Unexplained divergence between clients is a defect. The check applies
to every shared component and matters most for SessionComposer,
MessageThread, ModelSelector and the flow hooks (useSessionPageFlow,
useNewSessionFlow).
Resilient streaming. Long-lived streaming hooks recover from transient drops
with exponential backoff and full jitter before showing any error. A
non-terminal interruption enters reconnecting, keeps the last-known-good state
visible (error stays null, isReconnecting is true), and surfaces a
terminal error only when retries are exhausted. isTransientStreamError in
@stigmer/client (sdk/typescript/src/errors.ts) classifies transient against
deterministic; not-found, validation and auth errors never retry. Reconnect is
on by default and tunable through autoReconnect and reconnectOptions;
reconnect() is the manual fallback. Every consumer of streamState.stage
treats reconnecting as live.
Node-resolvable ESM output. The SDK packages are consumed by bundlers and by
plain Node (the stigmer CLI, npx @stigmer/mcp-server). Node ESM needs an
explicit extension on every relative specifier, so the packages write the .js
suffix on every relative import, export and dynamic import under
moduleResolution: "bundler", the convention the CLI, the Ink package, the MCP
server and the generated protos already follow. An extension-less specifier
resolves under a bundler and crashes every Node consumer with
ERR_MODULE_NOT_FOUND, and development never catches it because tests run under
tsx (stigmer/stigmer#209). The .js is a no-op for Vite and is stripped for
Turbopack by client-apps/web/turbopack-js-to-ts-loader.js, which must cover
from, dynamic import( and side-effect forms. scripts/verify-esm-node.mjs
enforces the invariant before publish.
The scoping container's layout contract. StigmerProvider's in-tree
container ships with no sizing and must never gain a default. A default
height: 100% looks harmless under auto-height parents and balloons under a
flex-item parent, because flexbox treats a flexed size as definite for
percentage resolution; a flex-1 prose column would grow the container to the
full column height even on a content-sized page (measured on the docs site).
Fixed-height embedding (stigmer/stigmer#260) is an explicit opt-in the host
applies from its own CSS:
.stgm[data-stgm-root] { height: 100%; min-height: 0 }. Two marker attributes
are stable public selectors: data-stgm-root on the in-tree container only and
data-stgm-portal on the portal container only, never both on one element; a
popup opened inside a modal DialogShell portals into that dialog's own
unmarked target, since the top layer would cover the body container. The
className prop is a theming channel mirrored onto both scope containers and
must never become a layout channel.
sdk/react/src/__tests__/provider-theme-scope.test.tsx pins the markers and
sdk/react/src/__tests__/provider-container.layout.browser.test.tsx pins the
layout in a real browser.
Console chrome is SDK-owned. WorkspaceSidebar and SettingsSidebar are
public @stigmer/react-sdk/console surfaces rendered by the web console and the
desktop app, so the hosts' chrome cannot drift apart (stigmer/stigmer#317).
Per-host differences enter only through named seams: renderLink (the host's
router link, or an inert row), the footer slot (each host's user menu),
renderEntryAccessory (the desktop's background-run indicator), and data as
props (useRecentActivity output or frozen fixtures, plus a now instant for
deterministic hosts). The app-shell frame (column width, collapse state, zone
switching, responsive wrapping) stays client-app-owned under Thin shells. Never
re-transcribe sidebar markup into a client app or a demo scene; extend the
component's seams. The same holds for every depicted surface: a demo scene
renders the real SDK organism with fixture data and hand-draws only documented
page framing.
Dont-dos
- No client-app imports in the SDK: nothing from
client-apps/web,client-apps/desktopor an@/path. - No framework dependencies in the SDK: no
next/*,next-themes,@tauri-apps/*. - No hard-coded colours or sizes: no hex values, no
bg-white, notext-[14px](a type-scale step,text-2xsthe smallest; thetext-size-from-scalelint rule refuses the rest). - No opacity modifiers on tokens: a dedicated variant
(
text-sidebar-muted-foreground), never a/60suffix; a missing variant is a new token. - No technical-role folders in client apps: group by domain, as
client-apps/web/src/domain/andclient-apps/desktop/src/pages/do, never by role (a hooks folder, a services folder). - No
useState(snapshot)for streaming data. - No whole-object dependencies in
useCallbackoruseMemo. - No
@starting-stylein virtualized contexts. - No single-client fix for shared SDK wiring: find every consumer across the client apps and fix them all.
Integration ergonomics
SDK APIs are user interfaces. Method names, hook return types, component props, error messages and TypeScript intellisense are the surfaces a platform builder touches every day.
- Clean props, sensible defaults, minimal required configuration.
- If a developer cannot get a component running in five minutes, the developer experience has failed.
- Hooks are exported beside styled components.
- Naming is critical: a name becomes part of the platform builder's codebase and is expensive to change.
- Every exported hook, component and type is a public API contract.
- A reader can name every type a public signature takes or returns. A type from
a peer dependency (
@stigmer/apis,@stigmer/client,react) is imported from the peer; any other (the kit's own, or one from a plain dependency such as@stigmer/react-coreor@stigmer/plugin-package) is exported by the kit, as a type, from the entry the function is in. A reader installs only the kit and its peers. The docs snippet gate compiles every reference signature and names one that breaks this at its page and line. - An
@exampleis read in two places: its reference page and the editor's hover over the.d.ts, where nothing is hidden. So it holds no hidden setup (the generators refuse a---cut---line). It takes what it needs as a literal or as a small component's typed props, imports what its reader imports, and compiles on its page (docs/STYLE.md, "Code examples compile").
Before writing any component
- Does it belong in the SDK or a client app? Default to the SDK.
- Can it be themed through
--stgm-*tokens without leaking styles? - Does it work identically embedded in a third-party dashboard and in the console?
- Are its hooks exported independently of its styled component?
- Does it use generated types from
@stigmer/client, not hand-written ones? - If it changes how a client app wires an SDK component, has every client app been updated?
- Does it offer an act that changes something? Then it appears only on the
server's yes, never while the check is in flight: ask with
useCheckPermissions(sdk/react/src/grant/useCheckPermissions.ts) failing closed, or declarerequireson aDetailAction; reading stays fail-open, and no disabled button stands in for an act the server refuses.
04 同仓库更多 Skills
writing-hookify-rules
This skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
sql-migrations
Write safe database schema migrations - reversible steps, online index builds, and backfills in batches.
repo-conventions
How to change Go code in this repository - error wrapping, table-driven tests, doc comments, no new dependencies, and running go test before finishing.
incident-postmortem
Write a blameless incident postmortem - timeline, impact, root cause, contributing factors and follow-up actions with owners.
data-analysis
Summarise a data export or table - lead with the key finding and give every number its baseline or comparison.
customer-support
Answer customer questions about orders, returns and refunds from the written policies and the order system, ending with a clear next step.