@ai-ecoverse/slicc-spectrum 1.69.0 → 1.69.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,7 +137,7 @@ 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>` | Cones with their scoops, status, context fill and unread count. A cone's row has a second line with its conversation title, as does its item 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: user messages (with attachments and a quiet timestamp, `time.when`; 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), assistant messages (with the time, and the model only on the first reply and whenever it changes from the previous one, `.meta .model` with the usage as its tooltip; the cone's name isn't repeated; Markdown with tables, task lists, quotes, code and images; thinking; tool calls, grouped into a collapsible cluster from three in a row, each with its input (a shell command after `$`), an optional `meta` such as a timeout, an edit's `diff` shown as a diff view, and an `image`, such as a screenshot, shown as a thumbnail under the closed card and in full when open, its output wrapped rather than cut off, 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; 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), standalone tool results, system messages (compaction markers, error cards, notices) and licks, SLICC's events into a cone, on every channel; a lick's `severity` (`warn` or `error`) turns its channel badge into a notice or negative one with an alert icon. Permission requests are allowed or denied in place. The header shows the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false`, 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. Between the header and the transcript, a `<slicc-notices>` shows the model's notices. On a follower without Control, a note (`p[data-follow="watch"]`) 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: user messages (with attachments and a quiet timestamp, `time.when`; 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), assistant messages (with the time, and the model only on the first reply and whenever it changes from the previous one, `.meta .model` with the usage as its tooltip; the cone's name isn't repeated; Markdown with tables, task lists, quotes, code and images; thinking; tool calls, grouped into a collapsible cluster from three in a row, each with its input (a shell command after `$`), an optional `meta` such as a timeout, an edit's `diff` shown as a diff view, and an `image`, such as a screenshot, shown as a thumbnail under the closed card and in full when open, its output wrapped rather than cut off, 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; 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), standalone tool results, system messages (compaction markers, error cards, notices) and licks, SLICC's events into a cone, on every channel; a lick's `severity` (`warn` or `error`) turns its channel badge into a notice or negative one with an alert icon. 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` too, and `<slicc-app>` does it as well, when it binds, on those events and on `follow`, so a new cone that starts on a provider that isn't set up moves at once. Between the header and the transcript, a `<slicc-notices>` shows the model's notices. On a follower without Control, a note (`p[data-follow="watch"]`) 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. |
141
141
  | `<slicc-notices>` | The model's `notices` as in-line alerts with their actions and a close button, 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**. 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. |
@@ -148,7 +148,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
148
148
  | `<slicc-diff-panel>` | The diff of one pending change (`path`), unified or split, with accept and revert. A change with `before` and `after` both `null` (a binary file, or text over 1 MB) shows “No diff for this file” and Open the file instead, and keeps Accept and Revert. |
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
- | `<slicc-settings>` | Theme, the model and thinking level for new cones, composer and diff preferences, and accounts to connect, reconnect or disconnect. An account whose status is `signing-in` (or a non-API-key account while its `connect` is pending) reads “Signing in…”; when the port has `cancel(id)`, its row offers Cancel (`data-action="cancel-sign-in"`), and the rejected `connect` that follows shows no error. Accounts with `group: 'mcp'` (MCP servers) get their own **MCP servers** heading (`.accounts[data-group=mcp]`). An `api-key` account asks for its `secret`: “{provider} API key” by default, “{provider} token” for `secret: 'token'`; when it is `expired` it reads **API key rejected** or **Token rejected**. An MCP server with `auth: 'oauth'` signs in in a pop-up: its button reads **Sign in**, and it reads **Needs sign-in** when `disconnected` and **Sign-in expired** when `expired`. 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.
151
+ | `<slicc-settings>` | Theme, the model and thinking level for new cones, composer and diff preferences, and accounts to connect, reconnect or disconnect. An account whose status is `signing-in` (or a non-API-key account while its `connect` is pending) reads “Signing in…”; when the port has `cancel(id)`, its row offers Cancel (`data-action="cancel-sign-in"`), and the rejected `connect` that follows shows no error. Accounts with `group: 'mcp'` (MCP servers) get their own **MCP servers** heading (`.accounts[data-group=mcp]`). An `api-key` account asks for its `secret`: “{provider} API key” by default, “{provider} token” for `secret: 'token'`; when it is `expired` it reads **API key rejected** or **Token rejected**. An MCP server with `auth: 'oauth'` signs in in a pop-up: its button reads **Sign in**, and it reads **Needs sign-in** when `disconnected` and **Sign-in expired** when `expired`. 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.
152
152
  | `<slicc-secrets>` | Settings › Secrets from the model's `vault`. It never shows a value or a placeholder. Value fields are `type=password` with no reveal toggle, are read once on submit and cleared at once (on success, refusal, error, cancel, close and disconnect), and are checked before the vault is called: under 8 UTF-8 bytes, CR, LF or NUL, or containing `fnox_proxy_` are refused with the vault's own wording. Values go only to `vault.put` and are never kept on the element or put in an event. The head shows **Locked** or **Unlocked** and the lock mode; locked, the vault's **Unlock** button (`slicc-vault-button[data-action=unlock]`); unlocked, **Lock** (`data-action="lock"`, with `lock`) and the vault's **Protect with a passkey** (lock `auto`) or **Change protection** button (`data-action="protect"`). A status line (`[data-availability]`) says "Waiting for the vault" while `availability()` is `connecting` or `waiting` (or `status()` is `null` without it), and "Unlock the vault to continue" while `scrub.needed` and not `scrub.ready`. Waiting requests show under **Waiting for you** (`[data-request]`, the agent's `prompt` as plain text under "From the agent, not checked by SLICC:", capped at 200 characters, and "Expires in N min"), with **Enter value** (`data-action="answer"`, which fires `value-request` with `{ id, vault }` for the `<slicc-value-request>` on the same vault) for `value` requests and **Cancel request** (`data-action="cancel-request"`); `oauth`, `jerk-seal`, `jerk` and `unseal` requests say what they ask for and offer Cancel request only. With `backupReminder: 'due'`, an alert (`[data-backup]`) offers **Export backup** (`data-action="export"`) and **Not now** (`data-action="dismiss-backup"`, with `dismissBackupReminder`). **Add secret** (`data-action="add-secret"`, `[data-form=add]`, a name and a value; a name that exists asks before it replaces the value), **Import…** (`data-action="import"`, `[data-form=import]`: Bundle, KeePass or six, a file, an optional password, then a pick of candidates, all checked but the `refused` ones (disabled, with their reason), marked "Replaces a secret" when they exist, with their suggested rules as untrusted text and a note that the file's rules aren't applied, and **Import N**, which sends `{ name, kind }` for a candidate of another `kind`; then "Imported N" and "Skipped NAME: reason" for each skipped name, from `reasons`) and **Export…** (`data-action="export"`, `[data-form=export]`: a passphrase twice, at least 8 bytes, behind a confirmation, downloaded as `slicc-secrets-<yyyy-mm-dd>.slicc-vault`, with "Last exported" or "Never exported" from `exportedAt`). Saved shows "Saved NAME in the vault" (`[data-said]`). The secrets, sorted by name (`li[data-secret][data-state]`, with `[data-rules]`, `[data-exits]` only when restricted, `[data-exec]` (any command, the listed commands, every bash) and how often programs were given it today, `[data-source]`, `[data-used]`, `[data-expires]`; a model record or an MCP token no rule names reads “Used for” (`[data-used-for]`, model requests to its provider or requests to its MCP server, by the agent) instead of where it's sent, with its account's state as its light, and shows its Provider (`[data-provider]`, the account's name) or Server (`[data-server]`)) offer **Replace value** (`data-action="replace"`, for `settings`, `process` and `import:*` secrets that aren't sign-ins) and **Remove** (`data-action="remove"`, behind a confirmation, passing the secret's `kind`), and removed ones, under **Removed**, **Restore** (`data-action="restore"`; ENOENT reads "Removed too long ago to restore"). Locked, value forms say "Unlock the vault first" and disable Save. Vault errors show their message, EACCES as "Unlock the vault first" and ENOTSUP as "This arrives with a later vault version". Each action shows only when the port has its method. Then **Rules SLICC refused** (`[data-refused]`), where the rules live (`[data-policy]`) and **Activity** (`ol.audit li[data-seq][data-type]`; an entry with `uid` and `tag` both `null` reads "by you", a `refresh` entry "Refreshed automatically" (with the sign-in it used), an `mcp` entry "Sent to the MCP server SERVER at DOMAIN" with its request and exit, and a policy entry lists its changes, its warning and, when accepted, how to change the file back; **Only reveals to programs**, `data-action="reveals"`, and **Show older**, `data-action="older"`). The section's `data-vault` is `locked`, `unlocked` or `loading`. A popup the browser blocked shows how to allow it (`[data-popup=blocked]`). Widened-rule notices aren't shown. |
153
153
  | `<slicc-vault-button>` | Places `vault.button()`'s element (the vault's own button frame), posts its `config` (`ButtonConfig` plus `theme`) once the button is `ready`, sets the frame's `color-scheme` to `theme` (or leaves it to the page) so the frame stays transparent, takes the reported `size` (with the focus-ring `inset` as negative margin) and `label` (as its title), moves focus to the control before or after it when the frame reports `focus` (`previous` or `next`), and fires `vault-popup` (bubbles, composed) with each `popup` event's `{ intent, state }`. spectrum sends only the `unlock` and `protect` intents. |
154
154
  | `<slicc-value-request>` | The vault's waiting `value` requests (`fnox set` on a terminal) as a modal dialog, one at a time, oldest first, with "N more waiting"; `<slicc-app>` mounts it. `dialog[data-request]`: "Enter a value for NAME" (a Name field when the request has no name), who asks, as on the Settings card ("docs-pass asks for NAME", or "A program running as uid N asks for NAME"), the prompt under "From the agent, not checked by SLICC:", a masked value field with the same rules as Settings › Secrets, "Expires in N min", and **Later** (`data-action="later"`, or Escape: closes, the request stays in Settings › Secrets, whose **Enter value** reopens it), **Cancel request** (`data-action="cancel-request"`, with `cancel`) and **Save** (`data-action="save"`, `put(name, value, request.id)`, with `put`). Locked, it shows the vault's Unlock button and disables Save. It closes, clearing the field, when the request leaves `waiting` or while the vault is away. |
@@ -241,7 +241,7 @@ With an `agentId` or a `cone`, the caller reaches the shared surfaces, the chats
241
241
  | `files` | slicc-kernel's OPFS | `list`, `read`, `write`, `remove`, and pending changes with their before and after text (`accept`, `revert`). The optional `mountFolder()` asks for a local folder and resolves with its mount point (for example `/mnt/photos`), or `null` when the user cancels; the panel calls it synchronously in the click, so the port can call `showDirectoryPicker()`. The optional `mounts()` lists the mount points, and the optional `mountInfo()` returns `MountInfo`s (`target`, optional `source` such as a linked device's `ref-node/proj`, and `type`) for them, used instead of `mounts()` when present; `eject(path)` unmounts one (the folder stays on disk). `mountFolder(path)` inserts the picked folder at a mount point that waits for one; the optional `needsFolder()` lists those mount points. The port emits `mounts` and then `files` after every mount and eject, including those made from the shell, and `mounts` whenever `needsFolder()` changes. |
242
242
  | `terminals` | slicc-kernel's terminal API | Terminal sessions; `backend(id)` returns a `TerminalBackend` for `<slicc-terminal>`. |
243
243
  | `browser` | `@ai-ecoverse/slicc-cdp` | Tabs, the active tab, `open`, `navigate`, `close`, `screenshot`. A `BrowserTab` may say it's `controlled` (an agent, or the user's terminal when `agentId` is `null`, is driving it now; `agentId` is the agent of its last action); `favicon` is ignored. The optional `actions(tabId?)` returns recent `BrowserAction`s, oldest first, all of them (bios keeps 500 per tab for 24 h, 5,000 in all; the panel shows 50 at a time) (`{ id, tabId, agentId, kind, target?, value?, length?, status: running \| done \| failed, error?, at }`, `kind` one of `open`, `goto`, `back`, `reload`, `close`, `select`, `snapshot`, `screenshot`, `eval`, `click`, `fill`, `type`, `press`, `scroll`, `request`; a `request` has `value` `'GET https://host/path'` and may have no tab), with an `action` event per new or settled action. The optional `watch(tabId, size?)` streams `frame` events (`{ tabId, src, at }`) for a controlled tab and returns a stop function; the panel watches only the focused tab while it's controlled and visible, with the frame's CSS size. **Stop** calls `agent.stop(agentId)`. |
244
- | `settings` | local storage | Theme, model, thinking level, composer and diff preferences, the models on offer, and accounts. |
244
+ | `settings` | local storage | Theme, model, thinking level, composer and diff preferences, the models on offer, and accounts. A model can say it is its provider's `default`. Emit `settings` or `accounts` when the models on offer change. |
245
245
  | `changes` (optional) | `@ai-ecoverse/slicc-agent` | Pending changes from git instead of the file system: `changes()`, `accept(path)` (stage), `revert(path)` (discard), and a `changes` event, as on `files`. When present, the UI reads changes only from this port, including the rail's count. A `Change` may carry `repo`, its repository root; put the active agent's repository first. The optional `unavailable()` returns why there can be no changes (no git, no repository), which the panel shows with commands in backticks made copyable, or `null`; emit `changes` when it flips. The optional `commands()` adds more copyable commands under that text, in order, without repeating its own. |
246
246
  | `memory` | `@ai-ecoverse/slicc-agent` | Memories with scope, section, tag and body; `save` and `remove`. `source: 'notes'` marks free text the panel shows as an entry titled after its section; saving it makes it an entry. The optional `scopes()` lists the scope picker's choices as `{ id, label, group }`, ungrouped first, then under “Cones” and “Roles”; without it the picker offers “Everyone” (`global`) and each cone. The panel re-reads `scopes()` on `memories` and `agents`, so emit `memories` when the list changes. A scope that disappears falls back to `global`. |
247
247
  | `monitor` | all of them | A snapshot of vitals, alerts and sections, re-emitted as the system changes, and `resync`. A `MonitorRow` has `name`, `meta`, `status` (`active`, `idle`, `warn`, `error`), optional `badges`, an optional `depth` (the indent level, for trees such as the task graph) and an optional stable `id` (the key while rows update). |
@@ -259,6 +259,8 @@ The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current'
259
259
 
260
260
  `createDummyModel({ follow: 'connecting' | 'waiting' | 'watch' | 'control' | 'full' | 'reconnecting' | 'unlinked' | 'failed' })` (`?follow=` on `/ui/`, which then leaves out the network port and offers only agents, chat, terminal and settings) makes the page a follower of SLICC on quay-studio: connecting, waiting to be allowed, View only, View and Control, everything, reconnecting, unlinked, or failed, where Try again connects with everything allowed. Leave counts in `model.follow.left` (the `/ui/` page then drops `?follow=`), and `model.follow.setScenario()` switches it. `followFixtures` and `DummyFollow` are exported from `/dummy`. `?links=followers` lists quay-studio (a CLI on this computer, Control always allowed), pier-nuc (a remote CLI, prompting), bay-laptop (a follower with everything allowed, watching, a terminal open), reef-phone (View only, watching) and tide-tablet (asking to follow); every links scenario has a follow link.
261
261
 
262
+ `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`).
263
+
262
264
  `createDummyModel({ monitor: 'agent' })` (`?monitor=agent` on `/ui/`) adds the agent's Tasks (a tree by `depth`), Processes and Recent problems sections to the monitor.
263
265
 
264
266
  `createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' })` (`?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`. Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
@@ -20,8 +20,10 @@ var DummySettings = class extends Emitter {
20
20
  #clock;
21
21
  #keys;
22
22
  #signIns = /* @__PURE__ */ new Map();
23
- constructor(defaults, models, accounts, storage, clock2, keys = [dummySettingsKey]) {
23
+ #gated;
24
+ constructor(defaults, models, accounts, storage, clock2, keys = [dummySettingsKey], gated = false) {
24
25
  super();
26
+ this.#gated = gated;
25
27
  this.#keys = keys;
26
28
  this.#storage = storage;
27
29
  this.#clock = clock2;
@@ -46,7 +48,11 @@ var DummySettings = class extends Emitter {
46
48
  this.emit("settings", this.get());
47
49
  }
48
50
  models() {
49
- return this.#models;
51
+ if (!this.#gated) return this.#models;
52
+ const live = new Set(
53
+ this.#accounts.filter((account) => account.status === "connected").map((account) => account.provider)
54
+ );
55
+ return this.#models.filter((option) => live.has(option.provider));
50
56
  }
51
57
  accounts() {
52
58
  return this.#accounts.map((account) => ({ ...account }));
@@ -99,4 +105,4 @@ export {
99
105
  dummySettingsKey,
100
106
  DummySettings
101
107
  };
102
- //# sourceMappingURL=chunk-2PKRU2CL.js.map
108
+ //# sourceMappingURL=chunk-X4C3AZJT.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/dummy/clock.ts", "../src/dummy/settings.ts"],
4
+ "sourcesContent": ["export interface Clock {\n delay: number;\n sleep(ticks?: number): Promise<void>;\n}\n\nexport function clock(delay: number): Clock {\n return {\n delay,\n sleep: (ticks = 1) => new Promise((resolve) => setTimeout(resolve, delay * ticks)),\n };\n}\n", "import { Emitter } from '../model/emitter.ts';\nimport type {\n Account,\n AccountStatus,\n ModelOption,\n Settings,\n SettingsEvents,\n SettingsPort,\n} from '../model/types.ts';\nimport type { Clock } from './clock.ts';\n\nexport const dummySettingsKey = 'slicc-ui.settings';\n\nexport class DummySettings extends Emitter<SettingsEvents> implements SettingsPort {\n #settings: Settings;\n #accounts: Account[];\n #models: readonly ModelOption[];\n #storage: Storage | null;\n #clock: Clock;\n #keys: readonly string[];\n #signIns = new Map<string, { before: AccountStatus; stop: () => void }>();\n #gated: boolean;\n\n constructor(\n defaults: Settings,\n models: readonly ModelOption[],\n accounts: readonly Account[],\n storage: Storage | null,\n clock: Clock,\n keys: readonly string[] = [dummySettingsKey],\n gated = false\n ) {\n super();\n this.#gated = gated;\n this.#keys = keys;\n this.#storage = storage;\n this.#clock = clock;\n this.#models = models;\n this.#accounts = accounts.map((account) => ({ ...account }));\n this.#settings = { ...defaults, ...this.#load() };\n }\n\n #load(): Partial<Settings> {\n try {\n const saved = this.#keys\n .map((key) => this.#storage?.getItem(key))\n .find((value) => value != null);\n return JSON.parse(saved ?? '{}');\n } catch {\n return {};\n }\n }\n\n get(): Settings {\n return { ...this.#settings };\n }\n\n update(patch: Partial<Settings>): void {\n this.#settings = { ...this.#settings, ...patch };\n this.#storage?.setItem(this.#keys[0], JSON.stringify(this.#settings));\n this.emit('settings', this.get());\n }\n\n models(): readonly ModelOption[] {\n if (!this.#gated) return this.#models;\n const live = new Set(\n this.#accounts\n .filter((account) => account.status === 'connected')\n .map((account) => account.provider)\n );\n return this.#models.filter((option) => live.has(option.provider));\n }\n\n accounts(): readonly Account[] {\n return this.#accounts.map((account) => ({ ...account }));\n }\n\n #set(id: string, patch: Partial<Account>): void {\n this.#accounts = this.#accounts.map((account) =>\n account.id === id ? { ...account, ...patch } : account\n );\n this.emit('accounts', this.accounts());\n }\n\n async connect(id: string, secret?: string): Promise<void> {\n const account = this.#accounts.find((candidate) => candidate.id === id);\n if (account && account.auth !== 'api-key' && !(await this.#signIn(account))) return;\n if (account?.auth === 'api-key' || !account) await this.#clock.sleep(10);\n if (account?.auth === 'api-key' && account.store !== 'vault' && !secret)\n throw new Error(\n `${account.provider} needs ${account.secret === 'token' ? 'a token' : 'an API key'}`\n );\n const identity =\n account?.group === 'mcp'\n ? account.identity\n : account?.auth === 'api-key'\n ? 'API key'\n : account?.identity || `${id}-user@example.com`;\n this.#set(id, { status: 'connected', identity });\n }\n\n #signIn(account: Account): Promise<boolean> {\n const before = this.#signIns.get(account.id)?.before ?? account.status;\n this.#signIns.get(account.id)?.stop();\n this.#set(account.id, { status: 'signing-in' });\n return new Promise((resolve) => {\n const stop = () => {\n this.#signIns.delete(account.id);\n this.#set(account.id, { status: before });\n resolve(false);\n };\n this.#signIns.set(account.id, { before, stop });\n void this.#clock.sleep(100).then(() => {\n if (this.#signIns.get(account.id)?.stop !== stop) return;\n this.#signIns.delete(account.id);\n resolve(true);\n });\n });\n }\n\n cancel(id: string): void {\n this.#signIns.get(id)?.stop();\n }\n\n disconnect(id: string): void {\n this.#set(id, { status: 'disconnected' });\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["clock"]
7
+ }
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  DummySettings,
8
8
  clock
9
- } from "./chunk-2PKRU2CL.js";
9
+ } from "./chunk-X4C3AZJT.js";
10
10
  import {
11
11
  Emitter
12
12
  } from "./chunk-T5NHW4KC.js";
@@ -3547,6 +3547,10 @@ var models = [
3547
3547
  { id: "local-guard", label: "Local guard", provider: "This browser", kind: "classifier" },
3548
3548
  { id: "local-tiny", label: "Local tiny (no tools)", provider: "This browser", tools: false }
3549
3549
  ];
3550
+ var laterModels = [
3551
+ { id: "gpt-6", label: "GPT-6", provider: "OpenAI", images: true },
3552
+ { id: "gpt-6-mini", label: "GPT-6 mini", provider: "OpenAI", images: true, default: true }
3553
+ ];
3550
3554
  var defaults = {
3551
3555
  color: "system",
3552
3556
  model: "claude-sonnet-5-5",
@@ -6272,6 +6276,7 @@ function createDummyModel({
6272
6276
  follow = "off",
6273
6277
  vault = "off",
6274
6278
  accounts: accounts2 = "settings",
6279
+ providers = "connected",
6275
6280
  hold = "off",
6276
6281
  browser: scenario = "idle",
6277
6282
  sprinkles: sprinkles2 = "builtin",
@@ -6307,10 +6312,14 @@ function createDummyModel({
6307
6312
  terminals: new DummyTerminals(terminals, files3, time),
6308
6313
  settings: new DummySettings(
6309
6314
  defaults,
6310
- models,
6311
- accounts2 === "vault" ? vaultAccounts(accounts) : accounts,
6315
+ providers === "none" ? [...models, ...laterModels] : models,
6316
+ (accounts2 === "vault" ? vaultAccounts(accounts) : accounts).map(
6317
+ (account) => providers === "none" && account.group !== "mcp" ? { ...account, status: "disconnected", identity: "" } : account
6318
+ ),
6312
6319
  storage,
6313
- time
6320
+ time,
6321
+ void 0,
6322
+ providers === "none"
6314
6323
  ),
6315
6324
  memory: new DummyMemory(memories, () => [
6316
6325
  { id: "global", label: "Everyone" },