A Möbius catalog mini-app. Install it from the in-app App Store.
Skills is a read-only catalog for the agent's skill files — the SKILL-style
markdown guides under /data/shared/skills/ that shape what the Möbius agent
can do. It enumerates the shared skills folder, parses each file's title and
one-line description, searches locally, and renders the selected skill as
sanitized markdown. Creating and editing a skill is routed to the agent (see
below), never written from the app.
Installed system apps that add always-on instructions are listed below the editable skills so their effect is visible. Those instructions apply for as long as the app stays installed; start a new chat after installing or uninstalling one to be sure the agent is working from the latest set.
| File | Role |
|---|---|
index.jsx |
Default-export React component: the list, search, detail view, and all UI/state. |
domain.js |
Dependency-free core for parsing, link classification, errors, and supplemental-app load coordination. Network access is injected, so it is unit-testable without React or the DOM. |
test/domain.test.js |
Regression tests for domain.js (run with npm test → node --test). |
mobius.json |
App manifest (id, permissions, runtime deps, offline contract). |
icon.png |
App icon. |
Skill files may be plain markdown or include a small YAML frontmatter block.
parseSkill() strips frontmatter from the rendered detail and reads:
- Title — the first
# Headingoutside any fenced code block. If a file has no#heading, the title falls back tonamefrom frontmatter, then to a Title-Cased version of the slug (filename without.md). - Description — the first non-empty paragraph after the title, up to ~240
chars, stopping at the next heading,
---, or a fenced code block. If the body has no paragraph,descriptionfrom frontmatter is used as a fallback.
Fenced code blocks ( ``` or ~~~) are tracked so a `# comment` or a
fence marker inside a code block is never mistaken for the title or the
description.
The app reads shared storage with its scoped app token — it can read shared files but cannot write them, so all mutations route through the agent:
GET /api/storage/shared-list/skills/— enumerate the skills folder (immediate children). The app keeps onlytype: "file",*.md, non-dotfile entries. It never probes guessed paths.GET /api/storage/shared/skills/<name>— fetch one skill's markdown.
A per-file fetch that fails (non-OK or thrown) keeps the row but marks it
Unavailable rather than rendering a blank skill, and emits an error signal
(source: "skill_load") so a corrupt or permission-broken file isn't mistaken
for intentionally empty content.
A failed Refresh keeps the last-known-good list on screen (it does not wipe it) and surfaces a small "Couldn't refresh" pill; retry from the header refresh button. The full error screen is reserved for the very first load.
Skills are shared, owner-authored context. Because a mini-app can't write shared
storage, the Edit button (detail view) and the empty-state Ask the agent
button post a moebius:new-chat message to the shell with a pre-filled draft,
opening an agent chat rather than saving in-app.
The whole app lives in one iframe, so an unhandled link click would navigate the
iframe away and brick the view. classifyLink() + a delegated click handler on
the rendered markdown route every link safely:
- Same-folder
.mdlink (e.g.[shapes](app-component-shapes.md)or./name.md) → opened in-app via the detail view, no iframe navigation. If the target isn't in the loaded set, the tap is swallowed (app stays up) and anerrorsignal fires. http:/https:link → opened in a new browser context viawindow.open(url, '_blank', 'noopener,noreferrer').- In-page
#fragment→ left to default (harmless, doesn't replace the doc). - Any other protocol or sub-path → navigation is blocked and an
errorsignal fires; the app stays mounted.
Emitted via window.mobius.signal(...), all flat primitives:
app_ready { item_count }— once, after the first successful load (gated so manual refreshes don't inflate open counts).item_opened { type: "skill", slug }— a skill detail was opened.edit_requested { type: "skill", slug }— the owner tapped Edit on a skill.item_created { type: "skill" }— the empty-state create CTA was launched (a request to create; it does not claim a file now exists).error { message, source, status? }—sourceisskill_load(per-file fetch),markdown_render(parse/sanitize),skill_link(blocked/unknown link), orload/refresh(list load).
This is an online-only viewer (offline_capable: false): it reads live shared
storage and has nothing useful to cache for a cold offline open. The manifest's
top-level offline object —
{ "reads": false, "writes": "none", "execution": "none" } — is the honest
offline contract. The installer reads it and stores it as the app's
offline_contract (exposed via the apps API), so it is kept deliberately, not
dead metadata. When offline, the app shows a plain "Offline" pill; the list
stays visible if it was already loaded.
Markdown is rendered with DOMPurify.sanitize(marked.parse(...)) — never raw
marked output. marked and dompurify are declared as esm_deps in
mobius.json.
npm test # node --test over test/*.test.js — no install needed (Node 18+)The tests cover domain.js only (the pure core); the React UI has no runtime
dependencies to mock. To compile-smoke the entry:
esbuild index.jsx --bundle --packages=external --format=esm --loader:.js=jsx --outfile=/dev/null