跳到正文
cccskills
免费在 GitHub 公开

kolu-client

kolu's client component conventions: SolidJS patterns, UI component rules, and toast notifications. Use when building or changing a client component, a store, a subscription, or a toast.

中文简介正在准备中。当前显示原文说明。

查看安装方法

包含的文件(1)

  • SKILL.md12.7 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

UI

  • Extract reusable UI into SolidJS components (one component per file in packages/client/src/).
  • Follow the frontend-design skill for UI design decisions — bold intentionality over generic aesthetics.

SolidJS Patterns

  • State per domain: Extract shared state into useXxx.ts modules (singleton pattern — create store once, cache at module level). Keep App.tsx as a thin layout shell.
  • Commands stay declarative: commands.ts registers commands — it should not contain async RPC calls, error handling, or multi-step workflows. Those belong in useXxx.ts handlers. Commands just call deps.handleFoo().
  • Components own their behavior: If a component has a keyboard shortcut or toggle state, it should manage that internally (always mounted if needed), not leak it to the parent.
  • createStore over createSignal<Record>: For keyed state (e.g. per-terminal metadata), use createStore from solid-js/store for fine-grained per-key reactivity.
  • Controlled components need selection PROVENANCE (echo loops): When you bridge an imperative/controlled library component (@kolu/solid-pierre's Pierre FileTree, an editor, a map) into Solid — driving its state from a store AND subscribing to its change callback — that callback fires for BOTH real user actions AND your own programmatic writes (echoes). Forwarding an echo back into the store that drives the component closes a frame-rate feedback loop: store write → the effect re-applies it into the component → the component re-emits → store writes again, ping-ponging at ~60-120Hz (the Code-tab file-tree "shimmer", #1841). Cut it with provenance, not by de-duping values (the echo drifts by a row, so value-equality won't catch it): arm a one-shot userGesture token from a capture-phase click/keydown listener on the component's container (click, not pointerdown — it matches the event the component selects on and covers touch taps; composed events cross a shadow root, so the token is set before the component's own row handler runs) and forward ONLY when it is set, consuming it on the first emit. Mind the timing — a controlled component's change emit is environment-dependent: synchronous inside the gesture dispatch when driven directly (a unit-test dispatchEvent), but deferred past a microtask in the real app (framework render + real browser event). Disarm the token on the next animation frame (which survives BOTH), never queueMicrotask — in production the emit lands after the microtask, so a microtask disarm races ahead of it and drops every real interaction (this shipped as #1846 and dropped every click). Pin the wrapper CONTRACT with a scripted mock emit, not the real library's timing (a real-component integration test measures the synchronous test-env timing and can't guard the deferred production case). (This is NOT a coarse-signal over-subscription — a reconcile-backed store already isolates unrelated leaf ticks like foreground.title; the loop is the controlled-component echo itself.)
  • @solid-primitives: Prefer community primitives (makePersisted, createResizeObserver, makeEventListener) over hand-rolled equivalents.
  • Props stay reactive: Never destructure props. Access via props.xxx. Pass accessors when needed.
  • Memos for multi-consumer derivations: Use createMemo when 2+ reactive contexts read the same derived value. Use plain functions for single-consumer or trivial derivations.
  • Surface bound primitives for server streams: Use app.cells.X.use(...), app.collections.X.use(...), app.streams.X.use(inputFn), and app.events.X.use(inputFn, handler, { onError }) from wire.ts's surfaceClient for streaming server state. The framework owns snapshot+deltas, retry, and reconcile-vs-assign. For a SURFACE-scoped raw streaming RPC that doesn't fit a primitive (a bulk snapshot feed), use client.rawStream(name, proc, input, { onItem }) — it enrols the stream into client.health() structurally (and throws if driven outside a reactive owner), so a raw stream can't silently escape the health fact. For a stream that ALREADY owns its pending/error (a createSubscription over a surface procedure), join it with client.enroll(name, sub). The bare unenrolledStreamCall lives at @kolu/surface/client (NOT the @kolu/surface/solid barrel) and is for a stream DELIBERATELY carved out of the surface health() fact — reached through a typed raw ref, either a SURFACE-scoped one (client.streams.X.unenrolled / client.collections.X.unenrolledKeys, the terminal attach as activePadiStreams.terminalAttach.unenrolled, whose transient re-attach must not flicker the connection-health gate — #1591) or a genuine root RPC outside any surface: unenrolledStreamCall(proc, input, { signal, onRetry }). Reaching such a ref WITHOUT wrapping it in unenrolledStreamCall silently loses reconnect handling. For one-shot RPC calls, use plain client.* calls with createSignal or createResource.
  • A mirror's liveness rides in the fact: client.health().live is now the FULL conjunction — transport-live ∧ every readiness cell's predicate (sub errors stay in subs). A cell becomes a readiness gate via CellSpec.liveWhen?: (v) => boolean (e.g. surface-remote's connection cell declares liveWhen: (v) => v.state === "connected"); surfaceClient opens an eager standing sub for each, so a mirroredSurface's connection-liveness leg folds into live BY CONSTRUCTION. Read the complete fact — NEVER hand-AND connection.state === "connected".
  • Paint the connection dot from the fact: Render the status dot with <HostStatusPip health={…}> from @kolu/surface/solid/HostStatusPip (its own entry, like <SurfaceGate> — not the JSX-free @kolu/surface/solid barrel). Its green is fact-only (default gateStatus(h) === "ready", shared with <SurfaceGate>); NEVER color a status dot from a raw cell .state. There are TWO honest lights and they answer different questions — pick by which one the dot is making: a gate-aligned dot (green ⇔ the body is drawable, so a pending first frame withholds green) is <HostStatusPip> on gateStatus; a page indicator for a whole connection (green ⇔ what this page reads is arriving) is conn.readout() from connectSurface / connectSurfaces — five states (connecting/live/degraded/reconnecting/retired), pending does NOT degrade it, degraded names the stopped subs, and needsReload marks the one state only a reload recovers. Never re-derive either from client.health() beside a transport status by hand; and never paint an indicator from a transport status alone (that is the green-over-a-dead-subscription lie the readout exists to close).
  • Socket-backed clients use connectSurface / connectSurfaces: They wire the always-on heartbeat / half-open watchdog in, so health().live catches a silent half-open (laptop sleep / wifi roam). The half-open-blind path is unspellable over every wire link (websocketLink, stdioLink, unixSocketLink — all branded half-openable at the one wireClient chokepoint) — and unspellable for real, not by convention: surfaceClient/surfaceClients THROW unless handed a watchdog-backed LiveSignal, and the only minter is createLiveSignal (@kolu/surface/solid), which wires the watchdog first. The brand is membership in a module-private, un-reflectable WeakSet on the whole HANDLE (LIVE_SIGNAL_HANDLES) that only createLiveSignal adds to — there is no exported stamper to forge with — and there is no heartbeat: false on the minting path to mint a handle without a watchdog. So a bare { live: () => true } (or an open/close-only () => socketStatus() === "live") can never become the input surfaceClient accepts. The timing is fail-fast bounded, and — crucially — createLiveSignal builds the wire link over the socket it watches and reconnects, and carries it ON the handle, then probes a real system/live round-trip over THAT link: there is no caller-supplied probe target to fabricate, so the watchdog provably probes the socket it guards. And because surfaceClient/surfaceClients take the WHOLE handle and read .link/.live off it themselves, the link the client is built over and the link the watchdog probes are the SAME object by construction — the old "mint a brand over a healthy socket, then build the client over a self-rolled websocketLink(deadWs2)" forge is simply unspellable (no external caller supplies a separate link), with nothing to re-check at runtime. (The probe gate reads the same status the signal reads, not a second readyState, so the gate and the signal can't disagree.) Hand-building a client therefore means surfaceClient(surface, transport) — passing createLiveSignal's WHOLE returned handle (its link and watchdog-backed live, paired on one object by construction; kolu's combined wire.ts, the minimal examples) — never a hand-rolled signal or a self-built websocketLink. There is no separate { live } seam to thread a blind accessor through. A directLink (in-process, no transport) can't half-open, so it is passed bare; a stdioLink/unixSocketLink is a WIRE link that CAN half-open and is refused bare just like a websocket (its in-repo use is a raw .rpc client — kaval-tui — or surface-remote's hostSession.startLiveness hand-wiring its own watchdog, never the Solid surfaceClient). When two layers watch one socket (an admin socket whose connectSurfaces owns the watchdog AND a <SurfaceAppProvider> lifecycle over it), the NON-brand layer (createServerLifecycle / the provider) opts ITS watchdog out with heartbeat: false — that's watchdog-ownership coordination, never a blind brand.
  • mapArray for dynamic per-entity subscriptions: When the set of subscriptions is driven by a reactive list (e.g., per-terminal metadata), use mapArray to create subscriptions. SolidJS handles lifecycle — each item gets its own reactive owner, automatically disposed when removed from the list. (useCollection already does this internally; reach for mapArray directly only when composing outside the framework.)
  • Plain calls for mutations: No mutation wrappers needed. Terminal mutations go through the ACTIVE host's bound procedures face — activePadiRpc.lifecycle.create(...) / .kill(...), activePadiRpc.chrome.* — and handle errors with .catch((err: Error) => toast.error(...)). activePadiRpc is padiMap.useEntry(activeHost).procedures (the active-host bound procedures face, typed from the declaration — the keyed map's link folds the host key into every call; reserved system.* procs and the link-root escape hatch live on .rpc). For a SPECIFIC (non-active) host, read padiMap.entry(host).procedures.<ns>.<verb>(...) directly — there is no padiRpcOf wrapper. The root terminal.* namespace was deleted at W1.R7; client.server.* / client.daemon.* / client.hosts.* (runtime pool membership) remain at the link root. Server pushes update subscriptions automatically.

Toast Conventions

  • Semantic variants: Use toast.success() for success outcomes, toast.error() for failures, toast.warning() for degraded states (e.g. non-zero exit codes), toast.info() for informational notices with actions. Never use bare toast() for outcomes — reserve it for neutral notifications (tips, exit-code-0).
  • Colocated, not centralized: Keep toast calls next to the logic that triggers them (mutation onError callbacks, post-await success lines). Do not extract into a separate toast helper module.
  • richColors is enabled: The <Toaster> has richColors set, so semantic variants automatically get colored backgrounds. Choosing the right variant matters for UX.
  • Loading toasts for slow operations: Use toast.loading() + update via { id } for operations with perceptible delay (worktree create/remove, session restore). Pattern: const id = toast.loading("…"); try { await op(); toast.success("…", { id }); } catch (err) { toast.error("…", { id }); throw err; }. Avoid toast.promise() — it returns the toast ID (not the resolved value) and swallows rejections.
  • Action toasts: For persistent notifications requiring user action, use duration: Infinity with an action prop (see server-update toast in rpc.ts).
  • Always surface err.message: When catching errors for toast display, include the server's error message: .catch((err: Error) => toast.error(\Failed to X: ${err.message}`))`. Never swallow the message with a generic string.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

blog-post

免费

Write a kolu blog post grounded in the real build history — mine the Claude Code session logs behind a feature for the actual story (for large efforts, fan out over the transcripts with an ultracode workflow), draft it in the author's voice, and wire it into the Astro site. Use when asked to write a blog post or engineering essay about something that was built, especially one that should read as a narrative of what actually happened rather than invented marketing copy.

中文简介正在准备中。当前显示原文说明。

juspay/kolu822026年10月21日 更新

ci

免费

Run this repo's CI end-to-end — kolu's policy on top of the odu runner: banned opt-out flags, x86_64-linux pinning, pool upkeep, fail-fast surfacing, e2e timing evidence, and the green-gate. Triggers on "run CI", "drive CI", "re-run the pipeline", "close the red check", "warm the pool". Runner mechanics live in the `/odu` skill.

中文简介正在准备中。当前显示原文说明。

juspay/kolu822026年10月21日 更新

Launch the kolu dev server on two RANDOM free ports so it never collides with the running production `kolu.service`, remember the ports for the rest of the session, and tear down only the dev instance. Load before running the app locally — for evidence capture, driving a live kolu with the chrome-devtools MCP, or any `just dev` you'd otherwise run by hand. Triggers on "run kolu locally", "launch the dev server", "boot kolu", "drive a live kolu", "start the app to screenshot it", or before pointing chrome-devtools at a local kolu.

中文简介正在准备中。当前显示原文说明。

juspay/kolu822026年10月21日 更新

diataxis

免费

Classify, write, and audit documentation per Diátaxis (diataxis.fr) — the four-quadrant method (tutorials · how-to guides · reference · explanation). Encodes the compass as a decision procedure, each quadrant's contract as hard rules, and an executable mixed-mode audit. Project-agnostic; the per-repo doc-location mapping lives in a repo rule of the project's own (kolu: the Kolu overlay below). Use when writing any user-facing doc, structuring a docs site, or auditing existing docs for mixed modes.

中文简介正在准备中。当前显示原文说明。

juspay/kolu822026年10月21日 更新

The Dock's agent-state folds — one fenced source of truth in @kolu/terminal-vocab, the load-bearing order≠colour split, and the one "is something happening" question. Use when editing agentProjection or the canvas dock.

中文简介正在准备中。当前显示原文说明。

juspay/kolu822026年10月21日 更新

E2E test conventions — semantic selectors, arrange-steps that prove the process started, runtime markers that cannot match the shell echo, and absolute-path tool invocations. Use when writing or changing a .feature scenario under packages/tests.

中文简介正在准备中。当前显示原文说明。

juspay/kolu822026年10月21日 更新

查看 juspay 的全部技能

报告此技能的问题