@ai-ecoverse/slicc-spectrum 1.77.1 → 1.78.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 +5 -5
- package/dist/slicc-dummy.js +48 -1
- package/dist/slicc-dummy.js.map +2 -2
- package/dist/slicc-ui.js +321 -153
- package/dist/slicc-ui.js.map +4 -4
- package/dist/types/app/agents.d.ts +3 -0
- package/dist/types/app/approvals.d.ts +6 -4
- package/dist/types/dummy/approvals.d.ts +1 -1
- package/dist/types/model/types.d.ts +37 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -136,10 +136,10 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
|
|
|
136
136
|
| :--- | :--- |
|
|
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
|
-
| `<slicc-agents>` | A tree (`role="tree"`) of cones, with their scoops
|
|
139
|
+
| `<slicc-agents>` | A tree (`role="tree"`) of cones, with their scoops listed under them. A cone with scoops starts with a quiet disclosure chevron (`data-action="toggle-cone"`, `swc-icon-chevron-right`, turned down when open and mirrored right to left); it's `aria-hidden` and out of the Tab order, since the row's `aria-expanded` carries the state. The chevron only expands and collapses; a click on the row, <kbd>Enter</kbd> or <kbd>Space</kbd> selects the agent and makes it the active one, which also expands its cone. <kbd>→</kbd> expands a cone and then moves to its first scoop, <kbd>←</kbd> moves from a scoop to its cone and then collapses it, and neither changes the active agent. Several cones can be open at once, and the active agent's cone is open again whenever the panel mounts, so on a phone a cone you tapped still shows its scoops when you come back to Agents. When the active scoop's cone is collapsed, the cone row takes the Tab stop. A row's first line starts with one circle: its unread count when there are unread messages (`99+` from a hundred up, at `--swc-font-size-50`, white on the accent fill, or on the negative fill when the cone is in error), 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, aligned to the top under the name, 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 and the tooltip say the status and the exact unread count too. A cone with scoops has a ribbon line below: one segment per scoop, coloured by status at 3:1 or more on every row background, then its scoops in words, naming only the busy ones, as in "2 scoops · 1 waiting" ("2 scoops" when all are idle). Its `aria-description` sums up every status, as in "2 scoops: 1 working, 1 idle". Rows without scoops or errors have no empty ribbon or error line. The drop or delete button sits in its own column on the row's right edge, under the context fill, so the title ellipsizes before it and the focus ring stays whole; it shows on hover, on keyboard focus of the row, when <kbd>Tab</kbd> from the selected row reaches it, and always on touch, and it hides again once a pointer that clicked it leaves. The selected row has an accent bar on its inline-start edge. Under forced colors, the dots, segments, unread circle and selection bar keep `CanvasText`, `Highlight` and `HighlightText`. 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)". |
|
|
140
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
|
-
| `<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
|
-
| `<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. |
|
|
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]`, with `[data-approval-kind]`); renders nothing without any. |
|
|
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 (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. 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 only Deny and Allow once; the dialog body carries `[data-approval-kind]` and fills the dialog's width at every size. |
|
|
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. |
|
|
@@ -251,7 +251,7 @@ With an `agentId` or a `cone`, the caller reaches the shared surfaces, the chats
|
|
|
251
251
|
| `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`link` while a linked slicc carries the traffic, with `exit` naming it, `proxy` from older bios for slicc on this computer, `extension` for the Chrome extension relay, `tailnet` while a Tailscale exit node carries the traffic, `page` for the page's own fetch, limited by CORS, or `null`), `health` (`ok`, `limited` or `failing`, decided by the port), a one-line `detail`, recent `failures` (`{ url, error, at }`, newest first), an optional `extensionUrl` and an optional `browser` (`{ via, detail? }`, `via` being `extension`, `proxy`, `link` when linked devices' browsers carry it (the line names the browser and its device, or counts them when there are several, counting connected devices only), or `null` when browser automation isn't available; a legacy `declined` flag is ignored, since there is no consent prompt any more). Emit `network` when it changes. The optional `check()` tests the route again. An optional `tailnet` (`TailnetStatus`) shows the Tailscale section: `state` (`off`, `loading`, `needs-login`, `starting`, `running` or `failed`, a status light), a plain-language `detail` (progress, or the failure reason in an error box with **Retry** through `check()`), a `loginUrl` (**Sign in to Tailscale**, a new tab), the `node` (`{ name, addresses }`; the name and the first 100.x address, each with a Copy button), `exitNode` (the name in use or `null`), `exitNodes` (`{ id, name, online }`), `autoExitNode`, `shieldsUp` (a muted line when `true`, a warning when `false`) and `peers` (a count). The optional `setTailnet(on)` adds the **Use Tailscale** switch (`data-action="tailnet"`); `submitAuthKey(key)` adds an auth-key field to `needs-login` (a password field without a `name`, cleared on submit, never kept in component state); `setExitNode(id | 'auto' | null)` adds the **Exit node** picker (None, Automatic, and each node, offline ones disabled) to `running`, with a line saying where traffic goes; `logoutTailnet()` adds **Sign out**, behind a confirm. A rejected call shows its message in the section. Peers are never named beyond the exit-node list. An optional `links` (`NetworkLinks`) shows **Linked devices**, slicc CLIs linked over WebRTC: `joinUrl` and `joinCommand` (each with Copy, `data-copy="join-url"` and `data-copy="join-command"`; both `null` shows "Preparing a join URL…"), an optional `followUrl` (the page URL with `#follow=` and the join URL, shown with a QR code). Join URLs let a device join, so the join URL, the command, the follow link and the tray's join link are masked (the last path segment and anything after it, or, without a path, everything after `#` or `?`; text without a URL is masked whole), with `data-secret="hidden"` until **Show** (`data-action="reveal-<copy>"`) sets it to `shown`; a new URL masks again, Copy copies the real value either way, and the QR code stays scannable, `devices` and `permission`. A `LinkedDevice` has an `id`, a display `name` (a leading `slicc` is shown as `SLICC`), the kernel `host` and `address`, a `mode` (`local` for a CLI on this computer attached over CDP, or `remote`), `offers` (`net`, `http`, `ssh`; `ssh` adds a copyable `ssh <host>`), a one-line `policy`, `exit` (whether it can carry internet traffic), a `state` (`connecting`, `connected`, `reconnecting` or `offline`, for a device that linked before and isn't there now; every device that ever linked is listed, so one that was refused can be allowed), optional `control` (`{ allowed, fixed? }`; `fixed` for one on this computer, always allowed), optional `controlling` (`prompt` or `watch` while a CLI prompts or watches the agent) optional `browsers` (`{ runtime, name }`, listed in the row as `data-browsers`, with the runtime such as `pier-nuc/chrome` that the 9222 facade drives), optional `kind` (`cli`, or `seven` for another SLICC that follows this one, "Follower" in the row), optional `access` (`{ view?, control?, terminal?, fixed? }`: only the keys present get a switch, so a CLI sends `{ control }`; `fixed` lists them as always allowed from this computer; it replaces `control`), optional `asking` (a follower that waits to be allowed), optional `terminals` (how many terminals it has open here) and optional `folders` (`LinkedFolder`s the device shares: `name`, `source` such as `pier-nuc/proj`, `readonly`, and `mounted`, the mount point once mounted). Folders list in the row (`[data-folders] li[data-folder]`, with `data-mounted` `yes` or `no`, a **Read-only** badge and "Mounted at …"); `mountFolder(source)` adds **Mount** (`data-action="mount-folder"`, `data-id` the source; only on a connected device) and resolves with the mount point, such as `/mnt/pier-nuc/proj`, and `ejectFolder(source)` adds **Eject** (`data-action="eject-folder"`); a failure shows on the links error line. Each device is an `li[data-device]` with `data-state`, `data-mode` and `data-kind`, and `data-asking` while it asks; once there are devices, the join details fold into **Link another device**. `permission` is `prompt` (an informative line asking to choose Allow in Chrome's Local Network Access prompt) or `denied` (a warning, with **Try again**, `data-action="retry-links"`, through `retryLinks()`). An optional `exit` (`NetworkExit`: `null`, `{ kind: 'tailnet', node }` with a Tailscale node id or `'auto'`, or `{ kind: 'link', id }`) is the default exit. The optional `setExit(exit)` replaces the Tailscale **Exit node** picker with one **Internet traffic** picker in Connection: Directly, then Tailscale (Automatic and its nodes) and Linked devices (those with `exit: true`), offline ones disabled unless chosen; a chosen device that isn't connected shows that traffic waits, since the exit fails closed. Without `setRemoteControl(id, allowed)`, `control` is a line saying whether it's allowed; with it, **Allow remote control** is a switch. Without `setAccess(id, 'view' \| 'control' \| 'terminal', allowed)`, `access` is a line saying what's allowed; with it, the switches call it, and **Allow** on a device that asks calls `setAccess(id, 'view', true)`. Turning View off should turn Control off too. `unlink(id)` adds **Unlink** (`data-action="unlink"`) to each device and `rotateJoinUrl()` adds **New join URL** (`data-action="rotate-join-url"`), both behind a confirm. Their failures show in the section. An optional `leaders` (`Leader[]`: `id`, the leader's hub origin, unique per leader; `name`, from the link's hello, with a leading `slicc` shown as `SLICC`; `hub`, the host to show; `state`, `connecting`, `reconnecting`, `connected`, `disconnected`, `failed` or `unlinked`; and an optional `error`) lists the SLICCs this one follows, shown when non-empty. `unlinked` means the leader removed this SLICC and bios already forgot it: send it in one status, then leave it out. `retryLeader(id)` adds **Retry** to a `failed` leader, `disconnectLeader(id)` stops following but keeps the saved join and the device certificate, so the row stays as `disconnected` across reloads, `reconnectLeader(id)` resumes it without a new link (`connecting`, then `connected`, or `unlinked`, shown once, if the leader unlinked this SLICC meanwhile), and `forgetLeader(id)` drops the join and the certificate for that hub, and the row goes, so the leader has to invite this SLICC again. Without the port, the indicator and the Network panel don't show. |
|
|
252
252
|
| `notices` (optional) | BIOS | `list()` returns `Notice`s for quiet, dismissible in-line notices at the top of every chat panel, above the transcript, in list order: `id`, `tone` (`info` reads as informative, `warning` as a notice), `title`, a plain-text `body` and `actions` (`{ id, label }`). Title, body and labels are text only. Notices use `role="status"` and never take focus. An action is a secondary button that stays pending until `act(id, action)` settles; a rejection shows its message under the body. The close button, labelled "Dismiss: <title>", hides the notice at once and calls `dismiss(id)` synchronously; it shows again only if the port drops the notice from `list()` and brings it back later. Emit `notices` whenever they change. A notice with `dismissible: false` has no close button and stays until the port drops it. Hooks: `[data-notice="<id>"]`, buttons with `data-action`, `[data-dismiss]` and `[data-error]`. |
|
|
253
253
|
| `tabs` (optional) | BIOS | `state()` returns this tab's `TabsState`: `role` (`owner` runs SLICC; `follower` shows the owner tab's SLICC; `connecting` while the owner boots or hands over; `alone` without SharedWorker), `stalled` (the owner stopped answering) and `skew` (`newer` or `older` when the owner runs another SLICC version, else `null`). Emit `tabs` when it changes. An owner shows nothing. A follower gets a quiet header light, "Another tab" (`[data-tabs][data-role]`, with `data-stalled` and `data-skew`; a dot on phones), info while connecting, and a notice while stalled or skewed, with its explanation as the tooltip. While connecting, an empty chat says "Connecting to the tab running SLICC…" (`[data-connecting]`). Skew, or else a stall, shows a warning banner at the top of every chat panel (`[data-tabs-notice="skew"\|"stalled"]`, `data-skew`): "Another tab runs a newer SLICC. Reload this tab." with **Reload** (`data-action="reload"`, calls `reload()`), "Another tab runs an older SLICC. Reload the other SLICC tab.", or "Chrome paused the tab running SLICC. Switch to it to continue.". An `alone` tab shows only the brand and "SLICC is open in another tab." (`[data-tabs="alone"]`). |
|
|
254
|
-
| `approvals` (optional) | BIOS | `list()` returns waiting `Approval`s; emit `approvals` when the list changes. A `sudo` approval has `id`, `requester` (`kind` `agent` or `user`, `name`, optional `agentId` and `where`, such as a terminal's title), `command` (without `sudo`; bios joins the kernel's argv shell-quoted), optional `target` (the user to run as; shown as `sudo -u {target}` unless `root`), optional `cwd`, `createdAt`, `expiresAt` (epoch ms) and `always` (offer Always allow, which covers the requester's user and this exact command). With `decide(id, 'once' \| 'always' \| 'deny')`, which resolves once the decision is recorded and then drops the request from `list()`, the tab shows a warning alert dialog (`dialog[data-approval][data-kind][data-requester]`): "Allow sudo?", "{name} wants to run this with sudo[ in {where}]:" for an agent or "You ran this with sudo[ in {where}] (as {name}):" for a user, the command, "In {cwd}", a notice when the command changes secret rules, what Always allow does, "Denied automatically in m:ss." and how many more wait, with **Deny** (`data-action="deny"`, focused; Escape also denies), **Always allow** (`always`) and **Allow once** (`once`). A command that names `/etc/fnox` (or anything in it) or `/etc/slicc/secrets-policy.toml`, resolved against `cwd`, globs included (with quotes and backslashes removed, operands after `--` included; a path with `[`, `{` or `$` counts when the part before it could lead to them), or that names `/`, `/etc` or `/etc/slicc` in a command other than a read-only one (such as `ls`, `cat`, `stat` or `grep`), or runs with `cwd` in `/etc/fnox`, or names a relative path with `fnox` or `secrets-policy` in it wherever `cd` may have led, gets "This changes secret rules." with the path (`[data-secret-rules]`, the path in the attribute). Read-only commands count only by bare name or from `/bin` and `/usr/bin`. `/proc/<pid>/root` counts as `/` and `/proc/self/cwd` as `cwd`. Otherwise a command with a parameter expansion or command substitution (`$…`, backticks), a `~+` or `~-` path or a `~` path that climbs with `..`, a path through another process's `/proc/<pid>/cwd`, `fd` or `map_files` or through `/dev/fd`, or more than 4096 characters, gets "This may change secret rules." with the first such part in the attribute. Other symlinks aren't followed. Either way there's no Always allow, even when `always` is `true` (`decide(id, 'always')` isn't called for it), so each change to the rules is asked for. For half a second after a request shows, only Deny works, so a double-click can't approve it. A rejected `decide` shows its message in the dialog. Pass `decide` in every tab: the first decision wins. When a request ends without a decision in this tab, emit `ended` with `{ id, reason }` and drop it from `list()`: `answered` (another tab decided) shows "Answered in another tab", `expired` shows "Sudo request timed out", each with the command and **Close** (`data-action="close"`, `dialog[data-gone]`), and `withdrawn` (the requester went away) closes the dialog silently. Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it. Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "Answer it in the tab running SLICC. Denied automatically at {time}." |
|
|
254
|
+
| `approvals` (optional) | BIOS | `list()` returns waiting `Approval`s; emit `approvals` when the list changes. A `sudo` approval has `id`, `requester` (`kind` `agent` or `user`, `name`, optional `agentId` and `where`, such as a terminal's title), `command` (without `sudo`; bios joins the kernel's argv shell-quoted), optional `target` (the user to run as; shown as `sudo -u {target}` unless `root`), optional `cwd`, `createdAt`, `expiresAt` (epoch ms) and `always` (offer Always allow, which covers the requester's user and this exact command). With `decide(id, 'once' \| 'always' \| 'deny')`, which resolves once the decision is recorded and then drops the request from `list()`, the tab shows a warning alert dialog (`dialog[data-approval][data-kind][data-requester]`): "Allow sudo?", "{name} wants to run this with sudo[ in {where}]:" for an agent or "You ran this with sudo[ in {where}] (as {name}):" for a user, the command, "In {cwd}", a notice when the command changes secret rules, what Always allow does, "Denied automatically in m:ss." and how many more wait, with **Deny** (`data-action="deny"`, focused; Escape also denies), **Always allow** (`always`) and **Allow once** (`once`). A command that names `/etc/fnox` (or anything in it) or `/etc/slicc/secrets-policy.toml`, resolved against `cwd`, globs included (with quotes and backslashes removed, operands after `--` included; a path with `[`, `{` or `$` counts when the part before it could lead to them), or that names `/`, `/etc` or `/etc/slicc` in a command other than a read-only one (such as `ls`, `cat`, `stat` or `grep`), or runs with `cwd` in `/etc/fnox`, or names a relative path with `fnox` or `secrets-policy` in it wherever `cd` may have led, gets "This changes secret rules." with the path (`[data-secret-rules]`, the path in the attribute). Read-only commands count only by bare name or from `/bin` and `/usr/bin`. `/proc/<pid>/root` counts as `/` and `/proc/self/cwd` as `cwd`. Otherwise a command with a parameter expansion or command substitution (`$…`, backticks), a `~+` or `~-` path or a `~` path that climbs with `..`, a path through another process's `/proc/<pid>/cwd`, `fd` or `map_files` or through `/dev/fd`, or more than 4096 characters, gets "This may change secret rules." with the first such part in the attribute. Other symlinks aren't followed. Either way there's no Always allow, even when `always` is `true` (`decide(id, 'always')` isn't called for it), so each change to the rules is asked for. For half a second after a request shows, only Deny works, so a double-click can't approve it. A rejected `decide` shows its message in the dialog (`[data-error]`, `dialog[data-rejected]`) in place of the countdown, with only **Close** (`data-action="close"`, focused; Escape also closes), which moves on to the next waiting request or closes the dialog. Pass `decide` in every tab: the first decision wins. When a request ends without a decision in this tab, emit `ended` with `{ id, reason }` and drop it from `list()`: `answered` (another tab decided) shows "Answered in another tab", `expired` shows "Sudo request timed out", each with the command and **Close** (`data-action="close"`, `dialog[data-gone]`), and `withdrawn` (the requester went away) closes the dialog silently. Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it. Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "Answer it in the tab running SLICC. Denied automatically at {time}." 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: false` and an optional `verdict` (`{ decision: 'allow' \| 'deny', reason }`, the automatic check's view). 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}" or "…deny this: {reason}" (`[data-verdict]`, the decision in the attribute). It offers only **Deny** and **Allow once**, whatever `always` says. `decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo. Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out". Without `decide`, a guest request's notice reads "Waiting for approval of a guest’s message" with "{name}'s message to {agentName} waits for approval on the device in control.", or "Waiting for approval of a guest’s tool call" with "{agentName} wants to run {tool} for {name}, which waits for approval on the device in control.", then "Denied automatically at {time}." |
|
|
255
255
|
| `vault` (optional) | BIOS | The vault's view for SLICC, typed after `@ai-ecoverse/slicc-vault` (spectrum re-declares the types it uses, and `compat/vault.ts` checks that the package's types fit them). `status()` returns the `VaultStatus` (`locked`, `lock`, `scrub`, `waiting` requests, and optional `exportedAt` and `backupReminder`) or `null` while loading, and `list()` the `VaultList` (`secrets` as `SecretView`s, `refused` rules, `exits`, and the first `audit` page with `auditMore`) or `null`. Emit `status` and `list` when they change (bios re-lists after the vault's `changed` events), and `availability` when the optional `availability()` (`connecting`, `waiting`, `locked` or `ready`) changes. `audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`). The optional page ops, each hiding its control when missing: `put(name, value, request?)` (a value request's `put` must use its `name`), `cancel(request)`, `remove(name, kind?)`, `restore(name, kind?)`, `import(kind, bytes, { password?, pick? })` (a `pick` stage, then `done`), `export(passphrase)` (the bundle's bytes), `lock()` and `dismissBackupReminder()`. They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`). Values go to the vault only through `put`, and never through another port. `button()` returns a new `VaultButton`, `{ element, post(message), listen(fn) }` returning an unsubscribe: bios wraps the vault's `button.html` frame and its postMessage protocol, and spectrum only sends `ButtonMessage`s and receives `ButtonEvent`s. The passkey lock (unlock and protect) happens in the vault's own popup, which only a click in that frame can open. `notices`, `acknowledge` and the `notice` event are still typed, and ignored. |
|
|
256
256
|
| `follow` (optional) | BIOS | Only on a follower page, a SLICC that shows another SLICC's agent over a link. `state()` returns the `FollowState`: `owner` (its display name), `link` (`connecting` for the first connection, `connected`, `reconnecting` after a connection dropped, `unlinked` when the owner unlinked this device, or `failed`), `access` (`{ view, control, terminal }`, what the owner allows this device), an optional `name` (this device's name on the owner, for the waiting page) and an optional `error` (shown when `failed`). Emit `follow` when it changes. Connected without `view` means waiting to be allowed. `leave()` forgets the link and the optional `retry()` tries again from `failed`. Pass only the surfaces a follower has to `<slicc-app>` (agents, chat, terminal and settings) and leave out `network`. |
|
|
257
257
|
|
|
@@ -265,7 +265,7 @@ The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current'
|
|
|
265
265
|
|
|
266
266
|
`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. 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.
|
|
267
267
|
|
|
268
|
-
`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
|
|
268
|
+
`createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'guest' | 'guest-follower' })` (`?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." (`guest-follower`: the same without `decide`). Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
|
|
269
269
|
|
|
270
270
|
`createDummyModel({ vault: 'empty' | 'secrets' | 'policy' | 'locked' | 'request' | 'backup' | 'waiting' | '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). It implements every op with the vault's checks, and keeps only names: `model.vault.puts` records `{ name, request? }`, `cancelled` the cancelled requests, `imports` each import's kind, size and pick, and `exported` the exports. Import offers five invented candidates (one replaces a secret, one is a model key, one is refused with a reason that the done stage repeats; KeePass adds a `_2` duplicate), six answers ENOTSUP, page ops are recorded with `uid: null, tag: null`, and value ops while locked answer EACCES. `model.vault.fail(code, message)` makes the next op fail. Its buttons are plain Spectrum buttons that answer at once: Unlock unlocks, and created buttons are in `model.vault.buttons`. `vaultFixtures`, `DummyVault` and `DummyVaultButton` are exported from `/dummy`. `createDummyModel({ hold: 'waiting' | 'locked' | 'off' })` (`?hold=` on `/ui/`) makes the agent hold two tool results; a `locked` hold clears when the dummy vault unlocks, and `model.agent.setHold(hold)` changes it.
|
|
271
271
|
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -14,6 +14,50 @@ import "./chunk-AXQFWXYZ.js";
|
|
|
14
14
|
|
|
15
15
|
// src/dummy/approvals.ts
|
|
16
16
|
var minutes = 6e4;
|
|
17
|
+
var mara = { kind: "guest", name: "Seat 2 (Mara)" };
|
|
18
|
+
function guestFixtures(now2 = Date.now()) {
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
id: "guest-note",
|
|
22
|
+
kind: "guest-message",
|
|
23
|
+
requester: mara,
|
|
24
|
+
agentId: "cone-sliccy",
|
|
25
|
+
agentName: "sliccy",
|
|
26
|
+
text: "Could you tidy the harbor schedule?\nKeep <b>Tuesday</b> as it is and **skip** the ferry rows.",
|
|
27
|
+
attachments: [
|
|
28
|
+
{ name: "schedule.csv", mimeType: "text/csv", size: 18432 },
|
|
29
|
+
{ name: "pier-map.png", mimeType: "image/png", size: 2411725 }
|
|
30
|
+
],
|
|
31
|
+
createdAt: now2,
|
|
32
|
+
expiresAt: now2 + 5 * minutes,
|
|
33
|
+
always: false,
|
|
34
|
+
verdict: { decision: "allow", reason: "it asks for an edit inside the shared folder." }
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "guest-fetch",
|
|
38
|
+
kind: "guest-tool",
|
|
39
|
+
requester: mara,
|
|
40
|
+
agentId: "scoop-docs",
|
|
41
|
+
agentName: "docs-pass",
|
|
42
|
+
tool: "fetch",
|
|
43
|
+
summary: "GET https://tides.example/api/harbor?days=7",
|
|
44
|
+
createdAt: now2 + 1,
|
|
45
|
+
expiresAt: now2 + 5 * minutes,
|
|
46
|
+
always: false,
|
|
47
|
+
verdict: { decision: "deny", reason: "guests may not reach hosts outside the session." }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "guest-late",
|
|
51
|
+
kind: "guest-message",
|
|
52
|
+
requester: { kind: "guest", name: "Seat 3 (Ilse)" },
|
|
53
|
+
agentId: "cone-sliccy",
|
|
54
|
+
text: "Is the pier map up to date?",
|
|
55
|
+
createdAt: now2 + 2,
|
|
56
|
+
expiresAt: now2 + 5 * minutes,
|
|
57
|
+
always: false
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
}
|
|
17
61
|
function approvalFixtures(scenario, now2 = Date.now()) {
|
|
18
62
|
const agent = {
|
|
19
63
|
id: "sudo-jq",
|
|
@@ -57,6 +101,7 @@ function approvalFixtures(scenario, now2 = Date.now()) {
|
|
|
57
101
|
always: true
|
|
58
102
|
}
|
|
59
103
|
];
|
|
104
|
+
if (scenario === "guest" || scenario === "guest-follower") return guestFixtures(now2);
|
|
60
105
|
if (scenario === "none") return [];
|
|
61
106
|
if (scenario === "user") return [user];
|
|
62
107
|
if (scenario === "many") return [agent, user, cache];
|
|
@@ -74,7 +119,8 @@ var DummyApprovals = class extends Emitter {
|
|
|
74
119
|
this.#approvals = approvalFixtures(scenario);
|
|
75
120
|
this.#time = time;
|
|
76
121
|
this.#fail = scenario === "fail";
|
|
77
|
-
if (scenario !== "follower"
|
|
122
|
+
if (scenario !== "follower" && scenario !== "guest-follower")
|
|
123
|
+
this.decide = (id, decision) => this.#decide(id, decision);
|
|
78
124
|
if (scenario === "elsewhere" || scenario === "withdrawn")
|
|
79
125
|
void time.sleep(100).then(() => this.end("sudo-jq", scenario === "elsewhere" ? "answered" : "withdrawn"));
|
|
80
126
|
}
|
|
@@ -93,6 +139,7 @@ var DummyApprovals = class extends Emitter {
|
|
|
93
139
|
async #decide(id, decision) {
|
|
94
140
|
await this.#time.sleep(10);
|
|
95
141
|
if (this.#fail) throw new Error("The kernel stopped waiting. Run the command again.");
|
|
142
|
+
if (id === "guest-late") throw new Error("Someone else already answered this.");
|
|
96
143
|
this.decisions.push([id, decision]);
|
|
97
144
|
this.end(id, "answered");
|
|
98
145
|
}
|