agentsclimarketplace

Moi workspace

Skill molefrog/moi/workspace/.claude/skills/moi-workspace

πŸ’Ύ MOI: agent-agnostic generative UI workspace

Install
npx -y skills add molefrog/moi --skill moi-workspace

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.

What its author says it does

Copied from the file, not written here

The moi workspace β€” the web UI the user chats from, extended with agent-authored applets (widgets, views) plus theme & config. Read this FIRST in two cases. (a) A message carries a hidden <moi-context>…</moi-context> envelope: it was fired from a moi workspace, so you are running inside one even if nothing else says so. (b) The user uses moi vocab β€” workspace, applet, widget, view, scratchpad, dashboard, or a `moi` command β€” or asks to build, edit, customize, or theme the workspace UI or its layout.

SKILL.md

16.9 KB, as published. Nobody here has run it

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:

  • "Widgets" - small reusable full-stack components displayed on the widgets page (dashboard). For overview, quick info, status or quick actions.
  • "Scratchpad" - a shared low-fi canvas for prototyping, working on ideas together, visualising concepts. Read SCRATCHPAD.md before building on or modifying it.
  • "Views" - full-stack embedded apps for bigger work, consume more space, live in their own tab.

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 in the dashboard 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
      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.

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.

  • moi bundle β€” compile changed applets
  • moi bundle --force β€” rebuild all applets (use after changing config)
  • moi refresh β€” re-fetch widget and view data without rebuilding (use after you mutated data the applets read β€” DB rows, files, external API records β€” so the displayed values catch up); --only widgets / --only views narrows the refresh to one kind
  • moi call-server-fn <module>/<fn> '[args]' β€” invoke a .server.ts function directly (smoke test)
  • moi debug logs β€” applet runtime errors on record (experimental)
  • moi theme --font=<key> β€” change font theme (omit --font to list options)
  • moi theme --color=<key> β€” change color preset (omit --color to list options)
  • moi config β€” set the workspace name & icon (moi config --help for usage)
  • moi env β€” list available env keys and where they come from (never values); moi env exec -- <cmd> runs a command with the workspace env (see Environment & secrets)
  • moi skill β€” show installed vs bundled skill versions; moi skill update to refresh

For more options, commands, use moi help.

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.

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 the browser loads (edits hot-reload). Read DESIGN.md / VIEW-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 from the same folder or .moi/package.json deps only β€” no @/ aliases.

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().

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.

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() }
}

Widgets

Live cards on the dashboard 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-*, outer border, or a card-like background) β€” the dashboard owns the shell, spacing, and elevation. Changing colSpan/rowSpan needs moi bundle --force. See DESIGN.md.

Typical loop: check/bun install deps β†’ write .moi/widgets/<name>.tsx β†’ moi bundle β†’ it appears on the dashboard β†’ change it and re-bundle, or moi refresh after mutating data, or any other moi command as needed. Views work the same way (.moi/views/<name>.tsx, appears as a nav tab).

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. Two feedback channels exist for what happens after the build; reach for them when they'd help (smoke-testing something new, 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; there is no routing inside a view.

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 finish with moi bundle --only views. 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 VIEW-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 β€” run moi skill update. Never mid-task: finish first, or do it at the end.
  • Re-read after updating β€” moi skill update rewrites SKILL.md and its companion docs (DESIGN.md, VIEW-DESIGN.md, SCRATCHPAD.md) on disk, so the copy already in your context is stale. Re-read this SKILL.md before you rely on it again β€” don't act on the old version.
  • 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.7.1" />

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.