Claude Skill

moi-workspace

The moi workspace — the web UI the user chats from, extended with agent-authored applets (widgets, views) plus theme & config. Read this FIRST when a message carries a hidden moi-context envelope or the user uses moi vocab such as workspace, applet, widget, view, scratchpad, dash

LLM Mart · 0 points · 12 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download molefrog-moi-workspace_.claude_skills_moi-workspace-fa41362.zip · 19 KB
Part of molefrog/moi — 4 skills

Install

skills CLI npx skills add https://github.com/molefrog/moi/tree/main/workspace/.claude/skills/moi-workspace
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install molefrog-moi@llmmart
Git git clone https://github.com/molefrog/moi.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole molefrog/moi collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

moi workspace

Workspace

You are working inside a moi workspace. It is a web UI that the user communicates with you through. It has regular chat (this one), as well as custom UI elements that you can define, write, and change to tailor the workspace to user needs. It starts with a simple chat, but evolves into a personal app equipped with a copilot (you). Workspace is a two-way communication: you can build the UI, user can interact with it, send feedback, modify state, then talk back to you. It's a shared UI that you and user work together in.

Workspace features/pages:

  • "Overview" - the workspace home, where small reusable full-stack Widgets surface quick info, status, and actions.
  • "Scratchpad" - a shared low-fi canvas for prototyping, working on ideas together, visualising concepts. Read references/SCRATCHPAD.md before building on or modifying it.
  • "Views" - provide a dedicated tab for sustained work, such as exploring data, filtering a table, or completing a multi-step task.

User can switch between these, but can access the chat (this conversation and other chats) from any place in the app (copilot mode), or on a dedicated page.

Workspace settings and customisation:

  • "Config": set name, icon, change other settings. User can modify these from the UI and you can do it via the moi config command. Call moi config --help for further docs.
  • "Theme": customize workspace fonts, colors, visual appearance. User can modify these from the UI and you can do it via the moi theme command. Call moi theme --help for further docs.

Glossary

"Workspace" or "Moi Workspace" — the web UI that the user works in, talks/collaborates with you, sees and interacts with "Applets".

"Project" - the primary working folder you (as an agent) work in. Managed by your harness; moi does not have a clear definition, but assumes this is the root folder in which it stores its state files.

"Chat" — a workspace is driven through agent conversations (this chat is one). Depending on your harness (Claude Code, openclaw, others differ in the details) there can be multiple chats, but they all share one workspace and one Project folder — the same filesystem, the same .moi folder, applets, config, and theme. Anything you build is visible to every chat, and another chat may have changed the workspace or the Project files since you last looked. Treat .moi and the Project folder as shared state, not yours alone. Internally, "chats" are sometimes aliased as "threads" — "chat" is product language, while "thread" is reserved for internal SDK, session, and persistence concepts.

"Applets" are standalone full-stack components that you write and maintain. They extend the Workspace UI.

"Applet Type" (one of)

  • "Widgets" (live on the Overview page)
  • "Views" are custom full-size pages that user can switch between.

"Moi CLI" — the globally installed moi command that you use to build applets, customize, and send events to "Workspace".

Where moi lives in filesystem

Source of truth - .moi folder in the root of "Project" folder. Contains source code of all Applets, bundled code, settings, etc. Can be committed to version control. Folder is partially initialised when Workspace starts, you have full ownership of it.

You do have access to the files in the root of Project — you can reference and load them from the "Applets" and elsewhere.

Folder structure:

my-agent-folder/
  .moi/
    widgets/                  <- source code of Widget React components
      total-users.tsx
      rps-chart.tsx
      _utils.tsx              <- `_`-prefixed files are shared code, not applets (optional)
      server-metrics.server.ts <- Server-side async functions the widget can call (optional)
      ...
    views/
      users.tsx
      crm.tsx
      users-api.server.ts     <- Server-side async functions the view can call (optional)
      ...
    package.json              <- Applet dependencies that you manage
    .workspace.json           <- Auto-generated. Do NOT read, edit, or `cat` this file. Use Moi CLI instead.
    .scratchpad.json          <- Scratchpad canvas snapshot. Internal — inspect only via `moi scratch read`, never open it.
    .scratchpad/              <- Scratchpad image files. Internal — pull pixels via `moi scratch read-image`, never open it.

Every dot-prefixed file or folder inside .moi/ (.build/, .cache/, .workspace.json, .scratchpad*, .gitignore, …) is a moi internal: auto-generated and liable to change format without notice. Avoid them as much as possible — do not read, edit, or delete them, and never point tooling at them; go through the moi CLI instead. Version control needs no special handling: the scaffolded .moi/.gitignore already excludes the machine-local entries (.build/, .cache/, node_modules/), while .workspace.json and .scratchpad.json are workspace state that ships with the repo — commit them as-is, just never hand-edit them. Your surface is the non-dot files: widgets/, views/, package.json, and code you place under .moi/ yourself.

Build environment

  • Bun is the required dependency of moi, so it must be installed
  • For package management always use bun
  • package.json is scaffolded during init. You are free to install/remove/do whatever with packages.
  • if packages aren't installed, it's your responsibility to call bun install
  • react and react-dom are stubs — they're provided by moi at runtime via the browser importmap. They're listed only so editors pick up the correct types.
  • moi bundle runs Bun's bundler, so standard Bun imports, loaders, and tricks apply (JSON, text, etc.) — see the Bun docs. Only the moi-specific imports (covered under Developing Applets) differ.

moi CLI

Treat moi as an external command — you cannot inspect or modify its sources. Use only the documented subcommands (moi bundle, moi bundle --force, etc.). Call moi --help for documentation. Run all moi commands from the project root — the folder that contains .moi/, never from inside .moi/ itself. You don't pass paths; moi resolves the workspace from where it's run.

Use the task-specific sections below for workflow guidance. The CLI will grow over time, so run moi --help to discover commands and moi <command> --help before using an unfamiliar command or option.

  • Develop applets: moi check, moi bundle, and moi refresh.
  • Call actions: moi call-server-fn.
  • Debug applets: moi debug logs (see Debugging applets).
  • Navigate the workspace: moi tabs and moi tabs focus (see Driving the workspace).
  • Customize the workspace: moi theme and moi config (see Customizing workspace appearance).
  • Use workspace env: moi env and moi env exec (see Environment & secrets).
  • Maintain workspace guidance: moi skill (see Keeping this skill current).

Customizing workspace appearance

moi theme shows the current font, color, radius, and agent appearance plus the available keys. Set one or several dimensions in a single command:

moi theme --font=<key> --color=<key> --radius=<key> --agent=<key>

Omit dimensions you do not want to change. Inspect with moi theme first instead of guessing keys; use moi theme --help if its options change.

Critical constraints when interacting with moi

  • Never read or modify files outside the .moi directory, unless the user explicitly asks. If you do need it -> ask for permission.
  • Do not start, stop, or inspect the Workspace web server — it is managed externally.
  • Do not run Git commands while building or verifying applets unless the user asks for Git work.

Developing Applets

Every applet — a Widget or a View — is a default-exported React component in .moi/<type>/<name>.tsx, optionally paired with a <name>.server.ts. moi bundle compiles each into a live module and reloads it in the browser. Run it after source edits. Read references/DESIGN.md first. Write normal React + Tailwind — below is only what's moi-specific.

Anatomy

// .moi/widgets/hello.tsx
import { useEffect, useState } from 'react'
import { getGreeting } from './hello.server' // optional server fn — see below

// Optional config — fields are per type (see Widgets / Views below). requiredEnv is shared.
export const config = { requiredEnv: ['API_KEY'] }

export default function Hello() {
  const [msg, setMsg] = useState('')
  useEffect(() => {
    getGreeting().then(setMsg) // call server fns like any async function
  }, [])
  return <div className="h-full w-full p-4">{msg}</div>
}

Imports resolve relatively (same folder, or elsewhere under .moi/ — e.g. ../lib/format) or from .moi/package.json deps — no @/ aliases. Files starting with _ (e.g. _utils.tsx) in widgets/ and views/ are never applet entry points — put code shared between applets there. moi bundle tracks these local imports: editing a shared module rebuilds every applet using it.

Applet styling

  • Use Tailwind for static styling. Do not add custom CSS, @apply, or static style={{}} values.
  • Use style={{}} only for computed data such as chart geometry, progress, per-item delays, or per-frame transforms.
  • Use icons from @tabler/icons-react. Do not add raw SVG icons or another icon pack. When the project provides .agents/rules/icons.md, follow its size and stroke policy.
  • Applets cannot import the host project's cn. If .moi/ui/utils.ts exists, import cn from ../ui/utils; otherwise use a local cx() when classes are conditional. Never build class names with template-literal ternaries.
function cx(...classes: (string | false | undefined | null)[]) {
  return classes.filter(Boolean).join(' ')
}

Standard UI components

Use bundled components for standard controls. Read the shared usage rules before composing them.

  • moi ui-components lists available components and recipes with installed state.
  • moi ui-components docs <name…> prints bundled usage docs. Read them before using an unfamiliar component or when its API may have changed.
  • moi ui-components add <name…> --install copies source into .moi/ui/ and installs npm dependencies. Pass all needed names in one call.
  • Import relatively, e.g. import { Button } from '../ui/button', then run moi bundle after editing applets.

Reuse familiar installed components without rereading their catalog entries, docs, or source. Inspect source only for local customizations, doc conflicts, or concrete build issues.

Server functions — <name>.server.ts

Export named async functions (only — no const, sync, or class) and call them from the component like ordinary async functions; arguments and return values are auto-serialized (Date, Map, Set, … work). They run on the Bun server with process.env and full filesystem access, at cwd = <workspace root> (the parent of .moi/, where you operate) — so workspace files are plain relative paths:

// hello.server.ts — read files, call APIs, query DBs…
export async function getGreeting(): Promise<string> {
  return (await Bun.file('./notes.md').text()).split('\n')[0]
}

The component fetches on mount; after you change underlying data a server fn reads, run moi refresh to re-pull it without a rebuild.

It's plain Bun — every Bun API is available with no setup: bun:sqlite, Bun.redis, Bun.s3, Bun.file, fetch, …

Workspace files & assets

  • Bundled asset — import logo from './logo.png' resolves to a URL at build time (images & fonts: png jpg gif svg webp avif ico woff woff2 ttf otf). For small art shipped beside the .tsx.

  • Workspace file — stream a file from the workspace via fileUrl from the moi package:

    import { fileUrl } from 'moi'
    ;<video src={fileUrl('clips/intro.mp4')} controls />
    

    fileUrl(path) maps a workspace-root-relative path to a streaming URL (HTTP range — media seeks, nothing is base64-inlined). Media/asset extensions only; .env, source, JSON and dotfiles are rejected. The path is plain data, so a .server.ts can return it and the component renders fileUrl(clip.file).

Rule of thumb: small own art → import; structured data → .server.ts returns it; large/streamable media → .server.ts returns the path, render with fileUrl().

Driving the workspace — focusTab & sendChatMessage

An applet can move the user to another tab and talk to you, through two functions from the moi package.

import { focusTab, sendChatMessage } from 'moi'

// A widget row drilling into a view, and a button that asks you to do something.
focusTab('view:orders', { order: 'o-1024' })
sendChatMessage('Chase order o-1024', { order: 'o-1024', carrier: 'dhl' })
  • focusTab(tab, params?) switches the workspace to a tab. Tab ids are overview, agent, scratchpad, and view:<id> — run moi tabs for the real list. params arrive as the target view's params prop.
  • sendChatMessage(message, context?) sends message to the active chat as if the user typed it. context is structured data you see and the user does not. Call it from event handlers, never during render. Context can contain additional instructions not visible to the user, describing how the task should be done.
  • params and context accept JSON serializable values only.

Params: the type is the contract

A view with addressable state declares a local Params type in its own file. Every field is optional and carries a comment, because the view must render sensibly with {} — a fresh mount, a plain tab-bar click, or a new browser tab all deliver nothing.

// .moi/views/orders.tsx
// The view's addressable state — what `focusTab('view:orders', …)` can set.
type Params = {
  // Order id to open in the detail pane; omit to show the list.
  order?: string
}

export default function Orders({ params = {} }: { params?: Params }) {
  // Values arrive from navigation state, so narrow before trusting them.
  const openOrder = typeof params.order === 'string' ? params.order : null
  …
}

Applets never import from each other, not even types. Before wiring a focusTab call, read the target view's source, mirror the shape you find there, and note where you read it. That file is the contract; the type is documentation, not a shared module. Widgets are never navigation targets — their params is always {}.

Environment & secrets

Each workspace has an effective env: keys from the project's .env / .env.local (when inheritance is enabled in settings) plus custom secrets the user manages in the workspace env settings. moi injects this env into:

  • applet server functions — read it as process.env inside .server.ts
  • any command run via moi env exec -- <cmd>
  • your own shell (Bash tool) — but only in some harnesses (e.g. Claude Code). Don't assume it: verify the key is visible first, or just use moi env exec, which works everywhere.

Rules:

  • Check before you assume. When a task needs a key or token — an API pull, a widget calling a service — run moi env first. It lists key names with their source (.env / custom) and flags declared requiredEnv keys that are missing. Values are never shown.
  • Key present → say which key you'll use and where it's from ("using NOTION_TOKEN from .env") and proceed. To run a script or one-off command with the workspace env, use moi env exec -- bun script.ts — it also picks up values changed after your session started.
  • Key missing → never invent or hardcode a value, and don't edit .env yourself. Tell the user the exact key name to add in the workspace env settings. Still build and wire the applet: declare the key in config.requiredEnv and handle its absence, so it works the moment the user sets it. If the user pastes a value in chat, store it with moi env set KEY=value (moi env unset KEY removes it).
  • Never print secret values — not in chat, not in logs. Refer to keys by name only.

Running commands with workspace env

Use moi env exec -- <command> [args...] for an arbitrary script or tool that needs the effective workspace env. The -- separator is required, and the env is resolved again on every invocation, so this also sees values added or changed during the chat.

moi env exec -- bun script.ts
moi env exec -- bun test integration.test.ts

Use moi call-server-fn instead when exercising an exported applet server function through its real runtime path.

process.env is readable only inside .server.ts (the .tsx runs in the browser) — keep API keys there. Either source may be absent, so always handle a missing key. List expected keys in config.requiredEnv — advisory only (it surfaces a hint in the UI and moi env; it's never enforced).

// forecast.server.ts
export async function getForecast(city: string) {
  const key = process.env.WEATHER_API_KEY // always current — env changes respawn the worker
  if (!key) return { error: 'Add WEATHER_API_KEY to your env' }
  const res = await fetch(`https://api.example.com/forecast?city=${encodeURIComponent(city)}`, {
    headers: { Authorization: `Bearer ${key}` }
  })
  if (!res.ok) return { error: `Weather API error ${res.status}` }
  return { data: await res.json() }
}

Verifying applets

Use moi check --only views/<id> or moi check --only widgets/<id> for the applet you changed. Use the kind alone only when the work spans several applets. The command owns the supported applet TypeScript setup and can gain more checks later. Do not create .moi/tsconfig.json, invoke tsc directly, or retry with ad hoc compiler flags and missing type packages.

For a frontend rebuild, use this stopping point:

  1. Run moi check --only <kind>/<id> once after source edits, then moi bundle --only <kind>/<id>.
  2. In the browser, exercise the changed interaction, such as reveal and rating, and check one narrow layout.
  3. Inspect moi debug logs --only <kind>/<id> --json for runtime errors.

Stop when these checks pass. Expand verification only when a check fails or the changed behavior needs another focused check. Do not search for repo tests by default. Run an existing applet test or moi call-server-fn only when the change touches the behavior it covers. If native-app inspection is unavailable, keep verification in the browser instead of retrying the unsupported tool.

After the final successful checks, always make tab focus the final workspace action:

  • After building or editing a widget, run moi tabs focus widgets.
  • After building or editing a view, run moi tabs focus view:<view-id>, using its file name or claimed builder id.

The focused applet is the handoff. Keep the final reply brief and user-facing. Do not include file or storage links, file paths, or bundle, test, and runtime-log summaries.

Widgets

Live cards on the Overview grid — many visible at once. config sets the grid footprint:

export const config = {
  colSpan: 2, // columns the card spans — 1–4
  rowSpan: 1, // rows the card spans — 1–4
  requiredEnv: ['API_KEY'] // optional env-key hints (advisory; see Environment & secrets)
} as const

Render content only: a plain h-full w-full region with no card chrome (rounded-*, shadow-*, or outer border) — the dashboard owns the shell, spacing, and elevation. It does not own the fill, so the widget must set its own opaque background. Changing colSpan/rowSpan needs moi bundle --force --only widgets/<id>. See references/DESIGN.md.

Debugging applets

moi bundle only proves an applet compiles — it can still fail to load in the browser, crash on render, or throw in its server functions. For frontend rebuilds, check runtime logs as required above. For other work, use these channels when smoke-testing new behavior or investigating a problem:

  • moi call-server-fn widgets/hello/getGreeting / moi call-server-fn views/crm/searchUsers '["ann", 10]' — run one .server.ts function directly (args are one JSON array). Each invocation runs in a fresh, isolated one-shot process with the same env, module loading, and timeout as the browser's calls, so a pass means the real path works — handy for trying a function without touching the UI. Server functions only; for arbitrary scripts use moi env exec.
  • moi debug logs — the applet errors the workspace has seen since each applet's last good build: browser-side load failures and render crashes, plus server-function (rpc) errors. The user's tab reports these automatically, so when the user says something is broken, what happened is usually already on record — a good first place to look. Entries clear when their applet next builds successfully. (moi debug is an experimental command group — expect its output and flags to evolve; use --json when you need to parse it.)

moi bundle's footer also mentions when runtime errors are on record, so standing breakage surfaces on its own.

Views

Full-screen apps, one per nav tab — the user switches tabs. A view has no router of its own, but it can be addressed: see Driving the workspace for focusTab and the params prop.

View builder requests

When the message's hidden <moi-context> envelope is marked View builder request, this chat is linked to a pending view tab. Before reading files, planning, or writing code, infer a short stable id, a clear sentence-case title, and a relevant icon from the requirements. Capitalize only the first word of the title. Your first action must claim them:

moi builder set <view-id> --builder <builder-id> --kind view --title "<title>" --icon <icon-id>

Choose the icon id from the available view icons in the hidden context. The id must use lowercase letters, numbers, _, or -. The first call locks the id; running the same command again may update its title and icon. After claiming, write .moi/views/<view-id>.tsx, use the same icon id in its config, and build it with moi bundle --only views/<view-id>. The tab uses the claimed title and icon while you work and changes into the built view after a successful bundle. (Bundling marks the view ready; the build state is otherwise server-managed, so you never set it to done by hand.)

export const config = {
  title: 'Customer overview', // sentence-case nav label — defaults to the file name
  icon: 'user', // icon id from the view-builder request
  requiredEnv: ['CRM_API_KEY'] // optional env-key hints (advisory; see Environment & secrets)
} as const

The inverse of a widget: a view owns its whole page — its own h-full w-full layout, scrolling (overflow-auto), padding, and chrome. Build it to read like an app screen. See references/DESIGN.md.

Keeping this skill current

This skill is installed with moi (via the CLI or the UI) and can fall behind when the moi CLI updates.

  • You'll know — moi commands warn you when this skill is behind.
  • To update — if the user asks for current or updated guidance, run moi skill update before applet work. Otherwise, update at the end of the task.
  • Reload selectively — the command reports which skills changed. Re-read this SKILL.md only when it reports that moi-workspace changed. If it did not change, keep using the copy already in context.
  • Then — if you updated, mention it.
Files (moi)
  • references
    • DESIGN.md 13.8 KB
      # moi applet design
      
      Applets should feel useful, clear, and at home in the workspace. Let their content give them character while keeping familiar typography, colors, and interactions.
      
      This guide is the highest-priority visual contract for widgets and views inside a moi workspace.
      Follow generic design guidance only where it agrees with this guide.
      
      ## Content and layout
      
      **Make the purpose clear at a glance.** Lead with the information or activity people came for. Use contrast to create clear levels of emphasis without making the interface feel washed out. Keep the main action easy to find.
      
      **Choose content before adding structure.** Include the information and controls the task needs. Add summaries, metrics, filters, and explanations when they help people understand or act. Available space is not a reason to add content. A sparse screen can be complete.
      
      **Consider the screen as a whole.** Individually useful elements can still compete or repeat one another. Consolidate related information and reveal secondary details when needed. Keep essential information and frequent actions readily available.
      
      **Give expression a clear focus.** Use typography, composition, color, imagery, texture, or a small content-linked metaphor to give the applet character. A visual detail may support the subject or mood without carrying data or enabling an action. Keep the treatment focused so the content remains easy to scan. Expand a focal region only when added working space helps the task.
      
      **Group through spacing and alignment.** Start with an open layout. Prefer spacing and alignment over separators. Add a divider only when adjacent regions need a visible boundary. Use surfaces only for interactive objects, independent scrolling regions, distinct states, or a dedicated work area. Keep ordinary headings, tabs, summaries, metrics, and sections unboxed. Avoid large rounded page frames and decorative rows of cards or nested cards.
      
      **Keep a consistent rhythm.** Use a 4 px spacing rhythm, with smaller gaps inside groups and larger gaps between them. Apply outer padding once. Align repeated values and actions so content changes do not move them around. When removing a container, remove its unnecessary padding and radius and realign its contents.
      
      **Keep the layout comfortable.** Give content enough room to scan without making it unnecessarily dense or stretching it to fill the frame. Use natural height for simple tasks and extra space for tables, boards, canvases, and other work areas that benefit from it. When everything fits comfortably, keep essential information and actions within one viewport. Adapt to narrow widths, long content, and chat sharing the workspace. Avoid horizontal page scrolling; wide content may scroll within its own bounded region.
      
      **Keep layers simple.** Prefer normal document order and bundled overlays with their existing portal and stacking behavior. Custom overlays must preserve applet scope and theme by using the closest `[data-applet]` root. Add minimal local z-index only when document order and a scoped portal cannot resolve the overlap.
      
      ## Shared system
      
      ### Typography
      
      **Use a small, consistent set of styles.** Establish clear roles for headings, ordinary content, and supporting information. Text with the same role and importance shares the same typography.
      
      Inherit the workspace font and use sentence case. Use regular weight for most text and medium for emphasis, including focal prompts or values when weight helps establish hierarchy. Reserve other weights for explicit user customization.
      
      **Choose readable defaults.** Use `text-sm` for controls and general UI, and `text-base` for focused content when space allows. Sizes below `text-sm` should be rare, limited to unusually tight controls. Use larger text for headings or a focal value.
      
      Reserve `font-mono` for code, commands, and code-like identifiers. Numeric values, measurements, percentages, timers, timestamps, prices, counts, labels, keyboard shortcuts, and key names use the workspace font. Add `tabular-nums` when stable widths or alignment help.
      
      ### Controls
      
      **Use familiar controls.** Follow the [UI component guidelines](UI-COMPONENTS.md) and use bundled components with their existing variants, sizes, and shapes. Controls with the same role and importance share component variants. Keep feedback near the action that caused it.
      
      ### Icons
      
      Use Tabler icons when they clarify meaning, identify a familiar action, or reinforce a focused visual metaphor. Avoid adding an icon to every label or control. Set stroke explicitly: `1.75` at 12–16 px and `1.5` at 20–24 px. Let components control icon size when provided.
      
      ### Color and surfaces
      
      **Follow the workspace theme.** Use semantic tokens and their matching foregrounds. The default root is `h-full w-full bg-background text-foreground`.
      
      For widgets, the host sets the `background` token to the workspace’s `primary`, supplies a readable `foreground`, and derives a light local `primary` action surface. The host wrapper is transparent: each widget must paint its own opaque root, normally with `bg-background`, or provide an opaque fallback behind full-bleed media. Views inherit the page theme.
      
      | Intent | Classes and guidance |
      | --- | --- |
      | Main content | `text-foreground` for primary content and controls |
      | Supporting content | `text-muted-foreground` for secondary emphasis |
      | Inset surface | `bg-card text-card-foreground`, used only for a distinct functional region |
      | Quiet fill | `bg-muted` for inset regions, skeletons, and disabled structure |
      | Control state | `bg-accent text-accent-foreground` for hover, active, and selected controls |
      | Main action | `bg-primary text-primary-foreground hover:bg-primary/90` |
      | State | Views may use `text-success`, `bg-success/10`, `text-destructive`, or `bg-destructive/10`; widgets use `text-foreground`. Add clear wording and, when useful, an icon or shape. |
      | Object edge | `ring-1 ring-border` for complete container and control outlines |
      | Focus | `focus-visible:ring-2 focus-visible:ring-ring` |
      | Separator | A one-sided border such as `border-b border-border` between adjacent regions |
      | Floating content | `bg-popover text-popover-foreground` for menus, tooltips, and popovers inside the applet scope |
      
      Use `text-foreground` and `text-muted-foreground` together to create clear hierarchy. Keep every level comfortably readable and avoid letting muted text dominate the screen. Communicate meaning through wording, symbols, or shapes as well as color.
      
      Use additional colors for meaningful data, imagery, brands, a content-linked visual accent, or an explicit user preference. Keep data mappings consistent and understandable. Derive custom tonal treatments from local tokens.
      
      **Keep surface treatment restrained.** Use the shared radius scale and existing control shapes. Reserve `rounded-full` for pills, circles, avatars, and status dots; do not turn rectangular controls into capsules. For custom regions, use rings for complete outlines and one-sided borders for separators; avoid doubling them.
      
      **Let texture support readability.** Texture is optional, limited to the root, and backed by an opaque semantic color. Available treatments are `texture-checker`, `texture-grid`, `texture-noise`, `texture-gradient-linear`, and `texture-inset-shadow`.
      
      For dense root content, use plain `bg-background`, `texture-gradient-linear`, or `texture-inset-shadow`. Checker, grid, and noise may surround dense content only when it already needs an opaque work area. Choose a quieter texture before adding containers. Avoid stacking or tuning textures, and omit them behind full-bleed imagery or visualizations. Coordinate related widgets and views.
      
      ### Interaction and states
      
      **Make every action usable.** Controls need clear names, comfortable targets, keyboard support, and visible focus. Charts need understandable labels or equivalent information. Preserve accessibility when simplifying the screen.
      
      **Show the states the applet can reach.**
      
      - **Loading:** preserve the expected content shape with a skeleton; use spinners for compact or shapeless work.
      - **Empty:** explain the absence and offer a relevant next step when one exists.
      - **Error:** explain the problem plainly and provide recovery.
      - **Refreshing:** keep existing content visible and show progress near the refresh action.
      - **Stale:** identify outdated information near the affected content.
      - **Disabled:** keep the control recognizable and make its unavailability understandable.
      
      Keep the main work surface in the same position and size as states change. Feedback must fit the applet’s available space.
      
      Use brief, interruptible motion to explain changes or acknowledge input. Respect reduced motion, avoid looping decoration and animation on frequently changing numbers, and make audio and video user-initiated.
      
      ## Widgets
      
      **Widgets support a quick glance or immediate action.** Choose the smallest footprint that presents the content clearly. Keep the main information prominent and supporting controls limited to the immediate task. Put tabs, deeper navigation, and multi-step work in a view. Keep hover feedback on controls; leave the widget surface unchanged.
      
      The host owns the widget’s outer shape, clipping, and elevation. Its wrapper is transparent. Set an opaque background on the widget’s full-size root without adding another outer card.
      
      The grid has four columns, 160 px rows, 8 px gaps, and a maximum width of 640 px. Widgets span one to four rows and columns. Width is flexible; height is fixed:
      
      `rowSpan × 160 + (rowSpan − 1) × 8`
      
      Fit content and reachable states within that height without accidental scrolling or clipped controls.
      
      ## Views
      
      **Views support sustained work.** Each view represents one screen and owns its layout, padding, and scrolling. Establish clear context, a primary work area, and relevant actions. A title is useful when the content does not already establish context.
      
      Prefer one main scroller. Add bounded internal scrolling only when independent scroll positions help the task. Internal tabs, filters, and master-detail layouts may organize the current task. Cross-screen navigation belongs to the workspace; do not add a client-side router.
      
      ## UI copy
      
      Words appear in a design for one reason: to make it easier to understand and use. They are design
      content, not decoration. Bring the same intentionality and minimalism to copywriting that you would
      bring to spacing and color. Before writing anything, ask what the design needs to say, and how it can
      best be said to help the person navigate the experience.
      
      Write from the end user's perspective. Name things in simple language people understand, not by how
      the system is built. Describe what something is or does in plain terms rather than selling it.
      Prefer specific, clear wording over clever wording.
      
      Use active voice by default. An action says exactly what will happen: "Save changes," not "Submit."
      Keep the same action name through the whole flow, so a button labeled "Publish" produces a
      confirmation that says "Published." Interface vocabulary helps people find their way around, and
      consistency helps them learn it.
      
      Treat failure and empty states as moments for direction, not mood. Explain what went wrong and how
      to fix it in the interface's voice. Errors do not apologize, and they are never vague. An empty state
      is an invitation to act.
      
      Keep the tone conversational: plain verbs, sentence case, no filler. Let each written element do
      exactly one job.
      
      Omit terminal periods in headings, labels, metadata, and short standalone UI lines. Use sentence
      punctuation for paragraphs or multiple complete sentences.
      
      ## Design slop
      
      Design slop is a repeated visual shortcut that makes an interface feel generic or agent-made. Avoid decorative patterns added by habit instead of because they suit the content.
      
      - **Decorative accent lines:** Do not place short colored lines, partial borders, or a single colored edge beside headings, prompts, or values as generic decoration. Choose a visual treatment connected to the content, or leave the area open.
      - **SaaS card kit:** Use repeated cards when they represent a real repeated unit, such as comparable
        metrics, records, or actions. Avoid turning unrelated sections into identical rounded containers
        with the same radius, border, and shadow. Surface treatment should reflect hierarchy and function.
      - **Template chrome:** Avoid stock decoration: all-caps eyebrows, middle-dot metadata,
        `LABEL — fragment` headings, monospace labels, and arrows on every action. Use these patterns only
        when they clarify hierarchy, state, or sequence.
      
      ## Final review
      
      - **Focus:** Identify what people notice first and what they can do next. Reduce competing emphasis.
      - **Subtract:** Temporarily remove secondary groups, helper copy, badges, icons, and wrappers. Restore an element when its absence creates a specific problem with the task, a decision, a state, or recovery. A single deliberate visual metaphor may remain when it gives the content recognizable character without adding copy, controls, or containers. Preserve useful content and accessible labels.
      - **Deduplicate:** Read all visible copy together and compare meaning, not wording. When two elements communicate the same context or instruction, keep the clearer one closest to the relevant action.
      - **Character:** Identify the applet’s visual idea. If it relies only on text and standard controls, consider one content-linked accent, metaphor, image, or expressive composition. Keep it subordinate to the main content.
      - **Unify:** Compare text styles and control variants by role, then check their combined emphasis. Merge unnecessary differences in typography, color, spacing, and shape.
      - **Contrast:** Check that contrast creates clear levels of emphasis and the screen does not feel washed out.
      - **Verify:** Check realistic content, narrow widths, reachable states, keyboard use, and whether content that should fit requires scrolling. For widgets, also review how the result sits beside other applets.
      
    • SCRATCHPAD.md 3.7 KB
      # Scratchpad
      
      The Scratchpad is the workspace's **shared whiteboard** — one freeform tldraw canvas the user
      draws on by hand and you drive through `moi scratch`. Same canvas, two authors; disk is the
      source of truth. Use it to sketch, diagram, annotate the user's drawing, or lay out boxes.
      
      ## Seeing
      
      **Always inspect the canvas through the commands below — never open, `cat`, `read`, or parse the
      snapshot file yourself.** The on-disk file (`.moi/.scratchpad.json`) is moi-internal: its schema is
      tldraw's and shifts without notice, blobs are stripped, and `moi scratch read` already gives you the
      clean, agent-friendly view. Treat it exactly like `.moi/.workspace.json` — CLI only.
      
      - `moi scratch read` — dump the canvas as JSON: each shape's `id`, `type`, position, size, and
        text. Off disk, so it works whether or not a browser tab is open. An image shape's `src` is a
        reference (`asset:...` or a URL), never the pixels; `missing: true` on a shape means its image
        file is gone, so don't bother calling `read-image` on it.
      - `moi scratch read-image <id>` — save one image shape to a file (its actual bytes; `read` only
        carries the reference). Off disk too.
      - `moi scratch view` — render the whole canvas to a PNG. Needs an open Scratchpad tab.
      
      `read` is for logic; `view` / `read-image` are for vision.
      
      ## Drawing
      
      ```
      moi scratch add text   --at <x,y> --text "..."           [--id NAME] [--color C] [--font-size S]
      moi scratch add rect   --at <x,y> --size <w,h> [--text]   [--id NAME] [--color C] [--fill F] [--font-size S]
      moi scratch add note   --at <x,y> --text "..."            [--id NAME] [--color C] [--font-size S]
      moi scratch add arrow  --from <id|x,y> --to <id|x,y>      [--id NAME] [--color C] [--stroke W] [--elbow]
      moi scratch add image  <path>                             [--at <x,y>] [--id NAME] [--quality lo|hi]
      moi scratch move   <id> --to <x,y>
      moi scratch set    <id> --text "..."
      moi scratch delete <id>
      moi scratch clear
      ```
      
      - `--id` names a shape so later commands can address it (`move` / `set` / `delete`, or as an
        arrow endpoint). Without it, the command prints the generated id.
      - `add arrow --from box1 --to box2` binds endpoints to those shapes, so the arrow follows when
        they move. Endpoints can also be bare `x,y`. `--elbow` routes with right angles.
      - `add image` resizes to fit the canvas — `--quality lo` (default) or `hi` for more pixels — so a
        huge file never gets embedded whole.
      - `--color` is `black|red|yellow|green|blue|grey` or any hex (snapped to nearest). The other style
        flags mirror each shape's toolbar controls: `--fill` (rect) is `none|semi|pattern|solid`;
        `--font-size` (text, note, and a rect's label) is `regular|big`; `--stroke` (arrow) is
        `small|large`. You can only make what the user can make by hand.
      - `--fill` picks how a rectangle's interior is painted (the outline is always the full `--color`):
        - `none` — transparent interior, just the colored outline. Reach for this to box/group other
          shapes without hiding them, or when the rect is a frame.
        - `semi` — a light, translucent wash of the color. Soft highlight; text and shapes underneath
          still read through it.
        - `pattern` — diagonal hatch lines in the color over a near-transparent interior. Reads as
          "marked / selected / special" without going fully opaque.
        - `solid` — fully opaque fill in the color (interior matches the outline). Use for solid blocks,
          legend keys, or a label chip; anything behind it is hidden.
        A rect's outline is always a rough (hand-drawn) stroke, matching what the toolbar draws. Default
        fill is `semi`; pass `--fill none` for an outline-only box or `--fill solid` for an opaque block.
      - Coordinates are tldraw canvas space (origin top-left, y down).
      
    • UI-COMPONENTS.md 3.3 KB
      # UI components
      
      Use bundled components for standard controls. They use Base UI, Tabler icons, and workspace
      theme tokens. Follow [DESIGN.md](DESIGN.md) for visual decisions.
      
      ## Workflow
      
      Run commands from the project root. Check what's available and installed, read the docs
      before using or changing a component, then add all missing components in one call.
      
      ```sh
      moi ui-components                                # catalog and installed state
      moi ui-components docs select date-picker         # usage and examples
      moi ui-components add select date-picker --install
      moi bundle                                       # rebuild after editing applets
      ```
      
      `add` copies source and support files into `.moi/ui/`. `--install` runs `bun install` in `.moi/`
      and may need network access. Without it, run the dependency command printed by `add`. Rebuilding
      with `moi bundle` is always a separate step.
      
      Recipes, ready-made component compositions, use the same `add` and `docs` commands. They install
      their building blocks; follow the recipe docs to compose them.
      
      ## Imports
      
      Use relative imports from applets and shared modules under `.moi/`:
      
      ```tsx
      import { Button } from '../ui/button'
      import { cn } from '../ui/utils'
      import { IconSearch } from '@tabler/icons-react'
      ```
      
      Adapt aliases in examples to relative imports. Applets cannot use `@/` aliases or import host
      components. Use `moi ui-components` for installation; no `components.json` setup is needed.
      
      ## Composition
      
      Use the anatomy and props in `moi ui-components docs <name>`. Inspect the installed source
      when an example differs or a component has local edits; it defines the supported API.
      Keep required groups, labels, and titles when composing parts.
      
      Custom triggers and close controls use the `render` prop:
      
      ```tsx
      <DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>
      ```
      
      Don't nest buttons inside triggers. When `render` replaces a button with a non-button
      element, also pass `nativeButton={false}`.
      
      ## Overlays
      
      Use the installed portal and stacking behavior; don't add a second portal or z-index overrides.
      `applet-portal.tsx` preserves applet styles for portalled content. Never edit or remove it.
      
      ## Styling and icons
      
      - Use semantic theme tokens and built-in variants. Reserve `className` for layout;
        avoid overriding component colors or typography and adding manual `dark:` color overrides.
      - Prefer default sizes. Use smaller controls when space calls for them, keeping neighboring
        controls consistent and comfortable to use.
      - Prefer `gap-*` for spacing, `size-*` for equal width and height, and `truncate` for ellipsis.
      - Use `cn()` from `../ui/utils` for conditional classes.
      - Use Tabler icons with explicit `stroke` following [the icon guidance](DESIGN.md#icons).
        Let components size their icons; in buttons, mark position with
        `data-icon="inline-start"` or `data-icon="inline-end"`.
      
      ## Customization and updates
      
      Use `moi theme` for workspace-wide appearance. When variants and layout props aren't enough,
      edit the source in `.moi/ui/`; every importing applet gets the change. Put reusable compositions
      in `_`-prefixed shared modules next to applets.
      
      Existing files are skipped by `add`. Use `--force` only for an intended reinstall, after checking
      local edits. Existing support files remain protected even with `--force`; request a dependency
      component explicitly to update it.
      
  • SKILL.md 23.6 KB
    ---
    name: moi-workspace
    description: The moi workspace — the web UI the user chats from, extended with agent-authored applets (widgets, views) plus theme & config. Read this FIRST when a message carries a hidden moi-context envelope or the user uses moi vocab such as workspace, applet, widget, view, scratchpad, dashboard, or a `moi` command, or asks to build, edit, customize, or theme the workspace UI or its layout.
    ---
    
    # moi workspace
    
    ## Workspace
    
    You are working inside a **moi workspace**. It is a web UI that the user communicates with you
    through. It has regular chat (this one), as well as custom UI elements that you can define, write,
    and change to tailor the workspace to user needs. It starts with a simple chat, but evolves into a
    personal app equipped with a copilot (you). Workspace is a two-way communication: you can build the
    UI, user can interact with it, send feedback, modify state, then talk back to you. It's a shared UI
    that you and user work together in.
    
    Workspace features/pages:
    
    - "Overview" - the workspace home, where small reusable full-stack Widgets surface quick info,
      status, and actions.
    - "Scratchpad" - a shared low-fi canvas for prototyping, working on ideas together, visualising
      concepts. Read `references/SCRATCHPAD.md` before building on or modifying it.
    - "Views" - provide a dedicated tab for sustained work, such as exploring data, filtering a table,
      or completing a multi-step task.
    
    User can switch between these, but can access the chat (this conversation and other chats) from
    **any place in the app** (copilot mode), or on a dedicated page.
    
    Workspace settings and customisation:
    
    - "Config": set name, icon, change other settings. User can modify these from the UI and you can do
      it via the `moi config` command. Call `moi config --help` for further docs.
    - "Theme": customize workspace fonts, colors, visual appearance. User can modify these from the UI
      and you can do it via the `moi theme` command. Call `moi theme --help` for further docs.
    
    ## Glossary
    
    "Workspace" or "Moi Workspace" — the web UI that the user works in, talks/collaborates with you,
    sees and interacts with "Applets".
    
    "Project" - the primary working folder _you_ (as an agent) work in. Managed by your harness; moi
    does not have a clear definition, but assumes this is the root folder in which it stores its state
    files.
    
    "Chat" — a workspace is driven through agent conversations (this chat is one). Depending on your
    harness (Claude Code, openclaw, others differ in the details) there can be **multiple chats**, but
    they all share **one** workspace **and one Project folder** — the same filesystem, the same `.moi`
    folder, applets, config, and theme. Anything you build is visible to every chat, and another chat
    may have changed the workspace or the Project files since you last looked. Treat `.moi` and the
    Project folder as shared state, not yours alone. Internally, "chats" are sometimes aliased as
    "threads" — "chat" is product language, while "thread" is reserved for internal SDK, session, and
    persistence concepts.
    
    "Applets" are standalone full-stack components that _you_ write and maintain. They extend the
    Workspace UI.
    
    "Applet Type" (one of)
    
    - "Widgets" (live on the Overview page)
    - "Views" are custom full-size pages that user can switch between.
    
    "Moi CLI" — the globally installed `moi` command that you use to build applets, customize, and send
    events to "Workspace".
    
    ## Where moi lives in filesystem
    
    Source of truth - `.moi` folder in the root of "Project" folder. Contains source code of all
    Applets, bundled code, settings, etc. Can be committed to version control. Folder is partially
    initialised when Workspace starts, you have full ownership of it.
    
    You _do have_ access to the files in the root of Project — you can reference and load them from the
    "Applets" and elsewhere.
    
    Folder structure:
    
    ```
    my-agent-folder/
      .moi/
        widgets/                  <- source code of Widget React components
          total-users.tsx
          rps-chart.tsx
          _utils.tsx              <- `_`-prefixed files are shared code, not applets (optional)
          server-metrics.server.ts <- Server-side async functions the widget can call (optional)
          ...
        views/
          users.tsx
          crm.tsx
          users-api.server.ts     <- Server-side async functions the view can call (optional)
          ...
        package.json              <- Applet dependencies that you manage
        .workspace.json           <- Auto-generated. Do NOT read, edit, or `cat` this file. Use Moi CLI instead.
        .scratchpad.json          <- Scratchpad canvas snapshot. Internal — inspect only via `moi scratch read`, never open it.
        .scratchpad/              <- Scratchpad image files. Internal — pull pixels via `moi scratch read-image`, never open it.
    ```
    
    Every dot-prefixed file or folder inside `.moi/` (`.build/`, `.cache/`, `.workspace.json`,
    `.scratchpad*`, `.gitignore`, …) is a moi internal: auto-generated and liable to change format
    without notice. Avoid them as much as possible — do not read, edit, or delete them, and never point
    tooling at them; go through the `moi` CLI instead. Version control needs no special handling: the
    scaffolded `.moi/.gitignore` already excludes the machine-local entries (`.build/`, `.cache/`,
    `node_modules/`), while `.workspace.json` and `.scratchpad.json` are workspace state that ships
    with the repo — commit them as-is, just never hand-edit them. Your surface is the non-dot files:
    `widgets/`, `views/`, `package.json`, and code you place under `.moi/` yourself.
    
    ## Build environment
    
    - Bun is the required dependency of moi, so it must be installed
    - For package management **always** use bun
    - package.json is scaffolded during init. You are free to install/remove/do whatever with packages.
    - if packages aren't installed, it's your responsibility to call `bun install`
    - `react` and `react-dom` are stubs — they're provided by moi at runtime via the browser importmap.
      They're listed only so editors pick up the correct types.
    - `moi bundle` runs **Bun's bundler**, so standard Bun imports, loaders, and tricks apply (JSON,
      text, etc.) — see the Bun docs. Only the moi-specific imports (covered under **Developing
      Applets**) differ.
    
    ## `moi` CLI
    
    Treat `moi` as an external command — you cannot inspect or modify its sources. Use only the
    documented subcommands (`moi bundle`, `moi bundle --force`, etc.). Call `moi --help` for
    documentation. Run all `moi` commands from the **project root** — the folder that contains `.moi/`,
    never from inside `.moi/` itself. You don't pass paths; moi resolves the workspace from where it's run.
    
    Use the task-specific sections below for workflow guidance. The CLI will grow over time, so run
    `moi --help` to discover commands and `moi <command> --help` before using an unfamiliar command or option.
    
    - **Develop applets:** `moi check`, `moi bundle`, and `moi refresh`.
    - **Call actions:** `moi call-server-fn`.
    - **Debug applets:** `moi debug logs` (see Debugging applets).
    - **Navigate the workspace:** `moi tabs` and `moi tabs focus` (see Driving the workspace).
    - **Customize the workspace:** `moi theme` and `moi config` (see Customizing workspace appearance).
    - **Use workspace env:** `moi env` and `moi env exec` (see Environment & secrets).
    - **Maintain workspace guidance:** `moi skill` (see Keeping this skill current).
    
    ## Customizing workspace appearance
    
    `moi theme` shows the current font, color, radius, and agent appearance plus the available keys.
    Set one or several dimensions in a single command:
    
    ```sh
    moi theme --font=<key> --color=<key> --radius=<key> --agent=<key>
    ```
    
    Omit dimensions you do not want to change. Inspect with `moi theme` first instead of guessing keys;
    use `moi theme --help` if its options change.
    
    ## Critical constraints when interacting with moi
    
    - Never read or modify files outside the `.moi` directory, unless the user explicitly asks. If you
      do need it -> ask for permission.
    - Do **not** start, stop, or inspect the Workspace web server — it is managed externally.
    - Do not run Git commands while building or verifying applets unless the user asks for Git work.
    
    ## Developing Applets
    
    Every applet — a **Widget** or a **View** — is a default-exported React component in
    `.moi/<type>/<name>.tsx`, optionally paired with a `<name>.server.ts`. `moi bundle` compiles each
    into a live module and reloads it in the browser. Run it after source edits. Read
    `references/DESIGN.md` first.
    Write normal React + Tailwind — below is only what's **moi-specific**.
    
    ### Anatomy
    
    ```tsx
    // .moi/widgets/hello.tsx
    import { useEffect, useState } from 'react'
    import { getGreeting } from './hello.server' // optional server fn — see below
    
    // Optional config — fields are per type (see Widgets / Views below). requiredEnv is shared.
    export const config = { requiredEnv: ['API_KEY'] }
    
    export default function Hello() {
      const [msg, setMsg] = useState('')
      useEffect(() => {
        getGreeting().then(setMsg) // call server fns like any async function
      }, [])
      return <div className="h-full w-full p-4">{msg}</div>
    }
    ```
    
    Imports resolve relatively (same folder, or elsewhere under `.moi/` — e.g. `../lib/format`) or
    from `.moi/package.json` deps — no `@/` aliases. Files starting with `_` (e.g. `_utils.tsx`) in
    `widgets/` and `views/` are never applet entry points — put code shared between applets there.
    `moi bundle` tracks these local imports: editing a shared module rebuilds every applet using it.
    
    ### Applet styling
    
    - Use Tailwind for static styling. Do not add custom CSS, `@apply`, or static `style={{}}` values.
    - Use `style={{}}` only for computed data such as chart geometry, progress, per-item delays, or
      per-frame transforms.
    - Use icons from `@tabler/icons-react`. Do not add raw SVG icons or another icon pack. When the
      project provides `.agents/rules/icons.md`, follow its size and stroke policy.
    - Applets cannot import the host project's `cn`. If `.moi/ui/utils.ts` exists, import `cn` from
      `../ui/utils`; otherwise use a local `cx()` when classes are conditional. Never build class
      names with template-literal ternaries.
    
    ```tsx
    function cx(...classes: (string | false | undefined | null)[]) {
      return classes.filter(Boolean).join(' ')
    }
    ```
    
    ### Standard UI components
    
    Use bundled components for standard controls. Read the
    [shared usage rules](references/UI-COMPONENTS.md) before composing them.
    
    - `moi ui-components` lists available components and recipes with installed state.
    - `moi ui-components docs <name…>` prints bundled usage docs. Read them before using an
      unfamiliar component or when its API may have changed.
    - `moi ui-components add <name…> --install` copies source into `.moi/ui/` and installs npm
      dependencies. Pass all needed names in one call.
    - Import relatively, e.g. `import { Button } from '../ui/button'`, then run `moi bundle`
      after editing applets.
    
    Reuse familiar installed components without rereading their catalog entries, docs, or source.
    Inspect source only for local customizations, doc conflicts, or concrete build issues.
    
    ### Server functions — `<name>.server.ts`
    
    Export named `async function`s (only — no `const`, sync, or class) and call them from the component
    like ordinary async functions; arguments and return values are auto-serialized (`Date`, `Map`,
    `Set`, … work). They run on the Bun server with `process.env` and full filesystem access, at
    `cwd = <workspace root>` (the parent of `.moi/`, where you operate) — so workspace files are plain
    relative paths:
    
    ```ts
    // hello.server.ts — read files, call APIs, query DBs…
    export async function getGreeting(): Promise<string> {
      return (await Bun.file('./notes.md').text()).split('\n')[0]
    }
    ```
    
    The component fetches on mount; after you change underlying data a server fn reads, run
    `moi refresh` to re-pull it without a rebuild.
    
    It's plain Bun — every Bun API is available with no setup: `bun:sqlite`, `Bun.redis`, `Bun.s3`,
    `Bun.file`, `fetch`, …
    
    ### Workspace files & assets
    
    - **Bundled asset** — `import logo from './logo.png'` resolves to a URL at build time (images &
      fonts: `png jpg gif svg webp avif ico woff woff2 ttf otf`). For small art shipped beside the
      `.tsx`.
    - **Workspace file** — stream a file from the workspace via `fileUrl` from the **`moi`** package:
    
      ```tsx
      import { fileUrl } from 'moi'
      ;<video src={fileUrl('clips/intro.mp4')} controls />
      ```
    
      `fileUrl(path)` maps a **workspace-root-relative** path to a streaming URL (HTTP range — media
      seeks, nothing is base64-inlined). Media/asset extensions only; `.env`, source, JSON and dotfiles
      are rejected. The path is plain data, so a `.server.ts` can return it and the component renders
      `fileUrl(clip.file)`.
    
    Rule of thumb: small own art → `import`; structured data → `.server.ts` returns it; large/streamable
    media → `.server.ts` returns the **path**, render with `fileUrl()`.
    
    ### Driving the workspace — `focusTab` & `sendChatMessage`
    
    An applet can move the user to another tab and talk to you, through two functions from the **`moi`**
    package.
    
    ```tsx
    import { focusTab, sendChatMessage } from 'moi'
    
    // A widget row drilling into a view, and a button that asks you to do something.
    focusTab('view:orders', { order: 'o-1024' })
    sendChatMessage('Chase order o-1024', { order: 'o-1024', carrier: 'dhl' })
    ```
    
    - `focusTab(tab, params?)` switches the workspace to a tab. Tab ids are `overview`, `agent`,
      `scratchpad`, and `view:<id>` — run `moi tabs` for the real list. `params` arrive as the target
      view's `params` prop.
    - `sendChatMessage(message, context?)` sends `message` to the active chat as if the user typed it.
      `context` is structured data you see and the user does not. Call it from event handlers, never
      during render. Context can contain additional instructions not visible to the user, describing how
      the task should be done.
    - `params` and `context` accept JSON serializable values only.
    
    #### Params: the type is the contract
    
    A view with addressable state declares a local `Params` type in its own file. Every field is
    optional and carries a comment, because the view must render sensibly with `{}` — a fresh mount, a
    plain tab-bar click, or a new browser tab all deliver nothing.
    
    ```tsx
    // .moi/views/orders.tsx
    // The view's addressable state — what `focusTab('view:orders', …)` can set.
    type Params = {
      // Order id to open in the detail pane; omit to show the list.
      order?: string
    }
    
    export default function Orders({ params = {} }: { params?: Params }) {
      // Values arrive from navigation state, so narrow before trusting them.
      const openOrder = typeof params.order === 'string' ? params.order : null
      …
    }
    ```
    
    **Applets never import from each other, not even types.** Before wiring a `focusTab` call, read the
    target view's source, mirror the shape you find there, and note where you read it. That file is the
    contract; the type is documentation, not a shared module. Widgets are never navigation targets —
    their `params` is always `{}`.
    
    ### Environment & secrets
    
    Each workspace has an effective env: keys from the project's `.env` / `.env.local` (when
    inheritance is enabled in settings) plus **custom secrets** the user manages in the workspace env
    settings. moi injects this env into:
    
    - applet server functions — read it as `process.env` inside `.server.ts`
    - any command run via `moi env exec -- <cmd>`
    - your own shell (Bash tool) — but only in some harnesses (e.g. Claude Code). Don't assume it:
      verify the key is visible first, or just use `moi env exec`, which works everywhere.
    
    Rules:
    - **Check before you assume.** When a task needs a key or token — an API pull, a widget calling a
      service — run `moi env` first. It lists key names with their source (`.env` / custom) and flags
      declared `requiredEnv` keys that are missing. Values are never shown.
    - **Key present** → say which key you'll use and where it's from ("using `NOTION_TOKEN` from
      `.env`") and proceed. To run a script or one-off command with the workspace env, use
      `moi env exec -- bun script.ts` — it also picks up values changed after your session started.
    - **Key missing** → never invent or hardcode a value, and don't edit `.env` yourself. Tell the user
      the exact key name to add in the workspace env settings. Still build and wire the applet: declare
      the key in `config.requiredEnv` and handle its absence, so it works the moment the user sets it.
      If the user pastes a value in chat, store it with `moi env set KEY=value`
      (`moi env unset KEY` removes it).
    - **Never print secret values** — not in chat, not in logs. Refer to keys by name only.
    
    ### Running commands with workspace env
    
    Use `moi env exec -- <command> [args...]` for an arbitrary script or tool that needs the effective
    workspace env. The `--` separator is required, and the env is resolved again on every invocation,
    so this also sees values added or changed during the chat.
    
    ```sh
    moi env exec -- bun script.ts
    moi env exec -- bun test integration.test.ts
    ```
    
    Use `moi call-server-fn` instead when exercising an exported applet server function through its
    real runtime path.
    
    `process.env` is readable **only** inside `.server.ts` (the `.tsx` runs in the browser) — keep API
    keys there. Either source may be absent, so always handle a missing key. List expected keys in
    `config.requiredEnv` — advisory only (it surfaces a hint in the UI and `moi env`; it's never
    enforced).
    
    ```ts
    // forecast.server.ts
    export async function getForecast(city: string) {
      const key = process.env.WEATHER_API_KEY // always current — env changes respawn the worker
      if (!key) return { error: 'Add WEATHER_API_KEY to your env' }
      const res = await fetch(`https://api.example.com/forecast?city=${encodeURIComponent(city)}`, {
        headers: { Authorization: `Bearer ${key}` }
      })
      if (!res.ok) return { error: `Weather API error ${res.status}` }
      return { data: await res.json() }
    }
    ```
    
    ## Verifying applets
    
    Use `moi check --only views/<id>` or `moi check --only widgets/<id>` for the applet you changed. Use
    the kind alone only when the work spans several applets. The command owns the supported applet
    TypeScript setup and can gain more checks later. Do not create `.moi/tsconfig.json`, invoke `tsc`
    directly, or retry with ad hoc compiler flags and missing type packages.
    
    For a frontend rebuild, use this stopping point:
    
    1. Run `moi check --only <kind>/<id>` once after source edits, then
       `moi bundle --only <kind>/<id>`.
    2. In the browser, exercise the changed interaction, such as reveal and rating, and check one
       narrow layout.
    3. Inspect `moi debug logs --only <kind>/<id> --json` for runtime errors.
    
    Stop when these checks pass. Expand verification only when a check fails or the changed behavior
    needs another focused check. Do not search for repo tests by default. Run an existing applet test or
    `moi call-server-fn` only when the change touches the behavior it covers. If native-app inspection is
    unavailable, keep verification in the browser instead of retrying the unsupported tool.
    
    After the final successful checks, always make tab focus the final workspace action:
    
    - After building or editing a widget, run `moi tabs focus widgets`.
    - After building or editing a view, run `moi tabs focus view:<view-id>`, using its file name or claimed
      builder id.
    
    The focused applet is the handoff. Keep the final reply brief and user-facing. Do not include file
    or storage links, file paths, or bundle, test, and runtime-log summaries.
    
    ## Widgets
    
    Live cards on the Overview grid — many visible at once. `config` sets the grid footprint:
    
    ```ts
    export const config = {
      colSpan: 2, // columns the card spans — 1–4
      rowSpan: 1, // rows the card spans — 1–4
      requiredEnv: ['API_KEY'] // optional env-key hints (advisory; see Environment & secrets)
    } as const
    ```
    
    Render **content only**: a plain `h-full w-full` region with no card chrome (`rounded-*`,
    `shadow-*`, or outer `border`) — the dashboard owns the shell, spacing, and elevation. It does not
    own the fill, so the widget must set its own opaque background.
    Changing `colSpan`/`rowSpan` needs `moi bundle --force --only widgets/<id>`. See
    `references/DESIGN.md`.
    
    ## Debugging applets
    
    `moi bundle` only proves an applet compiles — it can still fail to load in the browser, crash on
    render, or throw in its server functions. For frontend rebuilds, check runtime logs as required
    above. For other work, use these channels when smoke-testing new behavior or investigating a
    problem:
    
    - `moi call-server-fn widgets/hello/getGreeting` /
      `moi call-server-fn views/crm/searchUsers '["ann", 10]'` — run one `.server.ts` function
      directly (args are one JSON array). Each invocation runs in a fresh, isolated one-shot process
      with the same env, module loading, and timeout as the browser's calls, so a pass means the real
      path works — handy for trying a function without touching the UI. Server functions only; for
      arbitrary scripts use `moi env exec`.
    - `moi debug logs` — the applet errors the workspace has seen since each applet's last good
      build: browser-side load failures and render crashes, plus server-function (rpc) errors. The
      user's tab reports these automatically, so when the user says something is broken, what
      happened is usually already on record — a good first place to look. Entries clear when their
      applet next builds successfully. (`moi debug` is an experimental command group — expect its
      output and flags to evolve; use `--json` when you need to parse it.)
    
    `moi bundle`'s footer also mentions when runtime errors are on record, so standing breakage
    surfaces on its own.
    
    ## Views
    
    Full-screen apps, one per nav tab — the user switches tabs. A view has no router of its own, but it
    can be addressed: see Driving the workspace for `focusTab` and the `params` prop.
    
    ### View builder requests
    
    When the message's hidden `<moi-context>` envelope is marked `View builder request`, this chat is
    linked to a pending view tab. Before reading files, planning, or writing code, infer a short stable
    id, a clear sentence-case title, and a relevant icon from the requirements. Capitalize only the
    first word of the title. Your first action must claim them:
    
    ```sh
    moi builder set <view-id> --builder <builder-id> --kind view --title "<title>" --icon <icon-id>
    ```
    
    Choose the icon id from the available view icons in the hidden context. The id must use lowercase
    letters, numbers, `_`, or `-`. The first call locks the id; running the same command again may update
    its title and icon. After claiming, write `.moi/views/<view-id>.tsx`, use the same icon id in its
    config, and build it with `moi bundle --only views/<view-id>`. The tab uses the claimed title and icon
    while you work and changes into the built view after a successful bundle. (Bundling marks the view
    ready; the build state is otherwise server-managed, so you never set it to done by hand.)
    
    ```ts
    export const config = {
      title: 'Customer overview', // sentence-case nav label — defaults to the file name
      icon: 'user', // icon id from the view-builder request
      requiredEnv: ['CRM_API_KEY'] // optional env-key hints (advisory; see Environment & secrets)
    } as const
    ```
    
    The inverse of a widget: a view **owns its whole page** — its own `h-full w-full` layout, scrolling
    (`overflow-auto`), padding, and chrome. Build it to read like an app screen. See `references/DESIGN.md`.
    
    ## Keeping this skill current
    
    This skill is installed with moi (via the CLI or the UI) and can fall behind when the moi CLI updates.
    
    - **You'll know** — `moi` commands warn you when this skill is behind.
    - **To update** — if the user asks for current or updated guidance, run `moi skill update` before
      applet work. Otherwise, update at the end of the task.
    - **Reload selectively** — the command reports which skills changed. Re-read this `SKILL.md` only
      when it reports that `moi-workspace` changed. If it did not change, keep using the copy already
      in context.
    - **Then** — if you updated, mention it.
    
    <!-- moi skill version marker — read by `moi skill` to detect drift; do not edit by hand -->
    <moi-skill version="0.18.1" />
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related