@ai-ecoverse/slicc-spectrum 1.76.0 → 1.77.1

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
@@ -137,15 +137,15 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
137
137
  | `<slicc-app>` | The shell: header with the agent picker, status items, the update status, the network indicator, the tray, the View menu and the theme switch, rails, and a `<slicc-dock>`. Saves the dock layout per screen class to `storage` (default `localStorage`) under `layoutKey`. While connected it sets `overscroll-behavior` to `none !important` on the page's `html` and `body`, so only scroll areas inside panels rubber-band. It sets the page's `color-scheme` to its resolved theme, and when the page has a `theme-color` meta per `prefers-color-scheme`, gives every one of them the content of the meta for that theme, so the page and the installed app's title bar follow the app's setting instead of the OS. When the last app on the page is removed, the page's own inline values and `theme-color` contents come back. With a `follow` port, the header shows a `<slicc-follow>` chip, and while the follower connects, waits to be allowed, is unlinked or fails, a `<slicc-follow page>` (`.alone[data-follow]` with the page kind) takes the place of the panels; without Terminal access, the terminal surface and New terminal aren't offered, and open terminals close when it's taken away. |
138
138
  | `<slicc-dock>` | Generic. [dockview-core](https://dockview.dev/) 8 with a theme mapped to Spectrum tokens, in its own shadow root. Panels come from `factories` (`name → (params) => HTMLElement`). `open`, `close`, `has`, `content`, `focusPanel`, `cycleGroup`, `cycleTab`, `toJSON`, `restore`, `clear`, and the full `api`. Tabs have a context menu (close, close others, float, maximize). Events: `layout-change`, `active-panel-change`, `panel-close`. |
139
139
  | `<slicc-agents>` | A tree (`role="tree"`) of cones, with their scoops hidden under them. A row's first line starts with one circle: its unread count when there are unread messages (`9+` from ten up, white on the accent fill), otherwise a dot coloured by status. Then comes the name; the context fill, when it's high, sits in the top right corner. Every row has up to two lines starting with its status in words (`.state`, such as "Working"), then its conversation title, so the status stays visible when the unread count takes the circle; the accessible name says the status and the exact unread count too. A cone's row also has a ribbon along its bottom with one fixed-width segment per scoop, coloured by status; it's `aria-hidden`, and the cone's `aria-description` sums it up, as in "2 scoops: 1 working, 1 idle". On hover, on keyboard focus of the row, or when <kbd>Tab</kbd> from the selected row reaches its drop or delete button, a strip the full height of the row's right edge holds that button over the text; the context fill stays on top of it, and it hides again once a pointer that clicked it leaves. A click, <kbd>Enter</kbd> or <kbd>Space</kbd> on a cone expands or collapses it, as do <kbd>→</kbd> and <kbd>←</kbd> (<kbd>→</kbd> on an expanded cone moves to its first scoop, <kbd>←</kbd> on a scoop to its cone); only the focused cone is expanded, and its scoops list below it. A cone with scoops carries `aria-expanded`, and `aria-controls` names its scoop rows while they show. Expanding and collapsing have no motion. **New cone** sits at the bottom. The conversation title is also shown in the header's cone picker: `Agent.title`, or else the first line of the first user message, or "No messages yet". When cones share a name, their accessible names and the Delete cone, Freezer and confirmation labels add the title, as in "Delete cone sliccy (Hello from Bedrock)". Selecting one makes it the active agent. |
140
- | `<slicc-chat>` | One thread's conversation (its `agent`, or the active agent if unset), by day. 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. 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. 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. 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. 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. The label sits at the end and is cut with an ellipsis before it reaches that space. 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. 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. 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. 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. 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, 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; 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; 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). 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. A lick's `severity` (`warn` or `error`) adds an alert icon to its byline, a coloured edge to its box and a spoken prefix. Permission requests are allowed or denied in place. The header is one row: the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false` (with no model on offer, **Set up a model**, `data-action="set-up-model"`, opens Settings), the thinking picker, the context meter and **New conversation**, which calls `clear` and puts focus back in the composer; on a cone, when the port can freeze, its tooltip says the old conversation stays in the Freezer. It never wraps: when it's short of room, the model picker's label is cut with an ellipsis, and under 560px wide (`header[data-compact]`) New conversation is icon-only, with `accessible-label` and its tooltip. The pickers re-read `models()` on `settings`, `accounts` and `agents`. When a live agent's model, or `Settings.model`, is no longer offered and its provider (the id before `/`) isn't a connected account, it moves to the offered chat model with `default: true`, else the first one; frozen agents are left alone, and so is everything on a follower unless it has Control and is connected. The chat re-runs this on `follow` and `readOnly` too, and `<slicc-app>` does it as well, when it binds, on those events and on `follow` and `readOnly`, so a new cone that starts on a provider that isn't set up moves at once. The transcript pads its end, and its `scroll-padding-block-end`, by the composer's measured height, so at the bottom the last message sits above the composer and a focused element scrolls clear of it. While a reply streams, its status icon follows what it is doing: a pencil for an edit, a page for a read, a magnifier for a search, an eye for the browser and an hourglass for any other tool; thinking and text keep the AI logo. Between the header and the transcript, a `<slicc-notices>` shows the model's notices. On a follower without Control, or while the agent port's `readOnly()` returns a line, a note (`p[data-follow="watch"]`, that line first, else the follow-based one) takes the composer's place, the header shows the model's name instead of the pickers and New conversation, and there's no suggested follow-up. |
140
+ | `<slicc-chat>` | One thread's conversation (its `agent`, or the active agent if unset), by day. 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. 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. 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. 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. 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. The label sits at the end and is cut with an ellipsis before it reaches that space. 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. 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. 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. 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. 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, 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; 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). 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. A lick's `severity` (`warn` or `error`) adds an alert icon to its byline, a coloured edge to its box and a spoken prefix. 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. 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. While pending, cards stick to the top of the log and pile, each a step lower than the one before; a card behind the front one (`data-pinned`, `--depth`) is a little smaller and softer for each card in front of it, and the log's `scroll-padding` keeps the pile from covering whatever has focus. Clicking a piled card (not its controls), or <kbd>Enter</kbd> on it, scrolls it back to its place at the bottom of the chat, and focusing anything inside a covered card does the same. When a card leaves the pile (answered, timed out or decided elsewhere) while it has focus, focus moves to the next pending card, else the composer. Under `prefers-reduced-motion`, nothing animates: no transitions, and the scroll is instant. The header is one row: the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false` (with no model on offer, **Set up a model**, `data-action="set-up-model"`, opens Settings), the thinking picker, the context meter and **New conversation**, which calls `clear` and puts focus back in the composer; on a cone, when the port can freeze, its tooltip says the old conversation stays in the Freezer. It never wraps: when it's short of room, the model picker's label is cut with an ellipsis, and under 560px wide (`header[data-compact]`) New conversation is icon-only, with `accessible-label` and its tooltip. The pickers re-read `models()` on `settings`, `accounts` and `agents`. When a live agent's model, or `Settings.model`, is no longer offered and its provider (the id before `/`) isn't a connected account, it moves to the offered chat model with `default: true`, else the first one; frozen agents are left alone, and so is everything on a follower unless it has Control and is connected. The chat re-runs this on `follow` and `readOnly` too, and `<slicc-app>` does it as well, when it binds, on those events and on `follow` and `readOnly`, so a new cone that starts on a provider that isn't set up moves at once. The transcript pads its end, and its `scroll-padding-block-end`, by the composer's measured height, so at the bottom the last message sits above the composer and a focused element scrolls clear of it. While a reply streams, its status icon follows what it is doing: a pencil for an edit, a page for a read, a magnifier for a search, an eye for the browser and an hourglass for any other tool; thinking and text keep the AI logo. Between the header and the transcript, a `<slicc-notices>` shows the model's notices. On a follower without Control, or while the agent port's `readOnly()` returns a line, a note (`p[data-follow="watch"]`, that line first, else the follow-based one) takes the composer's place, the header shows the model's name instead of the pickers and New conversation, and there's no suggested follow-up. An empty chat says "No messages yet. Ask sliccy something." with the agent's name, and only "No messages yet." while watch-only. |
141
141
  | `<slicc-notices>` | The model's `notices` as in-line alerts with their actions and a close button (none with `dismissible: false`), after a banner from the `tabs` port when another tab is paused or runs a different SLICC, and a warning for each waiting approval when the `approvals` port has no `decide` (`[data-approval-notice]`); renders nothing without any. |
142
142
  | `<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`. 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`)**. Focus starts on Deny 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. |
143
143
  | `<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. |
144
144
  | `<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 (with a `vault.put`, the popup takes the value too, checks it like Settings › Secrets, puts it in the vault and attaches the name, showing errors inline and clearing the field; an empty value shares a name the vault already has, and a new value for one asks before it replaces it; while the vault is away (`availability()` `connecting` or `waiting`) or locked, it takes no value and says so; without one, it attaches the name and says to add the value in Settings › Secrets), or paste and drop files (25 MB each). Queued messages wait above it, to send now or drop; a steer the agent still holds (`mode: 'steer'`, `li.queued[data-mode=steer]`) reads "Sends at the next step" and can only be dropped. Model and thinking pickers. Where the browser has speech recognition, the `+` menu ends with **Enable push to talk** (`settings.pushToTalk`, optional and off by default), a checkable item (`menuitemcheckbox`, `aria-checked`) described as "Hold Alt+M to dictate", which stays open when toggled; Settings has the same switch. While it is on, holding <kbd>Alt</kbd>+<kbd>M</kbd> in the field (by `code`, so <kbd>Option</kbd>+<kbd>M</kbd> on a Mac types no µ; not with Ctrl, ⌘ or Shift, nor during IME composition) dictates into it, the field's description ends with "hold Alt+M to dictate", and releasing either key, or leaving the window, stops it. Without speech recognition there is no item and no switch, and Alt+M types as usual. It floats over the end of the transcript on a frosted layer that fades out above its top edge; under `prefers-reduced-transparency` the layer is solid, and under `forced-colors` it is `Canvas` with a top border. There's no row of key hints: the keys are the field's accessible description ("Enter sends, Shift+Enter adds a new line, / lists commands, @ mentions.", and while a reply runs "Enter steers, Ctrl+Enter queues, Escape stops, …"; with send on Enter off, Ctrl+Enter sends and Enter adds a line). The field's status icon, which is decorative, follows the input: the AI logo when empty, a pencil while typing, a brush for a moment after <kbd>Backspace</kbd> or <kbd>Delete</kbd>, a microphone while dictating; it stays the AI logo while a reply streams, and it swaps without animation. Drafts are kept per agent. On a follower while the link reconnects, the field is disabled and says so; the draft stays. While the agent holds tool results for the vault (`agent.hold()`), a status line above it says "Waiting for the vault" or "Unlock the vault to continue", with how many results are held, and offers the vault's **Unlock** button when locked and the model has a `vault`; if the browser blocks the vault window, the line says to allow pop-ups. |
145
145
  | `<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. Above the tree, **Search files** (the shared `sp-search[data-action=search]` from `search.ts`, as in Memory) filters the tree to the matching paths while it holds text, or hides it and says “No files match.” (`.note[data-empty]`) when none match; a letter or digit typed in the tree moves into it, <kbd>↓</kbd> goes back to the tree, and <kbd>Esc</kbd> in the tree, or opening a file from it, clears the search. File icons are monochrome, so colour in the tree only means git status. |
146
146
  | `<slicc-file-view>` | One file (`path`), highlighted, with its pending change and a link to the diff. |
147
- | `<slicc-changes>` | Pending changes with their author. Opens the diff, accepts or reverts one change or all of them (**Accept all** and **Revert all** are `data-action="accept-all"` and `data-action="revert-all"`). While watch-only (`watchOnly()`: a follower without Control, or the agent port's `readOnly()`), the list and the diffs stay but no Accept or Revert shows, and a revert confirmed after that is dropped. 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"`). |
148
- | `<slicc-diff-panel>` | The diff of one pending change (`path`), unified or split, with accept and revert, except while watch-only. 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. |
147
+ | `<slicc-changes>` | Pending changes with their author. Opens the diff, accepts or reverts one change or all of them (**Accept all** and **Revert all** are `data-action="accept-all"` and `data-action="revert-all"`). While watch-only (`watchOnly()`: a follower without Control, or the agent port's `readOnly()`), the list and the diffs stay but no Accept or Revert shows: a subdued "Watch only" (`[data-watch-note]`, plain text, not a live region) stands where Accept all and Revert all were, and a revert confirmed after that is dropped. With nothing pending it says "No pending changes. Edits by agents show up here for review.", and only "No pending changes." while watch-only. 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"`). |
148
+ | `<slicc-diff-panel>` | The diff of one pending change (`path`), unified or split, with accept and revert, except while watch-only, when a subdued "Watch only" (`[data-watch-note]`) stands in their place and the path keeps its ellipsis. 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. |
149
149
  | `<slicc-terminals>` | Terminal sessions in tabs, each a `<slicc-terminal>` on the session's `TerminalBackend`. Sessions stay alive while hidden; `exit` closes the tab. |
150
150
  | `<slicc-browser>` | What the agents' browser control is doing. It opens on the most recently driven tab (`.focused`), or on the tab overview: cards (`li[data-id]`, `data-controlled` while an agent drives the tab, `data-pulse` when another tab starts being driven, without switching to it) with a thumbnail, the agent and a small **Stop <agent>** (`data-action="stop-agent"`), and an address bar to open a URL. The focused tab has **All tabs** (`data-action="all-tabs"`), a tab picker (a list of tabs at 720 px and wider), the URL with **Copy** (`data-action="copy-url"`) and **Open** (`data-action="open-tab"`, activates the tab), who's using it (`.driving[data-driving=agent|user|none]`; a controlled tab without an agent reads **In use** and has no Stop) with a small **Stop <agent>** (`data-action="stop-agent"`) in the same row, which keeps its height with or without an agent, a view-only preview (`figure[data-view]`: `live` while watching with a frame under 3 s old, `shot` with "Updated N s ago", `none`) and the recent actions (`li[data-action-id][data-kind][data-status]`, failures with their error inline), the tab's own filtered first, then the latest 50, with **Show more** (`data-action="more-actions"`) adding 50 earlier ones each time up to all the port has ("Showing the latest N of M"); the list scrolls and takes focus. Actions with a URL (`open`, `goto`, `request`) expand (`data-action="action-url"`, `aria-expanded`) to show the full URL, or the request line, under the shortened one. Below 480 px they fold into **Recent actions (n)**, a chevron disclosure counting all of them, with the last one showing. Labels, URLs and errors are text only. With no tabs and `browser.via` `null` in the network status, it says browser control isn't connected (`data-browser="none"`) with the extension link and the commands to install and run slicc (`curl -fsSL https://get.sliccy.ai/slicc | sh`, then `slicc`, or `npx sliccy@next` with Node). The Browser rail button carries a dot (`data-driven`) while a tab is in use. |
151
151
  | `<slicc-settings>` | 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. 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`. An account with `fields` (`{ name, label, required? }`) shows a plain labelled input for each, and their values go to `connect(id, secret?, fields)`. An account with `store: 'vault'` keeps its key in the vault when the `vault` port has `put`: the masked key field (C5's value rules) goes to `vault.put(name, key, undefined, { kind: 'model', provider: account.id })` (or `{ kind: 'mcp', server }` for `mcp:<server>` accounts, named `MCP_<SERVER>_TOKEN`; model records use pi-ai's environment-variable name), then `connect(id)` runs without the secret; a connected one says “Key kept in the vault”, “Token kept in the vault” or “Kept in the vault”, and Disconnect removes the record first. Without `store: 'vault'` or `vault.put`, the key goes to `connect(id, secret)` as before. With a `vault` port, **Secrets** (`<slicc-secrets>`) follows. When the model for new cones was moved off a provider that isn't set up, a quiet note under its picker (`p[data-switched][role=status]`) says “Switched to {model} because the previous model's provider isn't set up.” until a model is picked.
@@ -824,10 +824,10 @@ var DummyAgent = class extends Emitter {
824
824
  this.#messages.get(agentId)?.push(message);
825
825
  this.emit("message", { agentId, message });
826
826
  }
827
- #status(agentId, status) {
827
+ #status(agentId, status2) {
828
828
  const agent = this.#agent(agentId);
829
- if (!agent || agent.status === status) return;
830
- agent.status = status;
829
+ if (!agent || agent.status === status2) return;
830
+ agent.status = status2;
831
831
  this.#changed();
832
832
  }
833
833
  #pace(agentId, steps) {
@@ -2241,6 +2241,95 @@ var welcome_default = `<!DOCTYPE html>
2241
2241
  </html>
2242
2242
  `;
2243
2243
 
2244
+ // src/dummy/status.ts
2245
+ var status = `<!DOCTYPE html>
2246
+ <html lang="en">
2247
+ <head>
2248
+ <meta charset="utf-8" />
2249
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2250
+ <title>Status</title>
2251
+ <style>
2252
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2253
+ body {
2254
+ font-family: var(--s2-font-family, 'Adobe Clean', -apple-system, system-ui, sans-serif);
2255
+ background: transparent;
2256
+ color: var(--s2-content-default, #e8e8e8);
2257
+ font-size: 14px;
2258
+ }
2259
+ .card {
2260
+ display: grid;
2261
+ gap: 12px;
2262
+ padding: 8px 20px 12px;
2263
+ }
2264
+ .row { display: flex; align-items: center; gap: 10px; }
2265
+ .dot {
2266
+ flex: none;
2267
+ width: 8px;
2268
+ height: 8px;
2269
+ border-radius: 50%;
2270
+ background: var(--dot);
2271
+ }
2272
+ .name { flex: 1; font-weight: 600; }
2273
+ .meta { font-size: 12px; color: var(--s2-content-subdued, #9a9a9a); }
2274
+ .steps { list-style: none; display: grid; gap: 6px; }
2275
+ .steps label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
2276
+ .actions { display: flex; justify-content: flex-end; gap: 8px; }
2277
+ button {
2278
+ font: inherit;
2279
+ padding: 6px 12px;
2280
+ border-radius: 999px;
2281
+ border: 1px solid var(--s2-gray-400, rgba(128, 128, 128, 0.5));
2282
+ background: transparent;
2283
+ color: inherit;
2284
+ cursor: pointer;
2285
+ }
2286
+ button.primary {
2287
+ background: var(--s2-accent, #3b63fb);
2288
+ border-color: transparent;
2289
+ color: #fff;
2290
+ }
2291
+ </style>
2292
+ </head>
2293
+ <body>
2294
+ <div class="card">
2295
+ <div class="row" style="--dot: #2d9d78">
2296
+ <span class="dot"></span>
2297
+ <span class="name">PR #58 \xB7 Add retries for upstream timeouts</span>
2298
+ <span class="meta">4 of 4 checks</span>
2299
+ </div>
2300
+ <div class="row" style="--dot: #e68619">
2301
+ <span class="dot"></span>
2302
+ <span class="name">Visual regression suite</span>
2303
+ <span class="meta">2 snapshots changed</span>
2304
+ </div>
2305
+ <div class="row" style="--dot: #2d9d78">
2306
+ <span class="dot"></span>
2307
+ <span class="name">Staging</span>
2308
+ <span class="meta">/health answered in 2 ms</span>
2309
+ </div>
2310
+ <ul class="steps">
2311
+ <li><label><input type="checkbox" checked /> TTL fix merged</label></li>
2312
+ <li><label><input type="checkbox" /> Merge the retry pull request</label></li>
2313
+ <li><label><input type="checkbox" /> Update the forecast docs to Celsius</label></li>
2314
+ <li><label><input type="checkbox" /> Tag 1.9.0</label></li>
2315
+ </ul>
2316
+ <div class="actions">
2317
+ <button type="button" data-action="review">Review snapshots</button>
2318
+ <button type="button" class="primary" data-action="continue">Continue</button>
2319
+ </div>
2320
+ </div>
2321
+ <script>
2322
+ document.querySelector('.actions').addEventListener('click', function (event) {
2323
+ var action = event.target.getAttribute('data-action');
2324
+ if (!action || !window.slicc) return;
2325
+ var done = document.querySelectorAll('.steps input:checked').length;
2326
+ slicc.lick({ action: action, data: { done: done } });
2327
+ });
2328
+ <\/script>
2329
+ </body>
2330
+ </html>
2331
+ `;
2332
+
2244
2333
  // src/dummy/extras.ts
2245
2334
  var day = 864e5;
2246
2335
  var now = Date.UTC(2026, 9, 5, 9, 0);
@@ -2444,6 +2533,15 @@ var sprinkles = [
2444
2533
  icon: "ice-cream-cone",
2445
2534
  agentId: "cone-sliccy",
2446
2535
  html: suggestions_default
2536
+ },
2537
+ {
2538
+ id: "status",
2539
+ name: "status",
2540
+ title: "Status",
2541
+ icon: "list-checks",
2542
+ agentId: "cone-kitchen",
2543
+ html: status,
2544
+ inline: true
2447
2545
  }
2448
2546
  ];
2449
2547
  var coneSprinkle = (agentId, name2, title, icon, body) => ({
@@ -2551,8 +2649,8 @@ var DummyFiles = class extends Emitter {
2551
2649
  this.#changes.delete(path);
2552
2650
  return;
2553
2651
  }
2554
- const status = before === null ? "added" : after === null ? "deleted" : "modified";
2555
- this.#changes.set(path, { path, status, before, after, agentId });
2652
+ const status2 = before === null ? "added" : after === null ? "deleted" : "modified";
2653
+ this.#changes.set(path, { path, status: status2, before, after, agentId });
2556
2654
  }
2557
2655
  #announce(path) {
2558
2656
  this.emit("files", this.#entries());
@@ -2917,7 +3015,7 @@ var licks = [
2917
3015
  "docs-pass",
2918
3016
  "Wants to push to example/harbor (branch docs/celsius)",
2919
3017
  "git push origin docs/celsius",
2920
- { state: "pending" }
3018
+ { state: "pending", expiresAt: Date.now() + 10 * 6e4 }
2921
3019
  ],
2922
3020
  [
2923
3021
  "sudo-request",
@@ -3197,70 +3295,7 @@ function work() {
3197
3295
  model: "claude-sonnet-5-5",
3198
3296
  parts: [
3199
3297
  { type: "text", text: "Here is where things stand." },
3200
- {
3201
- type: "card",
3202
- card: {
3203
- variant: "pr",
3204
- number: 58,
3205
- title: "Add retries for upstream timeouts",
3206
- branch: "fix/retry-upstream",
3207
- checks: "4 of 4 checks passed",
3208
- additions: 42,
3209
- deletions: 7,
3210
- files: 3,
3211
- tone: "positive"
3212
- }
3213
- },
3214
- {
3215
- type: "card",
3216
- card: {
3217
- variant: "tool",
3218
- title: "Visual regression suite",
3219
- badge: "bash",
3220
- status: "2 snapshots changed",
3221
- files: ["forecast-card.png", "forecast-card-dark.png"],
3222
- tone: "notice"
3223
- }
3224
- },
3225
- {
3226
- type: "card",
3227
- card: {
3228
- variant: "light",
3229
- title: "Staging is healthy",
3230
- detail: "/health answered in 2 ms",
3231
- tone: "informative"
3232
- }
3233
- },
3234
- {
3235
- type: "plan",
3236
- items: [
3237
- "Merge the retry pull request",
3238
- "Move the cache to KV",
3239
- "Update the forecast docs to Celsius",
3240
- "Tag 1.9.0"
3241
- ]
3242
- },
3243
- {
3244
- type: "check",
3245
- items: [
3246
- { text: "TTL fix merged" },
3247
- { text: "Retries reviewed", tone: "informative" },
3248
- { text: "Docs pass running", tone: "notice" },
3249
- { text: "KV migration not started", tone: "negative" }
3250
- ]
3251
- },
3252
- {
3253
- type: "diff",
3254
- path: "/workspace/harbor/src/lib/units.ts",
3255
- before: "export function toCelsius(f: number): number {\n return ((f - 32) * 5) / 9;\n}\n",
3256
- after: "export function toCelsius(f: number): number {\n return Math.round(((f - 32) * 5) / 9);\n}\n"
3257
- },
3258
- {
3259
- type: "link",
3260
- url: "https://api.example.com/docs/v2/forecast",
3261
- title: "Forecast API reference",
3262
- description: "Parameters, units and rate limits for /v2/forecast."
3263
- },
3298
+ { type: "sprinkle", sprinkle: "status" },
3264
3299
  {
3265
3300
  type: "delegation",
3266
3301
  kind: "scoop",
@@ -4373,7 +4408,7 @@ var DummyMonitor = class extends Emitter {
4373
4408
  const listed = agent.list();
4374
4409
  const agents2 = listed.filter((candidate) => candidate.kind === "cone").flatMap((cone) => [cone, ...listed.filter((candidate) => candidate.parentId === cone.id)]);
4375
4410
  const active = agents2.filter((candidate) => candidate.status !== "idle").length;
4376
- const status = tray2.status();
4411
+ const status2 = tray2.status();
4377
4412
  const fill = Math.max(...agents2.map((candidate) => candidate.contextFill), 0);
4378
4413
  const rows = (id) => this.#extra.find((section) => section.id === id)?.rows;
4379
4414
  const tasks = rows("tasks");
@@ -4388,20 +4423,20 @@ var DummyMonitor = class extends Emitter {
4388
4423
  return {
4389
4424
  updatedAt: Date.now(),
4390
4425
  vitals: [
4391
- ...status.budget ? [
4426
+ ...status2.budget ? [
4392
4427
  {
4393
4428
  id: "budget",
4394
- label: `Budget (${status.budget.window})`,
4395
- value: `${status.budget.percent}%`,
4396
- unit: status.budget.resets,
4397
- ratio: status.budget.percent / 100
4429
+ label: `Budget (${status2.budget.window})`,
4430
+ value: `${status2.budget.percent}%`,
4431
+ unit: status2.budget.resets,
4432
+ ratio: status2.budget.percent / 100
4398
4433
  }
4399
4434
  ] : [],
4400
4435
  {
4401
4436
  id: "spend",
4402
4437
  label: "Spend",
4403
- value: `$${(status.spent ?? 0).toFixed(2)}`,
4404
- delta: `$${(status.rate ?? 0).toFixed(2)}/h`,
4438
+ value: `$${(status2.spent ?? 0).toFixed(2)}`,
4439
+ delta: `$${(status2.rate ?? 0).toFixed(2)}/h`,
4405
4440
  series: wave(this.#tick + 7)
4406
4441
  },
4407
4442
  {
@@ -4457,10 +4492,10 @@ var DummyMonitor = class extends Emitter {
4457
4492
  detail: "Compact it soon to keep replies fast.",
4458
4493
  severity: "warn"
4459
4494
  })),
4460
- ...status.connection === "live" ? [] : [
4495
+ ...status2.connection === "live" ? [] : [
4461
4496
  {
4462
4497
  id: "tray",
4463
- title: `Tray ${status.connection}`,
4498
+ title: `Tray ${status2.connection}`,
4464
4499
  detail: "Followers can\u2019t see this session.",
4465
4500
  severity: "error"
4466
4501
  }
@@ -4499,7 +4534,7 @@ var DummyMonitor = class extends Emitter {
4499
4534
  {
4500
4535
  id: "followers",
4501
4536
  label: "Tray followers",
4502
- rows: status.followers.map((follower) => ({
4537
+ rows: status2.followers.map((follower) => ({
4503
4538
  name: follower.name,
4504
4539
  meta: follower.device,
4505
4540
  status: "active"
@@ -4864,11 +4899,11 @@ var DummyNetwork = class extends Emitter {
4864
4899
  this.#base = networkFixtures(scenario);
4865
4900
  this.#status = this.#with(this.#base);
4866
4901
  }
4867
- #withLeaders(status) {
4902
+ #withLeaders(status2) {
4868
4903
  const leaders = this.#leaders;
4869
- if (!leaders) return status;
4904
+ if (!leaders) return status2;
4870
4905
  this.#leaders = leaders.filter((leader) => leader.state !== "unlinked");
4871
- return { ...status, leaders };
4906
+ return { ...status2, leaders };
4872
4907
  }
4873
4908
  #leader(id, change) {
4874
4909
  this.#leaders = this.#leaders.map((leader) => {
@@ -4892,15 +4927,15 @@ var DummyNetwork = class extends Emitter {
4892
4927
  this.#leaders = this.#leaders.filter((leader) => leader.id !== id);
4893
4928
  this.#emit();
4894
4929
  }
4895
- #with(status) {
4896
- return this.#withLeaders(this.#withLinks(status));
4930
+ #with(status2) {
4931
+ return this.#withLeaders(this.#withLinks(status2));
4897
4932
  }
4898
- #withLinks(status) {
4933
+ #withLinks(status2) {
4899
4934
  const links = this.#links;
4900
- if (!links) return this.#withTailnet(status);
4935
+ if (!links) return this.#withTailnet(status2);
4901
4936
  const exit = this.#exit;
4902
- const direct = status.route === "link" && exit?.kind !== "link";
4903
- const base = direct ? { ...status, route: "page", detail: null, browser: { via: null } } : status;
4937
+ const direct = status2.route === "link" && exit?.kind !== "link";
4938
+ const base = direct ? { ...status2, route: "page", detail: null, browser: { via: null } } : status2;
4904
4939
  const linked = { ...base, links, exit };
4905
4940
  const device = exit?.kind === "link" ? links.devices.find((item) => item.id === exit.id) : void 0;
4906
4941
  const browsing = links.devices.some(
@@ -4912,18 +4947,18 @@ var DummyNetwork = class extends Emitter {
4912
4947
  if (!device) return routed;
4913
4948
  return { ...routed, health: device.state === "connected" ? "ok" : "failing", detail: null };
4914
4949
  }
4915
- #withTailnet(status) {
4950
+ #withTailnet(status2) {
4916
4951
  const tailnet = this.#tailnet;
4917
- if (!tailnet) return status;
4952
+ if (!tailnet) return status2;
4918
4953
  const exit = tailnet.state === "running" ? tailnet.exitNode : null;
4919
- if (!exit) return { ...status, tailnet };
4954
+ if (!exit) return { ...status2, tailnet };
4920
4955
  return {
4921
- ...status,
4956
+ ...status2,
4922
4957
  route: "tailnet",
4923
4958
  health: "ok",
4924
4959
  detail: `${exit} carries everything except this computer\u2019s own services.`,
4925
4960
  tailnet,
4926
- exit: this.#links ? status.exit : null
4961
+ exit: this.#links ? status2.exit : null
4927
4962
  };
4928
4963
  }
4929
4964
  #set(tailnet) {
@@ -5495,10 +5530,10 @@ var DummyTray = class extends Emitter {
5495
5530
  #status;
5496
5531
  #followers;
5497
5532
  #clock;
5498
- constructor(status, clock2) {
5533
+ constructor(status2, clock2) {
5499
5534
  super();
5500
- this.#status = structuredClone(status);
5501
- this.#followers = structuredClone(status.followers);
5535
+ this.#status = structuredClone(status2);
5536
+ this.#followers = structuredClone(status2.followers);
5502
5537
  this.#clock = clock2;
5503
5538
  }
5504
5539
  status() {
@@ -6217,7 +6252,7 @@ function vaultFixtures(scenario, now2 = Date.now()) {
6217
6252
  const empty = scenario === "empty";
6218
6253
  const policy = scenario === "policy" || scenario === "notice";
6219
6254
  const backup = scenario === "backup";
6220
- const status = {
6255
+ const status2 = {
6221
6256
  version: "0.1.0",
6222
6257
  protocol: 1,
6223
6258
  origin: "https://vault.sliccy.ai",
@@ -6242,7 +6277,7 @@ function vaultFixtures(scenario, now2 = Date.now()) {
6242
6277
  restricted: secrets.flatMap(({ name: name2, exits }) => exits ? [{ name: name2, exits }] : [])
6243
6278
  }
6244
6279
  };
6245
- return { status, list };
6280
+ return { status: status2, list };
6246
6281
  }
6247
6282
  var labels2 = {
6248
6283
  unlock: "Unlock the vault",
@@ -6328,8 +6363,8 @@ var DummyVault = class extends Emitter {
6328
6363
  acknowledged = [];
6329
6364
  constructor(scenario, time, records = []) {
6330
6365
  super();
6331
- const { status, list } = vaultFixtures(scenario);
6332
- this.#status = status;
6366
+ const { status: status2, list } = vaultFixtures(scenario);
6367
+ this.#status = status2;
6333
6368
  this.#list = { ...list, secrets: [...list.secrets, ...records] };
6334
6369
  this.#time = time;
6335
6370
  this.#available = scenario === "waiting" ? "waiting" : "ready";
@@ -6656,8 +6691,8 @@ function createDummyModel({
6656
6691
  vault: new DummyVault(vault, time, accounts2 === "vault" ? accountRecords() : [])
6657
6692
  }
6658
6693
  };
6659
- base.vault?.on("status", (status) => {
6660
- if (!status.locked && agent.hold()?.reason === "locked") agent.setHold(null);
6694
+ base.vault?.on("status", (status2) => {
6695
+ if (!status2.locked && agent.hold()?.reason === "locked") agent.setHold(null);
6661
6696
  });
6662
6697
  return {
6663
6698
  ...base,