@ai-ecoverse/slicc-spectrum 1.101.3 → 1.103.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
@@ -272,24 +272,25 @@ When cones share a name, their accessible names and the Delete cone, Freezer and
272
272
  One thread's conversation (its `agent`, or the active agent if unset), by day.
273
273
  Files dragged anywhere over the pane (not text, links or the dock's own drags) show Spectrum's drop zone over the whole pane (`swc-dropzone[data-surface=drop]`, in its dragged state: an accent border on an opaque accent-subtle fill, so the transcript doesn't show through), with an illustrated message: an attach icon, "Drop files to attach" and what it takes ("Images, text and other files up to 25 MB each join your next message. Folders can’t be attached."); a polite status (`[data-announce=drop]`) says "Drop to attach files to your message". The zone stays while the drag crosses into the composer or another child, and goes 100 ms after the drag leaves the pane (`SliccChat.leaveAfter`), on drop and on <kbd>Esc</kbd>. A drop anywhere on the pane attaches to the composer; while there is no composer (watch-only, frozen), on a guest seat, or while the composer waits to reconnect, the pane shows no zone and still consumes the file drag (`dropEffect` `none`), so a drop never opens the file in the tab.
274
274
  A message's time shows only when it is at least two minutes newer than the last time shown, and the first one after each day separator always shows; a hidden time stays for screen readers (`time.sr` with `datetime`, or `.sr` on licks and cards), and a meta row left empty isn't rendered.
275
- User messages (with attachments and a quiet timestamp, `time.when`, by that rule; the reader's own carry no name or delivery tag, while `data-mode` and `data-delivered` still say how they went; a message from another cone, a follower or a guest says who sent it, and an agent's request carries its name and an Agent badge; a message from another agent, an agent's request or a message `from` a cone (`origin` `agent`, or `from` set without `origin` `follower` or `guest`), is `[data-remote]`: a subtle indigo bubble and a 🤖 pill on its bottom edge, `aria-hidden` since the byline names the sender; people, the reader included, never get it), assistant messages (with the time by that rule, and the model only on the first reply and whenever it changes from the previous one, `.meta .model` with the usage as its tooltip; without a model the usage is the time's tooltip, and with neither it is the reply's own (`.reply[title]`); the turn always carries the usage as its `aria-description`; the cone's name isn't repeated; Markdown with tables, task lists, quotes, code and images; thinking; tool runs, one icon per run (`<slicc-tool-runs>`): three or more runs in a row form a cluster, a strip of icon buttons (`data-action="open-run"`, `data-target` the call's id, named by its `title` and status, `aria-expanded`) that each open one run's card below it, and on the right the model's label for the run, a button (`data-action="open-cluster"`, `aria-expanded`, named "Show all runs" while it has no text) that opens every card; the strip's tooltip and the group's accessible name sum it up, such as "3 commands, 1 read.
276
- Done"; prose breaks a run, hidden thinking and source links don't, and a standalone tool message or a done reply with only tool calls continues the run before it on the same day, its `data-id` moving onto its first card (`.tool[data-id]`); a single run is a strip of one, its icon and its `title` as the label, which opens with <kbd>Enter</kbd> or <kbd>Space</kbd> like the strip's buttons.
277
- A strip and its cards share one width, the column less a wide margin on each side, so they read as centred, and their edges line up.
278
- Icons fill from the start of a space kept for five, so a run that grows doesn't move its strip or its label; past five, the strip shows the newest five and scrolls back to the older ones, with <kbd>Tab</kbd> or a swipe, and returns to the newest when a run is added or the strip comes back into view.
279
- The label sits at the end and is cut with an ellipsis before it reaches that space.
280
- Under 480 px of column width the label takes a row of its own under the icons, which then show as many whole icons as fit.
281
- A cluster's label comes from the agent port's optional `toolLabel(agentId, callIds)`, asked once for each agent port, agent and set of runs; while a growing run waits for its new label, it keeps its last one, and without `toolLabel` the label is empty.
282
- A command's icon is the favicon of the site it names when an open browser tab already has a `data:` favicon for it (nothing is fetched; otherwise the globe), else the file browser's icon for the file it names, else the code icon; other tools show their kind's icon.
283
- A running run pulses (still, with a dashed ring, under `prefers-reduced-motion`), a done or cancelled one is grey and a failed one red, each with a corner glyph.
284
- The card (`.tool[data-tool][data-status][data-kind]`, a `details.run` under a pill and a `div[role=region]` named by its `title` and status under a strip) has no header of its own, no chevron, title, tool name or status: its input (a shell command after `$`), an edit's `diff` shown as a diff view, its output wrapped rather than cut off, with its ANSI styles shown (bold, dim, italic, underline, inverse, and the 16, 256 and truecolor foregrounds and backgrounds; the 16 foregrounds come from `--slicc-terminal-color-0` to `-15` when an embedder sets them, else the terminal panel's light tokens, which adapt in dark (so a card in dark uses Spectrum's colors, not the panel's VS Code ones); the 16 backgrounds are Spectrum's subtle tints under the default text, black a solid grey under the lightest grey, so text keeps 4.5:1; a 256 or truecolor background gets black or white text by its luminance unless the program set a 256 or truecolor foreground too; 256 and truecolor foregrounds show as sent; dim subdues default text and turns a bright color into its normal one) and every other escape stripped, a partial escape at the end of streamed output waiting for the rest (the model still sees the raw bytes, and a copy gets the text without escapes), an `image`, such as a screenshot, an optional `meta` such as a timeout, a standalone tool message's time ("Ran at 16:12", `.ran`, by the two-minute rule), and its `notes` (`{ severity, text }`, such as "Exited with code 5" or "Full output in /tmp/…") as lines under the output, `li[data-note][data-severity]`: `error` with an alert icon in the negative colour, `warn` with an alert icon and `info` with an info icon, both subdued; media galleries with images, video and audio; action cards for tools, pull requests and status; plans and checklists; inline diffs; question prompts (as decision cards, below); delegations; link previews; errors with a fix-it action, as a grey card (`.error-card[role=alert]`, `data-id` on system errors) inset by a wide margin on each side, a narrower one under 480 px: a red alert icon, the title in bold, the detail subdued and the action on the right, or under the detail below 360 px, announced as "Error: " and the title; an error with `action: 'login'` and an `account` (an `Account.id`) signs that account in: **Log in** calls `settings.connect(account)` synchronously in the click when the account is OAuth or needs no secret or fields, so a sign-in pop-up opens within the user gesture, and otherwise, or when `accounts()` doesn't have it, opens Settings at its row; the card follows the account's status from `accounts` (`data-login` is `idle`, `signing-in` or `connected`): while it is `signing-in` or `connect` is pending, **Log in** is pending and reads “Signing in…”, for an OAuth account the detail reads “Continue in the sign-in window that opened.” and, after `SliccChat.closedAfter` (20 s, since a closed window can't be detected), “Closed the sign-in window? Cancel, then Log in again.”, with **Cancel** (`data-action="cancel-login"`) when the port has `cancel`; once `connected`, the card is no longer an alert: a neutral info icon in the detail colour replaces the red one, the "Error: " prefix goes, the title reads “Signed in to {provider} again.” (the account's `provider`), the detail reads “Signed in. Retry to send it again.” (a polite status) and the button becomes **Retry**, which sends the last prompt again only when clicked; a rejected `connect` shows its message as negative help text (`[data-error=login]`), keeps **Log in** and adds a quiet **Open settings** (`data-action="settings"`) to that account; without an `account`, **Log in** opens Settings at Accounts, and watch-only and guest seats keep their gating; a content-filter stop offers **Drop the last turn**, which rewinds that turn and puts its prompt back into the composer; when the turns right before it failed too, the button reads **Drop N failed turns** and rewinds to before the first of them, putting the latest prompt back), system messages (error cards; compactions and notices, rewinds and model changes included, as full-width dividers with a line on each side, like the day: a compaction shows only its title, its `text` kept for screen readers, one that lost messages (`state: 'fallback'`, by default "Compaction failed, transcript truncated", or `discarded`, "Context discarded") an alert icon in the negative colour, and a notice its title and text on one line, `role="note"`; a divider that wraps keeps a short line on each side) and licks, SLICC's events into a cone, on every channel, on the right (`.lick-turn`): a byline with the title and the time by the two-minute rule, then a dashed box (`.lick[data-id][data-channel]`) with the text and the count (×3); emoji pills on its bottom edge (`.reactions .reaction`, `aria-hidden`, each named by its `title`) give the channel (⏰ Schedule, 🪝 Webhook, ✨ Sprinkle, …) and the outcome (👍 Allowed, 👎 Denied, also said in the box's hidden text).
275
+ User messages (with attachments and a quiet timestamp, `time.when`, by that rule; the reader's own carry no name or delivery tag, while `data-mode` and `data-delivered` still say how they went; a message from another cone, a follower or a guest says who sent it, and an agent's request carries its name and an Agent badge; a message from another agent, an agent's request or a message `from` a cone (`origin` `agent`, or `from` set without `origin` `follower` or `guest`), is `[data-remote]`: a subtle indigo bubble and a 🤖 pill on its bottom edge, `aria-hidden` since the byline names the sender; people, the reader included, never get it), assistant messages (with the time by that rule, and the model only on the first reply and whenever it changes from the previous one, `.meta .model` with the usage as its tooltip; without a model the usage is the time's tooltip, and with neither it is the reply's own (`.reply[title]`); the turn always carries the usage as its `aria-description`; the cone's name isn't repeated; Markdown with tables, task lists, quotes, code and images; thinking; tool calls, as the steps of the reply's response status (below); media galleries with images, video and audio; action cards for tools, pull requests and status; plans and checklists; inline diffs; question prompts (as decision cards, below); delegations; link previews; errors with a fix-it action, as a grey card (`.error-card[role=alert]`, `data-id` on system errors) inset by a wide margin on each side, a narrower one under 480 px: a red alert icon, the title in bold, the detail subdued and the action on the right, or under the detail below 360 px, announced as "Error: " and the title; an error with `action: 'login'` and an `account` (an `Account.id`) signs that account in: **Log in** calls `settings.connect(account)` synchronously in the click when the account is OAuth or needs no secret or fields, so a sign-in pop-up opens within the user gesture, and otherwise, or when `accounts()` doesn't have it, opens Settings at its row; the card follows the account's status from `accounts` (`data-login` is `idle`, `signing-in` or `connected`): while it is `signing-in` or `connect` is pending, **Log in** is pending and reads “Signing in…”, for an OAuth account the detail reads “Continue in the sign-in window that opened.” and, after `SliccChat.closedAfter` (20 s, since a closed window can't be detected), “Closed the sign-in window? Cancel, then Log in again.”, with **Cancel** (`data-action="cancel-login"`) when the port has `cancel`; once `connected`, the card is no longer an alert: a neutral info icon in the detail colour replaces the red one, the "Error: " prefix goes, the title reads “Signed in to {provider} again.” (the account's `provider`), the detail reads “Signed in. Retry to send it again.” (a polite status) and the button becomes **Retry**, which sends the last prompt again only when clicked; a rejected `connect` shows its message as negative help text (`[data-error=login]`), keeps **Log in** and adds a quiet **Open settings** (`data-action="settings"`) to that account; without an `account`, **Log in** opens Settings at Accounts, and watch-only and guest seats keep their gating; a content-filter stop offers **Drop the last turn**, which rewinds that turn and puts its prompt back into the composer; when the turns right before it failed too, the button reads **Drop N failed turns** and rewinds to before the first of them, putting the latest prompt back), system messages (error cards; compactions and notices, rewinds and model changes included, as full-width dividers with a line on each side, like the day: a compaction shows only its title, its `text` kept for screen readers, one that lost messages (`state: 'fallback'`, by default "Compaction failed, transcript truncated", or `discarded`, "Context discarded") an alert icon in the negative colour, and a notice its title and text on one line, `role="note"`; a divider that wraps keeps a short line on each side) and licks, SLICC's events into a cone, on every channel, on the right (`.lick-turn`): a byline with the title and the time by the two-minute rule, then a dashed box (`.lick[data-id][data-channel]`) with the text and the count (×3); emoji pills on its bottom edge (`.reactions .reaction`, `aria-hidden`, each named by its `title`) give the channel (⏰ Schedule, 🪝 Webhook, ✨ Sprinkle, …) and the outcome (👍 Allowed, 👎 Denied, also said in the box's hidden text).
285
276
  A lick with a body is a `details.lick` whose head (`summary.lick-head`, with a quiet chevron that turns when open) toggles it with a click, <kbd>Enter</kbd> or <kbd>Space</kbd>; clicking the body doesn't close it.
286
- Open licks and open tool runs hold for every view of the chat on the same model, so a layout switch keeps them.
277
+ A reply's tool calls are the steps of its response status (`swc-response-status[slot=status]`, `data-id` the reply's), at the top of the reply: one `swc-response-status-step` per call (`id` `run-` and the call's id, `data-tool`, `data-status`, `data-kind`, and `data-id` the message the call came from), labelled with the tool and its `title`, `active` while it runs, `complete` when done and `stopped` when it failed or was cancelled, which its label says in words (" · Failed", " · Cancelled").
278
+ The status keeps `data-action="open-cluster"` and each step `data-action="open-run"` with `data-target` the call's id: clicking the host from script opens it, as the old strip's buttons did, and opening a step opens its status too.
279
+ A step whose call has anything to show labels itself with a button (`.run-toggle`, `aria-expanded`, `aria-controls`) that opens the call under the step's row, at the full width of the column and indented to the step's text, in our own region (`.run-detail#detail-` and the call's id, `role="listitem"`, `hidden` while closed, `data-target` the call's id), not in Spectrum's 340 px description; the step carries `data-open` while it's open. The region is the call: its input (a shell command after `$`), an edit's `diff` shown as a diff view, its output, both kept on their lines and scrolling sideways rather than wrapping, with its ANSI styles shown (bold, dim, italic, underline, inverse, and the 16, 256 and truecolor foregrounds and backgrounds; the 16 foregrounds come from `--slicc-terminal-color-0` to `-15` when an embedder sets them, else the terminal panel's light tokens, which adapt in dark (so a card in dark uses Spectrum's colors, not the panel's VS Code ones); the 16 backgrounds are Spectrum's subtle tints under the default text, black a solid grey under the lightest grey, so text keeps 4.5:1; a 256 or truecolor background gets black or white text by its luminance unless the program set a 256 or truecolor foreground too; 256 and truecolor foregrounds show as sent; dim subdues default text and turns a bright color into its normal one) and every other escape stripped, a partial escape at the end of streamed output waiting for the rest (the model still sees the raw bytes, and a copy gets the text without escapes), an `image`, such as a screenshot, an optional `meta` such as a timeout, the time a joined call ran ("Ran at 16:12", `.ran`, by the two-minute rule), and its `notes` (`{ severity, text }`, such as "Exited with code 5" or "Full output in /tmp/…") as lines under the output, `li[data-note][data-severity]`: `error` with an alert icon in the negative colour, `warn` with an alert icon and `info` with an info icon, both subdued.
280
+ The status's header is always one line: while the reply streams or the agent works on the run, it is `active`, with the running call (or the last one) as its label and that call's kind as its loader, and each label stays for at least a second before the next one replaces it, so streamed arguments don't make it flicker; a reply without calls shows "Generating response" only until its first text or visible thinking.
281
+ Done, the status stays as `complete`, also when a call never left `running` (the step stays `active`): one call reads as its tool and `title`, more as the model's label for them or, until there is one, a count of the two most common kinds and the rest, such as "3 commands, 1 read" or "5 commands, 4 agent steps and 9 more", led by any failures ("1 failed, 1 cancelled · "), so the one-line header never cuts them off; a stopped reply says so.
282
+ Every call of a reply is in its one status, above its text; a later reply with text starts a new run, while a standalone tool message or a reply with only tool calls, streaming or done, joins the status of the run before it on the same day (hidden thinking and source links don't count as text), so a turn never collapses when its calls finish and past messages never move down.
283
+ A label never shows what may be a secret: after a sensitive key (`token`, `secret`, `password`, `api_key`, `Authorization` and the like) with `=`, `:` or a flag's space, at a word that starts with a known secret prefix (`ghp_`, `sk-`, `AKIA`, `xoxb-`, `eyJ` and others), or, while the call runs, at a last word that could still grow into one, it ends in "…"; the step's description shows the call as the agent sent it.
284
+ Spectrum notes: a step's description is at most 340 px wide (`.swc-ResponseStatusStep-detail` has no part or custom property for it), which is why a call opens in our own full-width region instead; a public hook for that width would let it use Spectrum's description. Two Spectrum behaviours are worked around: the header label is one line on a token line height (`--_swc-response-status-label-line-size` and `line-height: 0` on the status), because its cross-fade otherwise lays the label out 1.4 px off its settled place; and the label slot is never empty while active, because Spectrum then shows the running step's own label, which changes with every streamed chunk.
285
+ The label for two or more calls comes from the agent port's optional `toolLabel(agentId, callIds)`, asked once for each agent port, agent and set of calls; while a growing run waits for its new label, it keeps its last one.
286
+ Open licks, statuses and steps hold for every view of the chat on the same model, so a layout switch keeps them.
287
287
  A lick's `severity` (`warn` or `error`) adds an alert icon to its byline, a coloured edge to its box and a spoken prefix.
288
288
  A lick's optional `summary` (one plain line) replaces its `text` in the box and wraps instead of being cut, and its optional `report` (a file path) adds **Open the report** (`data-action="open-report"`, which fires `open-file` with the path) to the expanded lick, so a lick with only a `report` expands too.
289
289
  A `scoop-notify` lick never shows the agent's bracketed status line: a `text` that is `[scoop … completed]`, `failed` or `stopped` (or `[@… completed]`) reads "Scoop {name} finished", "Scoop {name} failed" or "Scoop {name} stopped", the name being its `title` without the role in brackets, a failed or stopped one takes the `error` tone and a ❌ pill, named and announced as Scoop failed or Scoop stopped, instead of ✅, and its body, without that line, renders as markdown (`.report .text`), its headings stepped down to the chat's `####` size so the card doesn't outshout the chat.
290
- Pending permission requests (`sudo-request` licks with `state: 'pending'`) and open questions are decision cards in the centre column (`.decision-turn[data-pending]`, `data-id` the lick's or the question's), inset like a tool cluster (a wide margin on each side, a narrower one under 480 px), with their buttons on the right: **Deny**, then **Allow** (`data-action` `deny` and `allow`), or the question's options.
290
+ Pending permission requests (`sudo-request` licks with `state: 'pending'`) and open questions are decision cards in the centre column (`.decision-turn[data-pending]`, `data-id` the lick's or the question's), inset by a wide margin on each side, a narrower one under 480 px, with their buttons on the right: **Deny**, then **Allow** (`data-action` `deny` and `allow`), or the question's options.
291
+ A request's command (its `body`, `pre.command`) is never cut: it wraps in full, in monospace, open or closed. When it runs past four lines (measured as rendered), the card shows the first four, a subdued "Show the full command to allow it." (`[data-allow-hint]`) and a quiet **Show the full command** with a chevron (`data-action="full-command"`, `data-shown` on it while shown, its `aria-expanded` on Spectrum's inner button, then **Hide the full command**), and has no **Allow** until the whole command shows; Deny and the countdown always stay. Showing it scrolls the whole card into the region when it fits, else its actions. A shown command stays shown for every view of the chat while the request waits; guests and timed-out requests show it whole.
291
292
  A request with `expiresAt` counts down as m:ss; the count is hidden from screen readers, which hear its end time, then "Less than a minute left" and "Timed out", so the log's live region doesn't speak every second; a timed-out request (`data-expired`) loses its buttons.
292
- While pending, cards leave the log for their own region between the header and the log (`section.asks`, named "Waiting for you"), so they never cover the transcript and the log ends below them; the log keeps an empty marker where each was asked (`.ask-place[data-place]`, the card's `data-id`). The region shows the newest card; with more than one waiting, a bar above it (**N waiting**, `data-action="show-waiting"`, `aria-expanded`) shows them all, oldest first (`.asks[data-open]`), and shows the countdown of the soonest timed card it hides. A hidden card stays mounted (`.ask[hidden]`), so a typed answer survives, and a new ask never hides the card that has focus: the list opens instead. Showing another agent closes the list and its details. The region is opaque, with a hairline and a soft shadow on its bottom edge. Open, each card is compact: its title line and its actions, the request's text and its command each cut to one line, the command still in monospace so nobody allows what they can't see, and the rest behind a quiet chevron (**Show details**, `data-action="ask-detail"`, `data-detail` on the card while shown). The region takes at most half the chat's height and always leaves the log room for the composer and its fade (and a few lines more while closed); when the cards need more, it scrolls inside, the bar stays on top, a fade at the bottom edge (`.asks[data-more]`) says more is below, and the scrollbar shows on hover and focus. Every card is a tab stop before its actions, and focusing anything in a card scrolls the whole card into the region.
293
+ While pending, cards leave the log for their own region between the header and the log (`section.asks`, named "Waiting for you"), so they never cover the transcript and the log ends below them; the log keeps an empty marker where each was asked (`.ask-place[data-place]`, the card's `data-id`). The region shows the newest card; with more than one waiting, a bar above it (**N waiting**, `data-action="show-waiting"`, `aria-expanded`) shows them all, oldest first (`.asks[data-open]`), and shows the countdown of the soonest timed card it hides. A hidden card stays mounted (`.ask[hidden]`), so a typed answer survives, and a new ask never hides the card that has focus: the list opens instead. Showing another agent closes the list and its details. The region is opaque, with a hairline and a soft shadow on its bottom edge. Open, each card is compact: its title line, its command and its actions, the request's text cut to one line and the rest of it behind a quiet chevron (**Show details**, `data-action="ask-detail"`, `data-detail` on the card while shown). The region takes at most half the chat's height and always leaves the log room for the composer and its fade (and a few lines more while closed); when the cards need more, it scrolls inside, the bar stays on top, a fade at the bottom edge (`.asks[data-more]`) says more is below, and the scrollbar shows on hover and focus. Every card is a tab stop before its actions, and focusing anything in a card scrolls the whole card into the region.
293
294
  Clicking a waiting card (not its controls), or <kbd>Enter</kbd> on it, scrolls the log to where it was asked, at the bottom of the view above the composer.
294
295
  When a card leaves the region (answered, timed out or decided elsewhere) while it has focus, focus moves to the newest card still waiting, else the composer; a timed-out card goes back to its place in the log.
295
296
  Under `prefers-reduced-motion`, nothing animates and the scroll is instant.
@@ -320,6 +321,7 @@ The model's `notices` as in-line alerts with their actions and a close button (n
320
321
  The model's waiting `approvals` as a modal alert dialog, one at a time, oldest first; `<slicc-app>` mounts it.
321
322
  Renders nothing without `approvals.decide`.
322
323
  A request that `secretRules()` flags shows a warning (`[data-secret-rules]`, the path or token) and no **Always allow (**Always allow in this directory**, with the cwd in its hint, when the approval has `alwaysScope: 'cwd'` and a `cwd`)**.
324
+ The command (`[data-command]`, or a guest tool call's `[data-summary]`) wraps in full; past four lines the dialog shows the first four and **Show the full command** (as in the chat, `data-action="full-command"` with `data-shown`), and shown, it scrolls inside its own box of at most `min(16lh, 50dvh)`. Always allow, its hint and Allow once wait until the end of the command has been in view (a zero-size `[data-end]` after its last line), and so does the element's `decide()` for anything but `deny`; once seen, the end stays seen while the request waits, and hiding the command again hides them again. Until then a subdued line (`[data-allow-hint]`) says "Show the full command to allow it." or, shown, "Scroll to the end of the command to allow it." The dialog keeps one width, so only its height changes. A closed request shows its command whole.
323
325
  Focus starts on Deny (on Close once `decide` rejects) and goes back, after Deny, Always allow, Allow once, <kbd>Esc</kbd> or Close, to what had it when the dialog opened; inside a terminal, to that `<slicc-terminal>` through its own `focus()`, even when it remounted meanwhile.
324
326
  Guest requests (`guest-message`, `guest-tool`) get their own copy, the guest's text as plain text (`[data-guest-text]`), attachments (`[data-attachments]`), the automatic check's verdict (`[data-verdict]`), and Deny and Allow once, plus **Always allow** (`data-action="always"`) with a hint saying what it covers when the approval's `always` is `true`; the dialog body carries `[data-approval-kind]` and fills the dialog's width at every size.
325
327
 
@@ -421,6 +423,16 @@ The Browser rail button carries a dot (`data-driven`) while a tab is in use.
421
423
  Theme, the model and thinking level for new cones, composer and diff preferences (push to talk only where the browser has speech recognition), and accounts to connect, reconnect or disconnect.
422
424
  When the `updates` port has `setDeveloper`, an Updates row has the **Developer mode** switch (`sp-switch[data-setting=developer]`, `checked` from `developer()` on every `items`), described as "Check npm for newer releases that aren't certified yet, and offer them as betas in Install / Update."; while `setDeveloper(on)` is pending it is disabled with `aria-busy` and a progress circle, and a rejection shows its message under it (`[data-error]`).
423
425
  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.
426
+ A `signing-in` account can carry `signIn` (`{ url, code? }`), which only counts while it is `signing-in`: the embedder's `connect(id)` opens the pop-up in the click and sends it to `url`, and Spectrum never opens a window itself.
427
+ With a `code` (a device code), the row (`.notes`), the **Add {name}** step (under its buttons, so they don't move) and the chat's expired-session card read “Enter this code on the sign-in page:” over the code in large monospace (`[data-id=sign-in-code]`, `user-select: all`) and a quiet **Copy** (size `m`, `s` in the chat card like its other buttons, `data-action="copy-sign-in-code"`, “Copy the code”) that writes it to the clipboard and reads “Copied”, also in a polite status, for two seconds, or “Couldn’t copy” when the clipboard refuses.
428
+ A `url` is a quiet **Open the sign-in page** link (`data-action="open-sign-in"`, a new tab with `rel="noopener noreferrer"`), for when the window was closed or blocked, only when it is `https:`, has no username, password or port, and its exact hostname is a known sign-in host for that account's id (`signInHosts` in `sign-in.ts`: `openai-codex` `auth.openai.com`, `xai` `accounts.x.ai`, `kimi-coding` `www.kimi.com`).
429
+ Any other `url` shows as plain selectable text (`[data-id=sign-in-url]`), with no link and no Copy, under a quiet warning with an alert icon (`[data-id=sign-in-unknown]`): “This sign-in page isn’t one SLICC knows. Check the address before you go there.”; the code and its Copy stay.
430
+ Without `signIn`, the signing-in view is unchanged, and the code and link go once the account is no longer `signing-in`.
431
+ An account with `auth: 'oauth'` and `secret: 'api-key'` signs in or takes a key: its primary action signs in, calling `connect(id, undefined, undefined, { method: 'sign-in' })` within the click.
432
+ Under it, a quiet standalone link-style button **Use an API key instead** (`button.swc-Link[data-action="use-key"]`), aligned with the row's text, opens the account's key form without Cancel and focuses its field (underlined in LinkText in forced colors), and **Sign in instead** (`data-action="use-sign-in"`) under the form closes it and focuses the sign-in button again.
433
+ Its key goes to `vault.put` as for any `store: 'vault'` account (or as the `secret` without one), then to `connect(id, undefined, fields, { method: 'key' })`, and it never reads “Signing in…” while that is pending.
434
+ Disconnect and Remove delete its key record first, as for any vault-backed key, and **Sign in instead** is hidden while its key connect is pending.
435
+ Every other account calls `connect` without `options`, as before.
424
436
  Accounts with `group: 'mcp'` (MCP servers) get their own **MCP servers** heading (`.accounts[data-group=mcp]`).
425
437
  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**.
426
438
  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`.
@@ -476,12 +488,17 @@ It closes, clearing the field, when the request leaves `waiting` or while the va
476
488
  Install / Update: component states, version changes, last checks, download and link counts, action buttons, failures with Retry, and collapsible install logs behind a quiet chevron, which stay open across a layout switch.
477
489
  A row with a pending version change (`from → to`, or a package's `version → offered`) and release notes shows a What’s new disclosure after the row's actions (in package rows, after the command chips), next to the Install log, `details[data-releases]` with `summary[data-action="whats-new"]`, which stays open across a layout switch like the log. It lists every release after the installed version up to the new one, newest first, each as `.release[data-version]` with its version linking to the release page (`a[data-action="release-notes"]`, named "Kernel 1.13.0 release notes"). Only the newest (`.release[data-newest]`) shows its notes; each older release is a collapsed header, `details.release` with `summary[data-action="release"]` holding the same chevron, its version link and badge across the full row and named "Kernel 1.12.2 notes, certified", that opens to its notes indented under its title and stays open across a layout switch like the disclosure (an older release without notes is a plain header). The notes are rendered as markdown in strict mode (`markdown(text, { strict: true })`: raw HTML tags are dropped and their text kept as plain text, only `https:` links stay links, and an image becomes a link with its alt text, so nothing loads; the notes' headings sit one level under the release's, through `level`). Notes over 8 KB (8,192 characters) per release are cut at the last line break before that, with "…" and a Read the full notes link (`[data-cut]`, named "Read the full notes for Kernel 1.13.0"); a release whose `url` isn't `https:` shows its version as plain text. The `<!-- cert-verify -->` block is dropped, and a block that starts with "certified" shows as a subtle Certified badge (`[data-certified]`); a heading line naming the version is dropped too, since the link already shows it. Without notes in the range there is no disclosure.
478
490
  Certified is shown only for ai-ecoverse releases (a `url` of `https://github.com/ai-ecoverse/<repo>/releases/…`) whose cert block names the same package and version (`certified <pkg>@<version>`; the package is checked on package rows, which know their npm name), and any other cert block is still dropped without a badge.
479
- An up-to-date row (no pending change) links its installed release with Release notes next to the version; an installed optional package without one links its npm page instead (`a[data-action="npm-page"]`, named "npm page for Biome 2.2.4"). Pins and additions in Your packages show no link. Every link opens in a new tab.
491
+ An up-to-date row (no pending change) links its installed release with Release notes next to the version; an installed optional package without one links its npm page instead (`a[data-action="npm-page"]`, named "npm page for Biome 2.2.4"), built from the package name alone as `https://www.npmjs.com/package/<name>` with each part of a scoped name encoded, never from npm's homepage or repository. Pins and additions in Your packages show no link. Every link opens in a new tab.
480
492
  An install shows one bar for update rows and package rows alike: it fills 0–50% while downloading and 50–100% while linking (`value` against `max-value="100"`). While the row runs it never goes back, not when pnpm's total grows and not when progress drops out between passes. While a download reports `bytes` with a total above 0, the first half fills by bytes and the value label reads "10.0 MB of 40.0 MB". Otherwise, and always while linking, the label falls back to the phase's "n of m" count. The bar starts at 0 again once the row settles.
481
493
  Opens at boot until the agent is ready without failures.
482
494
  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.
483
495
  Every automatic close is saved to the layout.
484
- 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.
496
+ When the port offers packages, an Optional packages section lists them below as compact rows, each with its state, version, size, requirements, commands, progress, errors and Install log; packages never count in the header or keep the panel open at boot.
497
+ A row's description is clamped to two lines at every width, and the row's buttons carry it as their accessible description (`ariaDescribedByElements`), so it can always be read in full.
498
+ While npm hasn't answered for a row (`registry: 'checking'`) its description slot reads "Checking npm…"; when npm can't be reached (`registry: 'unreachable'`) the row shows no description line at all, since the section hint says it once (`li[data-registry]`). Such a row has no Install and no light, an installed one keeps its version, Remove, npm page and Install log, and `retry` is never shown on it.
499
+ When a row is unreachable the section hint reads "Couldn’t reach npm, so versions and sizes aren’t shown." with one secondary **Retry** next to it (`data-action="retry-registry"`, named "Retry reaching npm", only when the port has `refreshPackages`). While it runs the hint reads "Checking npm…" and Retry is pending; when npm still can't be reached, the hint goes back and a polite live region (`[data-still]`) says "Still couldn’t reach npm." once. Retry keeps focus while it stays, and hands focus to the Optional packages heading whenever it goes while focused. An unreachable row that isn't installed shows only its label, and no row with `registry` describes its buttons.
500
+ A row with `withheld` reads "Not offered: <reason>." in a subdued line under its description (`[data-withheld]`, capped at 200 characters), and never shows Install or Update: an outdated withheld row reads Installed with its installed version.
501
+ A row with `newer` offers its older `offered` version as usual and says why the latest isn't offered in the same subdued line (`[data-newer]`), such as "0.26.0 isn’t offered yet: needs slicc-kernel 1.48.0 or newer."; a row that has both shows only `withheld`
485
502
  With a port that has `setDeveloper`, a row's `npm` check shows under its version: "npm: no newer release", "Checking npm…", its `reason` when the check failed (the state light stays), "{installed} (certified) · {latest} on npm, not offered: {reason}" (a certified update keeps its target, "6.6.0 (certified) → 6.7.0 · …", and so does a beta row, "6.13.5 beta · certified 6.6.0 → 6.7.0"), or, when `offered`, "{installed} (certified) · {latest} on npm, not certified" with "12 packages change in /opt/agent." (the folder in the label's parentheses; "1 package changes …"), followed by "@slicc/kernel-bridge and @slicc/vault stay at their certified versions." for `gated` ("… stays at its certified version." for one) and **Install beta**; `article[data-npm]` is the state.
486
503
  A row with `beta` has a notice **Beta** badge by its name (`[data-beta]`), a notice "Beta installed" light while it is `current` or `installed` ("Up to date" means the certified version only; Ready to apply, Failed and the running states keep their words), reads "{version} beta · certified {certified}" and offers **Back to certified** next to its other actions; Install beta shows only while `npm` is `offered`.
487
504
  While `npm` is `offered`, What’s new covers the releases from the installed version up to npm's `latest`, so an offered beta lists its own notes.
@@ -728,8 +745,8 @@ The optional `importProfile(file, { preview, again? })` takes a SLICC 6 profile
728
745
  `importCancel()` stops it, and `importing()` returns a `ProfileImportProgress` (`phase`: `idle`, `checking`, `writing`, `done` or `failed`, with `done`, `total` and `text`), with an `importing` event; the UI never sees kernel paths, so the adapter decides how the file reaches the agent.
729
746
  Names don't have to be unique, and an optional `Agent.title` names a cone's conversation; without it, the first user message does.
730
747
  Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`.
731
- The optional `toolLabel(agentId, callIds)` resolves with a short label for a cluster of tool runs (`callIds` in order), such as "Compare the docs with the live API", or `null`; the agent dedupes, caches and persists it.
732
- Spectrum asks once a cluster forms and again each time it grows, once per set of ids, and shows the label at the end of the strip.
748
+ The optional `toolLabel(agentId, callIds)` resolves with a short label for a run of tool calls (`callIds` in order), such as "Compare the docs with the live API", or `null`; the agent dedupes, caches and persists it.
749
+ Spectrum asks once a reply's status has two calls and again each time it grows, once per set of ids, and shows the label once the status is complete.
733
750
  The optional `readOnly()` returns a line, or `null`, with a `readOnly` event when it changes: while it's a non-empty string, such as when this follower runs a different major version from its owner, every surface is watch-only just as a follower without Control is (no composer, no cone or conversation actions, no pickers, no Stop, no Accept or Revert in Changes, inline sprinkles view only, no model settling), and the line takes the place of the follow-based watch-only note under the chat.
734
751
  The optional `starting()` returns a line, or `null`, with a `starting` event when it changes: while it's a non-empty string, there's no agent yet but one is on its way (such as while seven installs its first agent, or replaces one), the chat drops its empty state and the composer queues what's sent until an agent is there (see `<slicc-composer>`); without it, or at `null`, nothing changes.
735
752
 
@@ -763,6 +780,7 @@ Theme, model, thinking level, composer and diff preferences, the models on offer
763
780
  A model can say it is its provider's `default`.
764
781
  Emit `settings` or `accounts` when the models on offer change.
765
782
  The optional `providers()` returns the `ProviderOption`s that can be added (`{ id, name, description?, auth: 'api-key' | 'oauth' | 'fields' | 'local', fields?, keyUrl?, needs?, blocked?, vaultName? }`, sorted by name, added ones included), with a `providers` event when they change; `id` is pi-ai's provider id and becomes the `Account.id`. With it, `accounts()` lists only added accounts, `connect(id, secret?, fields?)` adds one (`group: 'providers'`; a `fields` provider becomes an `api-key` account with `store: 'vault'` and only its non-secret `fields`), and the optional `remove(id)` drops one. `Account` adds `keyUrl`, `description`, `vaultName`, the vault record for its key (else pi-ai's environment-variable name), and `url`, an `mcp:<server>` account's server URL. `needs: 'cors-free-transport'` on either is static (the provider doesn't answer a browser); `blocked: 'cors-free-transport'` is live, set while the current transport can't reach it, and cleared by a `providers` and an `accounts` event when an extension or a linked slicc appears.
783
+ `Account.signIn` (`{ url, code? }`) is what a `signing-in` account's sign-in page and device code are, and `connect` takes an optional fourth `options` (`{ method: 'sign-in' | 'key' }`) for an account that signs in or takes a key (`auth: 'oauth'` with `secret: 'api-key'`).
766
784
 
767
785
  #### `changes` (optional)
768
786
 
@@ -821,8 +839,12 @@ A row's and a package's optional `releases` (`UpdateRelease[]`) lists the GitHub
821
839
  Optional `developer()` (false by default) and `setDeveloper(on)`, on the owner's page only, turn developer mode on and off; `setDeveloper` resolves once settled or rejects with a displayed error, and the port emits `items` when either changes.
822
840
  A row's optional `beta` (`{ version, certified }`) is set while a beta is installed, and its optional `npm` (`{ state, latest?, reason?, changes?: { count, gated } }`, `state` one of `checking`, `current`, `offered`, `not-offered` or `failed`) is `null` while developer mode is off.
823
841
  Missing ports preserve existing adapters and report disconnected information in the panel.
824
- 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.
825
- 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` (`InstallProgress`, or `null` for an indeterminate bar), `error`, `actions` (`install`, `update`, `remove`, `retry`, `unpin`) and an optional `log`.
842
+ 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`, the `description` (npm's, untrusted: HTML tags and comments go and their text stays, entities are decoded, markdown emphasis, inline code, links and images become their text, leading headings, quotes, list markers and code fences go, then whitespace collapses and it's capped at 300 characters with "…"; it's never rendered as markup or a link), 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` (curated caveats, short plain-text lines shown muted under the row and never clamped, stripped the same way and each capped at 300 characters, 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.
843
+ 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 (read "28.4 MB on disk" with `unpacked: true`, when it's npm's `dist.unpackedSize`, else the bare size), `progress` (`InstallProgress`, or `null` for an indeterminate bar), `error`, `actions` (`install`, `update`, `remove`, `retry`, `unpin`) and an optional `log`.
844
+ The description is npm's, and curated caveats go in `notes`, shown under it.
845
+ Optional `registry` is `checking` or `unreachable` while the port has no npm data for the row, fresh or cached: send it with `offered: null`, no `size`, an empty `description` and no `commands`, keep an installed row's actions, and give a row that isn't installed `retry` (which refetches, so a spectrum without `registry` can still retry) or nothing while checking.
846
+ Optional `withheld` is the reason, in words and without a prefix, that seven offers no version of a package npm has, such as "needs slicc-kernel 1.48.0 or newer"; send it with `offered: null` and no `install` or `update`, and for an installed package send `newer` instead (an installed row with `withheld` still reads "Not offered: <reason>.").
847
+ Optional `newer` (`{ version, reason }`) is npm's latest when it doesn't pass the gate but the `offered` older version does, with its blocker in the same words as `withheld`.
826
848
  A row with `origin: 'user'` (ids such as `user:user:<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/user`.
827
849
  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/user`; `.meta[data-pin]` is `pinned` or `added`.
828
850
  A pin that is `removing` reads "Unpinning"; other states keep their usual lights.
@@ -833,8 +855,9 @@ Each install the panel doesn't own shows in the meta line as `[data-install]`, s
833
855
  When no install is the panel's, `[data-managed]` under it reads "Managed by pnpm. Remove it with `pnpm remove -g <package>`." (the command in a `code` element; or "Installed outside this panel. Remove it where it was installed." for another manager).
834
856
  Offer `update` for such a row to update it in place, for that user and with that manager, never as a second copy; never offer `remove` for it.
835
857
  Emit `packages` whenever they change.
858
+ Optional `refreshPackages()` refetches the catalog from npm, flips the rows without data to `checking`, emits `packages`, and resolves when the fetch settles; a rejection is ignored, since the rows say what happened.
836
859
  `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.
837
- Hooks: `section[data-section="packages"]`, `li[data-id][data-state]`, buttons with `data-action`, `[data-requires]`, `[data-notes]` and `[data-error]`.
860
+ Hooks: `section[data-section="packages"]`, `li[data-id][data-state]`, buttons with `data-action`, `li[data-registry]`, `[data-withheld]`, `[data-newer]`, `[data-size]`, `[data-requires]`, `[data-notes]` and `[data-error]`.
838
861
  Real installer wiring belongs to ai-ecoverse/slicc-bios#70.
839
862
 
840
863
  #### `network` (optional)
@@ -917,8 +940,8 @@ When a request ends without a decision in this tab, emit `ended` with `{ id, rea
917
940
  Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it.
918
941
  Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "It waits for the owner's approval.
919
942
  Denied automatically at {time}."
920
- A guest request has `kind` `guest-message` (`text`, optional `attachments` of `{ name, mimeType, size }`) or `guest-tool` (`tool`, `summary`), plus `requester` (`kind` `guest`), `agentId`, optional `agentName`, `createdAt`, `expiresAt`, `always` (offer Always allow, a policy default the owner can change) and an optional `verdict` (`{ decision: 'allow' | 'deny' | 'unsure', reason }`, the automatic check's view).
921
- Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); then "The automatic check would allow this: {reason}", "…deny this: {reason}" or "The automatic check wasn’t sure: {reason}" (`[data-verdict]`, the decision in the attribute).
943
+ A guest request has `kind` `guest-message` (`text`, optional `attachments` of `{ name, mimeType, size }`) or `guest-tool` (`tool`, `summary`, optional `args`, the call's arguments as text, which the embedder pretty-prints, and `argsTruncated`, `true` when only a prefix of them came), plus `requester` (`kind` `guest`), `agentId`, optional `agentName`, `createdAt`, `expiresAt`, `always` (offer Always allow, a policy default the owner can change) and an optional `verdict` (`{ decision: 'allow' | 'deny' | 'unsure', reason }`, the automatic check's view).
944
+ Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); when the summary is blank, the block shows the tool's name on its first line and then `args` instead, under the same four-line and end-of-command rules, and with no `args` either, "This request has no details." (`[data-id="no-details"]`) and Allow as usual; with a blank summary and `argsTruncated`, the dialog never offers Allow once or Always allow and `decide()` takes only `deny`, saying "This request is too long to show in full, so it can’t be allowed." (`[data-allow-hint]`) while the args still show; then "The automatic check would allow this: {reason}", "…deny this: {reason}" or "The automatic check wasn’t sure: {reason}" (`[data-verdict]`, the decision in the attribute).
922
945
  It offers **Deny** and **Allow once**, and, when `always` is `true`, **Always allow** (`always`, which calls `decide(id, 'always')`) with the hint "Always allow lets {name} send messages to {agentName ?? 'this cone'} without asking again." or "Always allow lets {agentName ?? 'this cone'} run exactly this {tool} call for {name} without asking again.".
923
946
  `decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo.
924
947
  Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out".
@@ -976,7 +999,9 @@ The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current'
976
999
  Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot.
977
1000
  The dummy's kernel offers three releases from 1.12.0 to 1.13.0, two of them certified, the agent one release to 0.25.0, and the syntax grammars only their installed release; its uv package offers two releases, Python links its release and the other installed packages their npm pages.
978
1001
  Dummy Update now and Retry simulate download and link progress, and a dummy package install reports half its `size` in bytes on the way; Restart agent and Reload only change fixture state, never restart a real process or reload the browser.
979
- `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'elsewhere' | '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, `elsewhere` has git installed for cone with pnpm and TypeScript there at an older version, whose Update keeps it in place, and Biome both from the panel and for cone, whose Remove leaves cone's copy, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`).
1002
+ `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'elsewhere' | 'npm' | 'checking' | 'offline' | '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, `elsewhere` has git installed for cone with pnpm and TypeScript there at an older version, whose Update keeps it in place, and Biome both from the panel and for cone, whose Remove leaves cone's copy, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`).
1003
+ `npm` is that catalog as it comes from npm: sizes on disk, TypeScript with a description over 300 characters that looks like markdown, Ruff with a curated note, esbuild offered at 0.25.10 while 0.26.0 isn't, PDF tools withheld and Biome installed at 2.2.4 while 2.3.0 isn't offered yet, with a hostile `homepage` and `repository` its npm page link ignores.
1004
+ `checking` and `offline` are the same rows before npm answers and when it can't be reached; the section Retry checks again (so does a row's `retry`, which only a spectrum without `registry` shows), and `offline` fails the first time and answers the second.
980
1005
  `createDummyModel({ beta: 'offered' | 'installed' | 'blocked' | 'failed' | 'off' })` (or `?beta=` on `/ui/`, off by default) turns developer mode on with an invented agent at 6.6.0 (certified) and 6.13.5 on npm, 12 packages and nothing gated: `offered` offers Install beta, `installed` runs the beta, `blocked` isn't offered because it needs a newer kernel, and `failed` is a failed beta install with Retry and Install beta; `setDeveloper` toggles it after a short wait (checking npm again when turned on), and Install beta and Back to certified simulate a short install.
981
1006
  `updateFixtures`, `betaFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`.
982
1007
  `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.
@@ -1025,6 +1050,10 @@ With `starting`, after 100 ticks of the dummy clock (3 s at the default delay) t
1025
1050
  `createDummyModel({ import: 'v6' | 'again' | 'fail' | 'cancel-check' })` (`?import=` on `/ui/`, off by default) adds `importProfile`, `importCancel` and `importing` to the agent: any zip previews an invented export from www.sliccy.ai (3 cones, 14 scoops, 2 frozen, Anthropic and OpenAI to sign in to again, 2 mounts and a flagged `files/cones/cone/.env`), and the import ticks to 14 in a few steps and adds 3 frozen cones with `v6: true` and the report `/root/v6-import-2026-10-18.md`; a second preview says when, and **Import again** adds fresh copies. `again` starts with an earlier import, `fail` rejects with "That zip isn’t a SLICC 6 profile export.", and `cancel-check` checks slowly. `DummyImport` and its fixtures (`importPreview`, `importCones`, `importReport`, …) are exported from `/dummy`.
1026
1051
 
1027
1052
  The kitchen sink's “docs-pass failed” card is an expired-session error for the invented `ai-adobe` account (Adobe AI, harbor-ops@example.com, `expired`, OAuth): **Log in** goes `signing-in`, then `connected` after a short wait, and the dummy records each `connect` in `settings.connects` (`{ id, gesture }`, whether `navigator.userActivation.isActive` was true at the call); inbox-triage's “Tickets connector” card names `mcp:tickets`, a token account, so its **Log in** opens Settings at that row; `createDummyModel({ login: 'fail' })` (`?login=fail` on `/ui/`) makes OAuth sign-ins reject with “The sign-in window was closed before it finished.”
1053
+ The dummy's sign-ins carry `signIn` while `signing-in`: OpenAI Codex a device code on its known host (`{ url: 'https://auth.openai.com/codex/device', code: 'WDJB-MJHT' }`, `deviceSignIn`); `createDummyModel({ login: 'code' })` (`?login=code` on `/ui/`) gives every OAuth sign-in that device code, `login: 'url'` (`?login=url`) only a sign-in page on that host (`pageSignIn`), and `login: 'lookalike'` (`?login=lookalike`) the device code on `https://auth.openai.com.example.net/device` (`lookalikeSignIn`), so only OpenAI Codex gets a link.
1054
+ Those three scenarios also add an expired OpenAI Codex account (`openai-codex`, `codexAccount`) and point the kitchen sink's “docs-pass failed” card at it, so its **Log in** shows the code with the link (or, with `lookalike`, the warning).
1055
+ The dummy records a `connect`'s `options` in `settings.connects`, and `{ method: 'key' }` connects without a sign-in, reading “API key”.
1056
+ `createDummyModel({ accounts: 'dual' })` (`?accounts=dual`) is `accounts: 'vault'` plus the invented Lumen AI (`lumen`, `auth: 'oauth'`, `secret: 'api-key'`, `store: 'vault'`, `keyUrl`, `vaultName: 'LUMEN_API_KEY'`), which signs in or takes a key.
1028
1057
  `createDummyModel({ providers: 'none' })` (`?providers=none` on `/ui/`) starts with no model provider connected: `models()` offers only the models of connected accounts, and connecting OpenAI offers the invented GPT-6 and GPT-6 mini (its `default`).
1029
1058
  `createDummyModel({ providers: 'all' })` (`?providers=all` on `/ui/`) adds `providers()` and `remove` with 14 invented providers (`providerFixtures` from `/dummy`): API keys for Anthropic, Cerebras, Groq, Mistral, OpenAI and OpenRouter, OAuth for Adobe AI, GitHub Copilot and OpenAI Codex, fields for Amazon Bedrock (region), Azure OpenAI (endpoint, a `customBase`, and deployment) and Cloudflare (account id, CORS-free, its own `vaultName`), and Ollama and LM Studio on this device; today's rows start as added, with Amazon Bedrock and a connected Ollama (`providerAccounts(accounts)`, `providerAccount(option)`). `connect` adds a provider and drops it again when it fails or is cancelled, a key or field starting with `bad` is refused (“{name} didn’t accept that key.”), an endpoint without `https://` is refused, and `remove` drops it.
1030
1059
  `createDummyModel({ providers: 'all', transport: 'none' })` (`?providers=all&transport=none`) has no CORS-free transport: every provider with `needs` is also `blocked`, and Cloudflare starts connected, so it reads Unreachable. `model.settings.setTransport('extension')` (or `'link'`) clears `blocked` live, with a `providers` and an `accounts` event, and `setTransport('none')` sets it again.
@@ -1033,8 +1062,9 @@ The dummy's `docs` and `tickets` MCP accounts have a `url`, the dummy vault reco
1033
1062
  `createDummyModel({ monitor: 'agent' })` (`?monitor=agent` on `/ui/`) adds the agent's Tasks (a tree by `depth`), Processes and Recent problems sections to the monitor, and its Running vital.
1034
1063
  Like slicc-agent's snapshot, the dummy's vitals are the budget, Spend, Active agents, Processes and Process memory (both with a `series`) and Fullest context.
1035
1064
 
1036
- `createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'guest' | 'guest-follower' | 'guest-always' })` (`?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, or one from an agent that copies a file over `/etc/fnox/config.toml`, or (`guest`) three guest requests: a message from Seat 2 (Mara) with attachments and an allow verdict, a tool call with a deny verdict, and a message whose `decide` rejects with "Someone else already answered this."
1037
- (`guest-follower`: the same without `decide`), or (`guest-always`) one guest tool call from Seat 2 (Mara) to `read_file` with `always: true`.
1065
+ `createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'guest' | 'guest-follower' | 'guest-always' | 'long' | 'guest-args' | 'guest-empty' | 'guest-truncated' })` (`?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, or one from an agent that copies a file over `/etc/fnox/config.toml`, or (`guest`) three guest requests: a message from Seat 2 (Mara) with attachments and an allow verdict, a tool call with a deny verdict, and a message whose `decide` rejects with "Someone else already answered this."
1066
+ (`guest-follower`: the same without `decide`), or (`guest-always`) one guest tool call from Seat 2 (Mara) to `read_file` with `always: true`, or (`long`) one sudo request whose seven-line command ends in `curl … | sh`, or one guest `write_file` call with a blank `summary`: with seven lines of `args` ending in the same tail (`guest-args`), with no `args` (`guest-empty`), or with a prefix of them and `argsTruncated` (`guest-truncated`).
1067
+ `createDummyModel({ asks: 'long' })` (`?asks=long`) adds a pending sudo request with that seven-line command to kitchen-sink (`k-long`).
1038
1068
  Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
1039
1069
 
1040
1070
  `createDummyModel({ vault: 'empty' | 'secrets' | 'policy' | 'locked' | 'request' | 'backup' | 'waiting' | 'purged' | 'endpoints-fail' | 'off' })` (`?vault=` on `/ui/`, off by default; `notice` is kept as another name for `policy`) adds an invented vault: no secrets yet; five secrets (an API key, a jerk lease from pier-nuc revealed to `pier publish`, a Tide Mail sign-in, one without a rule and a removed one) with an activity log that pages back once; the same with a refused rule and a policy change in the log; locked before the first unlock, with a waiting request from docs-pass; unlocked with a `fnox set` request for `TIDE_STATION_TOKEN`; the secrets with a due backup reminder; or a vault that hasn't answered yet (`model.vault.arrive()` brings it); Cloudflare's Account ID and AI Gateway ID and Azure's endpoint carry `vaultSetting`s; the dummy vault keeps `settings` on its records, logs each `settings` call in `model.vault.settingsCalls`, and refuses a setting whose value starts with `bad` (“The vault refused {KEY}. Check that value and try again.”). `endpoints-fail` is the secrets with an `endpoints` that refuses (an MCP URL with “The vault refused {url} for this token. Check the server's address, then try again.”, a model URL with “The vault refused that endpoint. Check the address and try again.”). The dummy vault records each `endpoints` change in `model.vault.endpointChanges`.
@@ -1081,16 +1111,17 @@ Each section is its own port, so an embedder can mix real and dummy per section,
1081
1111
  `<slicc-file-view>` has **Edit**: the file opens in a plain text area, and **Save** or `Mod+S` writes it through `files.write`; `Escape` cancels.
1082
1112
 
1083
1113
  The `kitchen-sink` cone holds every kind of message, content and lick in one conversation, for design work and screenshots.
1084
- Its clusters carry invented labels, which the dummy's `toolLabel` answers after a short pause, keyed by the title of the cluster's first call; one is long enough to be cut.
1114
+ Its runs of tool calls carry invented labels, which the dummy's `toolLabel` answers after a short pause, keyed by the title of the run's first call; one is long enough to be cut.
1085
1115
  Each reply to it is paced to take about 20 seconds, so its streaming states can be looked at.
1086
1116
  A prompt that says search or grep runs a search.
1087
- Its tool turn has two clusters and a standalone run with URL, file and plain commands in every state, and two dummy browser tabs carry invented `data:` favicons.
1117
+ Its tool turn has two runs and a standalone tool message with URL, file and plain commands in every state, and two dummy browser tabs carry invented `data:` favicons.
1088
1118
  The dummy's fixtures are invented: a small forecast API called harbor, with cones, scoops, a conversation per agent, pending changes (one of them binary, without a diff), browser tabs and accounts.
1089
1119
  The harbor cone ends with three `scoop-notify` licks: amber-heron's, finished with a `summary`, a markdown body and a `report` at `/shared/scoop-notifications/amber-heron.md`; grey-tern's, failed, with no summary; and salt-gull's, only the legacy bracketed line.
1090
1120
  Its only sprinkles are SLICC's own: `welcome` (the onboarding wizard, as a dip in the sliccy cone) and `suggestions`, copied from `slicc` (`packages/vfs-root/shared/sprinkles/`, commit `aa29784`) with their comments stripped, as is the sprinkle theme in `src/app/sprinkle-theme.css`.
1091
1121
  `suggestions` reads its stream from the invented `/home/.gelatiere/suggestions.json` through the bridge; there is no `/home/.welcomed`, so `welcome` starts its wizard.
1092
- Its replies are scripted by keyword (debug or investigate, tests, fix or add, open or docs, files, anything else), and their tool calls act on the other ports: an edit writes a file and shows up as a change, a browse opens a tab.
1093
- Debugging runs seven commands in a row, so its cluster grows past five while it streams.
1122
+ Its replies are scripted by keyword (replay, debug or investigate, tests, fix or add, open or docs, files, anything else), and their tool calls act on the other ports: an edit writes a file and shows up as a change, a browse opens a tab.
1123
+ Debugging runs seven commands in a row in one reply, so its status grows a step at a time while it streams.
1124
+ Replay plays a longer run across ten replies, with long commands whose arguments stream in and tool-only replies in a row, for checking that past messages never move down; `?replay` on the UI test page starts it on load.
1094
1125
 
1095
1126
  ### Distribution
1096
1127