mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-19 10:04:16 +00:00
Keep native traffic lights and window actions with document-scoped measured chrome; restore ordinary chrome for undeclared pages. Detect packaged helper changes through named and resolved bundle paths. Credit Yan for the initial native title-bar proposal. Version-neutral contribution. Cumulative independent Opus/Astra/GLM whole and repair reviews; original FAIL and nonblocking residuals retained. Selected and native qualification only, no full-suite claim. Cyber Pro author decision: explicit Git after baseline_stale refused commit_reviewed on the deliberately rebased private candidate. Review records rl-20261017T065715-0a7ef5ddd043 and rl-20261017T090810-00fb9463c717.
2071 lines
142 KiB
Markdown
2071 lines
142 KiB
Markdown
# Ouroboros Design System
|
||
|
||
Normative authority for the **visual and interaction semantics** of the Ouroboros
|
||
UI: what a size means, what a colour claims, what a chip is allowed to say.
|
||
|
||
Authority split:
|
||
|
||
- **This file** decides semantics — type scale, hierarchy, foreground and state
|
||
colour, status conventions, card/row anatomy, density.
|
||
- **`docs/DEVELOPMENT.md` → `## Design System`** decides the engineering rules
|
||
that keep those semantics intact — where values may live, which component is
|
||
the SSOT, what counts as review debt, how a visual change is verified.
|
||
|
||
Shared values and control recipes live in `web/ui.css`, loaded before page
|
||
styles by both the SPA and the served onboarding document. Page styles own
|
||
composition, not a second shared palette. This file names roles, not an inventory.
|
||
|
||
## Design bible: "Airy dopamine"
|
||
|
||
This distils the interface Ouroboros already has into rules, so every agent extends
|
||
it the same way. Every rule below derives from these lines and holds in both themes;
|
||
a component that needs its own exception is a design defect.
|
||
|
||
**In one sentence (for a designer).** A light, borderless soft UI in two themes: a
|
||
plain room (white, or near-black violet under glass controls); voice-tinted objects resting on a soft shadow;
|
||
everything pressable a capsule (a card when it carries a paragraph) standing on that shadow in one of three tones, whose
|
||
states change its colour, never its height, as on macOS; one crimson accent and
|
||
candy-coloured status chips; Apple system type in four sizes. iOS airiness with
|
||
Material 3 tonal colour; no gradient behind content, and no glows, textures, outlines or neumorphism.
|
||
|
||
**Principles**
|
||
|
||
1. **The room is plain.** Page, sidebar and panel backgrounds are the room colour or
|
||
one step off it. Never a gradient, glow, wash, texture or animation behind content.
|
||
2. **Objects rest; they are not outlined.** A bubble, card or group is a filled shape
|
||
on the *rest* shadow. What floats over moving content (the Shade, menus, the
|
||
composer field, dialogs, ↓ latest) takes the *float* shadow. Inside an object a
|
||
group or row is a *tray*, a soft neutral fill without a shadow, and fills never
|
||
stack. Only fields keep a hairline, because an editable box must read as editable;
|
||
the composer's field floats instead, and focus gives it the field's ring. On a dark
|
||
room a shadow barely shows, so each height is also a lighter surface (room, object,
|
||
capsule), and what floats over content is glass. The Shade is the one exception: it
|
||
continues its bar's ink and hangs on the float shadow, so the bar and its Shade read as
|
||
one surface unfolding.
|
||
3. **Colour says who and what.** Each voice tints its own objects: the owner blue,
|
||
Ouroboros blush, System amber, a Project fuchsia. A state is a candy tint with its
|
||
own ink, or a dot and a word. Hue says identity and fill strength importance, so a
|
||
mark that only identifies stays quieter than the name it marks. Crimson is the one
|
||
brand accent: live dots, the chosen place, the primary action. Colour lives on
|
||
objects, never on the room.
|
||
4. **One control, three tones, one height.** Everything pressable with a body is a
|
||
capsule standing on the rest shadow in every state (on the float shadow when it
|
||
flies over moving content, like ↓ latest): *neutral* (the capsule surface)
|
||
or *tonal* (its hue's tint and ink: crimson for the primary action and the chosen
|
||
tab or place, green for save, red for danger, amber for the owner's turn). A
|
||
*quiet* control (a link, a filter, an icon, a navigation row) has no body and never
|
||
stands. A state changes colour, never height: hover deepens the fill, a press
|
||
deepens it again, a chosen or switched-on capsule takes its tone and keeps standing;
|
||
what cannot be pressed now loses its shadow. No outlines. Shape follows content: a
|
||
one-line control is a capsule; a choice that carries a paragraph (an offered answer)
|
||
is a card with the object radius, standing like every other. A label that acts takes the
|
||
primary ink; a setting's name or an unchosen option the secondary ink; a setting's
|
||
value the meta ink, never the darkest; a chosen state its tone's ink. A status chip
|
||
is a flat tint: standing on a shadow means pressable.
|
||
5. **Air is spacing, never paleness.** Text keeps reading contrast; lightness comes
|
||
from room between and inside objects and from the absence of lines. Nothing is
|
||
faded to say "not yet known"; it says so in words or with an unlit mark.
|
||
6. **Colour answers touch.** A press answers with the capsule's own colour, never a
|
||
sink or a lift; live work loops as three moving dots; nothing else animates by
|
||
itself, and a state change never moves a neighbour, resizes a bar or changes a
|
||
height.
|
||
|
||
**Seed (light | dark)**
|
||
|
||
| Role | Light | Dark |
|
||
| --- | --- | --- |
|
||
| Room · sidebar | `#ffffff` · `#fafbfc` | `#0d0b0f` · `#0d0b0f` |
|
||
| Object · tray | `#ffffff` · neutral 3 % | white 3.5 % · neutral 3.5 % |
|
||
| Capsule (hover) · float | `#ffffff` (`#f5f6f8`) · `#ffffff` | violet glass `rgb(44 34 52 / .62)` (`rgb(60 46 70 / .72)`) · `rgb(38 31 46 / .94)` |
|
||
| Glass on controls and sheets over content | none | `blur(18px)` |
|
||
| Owner · Ouroboros · task card | `#e9eef9` · `#fdf2f4` · `#fff8f9` | blue glass `rgb(80 120 190 / .16)`→`rgb(55 85 155 / .13)` · crimson glass `rgb(201 53 69 / .2)`→`rgb(120 24 36 / .2)` · crimson 6 % |
|
||
| Crimson ink · on its tonal fill | `#bf2f40` · `#b52b3b` | `#f07a86` · `#ffa0aa` |
|
||
| Tonal fill, rest/hover | crimson `#c93545` 10/14 %, green 14/22, red 10/16, amber 18/26 | crimson `#f05a6a` 18/24 %, green, red, amber 16/24 |
|
||
| Rest shadow | `0 1px 2px rgb(28 33 51 / .05), 0 6px 18px rgb(28 33 51 / .06)` | `0 1px 2px rgb(0 0 0 / .5), 0 8px 24px rgb(0 0 0 / .35)` |
|
||
| Float shadow | `0 2px 6px rgb(28 33 51 / .06), 0 18px 44px rgb(28 33 51 / .11)` | `0 2px 8px rgb(0 0 0 / .55), 0 20px 50px rgb(0 0 0 / .6)` |
|
||
| Press | `brightness(.95)` | `brightness(1.12)` |
|
||
|
||
- Radii by role, both themes: capsule `999px` (every one-line control and chip) · object `18px`
|
||
(bubbles, cards, groups, menus, answer cards) · inner `12px` (trays and rows inside an object) ·
|
||
sheet `24px` (the Shade's lower corners).
|
||
- Type: the system face (SF Pro on Apple), 12 / 14 / 16 / 24; weight 400 to read,
|
||
500 for a status word, a root's title or a setting's name, 600 for labels,
|
||
controls and a setting's value.
|
||
- Space: 4 / 8 / 12 / 16 / 24; 14 between messages; 12 x 18 inside a bubble.
|
||
- Motion: 120 ms for colour and shadow (`--motion-tone`); no press motion; the
|
||
three-dot loop only while work runs.
|
||
- Where it lives: the values are `web/ui.css` tokens, dark in `:root` and light in
|
||
`:root[data-theme="light"]` (`--elev-*`, `--radius-object` / `-inner` / `-sheet`,
|
||
`--surface-object` / `-capsule` / `-float` / `-tray` / `-side`, `--glass`, `--voice-*`,
|
||
`--tone-*` with `--tone-crimson-ink`, `--press`); the recipes are ui.css "One control, three tones" and
|
||
style.css "Airy dopamine", written once for both themes. A page maps its own
|
||
elements onto these roles and adds no recipe.
|
||
|
||
|
||
The shell offers **Settings → Appearance → Light / Dark / System**. New clients
|
||
start on System; explicit Light or Dark remains pinned. The shared semantic
|
||
palettes in `web/ui.css` preserve geometry and status meanings. Both themes follow
|
||
the design bible above; neither has a decorative background.
|
||
On Windows the desktop window keeps its ordinary system caption, buttons, Snap and
|
||
resizing; only their light or dark tint follows the painted palette — the system
|
||
tint, not the page's colour — and a High Contrast theme keeps its own colours.
|
||
On macOS the main window is one surface: the page reaches the top edge under a
|
||
transparent title bar, the real traffic lights sit level with the header row, empty
|
||
parts of that band drag the window, and a double-click follows the Mac's setting:
|
||
Zoom, Minimize, Do Nothing or Fill. Fill relies on an available undocumented native
|
||
action, not an imitation by Zoom. Its appearance follows Light or Dark and inherits
|
||
under System. In full screen the bar hides until the pointer reveals it; the setup
|
||
and error windows and a UI that does not declare it keep the ordinary title bar. A
|
||
browser, another OS or an older desktop app reserves nothing.
|
||
|
||
Appearance belongs to a browser profile or desktop client, not an account or
|
||
server setting. Existing saved Light/Dark choices keep their meaning. Storage
|
||
failures are visible; clearing site data returns the choice to System. Switching
|
||
repaints mounted charts and diagrams without rebuilding views or losing drafts.
|
||
Independent iframe interiors remain author-owned, not automatically recoloured.
|
||
Desktop persistence requires a desktop app (launcher) built with persistent
|
||
WebView storage, 7.2.0 or later; in-app updates replace the core, never that app.
|
||
An older app opens its WebView in private mode, which erases website data each
|
||
time its window opens, so Settings → Appearance names that app by its own version
|
||
and says the choices reset at restart, instead of presenting them as kept. Server
|
||
restart alone cannot verify survival across full quit/relaunch. Mechanism and
|
||
deployment limits: ARCHITECTURE §3 “Navigation and shared UI contracts”.
|
||
|
||
---
|
||
|
||
## 1. Type scale
|
||
|
||
Four sizes. There is no fifth.
|
||
|
||
| Token | Value | Role |
|
||
| --- | --- | --- |
|
||
| `--type-meta` | 12px | Labels, notes, chips, timing/quota lines, captions |
|
||
| `--type-body` | 14px | Default reading text, values, controls, row titles |
|
||
| `--type-section` | 16px | Section and card titles |
|
||
| `--type-page` | 24px | Page / wizard step title, and display text (a login code) |
|
||
|
||
Line heights: `--line-meta` (1.35) for short meta lines, `--line-body` (1.5) for
|
||
prose, `--line-title` (1.3) for headings.
|
||
|
||
Rules:
|
||
|
||
- A value with no exact token **rounds to the nearest token**. It never mints a
|
||
fifth size. 13px becomes `--type-body`, 15px becomes `--type-section`, 11px
|
||
becomes `--type-meta`.
|
||
- **No new raw `10px` / `11px` text on a migrated surface.** Below 12px the
|
||
dark theme forces a choice between illegible and glaring, and the glaring
|
||
option is what produced the owner's "too much small high-contrast white text"
|
||
report. `tests/test_web_typography_static.py` enforces this.
|
||
- Control chrome keeps its own dimension tokens (`--button-font-size`,
|
||
`--pill-font-size`). They are control geometry, not the reading scale; do not
|
||
replace them with type tokens or vice versa.
|
||
- **The one reading exception: a full rich chat answer.** A chat bubble whose
|
||
message is rich Markdown (an answer, the Project completion mirror, a
|
||
`markdown: true` System row) is long text at the bubble's 16px reading
|
||
size. Its headings step up from that text: `--md-heading-major` (1.25em,
|
||
20px) for `#` and `##`, `--md-heading-minor` (1.125em, 18px) for `###`,
|
||
the reading size for deeper levels; its tables read at that size too. The
|
||
steps are relative to the reading text, not UI sizes, and only the rich
|
||
answer's heading rules read the two tokens (§5) — they and a delivered
|
||
document in the reader (§5 "Document reading"). Nothing else changes
|
||
size: controls, labels, cards and page chrome keep the four sizes, and so
|
||
do the compact surfaces (a Skill Review report, a task timeline) and the
|
||
other rich-content cards (the question card, the update letter).
|
||
|
||
## 2. Hierarchy rule
|
||
|
||
**In any row, card or field, exactly one thing is primary.** Everything else
|
||
steps down. Concretely:
|
||
|
||
- **Label** → `--type-meta` in `--text-meta`, sentence case.
|
||
- **Value** (the thing the owner came to read or change) → `--type-body` in
|
||
`--text-primary`.
|
||
- **Meta** (last run, quota, effort, timing, provenance) → `--type-meta` in
|
||
`--text-meta`.
|
||
- **Section title** → `--type-section` semibold; **subsection heading** →
|
||
`--type-body` semibold. A bare `<h3>`/`<h4>`/`<strong>` that inherits the
|
||
browser default is a defect: it lands at bold 16px full white and ties with,
|
||
or beats, the content it introduces.
|
||
- A Markdown heading is a subsection heading too — a `--type-body` semibold
|
||
label in a Skill Review report, the question card and the update letter,
|
||
and its row's own size in a task timeline — except inside a full rich chat
|
||
answer, which is reading text and follows the §1 ladder instead.
|
||
|
||
**The 12px UPPERCASE label pattern is retired** on migrated surfaces. All-caps
|
||
at a small size costs legibility, widens every label, and when a panel repeats
|
||
it dozens of times the labels collectively out-shout the values they describe.
|
||
Authored label strings should read as sentence case; CSS must not manufacture
|
||
caps with `text-transform`.
|
||
|
||
## 3. Foreground and state colour
|
||
|
||
| Token | Meaning |
|
||
| --- | --- |
|
||
| `--text-primary` | The one thing this row/card is about; the label of an action |
|
||
| `--text-meta` | Real secondary content: labels, notes, hints, meta lines |
|
||
| `--text-secondary` | A quieter step below meta: a dense repeated field; a setting's name or an unchosen option on a capsule |
|
||
| `--text-disabled` | Genuinely inert or incidental content only |
|
||
|
||
`--text-muted` is a legacy alias of `--text-disabled`; new work names
|
||
`--text-meta` or `--text-disabled` so the intent is readable in the diff.
|
||
|
||
`--text-secondary` is a real fourth step, not an alias: it sits between meta
|
||
and disabled and is written at ~50 call sites. Reach for it only when
|
||
`--text-meta` is genuinely too loud — a value repeated on every row of a dense
|
||
list, a chip's supporting word — and `--text-disabled` would be unreadable.
|
||
The criterion is the same one that governs the whole table: **if the owner has
|
||
to read it to act, it is `--text-meta` or brighter.** A load-bearing caveat, a
|
||
one-off note, a hint that explains a control: those stay at meta. A control's own
|
||
label is not content: it follows bible principle 4 (an action's label is primary, a
|
||
setting's name or an unchosen option secondary). A tone or an
|
||
unclassifiable ink is what produced the original "too much small
|
||
high-contrast white text" report from the opposite direction, so a rule that
|
||
declares a size and no colour is still the worst of the options.
|
||
|
||
Two failure modes this table exists to prevent, both observed in this codebase:
|
||
|
||
- **Secondary content parked at the disabled foreground.** A load-bearing
|
||
caveat rendered at `--text-disabled` reads as greyed-out chrome and gets
|
||
skipped. If the owner is meant to read it, it is `--text-meta`.
|
||
- **Secondary content with no foreground at all**, inheriting `--text-primary`.
|
||
This is the loudest of all failures because it is invisible in the CSS — the
|
||
rule simply declares a size and says nothing about colour.
|
||
|
||
### Brand accent
|
||
|
||
The brand red is **one value**, `--accent`. Every appearance of it is either
|
||
that token, the named roles built on it (`--accent-light` for accent text, the crimson ink on light,
|
||
`--accent-dim` for a fill, `--focus-accent-border` / `--focus-accent-ring` for
|
||
focus), or a rung of the accent alpha ladder (`--accent-04` … `--accent-65`; the crimson tone and its hover are `--accent-10` and `--accent-14`).
|
||
A new alpha is a rung added to the ladder, never an `rgba()` literal in a rule:
|
||
the ladder is what makes "make the accent calmer" a one-line change instead of
|
||
a grep.
|
||
|
||
The app and first-run wizard consume the same palette source. A shared accent
|
||
change updates its named roles and alpha ladder there; neither page shadows
|
||
those roles with its own values. `tests/test_web_typography_static.py` checks
|
||
the actual stylesheet links, nonempty shared roles and per-document variable
|
||
resolution. The SM1 browser oracle also checks both rendered documents after
|
||
restart, so a missing link or a page-local override cannot pass as consistency.
|
||
A wizard-only layout token is fine; a second value for a shared role is not.
|
||
|
||
### Focus
|
||
|
||
Keyboard focus has **one** appearance:
|
||
|
||
```css
|
||
outline: 2px solid var(--focus-accent-border);
|
||
outline-offset: 2px;
|
||
```
|
||
|
||
`outline-offset: -2px` is the only sanctioned variant, for a control that sits
|
||
flush inside a strip that clips an outer ring (sidebar rows, header buttons).
|
||
Nothing else: not a blue ring, not a green one, not a `box-shadow` standing in
|
||
for an outline, and not a colour picked to match the control it is on. A focus
|
||
ring is the reader's cursor; if it changes colour per surface, it stops
|
||
reading as one thing.
|
||
|
||
**Hover paint is not a focus ring.** A rule written as
|
||
`.x:hover, .x:focus-visible { background: … }` gives a keyboard user exactly
|
||
what a mouse user gets by accident and nothing that says "you are here". Where
|
||
a control wants both, the shared paint stays in the hybrid rule and the ring
|
||
goes in a `:focus-visible`-only rule of its own.
|
||
|
||
Text fields are the exception, and they keep their own established idiom:
|
||
`border-color: var(--focus-accent-border)` plus
|
||
`box-shadow: 0 0 0 3px var(--focus-accent-ring)`. A field already has a border
|
||
to recolour, so an outline outside it would be a second frame.
|
||
|
||
### Controls and editable choices
|
||
|
||
Text, number, password, select and multiline fields use the same `.ui-control`
|
||
family; `.ui-checkbox` keeps native checkbox behavior. `.ui-field` groups a
|
||
label, control and optional `.ui-field-help`. A placeholder is an example,
|
||
never the field's only name. Help and validation belong to that field without
|
||
changing the alignment of neighboring controls and their actions.
|
||
|
||
**Secret visibility has one meaning across Settings.** Show reveals the complete
|
||
current value as selectable, read-only text below the unchanged editing field;
|
||
long values wrap within the column. An edited value is shown from the local
|
||
draft; an untouched saved value is read only when requested. A prefix or a
|
||
configured-value placeholder is never a successful reveal. Hide removes that
|
||
text and retains the draft. Viewing changes neither the dirty state nor the
|
||
values submitted by Save or Test. Editing, clearing, reloading or leaving the
|
||
page closes the reveal, and a late read cannot reopen it. Built-in, custom,
|
||
skill-requested and MCP header fields use the same interaction; loading Settings
|
||
keeps saved secrets masked. Pending reads and failures are shown at the field.
|
||
|
||
A segmented choice row gives every choice the same width. Up to four fill their
|
||
row; a longer scale wraps into equal columns of four and leaves its last row partly
|
||
empty rather than stretching a lone choice; a narrow card stacks two; one-glyph
|
||
values such as review cycles stay one row. An empty inherit choice is as real as
|
||
any other, and the focused choice wears the one focus ring. A choice the running
|
||
Access suspends — Blocking under Cyber Pro — stays selected; the note beside it
|
||
names its effect for the current process and for the selected next start, from
|
||
the draft before Save.
|
||
|
||
Short fixed choices keep native selects, including the platform's own popup.
|
||
A control never widens its column: a select shows its chosen label on one
|
||
line, clipped at its own edge, and the full label stays in the platform's
|
||
popup.
|
||
Model selection uses the shared editable chooser: suggestions assist typing
|
||
without becoming an allowlist. A saved unknown model remains editable; a
|
||
catalog refresh preserves the real input, selection and composition. Escape
|
||
or blur closes suggestions without assigning a value. Selected, hover, focus,
|
||
disabled and invalid states have different meanings and remain distinguishable.
|
||
Availability never rewrites a selection: a Claude `[1m]` variant whose base
|
||
model a usable account lists reads Available, noting that the engine checks the
|
||
variant at session start, and an unread catalog reads Not checked. Maximum
|
||
response sits beside the context window as Auto or a whole number for this
|
||
exact model, server and account; Apply acknowledges it outside the Settings
|
||
draft, and a route change clears it. Web search is its own Source and Model:
|
||
Auto lists the routes it may use, a chosen source is strict, and skills, MCP
|
||
and browser tools stay independent.
|
||
|
||
Tabs expose one selected view and one keyboard entry point. Arrow keys and
|
||
Home/End move through available tabs; restoring a selected tab reveals it by
|
||
scrolling its strip, without moving the page or taking focus. Menus and
|
||
editable suggestion lists share viewport placement, not keyboard semantics:
|
||
a menu moves focus among actions, a chooser keeps it in the input. Dialog
|
||
focus stays in the modal context and returns on close when the caller remains
|
||
available. Popups sit outside decorative clipping and fit the usable viewport.
|
||
|
||
A **message field** — the Main and Project composer, a question's own answer —
|
||
sends with Enter by pressing its own Send action, whose validation, disabled
|
||
and pending state decide what is sent; a message is sent once. Shift+Enter
|
||
breaks the line; Ctrl, Cmd or Alt with Enter send like Enter. Enter sends
|
||
nothing when it commits an input-method composition or the field is empty,
|
||
disabled, read-only or settled, and once when held; a rejected answer keeps
|
||
its draft. The composer grows with its text or placeholder to one cap, then
|
||
scrolls. Touch keyboards are asked for a Send label; support varies. Every
|
||
other multiline field — file editors, JSON, settings and widget forms — keeps
|
||
Enter as a line break.
|
||
|
||
### Composer owner controls
|
||
|
||
Each composer saves its room's **Context** and **Effort** through the owner endpoints.
|
||
Main supplies a one-time copy to new Projects; later changes stay in their room.
|
||
Readback comes from Main state or the named Project, never another room. Saves apply
|
||
to new participants; running work retains its choices through Resume. A refusal
|
||
keeps the saved value and says why. No scope selector or extra settings row.
|
||
|
||
Each is a neutral **capsule** of the pills' height — ring, name, value: `Context Max`,
|
||
`Effort X-High`; the name in secondary ink at 500, the value in meta ink at 600,
|
||
and only the ring's lit arc coloured (crimson; Low amber, Nano the Project's).
|
||
Until the room's values arrive the ring stays unlit and the value secondary,
|
||
never a faded capsule. Closed, Effort names only the primary level; its ring (a 300°
|
||
arc: band minimum..maximum, dot primary) and the button's name carry the range.
|
||
Context's ring is three arcs, the chosen one lit: one of three modes, never a fill
|
||
level or capacity. A capsule opens **inline**, never as a dropdown, one at a time;
|
||
its value yields to the strip. Context's is Nano · Low · Max under a glass thumb; a
|
||
choice posts at once and the strip waits for the answer. Effort's seven levels carry
|
||
a primary pill and two helper bounds; the primary may lie outside them. A
|
||
segment is its word plus `--capsule-seg-pad` a side; the pill covers its segment;
|
||
band, pill and brackets share one vertical inset. The primary and helper bounds never
|
||
move each other; the brackets cannot cross. A level tap sets primary; each bracket
|
||
has its own handle. A drag keeps its grab offset on whole levels. A stored `minimal` shows at Low until its handle moves.
|
||
|
||
**A mark keeps from a rounded end the clearance it keeps from the straight edges.**
|
||
A pill (the thumb, the recommended pill) is concentric with the capsule, so the band
|
||
inset suffices; a square bracket at the last level would graze the curve, so
|
||
Effort's end gutter also clears it. The gutter is measured into the fit and outlasts
|
||
scrolling and dragging.
|
||
|
||
**The open strip stays on the composer's line**: beside the others when it fits
|
||
(padding down to `--space-1`), else alone while they step aside (down to 3 px, the
|
||
pill's corners tightening; the name and divider yield first), last scrolling inside
|
||
itself, faded where levels hide. A closed line too narrow drops the names, then
|
||
ellipsizes the longest values, never rings. All is measured, so no breakpoint.
|
||
|
||
**Hover is an accelerator; the press always works.** With a hovering fine pointer a
|
||
capsule opens ~160 ms after arrival (not while the other is open) and closes a
|
||
second after it leaves a 32 px zone unless a press pinned it; the press unpins.
|
||
Touch and keyboard use the press: Enter or Space opens onto the recommended handle
|
||
or the chosen mode; arrows step a handle (PageUp/PageDown too) or choose the next
|
||
mode, Home/End reach the ends; Esc closes and returns focus to the head; an outside
|
||
press closes. Handles are sliders naming their level; closed, they leave the tab
|
||
order. An effort change saves when the gesture ends, saves run in order (last wins),
|
||
and Send waits for both pending Context and Effort saves; a refused
|
||
save keeps the draft, says why and returns to the saved value — unsaved values are
|
||
never shown as saved.
|
||
|
||
### `.muted`
|
||
|
||
`.muted` is a **colour-only utility**: `color: var(--text-meta)`, nothing else.
|
||
It must never set `font-size`. It is written at ~50 call sites that already
|
||
sized themselves, and a size here would silently resize all of them. A scoped
|
||
rule (`.some-context .muted`) still wins on specificity where a surface needs a
|
||
local variant.
|
||
|
||
### Dark-theme contrast
|
||
|
||
WCAG 4.5:1 is a **floor, not a target**. On near-black, pushing small text
|
||
toward pure white causes halation — the glyphs bloom, and because everything is
|
||
maximally bright, nothing is emphasised. The result is a screen that is
|
||
simultaneously harder to read and flatter in hierarchy.
|
||
|
||
**De-emphasise the secondary rather than amplifying the primary.** When
|
||
something needs to stand out, drop the ink around it, do not raise its own. All
|
||
Primary (15.9:1) and meta (9.2:1) clear the 4.5:1 floor against `--bg-primary`
|
||
with room to spare. `--text-disabled` is deliberately BELOW it (3.5:1) and is
|
||
therefore reserved for genuinely disabled or incidental content, which WCAG
|
||
exempts; it must never carry meaning a reader has to obtain.
|
||
|
||
### A selected state is not exempt from contrast
|
||
|
||
A **status hue** and a **status foreground** are different values, and the
|
||
selected state of a control must use the foreground. Selected Advisory in the
|
||
enforcement group read `--amber` (`#f59e0b`) over a 12% amber wash: ~2:1 on the
|
||
light surface, unreadable exactly when the owner had chosen it. The rule that
|
||
closes this: a selected control tints with the `--status-*-fg` /
|
||
`--status-*-bg` / `--status-*-border` triple, which is defined per theme, and
|
||
never with the raw hue token, which is not.
|
||
|
||
The same reasoning covers images. A colour baked into a `data:` URI cannot be
|
||
themed, because a custom property cannot be interpolated into the URI string —
|
||
which is why the select chevron was a pale `#e2e8f0` on white. **The whole
|
||
image is the token** (`--select-arrow`), overridden per theme, not the colour
|
||
inside it. `tests/test_appearance_static.py` holds both facts.
|
||
|
||
## 4. Status and chips
|
||
|
||
A status has **an explicit foreground/background pair**, never a foreground
|
||
derived from whatever generic opacity sits on the element.
|
||
|
||
The connected Chat header says `Starting…` until a complete server-ready
|
||
observation confirms the supervisor can assign work; a live socket alone means
|
||
only that the UI transport is open. `Online` is reserved for a ready, idle
|
||
runtime; known queued, active or finalizing work keeps its own factual label. A
|
||
durably accepted incoming message likewise proves acceptance, not that its task
|
||
has begun or a model has read it.
|
||
|
||
Status, owner action, and urgent notification are separate product concepts:
|
||
|
||
- **Status** states a fact about the affected object, never implying that the
|
||
owner can or must act. Task status uses one factual word family, `Working`,
|
||
`Done`, `Done with warnings`, `Failed`, `Cancelled`, which is also the host's
|
||
durable label vocabulary for its own task rows in Main and the Project thread
|
||
(`OUTCOME_PHASE_HEADLINE`), not only the browser's.
|
||
- **Owner action** exists only when the responsible domain exposes a current
|
||
concrete continuation, such as Resume, Retry, Connect, Repair, Grant access,
|
||
or Restart: a real adjacent control, which severity alone never manufactures.
|
||
- **Urgent notification** is a rare, time-sensitive interruption. It uses the
|
||
product's explicit incident/notification seam, not a red status or a failed
|
||
task as a proxy.
|
||
|
||
Activity schedule rows use the same factual status rule: `active`, `disabled`,
|
||
`suppressed`, `consumed once · history` and `deletion pending` describe
|
||
lifecycle state, while the adjacent Disable/Enable, Restore and Delete controls
|
||
state the owner action. A consumed one-shot is history whether its task
|
||
succeeded or failed; the status never implies a result. Retained rows (consumed
|
||
and suppressed) collapse into one disclosure rather than padding the standing
|
||
list or disappearing: history the owner can still open, read and act on. A
|
||
suppressed skill row keeps Restore so the owner can ask for it back; a consumed
|
||
one keeps only Delete, since Enable on a schedule that cannot fire again would
|
||
be a lie. A deleted row still finishing work it already accepted stays in the
|
||
standing list and says what it waits for. Where generic Restore can cancel the
|
||
pending deletion, the button says Cancel deletion, not Enable; an exact hold
|
||
release stays separate and preserves the deletion intent.
|
||
|
||
A record of what the owner did earlier is history, not status. A skill's
|
||
OuroborosHub submission (the version it sent and its PR link) lives in the
|
||
card's details disclosure without a tone; it never claims the PR merged or gates
|
||
or replaces the card's current action, and forgetting it is an explicit local
|
||
choice, not the way to unlock one.
|
||
|
||
A task-bound `Reviews` history row may be its owner's only retained fact: it
|
||
keeps a neutral owner anchor visible but hides task status and typing until a
|
||
real task status or activity arrives; review presence alone never means
|
||
`Working`, `Done`, or owner attention.
|
||
|
||
A review only awaited when its task ended is not a warning: reviewers that had
|
||
simply not answered yet leave the task `Done`. For a plan review the task result
|
||
keeps the host's typed disclosure that the review was still open (the
|
||
`terminal_host_notice` field, read by the CLI and by parents), the card states
|
||
the same fact as its cause sentence, and no second chat bubble carries it; a
|
||
card amber or red for another reason keeps that reason, with the open review
|
||
stated beside it when the result records it. For task acceptance the host's
|
||
decision sentence already says no reviewer verdict was established. A real
|
||
outcome keeps its word: no reviewer quorum, a failed, refused or unresolved
|
||
slot, a collected blocking finding, a reviewer verdict nobody closed, a rail or
|
||
a blocking exit still read `Done with warnings` or `Failed`.
|
||
|
||
A reviewer panel that settles after its task already ended adds one System row
|
||
naming the reviewed version (delivered, different, or delivery unknown) before
|
||
its verdict. Its reviewer lines name the model that answered as an engine
|
||
reported it (else the requested one, labelled so: a direct API route's own
|
||
target is a request, not a report; two seats of one model stay two lines) and
|
||
say each outcome in words: passed it, rejected it, inconclusive, still awaited,
|
||
outcome unknown, not sent, unavailable, or answered without a readable verdict;
|
||
an unavailable reviewer's cause is quoted as the delegated engine's report,
|
||
never as the provider's. A reviewer's shortened note, like an engine cause the
|
||
host's own bound shortened, says only that it was shortened; the row offers the
|
||
exact applied record as a download once the host stored it, and a cut of the
|
||
whole row names that record only then, never a guessed link. Slot ids, verdict
|
||
tokens and omission markers stay in that record and the model's mailbox. The row
|
||
lands inside the finished card (its Reviews group carries the note, the timeline
|
||
keeps the row) without changing the card's chip, title or meta; a standalone row
|
||
appears only when the task has no card record in the page. Saving and showing
|
||
late criticism does not replace passing it to Ouroboros: the host owns sources
|
||
and fact delivery; Ouroboros judges the evidence, explains it and chooses any
|
||
further work on an admissible turn. The untyped terminal host notice and the
|
||
origin-addressed routing notices stay ordinary rows by design. Local diagnostic
|
||
failures stay inspectable in details and Logs but do not relabel the whole
|
||
still-working task. A failed child keeps a compact factual `Failed` marker
|
||
inside its parent while the root continues under its own authoritative status.
|
||
Internal reason codes belong in details and diagnostics, not compact headlines:
|
||
a card that shows a cause says it in the owner's words while the record keeps
|
||
the machine code, and a cause with no sentence yet stays raw rather than
|
||
borrowing a wrong one. The rails that end a task are such causes: the loop's
|
||
forced finalization (round limit, deadline, grace window, context, unabsorbed
|
||
children) and the supervisor's timeout reaper (maximum running time, deadline,
|
||
idle silence) keep their typed codes on the record and the incident key, and the
|
||
card, the reaper's grace toast, kill notice and salvage line, and the loop's own
|
||
fallback text all say the one sentence from the shared table
|
||
(`project_dialogue.TASK_CAUSE_PHRASES`, whose browser twin lives in
|
||
`log_events.js`). The routing receipt under an owner message is such a surface:
|
||
a refused addressing act carries the host-composed `cause` sentence
|
||
(`project_dialogue.routing_refusal_cause`, one host table for the receipt line,
|
||
the System row and the picker toast), a landed act carries none, and an unknown
|
||
reason stays raw. A provider failure quotes only the provider's own sentence (a
|
||
long one, or one its producer already cut, shortened in words beside the Logs
|
||
pointer); without one it names the host's classification of the failure (beside
|
||
the provider's own error code and parameter when an engine relays them, as the
|
||
provider's) and points at the Logs, which keep the host's record of it (a stream
|
||
error's sentence only as a bounded excerpt, so no row promises more): neither an
|
||
exception's text nor an engine's composed diagnostic reaches the owner's row.
|
||
Host text speaks only for the host's own actions, counts and signed quotes; a
|
||
source it could not read is unknown, never zero. A terminal whose preserved
|
||
output was never reviewed shows that output labelled, not hidden: a short
|
||
labelled excerpt beside the pointer to the full copy, so a `Failed` card over
|
||
applied work is never a bare headline and never names preserved bytes without a
|
||
way to reach them. Where a stop receipt already carries the same text in the
|
||
very chat the card is written to, the card keeps the label and pointer alone; a
|
||
card in another chat keeps the excerpt.
|
||
|
||
| Role | Foreground | Background | Border |
|
||
| --- | --- | --- | --- |
|
||
| Success / connected | `--status-ok-fg` | `--status-ok-bg` | `--status-ok-border` |
|
||
| Warning / degraded | `--status-warn-fg` | `--status-warn-bg` | `--status-warn-border` |
|
||
| Error / failed | `--status-error-fg` | `--status-error-bg` | `--status-error-border` |
|
||
| Neutral / classification | `--status-neutral-fg` | `--status-neutral-bg` | `--status-neutral-border` |
|
||
|
||
- **Status renders as dot + text.** The dot carries the state at a glance, so
|
||
the sentence need not shout it in saturated colour and sits at ordinary
|
||
reading contrast.
|
||
- **A known outcome owns the status; an unfinished lifecycle stands beside it.**
|
||
When a task's outcome is settled while post-task work still runs, the chip
|
||
states that outcome from the five-word family and `Finalizing…` is a SECOND,
|
||
quieter fact next to it: not a replacement, not a sixth status word, never
|
||
something the card title carries instead. The pair is one accessible name, so
|
||
a screen reader hears the outcome and the hold together. An owner stop
|
||
outranks the hold entirely and speaks for itself. Dashboard → Activity applies
|
||
this to a row's own outcome, never its root's: plain `running` yields to it, a
|
||
queue wait or hold (`pending`, paused, sleeping, held after Restart) stays
|
||
beside it as the second fact, and the row keeps its controls while the task
|
||
still occupies the queue.
|
||
- **Neutral is a real state**, not an absence of one. A classification chip
|
||
(which agent, which family) is neutral: a tag, not an alarm. A tone value the
|
||
code emits (`muted`) must have a rule; falling through to a default is how
|
||
chips end up white.
|
||
- Chips are `--type-meta`, not smaller, and are not uppercased.
|
||
- `--green` / `--amber` / `--red` are the saturated hues, for things that are
|
||
not text: dots, switch tracks, progress. The `--status-*-fg` tints are for
|
||
text on near-black; do not swap them. (A `--tone-ok` / `--tone-warn` /
|
||
`--tone-danger` alias family, plus `--accent-task` / `--accent-system` /
|
||
`--accent-user` / `--accent-project` and `--ui-tone-*`, were named here and
|
||
referenced by nothing, so every surface invented its own literal instead. They
|
||
are gone; the vocabulary above is the whole vocabulary.)
|
||
|
||
Current completion and independent criticism are separate facts. An informed Advisory
|
||
author finish may complete the current subject while its original review remains
|
||
FAIL or DEGRADED; the old critic alone must not paint that completion Failed.
|
||
Independent execution, artifact, verification or publication failures still apply.
|
||
Blocking corrections saved without fresh approval and an explicit unfinished stop
|
||
remain unaccepted; show the retained work and reason through the existing five-word
|
||
status family and details, without inventing reviewer PASS or a new status badge.
|
||
|
||
### The tone primitive
|
||
|
||
Two shapes carry a tone, and they are not interchangeable:
|
||
|
||
- **A status sentence** — `.ui-status[data-tone]`. Foreground only, rendered as
|
||
dot + text. Filling it would turn every inline status into a badge and make
|
||
"connected" the loudest thing on the panel. A room's status beside its title is
|
||
such a sentence, with a caret when it opens, never a filled chip.
|
||
- **A status chip** — `.ui-chip[data-tone]`. The full triple, because the chip
|
||
*is* the status and has nothing else to carry it.
|
||
|
||
Surfaces whose tone is a class suffix rather than `data-tone`
|
||
(`.skills-status-*`, `.skills-badge-*`, `.toast-*`, `.marketplace-state-*`,
|
||
`.chat-live-phase`, `.log-phase`, `.evo-runtime-pill`, `.widget-table-status`,
|
||
`.widget-metric` / `.widget-callout`) name the same tokens. A surface that
|
||
paints only its edge (a card tinted by its state, a callout's left rule) takes
|
||
the border and leaves its own background alone; a toast keeps its glass
|
||
background, because a translucent status fill over live page content costs the
|
||
text its contrast.
|
||
|
||
Adopting these tokens applies the semantic status contract, which already
|
||
governs every surface; it is not a token migration of those surfaces and does
|
||
not move them into the migrated set in section 8.
|
||
|
||
### Sidebar activity dots
|
||
|
||
Project navigation rows may carry the existing three 4px working dots
|
||
(`chat-live-typing`, 3px gap) for the live `active_chat_activities` census.
|
||
`Working`, `Thinking` and `Finalizing` are the only moving states, in the
|
||
existing 1.4s bounce rhythm; `Queued` stays static at a quieter step.
|
||
Budget-paused work, confirmed model access waits, required owner questions and
|
||
Project/task scope verification waits are static amber; `resumed` questions no
|
||
longer wait. A wait on the same producer row suppresses its working motion; an
|
||
independent working row keeps motion, and the row's accessible name states both
|
||
facts. Unknown or unconfirmed census state stays static and explicitly
|
||
unavailable. The dots take the row's own foreground, like the status-sentence
|
||
dot, so they never out-shout the title and follow its hover and selected ink;
|
||
the wait is the one hue, and no row paints them in a saturated project colour of
|
||
its own. In a Project row the dots and the unread dot each own a reserved
|
||
trailing column, so both sit at one x across rows whether or not the other is
|
||
present, whatever the name length, and while the sibling kebab is hovered,
|
||
focused or has its menu open; a deleting row draws only `Deleting…`, its census
|
||
fact kept in the accessible name. The collapsed Projects header carries the
|
||
aggregate dots beside its label, and an activity repaint keeps the existing row
|
||
and menu nodes. The dots are separate from unread dots and never carry a
|
||
counter, percent or text animation; reduced-motion clients get the same state
|
||
without the bounce.
|
||
|
||
### Project unread dot
|
||
|
||
A Project row's unread dot, and the Projects count, mean *there is a new message
|
||
in this Project's conversation that you have not read*, never *something moved*;
|
||
activity dots, notifications, toasts, task controls and Stop/Panic keep their
|
||
own meaning, unaffected by it.
|
||
|
||
Unread revisions follow successfully stored conversation rows: a failed write
|
||
keeps the existing live-delivery behavior but cannot advance saved history's
|
||
revision, so this counter guarantees no delivery durability.
|
||
|
||
- **Counts:** each new standalone message in the conversation — an ordinary or
|
||
proactive reply, a question, a root task's final answer, a standalone System
|
||
message (a root task's terminal incident included), a delivered photo, video,
|
||
file or link card. A question or a delivery is shown alone and counts whichever
|
||
task sends it, a child task included.
|
||
- **Does not count:** anything that changes a task card rather than the
|
||
conversation — narration and progress, lifecycle, incidents raised in a
|
||
task's progress (root worker failures included; they still toast), host rows placed in a card (custody, late
|
||
review results, merge receipts, a child task's terminal incident), and a child
|
||
task's own words and final: a child speaks to its parent, and its card shows
|
||
them. Where a row is shown decides, never its kind: an incident shown alone as
|
||
a System message counts, one shown in a card does not. A placed row is card
|
||
content even where the page has not loaded its card and shows it alone.
|
||
- **Read:** only after a covering history read paints the newest-arrived message
|
||
on screen in the visible room, clear of header and composer; being at the
|
||
bottom is not enough when later cards or owner messages buried it. Opening an
|
||
unread room therefore lands on that standalone message wherever it lies: until
|
||
a loaded page holds it, the opening reads older pages (the page limit bounds
|
||
what stays loaded, not this reach); a read room opens at the bottom. One
|
||
opening intent, ended by a gesture (in a bounded box too), a question, leaving
|
||
room or window or a failed page (bottom and Retry), never a jump on later
|
||
reads or new arrivals. A question opened from its Main mirror has priority;
|
||
showing it reads no newer messages elsewhere. Collapsed cards need not open.
|
||
The newest message is the one that arrived last: a late answer keeps its
|
||
written time and chronological place. While reading elsewhere the dot remains
|
||
until that message is shown by scrolling, `Scroll to latest message`, or a
|
||
return to the page/window; opening alone never clears it. Unknown newest
|
||
arrivals (an unfinished line, a damaged row) acknowledge nothing and are never
|
||
searched for; state refreshes recheck an open unread room until history is
|
||
repaired or newer standalone content arrives. A room with no standalone
|
||
message is read at the bottom only once its clean start is reached (by bounded
|
||
search, the opening or `Load more history`). An old child's final whose
|
||
lineage appears only on another page can keep the dot once the room is shown.
|
||
- **Several clients** share one read cursor that only moves forward: a room read
|
||
on one client stops showing the dot on the others at their next state
|
||
refresh, and nothing read is shown unread again.
|
||
|
||
### References and actions
|
||
|
||
One owner intent has one control, built in one module. Tokens and primitives
|
||
cannot guarantee that alone: every copy of a control can reuse a primitive while
|
||
its caller still chooses the words, glyph and class, which is how "take me to
|
||
this Project" came to be drawn six ways. The rule is therefore about intents.
|
||
|
||
- A **reference** points at an entity that exists in the product and goes there
|
||
when pressed. A Project is referred to by the Project reference (the Projects
|
||
glyph, the Project's name, `↗`) and by nothing else, wherever content points
|
||
at it. `project_reference.js` builds it and alone raises `ouro:open-project`;
|
||
a caller chooses a layout (`inline`, `bar`, `footer`), never a label or a
|
||
class. It names the Project wherever its row carries the name, even beside
|
||
same-named work; a Project never named, and the owner's routed message (whose
|
||
caption names the destination), read `Project`, never an id. It claims no
|
||
activity (the work card's chip does). Its accessible name says in words what
|
||
the glyph and the arrow say in pixels (`Open project <name>`); nothing depends
|
||
on hover.
|
||
- **Its form follows the surface's job.** It is a bound work card's wide footer
|
||
(the body stays the work's); under a mirrored answer or a System lifecycle
|
||
row, a reading-size capsule in the shared `createSystemMessageActions` row
|
||
(token spacing, wrapping, focus-ring clearance, never a clipped line: a
|
||
composition, not a card framework or global button-margin rule); the same
|
||
capsule at the end of the owner's routed message's one receipt line ("Passed
|
||
on to «work» in", "Started as a task in"); a meta capsule in a question head.
|
||
Long names: two lines in the footer, ellipsis in capsules, no smaller type;
|
||
accessible names stay whole.
|
||
- A **command** is a button: it changes or confirms something, or goes to a
|
||
place that is not such a reference (`Load older messages`, `Open widgets`).
|
||
Tabs, toggles, menus and the navigation list keep their own roles; a status
|
||
chip states a fact and is not a destination.
|
||
- An intent that will be drawn in a second place gets its door first (siblings:
|
||
`docs/inventories/UI_CONTROL_TEXT_INVENTORY.md`).
|
||
|
||
### Chat authorship and System rows
|
||
|
||
Voice follows authorship, not severity or transport. Model-authored replies use
|
||
Ouroboros's assistant voice; host-composed commands, receipts and diagnostics use
|
||
System voice. A **System row** is a chat message with `role="system"`, in the
|
||
yellow/amber system treatment or its task-card placement. `role` alone selects
|
||
voice; `system_type` names the kind, not the author (the Project question and
|
||
completion mirrors are System pointer rows showing model-authored bytes in
|
||
Ouroboros's voice). New host producers stamp both fields; relays keep them through
|
||
delivery, persistence and history. Model narration, proactive replies and
|
||
questions stay model-authored even when typed.
|
||
|
||
A host fact has a subject, and the subject names its carrier: a task's fact is a
|
||
row of its card, a fact about the owner's message is the typed annotation on that
|
||
message, and a landed act publishes no row (its receipt or card already says it).
|
||
A standalone System row exists only where nothing else can carry the fact: no mind
|
||
can speak, no mind will read the message, the owner's own direct act, the
|
||
runtime's state, Main's Project lifecycle, or words signed by their author (a
|
||
reminder, a skill notice). It is one short typed mark in owner words, never a
|
||
paragraph or text written for a model. `tests/test_host_message_voice.py` keeps
|
||
every host type stamped as a literal at its send site in one of these classes, with
|
||
its counted producer sites.
|
||
|
||
Language follows the same line. The install's interface language
|
||
(`OUROBOROS_UI_LANGUAGE`, Settings → Appearance → Language) translates chrome
|
||
and System voice at render from a translation memory the install generated or
|
||
imported, never from a dictionary in the repository: status words, cause
|
||
sentences and question status by their stable code (`tr`), labels and help copy
|
||
by their rendered English (the overlay), a relayed host sentence by its exact
|
||
text (`tx`). Model-authored text, owner-supplied names, logs and code are never
|
||
translated, and durable rows keep the English with their typed codes; a string
|
||
the memory lacks stays English, never blank or guessed.
|
||
|
||
Authorship also has a time axis. Ouroboros's voice means *the model, now*. Words
|
||
the mind wrote earlier for a later moment (a note left with
|
||
`schedule_followup(notify=true)`) reach the owner at that moment as a System row
|
||
(`system_type="reminder"`), never as a live reply: the host prepends one
|
||
signature line, `Reminder · Ouroboros · written <when> · for <when>` (plus
|
||
`· delivered <when>` when downtime held it back), and keeps the words verbatim,
|
||
never rewritten, summarized or re-timed; the next turn reads the row as a host
|
||
fact (`📋 [reminder]`), so Ouroboros knows what the owner was shown. A skill
|
||
holding the owner's `notify_owner` grant speaks the same way in its own name: a
|
||
`skill_notice` System row signed `Notice · <skill>`.
|
||
|
||
Formatting is asymmetric: assistant text always uses the sanitized chat markdown
|
||
renderer regardless of `markdown`; ordinary System text is escaped unless
|
||
`markdown: true`. The typed `skill_review` row keeps its dedicated renderer.
|
||
Voice does not confer task finality. The existing untyped terminal-host-notice
|
||
contract remains a documented exception, not a pattern for new notices.
|
||
|
||
Built-in chrome uses words and monochrome `currentColor` vectors, not decorative
|
||
emoji. System keeps its amber bubble and plain label; status stays explicit in
|
||
words and the tone primitive. Functional icons keep state and accessible names.
|
||
Known typed host prefixes are hidden only at display (`host_notice_text.js`);
|
||
unlisted or ambiguous records remain literal. History, keys, exports and the
|
||
existing Copy input are unchanged (Copy already uses translated text for two
|
||
notice types). Authored text, quoted evidence, names, documents, logs and author
|
||
widget icons/interiors are not chrome and keep their characters.
|
||
|
||
Main's empty-chat greeting speaks in neither voice: it is host copy in a quiet
|
||
`.chat-empty-welcome` placeholder (a `Welcome` label over one sentence), never a
|
||
bubble, a history row or a model reply (`web/modules/welcome_preference.js`). It
|
||
appears only after a successful recent history read whose own window reports
|
||
complete coverage, over a feed with no message or task card (the typing indicator
|
||
and the ephemeral reconnect notice are chrome, by the same rule that gives an empty
|
||
feed its history loading state). Every later read withdraws it while in flight, so
|
||
that loading state, then any failure, shows instead; only a complete answer brings
|
||
it back, and it leaves with the first message or task card. Project rooms never
|
||
show it.
|
||
|
||
The sentence is a hidden install-wide preference with no Settings control: the
|
||
`welcome` key of `state/ui_preferences.json` under the data root
|
||
(`~/Ouroboros/data` by default). Change only that key and keep the file's others:
|
||
|
||
```json
|
||
"welcome": {"mode": "default", "text": ""}
|
||
"welcome": {"mode": "hidden", "text": ""}
|
||
"welcome": {"mode": "custom", "text": "Good morning."}
|
||
```
|
||
|
||
`default` shows the built-in "Ouroboros has awakened", `hidden` shows nothing, and
|
||
`custom` shows `text` as plain text, never markup (nonblank, at most 500
|
||
characters; other modes retain but do not display valid text). The object has
|
||
exactly these two keys. Prefer `POST /api/ui/preferences` with `{"welcome": {...}}`:
|
||
it validates and merges the value (400 on refusal, without writing). For a hand
|
||
edit, stop Ouroboros, back up the file, and preserve valid whole-document JSON
|
||
and neighboring keys. An invalid welcome inside valid JSON falls back to
|
||
`default` independently; the next save stores that default. Malformed JSON
|
||
instead follows the existing whole-file fallback: all preferences read as
|
||
defaults, and a later save may replace the unreadable contents. Main reads the
|
||
preference when it opens and on every reconnect; there is no file watcher.
|
||
|
||
## 5. Card and section composition
|
||
|
||
- A panel is one `.ui-card`-family surface. A card on the room is an object
|
||
(`--surface-object` on `--elev-rest`, `--radius-object`, no border) and a group
|
||
or row inside it a tray (`--surface-tray`, `--radius-inner`); a tray inside a
|
||
tray drops its fill, never a second border weight.
|
||
- A section is: title (`--type-section`) → optional one-paragraph description
|
||
(`--type-body`, `--text-meta`) → content → optional note (`--type-meta`,
|
||
`--text-meta`). The description explains what the section decides; the note
|
||
carries consequences and caveats.
|
||
- **Order by importance and expected use.** Put first what a typical install
|
||
needs most often or what matters most, and last what most installs set once or
|
||
never: left to right for tabs, header actions, toolbars and pill rows, top to
|
||
bottom for sections, cards and sidebar entries. Decide from what the control does
|
||
on desktop, browser, Docker and phone installs, not from counts: the UI never
|
||
measures use and never reorders itself. Related sections stay together and move
|
||
as a group. The exceptions: the default tab is first; a setup flow keeps its
|
||
order (connect an account, choose models, assign agents); an ordered scale and an
|
||
owner-defined order (Widgets) are never re-sorted; destructive actions and About
|
||
come last, while an emergency stop (Panic) stays where people reach for it; Send
|
||
and Save keep their usual place. A familiar location moves only for a clear gain,
|
||
and a new section takes the place this rule gives it, not the top because it is
|
||
new.
|
||
- Subsections inside a section use a `--type-body` semibold heading and stay
|
||
visually grouped with their own rows, their own add action in the head
|
||
(List editors, below). A heading that floats equidistant between two groups
|
||
belongs to neither.
|
||
- **A disclosure shows that it opens.** Its trigger carries a visible open/closed
|
||
marker — the native triangle, the `▸`/`▾` pair, a caret or button/card chrome —
|
||
and reads as a control: a summary beside a help line at the same size takes
|
||
control ink (`--text-primary`) and its own line; one that already reads as a
|
||
control may stay in meta ink. Overriding `display` on a summary drops the
|
||
native marker, so draw it. What opens has one coherent boundary tied to its
|
||
trigger: what a bar discloses unfolds from the bar's bottom edge as its Shade,
|
||
and opening moves no neighbouring target. **Floating chrome stays readable:** over scrolling
|
||
content, closed or open, content never shows through its text, and state tints
|
||
sit on that backing. Text controls in one floating group share height and
|
||
baseline; width, tone and icon shape follow their role. The group reflows by
|
||
its own container's width without losing labels, targets or focus.
|
||
- Spacing comes from the 8pt tokens (`--space-*`); a new visual dimension
|
||
becomes a CSS variable before it becomes a page-local literal.
|
||
- An item in a popup menu or a picker list highlights with
|
||
`--menu-item-hover`. One gesture, one fill: a menu that highlights at a
|
||
different strength than the menu beside it reads as a different control.
|
||
- **Content text is always selectable and copyable.** A control may suppress
|
||
selection (`user-select: none`) only on its own label or chrome, never on
|
||
content it contains. Where content lives inside a click-to-toggle surface
|
||
(a task card's summary), the surface ignores a pointer click whose drag
|
||
produced a non-empty selection; keyboard activation is unaffected.
|
||
- **A markdown heading inside chat is never a page title.** In a full rich
|
||
answer (§1: answers, the Project completion mirror, `markdown: true` System
|
||
rows) it is a section of the answer and never smaller than the text it
|
||
opens: the §1 reading ladder — `#` = `##`, then `###`, then deeper levels
|
||
at the reading size — semibold, in the bubble's own ink, with clearly more
|
||
space above than below. The compact renderer's surfaces keep their density
|
||
(§2): in a Skill Review report every level is a `--type-body` semibold
|
||
label, and in a task card's timeline a heading renders inline, without
|
||
block margins, at its row's own size, with a copyable line break before
|
||
the following paragraph. Other rich-content cards (the question card, the
|
||
update letter) keep `--type-body` semibold labels. Bold text in chat
|
||
Markdown and rich-content cards is semibold, so a bold lead sentence never
|
||
outweighs a heading. The page-size `md-h1` belongs to non-chat surfaces
|
||
only.
|
||
- **Rendered Markdown keeps its author's structure and text.**
|
||
- A table stays a grid and fills its column. Cells wrap between words and
|
||
align to the top; the author's GFM column alignment holds (numbers right,
|
||
centred columns centred) with tabular figures; only a link, hash or code
|
||
span may break anywhere. In a full rich answer it reads at the answer's
|
||
text size; compact surfaces and cards keep `--type-body`. A table in
|
||
either renderer scrolls sideways only when its columns cannot fit even
|
||
wrapped, and only then is it a named keyboard region (`Scrollable
|
||
table`) whose sides that still hide columns fade. No stacked row cards,
|
||
no guessing a table's purpose from its words, no smaller text to make it
|
||
fit.
|
||
- A thematic break is a `--surface-border` hairline with section spacing.
|
||
Inline code keeps the ink of the text around it on its neutral pill.
|
||
- Code is the author's literal text: a fence or code span shows and
|
||
copies exactly what was written (`&`, `<div>`, `**stars**`
|
||
included), and no formatting rule reaches inside it. A rich answer
|
||
reads code as its Markdown parser does. The compact renderer keeps the
|
||
promise for its own narrower grammar only: a fence opened by three
|
||
backticks, an optional label made of ASCII letters, digits, underscores
|
||
and `+ # . -` (at least one letter, digit or underscore; `c++`, `c#` and
|
||
`objective-c` included), optional trailing spaces or tabs, and LF or CRLF,
|
||
closed by the next three backticks; and a code span of
|
||
one or two backticks around text on one line. Other code forms (info
|
||
attributes after a space, a span across lines, a longer backtick run,
|
||
a `~~~` or indented block) are outside that
|
||
grammar, and the compact renderer does not promise them literal. Rich
|
||
prose reads one stored entity layer (`&` shows `&`); an entity never
|
||
creates structure, and raw HTML stays literal text.
|
||
- A Markdown image is never loaded. It stays a visible reference,
|
||
`Image: <alt>`. In a rich answer it is a link through the chat URL policy
|
||
when its address is allowed, plain text otherwise, and inside an authored
|
||
link that link's own text; the compact renderer links it like its other
|
||
links (a refused address becomes their `#` placeholder). Chat media sent
|
||
through the media path is unaffected.
|
||
- **A narrow chat column gives every bubble its full width.** A column of
|
||
620px or less — a phone or a narrow Project panel alike — drops the
|
||
wide-screen gutters; the column, not the viewport, decides.
|
||
- **A task card's summary outranks its details.** The latest-activity line is
|
||
`--type-body`; collapsed timeline rows are a dense log at `--type-meta` in
|
||
`--text-secondary`; an expanded row returns to `--type-body` in
|
||
`--text-primary`. Details never render larger than the summary above them;
|
||
an inline label inside a row is semibold at the row's own size.
|
||
- **A nested child card is subordinate to its root.** Its compact identity row
|
||
shows status, role, notes and a chevron; a short task id disambiguates
|
||
otherwise identical siblings. Executor facts, the agent/coordinator model and
|
||
the effort chip occupy the metadata row, so the task's coordinating model
|
||
cannot masquerade as its external executor. The chip is one text part of that
|
||
line, composed from the child's own frames: the applied level (`Effort High`),
|
||
the asked level when it differed (`Effort High (asked Ultra)`), then its
|
||
source: a pin glyph before a pinned level, `· model name` for a level the
|
||
model name carries, `(Cyber Pro)` under Cyber Pro, nothing for the owner's
|
||
range. A frame without a level paints no chip; an older frame is never
|
||
back-filled. A child keeps one useful activity line visible, a root up to
|
||
three; empty activity reserves no band, and a duplicate title is not activity.
|
||
Full narration and Reviews expand independently. The root keeps primary title
|
||
ink at weight 500, children secondary ink at 400. Nested frames preserve real
|
||
ancestry; their opaque secondary surface avoids accumulating translucent white
|
||
tints at greater depth.
|
||
- **An executor label names the helper and states only exceptions.** The
|
||
ordinary case is the bare name — an assigned route beside the card's own
|
||
`Working` chip, or every settled run ok; it names the selected helper and is
|
||
not proof of current computation. A progress actor adds `last update`; a
|
||
refused pin, no run, runs not settled, failed or unverified runs, unreadable
|
||
evidence and an undisclosed outcome split qualify it. One priority phrase
|
||
stays visible; the detail names the other exceptions. Terminal records without
|
||
run evidence remain explicitly unknown; no promise of a later receipt replaces
|
||
it. Counts, the model and its source (requested, observed or `model unconfirmed`), spend
|
||
and access form one detail sentence that the expanded card prints as text,
|
||
never only a title. Settled observed models are separate historical facts,
|
||
not a claim that their union is the current actor. Missing identity stays
|
||
unconfirmed; marks and configured routes never manufacture execution.
|
||
|
||
A completion notice distinguishes the task's recorded end from the time the
|
||
notification was added, both as absolute local dates outside the answer's
|
||
copyable body; an unknown historical end time says so explicitly. Inside the
|
||
room, the task card's saved end line keeps its notification time and adds the
|
||
same note whenever the recorded end falls in another minute or is unknown;
|
||
minutes compare as instants, and two that read alike on the local clock (a
|
||
repeated daylight-saving hour) carry zone names. Lines saved before the host
|
||
recorded end times stay as they were. Delivery keeps its present place in Main;
|
||
an older Failed remains that task's result even after a different task succeeds.
|
||
|
||
### Document reading
|
||
|
||
A Markdown or plain-text file Ouroboros delivers in chat can be read in the app:
|
||
its card says `Read`, which opens a reading dialog over the conversation (on a
|
||
phone a full sheet), and Close or Escape returns to the same place in the chat
|
||
with focus back on the card. Leaving that chat's screen (another page, a Project
|
||
room over Main, a notification) closes the reader; files staged in the composer
|
||
stay. Other files keep the card's Open / Download dialog; the reader keeps Open
|
||
and Download too.
|
||
|
||
- It shows the delivered copy, never what a file path holds now. A copy that is
|
||
missing, failed its integrity check, changed after delivery or cannot be read
|
||
on this device says so in the reader, with Retry only where retrying can
|
||
help; nothing else is shown in its place.
|
||
- Name, type and size are the reader's header, not Ouroboros's words: a
|
||
document is not a chat message and adds none. Like model text, the name and
|
||
the document are never translated; the reader's own controls are.
|
||
- Markdown reads as a document: a single line break inside a paragraph is soft
|
||
(an explicit hard break stays), at the reading size and heading ladder of a
|
||
rich answer (§1). Everything else follows "Rendered Markdown keeps its
|
||
author's structure and text": raw HTML and SVG stay literal, images stay
|
||
references and never load, and a relative link, which would point into a
|
||
folder the reader does not open, reads as its words. `Source` shows the
|
||
decoded text unformatted (a leading byte-order mark dropped, invalid bytes
|
||
read as <20>), so it is not a byte view. Nothing is editable; all text is
|
||
selectable.
|
||
- Plain text reads monospaced and wraps; only a table or code block scrolls
|
||
sideways, inside itself.
|
||
- The reader reads at most the first 1 MiB. A longer file shows that prefix
|
||
with a note saying how much is shown and that Download has the whole; a
|
||
character cut by the limit is withheld, not reported as damage. Bytes that are
|
||
not UTF-8 show as <20> with a note; a file with NUL bytes is not shown as text.
|
||
A read that has not finished within 30 seconds stops and says so with Retry;
|
||
nothing retries by itself.
|
||
|
||
### History edges
|
||
|
||
An initial read shows one disabled `Loading saved history…` pill at the feed edge,
|
||
not a second loading sentence in the header. Task activity remains separate.
|
||
Persistent history notices are wrapping text in the room's header chrome: the
|
||
Project status bar without an opaque strip, Main's floating header on its own
|
||
reading backing (§5).
|
||
|
||
A room always opens at its newest message: Main and every Project, however long
|
||
ago its last message was and however much other rooms wrote since. Returning to
|
||
a room in the same app session opens it at its newest message too, Main after a
|
||
closed Project included (owner decisions 2026-07-10 and 2026-10-05). Only an explicit navigation to one place
|
||
lands elsewhere: a question opened from its Main mirror, or a row of the room's
|
||
work status, whose landing reads the page around that work as the room's history
|
||
from there on: `Load more history` continues older from it and `Scroll to latest
|
||
message` returns to the present. A window merely shown again keeps the reader where they were.
|
||
|
||
A room's history is its own. A page is counted in the room's messages, never in
|
||
bytes of other rooms' traffic: the newest page holds the room's newest messages,
|
||
and `Load more history` reads the next older messages of the same room, only
|
||
ever older ones (owner decisions 2026-09-14 and 2026-10-05). It shows only while
|
||
older history exists, and one press keeps reading until messages land or the
|
||
room's beginning is reached, so no press is empty. A positive scroll gesture at
|
||
the reading edge loads the same way. Messages arriving while the reader is in
|
||
older history never move where the next press goes on: it reads above what is
|
||
being read, and anything a long absence left unloaded below is read by the
|
||
return to the present. Narration (task progress) keeps loading
|
||
alongside the conversation, but only the conversation keeps the control: once the
|
||
room's beginning is reached it leaves, even if older narration of the oldest
|
||
cards remains unread. A page around work that only narrated pages that narration.
|
||
The floating `Scroll to latest message` is the one return to the present; when
|
||
the present is already loaded it moves there and follows without a read.
|
||
|
||
A failed history read offers Retry; a failed read of the present by ↓ leaves the
|
||
view in place with the same Retry. A failed full-output read keeps the line's
|
||
capped preview and offers no Retry of its own; collapsing and expanding the line
|
||
asks again. Reading on, even inside a bounded box, supersedes a ↓ or question
|
||
still loading. A wheel, swipe or key over a bounded box moves that box, not the
|
||
conversation, until the box reaches its edge. A scrollbar drag follows new
|
||
replies only when released at the live edge; a wheel, swipe or key reading down
|
||
follows once its scrolling ends there. Expanding a line or receiving its full
|
||
output leaves it in place; only a newer line moves a card timeline to its end.
|
||
New replies remain below in the same live conversation without moving the passage.
|
||
|
||
When loaded fragments are disconnected or their coverage is uncertain (reading
|
||
far back releases the newest pages from memory while replies keep arriving), the
|
||
same readable note stays in Main and Project header chrome: `Some saved history
|
||
is not loaded. Shown messages may have gaps.` A failed read says so distinctly.
|
||
Bytes written after a read found a source empty count as missing until a later
|
||
read delivers them from its start. Mixed task cards keep one node and use this
|
||
general note: dates, common row IDs and an exhausted cursor cannot establish a
|
||
separator or full coverage. `Beginning of saved history` requires the room's
|
||
conversation to be delivered completely. Retained origins say `Saved project
|
||
context`; a matching canonical source row adopts that node and removes the label.
|
||
The context itself certifies no archive coverage. When a window is shown again
|
||
and the reader's exact place is gone, the view falls back to its card, then the
|
||
previous clamped position, and the same persistent note explains the
|
||
approximation until explicit navigation clears it. Visible rows and card lines,
|
||
selection, focus and expanded Reviews keep their actual nodes during
|
||
reconciliation.
|
||
|
||
### Room work status
|
||
|
||
Each room's bar has one status, right after its title: the short word in its
|
||
status ink, then one mark, as on the sidebar's rows; no frame at rest, a neutral
|
||
tint on hover and while open, a caret when it opens. Live work moves three dots,
|
||
queued work holds three still ones, any other fact shows one. One producer
|
||
(`computeDerivedChatStatus`) gives the word, tone and dots together, in the
|
||
cards' short words; while a question waits for the owner's answer the mark turns
|
||
amber (moving if work runs), unless the word already says so. The title ellipsizes first, and
|
||
a new word never moves a neighbour or resizes the bar. When there is work to
|
||
show, pressing the status, or ↓/↑ on it, unfolds that room's work and only that
|
||
room's as one Shade under the bar, the same in every room: its census rows, the
|
||
owner's unconfirmed sends and unfinished cards the census has not confirmed. A
|
||
Project with nothing else keeps its latest represented root. A row names its work
|
||
(the card's coined name or title, else the owner's message) in two lines that
|
||
keyboard focus or open details show whole, with the state under the name in the
|
||
card's or census's own words: an outcome stays beside `Finalizing…`, an access
|
||
wait is not an answer wait, unknown stays unknown, a pause keeps its cause and
|
||
each paused remainder its own row. The name goes to the work's message wherever
|
||
it lies, finished work included: its card or the owner's message when loaded, else
|
||
the room reads the page of its history around the work's first message there and
|
||
lands on it, the list staying open while it looks. Before that a row claims nothing
|
||
about where its message is; when the room's history holds none (`No message yet.`,
|
||
or `No message saved in this chat.` once ended), belongs to another room, or cannot
|
||
be checked, its details say so. `View question`
|
||
goes to the exact waited question and answers nothing; Resume and ⋯ are that exact
|
||
activity's shared task controls, offered only while the census confirms it. While
|
||
open, order and focus hold, and work that ends keeps its row and outcome, without
|
||
controls, until close. Escape, an outside press or leaving closes it; `Back to
|
||
reading` returns to the passage left, reading its page again if it was let go, or
|
||
to the present the reader was following. Navigation never changes the next message's
|
||
recipient, opens another pane or manufactures activity.
|
||
|
||
### List editors
|
||
|
||
A list editor is any section where the owner adds and edits entries in place:
|
||
the Available subagents roster, MCP servers, custom keys.
|
||
|
||
- A section-level add action acts from its group's header (§6). A list editor's
|
||
new entry appears at the end of its own group, is scrolled into view (the
|
||
shortest distance, without animation) and takes the caret in its first field.
|
||
A button that stays in view while the entry it made is born off-screen has not
|
||
finished its job.
|
||
- A freshly added entry is an invitation, not an error. Where a list editor
|
||
validates in the browser (today the Available subagents roster), the entry
|
||
shows a neutral hint in its own meta line until the owner tries to save; the
|
||
error then names the entry and stands beside it (the entry tinted with the
|
||
status pair, never dimmed), with the section-level line as the summary. A save
|
||
attempt judges the entries that existed then; one added afterwards is an
|
||
invitation again.
|
||
- A per-entry on/off switch is a native `.ui-checkbox` leading that entry's
|
||
head, before its title, with its own accessible name and pointer target. It
|
||
is a draft like every other field — the section's Save is the one writer, and
|
||
no entry saves on its own click. An entry switched off is not dimmed, locked
|
||
or removed: it keeps every control editable and its own status reading, and
|
||
the switch never merges into the availability axis beside it.
|
||
- A multi-field card (an MCP server) follows the add-and-reveal rule without
|
||
adopting the §6 row anatomy.
|
||
|
||
### Reviews inside task cards
|
||
|
||
Real tasks and real subagents are cards. Reviews are a subsection of the
|
||
exact real task that owns their presentation. Harness and neutral API marks
|
||
identify the delivery channel alongside explicit execution evidence; they are
|
||
not child-task cards and never prove execution by themselves.
|
||
|
||
- A collapsed task card, and a collapsed nested child card the same way, shows
|
||
only a quiet `Reviews N` count, optionally with an active count, at the start
|
||
of its one action row under the metadata line (its controls right-aligned on
|
||
that row): no aggregate pass/fail alert, no synthesized verdict, no review
|
||
dollars.
|
||
- Expanding `Reviews` reveals one row per currently admitted review group
|
||
(`Skill review`, `Plan review`, or `Task acceptance`). Expanding a group
|
||
reveals its ordered attempt rows. Group state and verdict remain
|
||
domain-specific; one blocker never recolours the whole task card.
|
||
- Start progress labels the frozen model/route/profile as requested; settlement
|
||
reports that same slot's observed execution or says it was not reported. An API
|
||
model sent in a request is not an independently observed provider label, and
|
||
duplicate model slots remain distinct. No global last-run identity fills a gap.
|
||
- Disclosure is user-owned. Review results, retries, failures, terminal task
|
||
state, reconnect, and lazy-detail loading update content in place but never
|
||
open or close the task, Reviews section, or group.
|
||
- The Plan group is the plan record (nobody votes, so it carries no verdict
|
||
word): its line reads `N objections may hold` (an owner-marked reviewer's
|
||
objection, warning tone), `N open objections` (only added critics', warning
|
||
tone), `awaiting answers` (working tone while its task runs), `no open
|
||
objection` (done tone) or `not asked yet`; `objections not projected` in the
|
||
neutral tone when the detail lacks the host's fold, which the panel never
|
||
re-derives. The revision row shows the latest revision's goal, `done_when`
|
||
and earlier revisions' claims; each ask row reads `k of n answered` while
|
||
replies are owed and lists each participant (`added — never holds`,
|
||
`awaiting`, `not sent — <reason>`) and its objections (`(not adjudicated)`,
|
||
`(never holds)`). A record of the earlier schema keeps its wave rendering
|
||
below.
|
||
- An earlier-schema plan wave whose reviewers may still answer reads as work in
|
||
progress only while its own task runs: the working tone,
|
||
`in progress · k of n answered` where the verdict token would sit, and each
|
||
awaited reviewer listed as awaiting, not unavailable, under no verdict word;
|
||
once the task has ended the same wave reads `no verdict · k of n answered` in
|
||
the neutral tone. A reviewer whose window expired is unresolved. A settled
|
||
wave whose reviewers were too few for a verdict reads
|
||
`no verdict · k of n answered · m unavailable` in the neutral tone: the
|
||
verdict word `DEGRADED` is the host's placeholder and never paints.
|
||
- A task-acceptance panel whose reviewers may still answer never reads as a bare
|
||
verdict token. While its own task runs it is work in progress: the working
|
||
tone and `in progress · k of n answered`, or `PASS so far · k of n answered`
|
||
once the quorum is met; once the task has ended it reads
|
||
`no verdict · k of n answered` in the neutral tone, or
|
||
`PASS · k of n answered`, until a late settlement replaces it. A reviewer FAIL
|
||
keeps the error tone and reads `FAIL · k of n answered` in both.
|
||
- On a plan wave and an acceptance panel alike, a slot neither answered nor
|
||
awaited (a settled failure, an expired window, lost custody, a refusal) adds
|
||
`· m unavailable` and keeps the warning tone beside the awaited slots; a
|
||
settled plan wave with no awaited slot reads its `no verdict` line in the
|
||
neutral tone (an acceptance panel with none reads the same line but keeps the
|
||
warning tone, its stored `DEGRADED` staying in the attempt detail), and each
|
||
unavailable plan reviewer row names the model and quotes the engine's reported
|
||
sentence when one exists, the failure code staying in the task detail and
|
||
Logs.
|
||
- A plan wave the mind ordered weaker than the owner's effort setting says so in
|
||
its attempt detail as one keyed line naming each seat, the ordered effort and
|
||
the setting (`Reviewers ordered weaker than your setting: s1 low (setting
|
||
xhigh)`), at desktop and phone width alike; the verdict token is never
|
||
recoloured for it. A seat that did not answer a same-spec cycle adds `· did
|
||
not answer; its earlier finding is still listed` to its unavailable row (a
|
||
never-sent seat: `· not sent; its earlier finding is still listed`).
|
||
- An awaited or unresolved reviewer row adds `· since HH:MM` in the viewer's
|
||
local 24-hour clock, prefixed with the short date when the wait began on an
|
||
earlier day, only where the host recorded the moment it sent that reviewer's
|
||
request; a time is never inferred.
|
||
- A panel that settled after its task ended stays one attempt row of its group,
|
||
labelled as settled after the task ended; its note (which verdict, which
|
||
revision, whether a reviewer's outcome is still unknown) is host-composed and
|
||
printed verbatim, leading the attempt detail.
|
||
- Stable keyed rows are reconciled in place: a routine update preserves the
|
||
exact lazy-detail node, focused descendant and its reading position. Expanded
|
||
groups state exact aggregate accounting when projected, attempt detail exact
|
||
accounting when the domain can prove it, and otherwise each says
|
||
`Cost unavailable`; collapsed rows never show dollars.
|
||
- Harness marks are monochrome `currentColor` vectors with adjacent visible
|
||
text. They carry identity only, remain neutral across status states, and use a
|
||
generic text-preserving fallback for unknown harnesses; direct API is shown
|
||
neutrally as `API`.
|
||
- Vector provenance: Claude, Cursor, and OpenCode paths come from Simple Icons;
|
||
Codex/OpenAI comes from SVGL. Product names and marks remain the property of
|
||
their owners.
|
||
|
||
### Quiz card
|
||
|
||
The owner quiz card (`web/modules/chat_decision.js`, `.chat-quiz-*` in
|
||
`web/style.css`) is a chat-delivered decision surface. Optional clarification is
|
||
fire-and-continue: the task states its assumption. Required waiting keeps the
|
||
same card and says explicitly that the task awaits the owner, with Stop and
|
||
existing task deadlines still effective; an optional bound on that wait resumes
|
||
the task with a host notice and leaves the card open, the notice saying what the
|
||
card does (with a stated assumption the task proceeds under it; without one, no
|
||
answer is explicitly not consent). Silence is never an answer on either surface.
|
||
A card outlives its asking task: after the task finishes, the owner can still
|
||
answer, and the answer arrives as their own message in that chat. After
|
||
settlement, status and the owner's recorded answer keep both forms readable.
|
||
Anatomy, top to bottom:
|
||
|
||
1. **Head** — neutral `Question` chip (`--type-meta`, neutral pair) and a status
|
||
as dot + text. The lifecycle word family is closed and leads with the one
|
||
word that answers "is there an unanswered question for me?":
|
||
`Waiting for your answer` needs positive wait evidence (the task's live wait
|
||
record, or the original required flag before any record exists); a resumed
|
||
wait — owner input or other mail woke the task, or the bound closed — reads
|
||
`Unanswered · task continued`; an open question without any wait evidence
|
||
reads `Unanswered`; `Unanswered · task finished` keeps the neutral dot and
|
||
answerability (a late answer arrives as the owner's message); `You answered` uses the ok dot;
|
||
`Replaced by a newer question` and `Question closed` use the disabled dot; an unreadable source
|
||
reads `Status unavailable`, never an invented invitation. No answer-deadline
|
||
countdown: task completion closes its mailbox, not the question's answerability.
|
||
Pressing immediately shows `Sending…` and the brand-marked choice, keeping the
|
||
full form and focus; inert controls use subdued ink and submitted fields become
|
||
read-only. No success before canonical confirmation. `Not confirmed` means unknown,
|
||
not failed: only then show Check status, Retry same answer and Forget unconfirmed
|
||
local answer. Recovery first reads exact-quiz truth; explicit retry sends identical
|
||
bytes. Forget cannot cancel or retract a server answer; answered-with-delivery-owed
|
||
remains recorded.
|
||
While the card takes an answer the head ends with one quiet × (`Close question
|
||
without answering`): one tap closes it, no reason to type, and it waits while an
|
||
answer is typed, so a mis-tap never discards the owner's words.
|
||
2. **Question** — the one primary thing, by position and ink: readable
|
||
`--type-body` text in `--text-primary`, regular weight. Its emphasis is the
|
||
asker's own (authored headings and `**…**` are semibold), so a question of
|
||
several lines keeps a title instead of reading as one bold block. Its
|
||
paragraphs, lists, checklists, tables and code keep the shared rich-content
|
||
gutter, rhythm and bounded code scrolling. The card never infers a title from
|
||
the first line or rewrites authored Markdown to fit, and the question has no
|
||
quiz-specific length cap. Directly under it, a muted plain-text host line
|
||
(`.chat-quiz-host-facts`, `--type-meta`, `--text-meta`) states what only the
|
||
host knows: the asking task, how its run started and when the owner last
|
||
wrote in this chat, unknown facts said as unknown; without `host_facts` the
|
||
line is absent.
|
||
3. **Stake** — optional one-liner (`At stake: …`), `--type-meta`, `--text-meta`.
|
||
4. **Options** — zero to six real owner actions: elevated cards (the capsule surface
|
||
on the rest shadow with the object radius; a choice, never a field), the label in
|
||
`--text-primary` at 600; an optional per-option detail steps down to meta ink inside
|
||
the card. Hover and press change the card's colour. After settlement they lie flat
|
||
in `--text-disabled`; the chosen option takes the ok pair, its detail the meta ink. Options are capped by the shared Python↔JS constant
|
||
(`MAX_QUIZ_OPTIONS`); with none, the free answer is the whole answer.
|
||
5. **Free answer** — while the card is open, a compact always-visible field
|
||
(`Your answer or comment…`, the field's hairline) with a `Send my answer`
|
||
crimson capsule, enabled and standing only once something is typed; it is a message field, so Enter sends and
|
||
Shift+Enter breaks the line (§3 "Controls and editable choices"). No option
|
||
ever has to be the least wrong one: the text rides with an option click as
|
||
the owner's remark, or goes alone as the owner's own answer. It uses the
|
||
card's own ink and surface tokens (never the legacy chat input), is capped
|
||
by the shared Python↔JS constant (`MAX_DECISION_COMMENT`), and disappears
|
||
the moment the card settles.
|
||
Origin-local drafts and frozen attempts persist across closure only with a
|
||
persistent browser profile (older private WebViews erase them: Appearance above).
|
||
Restore reads, never resends; storage failure warns to keep/copy words.
|
||
**Clear local draft** removes unsent words, not submitted answers. Canonical
|
||
settlement clears the copy; winning bytes appear literally as `Owner's answer: …`
|
||
under the options, beside the highlighted choice or as the whole answer if none,
|
||
retaining line breaks (`--type-body`/`--text-primary`).
|
||
6. **Assumption or waiting** — the signature line (`Continuing meanwhile: …`
|
||
for optional clarification, an explicit waiting statement for required input),
|
||
`--type-meta`, `--text-meta`, parted by space. While the card is
|
||
open it names the default path. The optional assumption remains after settlement
|
||
as the record of work continued meanwhile. Required waiting copy disappears
|
||
when the card settles; the status and recorded owner answer remain.
|
||
|
||
Inside a bubble the card adds no box: the bubble is its object. Its × is a quiet
|
||
control, without a body until hover. The card uses tokens for every
|
||
type size and colour, including pill radius and option gap. Every focusable element shares the 2px `--focus-accent-border` ring
|
||
with 2px offset. Component min/max widths keep local literals, as elsewhere in chat.
|
||
|
||
**Project question mirror.** Main is the list of what needs the owner; nothing waits in a section of its own. A Project question's Main copy is the Project's own `buildQuizCard` card, whole as anatomized above, in an assistant bubble at its ask time in the feed; the Project reference beside `Question` opens the exact original, and answers, materials and recovery controls work in Main. It uses the shared ring and `--project` tints; names ellipsize (full titles) before status; narrow heads wrap. History, live pointers and the census share one task/quiz card and lifecycle wording; the canonical current questions only confirm or settle copies the feed holds, never mount them, and an incomplete read withholds only what its typed gaps name. Optional, resumed and terminal-author questions stay answerable until answered or closed; no display/history cap drops them. Replaced questions stay read-only. The owner closes from the card; a model closes its own or a host-proven continued task line's question, or a finished task's, always with its own reason. Closure records a reason, never an answer/approval. A late answer left delivery-unknown by a restart offers Remove from pending (no resend, never marked delivered). Publication destination stays fixed across Project conversion; current room membership controls display. Explicit materials use registered task artifacts and the existing reader.
|
||
|
||
A task that cannot continue without the owner is one System message in the feed, stamped when first seen: the task, one sentence, its own controls (revision-bound model access, Resume/Stop, children's included; activity counts stay root-only). Sign-in, unobservable automatic access, disabled automatic continuation, budget pauses and Resume-only holds qualify; viable automatic account/quota recovery, queues, self-waking sleep, background scope verification and own Pause stay quiet. Deduplicate the same quiz/wait identity, not all waits on a task; the message leaves when a complete census drops the row. Missing action evidence stays unavailable; visibility never authorizes login, budget changes or automatic Resume.
|
||
|
||
One amber pill among the composer's pills (their row, height and type, at its end; `--status-warn-*`) counts the unanswered questions and owner actions — `1 question`, `3 questions`, `1 action`, `4 waiting` — and shows only while one of them is off screen; a narrow row keeps the glyph and number. Each press shows the next, oldest first, cycling: the card head lands under the chat header and the question text takes focus, never the answer field, so no touch keyboard opens. A question outside the loaded history mounts at its own date under the existing history note; there is no history seek.
|
||
|
||
A confirmed answer/closure shows its result for five seconds, then retires only the Main copy through message/media disposal without moving the viewport; the Project card stays. Pending canonical delivery keeps the read-only form and a compact pending fact. The interval starts once, including simultaneous form/result delivery. Focus remains during it, then moves to the next Main question or keyboard owner's composer without summoning a touch keyboard. History, reconnect and stale snapshots cannot resurrect settled forms; a settled pointer never mounts unless its delivery is owed. A newly accepted question closed before first exposure publishes only its closed state; an old answer without explicit delivery debt creates no new owner turn.
|
||
|
||
Unavailable canonical refresh retains text, references and drafts but disables affected controls. Missing, failed or wrong-Project reads say `Status unavailable`; absent form text says `Open the original question for its text.` The chip opens the original; owned refresh retries.
|
||
|
||
**Work portal.** A work living in a Project room keeps its one Main card at the
|
||
request's place, whoever moved it (Turn into project, the mind's move, a
|
||
binding); a Project start or transfer row only says where it went, never a
|
||
second entry. The card is the work's portal: status chip and quiet fact, title,
|
||
one latest line (two lines, cut on a word), the meta line (tool calls · money ·
|
||
updated), one action row `[Reply] [Resume] [Stop…]` with one primary, and the
|
||
Project reference as its full-width footer. Timeline, Reviews and children live
|
||
in the room; the summary is reading content, not a disclosure. The room's census
|
||
row supplies the present (its last line, tool calls, `in background` for
|
||
detached work); a missing fact leaves the card as it was, never a guessed line
|
||
or count. When the work ends the line goes and the chip states the outcome from
|
||
the final's typed outcome or the durable detail. An undurable Main receipt keeps
|
||
a dashed border and a plain warning; the binding holds. The owner's routing line
|
||
under his message steps aside while that work's portal stands in the feed; words
|
||
passed on to the work later keep theirs. A Continue is the same card: the
|
||
admitted successor takes the work's place and first name, each episode one meta
|
||
line (`Episode 1 · 17:33–18:15 · Failed: <cause>`), the current one owning the
|
||
chip.
|
||
|
||
**Project completion mirror.** Eligible managed/transferred Project finals reach Main as Markdown in Ouroboros's voice, host-stamped only for a model-authored final, so the browser never infers authorship; ordinary Project-native replies stay local. A long answer folds to about seven lines of complete selectable text, faded only where text is hidden; folded text has no keyboard stops. The Project reference below opens the complete answer and its delivered documents; Main has no in-place expansion or Files inventory. A nonclean final carries one compact factual limitation from recorded outcome/acceptance beside the answer, wrapping legibly at narrow widths; a clean final has no status caption. Copy copies the answer alone. Ordinary document readers, quiz materials and task-card Files stay on their existing surfaces; retained availability is not delivery or owner receipt. Full reviews, logs and progress stay in their sources. Durable rows/wire frames stay `role="system"` with unchanged plain text, so the Telegram mirror and Ouroboros's own context behave as before; no-answer endings and historical rows without an authored answer keep System voice and the Project reference. One logical identity joins retries/history and keeps one-ending-one-notification; external transport stays at-least-once.
|
||
|
||
History with no current execution or known outcome keeps its expandable content under `Outcome unavailable`, without a task chip, typing or Stop. Before complete live-source reconciliation, it is `Activity unconfirmed`. Positive current activity restores only its proven controls. A delivery warning may coexist with a preserved task-acceptance PASS. Model metadata says `Last solve response`, naming the initial request only when the route changed.
|
||
|
||
History placement is separate from the task's outcome: ordinary
|
||
`Saving task history` and `Task history saved` appear only in task details and
|
||
Logs, and a storage failure adds `History storage problem` to the existing
|
||
collapsed-card metadata without changing Done/Failed/Cancelled or creating a
|
||
chat message (successful retention clears it). Details and Logs name the
|
||
recorded failure reasons, repeated ones grouped with their counts; Logs Raw
|
||
keeps the complete source/failure facts, and a missing recorded reason is stated
|
||
explicitly. Existing task-detail hydration and replay refresh this projection.
|
||
|
||
### Chat attachments
|
||
|
||
The owner's photos and files are part of the owner's message, shown the way
|
||
Ouroboros's own deliveries are: one message, one bubble, never one bubble per
|
||
file (owner choice 1C/2A/3A/4A/5A/6A with mobile first-class).
|
||
|
||
- **Order:** attachments sit above the caption inside the owner's bubble; a
|
||
message with no words shows its attachments alone, never an empty text row.
|
||
- **Photos are whole on both sides.** One photo keeps its own ratio up to
|
||
`--chat-photo-max-height`, never enlarged; a tiny, very narrow or very short
|
||
one sits inside a `--chat-photo-min-box` box so its `•••` is never clipped
|
||
(either side); several share the existing two-column grid with
|
||
letterboxed (`contain`) tiles, one column in a narrow chat column. Ouroboros's
|
||
own photo tiles letterbox the same way — no side crops an image.
|
||
- **Video and audio** play in the existing inline players; every other file is
|
||
the existing file card (no PDF viewer). A preview the engine cannot show (HEIC
|
||
in Chromium, an unsupported codec) becomes an ordinary card with Open/Download
|
||
and "Preview unavailable"; a file deleted since it was sent, or whose size or
|
||
mtime no longer matches the recorded stat witness (a replace or rewrite that
|
||
changed either), is the inert card "Unavailable". The witness is not a hash and
|
||
nothing rehashes per view: a same-size replacement that restores the mtime is
|
||
not detected and shows as the original.
|
||
- **Actions never depend on hover:** the photo `•••` and file cards are always
|
||
visible and finger-sized under a coarse pointer; a long name wraps to two lines
|
||
and the card's dialog shows it whole. Open/Save succeed only when the desktop
|
||
bridge says so; a copied link is a fallback, never reported as opened.
|
||
The photo's Copy to clipboard puts the image itself there or says it could
|
||
not (Open in new tab and Download stay); it never copies an address instead.
|
||
- **Handing a file to a task adds nothing on success:** it arrives in the task's
|
||
own message. A file the host could not attach is named in one wrapping line
|
||
right under the owner's words ("Not attached to the task: …", from the routing
|
||
receipt, live and on replay, never folded away in Main), and Telegram gets the
|
||
same line. A new turn whose every file was refused is the System mark "Not
|
||
started: the attachments could not be staged (N files)"; one that starts with
|
||
only some of them says so in its own words.
|
||
- **The same view everywhere:** the sender's bubble, another tab and the replay
|
||
after reload or restart render the one server view of each attachment. The
|
||
stored text still names the files for the model; only the exact tail the web
|
||
composer generated, on its own row (or text the row marks as host-written,
|
||
`text_placeholder`, such as "(image attached)", a mark the echo carries too),
|
||
is hidden from the caption: the owner's words, even those same ones, and a
|
||
Telegram or skill caption are never rewritten. Every attachment renders,
|
||
however many; only the text tail is bounded. A Project's start message copied
|
||
from Main shows the attachments its row recorded when that row is outside the
|
||
window. History without recorded attachments stays as it was; nothing is
|
||
guessed from a file name.
|
||
- **Composer:** each staged image shows a small thumbnail beside its name; the
|
||
field stays focusable (read-only, not disabled) while files upload, so a phone
|
||
keeps its keyboard. `/restart` typed with staged files stays the Restart
|
||
command (the files ride its row); any other words are a message. A sent
|
||
message (words or files; a bare slash command is a command) is not yet saved:
|
||
if the socket closes, or the host refuses the frame, before its "Input saved"
|
||
echo, the bubble says "Not confirmed as saved" with "Send again", which
|
||
resends the same frame and id (the host rejoins it if it was saved after all,
|
||
never a second message), and "Discard", which forgets this tab's copy (never
|
||
an upload). Nothing resends by itself. The frame is kept, with no count bound,
|
||
until that id's saved echo or history row settles it (a row the running host
|
||
has not yet dispatched keeps it until the host says which): a bubble the feed
|
||
released or rebuilt keeps its frame, and a Project room holding one is hidden,
|
||
not destroyed. The tab's sessionStorage keeps its words, id, routing and
|
||
upload references (never file bytes) across a reload (the asset reload a
|
||
reconnect forces included) and a room's teardown; after a reload the first
|
||
history read settles it or shows the same doubt. A browser that refuses to
|
||
keep it says so, and Send again then lasts until a reload; a kept copy it
|
||
cannot read back is said too, never silently dropped. A saved row the host
|
||
knows never reached the agent (its write failed before dispatch, in the
|
||
running host) says "Saved, not delivered." and keeps Send again: the host
|
||
hands that row over once. A kept message whose saved row a host process before
|
||
a restart took says "Saved; delivery not confirmed." with Send as new, which
|
||
only puts the words back in the composer as a new message: nothing is
|
||
replayed. Rows nobody kept stay plain history ("Input saved").
|
||
|
||
### Replies
|
||
|
||
A reply is a reference: the owner says which row he means, Ouroboros decides
|
||
what to do, and nothing routes by it. Every saved row offers Reply (beside Copy
|
||
on a bubble, first in a root work card's action row, in the row menu), never on
|
||
hover only. Reply stages one strip above the composer (`Replying to`, the quoted
|
||
author, one line, ×; Esc cancels); the sent message carries only the row's
|
||
address, and the host makes the snippet. An answerable question card answers in
|
||
its own field. The quote panel is one component (bubble, strip, relay): above
|
||
the text, a 2 px bar and a tint in the quoted author's ink, one line cut on a
|
||
word (a photo leads with its thumbnail); pressing it goes to the quoted row when
|
||
loaded. An Ouroboros answer quotes the message it answers only when that message
|
||
is not directly above it (its own card does not count). A relay arrives in the
|
||
work's room as one Ouroboros bubble: each owner message it passes on, in his
|
||
complete words with where and when he wrote them (six lines, then Show all;
|
||
files named, a captionless file by name alone), then Ouroboros's note (Telegram:
|
||
plain text); his Main message keeps its one receipt line. The row menu
|
||
(right-click, Shift+F10 or the ContextMenu key on the focused row, a long press
|
||
on touch) offers the row's own doors: Reply, Copy, Open project and, on a work
|
||
card, Resume, Pause and Stop… exactly when the card does; Esc or an outside
|
||
press closes it, focus returning to the row. Words the owner selected keep the
|
||
browser's menu.
|
||
|
||
### Conversation activity block
|
||
|
||
A task's activity block is in the transcript exactly when the record already
|
||
holds something to show — one predicate (`web/modules/chat.js::blockVisible`),
|
||
re-read at every mutation, with no sticky flag: no kind is shown
|
||
unconditionally (a Presence turn and a consciousness wake-up are direct turns
|
||
and follow the same rules as any other); open owner attention (a model wait, a
|
||
pending stop, a host-attested Stop the record still offers — the same reading
|
||
the control uses, so a block never stands on a Stop it hides); a child card; a
|
||
review group; a content row; a terminal outcome other than Done. The completion note is not content:
|
||
a turn that ran no tool and finished Done leaves no block — live, after a
|
||
reload and after a reconnect.
|
||
|
||
Narration leads; routine execution evidence stays compact; exceptions keep their
|
||
explanation and controls. The block's title, collapsed activity line and
|
||
timeline carry the turn's own narration (progress frames the model authored,
|
||
marked by the typed `narration` fact the worker stamps at that one producer; a
|
||
frame without it is legacy and read as narration) and whatever needs the owner's
|
||
eyes: a failed or timed-out step, a wait, a review, a child. Host notes on how
|
||
the turn runs (a checkpoint, a model fallback, a review verdict, a nudge) are
|
||
visible timeline rows that never claim the title or the collapsed line, so a
|
||
turn with only host notes keeps its coined or task name and an empty activity
|
||
line. A task that ends with a cause states it on the collapsed line in the
|
||
owner's words (the sentence of the durable row and the expanded body), so a
|
||
`Done with warnings` chip never stands beside an unrelated last action; a clean
|
||
ending keeps the last narration. Successful tool calls are not rows: they fold
|
||
into ONE evidence row per block (`N tool calls`, `N tool calls · M errors` once
|
||
a call failed) at the first call's position and time, patched in place; Expand
|
||
shows per-tool counts (`read_file ×3 · web_search`), and the phase (`calling`
|
||
while a tracked call runs, `warn` once one failed, `result` otherwise) shows in
|
||
ink, not words. A failed or timed-out call keeps its own diagnostic row and
|
||
counts once. A completed check command whose recorded criterion did not pass
|
||
keeps a separate expandable warning, “Check did not pass: …”, adding no call
|
||
error or task failure; its first sentence tells the check process from its
|
||
criterion, and its details name both and disclose bounded output. Live,
|
||
post-terminal and replay share that invocation-bound row; a later passing check
|
||
does not erase the earlier failure, and a comparison without a verdict is
|
||
unconfirmed, not a proven mismatch. On replay, recorded call times place the
|
||
evidence row before its diagnostics; a missing start permits only an
|
||
observed-time or legacy carrier fallback, not a reconstructed first call. Wait
|
||
end and operation settlement are independent: late success retires the
|
||
provisional timeout notice but keeps “wait ended” in the evidence row, and late
|
||
failure keeps its operation error, in either arrival order. A historical start
|
||
alone means outcome unknown, never Running or Failed.
|
||
`web/modules/chat_activity.js::toolEvidenceView` builds the row live and on
|
||
replay. Host invocation IDs join start/wait/settlement; legacy observations
|
||
without sufficient identity stay separate even with matching names and
|
||
arguments. Host metrics fill absent counts field by field; canonical
|
||
per-invocation evidence reconstructs later settlements on history/reconnect
|
||
without resurrecting frozen wait errors. Reads are bounded and carry coverage;
|
||
absent evidence is no proof of success. Start-only (live or legacy) and
|
||
incomplete replay evidence keep a known aggregate error; only complete
|
||
settlement evidence can replace an earlier wait error. Typed tool evidence after
|
||
task terminal updates counts and diagnostics on root and child cards, keeping
|
||
the terminal task phase and controls. The row keeps its live position; history
|
||
admits carrier evidence before summaries, progress or references choose their
|
||
presentation. Block presence is consistent across reload and reconnect. A moved
|
||
work lives in its Project, its Main card the work's portal ("Work portal").
|
||
`N notes` in the collapsed header counts timeline items, the evidence row among
|
||
them. A threshold crossing names its bound and threshold without strategic
|
||
advice; distinct bounds and thresholds get distinct rows, and historical
|
||
producers without a named bound keep their old label.
|
||
|
||
Delegated executor activity keeps its own attributed voice in the root or child
|
||
timeline. Messages lead; thinking stays folded in its own details, and routine
|
||
technical events collapse into counts with per-event detail under Expand.
|
||
Problems, unread ranges and omitted previews stay visible with a full-source
|
||
reference. Preview limits include serialized size; they never erase the carrier
|
||
or its source. Overlapping reads deduplicate by sequence identity, retaining any
|
||
unseen tail's omission notice; equal words alone are not duplicates. A stream end
|
||
before the observed cursor states unresolved coverage. Live, reconnect and replay
|
||
use the same projection. Telegram reserves room for problems and incompleteness
|
||
beside its disclosed speech preview and keeps the complete task reference.
|
||
|
||
The block's chrome follows the work it stands on
|
||
(`web/modules/chat.js::blockHasWork`, the presence facts minus open attention
|
||
and a bare terminal outcome), never the lane that ran the turn (owner decision
|
||
16.09: real work is a task card, a greeting is nothing). A block with work (a
|
||
review group, a child card, an evidence or narration row, a tool error) is the
|
||
task card, whether a managed root or a direct conversation turn produced it: a
|
||
title (the coined name or the latest narration headline; a finished card with
|
||
neither reads `Task activity`, a running one stays untitled because its chip
|
||
already says Working), the status chip, Stop while the host attests it, and
|
||
`Turn into project` in Main unless its origin is already bound (a direct turn's
|
||
later rows then route to the Project room like a work that called
|
||
`move_to_project`); its meta line and one action row ([Reply] first, then Turn
|
||
into project, Resume, Stop…, one primary) form its foot. A nested child card is
|
||
work inside its root's block, never a block of its own: it carries neither
|
||
control, and its root's conversion converts the whole work. A block that exists
|
||
only for open attention (a model wait, a pending or host-offered Stop) or only
|
||
for a non-Done ending of a turn that did no work carries no title placeholder
|
||
and no conversion; its chip says its state (Waiting…, Cancelling…, Failed), the
|
||
wait controls stay, and its first row of work gives it the title. The collapsed
|
||
header carries the tool count live and, once the turn ends, cost and duration (a
|
||
replayed header carries count and cost; duration is a live fact); its `updated`
|
||
stamp follows the turn's own narration, never a host note or a tool call. Money
|
||
there is ONE line whose wording carries its openness and never renders an
|
||
unknown as a number: `$0.00` only where priced rows evidenced that zero,
|
||
`up to $1.20` while the tracked subtotal is inexact, `Tracked: $1.20` when part
|
||
of the same scope has no price (`Tracked: up to $1.20` when that subtotal is
|
||
inexact too), and `Cost unknown` for unpriced rows with no tracked amount. An
|
||
empty, intact ledger has no money line; an empty view with an integrity gap
|
||
stays unknown; neither proves a free result. The chip's short word (`Sending`,
|
||
`Queued`) and `Tracked:` are explained in the card's details (exact phase;
|
||
`some steps have no price`), not a tooltip; real ` · ` text separates meta parts
|
||
for copies and assistive technology. The host's `_is_direct_chat` fact keeps its
|
||
host jobs (routing, census `kind`, Stop custody, terminal rows) and, on the
|
||
client, only the header pill (a direct turn keeps the census verdict beside its
|
||
block). A block that existed only for open attention leaves when that attention
|
||
closes: a wait-only block disappears when the wait resolves, and the resolved
|
||
episode's no-reopen ledger survives with the record, so a stale revision cannot
|
||
bring it back.
|
||
|
||
An addressing call (`start_task`, `move_to_project`, `tell_task`: the
|
||
routing-verb family `ouroboros/tool_capabilities.py::ROUTING_VERBS` owns) is
|
||
stamped by the host on its live tool-call frames (`routing_action`) and counted
|
||
in the task metrics (`routing_tool_calls`); its receipt is the typed routing
|
||
annotation on the owner's message. Such a call is a receipt row: it renders
|
||
inside a block that exists for other reasons but is never content the block
|
||
stands on, and the evidence row of a turn whose calls were all addressing calls,
|
||
without error, is a receipt row too (live from the stamped frames, on reload
|
||
from `routing_tool_calls`). So a turn that only addressed work («turn this into
|
||
a project») draws no block, live or on reload: the annotation on the owner
|
||
message and the managed root's own card are its whole record (owner decision
|
||
11.09). Successful local completion acts get the same receipt semantics through
|
||
the host's `completion_control` fact and `completion_tool_calls` aggregate,
|
||
keeping their actual call count without manufacturing work; failed completion
|
||
calls keep their error. A failed addressing call is an error row and therefore
|
||
content, as is any recorded tool error. A REFUSED addressing act is told where
|
||
the work lives and never in Ouroboros's voice (owner 16.09): the receipt line
|
||
states the cause in the owner's words, the failed call stays the error row
|
||
inside the block, and the tool result carries the typed reason (with the cause
|
||
and repair in `detail` where the producer holds one) so the model narrates; no
|
||
host bubble interrupts a narrating turn. When the host itself issued the act (a
|
||
Swarm message, a skill-card repair, a picker click), or a refused owner steer
|
||
carries no owner-message receipt, the host states the refusal as ONE typed
|
||
System row (`task_not_started`, `task_start_unconfirmed` for unconfirmed
|
||
admission, or `steer_not_delivered` for steering) in the issuing chat, keyed to
|
||
the named target. The Project start row is announced only once the task is
|
||
really queued. A refusal receipt with neither options nor a cause sentence reads
|
||
«Not routed», never «Choose a target». No client list of tool names decides
|
||
presence (`docs/development/02-naming-and-boundaries.md`, "an open default
|
||
behind a closed exception list"), and no client reading of a note's text decides
|
||
whether it is narration. A change that draws one row or line per event is judged
|
||
at a realistic burst size (a multi-call turn, collapsed and expanded, at desktop
|
||
and phone width), never at a two-event fixture.
|
||
|
||
### Subscription waits inside task cards
|
||
|
||
Quota exhaustion and a confirmed need to sign in again use the same component,
|
||
`model_wait.js` with `model_wait.css`, inside the turn's existing host: the task
|
||
card of a turn that has done work, the bare block of one that has not. Each
|
||
waiting role has its own row; model, account and reason are separate facts, and
|
||
the controls stay visible when the task's timeline is collapsed. Waiting carries
|
||
a quiet warning status and no computation animation, typing, activity counter or
|
||
invented progress. Completed steps remain intact; a held worker slot and its
|
||
effect on the queue are stated only when the host reports that fact.
|
||
|
||
The quota row offers automatic continuation, initially enabled, and shows a
|
||
known reset time or an explicit unknown; an authentication row instead offers
|
||
the existing Accounts sign-in flow. Both can open Settings, retry explicitly, or
|
||
choose a replacement model/account in the shared model-role editor. The
|
||
replacement affects the named waiting role until its task or review operation
|
||
ends; an unchecked "Also save this role in Settings" checkbox separately
|
||
requests persistence. Fallback Local stays shared by its Settings group:
|
||
changing Local for one waiting fallback makes the replacement task-only, its
|
||
persistence checkbox cleared and disabled with an explanation while Apply
|
||
remains available. Model and account changes can still be saved when the shared
|
||
Local value is unchanged; permanent group-wide Local changes remain in Models.
|
||
|
||
A pool confirmed to contain both accounts requiring sign-in and accounts waiting
|
||
for quota says "Waiting for access" and names both causes. It keeps automatic
|
||
continuation and opens Accounts without selecting a profile or initiating login.
|
||
A dated pool refusal without a confirmed cause says "Waiting for an account",
|
||
retains the engine's reset forecast and keeps automatic continuation without
|
||
claiming quota exhaustion or guaranteed availability. It never offers sign-in.
|
||
Only confirmed quota pauses the execution clock; authentication and unconfirmed
|
||
availability waits consume it. Calendar deadlines remain fixed. Configured
|
||
fallback routes are tried before a wait card is shown. Work continuing on a
|
||
fallback is not a wait: nothing claims the primary recovered, the model alone
|
||
asks to return, and only the primary's own accepted reply counts as a return.
|
||
|
||
A submitted action is shown as pending until the task reports its application; a
|
||
saved Settings change and a still-pending task change are disclosed separately.
|
||
When a requested Settings save failed or is unconfirmed, resolving the wait
|
||
shows one toast with those facts; closing its card without resolution announces
|
||
nothing. Retries preserve the original request identity and payload. Revisioned
|
||
rows reject older observations, and resolved episodes never reopen. Terminal
|
||
authors retain exact live paid-review controls without reviving author status;
|
||
other waits end. A mailbox delivery failure keeps Retry request available for
|
||
that same accepted command, even while its application remains pending. The
|
||
current task attempt selects live actions; a previous attempt's retained pause
|
||
never makes a new working attempt appear to wait. Updates and history rebuilds
|
||
preserve the same keyed editor, its draft, persistence checkbox, focus and
|
||
reading position. Wait pickers omit Context controls on every render: their
|
||
action changes model/account only. Closing the chat disposes view resources
|
||
without claiming it stopped the task; continuation requires the Ouroboros
|
||
process to keep running.
|
||
|
||
A consciousness wake-up needs nothing of its own here: it is an ordinary direct
|
||
turn with its own task id, so its model wait, controls and temporary model
|
||
change behave exactly as an owner turn's and last until that turn ends; the
|
||
persistence checkbox still saves the consciousness role. The origin label
|
||
`Consciousness` identifies the turn in the block's meta line, on its final
|
||
bubble and on the cards of tasks it started, never a separate card, a reused
|
||
slot or a different vocabulary.
|
||
|
||
An already-delivered answer does not close a still-open post-task synthesis.
|
||
Consolidation or chooser waits use the same role controls in that task's
|
||
existing finalizing card. Pooled tasks, including API-only tasks, keep their
|
||
worker slot until post-work settles; the answer arrives early. Ordinary native
|
||
chat post-work holds no worker slot; its existing card keeps the same live
|
||
post-task model-wait controls after ordinary dialogue admission closes. Both claims follow the host's live owner
|
||
and post-task checkpoint, not the presence of answer text or a cost estimate.
|
||
Failed main work stays visibly failed after history reload while post-work
|
||
controls remain live; the unfinished checkpoint never erases the outcome.
|
||
|
||
### Widgets board
|
||
|
||
The Widgets page is a board of live cards. The owner arranges the board; each
|
||
widget's content belongs to its author.
|
||
|
||
- **A masonry in the owner's order.** Cards are packed into columns like a
|
||
Pinterest board: in the order the owner set with each card's move handle,
|
||
every card goes to the lowest free place its width fits, so cards of different
|
||
heights stack without a row layout's voids. The board takes its column count
|
||
from the list's width and the widths its cards ask for; a board where the
|
||
owner set no width is exactly the board before owner widths existed. A new
|
||
widget joins the end of the order; the order is stored when the owner moves a
|
||
card, so after a window reload the widgets added since the last move follow
|
||
the stored ones alphabetically. A widget whose skill is off is not drawn; its
|
||
place in the order (moving the other cards keeps its slot) and its width stay
|
||
stored for its return, and nothing is reserved meanwhile.
|
||
- **Width is a column span, the owner's over the author's.** A card spans 1,
|
||
2 or 3 columns, or Full width. Until the owner picks one, the author's `span`
|
||
decides (`1` one column, `2` two), and the board may widen a lone one-column
|
||
card beside wide ones to fill its lane. The board never widens or narrows a
|
||
width the owner chose, except that no card is wider than the board: 3
|
||
columns on a board of 2 is the whole row. Full width is always the whole
|
||
row. Because the column count follows the widths the cards ask for, a
|
||
card's width can make the other cards' columns wider or narrower. The
|
||
owner's choice holds for that card on every screen of the installation
|
||
until Reset size returns the author's default.
|
||
- **Widths that do not tile leave gaps; the board does not backfill.** A
|
||
card that cannot fit beside the cards above it starts below them: a
|
||
3-column card after a 2-column one on a 4-column board leaves the column
|
||
beside the first card empty above it. The board never repacks cards out of
|
||
the owner's order to hide such a gap; the owner closes it by changing a
|
||
width or the order.
|
||
- **Height follows content.** A card is as tall as its widget: a table shows
|
||
every row, a framed widget its declared or reported frame height
|
||
(`docs/CREATING_SKILLS.md`). No card is pinned to a height. When a card
|
||
grows or shrinks, the masonry packs again, so cards after it can move,
|
||
even to another column.
|
||
- **A narrow list is one column.** When the list is too narrow for two
|
||
columns of 280px — a phone, the Telegram mini app, a narrow window — the
|
||
cards stack at full width in the same order and widths do not apply; the
|
||
board returns when the list is wide again. The list's own width decides,
|
||
not the viewport.
|
||
- **The card menu works on every surface.** Every card's ⋮ menu carries Size
|
||
(1, 2, 3 columns, Full width and Reset size) beside a framed card's launch
|
||
policy, so a touch screen, a keyboard or the mini app — no hover there, and
|
||
a vertical drag collapses it — can size any card; on a narrow list the menu
|
||
says that widths apply when the list is wide. Wherever a step would change a
|
||
card's width, its right edge can also be dragged between the widths the
|
||
steps give it (Escape cancels) or stepped with its arrow keys; the only card
|
||
on a board, which every step leaves the whole row, offers no edge.
|
||
- **Arranging never restarts a widget.** Reordering, resizing or a change of
|
||
the column count changes only where a card is painted; a running widget
|
||
keeps its frame and its state. Keyboard focus keeps the card's original
|
||
position until the window reloads.
|
||
- **One arrangement per installation.** Order and widths are owner UI state
|
||
stored on the server, the same for every window and device of the
|
||
installation; when two windows arrange at once the last change wins. A
|
||
width that failed to save stays on screen and says so under the list until
|
||
a later width change saves it; nothing retries on its own.
|
||
|
||
## 6. Account group / row anatomy
|
||
|
||
For a repeated identity row (a connected agent account, a subagent row,
|
||
a server entry):
|
||
|
||
1. **Classification chip** — neutral pair, `--type-meta`. Only where the row's
|
||
family is not already expressed by the group it sits in; inside a per-family
|
||
card the chip repeats the header and is dropped.
|
||
2. **Name** — `--type-body` semibold, `--text-primary`. The one primary thing.
|
||
3. **Identity detail** (email, plan) — `--type-meta`, `--text-meta`.
|
||
4. **Status** — dot + a factual label from the status pairs. On Agents cards,
|
||
use one short availability label; its sentence is the title, and is said
|
||
under the head when it adds a specific pin, model or limit. Other row types
|
||
keep the status wording their own contract requires.
|
||
5. **Meta line** — `--type-meta`, `--text-meta`, on its own line under the
|
||
name. Quantities are stated in human words ("38% used · resets in 2h"), and
|
||
an instant is humanized. A row never leads with a raw ISO timestamp.
|
||
6. **Actions** — docked right in a slot of their own, legible at rest. An action
|
||
rendered at secondary ink reads as disabled: its label is `--text-primary` (a
|
||
setting's name and an unchosen option take the secondary ink, bible principle 4).
|
||
|
||
**The main view carries understanding, state and action; justification is
|
||
disclosed where needed.** A row reads identity, state, facts; its primary choices,
|
||
exceptions and owner actions stay visible, and a value keeps its honest form
|
||
(`up to`, `Tracked:`, unknown). A rule every row shares is said once for the
|
||
section; history, provenance and why a state or value reads so wait in details
|
||
pointer, touch and keyboard open, never hover alone. New copy serves a concrete decision or error and adds meaning no
|
||
control or status already carries: a checked box is not re-said beside it, and
|
||
an unsaved draft is said once by the editor or its host page, not on every row. A conditional
|
||
line never moves the control that caused it: controls own stable slots, and a
|
||
line an edit toggles opens beside or below them. Clarity comes from placement,
|
||
never from smaller text or a line quota; real exceptions all stay.
|
||
|
||
For a row with one action and a durable result, the result occupies the flexible
|
||
left side and the neutral action stays docked on the right. Field-level actions
|
||
(for example Show/Clear) keep the field's control height; they are not reused as
|
||
the compact result-row action.
|
||
|
||
Rows of the same kind are equivalent: no row gets extra visual weight for
|
||
being first, default, or native. Grouping and section-level actions express
|
||
which family a row belongs to, and a section-level action (add, connect)
|
||
belongs in its group's header rather than attached to one privileged row.
|
||
|
||
**A degraded row is emphasised, not dimmed.** Lowering a whole row's opacity
|
||
greys out the sentence that reports the problem and makes its still-clickable
|
||
controls read as disabled. Tint the row with the matching `--status-*-bg`
|
||
instead, and let the status text carry the claim.
|
||
|
||
## 7. Onboarding density
|
||
|
||
Accounts is the common connection surface for subscriptions and API keys. Open sign-in link hands off to the current desktop, browser or Telegram host while retaining the wizard; Copy is separate. An unavailable host opener reports a retryable failure, and a supported copy fallback says that it copied rather than claiming an open.
|
||
Models and Agents edit assignments; adding a connection updates available
|
||
choices without replacing an owner's assignments. A model role uses one compact
|
||
Source / Model / Account row. The account is a property of that role: Auto
|
||
rotates compatible accounts and an explicit pin stays pinned. A model inherited
|
||
from Main remains visibly inherited while its account can be pinned
|
||
independently. Fallbacks use the same row in their saved order, with adjacent
|
||
move/remove controls and the group's Add action.
|
||
|
||
Context details distinguish the exact route's advertised Auto window from a
|
||
manual value labelled "set by you". Changing an account withdraws the previous
|
||
account's metadata immediately, including during a failed or pending catalog
|
||
read. Unknown limits stay unknown. Catalog updates keep the edited field and
|
||
caret in place and never assign a model. `model_roles.js` and `model_roles.css`
|
||
own the shared Settings/wizard editor.
|
||
|
||
A source is chosen, never spelled. Every surface that assigns a model — the
|
||
Models roles, Available subagents (reviewers included), the first-run wizard and
|
||
the quota-wait picker — offers one grouped source select with the same groups
|
||
in the same order: Subscriptions · models, API keys (one entry per provider with a stored
|
||
credential, then one disabled pointer to Accounts; a saved choice without a
|
||
credential stays selectable as "(no key)"), Agents · sessions where a session
|
||
is possible. The model chooser lists only the chosen source's catalog, so a
|
||
suggestion's transport is the selected source; any id can still be typed. The
|
||
stored spellings (`provider::model`, `claudexor::source=model`,
|
||
`harness=model`) are serialization authored by the editor: never required from
|
||
the owner, never a field placeholder or help-text instruction, never the
|
||
primary displayed value; the exact stored id may appear in secondary details
|
||
(Agents use Details & history). A configured-subagent reference is the one place a stored spelling
|
||
names a thing: a roster row is labelled by its handle — its route target plus
|
||
the facets that row really runs with, defaults omitted — because a friendlier
|
||
stored label rots as soon as the owner re-points the row. The route identity chip names the source (API · OpenAI, Codex · model,
|
||
Claude Code · agent), not the channel alone. A last-run receipt is shown
|
||
against the route that produced it: when the row's route changed since, the
|
||
line says so and names the earlier route.
|
||
|
||
Subscription model sources and agent sessions never imply each other's model
|
||
inventory. Source ids are opaque; the model-sources catalog names the
|
||
credential harness. Saved subscription-model account pins survive catalog gaps
|
||
and unrelated saves; direct API-key models do not offer a subscription-account
|
||
pin. Catalog entries are suggestions, not account-specific entitlement or
|
||
context evidence. Changing a model or account never changes the delivery kind:
|
||
an API catalog row keeps its explicit Native or Packet choice regardless of its
|
||
subagent id, while a session row retrieves with its native tools. Session Access
|
||
uses the catalog's existing select: Full system access (default), Working files,
|
||
or Read only. Writable reviewers check private copies; their outputs remain
|
||
evidence without product integration. Access never changes delivery. Catalog refreshes
|
||
preserve the edited value, focus, selection and scroll position. Returning to a
|
||
reviewer's previous source restores that source's model/account draft; a source
|
||
not previously selected starts without another source's pin.
|
||
|
||
The wizard has five steps: Accounts, Models, Review, Budget, Summary. Agent
|
||
connection is inside Accounts; Codex is the recommended connection for starting
|
||
without an API key. Other existing agent connections describe their actual agent
|
||
capability. Connected reports sign-in, independently of the model-source and
|
||
suggestion reads. Accounts names pending, failed or partial reads and offers a
|
||
contextual Retry that preserves the current fields. A known model source allows
|
||
Continue and manual model entry even when its inventory or automatic suggestions
|
||
cannot be read; an unknown source explains why Continue is unavailable. New
|
||
subscription-only installs clear only untouched shipped API suggestions without
|
||
access, requiring Main while Light can inherit it and Fallback can stay empty.
|
||
Stored or edited values remain intact.
|
||
|
||
Review & start computes the skipped model and reviewer steps before showing
|
||
Summary. After a failed automatic setup, an explicit recovery action prepares
|
||
all reviewers on the selected Main model, including its account and processing
|
||
choice. This is disclosed as one model for every review, not model diversity.
|
||
The resulting Summary is shown before a separate Start saves it; subsequent
|
||
manual edits remain authoritative. Completion without the automatic preset
|
||
leaves later configuration to Settings. Summary names exactly the assignments
|
||
the one atomic Finish saves, including deep self-review. Reviewers remain
|
||
editable with the same controls as Settings. A subscription-only Budget step leads with quota/reset facts and keeps
|
||
optional API spending fields collapsed. "No API key" never claims unlimited free
|
||
work or that paid provider credits were enabled.
|
||
|
||
The first-run wizard is a compact flow that must not scroll at the default
|
||
desktop window size merely because a step has several fields.
|
||
|
||
- Step title `--type-page`; card titles `--type-section`; field labels and
|
||
notes `--type-meta`. No display size above `--type-page`.
|
||
- Short-viewport adaptation hides explanatory copy rather than shrinking type.
|
||
Once copy is hidden, shaving pixels off a title buys nothing and costs the
|
||
scale.
|
||
- Field labels are sentence case at meta ink — a wizard step shows a dozen at
|
||
once, and its job is to get one value typed, not to present a grid of
|
||
headings.
|
||
|
||
### Advanced settings
|
||
|
||
The panels a new owner opens first (Models, Providers, Agents, Behavior) show only what an owner touches in the first week. Anything a typical owner never needs, or would have to look up before using it safely, lives under Advanced: deployment plumbing (bind hosts, process pools, runtime limits), transport and trust knobs such as the extra CA bundle, MCP servers, and the rarely used provider cards, which stay on the Providers page but fold under "More providers". A control that needs a paragraph of explanation before it can be used safely is an Advanced control by definition. The engineering seam is the `advanced` settings panel and the `advanced: true` card flag in `web/modules/settings_ui.js` (DEVELOPMENT "Onboarding and Settings surfaces").
|
||
|
||
## 8. Migration state
|
||
|
||
The scale is applied to complete component families and declared page regions.
|
||
Using a migrated field inside a historical page does not claim the whole page
|
||
has migrated. Migrated today:
|
||
|
||
- `web/ui.css` (shared palette, fields, buttons, status/chip recipes and
|
||
menu/chooser chrome, used by both top-level documents and optional author pages)
|
||
- `web/settings.css` (settings shell, model/effort cards, MCP cards)
|
||
- `web/onboarding.css` (the whole first-run wizard)
|
||
- `web/model_roles.css` (shared role editor)
|
||
- `web/room_work.css` (room work disclosure, rows and reading return)
|
||
- `web/style.css` regions paired by `design-system:migrated-begin` /
|
||
`design-system:migrated-end`: harness accounts; Dashboard → Updates (status
|
||
card, one action row, collapsed Recovery with one restore list) and Storage;
|
||
chat typography, foreground and status colour. Chat geometry retains local
|
||
literals per the viewport reserve contract; shared palette channels keep
|
||
translucent glass coherent across themes.
|
||
- the global `.muted`, `.form-section h3` and shared `.ui-status` tone rules
|
||
|
||
The remaining page-specific typography in skills, marketplace, widgets, logs
|
||
and evolution is historical. Their adopted common controls follow the shared
|
||
family; unrelated page rules keep their literals until their own pass. Migrate
|
||
each selected family completely and remove its replaced recipes in the same
|
||
change. Do not introduce a half-tokenised second field family or describe a
|
||
control adoption as an all-page redesign. The semantic status/action/notification
|
||
contract already applies everywhere.
|
||
|
||
`tests/test_web_typography_static.py` guards the migrated set only. Extending
|
||
the guard and migrating the corresponding family or region are the same commit.
|
||
The bible's materials (objects, trays, sheets and the three capsule tones) reach
|
||
every page in both themes through the "Airy dopamine" blocks of ui.css, style.css and
|
||
the page sheets; that mapping is not a typography migration of those pages.
|
||
|
||
### Author freedom
|
||
|
||
An extension may use the optional shared buttons, fields and status functions
|
||
inside its own module or route-iframe page, override them, or design a completely
|
||
independent interface. `.ouro-ui` supplies font and native dark-control context;
|
||
the named classes opt controls into the recipes, with no page-wide reset.
|
||
The kit reads the installed source at a new mount; retained frames keep the styling
|
||
they loaded. A module may opt into the existing `OuroborosWidget.onTheme` signal
|
||
and apply its own `data-theme` rules, but the kit introduces no theme polling,
|
||
forced remount or mandatory visual conformance. Author layout, validation, operations and loading feedback remain
|
||
author-owned; the small source recipes are in `docs/examples/author_ui_kit/`.
|
||
|
||
---
|
||
|
||
## 9. Notifications
|
||
|
||
This section is the ONE canonical statement of when Ouroboros pulls the owner
|
||
back to the interface. `docs/DEVELOPMENT.md` points here and adds only
|
||
engineering rules; no second policy list may exist.
|
||
|
||
**Meaning.** A notification means *come back, there is something here for you* —
|
||
never *look, I am still working*. It exists so the owner can leave the window
|
||
and still be reached by a question or a finished task.
|
||
|
||
**Startup & background.** Settings → Behavior owns host lifecycle choices, separate from the client-local Appearance controls. Sign-in startup is off until the owner opts in and always names the computer running Ouroboros, even in a remote client. OS registration is the truth: another copy, an OS-disabled entry and an unavailable app build stay visible, never a saved-settings guess. Automatic startup preserves Panic stops and saved pauses; an explicit owner start releases Panic. The second control, *keep Ouroboros running in the background when the window is closed*, is also off until chosen and lives in the host's settings. Until the owner decides, the first close of the desktop window asks once — Ouroboros will keep working in the background (tasks, schedules, Telegram): keep it running, or quit? — and the answer becomes that checkbox; dismissing the question quits. macOS labels its buttons Keep running and Quit; Windows keeps standard OK and Cancel, with a line saying OK keeps it running and Cancel quits. While it is on, closing hides the window behind a live indicator (the Windows notification-area icon; the macOS menu-bar item and Dock) whose menu shows the server's real state (work only from confirmed phases; starting, an error, unconfirmed activity or no answer said as such) and offers Open, Panic and Quit; if no indicator appears the close quits, so a hidden process always has a visible way back. Quit, Cmd+Q and signing out always quit, unasked. A sign-in start stays hidden only when both checkboxes are on and the indicator actually appeared; opening the app while it starts shows the window; there is no third "start minimized" setting. Linux shows the background control as not yet available; closing quits there. Android keeps its existing automatic boot entry by default, under the same Panic-preservation contract. The card is last on Behavior: set once; on a browser or Docker host it only explains itself. A control the host cannot offer says why as a plain note in meta ink (no warning dot); two controls with the same reason say it once, after both; a state the owner can change (another copy, an entry the OS turned off) is a warning, a failed read or write an error. Notes and statuses start at the checkbox label's text, a status dot hanging under the checkbox.
|
||
|
||
**Telegram completion messages are separate from these client alerts.** The
|
||
existing completion toggle Off suppresses every additional terminal line, failures
|
||
included; On sends endings, naming material problems before review provenance.
|
||
An otherwise finished task with only technical caveats says *finished*, without
|
||
a warning or success icon, and points to review and verification details in the
|
||
app. It never claims approval. Missing promised output, unresolved work, delivery
|
||
uncertainty and unmet or unknown review obligations remain material. This phone
|
||
projection never shortens full card reasons or rewrites outcomes. The status-only
|
||
Main mirror follows the toggle too, but preserved intermediate content does not.
|
||
Ordinary replies, files, questions and reply-editing Silent Mode remain independent. Unrecoverable old mirrors keep their text rather than guessing it is status-only. There is no new mode, cross-channel deduplication or retroactive rewrite.
|
||
|
||
**When the client runs.** Notifications are a property of a running client, and
|
||
there is no push channel. On the desktop the Telegram bridge runs inside the same
|
||
server, so it reaches the owner while the window is closed only in background
|
||
mode; quitting ends it together with the server.
|
||
|
||
**Focus does not suppress, and neither does a closed room.** While a category is
|
||
on, its event notifies whether or not the window has focus and whether or not
|
||
the relevant chat is open. A Project's question reaches the owner even when that
|
||
Project was never opened in this session — which is the whole point, and the
|
||
reason the subscription belongs to the client rather than to a room. That is
|
||
deliberate for the first version: we measure how it feels before adding clever
|
||
exceptions.
|
||
|
||
**Two authorities, one surface.** Every category is one of two kinds:
|
||
|
||
| Category | Kind | What decides |
|
||
|---|---|---|
|
||
| A question or decision is waiting | required | a confirmed lifecycle fact: the question carries a positive wait |
|
||
| A task finished or stopped | required | a positive typed terminal fact on a ROOT task |
|
||
| Messages Ouroboros sends while working | LLM-first | Ouroboros chose to speak outside the turn's answer (a proactive message in its room or as a Main notice, or an optional question) |
|
||
| A note Ouroboros left for this moment, or a granted skill's notice (same toggle) | LLM-first; owner grant | when it wrote the note, Ouroboros chose to leave these words for this time; a skill speaks only under the owner's `notify_owner` grant; the host only shows the signed `reminder`/`skill_notice` row |
|
||
| Ordinary replies in Main | separate toggle | an ordinary finished reply in the Main thread |
|
||
|
||
*Required* means the application asks for delivery from its own state rather
|
||
than relying on the model to remember. It does not mean the notification
|
||
bypasses OS permission, Do Not Disturb or platform limits — nothing here
|
||
claims that.
|
||
|
||
*LLM-first* costs no extra model call and needs no new host field: the signal is
|
||
that Ouroboros already chose to speak outside its answer
|
||
(`send_user_message`) or to ask something optional. The decision to send is the
|
||
mind's; the client only carries it. Stated precisely because the tool contract
|
||
asks for such a message at the START of long work as well: this category means
|
||
"Ouroboros said something while working", not a claim that it weighed whether
|
||
to interrupt you. Judge it by use and turn it off if it is too chatty.
|
||
|
||
**What never notifies.** Progress, a transient failure, one tool's error and a
|
||
reviewer's finding are not notifications; they stay in the transcript. A child
|
||
task never notifies the owner: it escalates to its parent, and only the parent's
|
||
own message or the root's terminal can ring. Machine partitions — the hidden
|
||
chat and agent-to-agent traffic — never reach a banner. A Project's room
|
||
notifies once this client knows it; any other ordinary conversation, including
|
||
one arriving over an external transport, is treated as the owner's exactly as
|
||
the Main thread itself treats it.
|
||
|
||
**A terminal is not always an ending.** An update or restart teardown reports a
|
||
task as interrupted and then requeues it; that is not a finished task and does
|
||
not notify, so the task's real completion still can.
|
||
|
||
**One ending, one notification.** An ordinary reply and the terminal of the same
|
||
task are one event, not two: whichever arrives first rings, and the other is
|
||
collapsed. The same holds for the several wire shapes a finished task has.
|
||
|
||
**System notifications first.** Where the desktop app offers them, an alert is
|
||
handed to the operating system's own notifications: sent even while the window
|
||
is in front, titled with the kind of event, sounding with the system's own sound,
|
||
and kept where that system keeps notifications. The system taking it is not a
|
||
promise that anyone saw a banner: its notification settings, Focus and Do Not
|
||
Disturb still decide. macOS uses `UNUserNotificationCenter`, whose one permission
|
||
question is asked only from the owner's gesture — switching notifications on, or
|
||
Test — never by an alert, which falls back while the question is unasked; Windows
|
||
shows a notification-area balloon, which Windows 10/11 present as a system
|
||
notification, on an icon of its own per notification so that each click opens
|
||
its own source; Linux uses the freedesktop notification service when
|
||
the desktop runs one. Not yet allowed, denied, unavailable or failed is a typed
|
||
answer, never silence (an error the system returns instead of asking is that
|
||
error, not a denial): that alert falls back to the browser banner or the in-app
|
||
toast. One the system took without answering in time may still appear, so
|
||
nothing else is shown for it and its click still opens its source. In-app
|
||
updates do not replace the desktop app, so an app built before this capability
|
||
falls back and Settings says that system notifications need the current app.
|
||
Android's host keeps its own channel for its own service; its page has no system
|
||
notifications and says so.
|
||
|
||
**One sound.** At most one sound per event. A system notification owns its
|
||
sound: the operating system plays it under its own volume and Do Not Disturb,
|
||
and nothing else plays one beside it. Without one, where the browser shows a
|
||
banner, the banner owns the sound; where a desktop bridge is available, the
|
||
launcher owns one system sound (or reports that it could not play one);
|
||
otherwise the app plays one short tone. Never both, and the Sound choice remains
|
||
authoritative: Sound off sends a system notification silently, a Windows balloon
|
||
included (the shell's own balloon call with `NIIF_NOSOUND`). If that call is
|
||
unavailable, a silent request falls back to the page without a balloon; the
|
||
WinForms fallback is used only when sound was requested, and since it answers
|
||
nothing, its balloon counts as unconfirmed. An app built before system
|
||
notifications cannot silence the balloon it shows while its window is hidden,
|
||
so with Sound off the page does not ask that app at all: the alert is a silent
|
||
browser banner or in-app toast, without its balloon, Dock mark or window raise.
|
||
|
||
**Each open window is its own client.** Settings, permission and the
|
||
duplicate-collapsing that keeps one event to one notification all belong to one
|
||
running page. Two windows of the same device — the desktop shell and a browser
|
||
tab — are two clients and can each notify for the same event. That is the
|
||
honest consequence of per-client settings, not a bug we have hidden.
|
||
|
||
**Click goes to the source.** A notification opens the question or the result it
|
||
is about — the Project room and the exact question when it has one, otherwise
|
||
the conversation. Project identity comes from the full registry, including rooms
|
||
outside the sidebar. An unavailable destination leaves the view in place and says
|
||
so. A Main-transport notice opens Main, including Telegram; numeric addresses
|
||
never imply a Project. A system notification first opens the window, hidden or not,
|
||
then the same source. No reply is composed from the banner. The one exception is
|
||
the fallback banner the desktop indicator shows while the window is hidden in
|
||
background mode (Windows, when no system notification was shown): its click
|
||
opens the window as it was left and does not navigate.
|
||
|
||
**Content is private by default.** Only the kind of event is shown until the
|
||
owner turns message text on, because a banner can appear on a shared screen.
|
||
|
||
**Deliberately absent.** No numeric badge, no repeated reminder, no inline
|
||
reply, no Telegram escalation, and no notification or attention after the
|
||
application quits. When no system notification was shown and the packaged
|
||
desktop launcher exposes its optional `request_attention` bridge, a live
|
||
notification may raise a visible window and ask the operating system for one
|
||
standard sound. A window the owner hid in background mode is never raised by an
|
||
alert: Windows shows a banner from its notification-area icon, macOS marks and
|
||
bounces the Dock icon with the system sound, and the window opens from that
|
||
banner, the indicator or the Dock. That holds with browser notifications allowed
|
||
too: the page asks the launcher before showing a browser banner, whose click
|
||
could not bring a hidden window back; a visible window keeps the browser banner,
|
||
which owns the sound. These are native attention cues, not proof
|
||
that a system banner was delivered; unsupported or older launchers fall back to
|
||
the browser banner or in-app toast and report that capability honestly.
|
||
|
||
**Settings.** The controls live on **Settings → Appearance**, under the theme
|
||
block, and are stored per client exactly like the appearance choice: the desktop
|
||
window and each browser keep their own, nothing reaches the server. A test
|
||
button is the honest way to see what this system actually does with a
|
||
notification, including a denied permission, and in the desktop app it is the
|
||
gesture that lets the system ask. The status line names the surface this client
|
||
is using — the system's notifications, a browser banner or the in-app toast —
|
||
keeps the system's permission apart from what became of the last alert (taken,
|
||
unconfirmed or fallen back), and, in an older desktop app, says that system
|
||
notifications need the current app.
|
||
|
||
**Known limits of this version**, stated rather than discovered later:
|
||
|
||
- A task retried under the same id has one conclusion as far as notifications
|
||
are concerned; a second terminal for that id stays quiet.
|
||
- Duplicate collapsing is bounded (the oldest keys are forgotten after a very
|
||
large number of events in one session), so a frame for a long-past event
|
||
could ring again.
|
||
- A child task is recognised from the delegation facts its traffic carries (its
|
||
declared lineage, or the executor enrichment on its own terminal). Nothing on
|
||
the wire declares a task to BE a root, so a task with no such fact is treated
|
||
as one: requiring proof of root-ness would silence every finished task. The
|
||
narrow residual is a child that carries no delegation fact at all and whose
|
||
first observed frame is its terminal — it would notify once.
|
||
- An event that happens while the socket is down never rings: reconnect replays
|
||
history, and history is deliberately silent.
|
||
- A Windows balloon stays in Windows' notification list only while its icon is
|
||
in the notification area: until it is clicked, until a fourth retires the
|
||
oldest of three, or until the app quits. One that timed out or was dismissed
|
||
keeps its icon; nothing is shown again.
|
||
- An alert the system took without confirming in time can stay unseen: it is
|
||
not repeated in the app, and Settings says so. On Linux its click cannot open
|
||
the source, because the server's id for it never came back.
|
||
- A system notification clicked after its page reloaded only opens the window:
|
||
click targets live as long as the page that sent them, with no stored ledger.
|
||
- What a system notification looks like is the system's choice: macOS's style
|
||
for Ouroboros (banners, alerts or none), Focus and Do Not Disturb, Windows'
|
||
notification settings, and a Linux server's optional actions (without them a
|
||
click cannot open the source) and sound (without it the alert is silent);
|
||
Settings names what that server lacks.
|