@ai-ecoverse/slicc-spectrum 1.56.2 → 1.58.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 +9 -7
- package/dist/slicc-dummy.js +203 -23
- package/dist/slicc-dummy.js.map +3 -3
- package/dist/slicc-ui.js +237 -20
- package/dist/slicc-ui.js.map +3 -3
- package/dist/types/app/files.d.ts +4 -0
- package/dist/types/app/monitor.d.ts +14 -0
- package/dist/types/app/tray.d.ts +4 -2
- package/dist/types/dummy/files.d.ts +3 -1
- package/dist/types/dummy/index.d.ts +2 -1
- package/dist/types/dummy/monitor.d.ts +3 -2
- package/dist/types/dummy/network.d.ts +6 -2
- package/dist/types/model/types.d.ts +25 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
|
|
|
139
139
|
| `<slicc-approvals>` | The model's waiting `approvals` as a modal alert dialog, one at a time, oldest first; `<slicc-app>` mounts it. Renders nothing without `approvals.decide`. |
|
|
140
140
|
| `<slicc-follow>` | The model's `follow` state. By default a header chip (`#chip[data-follow]` with the link state: **Connecting to**, **Following**, **Reconnecting to**, **Unlinked from** or **Can’t reach** the owner; `compact` keeps only a follow icon and the light) whose popover says what this device can do (`[data-can]`), that the conversation may be out of date while reconnecting, and with Control that sending waits (`[data-reconnecting]`), and has **Leave** (`data-action="leave"`, behind a confirm unless unlinked). With `page`, the full page for `connecting`, `waiting` (connected without View: "Waiting for {owner} to allow this device"), `unlinked` and `failed` (`[data-follow-page]`), with **Try again** (`data-action="retry"`) when failed and the port has `retry`, and **Leave**. A rejected call shows its message (`[data-error]`). Renders nothing without the port. |
|
|
141
141
|
| `<slicc-composer>` | The message box. <kbd>Enter</kbd> sends, or steers a running reply: the message joins after the current tool round, without interrupting; <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>Enter</kbd> queues a follow-up that runs after the reply and can be withdrawn; <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line; <kbd>Tab</kbd> takes the suggested follow-up; <kbd>↑</kbd>/<kbd>↓</kbd> at the start or end walk this agent's history; <kbd>Esc</kbd> stops. `/` completes commands (`/clear`, `/compact`, `/model`, `/thinking`, `/scoop`, `/stop`, `/freeze` when the port can freeze, `/theme`) and their arguments, `@` mentions agents and files. Attach from the computer, from SLICC's files, a browser screenshot or a shared secret, or paste and drop files (25 MB each). Queued messages wait above it, to send now or drop. Model and thinking pickers, and dictation where the browser has speech recognition. Drafts are kept per agent. On a follower while the link reconnects, the field is disabled and says so; the draft stays. |
|
|
142
|
-
| `<slicc-files>` | The file system as a `<slicc-file-tree>`, with pending changes as git status. Opening a file opens it in a tab. With `files.mountFolder`, **Mount a folder…** (`data-action="mount-folder"`) mounts a local folder, then selects and expands it in the tree; with `files.mounts`, a **Mounted** strip lists the mount points, each with **Eject** (`data-action="eject"`, `data-path` the mount point) when the port has `eject`. With `files.needsFolder`, a **Needs a folder** section lists the mount points waiting for a folder, each with **Insert folder…** (`data-action="insert-folder"`, `data-id` the mount point), which calls `mountFolder(path)`. A failed mount, insert or eject shows its message under the bar. |
|
|
142
|
+
| `<slicc-files>` | The file system as a `<slicc-file-tree>`, with pending changes as git status. Opening a file opens it in a tab. With `files.mountFolder`, **Mount a folder…** (`data-action="mount-folder"`) mounts a local folder, then selects and expands it in the tree; with `files.mounts`, a **Mounted** strip lists the mount points (with `files.mountInfo`, each mount with a `source` reads `/mnt/proj · ref-node/proj`, `data-source` on its `.path`), each with **Eject** (`data-action="eject"`, `data-path` the mount point) when the port has `eject`. With `files.needsFolder`, a **Needs a folder** section lists the mount points waiting for a folder, each with **Insert folder…** (`data-action="insert-folder"`, `data-id` the mount point), which calls `mountFolder(path)`. A failed mount, insert or eject shows its message under the bar. |
|
|
143
143
|
| `<slicc-file-view>` | One file (`path`), highlighted, with its pending change and a link to the diff. |
|
|
144
144
|
| `<slicc-changes>` | Pending changes with their author. Opens the diff, accepts or reverts one change or all of them. Changes with a `repo` show their folder relative to it, and no author when `agentId` is `null`; they group under a header per repository (its path and count), even when there is only one, in the order the port returns them. In a diff panel narrower than 480 px, Accept and Revert (`data-action="accept"`, `data-action="revert"`) show only their icons. Revert and Revert all ask with a destructive confirm that says the uncommitted (or, without a repo, pending) changes can't be undone; a failed revert shows its message under the bar. When the port's `unavailable()` returns a string, the panel shows it instead of the list, with each `backtick` span as inline code in the text's color and, below, each command once with a **Copy** button (`data-command`, `data-action="copy-command"`). |
|
|
145
145
|
| `<slicc-diff-panel>` | The diff of one pending change (`path`), unified or split, with accept and revert. A change with `before` and `after` both `null` (a binary file, or text over 1 MB) shows “No diff for this file” and Open the file instead, and keeps Accept and Revert. |
|
|
@@ -148,7 +148,7 @@ 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
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. |
|
|
@@ -199,26 +199,28 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
|
|
|
199
199
|
| Port | Later backed by | |
|
|
200
200
|
| :--- | :--- | :--- |
|
|
201
201
|
| `agent` | `@ai-ecoverse/slicc-agent` | Cones and scoops, the active agent, conversations, `send` (with attachments, as a send, a steer or a queued follow-up) and `stop`, the queue, suggestions, answers to questions, permission decisions, compaction, clearing (`clear(agentId)` starts a new conversation: on a cone, a port with `freeze` first puts the old one in the Freezer as its own row, which thaws into a cone of its own; a scoop's is just reset), the model per agent and new scoops. The optional `rewind(agentId, messageId)` drops the user turn before that message and everything after it, and resolves with the turn's prompt (text and attachments) for the composer, or `null`. The optional `drop(agentId)` removes a scoop: it stops and its working folder is deleted, while its conversation and reports are kept, and `createCone(name)` resolves with a new cone; the dummy rejects a cone name that is taken. `freeze(agentId)` is optional too: without it, **Delete cone** and `/freeze` don't show. `frozen()` lists the Freezer's rows; a `FrozenCone` may carry `kind` (`cone`, `scoop` or `agent`), `live` for a conversation that's still running, and `thawedAs`, the id of the cone a scoop or agent run was thawed into. `thaw(id)` brings a cone back, or forks a scoop or agent run into a new cone, and `discard(id)` removes a row. Names don't have to be unique, and an optional `Agent.title` names a cone's conversation; without it, the first user message does. Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`. |
|
|
202
|
-
| `files` | slicc-kernel's OPFS | `list`, `read`, `write`, `remove`, and pending changes with their before and after text (`accept`, `revert`). The optional `mountFolder()` asks for a local folder and resolves with its mount point (for example `/mnt/photos`), or `null` when the user cancels; the panel calls it synchronously in the click, so the port can call `showDirectoryPicker()`. The optional `mounts()` lists the mount points, and `eject(path)` unmounts one (the folder stays on disk). `mountFolder(path)` inserts the picked folder at a mount point that waits for one; the optional `needsFolder()` lists those mount points. The port emits `mounts` and then `files` after every mount and eject, including those made from the shell, and `mounts` whenever `needsFolder()` changes. |
|
|
202
|
+
| `files` | slicc-kernel's OPFS | `list`, `read`, `write`, `remove`, and pending changes with their before and after text (`accept`, `revert`). The optional `mountFolder()` asks for a local folder and resolves with its mount point (for example `/mnt/photos`), or `null` when the user cancels; the panel calls it synchronously in the click, so the port can call `showDirectoryPicker()`. The optional `mounts()` lists the mount points, and the optional `mountInfo()` returns `MountInfo`s (`target`, optional `source` such as a linked device's `ref-node/proj`, and `type`) for them, used instead of `mounts()` when present; `eject(path)` unmounts one (the folder stays on disk). `mountFolder(path)` inserts the picked folder at a mount point that waits for one; the optional `needsFolder()` lists those mount points. The port emits `mounts` and then `files` after every mount and eject, including those made from the shell, and `mounts` whenever `needsFolder()` changes. |
|
|
203
203
|
| `terminals` | slicc-kernel's terminal API | Terminal sessions; `backend(id)` returns a `TerminalBackend` for `<slicc-terminal>`. |
|
|
204
204
|
| `browser` | `@ai-ecoverse/slicc-cdp` | Tabs, the active tab, `open`, `navigate`, `close`, `screenshot`. A `BrowserTab` may say it's `controlled` (an agent, or the user's terminal when `agentId` is `null`, is driving it now; `agentId` is the agent of its last action); `favicon` is ignored. The optional `actions(tabId?)` returns recent `BrowserAction`s (`{ id, tabId, agentId, kind, target?, value?, length?, status: running \| done \| failed, error?, at }`, `kind` one of `open`, `goto`, `back`, `reload`, `close`, `select`, `snapshot`, `screenshot`, `eval`, `click`, `fill`, `type`, `press`, `scroll`, `request`; a `request` has `value` `'GET https://host/path'` and may have no tab), with an `action` event per new or settled action. The optional `watch(tabId, size?)` streams `frame` events (`{ tabId, src, at }`) for a controlled tab and returns a stop function; the panel watches only the focused tab while it's controlled and visible, with the frame's CSS size. **Stop** calls `agent.stop(agentId)`. |
|
|
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). 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)
|
|
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), 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. 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}." |
|
|
216
216
|
| `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`. |
|
|
217
217
|
|
|
218
|
-
The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`. Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot. Dummy Update now and Retry simulate download and link progress; Restart agent and Reload only change fixture state, never restart a real process or reload the browser. `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'off' })` (or `?packages=` on `/ui/`, off by default) adds invented optional packages in every state: Install and Retry finish after a short wait and install missing requirements too, `fail` makes them fail, `user` adds a pinned agent-kit and two additions (one with a warning) that Unpin and Remove take out of the list, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`). `updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`. `createDummyModel({ mounts: 'mount' | 'cancel' | 'fail' | 'needs' })` (or `?mounts=` on `/ui/`) turns on the optional mount members, which are off by default: `mountFolder` mounts invented files at `/mnt/photos`, resolves with `null`, or rejects, as `model.files.mount.scenario` says; `needs` also lists `/mnt/photos` in `needsFolder()` until a folder is inserted there. `createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc-node (with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port. `model.network.setScenario()` switches it and Check again re-emits after a short wait. `createDummyModel({ tailnet: 'off' | 'needs-login' | 'running' | 'failed' })` (or `?tailnet=` on `/ui/`, off by default) adds an invented tailnet: an auth key starting with `tskey-` signs in, `mast-pi` and `keel-router` are online exit nodes and `buoy-laptop` is offline, and Retry on `failed` connects. While the dummy tailnet runs with an exit node, the route is `tailnet` and the health `ok`. `createDummyModel({ links: 'none' | 'preparing' | 'linked' | 'reconnecting' | 'exit' | 'prompt' | 'denied' | 'remote' | 'followers' })` (or `?links=` on `/ui/`, off by default) adds invented linked devices and the exit members: `linked` has quay-studio (on this computer) and pier-nuc, which can be exits and have a Chrome each, and dock-builder, which can't; with no other browser automation, the dummy reports `via: 'link'`; `exit` sends traffic through quay-studio; `reconnecting` holds traffic for pier-nuc (health `failing`, and `ok` while the exit device is connected, both without a `detail`, as bios reports them); `remote` has pier-nuc allowed and prompting, quay-studio watching, and an offline reef-laptop that only ever connected for control; `prompt` and `denied` wait for Local Network Access, and Try again links quay-studio. New join URL changes the URL, and `model.network.setLinks()` switches scenarios. `networkFixtures`, `tailnetFixtures`, `linksFixtures`, `joinUrl` and `DummyNetwork` are exported from `/dummy`. `createDummyModel({ changes: 'files' | 'git' | 'nogit' })` (or `?changes=` on `/ui/`, default `files`) adds a git `changes` port: `git` lists the pending changes under `/workspace/harbor` and `/workspace/skills` with their `repo` and no author, and `nogit` reports git as unavailable. `DummyChanges` is exported from `/dummy`. `createDummyModel({ notices: 'storage' | 'mixed' | 'fail' | 'off' })` (or `?notices=` on `/ui/`, off by default) adds an invented storage warning with Try again, which clears it after a short wait (`mixed` adds an informative notice without actions, and `fail` makes Try again fail). `noticeFixtures` and `DummyNotices` are exported from `/dummy`. `createDummyModel({ browser: 'idle' | 'driving' | 'off' })` (or `?browser=` on `/ui/`, default `idle`) sets the dummy browser: `idle` has a history of invented actions and no driven tab, `driving` has harbor and quiet-otter driving two tabs, with harbor's last click still running and frames every 500 ms while watched, and `off` has no tabs. Stopping an agent releases its tabs. `DummyBrowser`, `browserHistory` and `page` are exported from `/dummy`. `createDummyModel({ sprinkles: 'builtin' | 'cones' })` (or `?sprinkles=` on `/ui/`, default `builtin`) adds, with `cones`, invented sprinkles of other cones: a Board in harbor and one in tide-tables, and a Changelog in release-notes. `createDummyModel({ tabs: 'owner' | 'follower' | 'connecting' | 'stalled' | 'newer' | 'older' | 'alone' | 'off' })` (or `?tabs=` on `/ui/`, off by default) adds a tabs port in that state; `model.tabs.setScenario()` switches it, and Reload connects again and becomes a follower after a short wait. `tabsFixtures` and `DummyTabs` are exported from `/dummy`.
|
|
218
|
+
The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`. Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot. Dummy Update now and Retry simulate download and link progress; Restart agent and Reload only change fixture state, never restart a real process or reload the browser. `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'off' })` (or `?packages=` on `/ui/`, off by default) adds invented optional packages in every state: Install and Retry finish after a short wait and install missing requirements too, `fail` makes them fail, `user` adds a pinned agent-kit and two additions (one with a warning) that Unpin and Remove take out of the list, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`). `updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`. `createDummyModel({ mounts: 'mount' | 'cancel' | 'fail' | 'needs' })` (or `?mounts=` on `/ui/`) turns on the optional mount members, which are off by default: `mountFolder` mounts invented files at `/mnt/photos`, resolves with `null`, or rejects, as `model.files.mount.scenario` says; `needs` also lists `/mnt/photos` in `needsFolder()` until a folder is inserted there. `createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc-node (with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port. `model.network.setScenario()` switches it and Check again re-emits after a short wait. `createDummyModel({ tailnet: 'off' | 'needs-login' | 'running' | 'failed' })` (or `?tailnet=` on `/ui/`, off by default) adds an invented tailnet: an auth key starting with `tskey-` signs in, `mast-pi` and `keel-router` are online exit nodes and `buoy-laptop` is offline, and Retry on `failed` connects. While the dummy tailnet runs with an exit node, the route is `tailnet` and the health `ok`. `createDummyModel({ links: 'none' | 'preparing' | 'linked' | 'reconnecting' | 'exit' | 'prompt' | 'denied' | 'remote' | 'followers' })` (or `?links=` on `/ui/`, off by default) adds invented linked devices and the exit members: `linked` has quay-studio (on this computer) and pier-nuc, which can be exits and have a Chrome each, and dock-builder, which can't; quay-studio shares `sketches` and pier-nuc `proj` and a read-only `archive`, which **Mount** attaches to the dummy files at `/mnt/<source>` (with `mountInfo` naming the source) and **Eject** or the Files strip detaches; with no other browser automation, the dummy reports `via: 'link'`; `exit` sends traffic through quay-studio; `reconnecting` holds traffic for pier-nuc (health `failing`, and `ok` while the exit device is connected, both without a `detail`, as bios reports them); `remote` has pier-nuc allowed and prompting, quay-studio watching, and an offline reef-laptop that only ever connected for control; `prompt` and `denied` wait for Local Network Access, and Try again links quay-studio. New join URL changes the URL, and `model.network.setLinks()` switches scenarios. `networkFixtures`, `tailnetFixtures`, `linksFixtures`, `joinUrl` and `DummyNetwork` are exported from `/dummy`. `createDummyModel({ changes: 'files' | 'git' | 'nogit' })` (or `?changes=` on `/ui/`, default `files`) adds a git `changes` port: `git` lists the pending changes under `/workspace/harbor` and `/workspace/skills` with their `repo` and no author, and `nogit` reports git as unavailable. `DummyChanges` is exported from `/dummy`. `createDummyModel({ notices: 'storage' | 'mixed' | 'fail' | 'off' })` (or `?notices=` on `/ui/`, off by default) adds an invented storage warning with Try again, which clears it after a short wait (`mixed` adds an informative notice without actions, and `fail` makes Try again fail). `noticeFixtures` and `DummyNotices` are exported from `/dummy`. `createDummyModel({ browser: 'idle' | 'driving' | 'off' })` (or `?browser=` on `/ui/`, default `idle`) sets the dummy browser: `idle` has a history of invented actions and no driven tab, `driving` has harbor and quiet-otter driving two tabs, with harbor's last click still running and frames every 500 ms while watched, and `off` has no tabs. Stopping an agent releases its tabs. `DummyBrowser`, `browserHistory` and `page` are exported from `/dummy`. `createDummyModel({ sprinkles: 'builtin' | 'cones' })` (or `?sprinkles=` on `/ui/`, default `builtin`) adds, with `cones`, invented sprinkles of other cones: a Board in harbor and one in tide-tables, and a Changelog in release-notes. `createDummyModel({ tabs: 'owner' | 'follower' | 'connecting' | 'stalled' | 'newer' | 'older' | 'alone' | 'off' })` (or `?tabs=` on `/ui/`, off by default) adds a tabs port in that state; `model.tabs.setScenario()` switches it, and Reload connects again and becomes a follower after a short wait. `tabsFixtures` and `DummyTabs` are exported from `/dummy`.
|
|
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.
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -2306,9 +2306,11 @@ var DummyFiles = class extends Emitter {
|
|
|
2306
2306
|
mount;
|
|
2307
2307
|
mountFolder;
|
|
2308
2308
|
mounts;
|
|
2309
|
+
mountInfo;
|
|
2309
2310
|
eject;
|
|
2310
2311
|
needsFolder;
|
|
2311
2312
|
#mounted = /* @__PURE__ */ new Set();
|
|
2313
|
+
#sources = /* @__PURE__ */ new Map();
|
|
2312
2314
|
#needs = /* @__PURE__ */ new Set();
|
|
2313
2315
|
#contents = /* @__PURE__ */ new Map();
|
|
2314
2316
|
#directories = /* @__PURE__ */ new Set();
|
|
@@ -2321,8 +2323,7 @@ var DummyFiles = class extends Emitter {
|
|
|
2321
2323
|
this.mount = mount;
|
|
2322
2324
|
if (mount) {
|
|
2323
2325
|
this.mountFolder = (path) => this.#mountFolder(mount, path);
|
|
2324
|
-
this
|
|
2325
|
-
this.eject = (path) => this.#eject(path);
|
|
2326
|
+
this.#ejectable();
|
|
2326
2327
|
this.needsFolder = () => [...this.#needs];
|
|
2327
2328
|
if (mount.scenario === "needs") this.#needs.add(mount.point);
|
|
2328
2329
|
}
|
|
@@ -2410,6 +2411,27 @@ var DummyFiles = class extends Emitter {
|
|
|
2410
2411
|
else if (change.status === "added") this.#contents.delete(path);
|
|
2411
2412
|
this.#announce(path);
|
|
2412
2413
|
}
|
|
2414
|
+
#ejectable() {
|
|
2415
|
+
this.mounts = () => [...this.#mounted];
|
|
2416
|
+
this.mountInfo = () => [...this.#mounted].map((target) => {
|
|
2417
|
+
const source = this.#sources.get(target);
|
|
2418
|
+
return source ? { target, source, type: "hostfs" } : { target, type: "local" };
|
|
2419
|
+
});
|
|
2420
|
+
this.eject = (path) => this.#eject(path);
|
|
2421
|
+
}
|
|
2422
|
+
async attach(target, source, contents) {
|
|
2423
|
+
await this.#clock.sleep();
|
|
2424
|
+
if (!this.eject) this.#ejectable();
|
|
2425
|
+
const files3 = Object.entries(contents).map(([path, text]) => {
|
|
2426
|
+
const file = `${target}/${path}`;
|
|
2427
|
+
this.#put(file, text, Date.now());
|
|
2428
|
+
return file;
|
|
2429
|
+
});
|
|
2430
|
+
this.#mounted.add(target);
|
|
2431
|
+
this.#sources.set(target, source);
|
|
2432
|
+
this.#remount(files3);
|
|
2433
|
+
return target;
|
|
2434
|
+
}
|
|
2413
2435
|
async #mountFolder(mount, point) {
|
|
2414
2436
|
await this.#clock.sleep();
|
|
2415
2437
|
if (mount.scenario === "cancel") return null;
|
|
@@ -2431,6 +2453,7 @@ var DummyFiles = class extends Emitter {
|
|
|
2431
2453
|
async #eject(path) {
|
|
2432
2454
|
await this.#clock.sleep();
|
|
2433
2455
|
if (!this.#mounted.delete(path)) throw new Error(`Nothing is mounted at ${path}.`);
|
|
2456
|
+
this.#sources.delete(path);
|
|
2434
2457
|
const inside = (candidate) => candidate === path || candidate.startsWith(`${path}/`);
|
|
2435
2458
|
const files3 = [...this.#contents.keys()].filter(inside);
|
|
2436
2459
|
for (const file of files3) {
|
|
@@ -3528,7 +3551,7 @@ var agents = [
|
|
|
3528
3551
|
parentId: null,
|
|
3529
3552
|
status: "waiting",
|
|
3530
3553
|
model: "claude-sonnet-5-5",
|
|
3531
|
-
contextFill: 0.
|
|
3554
|
+
contextFill: 0.84,
|
|
3532
3555
|
unread: 2
|
|
3533
3556
|
},
|
|
3534
3557
|
{
|
|
@@ -3935,12 +3958,100 @@ function wave(seed, length = 24) {
|
|
|
3935
3958
|
(_, i) => Math.round(40 + 25 * Math.sin((i + seed) / 3) + 10 * Math.cos(i * seed / 5))
|
|
3936
3959
|
);
|
|
3937
3960
|
}
|
|
3961
|
+
var agentSections = [
|
|
3962
|
+
{
|
|
3963
|
+
id: "tasks",
|
|
3964
|
+
label: "Tasks",
|
|
3965
|
+
rows: [
|
|
3966
|
+
{ id: "t1", name: "generating", meta: "sliccy \xB7 running \xB7 12s", status: "active" },
|
|
3967
|
+
{
|
|
3968
|
+
id: "t2",
|
|
3969
|
+
name: "tool bash",
|
|
3970
|
+
meta: "sliccy \xB7 running \xB7 8s",
|
|
3971
|
+
status: "active",
|
|
3972
|
+
depth: 1,
|
|
3973
|
+
badges: ["bash"]
|
|
3974
|
+
},
|
|
3975
|
+
{
|
|
3976
|
+
id: "t3",
|
|
3977
|
+
name: "scoop quiet-otter",
|
|
3978
|
+
meta: "quiet-otter \xB7 waiting \xB7 2m",
|
|
3979
|
+
status: "idle",
|
|
3980
|
+
depth: 1,
|
|
3981
|
+
badges: ["waits on 2"]
|
|
3982
|
+
},
|
|
3983
|
+
{
|
|
3984
|
+
id: "t4",
|
|
3985
|
+
name: "tool read_file",
|
|
3986
|
+
meta: "quiet-otter \xB7 completing \xB7 1s",
|
|
3987
|
+
status: "active",
|
|
3988
|
+
depth: 2
|
|
3989
|
+
},
|
|
3990
|
+
{
|
|
3991
|
+
id: "t5",
|
|
3992
|
+
name: "report for quiet-otter",
|
|
3993
|
+
meta: "quiet-otter \xB7 pending \xB7 2m",
|
|
3994
|
+
status: "idle",
|
|
3995
|
+
depth: 2,
|
|
3996
|
+
badges: ["background"]
|
|
3997
|
+
},
|
|
3998
|
+
{
|
|
3999
|
+
id: "t6",
|
|
4000
|
+
name: "cron tide-check",
|
|
4001
|
+
meta: "session \xB7 aborting \xB7 14s",
|
|
4002
|
+
status: "warn",
|
|
4003
|
+
badges: ["aborting"]
|
|
4004
|
+
}
|
|
4005
|
+
]
|
|
4006
|
+
},
|
|
4007
|
+
{
|
|
4008
|
+
id: "processes",
|
|
4009
|
+
label: "Processes",
|
|
4010
|
+
rows: [
|
|
4011
|
+
{
|
|
4012
|
+
id: "p412",
|
|
4013
|
+
name: "bash",
|
|
4014
|
+
meta: "pid 412 \xB7 slicc \xB7 3m",
|
|
4015
|
+
status: "idle",
|
|
4016
|
+
badges: ["terminal"]
|
|
4017
|
+
},
|
|
4018
|
+
{
|
|
4019
|
+
id: "p418",
|
|
4020
|
+
name: "node build.mjs --watch",
|
|
4021
|
+
meta: "pid 418 \xB7 slicc \xB7 1m",
|
|
4022
|
+
status: "active",
|
|
4023
|
+
badges: ["sliccy"]
|
|
4024
|
+
},
|
|
4025
|
+
{
|
|
4026
|
+
id: "p433",
|
|
4027
|
+
name: "grep -r harbor .",
|
|
4028
|
+
meta: "pid 433 \xB7 slicc \xB7 40s",
|
|
4029
|
+
status: "warn",
|
|
4030
|
+
badges: ["quiet-otter"]
|
|
4031
|
+
}
|
|
4032
|
+
]
|
|
4033
|
+
},
|
|
4034
|
+
{
|
|
4035
|
+
id: "problems",
|
|
4036
|
+
label: "Recent problems",
|
|
4037
|
+
rows: [
|
|
4038
|
+
{
|
|
4039
|
+
id: "e1",
|
|
4040
|
+
name: "quiet-otter \xB7 turn",
|
|
4041
|
+
meta: "The model returned an error: overloaded. Retrying in 8 s.",
|
|
4042
|
+
status: "error"
|
|
4043
|
+
}
|
|
4044
|
+
]
|
|
4045
|
+
}
|
|
4046
|
+
];
|
|
3938
4047
|
var DummyMonitor = class extends Emitter {
|
|
3939
4048
|
#model;
|
|
3940
4049
|
#tick = 0;
|
|
3941
|
-
|
|
4050
|
+
#extra;
|
|
4051
|
+
constructor(model, extra = []) {
|
|
3942
4052
|
super();
|
|
3943
4053
|
this.#model = model;
|
|
4054
|
+
this.#extra = extra;
|
|
3944
4055
|
const update = () => this.resync();
|
|
3945
4056
|
model.agent.on("agents", update);
|
|
3946
4057
|
model.terminals.on("terminals", update);
|
|
@@ -3959,7 +4070,9 @@ var DummyMonitor = class extends Emitter {
|
|
|
3959
4070
|
const fill = Math.max(...agents2.map((candidate) => candidate.contextFill), 0);
|
|
3960
4071
|
const agentRows = agents2.map(
|
|
3961
4072
|
(candidate) => ({
|
|
3962
|
-
|
|
4073
|
+
id: candidate.id,
|
|
4074
|
+
name: candidate.name,
|
|
4075
|
+
depth: candidate.kind === "scoop" ? 1 : 0,
|
|
3963
4076
|
meta: `${candidate.status} \xB7 ${Math.round(candidate.contextFill * 100)}% context`,
|
|
3964
4077
|
status: statuses[candidate.status],
|
|
3965
4078
|
badges: candidate.unread ? [`${candidate.unread} unread`] : []
|
|
@@ -3978,17 +4091,19 @@ var DummyMonitor = class extends Emitter {
|
|
|
3978
4091
|
{
|
|
3979
4092
|
id: "spend",
|
|
3980
4093
|
label: "Spend",
|
|
3981
|
-
value: `$${status.spent.toFixed(2)}`,
|
|
3982
|
-
delta: `$${status.rate.toFixed(2)}/h`,
|
|
4094
|
+
value: `$${(status.spent ?? 0).toFixed(2)}`,
|
|
4095
|
+
delta: `$${(status.rate ?? 0).toFixed(2)}/h`,
|
|
3983
4096
|
series: wave(this.#tick + 7)
|
|
3984
4097
|
},
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
4098
|
+
...status.budget ? [
|
|
4099
|
+
{
|
|
4100
|
+
id: "budget",
|
|
4101
|
+
label: `Budget (${status.budget.window})`,
|
|
4102
|
+
value: `${status.budget.percent}%`,
|
|
4103
|
+
unit: status.budget.resets,
|
|
4104
|
+
ratio: status.budget.percent / 100
|
|
4105
|
+
}
|
|
4106
|
+
] : [],
|
|
3992
4107
|
{
|
|
3993
4108
|
id: "context",
|
|
3994
4109
|
label: "Fullest context",
|
|
@@ -3997,7 +4112,7 @@ var DummyMonitor = class extends Emitter {
|
|
|
3997
4112
|
}
|
|
3998
4113
|
],
|
|
3999
4114
|
alerts: [
|
|
4000
|
-
...agents2.filter((candidate) => candidate.contextFill >= 0.
|
|
4115
|
+
...agents2.filter((candidate) => candidate.contextFill >= 0.8).map((candidate) => ({
|
|
4001
4116
|
id: `fill-${candidate.id}`,
|
|
4002
4117
|
title: `${candidate.name} is ${Math.round(candidate.contextFill * 100)}% full`,
|
|
4003
4118
|
detail: "Compact it soon to keep replies fast.",
|
|
@@ -4014,6 +4129,7 @@ var DummyMonitor = class extends Emitter {
|
|
|
4014
4129
|
],
|
|
4015
4130
|
sections: [
|
|
4016
4131
|
{ id: "agents", label: "Cones and scoops", rows: agentRows },
|
|
4132
|
+
...this.#extra,
|
|
4017
4133
|
{
|
|
4018
4134
|
id: "terminals",
|
|
4019
4135
|
label: "Terminals",
|
|
@@ -4169,7 +4285,8 @@ var linkedDevices = [
|
|
|
4169
4285
|
exit: true,
|
|
4170
4286
|
state: "connected",
|
|
4171
4287
|
browsers: [{ runtime: "quay-studio/chrome", name: "Chrome" }],
|
|
4172
|
-
control: { allowed: true, fixed: true }
|
|
4288
|
+
control: { allowed: true, fixed: true },
|
|
4289
|
+
folders: [{ name: "sketches", source: "quay-studio/sketches", readonly: false }]
|
|
4173
4290
|
},
|
|
4174
4291
|
{
|
|
4175
4292
|
id: "link-pier",
|
|
@@ -4182,7 +4299,11 @@ var linkedDevices = [
|
|
|
4182
4299
|
exit: true,
|
|
4183
4300
|
state: "connected",
|
|
4184
4301
|
browsers: [{ runtime: "pier-nuc/chrome", name: "Chrome" }],
|
|
4185
|
-
control: { allowed: false }
|
|
4302
|
+
control: { allowed: false },
|
|
4303
|
+
folders: [
|
|
4304
|
+
{ name: "proj", source: "pier-nuc/proj", readonly: false },
|
|
4305
|
+
{ name: "archive", source: "pier-nuc/archive", readonly: true }
|
|
4306
|
+
]
|
|
4186
4307
|
},
|
|
4187
4308
|
{
|
|
4188
4309
|
id: "link-dock",
|
|
@@ -4197,6 +4318,15 @@ var linkedDevices = [
|
|
|
4197
4318
|
control: { allowed: false }
|
|
4198
4319
|
}
|
|
4199
4320
|
];
|
|
4321
|
+
function folderFiles(folder) {
|
|
4322
|
+
return {
|
|
4323
|
+
"README.md": `# ${folder.name}
|
|
4324
|
+
|
|
4325
|
+
Shared from ${folder.source}.
|
|
4326
|
+
`,
|
|
4327
|
+
"notes.txt": "Tide tables, rope lengths, and a list of knots to learn.\n"
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4200
4330
|
function joinUrl(rotation = 0) {
|
|
4201
4331
|
return `https://join.slicc.example/l/${rotation ? `r${rotation}-` : ""}7Hq2kd9FXw3m`;
|
|
4202
4332
|
}
|
|
@@ -4283,7 +4413,7 @@ function linksFixtures(scenario, rotation = 0) {
|
|
|
4283
4413
|
const url = joinUrl(rotation);
|
|
4284
4414
|
const base = {
|
|
4285
4415
|
joinUrl: url,
|
|
4286
|
-
joinCommand: `npx sliccy ${url} follow`,
|
|
4416
|
+
joinCommand: `npx sliccy@next ${url} follow`,
|
|
4287
4417
|
followUrl: followUrl(rotation),
|
|
4288
4418
|
permission: null
|
|
4289
4419
|
};
|
|
@@ -4347,7 +4477,10 @@ var DummyNetwork = class extends Emitter {
|
|
|
4347
4477
|
setAccess;
|
|
4348
4478
|
rotateJoinUrl;
|
|
4349
4479
|
retryLinks;
|
|
4350
|
-
|
|
4480
|
+
mountFolder;
|
|
4481
|
+
ejectFolder;
|
|
4482
|
+
#files;
|
|
4483
|
+
constructor(scenario, clock2, tailnet, links, files3) {
|
|
4351
4484
|
super();
|
|
4352
4485
|
this.scenario = scenario;
|
|
4353
4486
|
this.#clock = clock2;
|
|
@@ -4360,6 +4493,10 @@ var DummyNetwork = class extends Emitter {
|
|
|
4360
4493
|
this.setAccess = (id, key, allowed) => this.#access(id, key, allowed);
|
|
4361
4494
|
this.rotateJoinUrl = () => this.#rotate();
|
|
4362
4495
|
this.retryLinks = () => this.#retry();
|
|
4496
|
+
this.mountFolder = (source) => this.#mountFolder(source);
|
|
4497
|
+
this.ejectFolder = (source) => this.#ejectFolder(source);
|
|
4498
|
+
this.#files = files3;
|
|
4499
|
+
files3?.on("mounts", (targets) => this.#mounted(targets));
|
|
4363
4500
|
}
|
|
4364
4501
|
this.#base = networkFixtures(scenario);
|
|
4365
4502
|
this.#status = this.#with(this.#base);
|
|
@@ -4445,6 +4582,45 @@ var DummyNetwork = class extends Emitter {
|
|
|
4445
4582
|
if (this.#exit?.kind === "link" && this.#exit.id === id) this.#exit = null;
|
|
4446
4583
|
this.#emit();
|
|
4447
4584
|
}
|
|
4585
|
+
#folder(source) {
|
|
4586
|
+
const folder = this.#links?.devices.flatMap((device) => device.folders ?? []).find((item) => item.source === source);
|
|
4587
|
+
if (!folder) throw new Error(`No linked device shares ${source}.`);
|
|
4588
|
+
return folder;
|
|
4589
|
+
}
|
|
4590
|
+
#mark(mounted2) {
|
|
4591
|
+
const links = this.#links;
|
|
4592
|
+
const devices = links.devices.map(
|
|
4593
|
+
(device) => device.folders ? {
|
|
4594
|
+
...device,
|
|
4595
|
+
folders: device.folders.map((folder) => {
|
|
4596
|
+
const { mounted: _, ...rest } = folder;
|
|
4597
|
+
const at3 = mounted2(folder);
|
|
4598
|
+
return at3 ? { ...rest, mounted: at3 } : rest;
|
|
4599
|
+
})
|
|
4600
|
+
} : device
|
|
4601
|
+
);
|
|
4602
|
+
this.#links = { ...links, devices };
|
|
4603
|
+
this.#emit();
|
|
4604
|
+
}
|
|
4605
|
+
#mounted(targets) {
|
|
4606
|
+
this.#mark((folder) => targets.find((target) => target === `/mnt/${folder.source}`));
|
|
4607
|
+
}
|
|
4608
|
+
async #mountFolder(source) {
|
|
4609
|
+
const folder = this.#folder(source);
|
|
4610
|
+
if (folder.mounted) return folder.mounted;
|
|
4611
|
+
const target = `/mnt/${source}`;
|
|
4612
|
+
if (this.#files) await this.#files.attach(target, source, folderFiles(folder));
|
|
4613
|
+
else await this.#clock.sleep();
|
|
4614
|
+
this.#mark((item) => item.source === source ? target : item.mounted);
|
|
4615
|
+
return target;
|
|
4616
|
+
}
|
|
4617
|
+
async #ejectFolder(source) {
|
|
4618
|
+
const folder = this.#folder(source);
|
|
4619
|
+
if (!folder.mounted) throw new Error(`${source} isn\u2019t mounted.`);
|
|
4620
|
+
if (this.#files?.eject) await this.#files.eject(folder.mounted);
|
|
4621
|
+
else await this.#clock.sleep();
|
|
4622
|
+
this.#mark((item) => item.source === source ? void 0 : item.mounted);
|
|
4623
|
+
}
|
|
4448
4624
|
async #rotate() {
|
|
4449
4625
|
await this.#clock.sleep(4);
|
|
4450
4626
|
this.#rotation += 1;
|
|
@@ -4452,7 +4628,7 @@ var DummyNetwork = class extends Emitter {
|
|
|
4452
4628
|
this.#links = {
|
|
4453
4629
|
...this.#links,
|
|
4454
4630
|
joinUrl: url,
|
|
4455
|
-
joinCommand: `npx sliccy ${url} follow`,
|
|
4631
|
+
joinCommand: `npx sliccy@next ${url} follow`,
|
|
4456
4632
|
followUrl: followUrl(this.#rotation)
|
|
4457
4633
|
};
|
|
4458
4634
|
this.#emit();
|
|
@@ -5282,7 +5458,8 @@ function createDummyModel({
|
|
|
5282
5458
|
approvals = "off",
|
|
5283
5459
|
follow = "off",
|
|
5284
5460
|
browser: scenario = "idle",
|
|
5285
|
-
sprinkles: sprinkles2 = "builtin"
|
|
5461
|
+
sprinkles: sprinkles2 = "builtin",
|
|
5462
|
+
monitor = "dummy"
|
|
5286
5463
|
} = {}) {
|
|
5287
5464
|
const time = clock(delay);
|
|
5288
5465
|
const mount = mounts === "off" ? null : { scenario: mounts, point: mountPoint, files: mounted };
|
|
@@ -5333,14 +5510,17 @@ function createDummyModel({
|
|
|
5333
5510
|
...packages === "off" ? {} : { packages },
|
|
5334
5511
|
confirm
|
|
5335
5512
|
}),
|
|
5336
|
-
...network === "off" ? {} : { network: new DummyNetwork(network, time, tailnet, links) },
|
|
5513
|
+
...network === "off" ? {} : { network: new DummyNetwork(network, time, tailnet, links, files3) },
|
|
5337
5514
|
...changes === "files" ? {} : { changes: new DummyChanges(files3, changes === "git") },
|
|
5338
5515
|
...notices === "off" ? {} : { notices: new DummyNotices(notices, time) },
|
|
5339
5516
|
...tabs2 === "off" ? {} : { tabs: new DummyTabs(tabs2, time) },
|
|
5340
5517
|
...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time) },
|
|
5341
5518
|
...follow === "off" ? {} : { follow: new DummyFollow(follow, time) }
|
|
5342
5519
|
};
|
|
5343
|
-
return {
|
|
5520
|
+
return {
|
|
5521
|
+
...base,
|
|
5522
|
+
monitor: new DummyMonitor(base, monitor === "agent" ? agentSections : void 0)
|
|
5523
|
+
};
|
|
5344
5524
|
}
|
|
5345
5525
|
export {
|
|
5346
5526
|
DummyApprovals,
|