@ai-ecoverse/slicc-spectrum 1.79.1 → 1.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -157,7 +157,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
157
157
  | `<slicc-monitor>` | The live monitor: vitals (`.vital[data-id]`), each a meter when it has a `ratio` (budget, fullest context) and otherwise a value, its unit and delta on one line and a sparkline from its `series` (spend, active agents, processes, process memory). Each card keeps its place whatever it shows: the value line never wraps (a unit or delta that doesn't fit is left out whole rather than cut, and stays in the line's `title` and its text; a vital's optional `hint` replaces that tooltip, such as the exact reset time behind "resets in 4 days"), a card without a `series` keeps the sparkline's room, a meter keeps its description's line, and a card left alone on the last row spans it, so short and long values, 0% and 100%, and missing data move nothing below them. Then alerts (an alert's optional `command` shows after its detail, led by its `commandLabel` or "Run", as unwrapped code with **Copy**, `[data-command]` and `data-action="copy-command"`), and the sections (the agent's Tasks, Processes and Recent problems, then terminals, tabs, pending changes and tray followers; cones and sprinkles have their own panels; any other section an agent sends, such as an older agent's `agents` or `sprinkles`, renders the same way). Each section (`li.section[data-id]`) shows its count and an attention badge, and opens its surface with **Open** where one exists; the counts line up in one column whether or not a section has **Open**, and a section without rows keeps its name in line with the others (`agents`, `terminals` and `processes` → terminal, `tabs`, `changes`). A section with rows expands into them with its toggle (`.toggle`, `aria-expanded`; `data-open` on the section): collapsed by default, expanded while a row is `warn` or `error`, until the user toggles it. Each row (`li.row[data-status]`, `data-id` from the row's `id`) shows a status light (blue active, gray idle, orange warn, red error, with the status spoken), the name, the meta line and its badges, indented by `depth` (`data-depth`, up to 6). |
158
158
  | `<slicc-freezer>` | Every conversation in the session, from `frozen()`: cones, scoops and agent runs, each with a kind badge, live ones first with a Live light, then the newest first. A live row offers **Open**, which selects that agent; a stopped one offers **Thaw** and **Remove**, which deletes its working folders after a confirmation. A thawed scoop or agent run keeps its row, which says what it was thawed as. **Delete cone**, in the agents list (or <kbd>Delete</kbd> on a cone's row) and the header's cone picker, stops a cone after a confirmation; `/freeze` does it at once. Either way its conversation stays here, and a row without a `kind` counts as a cone. |
159
159
  | `<slicc-sprinkle>` | A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit, even when it loads out of view (the frame measures itself on load, after icons and fonts arrive, and on every resize). A dip has a tab-like handle: drag it into the dock (or onto a rail, or use its open-as-panel button) and it lives on as a sprinkle panel, with a note in the chat where it was; which dips are out is saved in `slicc-ui.dips`. It runs in a sandboxed frame (`allow-scripts` only) with SLICC's sprinkle theme (the `--s2-*` tokens and `.sprinkle-*` classes), the app's fonts, and Lucide icons (`<i data-lucide>`, `LucideIcons.render()`). It talks back through `slicc.lick({ action, data })`, which reaches the owning cone as a `sprinkle` lick, and, when the port has `call`, reads files and keeps state (see [Sprinkle bridge](#sprinkle-bridge)). Panel sprinkles sit at the top of the right rail with their Lucide icon. |
160
- | `<slicc-network>` | Network: the route and its health in plain words with the bios `detail`, a line on browser automation (`data-browser`: through the SLICC extension, through slicc on this computer (`proxy`, or a linked device on this computer), through a linked device's browser, that slicc on this computer has no browser to control when it carries the traffic and `via` is `null`, or not available with how to get it) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, an **Internet traffic** picker (`data-action="exit"`, in `[data-exit-picker]` with the exit's kind) when the port has `setExit`, the route line saying where traffic goes, and under the picker a warning that traffic waits while the chosen device isn't connected (`data-exit="device"`, `data-held`, with **Send directly**, `data-action="exit-direct"`, which calls `setExit(null)`) or, when `route` isn't `tailnet`, the Tailscale exit node in use (`data-exit="node"`), a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, a **Linked devices** section (`data-links`: `linked`, `connecting`, `reconnecting`, `offline`, `none` or `permission`; each device row has a **Prompting** or **Watching** badge, `data-controlling`, while it controls the agent, and **Allow remote control**, `sp-switch[data-action="remote-control"]`, behind a confirm when turned on, or `data-control="fixed"` for a device on this computer; with `access`, an **Allow** group of **View**, **Control** and **Terminal** switches, `data-action="access-view"`, `access-control` and `access-terminal`, each confirmed when turned on, Terminal's saying it gives a root shell, and Control disabled while View is off; a device that asks to follow shows **Asks to follow** and **Allow**, `data-action="allow-follow"`; a **Terminal open** badge, `data-terminals`, counts its terminals; with `followUrl`, the join details add **Open on another device** with a QR code and the link, `data-join="follow"` and `data-copy="follow-url"`) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and the commands to run slicc, each in a `.command[data-command]` block with a copy button: `curl -fsSL https://get.sliccy.ai/slicc | sh` (its button is `data-action="copy-command"`), `slicc`, and, with Node, `npx sliccy@next`; folded away when the route already reaches the whole web (`proxy`, `extension`, `link` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line says "Through slicc on this computer" for a `local` device, or names a remote device and its `policy`, or says it's waiting for it, and `data-route` is `link`, whatever `route` says), and recent failures with host, error and time. When the model has a `tray` port, a **Sync** section (`data-sync` with the connection) follows the connection: the tray's state, the float's name, role and runtime, the join link, masked (`…/join/••••••`) until **Show** (`data-action="reveal-tray-join"`, which turns into **Hide**), with **Copy** (`data-action="copy-tray-join"`), the followers (`li[data-follower]`), and **Disconnect** (`data-action="tray-disconnect"`, confirmed) or **Reconnect** (`data-action="tray-reconnect"`). When the status has `leaders`, a **Leaders** section (`section[data-leaders]` with the count) follows Sync, since it's the other direction of the same tray link: Sync is this SLICC's own tray and who follows it, Leaders the SLICCs it follows. Each leader is an `li[data-leader]` (the id) with `data-state`: its name, its hub as a quiet second line and a status light with the state in words (Connecting, Reconnecting, Connected, Disconnected, Failed, Unlinked); `failed` adds its `error` as plain text (`[data-leader-error]`, "Couldn’t reach its hub." without one) and **Retry** (`data-action="retry-leader"`), and `unlinked` says "{name} stopped sharing with this SLICC." (`[data-unlinked]`) without actions. **Disconnect** (`data-action="disconnect-leader"`) runs at once, without a confirm, since it's reversible: the row stays as Disconnected (a neutral light), with **Reconnect** (`data-action="reconnect-leader"`) and **Forget** and no Disconnect or Retry. **Forget** (`data-action="forget-leader"`) asks "Forget {name}?" ("To follow it again, you’ll need a new invite from it.") with a negative confirm, and the row goes. Each shows only when the port has its method; while one runs, the row's buttons are disabled, and a failure shows in the row (`data-error="leader"`). When the model has a `guests` port, a **Guests** section (`section[data-guests]` with the count) follows Leaders: **Invite a guest** (`data-action="invite-guest"`, accent, disabled while no cone is live; frozen cones are left out of the invite too), or, while `unavailable()` (or the last `unavailable` event, when the port has no method) is a sentence, that sentence instead, subdued (`[data-guests-unavailable]`). Each seat is an `li[data-seat]` (the id) with `data-connected` (`true` or `false`): its label, the cone's name as a quiet second line (`[data-seat-cone]`), "Expires in {n} hours" under a day, "Expires in {n} days" after, or "No expiry" (`[data-expiry]`), a status light with **Connected** or **Not connected**, a count badge (`data-pending`) reading "{n} waiting", whose accessible name is "{n} waiting for review" when `pending` is above 0, and **Revoke** (`data-action="revoke-guest"`), which asks "Revoke {label}?" ("Their link stops working at once. To let them back in, invite them again.") with a negative confirm; while it runs, the rows' buttons are disabled, and a failure shows in the row (`data-error="guest"`). With no seats it says "No guests yet." (`[data-guests-empty]`). Invite opens `<slicc-guest-invite>`, a modal: **Cone** (`#guest-cone`, the active cone, or a selected scoop's cone, else the first), **Name** (`#guest-name`, required, trimmed, at most 40 characters, "Shown to you and in the cone, never set by the guest."), **Link expires** (`#guest-expiry`: 1 hour, 1 day, 7 days by default, or 30 days), **Their messages** (`#guest-messages`: You approve each one, The automatic check decides, {cone} decides, {scoop} decides for each of the cone's scoops, or No review, which are `user`, `agent`, `cone`, `scoop:<name>` and `off`) and **Their tool calls** (`#guest-tools`: `user`, `agent` and `scoop:<name>` only), both `user` by default, each with the chosen option's meaning under it. **Create link** (`data-action="create-guest-link"`) calls `mint(cone, { label, expiresAt, gates })`; while it runs the form is busy and Escape does nothing, and a rejection shows its message inline (`data-error="guest"`). On success the dialog shows the URL once (`[data-guest-invite="link"]`): a read-only field (`data-copy="guest-url"`) with **Copy** (`data-action="copy-guest-url"`), "This link is shown only once. Copy it now." and **Done** (`data-action="done-guest"`). The URL is never stored or shown again: closing the dialog drops it. The header's **Network** indicator (`data-network`, `data-health`) opens it: a status light, green for `ok`, yellow for `limited` and red for `failing`, with the state in its text and label and `detail` as its tooltip, which adds "Tailscale is connected." while the tailnet runs; on a phone, a cloud icon for the state. While a linked device asks to follow, controls the agent, watches it or has a terminal open, an indicator (`data-remote`, `ask`, `prompt`, `terminal` or `watch`) sits before it, **{name} asks to follow**, **Controlled by**, **Terminal open from** or **Watched by** the device's name (or the number of devices; in that order of precedence), says who does what in its tooltip and opens the panel too. Offered only when the model has a `network` port. |
160
+ | `<slicc-network>` | Network: the route and its health in plain words with the bios `detail`, a line on browser automation (`data-browser`: through the SLICC extension, through slicc on this computer (`proxy`, or a linked device on this computer), through a linked device's browser, that slicc on this computer has no browser to control when it carries the traffic and `via` is `null`, or not available with how to get it) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, an **Internet traffic** picker (`data-action="exit"`, in `[data-exit-picker]` with the exit's kind) when the port has `setExit`, the route line saying where traffic goes, and under the picker a warning that traffic waits while the chosen device isn't connected (`data-exit="device"`, `data-held`, with **Send directly**, `data-action="exit-direct"`, which calls `setExit(null)`) or, when `route` isn't `tailnet`, the Tailscale exit node in use (`data-exit="node"`), a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, a **Linked devices** section (`data-links`: `linked`, `connecting`, `reconnecting`, `offline`, `none` or `permission`; each device row has a **Prompting** or **Watching** badge, `data-controlling`, while it controls the agent, and **Allow remote control**, `sp-switch[data-action="remote-control"]`, behind a confirm when turned on, or `data-control="fixed"` for a device on this computer; with `access`, an **Allow** group of **View**, **Control** and **Terminal** switches, `data-action="access-view"`, `access-control` and `access-terminal`, each confirmed when turned on, Terminal's saying it gives a root shell, and Control disabled while View is off; a device that asks to follow shows **Asks to follow** and **Allow**, `data-action="allow-follow"`; a **Terminal open** badge, `data-terminals`, counts its terminals; with `followUrl`, the join details add **Open on another device** with a QR code and the link, `data-join="follow"` and `data-copy="follow-url"`) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and the commands to run slicc, each in a `.command[data-command]` block with a copy button: `curl -fsSL https://get.sliccy.ai/slicc | sh` (its button is `data-action="copy-command"`), `slicc`, and, with Node, `npx sliccy@next`; folded away when the route already reaches the whole web (`proxy`, `extension`, `link` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line says "Through slicc on this computer" for a `local` device, or names a remote device and its `policy`, or says it's waiting for it, and `data-route` is `link`, whatever `route` says), and recent failures with host, error and time. When the model has a `tray` port, a **Sync** section (`data-sync` with the connection) follows the connection: the tray's state, the float's name, role and runtime, the join link, masked (`…/join/••••••`) until **Show** (`data-action="reveal-tray-join"`, which turns into **Hide**), with **Copy** (`data-action="copy-tray-join"`), the followers (`li[data-follower]`), and **Disconnect** (`data-action="tray-disconnect"`, confirmed) or **Reconnect** (`data-action="tray-reconnect"`). When the status has `leaders`, a **Leaders** section (`section[data-leaders]` with the count) follows Sync, since it's the other direction of the same tray link: Sync is this SLICC's own tray and who follows it, Leaders the SLICCs it follows. Each leader is an `li[data-leader]` (the id) with `data-state`: its name, its hub as a quiet second line and a status light with the state in words (Connecting, Reconnecting, Connected, Disconnected, Failed, Unlinked); `failed` adds its `error` as plain text (`[data-leader-error]`, "Couldn’t reach its hub." without one) and **Retry** (`data-action="retry-leader"`), and `unlinked` says "{name} stopped sharing with this SLICC." (`[data-unlinked]`) without actions. **Disconnect** (`data-action="disconnect-leader"`) runs at once, without a confirm, since it's reversible: the row stays as Disconnected (a neutral light), with **Reconnect** (`data-action="reconnect-leader"`) and **Forget** and no Disconnect or Retry. **Forget** (`data-action="forget-leader"`) asks "Forget {name}?" ("To follow it again, you’ll need a new invite from it.") with a negative confirm, and the row goes. Each shows only when the port has its method; while one runs, the row's buttons are disabled, and a failure shows in the row (`data-error="leader"`). When the model has a `guests` port, a **Guests** section (`section[data-guests]` with the count) follows Leaders: **Invite a guest** (`data-action="invite-guest"`, accent, disabled while no cone is live; frozen cones are left out of the invite too), or, while `unavailable()` (or the last `unavailable` event, when the port has no method) is a sentence, that sentence instead, subdued (`[data-guests-unavailable]`). Each seat is an `li[data-seat]` (the id) with `data-connected` (`true` or `false`): its label, "on {cone}" as a quiet second line (`[data-seat-cone]`), "Expires in {n} hours" under a day, "Expires in {n} days" after, or "No expiry" (`[data-expiry]`), a status light with **Connected** or **Not connected**, a count badge (`data-pending`) reading "{n} waiting", whose accessible name is "{n} waiting for review" when `pending` is above 0, and **Revoke** (`data-action="revoke-guest"`), which asks "Revoke {label}?" ("Their link stops working at once. To let them back in, invite them again.") with a negative confirm; while it runs, the rows' buttons are disabled, and a failure shows in the row (`data-error="guest"`). With no seats it says "No guests yet." (`[data-guests-empty]`). Invite opens `<slicc-guest-invite>`, a modal: **Cone** (`#guest-cone`, the active cone, or a selected scoop's cone, else the first), **Name** (`#guest-name`, required, trimmed, at most 40 characters, "Shown to you and in the cone, never set by the guest."), **Link expires** (`#guest-expiry`: 1 hour, 1 day, 7 days by default, or 30 days), **Their messages** (`#guest-messages`: You approve each one, The automatic check decides, {cone} decides, {scoop} decides for each of the cone's scoops, or No review, which are `user`, `agent`, `cone`, `scoop:<name>` and `off`) and **Their tool calls** (`#guest-tools`: `user`, `agent` and `scoop:<name>` only), both `user` by default, each with the chosen option's meaning under it. **Create link** (`data-action="create-guest-link"`) calls `mint(cone, { label, expiresAt, gates })`; while it runs the form is busy and Escape does nothing, and a rejection shows its message inline (`data-error="guest"`). On success the dialog shows the URL once (`[data-guest-invite="link"]`): a read-only field (`data-copy="guest-url"`) with **Copy** (`data-action="copy-guest-url"`), "This link is shown only once. Copy it now." and **Done** (`data-action="done-guest"`). The URL is never stored or shown again: closing the dialog drops it. The header's **Network** indicator (`data-network`, `data-health`) opens it: a status light, green for `ok`, yellow for `limited` and red for `failing`, with the state in its text and label and `detail` as its tooltip, which adds "Tailscale is connected." while the tailnet runs; on a phone, a cloud icon for the state. While a linked device asks to follow, controls the agent, watches it or has a terminal open, an indicator (`data-remote`, `ask`, `prompt`, `terminal` or `watch`) sits before it, **{name} asks to follow**, **Controlled by**, **Terminal open from** or **Watched by** the device's name (or the number of devices; in that order of precedence), says who does what in its tooltip and opens the panel too. Offered only when the model has a `network` port. |
161
161
  | `<slicc-tray>` | The tray items in the header, each opening a panel when that panel is in `panels`: the connection (`#chip`, a status light with the float's name and followers, `data-surface="network"`) opens Network at its **Sync** section, and the budget (`#usage`, `data-surface="monitor"`) opens the Monitor. Without the panel an item is a plain status with its label. |
162
162
  | `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
163
163
  | `<slicc-prompt>` | The name dialog `prompt()` opens. Not for direct use. |
@@ -252,7 +252,7 @@ With an `agentId` or a `cone`, the caller reaches the shared surfaces, the chats
252
252
  | `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`link` while a linked slicc carries the traffic, with `exit` naming it, `proxy` from older bios for slicc on this computer, `extension` for the Chrome extension relay, `tailnet` while a Tailscale exit node carries the traffic, `page` for the page's own fetch, limited by CORS, or `null`), `health` (`ok`, `limited` or `failing`, decided by the port), a one-line `detail`, recent `failures` (`{ url, error, at }`, newest first), an optional `extensionUrl` and an optional `browser` (`{ via, detail? }`, `via` being `extension`, `proxy`, `link` when linked devices' browsers carry it (the line names the browser and its device, or counts them when there are several, counting connected devices only), or `null` when browser automation isn't available; a legacy `declined` flag is ignored, since there is no consent prompt any more). Emit `network` when it changes. The optional `check()` tests the route again. An optional `tailnet` (`TailnetStatus`) shows the Tailscale section: `state` (`off`, `loading`, `needs-login`, `starting`, `running` or `failed`, a status light), a plain-language `detail` (progress, or the failure reason in an error box with **Retry** through `check()`), a `loginUrl` (**Sign in to Tailscale**, a new tab), the `node` (`{ name, addresses }`; the name and the first 100.x address, each with a Copy button), `exitNode` (the name in use or `null`), `exitNodes` (`{ id, name, online }`), `autoExitNode`, `shieldsUp` (a muted line when `true`, a warning when `false`) and `peers` (a count). The optional `setTailnet(on)` adds the **Use Tailscale** switch (`data-action="tailnet"`); `submitAuthKey(key)` adds an auth-key field to `needs-login` (a password field without a `name`, cleared on submit, never kept in component state); `setExitNode(id | 'auto' | null)` adds the **Exit node** picker (None, Automatic, and each node, offline ones disabled) to `running`, with a line saying where traffic goes; `logoutTailnet()` adds **Sign out**, behind a confirm. A rejected call shows its message in the section. Peers are never named beyond the exit-node list. An optional `links` (`NetworkLinks`) shows **Linked devices**, slicc CLIs linked over WebRTC: `joinUrl` and `joinCommand` (each with Copy, `data-copy="join-url"` and `data-copy="join-command"`; both `null` shows "Preparing a join URL…"), an optional `followUrl` (the page URL with `#follow=` and the join URL, shown with a QR code). Join URLs let a device join, so the join URL, the command, the follow link and the tray's join link are masked (the last path segment and anything after it, or, without a path, everything after `#` or `?`; text without a URL is masked whole), with `data-secret="hidden"` until **Show** (`data-action="reveal-<copy>"`) sets it to `shown`; a new URL masks again, Copy copies the real value either way, and the QR code stays scannable, `devices` and `permission`. A `LinkedDevice` has an `id`, a display `name` (a leading `slicc` is shown as `SLICC`), the kernel `host` and `address`, a `mode` (`local` for a CLI on this computer attached over CDP, or `remote`), `offers` (`net`, `http`, `ssh`; `ssh` adds a copyable `ssh <host>`), a one-line `policy`, `exit` (whether it can carry internet traffic), a `state` (`connecting`, `connected`, `reconnecting` or `offline`, for a device that linked before and isn't there now; every device that ever linked is listed, so one that was refused can be allowed), optional `control` (`{ allowed, fixed? }`; `fixed` for one on this computer, always allowed), optional `controlling` (`prompt` or `watch` while a CLI prompts or watches the agent) optional `browsers` (`{ runtime, name }`, listed in the row as `data-browsers`, with the runtime such as `pier-nuc/chrome` that the 9222 facade drives), optional `kind` (`cli`, or `seven` for another SLICC that follows this one, "Follower" in the row), optional `access` (`{ view?, control?, terminal?, fixed? }`: only the keys present get a switch, so a CLI sends `{ control }`; `fixed` lists them as always allowed from this computer; it replaces `control`), optional `asking` (a follower that waits to be allowed), optional `terminals` (how many terminals it has open here) and optional `folders` (`LinkedFolder`s the device shares: `name`, `source` such as `pier-nuc/proj`, `readonly`, and `mounted`, the mount point once mounted). Folders list in the row (`[data-folders] li[data-folder]`, with `data-mounted` `yes` or `no`, a **Read-only** badge and "Mounted at …"); `mountFolder(source)` adds **Mount** (`data-action="mount-folder"`, `data-id` the source; only on a connected device) and resolves with the mount point, such as `/mnt/pier-nuc/proj`, and `ejectFolder(source)` adds **Eject** (`data-action="eject-folder"`); a failure shows on the links error line. Each device is an `li[data-device]` with `data-state`, `data-mode` and `data-kind`, and `data-asking` while it asks; once there are devices, the join details fold into **Link another device**. `permission` is `prompt` (an informative line asking to choose Allow in Chrome's Local Network Access prompt) or `denied` (a warning, with **Try again**, `data-action="retry-links"`, through `retryLinks()`). An optional `exit` (`NetworkExit`: `null`, `{ kind: 'tailnet', node }` with a Tailscale node id or `'auto'`, or `{ kind: 'link', id }`) is the default exit. The optional `setExit(exit)` replaces the Tailscale **Exit node** picker with one **Internet traffic** picker in Connection: Directly, then Tailscale (Automatic and its nodes) and Linked devices (those with `exit: true`), offline ones disabled unless chosen; a chosen device that isn't connected shows that traffic waits, since the exit fails closed. Without `setRemoteControl(id, allowed)`, `control` is a line saying whether it's allowed; with it, **Allow remote control** is a switch. Without `setAccess(id, 'view' \| 'control' \| 'terminal', allowed)`, `access` is a line saying what's allowed; with it, the switches call it, and **Allow** on a device that asks calls `setAccess(id, 'view', true)`. Turning View off should turn Control off too. `unlink(id)` adds **Unlink** (`data-action="unlink"`) to each device and `rotateJoinUrl()` adds **New join URL** (`data-action="rotate-join-url"`), both behind a confirm. Their failures show in the section. An optional `leaders` (`Leader[]`: `id`, the leader's hub origin, unique per leader; `name`, from the link's hello, with a leading `slicc` shown as `SLICC`; `hub`, the host to show; `state`, `connecting`, `reconnecting`, `connected`, `disconnected`, `failed` or `unlinked`; and an optional `error`) lists the SLICCs this one follows, shown when non-empty. `unlinked` means the leader removed this SLICC and bios already forgot it: send it in one status, then leave it out. `retryLeader(id)` adds **Retry** to a `failed` leader, `disconnectLeader(id)` stops following but keeps the saved join and the device certificate, so the row stays as `disconnected` across reloads, `reconnectLeader(id)` resumes it without a new link (`connecting`, then `connected`, or `unlinked`, shown once, if the leader unlinked this SLICC meanwhile), and `forgetLeader(id)` drops the join and the certificate for that hub, and the row goes, so the leader has to invite this SLICC again. Without the port, the indicator and the Network panel don't show. |
253
253
  | `notices` (optional) | BIOS | `list()` returns `Notice`s for quiet, dismissible in-line notices at the top of every chat panel, above the transcript, in list order: `id`, `tone` (`info` reads as informative, `warning` as a notice), `title`, a plain-text `body` and `actions` (`{ id, label }`). Title, body and labels are text only. Notices use `role="status"` and never take focus. An action is a secondary button that stays pending until `act(id, action)` settles; a rejection shows its message under the body. The close button, labelled "Dismiss: <title>", hides the notice at once and calls `dismiss(id)` synchronously; it shows again only if the port drops the notice from `list()` and brings it back later. Emit `notices` whenever they change. A notice with `dismissible: false` has no close button and stays until the port drops it. Hooks: `[data-notice="<id>"]`, buttons with `data-action`, `[data-dismiss]` and `[data-error]`. |
254
254
  | `tabs` (optional) | BIOS | `state()` returns this tab's `TabsState`: `role` (`owner` runs SLICC; `follower` shows the owner tab's SLICC; `connecting` while the owner boots or hands over; `alone` without SharedWorker), `stalled` (the owner stopped answering) and `skew` (`newer` or `older` when the owner runs another SLICC version, else `null`). Emit `tabs` when it changes. An owner shows nothing. A follower gets a quiet header light, "Another tab" (`[data-tabs][data-role]`, with `data-stalled` and `data-skew`; a dot on phones), info while connecting, and a notice while stalled or skewed, with its explanation as the tooltip. While connecting, an empty chat says "Connecting to the tab running SLICC…" (`[data-connecting]`). Skew, or else a stall, shows a warning banner at the top of every chat panel (`[data-tabs-notice="skew"\|"stalled"]`, `data-skew`): "Another tab runs a newer SLICC. Reload this tab." with **Reload** (`data-action="reload"`, calls `reload()`), "Another tab runs an older SLICC. Reload the other SLICC tab.", or "Chrome paused the tab running SLICC. Switch to it to continue.". An `alone` tab shows only the brand and "SLICC is open in another tab." (`[data-tabs="alone"]`). |
255
- | `approvals` (optional) | BIOS | `list()` returns waiting `Approval`s; emit `approvals` when the list changes. A `sudo` approval has `id`, `requester` (`kind` `agent` or `user`, `name`, optional `agentId` and `where`, such as a terminal's title), `command` (without `sudo`; bios joins the kernel's argv shell-quoted), optional `target` (the user to run as; shown as `sudo -u {target}` unless `root`), optional `cwd`, `createdAt`, `expiresAt` (epoch ms) and `always` (offer Always allow, which covers the requester's user and this exact command). With `decide(id, 'once' \| 'always' \| 'deny')`, which resolves once the decision is recorded and then drops the request from `list()`, the tab shows a warning alert dialog (`dialog[data-approval][data-kind][data-requester]`): "Allow sudo?", "{name} wants to run this with sudo[ in {where}]:" for an agent or "You ran this with sudo[ in {where}] (as {name}):" for a user, the command, "In {cwd}", a notice when the command changes secret rules, what Always allow does, "Denied automatically in m:ss." and how many more wait, with **Deny** (`data-action="deny"`, focused; Escape also denies), **Always allow** (`always`) and **Allow once** (`once`). A command that names `/etc/fnox` (or anything in it) or `/etc/slicc/secrets-policy.toml`, resolved against `cwd`, globs included (with quotes and backslashes removed, operands after `--` included; a path with `[`, `{` or `$` counts when the part before it could lead to them), or that names `/`, `/etc` or `/etc/slicc` in a command other than a read-only one (such as `ls`, `cat`, `stat` or `grep`), or runs with `cwd` in `/etc/fnox`, or names a relative path with `fnox` or `secrets-policy` in it wherever `cd` may have led, gets "This changes secret rules." with the path (`[data-secret-rules]`, the path in the attribute). Read-only commands count only by bare name or from `/bin` and `/usr/bin`. `/proc/<pid>/root` counts as `/` and `/proc/self/cwd` as `cwd`. Otherwise a command with a parameter expansion or command substitution (`$…`, backticks), a `~+` or `~-` path or a `~` path that climbs with `..`, a path through another process's `/proc/<pid>/cwd`, `fd` or `map_files` or through `/dev/fd`, or more than 4096 characters, gets "This may change secret rules." with the first such part in the attribute. Other symlinks aren't followed. Either way there's no Always allow, even when `always` is `true` (`decide(id, 'always')` isn't called for it), so each change to the rules is asked for. For half a second after a request shows, only Deny works, so a double-click can't approve it. A rejected `decide` shows its message in the dialog (`[data-error]`, `dialog[data-rejected]`) in place of the countdown, with only **Close** (`data-action="close"`, focused; Escape also closes), which moves on to the next waiting request or closes the dialog. Pass `decide` in every tab: the first decision wins. When a request ends without a decision in this tab, emit `ended` with `{ id, reason }` and drop it from `list()`: `answered` (another tab decided) shows "Answered in another tab", `expired` shows "Sudo request timed out", each with the command and **Close** (`data-action="close"`, `dialog[data-gone]`), and `withdrawn` (the requester went away) closes the dialog silently. Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it. Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "Answer it in the tab running SLICC. Denied automatically at {time}." A guest request has `kind` `guest-message` (`text`, optional `attachments` of `{ name, mimeType, size }`) or `guest-tool` (`tool`, `summary`), plus `requester` (`kind` `guest`), `agentId`, optional `agentName`, `createdAt`, `expiresAt`, `always: false` and an optional `verdict` (`{ decision: 'allow' \| 'deny', reason }`, the automatic check's view). Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); then "The automatic check would allow this: {reason}" or "…deny this: {reason}" (`[data-verdict]`, the decision in the attribute). It offers only **Deny** and **Allow once**, whatever `always` says. `decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo. Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out". Without `decide`, a guest request's notice reads "Waiting for approval of a guest’s message" with "{name}'s message to {agentName} waits for approval on the device in control.", or "Waiting for approval of a guest’s tool call" with "{agentName} wants to run {tool} for {name}, which waits for approval on the device in control.", then "Denied automatically at {time}." |
255
+ | `approvals` (optional) | BIOS | `list()` returns waiting `Approval`s; emit `approvals` when the list changes. A `sudo` approval has `id`, `requester` (`kind` `agent` or `user`, `name`, optional `agentId` and `where`, such as a terminal's title), `command` (without `sudo`; bios joins the kernel's argv shell-quoted), optional `target` (the user to run as; shown as `sudo -u {target}` unless `root`), optional `cwd`, `createdAt`, `expiresAt` (epoch ms) and `always` (offer Always allow, which covers the requester's user and this exact command). With `decide(id, 'once' \| 'always' \| 'deny')`, which resolves once the decision is recorded and then drops the request from `list()`, the tab shows a warning alert dialog (`dialog[data-approval][data-kind][data-requester]`): "Allow sudo?", "{name} wants to run this with sudo[ in {where}]:" for an agent or "You ran this with sudo[ in {where}] (as {name}):" for a user, the command, "In {cwd}", a notice when the command changes secret rules, what Always allow does, "Denied automatically in m:ss." and how many more wait, with **Deny** (`data-action="deny"`, focused; Escape also denies), **Always allow** (`always`) and **Allow once** (`once`). A command that names `/etc/fnox` (or anything in it) or `/etc/slicc/secrets-policy.toml`, resolved against `cwd`, globs included (with quotes and backslashes removed, operands after `--` included; a path with `[`, `{` or `$` counts when the part before it could lead to them), or that names `/`, `/etc` or `/etc/slicc` in a command other than a read-only one (such as `ls`, `cat`, `stat` or `grep`), or runs with `cwd` in `/etc/fnox`, or names a relative path with `fnox` or `secrets-policy` in it wherever `cd` may have led, gets "This changes secret rules." with the path (`[data-secret-rules]`, the path in the attribute). Read-only commands count only by bare name or from `/bin` and `/usr/bin`. `/proc/<pid>/root` counts as `/` and `/proc/self/cwd` as `cwd`. Otherwise a command with a parameter expansion or command substitution (`$…`, backticks), a `~+` or `~-` path or a `~` path that climbs with `..`, a path through another process's `/proc/<pid>/cwd`, `fd` or `map_files` or through `/dev/fd`, or more than 4096 characters, gets "This may change secret rules." with the first such part in the attribute. Other symlinks aren't followed. Either way there's no Always allow, even when `always` is `true` (`decide(id, 'always')` isn't called for it), so each change to the rules is asked for. For half a second after a request shows, only Deny works, so a double-click can't approve it. A rejected `decide` shows its message in the dialog (`[data-error]`, `dialog[data-rejected]`) in place of the countdown, with only **Close** (`data-action="close"`, focused; Escape also closes), which moves on to the next waiting request or closes the dialog. Pass `decide` in every tab: the first decision wins. When a request ends without a decision in this tab, emit `ended` with `{ id, reason }` and drop it from `list()`: `answered` (another tab decided) shows "Answered in another tab", `expired` shows "Sudo request timed out", each with the command and **Close** (`data-action="close"`, `dialog[data-gone]`), and `withdrawn` (the requester went away) closes the dialog silently. Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it. Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "Answer it in the tab running SLICC. Denied automatically at {time}." A guest request has `kind` `guest-message` (`text`, optional `attachments` of `{ name, mimeType, size }`) or `guest-tool` (`tool`, `summary`), plus `requester` (`kind` `guest`), `agentId`, optional `agentName`, `createdAt`, `expiresAt`, `always: false` and an optional `verdict` (`{ decision: 'allow' \| 'deny' \| 'unsure', reason }`, the automatic check's view). Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); then "The automatic check would allow this: {reason}", "…deny this: {reason}" or "The automatic check wasn’t sure: {reason}" (`[data-verdict]`, the decision in the attribute). It offers only **Deny** and **Allow once**, whatever `always` says. `decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo. Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out". Without `decide`, a guest request's notice reads "Waiting for approval of a guest’s message" with "{name}'s message to {agentName} waits for approval on the device in control.", or "Waiting for approval of a guest’s tool call" with "{agentName} wants to run {tool} for {name}, which waits for approval on the device in control.", then "Denied automatically at {time}." |
256
256
  | `vault` (optional) | BIOS | The vault's view for SLICC, typed after `@ai-ecoverse/slicc-vault` (spectrum re-declares the types it uses, and `compat/vault.ts` checks that the package's types fit them). `status()` returns the `VaultStatus` (`locked`, `lock`, `scrub`, `waiting` requests, and optional `exportedAt` and `backupReminder`) or `null` while loading, and `list()` the `VaultList` (`secrets` as `SecretView`s, `refused` rules, `exits`, and the first `audit` page with `auditMore`) or `null`. Emit `status` and `list` when they change (bios re-lists after the vault's `changed` events), and `availability` when the optional `availability()` (`connecting`, `waiting`, `locked` or `ready`) changes. `audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`). The optional page ops, each hiding its control when missing: `put(name, value, request?)` (a value request's `put` must use its `name`), `cancel(request)`, `remove(name, kind?)`, `restore(name, kind?)`, `import(kind, bytes, { password?, pick? })` (a `pick` stage, then `done`), `export(passphrase)` (the bundle's bytes), `lock()` and `dismissBackupReminder()`. They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`). Values go to the vault only through `put`, and never through another port. `button()` returns a new `VaultButton`, `{ element, post(message), listen(fn) }` returning an unsubscribe: bios wraps the vault's `button.html` frame and its postMessage protocol, and spectrum only sends `ButtonMessage`s and receives `ButtonEvent`s. The passkey lock (unlock and protect) happens in the vault's own popup, which only a click in that frame can open. `notices`, `acknowledge` and the `notice` event are still typed, and ignored. |
257
257
  | `guests` (optional) | the tray hub | Guest seats on a cone. `list()` returns the `GuestSeat`s (`id`, `label`, `agentId`, the cone, `createdAt`, `expiresAt` or `null`, `gates`, `connected` and `pending`, how many of their messages or tool calls wait for review); a revoked or expired seat leaves it at once. Emit `seats` when it changes. `mint(agentId, { label, expiresAt, gates })` resolves with the `seat` and its `url`, which the panel shows once and never keeps, and `revoke(id)` ends a seat; both reject with an `Error` whose message is a sentence to show. The hub caps `expiresAt` at 30 days. `gates` (`{ messages, tools }`) are fixed at mint, so changing them means revoking and minting again; each is a `GuestGate`: `user` (a person with Control approves each one), `cone` (the shared cone itself decides, messages only), `agent` (the automatic check decides by the owner's intent and asks a person when unsure), `scoop:<name>` (that scoop decides) or `off` (no review: messages go straight in as follow-ups, messages only). The optional `unavailable()` returns a sentence when seats can't be minted here (no tray hub, or a follower without Control), or `null`; emit `unavailable` with it when it changes. |
258
258
  | `follow` (optional) | BIOS | Only on a follower page, a SLICC that shows another SLICC's agent over a link. `state()` returns the `FollowState`: `owner` (its display name), `link` (`connecting` for the first connection, `connected`, `reconnecting` after a connection dropped, `unlinked` when the owner unlinked this device, or `failed`), `access` (`{ view, control, terminal }`, what the owner allows this device), an optional `name` (this device's name on the owner, for the waiting page) and an optional `error` (shown when `failed`). Emit `follow` when it changes. Connected without `view` means waiting to be allowed. `leave()` forgets the link and the optional `retry()` tries again from `failed`. Pass only the surfaces a follower has to `<slicc-app>` (agents, chat, terminal and settings) and leave out `network`. |
@@ -328,7 +328,7 @@ Unit tests live in `test/unit/`, which is gitignored. The pre-commit hook runs t
328
328
 
329
329
  - **Setup** (`CERT_RUNNER`): main and the pull request are each built and served by `test/integration/chrome.mjs`, and each is open in a Chrome with a window under Xvfb, so hover works: main on CDP 9301 (`:8301/ui/?delay=0`), the pull request on CDP 9302 (`:8302`). Claude drives both with `playwright-core`, shoots the same states side by side (light and dark, desktop and a 420×860 mobile viewport), and reads spectrum.adobe.com, seeded with every URL in its sitemap.
330
330
  - **What it checks:** tokens only, never colour alone, contrast in dark, focus, copy, typography, icons, layout at 420, and the `data-*` hooks embedders test on. The prompt carries the setup pitfalls too: wait for fonts, blur focus, compare at the same scroll anchor, and use a real mobile viewport.
331
- - **The comment:** Claude writes `comment.md` and its screenshots. A later step publishes the screenshots as the run's `design-review-shots-<pr>` artifact and posts the comment, with each screenshot reference turned into a line naming the file, linked to that artifact. They aren't inlined: `gh pr comment --attach` needs a user token, which the workflow's token isn't.
331
+ - **The comment:** Claude writes `comment.md` and its screenshots, uploaded as the run's `design-review-comment-<pr>` and `design-review-shots-<pr>` artifacts. A separate `post` job on a fresh runner, the only one with the Cloudflare token, puts the screenshots in the R2 bucket slicc uses for PR screenshots (`slicc-pr-screenshots`, under `slicc-spectrum/pr-<n>/<run>/`) and posts the comment with them inline from its public URL. A screenshot that doesn't upload is linked to the artifact instead. The token is the org secret `CLOUDFLARE_API_TOKEN`; `CLOUDFLARE_ACCOUNT_ID`, `R2_BUCKET` and `R2_PUBLIC_BASE_URL` default to slicc's values.
332
332
 
333
333
  ## License
334
334
 
@@ -54,7 +54,11 @@ function guestFixtures(now2 = Date.now()) {
54
54
  text: "Is the pier map up to date?",
55
55
  createdAt: now2 + 2,
56
56
  expiresAt: now2 + 5 * minutes,
57
- always: false
57
+ always: false,
58
+ verdict: {
59
+ decision: "unsure",
60
+ reason: "it looks low-risk, but it isn\u2019t clear it serves what the owner asked for."
61
+ }
58
62
  }
59
63
  ];
60
64
  }