docs: plan a pixel-art desktop mascot ("Cluck")
Design-only (no code yet): an MBSE subsystem model for a chicken mascot that roams the desktop shell, is draggable, opens a Sims-style nested radial menu, and has a tamagotchi lifecycle (egg -> chick -> adult) that reacts to real app activity (chat streaming, knowledge-graph writes, signals). Everything (animations, autonomous behaviors, menu actions, environment reactions) is scoped as a data-driven registry for easy extension. - docs/mascot/README.md: subsystem Model conforming to docs/mbse's Holt-based Framework — mission/boundary, requirements, structural view (module registry map), behavioral view (behavior FSM + lifecycle state machines + a stimulus sequence diagram), interfaces view (which web stores it observes, read-only), extension guide, verification view. - plans/2026-07-20-desktop-mascot.md: the concrete file-by-file implementation plan for web/src/lib/mascot/ derived from the model, with an ordered build sequence and a manual browser verification checklist. - Indexed both in docs/index.md and plans/index.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
335
docs/mascot/README.md
Normal file
335
docs/mascot/README.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Oikos — Desktop Mascot Subsystem Model
|
||||
|
||||
> Companion to [the platform Model](../mbse/README.md) and
|
||||
> [the Framework](../mbse/framework.md). This document is a **subsystem
|
||||
> Model** in Holt's sense — it conforms to the same Framework (Ontology +
|
||||
> Viewpoints, Markdown + Mermaid Notation) rather than restating it, scoped
|
||||
> to a single not-yet-built subsystem of the `web` component: the desktop
|
||||
> mascot ("Cluck"), a pixel-art chicken that lives on the desktop shell.
|
||||
> Where the platform-wide Views in [../mbse/README.md](../mbse/README.md)
|
||||
> and the component View for `web/src` in
|
||||
> [../mbse/components.md](../mbse/components.md#5-web-control-room) speak
|
||||
> at the level of "the SPA," this document goes one layer deeper into one
|
||||
> feature of it — the same relationship [components.md](../mbse/components.md)
|
||||
> has to [README.md](../mbse/README.md), applied recursively.
|
||||
|
||||
**Status of this Model:** the subsystem it describes does not exist in
|
||||
code yet. Every View below is marked **Planned**, not **Verified** —
|
||||
compare to [../mbse/README.md](../mbse/README.md)'s confidence grading,
|
||||
which this document borrows. The corresponding implementation plan is
|
||||
[plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md).
|
||||
|
||||
## Views in this model
|
||||
|
||||
| # | View | Concern it addresses |
|
||||
|---|---|---|
|
||||
| [1](#1-mission--system-context) | Mission & System Context | Why a mascot, and what is it never allowed to do? |
|
||||
| [2](#2-requirements) | Requirements | What must it do, traced from the original request? |
|
||||
| [3](#3-structural-view) | Structural View | What modules make it up, and which are the extension points? |
|
||||
| [4](#4-behavioral-view) | Behavioral View | How does it move, live, and react, moment to moment? |
|
||||
| [5](#5-interfaces-view) | Interfaces View | What does it read from the rest of the system, and how does it persist itself? |
|
||||
| [6](#6-extension-guide) | Extension Guide | How does a future engineer add an animation, behavior, menu action, or reaction? |
|
||||
| [7](#7-verification-view) | Verification View | How will we know it works, once built? |
|
||||
|
||||
## 1. Mission & System Context
|
||||
|
||||
**Stakeholders:** the operator (delight, ambient awareness of system
|
||||
state without opening a window); future engineers extending the mascot's
|
||||
behaviors/reactions/menu.
|
||||
|
||||
**Mission:** give the desktop shell a persistent, living presence that
|
||||
makes background system activity legible at a glance — a chat streaming,
|
||||
a knowledge-graph write, a critical signal — without requiring a window to
|
||||
be open, while doubling as a lightweight tamagotchi for its own sake
|
||||
(delight is a legitimate requirement here, not a side effect).
|
||||
|
||||
**Boundary — what the mascot is, and is not:**
|
||||
|
||||
- It is a **purely client-side, read-only observer**. It subscribes to
|
||||
existing `web` stores (chat, activity, events, dashboard summary) the
|
||||
same way any other UI component does.
|
||||
- It **never calls a mutating API endpoint** and is not a new actuation
|
||||
path — it has no relationship to the `run` gate, `Execution`, or
|
||||
`Approval` entities described in [the platform Ontology](../mbse/ontology.md).
|
||||
Its only "mutation" is its own tamagotchi state, stored client-side.
|
||||
- It is scoped entirely inside the `web` component
|
||||
([../mbse/components.md §5](../mbse/components.md#5-web-control-room));
|
||||
it introduces no new backend surface, no new MCP tool, no new REST route.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph SURFACE["Desktop shell surface (Desktop.svelte)"]
|
||||
ICONS["Icon layer\nz-0"]
|
||||
LAUNCH["Task launcher\nz-10"]
|
||||
WIN["WindowLayer\nz-40"]
|
||||
MASCOT["MascotLayer\nz-45\n(this subsystem)"]
|
||||
MENU["Desktop context menu\nz-50"]
|
||||
end
|
||||
|
||||
MASCOT -->|subscribes, read-only| EVENTS["stores/events.ts\nliveEvents (SSE)"]
|
||||
MASCOT -->|subscribes, read-only| CHAT["stores/chat.ts\nstreaming"]
|
||||
MASCOT -->|subscribes, read-only| ACTIVITY["stores/activity.ts\nactivityLog"]
|
||||
MASCOT -->|subscribes, read-only| CONTEXT["stores/context.ts\nsummary"]
|
||||
MASCOT -->|reads/writes| LS["localStorage\noikos-mascot"]
|
||||
|
||||
style MASCOT fill:#fff3e0,stroke:#e65100
|
||||
```
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
Traced from the original feature request. All **Planned**.
|
||||
|
||||
| ID | Statement | Source | Status |
|
||||
|---|---|---|---|
|
||||
| MASC-1 | The mascot SHALL render as pixel-art, drawn from code (string pixel-grids + palette), not binary sprite assets | User request | Planned |
|
||||
| MASC-2 | The mascot SHALL roam the desktop surface autonomously, walking along the ground (surface bottom, above the taskbar) under gravity | User request + design decision | Planned |
|
||||
| MASC-3 | The mascot SHALL be draggable with the mouse; releasing it mid-air SHALL trigger a flutter-fall back to the ground | User request + design decision | Planned |
|
||||
| MASC-4 | Right-clicking the mascot SHALL open a round (Sims-style) interaction menu supporting nested submenus | User request | Planned |
|
||||
| MASC-5 | The mascot SHALL have a tamagotchi lifecycle: egg → chick → adult, with a user-assignable name | User request | Planned |
|
||||
| MASC-6 | The mascot's stage, name, and stats SHALL persist across reloads | User request (implied by "tamagotchi") | Planned |
|
||||
| MASC-7 | The mascot SHALL have idle states (autonomous behavior when untouched) and interactive states (drag, click, menu) | User request | Planned |
|
||||
| MASC-8 | The mascot SHALL react visibly to real application activity: chat streaming, knowledge-graph writes, critical signals | User request ("aware of its environment... feels alive and connected") | Planned |
|
||||
| MASC-9 | Animations, behaviors, menu actions, and reactions SHALL each be defined in a single data-driven registry, so a new one can be added without touching the engine code | User request ("easily expansible") | Planned |
|
||||
| MASC-10 (NFR) | The mascot's game loop SHALL run at ~30fps via `setTimeout`, not `requestAnimationFrame`, matching the repo's existing [`GraphBackground.svelte`](../../web/src/lib/components/GraphBackground.svelte) convention (rAF suspends in some hidden-tab embeddings) | Codebase convention | Planned |
|
||||
| MASC-11 (NFR) | The mascot SHALL never write to the API; all mutation is local (localStorage) | Design decision, this document §1 | Planned |
|
||||
| MASC-12 (NFR) | Persistence writes SHALL be debounced (~300ms), never per animation frame | Codebase convention ([`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) | Planned |
|
||||
|
||||
## 3. Structural View
|
||||
|
||||
**Stakeholders:** an engineer implementing or extending the mascot.
|
||||
**Why this View earns its place:** MASC-9 (extensibility) is only real if
|
||||
the module boundaries actually separate data (registries) from engine
|
||||
code; this View is the check that they do.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class types_ts {
|
||||
<<module>>
|
||||
PixelGrid
|
||||
AnimName
|
||||
MascotStage
|
||||
BehaviorId
|
||||
Stimulus
|
||||
RadialAction
|
||||
}
|
||||
class palette_ts {
|
||||
<<module, registry>>
|
||||
PALETTE: char to CSS color
|
||||
}
|
||||
class sprites_ts {
|
||||
<<module, registry>>
|
||||
SPRITES: Stage to AnimName to AnimDef
|
||||
resolveAnim(stage, name)
|
||||
}
|
||||
class render_ts {
|
||||
<<module, stateless>>
|
||||
drawFrame(ctx, grid, palette, flip)
|
||||
}
|
||||
class state_svelte_ts {
|
||||
<<module, runes>>
|
||||
MascotModel state
|
||||
grantXp() feed() pet() setName()
|
||||
tickLifecycle() advanceStageIfReady()
|
||||
persist (debounced, oikos-mascot)
|
||||
}
|
||||
class behavior_ts {
|
||||
<<module, registry>>
|
||||
BEHAVIORS: BehaviorId to BehaviorDef
|
||||
stepMascot(rt, model, now, dt)
|
||||
}
|
||||
class stimuli_ts {
|
||||
<<module, registry>>
|
||||
REACTIONS: id to ReactionDef
|
||||
attachStimuli(emit)
|
||||
}
|
||||
class actions_ts {
|
||||
<<module, registry>>
|
||||
MASCOT_ACTIONS: RadialAction tree
|
||||
registerMascotAction()
|
||||
}
|
||||
class Mascot_svelte {
|
||||
<<component>>
|
||||
canvas render loop 30fps
|
||||
pointer drag/click/contextmenu
|
||||
}
|
||||
class MascotLayer_svelte {
|
||||
<<component>>
|
||||
z-45 absolute overlay
|
||||
hosts Mascot + RadialMenu + bubble
|
||||
}
|
||||
class RadialMenu_svelte {
|
||||
<<component>>
|
||||
z-60 fixed, nested rings
|
||||
}
|
||||
class NameDialog_svelte {
|
||||
<<component>>
|
||||
}
|
||||
|
||||
sprites_ts --> palette_ts : indexes
|
||||
sprites_ts --> types_ts : uses
|
||||
Mascot_svelte --> render_ts : draws frames
|
||||
Mascot_svelte --> sprites_ts : resolves anim
|
||||
Mascot_svelte --> behavior_ts : steps FSM
|
||||
Mascot_svelte --> state_svelte_ts : reads/mutates model
|
||||
MascotLayer_svelte --> Mascot_svelte : hosts
|
||||
MascotLayer_svelte --> RadialMenu_svelte : hosts, on contextmenu
|
||||
MascotLayer_svelte --> stimuli_ts : attaches on mount
|
||||
MascotLayer_svelte --> NameDialog_svelte : hosts, on hatch/rename
|
||||
RadialMenu_svelte --> actions_ts : renders tree
|
||||
stimuli_ts --> behavior_ts : forceBehavior(react)
|
||||
```
|
||||
|
||||
**The four extension registries** (MASC-9's concrete answer — see also
|
||||
[§6 Extension Guide](#6-extension-guide)): `SPRITES` (animations),
|
||||
`BEHAVIORS` (autonomous states), `MASCOT_ACTIONS` (radial menu tree),
|
||||
`REACTIONS` (environment stimuli). Each is plain data; the engine
|
||||
(`behavior.ts`'s `stepMascot`, `Mascot.svelte`'s loop, `RadialMenu.svelte`'s
|
||||
renderer) is generic over whatever the registry currently contains.
|
||||
|
||||
**Mount point:** two lines in
|
||||
[`Desktop.svelte`](../../web/src/lib/components/desktop-shell/Desktop.svelte) —
|
||||
`<MascotLayer />` rendered inside the surface `<div>` (the `relative
|
||||
min-h-0 flex-1 overflow-hidden` element), after `<WindowLayer />`, so its
|
||||
`absolute inset-0` shares the surface's coordinate space and its ground
|
||||
line is exactly the surface's bottom edge (= the taskbar's top edge).
|
||||
|
||||
## 4. Behavioral View
|
||||
|
||||
**Stakeholders:** an engineer reasoning about "what does the mascot do
|
||||
right now, and why." **Why this View earns its place:** a mascot with an
|
||||
implicit, ad-hoc state machine is unmaintainable the moment a second
|
||||
behavior or reaction is added; this View is the state machine made
|
||||
explicit before any of it is coded.
|
||||
|
||||
### 4.1 Behavior FSM (moment-to-moment autonomy)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> egg
|
||||
egg --> chick : hatchProgress reaches 1\n(advanceStageIfReady)
|
||||
|
||||
state chick_and_adult_behaviors {
|
||||
[*] --> idle
|
||||
idle --> wander : weighted random pick\non behaviorUntil expiry
|
||||
wander --> idle
|
||||
idle --> peck : weighted random pick
|
||||
peck --> idle
|
||||
idle --> sleep : weighted random pick
|
||||
sleep --> idle
|
||||
wander --> falling : y below ground\n(off a dragged edge, etc.)
|
||||
idle --> dragged : pointerdown + move\npast 5px threshold
|
||||
wander --> dragged : pointerdown + move
|
||||
sleep --> dragged : pointerdown + move\n(interrupts sleep)
|
||||
dragged --> falling : pointerup, released mid-air
|
||||
falling --> land : y reaches ground
|
||||
land --> idle
|
||||
[*] --> react : stimulus dispatched\n(priority/cooldown gated)
|
||||
react --> idle : durationMs elapsed,\nreturns to prior-or-idle
|
||||
}
|
||||
|
||||
chick --> adult : xp reaches ADULT_XP\n(advanceStageIfReady)
|
||||
```
|
||||
|
||||
`dragged` always wins over any autonomous behavior; `sleep` is broken only
|
||||
by a reaction whose `ReactionDef.interruptsSleep` is true (§4.3) or by a
|
||||
drag. Weighted-random idle selection (`weight` field in `BehaviorDef`)
|
||||
picks the next autonomous behavior only when the current one's `next()`
|
||||
returns null past `behaviorUntil` — see
|
||||
[plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md)
|
||||
for the concrete weights.
|
||||
|
||||
### 4.2 Tamagotchi lifecycle (long-lived state)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> egg : first load,\ndefaultModel()
|
||||
egg --> chick : active time >= HATCH_MS (3min)\n+ NameDialog shown
|
||||
chick --> adult : xp >= ADULT_XP (200)
|
||||
adult --> [*]
|
||||
```
|
||||
|
||||
This is a separate state machine from §4.1: §4.1 governs frame-to-frame
|
||||
motion/animation, §4.2 governs the tamagotchi's slow-moving `MascotModel`
|
||||
(persisted, ticked ~1x/sec via `tickLifecycle`, not every frame).
|
||||
|
||||
### 4.3 Example sequence — an environment stimulus becomes a visible reaction
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SSE as stores/events.ts (SSE)
|
||||
participant Stim as stimuli.ts attachStimuli
|
||||
participant FSM as behavior.ts
|
||||
participant Mascot as Mascot.svelte (canvas)
|
||||
|
||||
SSE->>Stim: liveEvents updates,\nnew head event severity=critical
|
||||
Stim->>Stim: check REACTIONS['alarmed']\ncooldown + priority
|
||||
Stim->>FSM: forceBehavior(rt, 'react', {anim: 'react-alarm', durationMs})
|
||||
FSM->>FSM: interrupts current behavior\n(even sleep, interruptsSleep=true)
|
||||
FSM->>Mascot: rt.behavior = 'react', rt.anim = 'react-alarm'
|
||||
Mascot->>Mascot: next 30fps tick draws\nreact-alarm frame
|
||||
Note over FSM: after durationMs,\nnext() returns to idle
|
||||
```
|
||||
|
||||
## 5. Interfaces View
|
||||
|
||||
**Stakeholders:** an engineer wiring a new store into the mascot's
|
||||
awareness, or auditing what it depends on.
|
||||
|
||||
| Interface | Direction | Shape | Notes |
|
||||
|---|---|---|---|
|
||||
| [`stores/events.ts`](../../web/src/lib/stores/events.ts) `liveEvents` | consumed | `Writable<OikosEvent[]>`, newest-first, ref-counted via `subscribeEvents()` | `OikosEvent.type` families: `approval.*`, `signal.*`, `execution.*`, `health.changed`; `severity: 'info'\|'warning'\|'critical'` |
|
||||
| [`stores/chat.ts`](../../web/src/lib/stores/chat.ts) `streaming` | consumed | `Writable<boolean>` | false→true edge triggers the `thinking` reaction, held while true |
|
||||
| [`stores/activity.ts`](../../web/src/lib/stores/activity.ts) `activityLog` | consumed | derived `Readable<ActivityEntry[]>`, **recomputed wholesale** on every emission — not append-only | new entries with `type === 'knowledge'` detected by diffing entry `id`s between emissions, not by treating it as a stream |
|
||||
| [`stores/context.ts`](../../web/src/lib/stores/context.ts) `summary` | consumed | `Writable<DashboardSummary\|null>` | ambient state (open signal counts via `openSignalCount(summary)`) |
|
||||
| `localStorage['oikos-mascot']` | owned | `MascotModel` JSON, `{ version: 1, stage, name, hatchProgress, happiness, xp, hatchedAt, lastPos: {x}, lastSeen }` | debounced write (~300ms, mirrors [`stores/windows.ts`](../../web/src/lib/stores/windows.ts) wmkit persist) + `beforeunload` flush; `version` field reserved for a future `migrate()`; multi-tab is last-writer-wins (accepted, documented, not solved) |
|
||||
| [`Desktop.svelte`](../../web/src/lib/components/desktop-shell/Desktop.svelte) mount | owned | `<MascotLayer />`, 2-line insertion | see §3 |
|
||||
|
||||
No interface in this table is a write path to the Oikos API — consistent
|
||||
with §1's boundary statement (MASC-11).
|
||||
|
||||
## 6. Extension Guide
|
||||
|
||||
**Stakeholders:** a future engineer adding one new animation, behavior,
|
||||
menu action, or reaction — this is the Viewpoint 4's "why" made concrete
|
||||
as a recipe rather than prose (mirrors [../mbse/framework.md §7](../mbse/framework.md)'s
|
||||
Process Set treatment).
|
||||
|
||||
| To add a... | Touch only | Nothing else changes because |
|
||||
|---|---|---|
|
||||
| **Animation** | Add the name to the `AnimName` union in `types.ts`; add frames to `SPRITES[stage]` in `sprites.ts` | `resolveAnim()` and the renderer are generic over the registry |
|
||||
| **Behavior** | Add the id to `BehaviorId`; add one `BehaviorDef` entry to `BEHAVIORS` in `behavior.ts` | `stepMascot()` and the weighted-random idle selector consume `BEHAVIORS` generically |
|
||||
| **Radial menu action** | Add a `RadialAction` node to `MASCOT_ACTIONS` in `actions.ts` (or call `registerMascotAction()`), optionally nested under `children` | `RadialMenu.svelte` renders whatever tree it's given, including nesting depth |
|
||||
| **Environment reaction** | Add a `ReactionDef` to `REACTIONS` in `stimuli.ts`; wire one `store subscription -> predicate -> emit(reaction)` block inside `attachStimuli()` | priority/cooldown/interrupt dispatch logic in `attachStimuli()` is generic over `REACTIONS` |
|
||||
|
||||
## 7. Verification View
|
||||
|
||||
**Stakeholders:** whoever implements this subsystem and needs to know
|
||||
when it's actually done, not just compiled.
|
||||
|
||||
Manual browser checklist (no automated test harness planned for v1 — see
|
||||
[plans/2026-07-20-desktop-mascot.md](../../plans/2026-07-20-desktop-mascot.md)
|
||||
for the same list in implementation-order context):
|
||||
|
||||
- Egg renders grounded at the surface bottom, wiggles occasionally, survives
|
||||
a reload at the same x (confirm `oikos-mascot` is debounced — no writes
|
||||
fire from mere walking, only from discrete transitions).
|
||||
- Dragging the egg up and releasing triggers a flutter-fall with no
|
||||
tunneling below the taskbar; dragging past the surface edges clamps.
|
||||
- Forcing hatch (debug menu action) transitions to chick, opens the name
|
||||
dialog, and the name persists across reload.
|
||||
- Chick wanders and flips sprite at surface edges, pecks, sleeps
|
||||
autonomously; a plain click (no drag) triggers a pet/hop reaction.
|
||||
- Right-clicking the chicken opens the radial menu centered on it, without
|
||||
triggering the desktop's own right-click menu; a nested submenu (Feed)
|
||||
opens correctly; Escape pops one level then closes; an outside click
|
||||
closes it; the menu stays fully visible when the chicken is near a
|
||||
screen edge or corner.
|
||||
- With one or more windows open (including a maximized one), the chicken
|
||||
visibly walks above them without breaking window drag/resize/close.
|
||||
- Starting a chat and observing it stream triggers the `thinking` reaction
|
||||
for the duration; a simulated knowledge-graph write triggers `eureka`
|
||||
once per cooldown window; a simulated critical signal triggers `alarmed`
|
||||
even while the chicken is asleep.
|
||||
- Resizing the browser viewport re-grounds and re-clamps the chicken.
|
||||
- Both the Terracotta and Carbon themes keep the pixel-art palette legible.
|
||||
- `npm run build` passes with no new errors.
|
||||
Reference in New Issue
Block a user