@ai-ecoverse/slicc-spectrum 1.56.0 → 1.57.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
@@ -58,7 +58,7 @@ Keystrokes reach `write` as the bytes a terminal would send: UTF-8 text, `\r` fo
58
58
 
59
59
  ### slicc-kernel
60
60
 
61
- `kernelBackend(kernel, { argv, cwd, env })` adapts slicc-kernel's terminal API to the interface above. `argv` defaults to `['bash', '-i']`. It is typed structurally, so this package doesn't depend on slicc-kernel. If the session fails, its error message is printed and `exit` reports status 1.
61
+ `kernelBackend(kernel, { argv, cwd, user, env })` adapts slicc-kernel's terminal API to the interface above. `argv` defaults to `['bash', '-i']`. `user` (a name or uid, slicc-kernel 1.44 and later) is who the session runs as; without it the kernel runs it as `root`, with `HOME=/root`. It is typed structurally, so this package doesn't depend on slicc-kernel. If the session fails, its error message is printed and `exit` reports status 1.
62
62
 
63
63
  ```js
64
64
  import { createKernel } from './node_modules/@ai-ecoverse/slicc-kernel/dist/index.js';
@@ -148,10 +148,10 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
148
148
  | `<slicc-settings>` | Theme, the model and thinking level for new cones, composer and diff preferences, and accounts to connect, reconnect or disconnect. An account whose status is `signing-in` (or a non-API-key account while its `connect` is pending) reads “Signing in…”; when the port has `cancel(id)`, its row offers Cancel (`data-action="cancel-sign-in"`), and the rejected `connect` that follows shows no error. Accounts with `group: 'mcp'` (MCP servers) get their own **MCP servers** heading (`.accounts[data-group=mcp]`). An `api-key` account asks for its `secret`: “{provider} API key” by default, “{provider} token” for `secret: 'token'`; when it is `expired` it reads **API key rejected** or **Token rejected**. An MCP server with `auth: 'oauth'` signs in in a pop-up: its button reads **Sign in**, and it reads **Needs sign-in** when `disconnected` and **Sign-in expired** when `expired`. |
149
149
  | `<slicc-updates>` | Install / Update: component states, version changes, last checks, download and link counts, action buttons, failures with Retry, and collapsible install logs. Opens at boot until the agent is ready without failures. Background updates stay in the header's update status; new failures reopen the panel, and after a reload into a boot that isn't ready yet such a panel closes once the agent is ready. Every automatic close is saved to the layout. When the port offers packages, an Optional packages section lists them below as compact rows, each with its state, version, download size, requirements, commands, progress, errors and Install log; packages never count in the header or keep the panel open at boot. Packages the user added or pinned with pnpm show up in their own Your packages section below it, with a Pinned or Added light, their version against seven's, and Unpin or Remove. Also available through View and the rails. |
150
150
  | `<slicc-memory>` | What agents remember, per scope (everyone, a cone or a role), grouped by section: search, filter by tag, expand, edit, add and forget. Free text outside an entry shows “From MEMORY.md”. |
151
- | `<slicc-monitor>` | The live monitor: vitals with sparklines (active agents, spend, budget, fullest context), alerts, and the topology (cones and scoops, terminals, tabs, pending changes, sprinkles, tray followers). |
151
+ | `<slicc-monitor>` | The live monitor: vitals with sparklines (active agents, spend, budget, fullest context), alerts, and the topology (cones and scoops, terminals, tabs, pending changes, sprinkles, tray followers). Each section (`li.section[data-id]`) shows its count and an attention badge, and opens its surface with **Open** where one exists (`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). |
152
152
  | `<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. |
153
153
  | `<slicc-sprinkle>` | A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit. 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. |
154
- | `<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-node, through a linked device's browser, that slicc-node has no browser attached when `route` is `proxy` 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 `npx @ai-ecoverse/slicc-node` with a copy button (`data-action="copy-command"`), folded away when the route already reaches the whole web (`proxy`, `extension` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line names the 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 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"`). 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. |
154
+ | `<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-node, through a linked device's browser, that slicc-node has no browser attached when `route` is `proxy` 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 `npx @ai-ecoverse/slicc-node` with a copy button (`data-action="copy-command"`), folded away when the route already reaches the whole web (`proxy`, `extension` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line names the 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"`). 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. |
155
155
  | `<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. |
156
156
  | `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
157
157
  | `<slicc-prompt>` | The name dialog `prompt()` opens. Not for direct use. |
@@ -205,11 +205,11 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
205
205
  | `settings` | local storage | Theme, model, thinking level, composer and diff preferences, the models on offer, and accounts. |
206
206
  | `changes` (optional) | `@ai-ecoverse/slicc-agent` | Pending changes from git instead of the file system: `changes()`, `accept(path)` (stage), `revert(path)` (discard), and a `changes` event, as on `files`. When present, the UI reads changes only from this port, including the rail's count. A `Change` may carry `repo`, its repository root; put the active agent's repository first. The optional `unavailable()` returns why there can be no changes (no git, no repository), which the panel shows with commands in backticks made copyable, or `null`; emit `changes` when it flips. |
207
207
  | `memory` | `@ai-ecoverse/slicc-agent` | Memories with scope, section, tag and body; `save` and `remove`. `source: 'notes'` marks free text the panel shows as an entry titled after its section; saving it makes it an entry. The optional `scopes()` lists the scope picker's choices as `{ id, label, group }`, ungrouped first, then under “Cones” and “Roles”; without it the picker offers “Everyone” (`global`) and each cone. The panel re-reads `scopes()` on `memories` and `agents`, so emit `memories` when the list changes. A scope that disappears falls back to `global`. |
208
- | `monitor` | all of them | A snapshot of vitals, alerts and sections, re-emitted as the system changes, and `resync`. |
208
+ | `monitor` | all of them | A snapshot of vitals, alerts and sections, re-emitted as the system changes, and `resync`. A `MonitorRow` has `name`, `meta`, `status` (`active`, `idle`, `warn`, `error`), optional `badges`, an optional `depth` (the indent level, for trees such as the task graph) and an optional stable `id` (the key while rows update). |
209
209
  | `sprinkles` | `@ai-ecoverse/slicc-agent` | Sprinkles (`inline` ones show as dips in the chat, not in the rail) and `send`, which turns a sprinkle's lick into a lick on its cone. The rail groups sprinkles by their `agentId`: the selected cone's come first, then the other cones' in the agents list's order, each group set apart by a line (`.rail .cones`) and each button carrying `data-cone`. A sprinkle of another cone reads `{cone name} / {title}` in its tooltip, its label and the tab it opens; the name comes from the agents list, so give each sprinkle an `id` that stays stable when a cone is renamed, such as `{coneId}/{name}`. The optional `call(id, method, args)` serves the [sprinkle bridge](#sprinkle-bridge) for sprinkle `id`: `readFile` and `exists` (`[path]`) resolve with a string and a boolean, `getState` (`[]`) with the saved value or `null`, and `setState` (`[value]`) once it is saved. The adapter owns path policy and keeps state per sprinkle; `<slicc-sprinkle>` awaits `getState` before it builds a frame, mirrors what the frame saves, and forwards only these four methods from its own frame. The dummy keeps state in memory and maps `/shared/x` to `/home/x` in its files, refusing anything outside `/home`. |
210
- | `tray` | the tray protocol | Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`. |
210
+ | `tray` | the tray protocol | Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`. `spent`, `rate` and `budget` are optional: the budget chip's tooltip and label leave out the spend while `spent` is 0 or missing, and the rate while `rate` is, and the chip itself hides without `budget`, or while it's at 0% with nothing spent. |
211
211
  | `updates` (optional) | BIOS installers | `list()` returns `UpdateItem` rows for BIOS, kernel, agent, grammars, global pnpm packages, skills and UI. `ready()` explicitly reports agent readiness, independently of agent activity. Emit `items` whenever rows or readiness change. Use `queued` for a row that waits for another component before it starts; it reads "Waiting" and doesn't count as updating. Use `starting` for an installed row that is only starting up: it reads "Starting" without a checked line, and the header shows a neutral "Starting" only when nothing is failed, updating or ready. A `failed` row with an installed `from` version reads "Update failed". `act(id, action)` resolves when dispatched or rejects with a displayed error; actions are `retry`, `update-now`, `restart-agent` and `reload`. Missing ports preserve existing adapters and report disconnected information in the panel. Optional `packages()` returns `PackageItem` rows for optional tools, each with a short catalog `id`, its npm `package` (shown in the name's tooltip and at the top of its Install log), `label`, a one-sentence `description`, the `commands` it provides, `requires` (other package ids, read as "Uses Python" while it's installed, or "Installs Python too" on a row that isn't installed yet; an installed row leaves out a requirement that isn't installed, since its warning says so), optional `notes` (short plain-text lines shown muted under the row, such as "1 untested dependency: …"; leave it out when there's nothing to say), optional `warnings` (lines with an alert icon, `data-warnings`, such as "Needs Python, which isn't installed. Install Python again to use it."; the dummy adds that one when a required package is removed), `state` (`available`, `queued`, `installing`, `installed`, `outdated`, `updating`, `removing` or `failed`), the installed `version`, the `offered` version and its `size` in bytes, `progress` counted in packages (or `null` for an indeterminate bar), `error`, `actions` (`install`, `update`, `remove`, `retry`, `unpin`) and an optional `log`. A row with `origin: 'user'` (ids such as `user:agent:<name>`) goes into Your packages, `section[data-section="user-packages"]`, which only shows when there is one; its `pin` holds `base` (the version seven ships, or `null` for a package seven doesn't ship), the user's `range` and the `project` folder, such as `/opt/agent`. An installed pin reads "Pinned" with `0.24.0 · seven ships 0.25.0 · in /opt/agent` ("the version seven ships" when they match), and an addition reads "Added" with `^1.2.0 → 1.2.4 · in /opt/agent`; `.meta[data-pin]` is `pinned` or `added`. A pin that is `removing` reads "Unpinning"; other states keep their usual lights. Offer `unpin` for pins and `remove` for additions. Emit `packages` whenever they change. `actPackage(id, action)` keeps the row's button pending until it settles; it shows any confirm itself, resolves without a change when that confirm is cancelled, and rejects with a displayed error on a real failure. Hooks: `section[data-section="packages"]`, `li[data-id][data-state]`, buttons with `data-action`, `[data-requires]`, `[data-notes]` and `[data-error]`. Real installer wiring belongs to ai-ecoverse/slicc-bios#70. |
212
- | `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`proxy` for slicc-node, `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), `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) and optional `terminals` (how many terminals it has open here). 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. Without the port, the indicator and the Network panel don't show. |
212
+ | `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`proxy` for slicc-node, `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) and optional `terminals` (how many terminals it has open here). 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. Without the port, the indicator and the Network panel don't show. |
213
213
  | `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. Hooks: `[data-notice="<id>"]`, buttons with `data-action`, `[data-dismiss]` and `[data-error]`. |
214
214
  | `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"]`). |
215
215
  | `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}", 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`). 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. 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}." |
@@ -219,6 +219,8 @@ The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current'
219
219
 
220
220
  `createDummyModel({ follow: 'connecting' | 'waiting' | 'watch' | 'control' | 'full' | 'reconnecting' | 'unlinked' | 'failed' })` (`?follow=` on `/ui/`, which then leaves out the network port and offers only agents, chat, terminal and settings) makes the page a follower of SLICC on quay-studio: connecting, waiting to be allowed, View only, View and Control, everything, reconnecting, unlinked, or failed, where Try again connects with everything allowed. Leave counts in `model.follow.left` (the `/ui/` page then drops `?follow=`), and `model.follow.setScenario()` switches it. `followFixtures` and `DummyFollow` are exported from `/dummy`. `?links=followers` lists quay-studio (a CLI on this computer, Control always allowed), pier-nuc (a remote CLI, prompting), bay-laptop (a follower with everything allowed, watching, a terminal open), reef-phone (View only, watching) and tide-tablet (asking to follow); every links scenario has a follow link.
221
221
 
222
+ `createDummyModel({ monitor: 'agent' })` (`?monitor=agent` on `/ui/`) adds the agent's Tasks (a tree by `depth`), Processes and Recent problems sections to the monitor.
223
+
222
224
  `createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' })` (`?approvals=` on `/ui/`) adds waiting sudo requests: one from an agent, one from a terminal user, three in a row (the last as `www-data`), one without `decide`, one whose `decide` fails, one that expires in 24 seconds, or two whose first is answered in another tab or withdrawn after a few seconds. Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
223
225
 
224
226
  On phones the update status and the tray connection show only their status light (the tray's budget is hidden) and the network indicator a cloud icon, with the text in their labels and tooltips; the agent picker keeps room for the cone's name, cut with an ellipsis when long, and the bottom rail scrolls when needed. The update status comes first so progress and failures remain reachable on narrow screens.
@@ -240,7 +242,7 @@ app.model = createKernelModel({ kernel, root, storage: localStorage, files: { sk
240
242
  ```
241
243
 
242
244
  - **`KernelFiles`** lists the OPFS root as the tree, reads and writes files through the OPFS API, and removes them recursively. Directories in `skip` show up but aren't scanned; paths in `hide` (and everything under them) don't show up at all. `start()` rescans on every `FileSystemObserver` record, or every `interval` ms (default 2000) where the browser can't observe OPFS, and emits `files` and a `file` per changed file, so open tabs follow what the terminal writes. Without an agent there are no pending changes.
243
- - **`KernelTerminals`** opens `bash -i` (or `argv`) in `cwd` (default `/home`) through `kernelBackend`, one session per terminal, as many as the user opens. `open` (default 1) is how many it starts with.
245
+ - **`KernelTerminals`** opens `bash -i` (or `argv`) in `cwd` (default `/home`) as `user` (default the kernel's, `root`) through `kernelBackend`, one session per terminal, as many as the user opens. `open` (default 1) is how many it starts with.
244
246
  - Everything else is idle (`IdleAgent`, `IdleBrowser`, `IdleMemory`, `IdleMonitor`, `IdleSprinkles`, `IdleTray`, from `idleModel(storage)`): empty lists, and actions that need a backend throw. Settings are kept in `storage`. Leave their surfaces out of `surfaces`.
245
247
 
246
248
  Each section is its own port, so an embedder can mix real and dummy per section, for example `{ ...createDummyModel(), files: new KernelFiles(root) }`.
@@ -21,7 +21,7 @@ function cwdWatcher(onCwd) {
21
21
  pending = open >= 0 && !text.slice(open).match(/\x07|\x1b\\/) ? text.slice(open) : "";
22
22
  };
23
23
  }
24
- function kernelBackend(kernel, { argv = ["bash", "-i"], cwd, env, onCwd } = {}) {
24
+ function kernelBackend(kernel, { argv = ["bash", "-i"], cwd, user, env, onCwd } = {}) {
25
25
  return {
26
26
  async open(sink, { cols, rows }) {
27
27
  const watch = onCwd && cwdWatcher(onCwd);
@@ -29,7 +29,14 @@ function kernelBackend(kernel, { argv = ["bash", "-i"], cwd, env, onCwd } = {})
29
29
  watch?.(bytes);
30
30
  sink.output(bytes);
31
31
  };
32
- const terminal = await kernel.openTerminal(argv, { cwd, env, cols, rows, onData });
32
+ const terminal = await kernel.openTerminal(argv, {
33
+ cwd,
34
+ ...user === void 0 ? {} : { user },
35
+ env,
36
+ cols,
37
+ rows,
38
+ onData
39
+ });
33
40
  terminal.exited.then(
34
41
  (status) => sink.exit(status),
35
42
  (error) => {
@@ -53,4 +60,4 @@ export {
53
60
  cwdPromptCommand,
54
61
  kernelBackend
55
62
  };
56
- //# sourceMappingURL=chunk-YVJG5657.js.map
63
+ //# sourceMappingURL=chunk-XYPGNJAO.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/kernel-backend.ts"],
4
+ "sourcesContent": ["import type { TerminalBackend, TerminalSignal } from './backend.ts';\n\nexport interface KernelTerminal {\n readonly exited: Promise<number>;\n write(data: Uint8Array): void;\n resize(cols: number, rows: number): void;\n signal(name: TerminalSignal): void;\n close(): void;\n}\n\nexport interface KernelTerminalOptions {\n cwd?: string;\n user?: string | number;\n env?: Record<string, string>;\n cols: number;\n rows: number;\n onData: (bytes: Uint8Array) => void;\n}\n\nexport interface TerminalKernel {\n openTerminal(argv: string[], options: KernelTerminalOptions): Promise<KernelTerminal>;\n}\n\nexport interface KernelBackendOptions {\n argv?: string[];\n cwd?: string;\n user?: string | number;\n env?: Record<string, string>;\n onCwd?: (cwd: string) => void;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\nconst osc7 = /\\x1b\\]7;file:\\/\\/[^/\\x07\\x1b]*(\\/[^\\x07\\x1b]*)(?:\\x07|\\x1b\\\\)/g;\n\nexport const cwdPromptCommand = `printf '\\\\033]7;file://%s%s\\\\007' \"\\${HOSTNAME:-}\" \"$PWD\"`;\n\nfunction decodePath(path: string): string {\n try {\n return decodeURIComponent(path);\n } catch {\n return path;\n }\n}\n\nexport function cwdWatcher(onCwd: (cwd: string) => void): (bytes: Uint8Array) => void {\n let pending = '';\n return (bytes) => {\n const text = pending + decoder.decode(bytes, { stream: true });\n let last: string | undefined;\n for (const match of text.matchAll(osc7)) last = match[1];\n if (last !== undefined) onCwd(decodePath(last));\n const open = text.lastIndexOf('\\x1b]7;');\n pending = open >= 0 && !text.slice(open).match(/\\x07|\\x1b\\\\/) ? text.slice(open) : '';\n };\n}\n\nexport function kernelBackend(\n kernel: TerminalKernel,\n { argv = ['bash', '-i'], cwd, user, env, onCwd }: KernelBackendOptions = {}\n): TerminalBackend {\n return {\n async open(sink, { cols, rows }) {\n const watch = onCwd && cwdWatcher(onCwd);\n const onData = (bytes: Uint8Array) => {\n watch?.(bytes);\n sink.output(bytes);\n };\n const terminal = await kernel.openTerminal(argv, {\n cwd,\n ...(user === undefined ? {} : { user }),\n env,\n cols,\n rows,\n onData,\n });\n terminal.exited.then(\n (status) => sink.exit(status),\n (error) => {\n sink.output(encoder.encode(`\\r\\n${error instanceof Error ? error.message : error}\\r\\n`));\n sink.exit(1);\n }\n );\n return {\n write: (data) => terminal.write(data),\n resize: (c, r) => terminal.resize(c, r),\n signal: (name) => terminal.signal(name),\n close: () => terminal.close(),\n };\n },\n };\n}\n"],
5
+ "mappings": ";AA+BA,IAAM,UAAU,IAAI,YAAY;AAChC,IAAM,UAAU,IAAI,YAAY;AAChC,IAAM,OAAO;AAEN,IAAM,mBAAmB;AAEhC,SAAS,WAAW,MAAsB;AACxC,MAAI;AACF,WAAO,mBAAmB,IAAI;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,WAAW,OAA2D;AACpF,MAAI,UAAU;AACd,SAAO,CAAC,UAAU;AAChB,UAAM,OAAO,UAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAC7D,QAAI;AACJ,eAAW,SAAS,KAAK,SAAS,IAAI,EAAG,QAAO,MAAM,CAAC;AACvD,QAAI,SAAS,OAAW,OAAM,WAAW,IAAI,CAAC;AAC9C,UAAM,OAAO,KAAK,YAAY,SAAS;AACvC,cAAU,QAAQ,KAAK,CAAC,KAAK,MAAM,IAAI,EAAE,MAAM,aAAa,IAAI,KAAK,MAAM,IAAI,IAAI;AAAA,EACrF;AACF;AAEO,SAAS,cACd,QACA,EAAE,OAAO,CAAC,QAAQ,IAAI,GAAG,KAAK,MAAM,KAAK,MAAM,IAA0B,CAAC,GACzD;AACjB,SAAO;AAAA,IACL,MAAM,KAAK,MAAM,EAAE,MAAM,KAAK,GAAG;AAC/B,YAAM,QAAQ,SAAS,WAAW,KAAK;AACvC,YAAM,SAAS,CAAC,UAAsB;AACpC,gBAAQ,KAAK;AACb,aAAK,OAAO,KAAK;AAAA,MACnB;AACA,YAAM,WAAW,MAAM,OAAO,aAAa,MAAM;AAAA,QAC/C;AAAA,QACA,GAAI,SAAS,SAAY,CAAC,IAAI,EAAE,KAAK;AAAA,QACrC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD,eAAS,OAAO;AAAA,QACd,CAAC,WAAW,KAAK,KAAK,MAAM;AAAA,QAC5B,CAAC,UAAU;AACT,eAAK,OAAO,QAAQ,OAAO;AAAA,EAAO,iBAAiB,QAAQ,MAAM,UAAU,KAAK;AAAA,CAAM,CAAC;AACvF,eAAK,KAAK,CAAC;AAAA,QACb;AAAA,MACF;AACA,aAAO;AAAA,QACL,OAAO,CAAC,SAAS,SAAS,MAAM,IAAI;AAAA,QACpC,QAAQ,CAAC,GAAG,MAAM,SAAS,OAAO,GAAG,CAAC;AAAA,QACtC,QAAQ,CAAC,SAAS,SAAS,OAAO,IAAI;AAAA,QACtC,OAAO,MAAM,SAAS,MAAM;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -3935,12 +3935,100 @@ function wave(seed, length = 24) {
3935
3935
  (_, i) => Math.round(40 + 25 * Math.sin((i + seed) / 3) + 10 * Math.cos(i * seed / 5))
3936
3936
  );
3937
3937
  }
3938
+ var agentSections = [
3939
+ {
3940
+ id: "tasks",
3941
+ label: "Tasks",
3942
+ rows: [
3943
+ { id: "t1", name: "generating", meta: "sliccy \xB7 running \xB7 12s", status: "active" },
3944
+ {
3945
+ id: "t2",
3946
+ name: "tool bash",
3947
+ meta: "sliccy \xB7 running \xB7 8s",
3948
+ status: "active",
3949
+ depth: 1,
3950
+ badges: ["bash"]
3951
+ },
3952
+ {
3953
+ id: "t3",
3954
+ name: "scoop quiet-otter",
3955
+ meta: "quiet-otter \xB7 waiting \xB7 2m",
3956
+ status: "idle",
3957
+ depth: 1,
3958
+ badges: ["waits on 2"]
3959
+ },
3960
+ {
3961
+ id: "t4",
3962
+ name: "tool read_file",
3963
+ meta: "quiet-otter \xB7 completing \xB7 1s",
3964
+ status: "active",
3965
+ depth: 2
3966
+ },
3967
+ {
3968
+ id: "t5",
3969
+ name: "report for quiet-otter",
3970
+ meta: "quiet-otter \xB7 pending \xB7 2m",
3971
+ status: "idle",
3972
+ depth: 2,
3973
+ badges: ["background"]
3974
+ },
3975
+ {
3976
+ id: "t6",
3977
+ name: "cron tide-check",
3978
+ meta: "session \xB7 aborting \xB7 14s",
3979
+ status: "warn",
3980
+ badges: ["aborting"]
3981
+ }
3982
+ ]
3983
+ },
3984
+ {
3985
+ id: "processes",
3986
+ label: "Processes",
3987
+ rows: [
3988
+ {
3989
+ id: "p412",
3990
+ name: "bash",
3991
+ meta: "pid 412 \xB7 slicc \xB7 3m",
3992
+ status: "idle",
3993
+ badges: ["terminal"]
3994
+ },
3995
+ {
3996
+ id: "p418",
3997
+ name: "node build.mjs --watch",
3998
+ meta: "pid 418 \xB7 slicc \xB7 1m",
3999
+ status: "active",
4000
+ badges: ["sliccy"]
4001
+ },
4002
+ {
4003
+ id: "p433",
4004
+ name: "grep -r harbor .",
4005
+ meta: "pid 433 \xB7 slicc \xB7 40s",
4006
+ status: "warn",
4007
+ badges: ["quiet-otter"]
4008
+ }
4009
+ ]
4010
+ },
4011
+ {
4012
+ id: "problems",
4013
+ label: "Recent problems",
4014
+ rows: [
4015
+ {
4016
+ id: "e1",
4017
+ name: "quiet-otter \xB7 turn",
4018
+ meta: "The model returned an error: overloaded. Retrying in 8 s.",
4019
+ status: "error"
4020
+ }
4021
+ ]
4022
+ }
4023
+ ];
3938
4024
  var DummyMonitor = class extends Emitter {
3939
4025
  #model;
3940
4026
  #tick = 0;
3941
- constructor(model) {
4027
+ #extra;
4028
+ constructor(model, extra = []) {
3942
4029
  super();
3943
4030
  this.#model = model;
4031
+ this.#extra = extra;
3944
4032
  const update = () => this.resync();
3945
4033
  model.agent.on("agents", update);
3946
4034
  model.terminals.on("terminals", update);
@@ -3959,7 +4047,9 @@ var DummyMonitor = class extends Emitter {
3959
4047
  const fill = Math.max(...agents2.map((candidate) => candidate.contextFill), 0);
3960
4048
  const agentRows = agents2.map(
3961
4049
  (candidate) => ({
3962
- name: candidate.kind === "scoop" ? `\u21B3 ${candidate.name}` : candidate.name,
4050
+ id: candidate.id,
4051
+ name: candidate.name,
4052
+ depth: candidate.kind === "scoop" ? 1 : 0,
3963
4053
  meta: `${candidate.status} \xB7 ${Math.round(candidate.contextFill * 100)}% context`,
3964
4054
  status: statuses[candidate.status],
3965
4055
  badges: candidate.unread ? [`${candidate.unread} unread`] : []
@@ -3978,17 +4068,19 @@ var DummyMonitor = class extends Emitter {
3978
4068
  {
3979
4069
  id: "spend",
3980
4070
  label: "Spend",
3981
- value: `$${status.spent.toFixed(2)}`,
3982
- delta: `$${status.rate.toFixed(2)}/h`,
4071
+ value: `$${(status.spent ?? 0).toFixed(2)}`,
4072
+ delta: `$${(status.rate ?? 0).toFixed(2)}/h`,
3983
4073
  series: wave(this.#tick + 7)
3984
4074
  },
3985
- {
3986
- id: "budget",
3987
- label: `Budget (${status.budget.window})`,
3988
- value: `${status.budget.percent}%`,
3989
- unit: status.budget.resets,
3990
- ratio: status.budget.percent / 100
3991
- },
4075
+ ...status.budget ? [
4076
+ {
4077
+ id: "budget",
4078
+ label: `Budget (${status.budget.window})`,
4079
+ value: `${status.budget.percent}%`,
4080
+ unit: status.budget.resets,
4081
+ ratio: status.budget.percent / 100
4082
+ }
4083
+ ] : [],
3992
4084
  {
3993
4085
  id: "context",
3994
4086
  label: "Fullest context",
@@ -4014,6 +4106,7 @@ var DummyMonitor = class extends Emitter {
4014
4106
  ],
4015
4107
  sections: [
4016
4108
  { id: "agents", label: "Cones and scoops", rows: agentRows },
4109
+ ...this.#extra,
4017
4110
  {
4018
4111
  id: "terminals",
4019
4112
  label: "Terminals",
@@ -4283,7 +4376,7 @@ function linksFixtures(scenario, rotation = 0) {
4283
4376
  const url = joinUrl(rotation);
4284
4377
  const base = {
4285
4378
  joinUrl: url,
4286
- joinCommand: `npx sliccy ${url} follow`,
4379
+ joinCommand: `npx sliccy@next ${url} follow`,
4287
4380
  followUrl: followUrl(rotation),
4288
4381
  permission: null
4289
4382
  };
@@ -4452,7 +4545,7 @@ var DummyNetwork = class extends Emitter {
4452
4545
  this.#links = {
4453
4546
  ...this.#links,
4454
4547
  joinUrl: url,
4455
- joinCommand: `npx sliccy ${url} follow`,
4548
+ joinCommand: `npx sliccy@next ${url} follow`,
4456
4549
  followUrl: followUrl(this.#rotation)
4457
4550
  };
4458
4551
  this.#emit();
@@ -5282,7 +5375,8 @@ function createDummyModel({
5282
5375
  approvals = "off",
5283
5376
  follow = "off",
5284
5377
  browser: scenario = "idle",
5285
- sprinkles: sprinkles2 = "builtin"
5378
+ sprinkles: sprinkles2 = "builtin",
5379
+ monitor = "dummy"
5286
5380
  } = {}) {
5287
5381
  const time = clock(delay);
5288
5382
  const mount = mounts === "off" ? null : { scenario: mounts, point: mountPoint, files: mounted };
@@ -5340,7 +5434,10 @@ function createDummyModel({
5340
5434
  ...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time) },
5341
5435
  ...follow === "off" ? {} : { follow: new DummyFollow(follow, time) }
5342
5436
  };
5343
- return { ...base, monitor: new DummyMonitor(base) };
5437
+ return {
5438
+ ...base,
5439
+ monitor: new DummyMonitor(base, monitor === "agent" ? agentSections : void 0)
5440
+ };
5344
5441
  }
5345
5442
  export {
5346
5443
  DummyApprovals,