@ai-ecoverse/slicc-spectrum 1.83.0 → 1.84.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 +832 -123
- package/dist/slicc-dummy.js +29 -0
- package/dist/slicc-dummy.js.map +2 -2
- package/dist/slicc-ui.js +64 -31
- package/dist/slicc-ui.js.map +3 -3
- package/dist/types/app/approvals.d.ts +1 -0
- package/dist/types/app/guests.d.ts +3 -1
- package/dist/types/dummy/approvals.d.ts +1 -1
- package/dist/types/dummy/guests.d.ts +4 -2
- package/dist/types/dummy.d.ts +1 -1
- package/dist/types/model/types.d.ts +7 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# slicc-spectrum
|
|
2
2
|
|
|
3
|
-
Web components for SLICC: `<slicc-terminal>`, and SLICC's app UI on [Spectrum Web Components](https://opensource.adobe.com/spectrum-web-components/) (see [UI](#ui)).
|
|
3
|
+
Web components for SLICC: `<slicc-terminal>`, and SLICC's app UI on [Spectrum Web Components](https://opensource.adobe.com/spectrum-web-components/) (see [UI](#ui)).
|
|
4
|
+
`<slicc-terminal>` is a terminal built on [wterm](https://github.com/vercel-labs/wterm) (`@wterm/dom` 0.5.4, Zig/WASM VT core) that talks to [`@ai-ecoverse/slicc-kernel`](https://github.com/ai-ecoverse/slicc-kernel) through a small backend interface.
|
|
5
|
+
It is a standards custom element with no framework and no shadow root, so it can move to a shared web-components repo unchanged.
|
|
4
6
|
|
|
5
7
|
```html
|
|
6
8
|
<slicc-terminal></slicc-terminal>
|
|
@@ -16,24 +18,62 @@ Web components for SLICC: `<slicc-terminal>`, and SLICC's app UI on [Spectrum We
|
|
|
16
18
|
|
|
17
19
|
## Element
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
### `backend`
|
|
22
|
+
|
|
23
|
+
A `TerminalBackend`.
|
|
24
|
+
Setting it closes the current session and opens a new one.
|
|
25
|
+
|
|
26
|
+
### `cols`, `rows` attributes
|
|
27
|
+
|
|
28
|
+
Fixed grid size.
|
|
29
|
+
Without either, the grid fits the element and follows its size.
|
|
30
|
+
|
|
31
|
+
### `ready`
|
|
32
|
+
|
|
33
|
+
Promise that resolves with the element once wterm has mounted and the session is open.
|
|
34
|
+
Rejects if either fails.
|
|
35
|
+
Setting `backend` or removing the element replaces it with a fresh promise, and anyone still waiting on the old one gets the new outcome.
|
|
36
|
+
|
|
37
|
+
### `cols`, `rows` properties
|
|
38
|
+
|
|
39
|
+
Current grid size.
|
|
40
|
+
|
|
41
|
+
### `resize(cols, rows)`
|
|
42
|
+
|
|
43
|
+
Sets both attributes and resizes once.
|
|
44
|
+
|
|
45
|
+
### `fit()`
|
|
46
|
+
|
|
47
|
+
Removes both attributes and fits the grid to the element.
|
|
48
|
+
|
|
49
|
+
### `send(data)`
|
|
50
|
+
|
|
51
|
+
Sends a string or bytes to the backend, as if typed.
|
|
52
|
+
|
|
53
|
+
### `write(data)`
|
|
54
|
+
|
|
55
|
+
Writes a string or bytes to the screen without involving the backend.
|
|
56
|
+
|
|
57
|
+
### `redraw(data)`
|
|
58
|
+
|
|
59
|
+
Writes a string or bytes to the screen only, without answering any terminal query in it (for replaying history).
|
|
60
|
+
|
|
61
|
+
### `signal(name)`
|
|
62
|
+
|
|
63
|
+
Sends `SIGINT`, `SIGTSTP`, `SIGQUIT` or `SIGHUP` to the backend.
|
|
64
|
+
|
|
65
|
+
### `readText()`
|
|
66
|
+
|
|
67
|
+
Resolves with the text of the scrollback and the screen.
|
|
68
|
+
|
|
69
|
+
### `focus()`
|
|
70
|
+
|
|
71
|
+
Focuses the terminal input.
|
|
33
72
|
|
|
34
73
|
Events, all `CustomEvent`s with the payload in `detail`: `ready` `{cols, rows}`, `resize` `{cols, rows}`, `exit` `{status}`, `title` `{title}`, `bell` `{count}`, `error` `{error}`.
|
|
35
74
|
|
|
36
|
-
It answers OSC 10 and OSC 11 color queries (`ESC ] 10 ; ? ST` and `ESC ] 11 ; ? ST`) with the foreground and background it shows at that moment, as `rgb:rrrr/gggg/bbbb`, so command-line tools can tell a light theme from a dark one and follow a theme switch.
|
|
75
|
+
It answers OSC 10 and OSC 11 color queries (`ESC ] 10 ; ? ST` and `ESC ] 11 ; ? ST`) with the foreground and background it shows at that moment, as `rgb:rrrr/gggg/bbbb`, so command-line tools can tell a light theme from a dark one and follow a theme switch.
|
|
76
|
+
The answer reaches the backend before replies to anything that comes later in the output.
|
|
37
77
|
|
|
38
78
|
Removing the element closes the session and destroys the screen. Adding it again mounts a fresh screen and opens a new session on the same backend.
|
|
39
79
|
|
|
@@ -57,11 +97,22 @@ interface TerminalSession {
|
|
|
57
97
|
}
|
|
58
98
|
```
|
|
59
99
|
|
|
60
|
-
Keystrokes reach `write` as the bytes a terminal would send: UTF-8 text, `\r` for Enter, `0x03` for Ctrl+C, escape sequences for arrows and function keys, and bracketed paste when the application has turned it on.
|
|
100
|
+
Keystrokes reach `write` as the bytes a terminal would send: UTF-8 text, `\r` for Enter, `0x03` for Ctrl+C, escape sequences for arrows and function keys, and bracketed paste when the application has turned it on.
|
|
101
|
+
Pasted `\r\n` and `\n` line breaks are sent as `\r`, as xterm.js does, in both plain and bracketed paste.
|
|
102
|
+
The backend's line discipline turns `0x03`, `0x1a` and `0x1c` into signals, as a real pty does.
|
|
103
|
+
`signal()` is optional.
|
|
104
|
+
Without it, `terminal.signal()` writes the matching control byte instead, and `SIGHUP` is dropped.
|
|
105
|
+
Output may arrive in chunks of any size and split anywhere, including inside UTF-8 sequences and escape sequences.
|
|
106
|
+
While `open()` is pending, input (keystrokes, `send()` and wterm's automatic replies) is buffered and written in order once the session opens.
|
|
107
|
+
If that open fails or is superseded, the buffer is dropped.
|
|
61
108
|
|
|
62
109
|
### slicc-kernel
|
|
63
110
|
|
|
64
|
-
`kernelBackend(kernel, { argv, cwd, user, env })` adapts slicc-kernel's terminal API to the interface above.
|
|
111
|
+
`kernelBackend(kernel, { argv, cwd, user, env })` adapts slicc-kernel's terminal API to the interface above.
|
|
112
|
+
`argv` defaults to `['bash', '-i']`.
|
|
113
|
+
`user` (a name or uid, slicc-kernel 1.44 and later) is who the session runs as; without it the kernel runs it as `root`, with `HOME=/root`.
|
|
114
|
+
It is typed structurally, so this package doesn't depend on slicc-kernel.
|
|
115
|
+
If the session fails, its error message is printed and `exit` reports status 1.
|
|
65
116
|
|
|
66
117
|
```js
|
|
67
118
|
import { createKernel } from './node_modules/@ai-ecoverse/slicc-kernel/dist/index.js';
|
|
@@ -92,21 +143,31 @@ The stylesheet (wterm's plus the theme) is constructed once per document and ado
|
|
|
92
143
|
|
|
93
144
|
## Distribution
|
|
94
145
|
|
|
95
|
-
`npm run build` writes `dist/slicc-terminal.js` (one ESM bundle with no bare imports), its source map and `dist/wterm.wasm`.
|
|
146
|
+
`npm run build` writes `dist/slicc-terminal.js` (one ESM bundle with no bare imports), its source map and `dist/wterm.wasm`.
|
|
147
|
+
The bundle loads the WASM with `new URL('./wterm.wasm', import.meta.url)`, so `dist/` works from any path, including from OPFS through the slicc-bios service worker. wterm's base64-inlined copy of the WASM is left out of the bundle.
|
|
148
|
+
TypeScript declarations go to `dist/types/`, and the package's `types` export points at them.
|
|
149
|
+
Set `SliccTerminal.wasmUrl` before the first element connects to load it from elsewhere.
|
|
96
150
|
|
|
97
151
|
## wterm notes
|
|
98
152
|
|
|
99
153
|
Things found while building this against wterm 0.5.4, recorded here, not pushed upstream:
|
|
100
154
|
|
|
101
|
-
- The Zig core maps 24-bit colours (`38;2;r;g;b`) to the nearest of the 256 palette colours, and the DOM renderer draws the 6×6×6 cube in steps of 51 rather than xterm's 0/95/135/175/215/255.
|
|
155
|
+
- The Zig core maps 24-bit colours (`38;2;r;g;b`) to the nearest of the 256 palette colours, and the DOM renderer draws the 6×6×6 cube in steps of 51 rather than xterm's 0/95/135/175/215/255.
|
|
156
|
+
`@wterm/ghostty` is the full-VT alternative.
|
|
102
157
|
- wterm sends pasted text with its `\n` line breaks unchanged, whereas xterm.js turns them into `\r`. The element does that conversion itself.
|
|
103
158
|
- `@wterm/core` always ships the WASM inlined as base64 behind a dynamic import. A build without it, or an explicit entry point that takes a URL, would save the bundler plugin.
|
|
104
159
|
- wterm focuses its input when it finishes mounting. `<slicc-terminals>` hands focus back to where it was unless the terminal was focused on purpose.
|
|
105
|
-
- wterm reads focus and selection from `ownerDocument` (`activeElement`, `getSelection()`), so selection and copy don't work inside a shadow root that wterm itself owns.
|
|
160
|
+
- wterm reads focus and selection from `ownerDocument` (`activeElement`, `getSelection()`), so selection and copy don't work inside a shadow root that wterm itself owns.
|
|
161
|
+
That's why the element renders in light DOM.
|
|
162
|
+
It still works when the element is placed inside someone else's shadow root.
|
|
106
163
|
|
|
107
164
|
## UI
|
|
108
165
|
|
|
109
|
-
`dist/slicc-ui.js` is SLICC's app shell on Spectrum 2, in light and dark.
|
|
166
|
+
`dist/slicc-ui.js` is SLICC's app shell on Spectrum 2, in light and dark.
|
|
167
|
+
It is moving from Spectrum Web Components 1.12 to Gen2 ([`@adobe/spectrum-wc`](https://www.npmjs.com/package/@adobe/spectrum-wc)): the Gen2 theme classes (`swc-theme swc-theme--sizeM swc-theme--light|dark`) and `--swc-*` tokens apply inside `<slicc-app>`, and icons are Spectrum 2 workflow icons from `@adobe/spectrum-wc-icons`.
|
|
168
|
+
Components Gen2 doesn't ship yet stay on 1.12 inside a scoped `<sp-theme system="spectrum-two">` ([#60](https://github.com/ai-ecoverse/slicc-spectrum/issues/60)).
|
|
169
|
+
It runs against a typed model, not against the kernel, the agent or the browser directly.
|
|
170
|
+
`dist/slicc-dummy.js` provides a dummy model that streams replies and tool calls with timers, and `dist/slicc-kernel-model.js` a real one for files and terminals on slicc-kernel (see [Kernel model](#kernel-model)).
|
|
110
171
|
|
|
111
172
|
```html
|
|
112
173
|
<slicc-app></slicc-app>
|
|
@@ -122,57 +183,380 @@ Things found while building this against wterm 0.5.4, recorded here, not pushed
|
|
|
122
183
|
|
|
123
184
|
### Layouts and rails
|
|
124
185
|
|
|
125
|
-
The layout follows the width of `<slicc-app>`, which it reflects as its `screen` attribute: `phone` (under 640 px) has one column, `tablet` (under 1200 px) two, `desktop` three, like SLICC v6.
|
|
126
|
-
|
|
127
|
-
|
|
186
|
+
The layout follows the width of `<slicc-app>`, which it reflects as its `screen` attribute: `phone` (under 640 px) has one column, `tablet` (under 1200 px) two, `desktop` three, like SLICC v6.
|
|
187
|
+
Each screen class starts with few panels open and saves its own layout (`slicc-ui.layout.v2.<screen>`).
|
|
188
|
+
Every surface has a home side, left, center or right.
|
|
189
|
+
Every panel has an icon in the left or right rail (on a phone, in one bar at the bottom, with the rest in its More menu).
|
|
190
|
+
The icon stays when its panel is open; while the panel is visible it shows selected (`.selected`, `data-visible`, labelled "{Title}, shown").
|
|
191
|
+
One click puts a closed panel back on its side, next to its neighbours, or brings an open one to the front.
|
|
192
|
+
Each thread, a cone's or a scoop's conversation, opens in its own chat panel (`chat:<agent id>`), as a tab in the same group as the other chats.
|
|
193
|
+
Selecting a thread opens or reveals its tab, activating a chat tab selects its thread, and a thread's chat closes when the thread goes away (frozen or deleted).
|
|
194
|
+
|
|
195
|
+
`surfaces` on `<slicc-app>` (default: every surface, exported as `surfaces`) is what the app offers: the panels it can open, the View menu, the rails and `Alt+1…9`.
|
|
196
|
+
A saved layout with a panel it doesn't offer is dropped for the default.
|
|
197
|
+
An embedder can pass a subset with its own `open` per screen class.
|
|
198
|
+
The agent picker and the tray show only when `chat` is offered, the update status only with `updates`, the network indicator only with `network` and a `network` port, and the settings button only with `settings`.
|
|
199
|
+
Elements with `slot="status"` show in the header as compact single-line items, before the update status, and are cut with an ellipsis when long; a `hidden` one takes no space.
|
|
200
|
+
There are no strips under the header.
|
|
201
|
+
On a phone the status items are hidden.
|
|
128
202
|
|
|
129
203
|
### Fonts
|
|
130
204
|
|
|
131
|
-
Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectrum's Source Code Pro.
|
|
205
|
+
Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectrum's Source Code Pro.
|
|
206
|
+
Source Code Pro (OFL) is in `dist/fonts/`, same origin.
|
|
207
|
+
Neither Adobe Clean can ship in a public package.
|
|
208
|
+
`<slicc-app>` registers the variable face (`adobe-clean-spectrum-vf`, weights 100–900) from `variableFont`, a woff2 URL that defaults to the Adobe Fonts file React Spectrum S2 uses; a host can point it at its own copy, or set it to `null` to leave it out.
|
|
209
|
+
The legacy faces come from `fontBase` (`AdobeClean-{Regular,Medium,Bold,ExtraBold}.otf`), which defaults to `/fonts/` on the current origin.
|
|
210
|
+
Every `*.sliccy.ai` host passes it through to v6, and `npm start` proxies it from `seven.sliccy.ai`.
|
|
211
|
+
The sans stack (`--swc-sans-font-family-stack`, also used by the 1.12 components) is the variable face, then Adobe Clean, then the system font stack, so text falls back face by face wherever a file doesn't load.
|
|
212
|
+
`?delay=` sets the dummy's tick in milliseconds (default 30) , `?color=light|dark|system` the theme, and `?grammars=` the grammar base.
|
|
132
213
|
|
|
133
214
|
### Elements
|
|
134
215
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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. With the model's `seat` set, the chat is a guest's page: the header shows only the cone's name (`[data-seat-cone]`) and its status, with no pickers, meter or New conversation; the guest's own messages (`origin` `guest` whose `seat` is the seat's `id`, or, without a `seat`, whose `from` is its `label`) read as the reader's own, with no name, while other guests keep "From {label}" and the host's messages read "From the host"; an open question or a permission request takes the normal card border, without its buttons, form, countdown or pile (no `data-pending`), and says "The host answers this." (`[data-host-answers]`, subdued); once answered, it shows the answer as everyone sees it; a reviewed guest message's outcome (a notice titled "It was approved.", "Nobody approved this in time.", "It was declined." or "It was withdrawn.", with the message as its text) reads as the quoted message, cut to one line, then the outcome (`.marker[data-ended]`); an approved one shows only "It was approved.", since the delivered message follows right after it; inline sprinkles inert with "View only on this device", error cards have no action and there's no suggested follow-up; the models aren't settled, and files can't be dropped. An empty guest chat says "No messages yet. Say hello to sliccy." |
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
216
|
+
#### `<slicc-app>`
|
|
217
|
+
|
|
218
|
+
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>`.
|
|
219
|
+
Saves the dock layout per screen class to `storage` (default `localStorage`) under `layoutKey`.
|
|
220
|
+
While connected it sets `overscroll-behavior` to `none !important` on the page's `html` and `body`, so only scroll areas inside panels rubber-band.
|
|
221
|
+
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.
|
|
222
|
+
When the last app on the page is removed, the page's own inline values and `theme-color` contents come back.
|
|
223
|
+
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.
|
|
224
|
+
It offers its `surfaces` (the View menu, More panels, the rails and, on a phone, the bottom bar and More), except the Network panel when the model has neither a `network` nor a `guests` port, and the terminal while terminals aren't allowed; rebinding to a model without either port closes Network.
|
|
225
|
+
|
|
226
|
+
#### `<slicc-dock>`
|
|
227
|
+
|
|
228
|
+
Generic.
|
|
229
|
+
[dockview-core](https://dockview.dev/) 8 with a theme mapped to Spectrum tokens, in its own shadow root.
|
|
230
|
+
Panels come from `factories` (`name → (params) => HTMLElement`).
|
|
231
|
+
`open`, `close`, `has`, `content`, `focusPanel`, `cycleGroup`, `cycleTab`, `toJSON`, `restore`, `clear`, and the full `api`.
|
|
232
|
+
Tabs have a context menu (close, close others, float, maximize).
|
|
233
|
+
Events: `layout-change`, `active-panel-change`, `panel-close`.
|
|
234
|
+
|
|
235
|
+
#### `<slicc-agents>`
|
|
236
|
+
|
|
237
|
+
A tree (`role="tree"`) of cones, with their scoops listed under them.
|
|
238
|
+
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.
|
|
239
|
+
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.
|
|
240
|
+
<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.
|
|
241
|
+
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.
|
|
242
|
+
When the active scoop's cone is collapsed, the cone row takes the Tab stop.
|
|
243
|
+
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.
|
|
244
|
+
Then comes the name; the context fill, when it's high, sits in the top right corner.
|
|
245
|
+
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.
|
|
246
|
+
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).
|
|
247
|
+
Its `aria-description` sums up every status, as in "2 scoops: 1 working, 1 idle".
|
|
248
|
+
Rows without scoops or errors have no empty ribbon or error line.
|
|
249
|
+
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.
|
|
250
|
+
The selected row has an accent bar on its inline-start edge.
|
|
251
|
+
Under forced colors, the dots, segments, unread circle and selection bar keep `CanvasText`, `Highlight` and `HighlightText`.
|
|
252
|
+
A cone with scoops carries `aria-expanded`, and `aria-controls` names its scoop rows while they show.
|
|
253
|
+
Expanding and collapsing have no motion.
|
|
254
|
+
**New cone** sits at the bottom.
|
|
255
|
+
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".
|
|
256
|
+
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)".
|
|
257
|
+
|
|
258
|
+
#### `<slicc-chat>`
|
|
259
|
+
|
|
260
|
+
One thread's conversation (its `agent`, or the active agent if unset), by day.
|
|
261
|
+
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.
|
|
262
|
+
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.
|
|
263
|
+
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.
|
|
264
|
+
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.
|
|
265
|
+
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.
|
|
266
|
+
The label sits at the end and is cut with an ellipsis before it reaches that space.
|
|
267
|
+
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.
|
|
268
|
+
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.
|
|
269
|
+
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.
|
|
270
|
+
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.
|
|
271
|
+
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).
|
|
272
|
+
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.
|
|
273
|
+
A lick's `severity` (`warn` or `error`) adds an alert icon to its byline, a coloured edge to its box and a spoken prefix.
|
|
274
|
+
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.
|
|
275
|
+
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.
|
|
276
|
+
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.
|
|
277
|
+
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.
|
|
278
|
+
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.
|
|
279
|
+
Under `prefers-reduced-motion`, nothing animates: no transitions, and the scroll is instant.
|
|
280
|
+
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.
|
|
281
|
+
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.
|
|
282
|
+
The pickers re-read `models()` on `settings`, `accounts` and `agents`.
|
|
283
|
+
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.
|
|
284
|
+
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.
|
|
285
|
+
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.
|
|
286
|
+
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.
|
|
287
|
+
Between the header and the transcript, a `<slicc-notices>` shows the model's notices.
|
|
288
|
+
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.
|
|
289
|
+
An empty chat says "No messages yet.
|
|
290
|
+
Ask sliccy something." with the agent's name, and only "No messages yet." while watch-only.
|
|
291
|
+
With the model's `seat` set, the chat is a guest's page: the header shows only the cone's name (`[data-seat-cone]`) and its status, with no pickers, meter or New conversation; the guest's own messages (`origin` `guest` whose `seat` is the seat's `id`, or, without a `seat`, whose `from` is its `label`) read as the reader's own, with no name, while other guests keep "From {label}" and the host's messages read "From the host"; an open question or a permission request takes the normal card border, without its buttons, form, countdown or pile (no `data-pending`), and says "The host answers this."
|
|
292
|
+
(`[data-host-answers]`, subdued); once answered, it shows the answer as everyone sees it; a reviewed guest message's outcome (a notice titled "It was approved.", "Nobody approved this in time.", "It was declined." or "It was withdrawn.", with the message as its text) reads as the quoted message, cut to one line, then the outcome (`.marker[data-ended]`); an approved one shows only "It was approved.", since the delivered message follows right after it; inline sprinkles inert with "View only on this device", error cards have no action and there's no suggested follow-up; the models aren't settled, and files can't be dropped.
|
|
293
|
+
An empty guest chat says "No messages yet.
|
|
294
|
+
Say hello to sliccy."
|
|
295
|
+
|
|
296
|
+
#### `<slicc-notices>`
|
|
297
|
+
|
|
298
|
+
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.
|
|
299
|
+
|
|
300
|
+
#### `<slicc-approvals>`
|
|
301
|
+
|
|
302
|
+
The model's waiting `approvals` as a modal alert dialog, one at a time, oldest first; `<slicc-app>` mounts it.
|
|
303
|
+
Renders nothing without `approvals.decide`.
|
|
304
|
+
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`)**.
|
|
305
|
+
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.
|
|
306
|
+
Guest requests (`guest-message`, `guest-tool`) get their own copy, the guest's text as plain text (`[data-guest-text]`), attachments (`[data-attachments]`), the automatic check's verdict (`[data-verdict]`), and Deny and Allow once, plus **Always allow** (`data-action="always"`) with a hint saying what it covers when the approval's `always` is `true`; the dialog body carries `[data-approval-kind]` and fills the dialog's width at every size.
|
|
307
|
+
|
|
308
|
+
#### `<slicc-follow>`
|
|
309
|
+
|
|
310
|
+
The model's `follow` state.
|
|
311
|
+
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).
|
|
312
|
+
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**.
|
|
313
|
+
A rejected call shows its message (`[data-error]`).
|
|
314
|
+
Renders nothing without the port.
|
|
315
|
+
|
|
316
|
+
#### `<slicc-composer>`
|
|
317
|
+
|
|
318
|
+
The message box.
|
|
319
|
+
<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.
|
|
320
|
+
`/` completes commands (`/clear`, `/compact`, `/model`, `/thinking`, `/scoop`, `/stop`, `/freeze` when the port can freeze, `/theme`) and their arguments, `@` mentions agents and files.
|
|
321
|
+
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).
|
|
322
|
+
<kbd>Esc</kbd> in the `+` menu closes it, from any item, and puts focus back on `+`; it goes no further, so it doesn't also stop a reply.
|
|
323
|
+
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.
|
|
324
|
+
Model and thinking pickers.
|
|
325
|
+
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.
|
|
326
|
+
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.
|
|
327
|
+
Without speech recognition there is no item and no switch, and Alt+M types as usual.
|
|
328
|
+
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.
|
|
329
|
+
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).
|
|
330
|
+
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.
|
|
331
|
+
Drafts are kept per agent.
|
|
332
|
+
On a follower while the link reconnects, the field is disabled and says so; the draft stays.
|
|
333
|
+
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.
|
|
334
|
+
With the model's `seat` set it's a guest's composer: <kbd>Enter</kbd> and the send button send through `agent.send(cone, text)`, <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line and <kbd>↑</kbd>/<kbd>↓</kbd> walk the guest's own messages; there's no steer, queue, Stop, attachments (the field is `collapsed`, so its + is hidden and inert, and pasted files are ignored), `/` commands, `@` mentions, + menu or push to talk, and it never reads files; its description is "Enter sends, Shift+Enter adds a new line."
|
|
335
|
+
A rejected send shows the rejection's message above the field (`[data-send-error]`, `role="alert"`) and puts the draft back, ahead of anything typed since.
|
|
336
|
+
The guest's messages waiting for review (`agent.queue()`) show above the field as rows reading "Waiting for review" (`li.queued[data-mode="review"]`), with no Send now or remove, and the composer never calls `unqueue`.
|
|
337
|
+
A quiet line under it (`[data-seat-note]`) says who reviews ("The host reviews what you send before it reaches the agent." for `user`, "An automatic check …" for `agent`, the cone's name for `cone` and `scoop:<name>`), or "You’re chatting with sliccy as {label}." when `gates.messages` is `off`, and adds "Your link expires at {time}." when `expiresAt` is under a day away.
|
|
338
|
+
|
|
339
|
+
#### `<slicc-files>`
|
|
340
|
+
|
|
341
|
+
The file system as a `<slicc-file-tree>`, with pending changes as git status.
|
|
342
|
+
Opening a file opens it in a tab.
|
|
343
|
+
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`.
|
|
344
|
+
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)`.
|
|
345
|
+
A failed mount, insert or eject shows its message under the bar.
|
|
346
|
+
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.”
|
|
347
|
+
(`.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.
|
|
348
|
+
File icons are monochrome, so colour in the tree only means git status.
|
|
349
|
+
|
|
350
|
+
#### `<slicc-file-view>`
|
|
351
|
+
|
|
352
|
+
One file (`path`), highlighted, with its pending change and a link to the diff.
|
|
353
|
+
|
|
354
|
+
#### `<slicc-changes>`
|
|
355
|
+
|
|
356
|
+
Pending changes with their author.
|
|
357
|
+
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"`).
|
|
358
|
+
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.
|
|
359
|
+
With nothing pending it says "No pending changes.
|
|
360
|
+
Edits by agents show up here for review.", and only "No pending changes." while watch-only.
|
|
361
|
+
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.
|
|
362
|
+
In a diff panel narrower than 480 px, Accept and Revert (`data-action="accept"`, `data-action="revert"`) show only their icons.
|
|
363
|
+
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.
|
|
364
|
+
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"`).
|
|
365
|
+
|
|
366
|
+
#### `<slicc-diff-panel>`
|
|
367
|
+
|
|
368
|
+
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.
|
|
369
|
+
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.
|
|
370
|
+
|
|
371
|
+
#### `<slicc-terminals>`
|
|
372
|
+
|
|
373
|
+
Terminal sessions in tabs, each a `<slicc-terminal>` on the session's `TerminalBackend`.
|
|
374
|
+
Sessions stay alive while hidden; `exit` closes the tab.
|
|
375
|
+
|
|
376
|
+
#### `<slicc-browser>`
|
|
377
|
+
|
|
378
|
+
What the agents' browser control is doing.
|
|
379
|
+
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.
|
|
380
|
+
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.
|
|
381
|
+
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.
|
|
382
|
+
Below 480 px they fold into **Recent actions (n)**, a chevron disclosure counting all of them, with the last one showing.
|
|
383
|
+
Labels, URLs and errors are text only.
|
|
384
|
+
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).
|
|
385
|
+
The Browser rail button carries a dot (`data-driven`) while a tab is in use.
|
|
386
|
+
|
|
387
|
+
#### `<slicc-settings>`
|
|
388
|
+
|
|
389
|
+
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.
|
|
390
|
+
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.
|
|
391
|
+
Accounts with `group: 'mcp'` (MCP servers) get their own **MCP servers** heading (`.accounts[data-group=mcp]`).
|
|
392
|
+
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**.
|
|
393
|
+
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`.
|
|
394
|
+
An account with `fields` (`{ name, label, required? }`) shows a plain labelled input for each, and their values go to `connect(id, secret?, fields)`.
|
|
395
|
+
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.
|
|
396
|
+
Without `store: 'vault'` or `vault.put`, the key goes to `connect(id, secret)` as before.
|
|
397
|
+
With a `vault` port, **Secrets** (`<slicc-secrets>`) follows.
|
|
398
|
+
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.
|
|
399
|
+
|
|
400
|
+
#### `<slicc-secrets>`
|
|
401
|
+
|
|
402
|
+
Settings › Secrets from the model's `vault`.
|
|
403
|
+
It never shows a value or a placeholder.
|
|
404
|
+
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.
|
|
405
|
+
Values go only to `vault.put` and are never kept on the element or put in an event.
|
|
406
|
+
The head shows **Locked** or **Unlocked** and the lock mode ("Unlocks automatically in this browser", "Unlocks with a passkey" or "Unlocks with a passphrase"); 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"`).
|
|
407
|
+
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`.
|
|
408
|
+
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.
|
|
409
|
+
With `backupReminder: 'due'`, an alert (`[data-backup]`) offers **Export backup** (`data-action="export"`) and **Not now** (`data-action="dismiss-backup"`, with `dismissBackupReminder`).
|
|
410
|
+
**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`).
|
|
411
|
+
Saved shows "Saved NAME in the vault" (`[data-said]`).
|
|
412
|
+
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").
|
|
413
|
+
Locked, every form (Add secret, Replace value, Import, Export) says "Unlock the vault first" (`p[data-locked]`) next to the vault's own small **Unlock** button (`slicc-vault-button[data-action=unlock]`) and disables its submit; once unlocked, the form stays open with what was typed and its submit enables.
|
|
414
|
+
Vault errors show their message, EACCES as "Unlock the vault first" and ENOTSUP as "This arrives with a later vault version".
|
|
415
|
+
Each action shows only when the port has its method.
|
|
416
|
+
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"`).
|
|
417
|
+
The section's `data-vault` is `locked`, `unlocked` or `loading`.
|
|
418
|
+
A popup the browser blocked shows how to allow it (`[data-popup=blocked]`).
|
|
419
|
+
Widened-rule notices aren't shown.
|
|
420
|
+
|
|
421
|
+
#### `<slicc-vault-button>`
|
|
422
|
+
|
|
423
|
+
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.
|
|
424
|
+
|
|
425
|
+
#### `<slicc-value-request>`
|
|
426
|
+
|
|
427
|
+
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.
|
|
428
|
+
`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`).
|
|
429
|
+
Locked, it shows the vault's Unlock button and disables Save.
|
|
430
|
+
It closes, clearing the field, when the request leaves `waiting` or while the vault is away.
|
|
431
|
+
|
|
432
|
+
#### `<slicc-updates>`
|
|
433
|
+
|
|
434
|
+
Install / Update: component states, version changes, last checks, download and link counts, action buttons, failures with Retry, and collapsible install logs.
|
|
435
|
+
Opens at boot until the agent is ready without failures.
|
|
436
|
+
Background updates stay in the header's update status; new failures reopen the panel, and after a reload into a boot that isn't ready yet such a panel closes once the agent is ready.
|
|
437
|
+
Every automatic close is saved to the layout.
|
|
438
|
+
When the port offers packages, an Optional packages section lists them below as compact rows, each with its state, version, download size, requirements, commands, progress, errors and Install log; packages never count in the header or keep the panel open at boot.
|
|
439
|
+
Packages the user added or pinned with pnpm show up in their own Your packages section below it, with a neutral Pinned light (a hold, not a success) or a positive Added light, their version against seven's, and Unpin or Remove.
|
|
440
|
+
Also available through View and the rails.
|
|
441
|
+
|
|
442
|
+
#### `<slicc-memory>`
|
|
443
|
+
|
|
444
|
+
What agents remember, grouped by section: search, filter by scope and tag, expand, edit, add and forget.
|
|
445
|
+
The scope picker starts on **Everyone** (`*`), which lists every memory with its scope on it (`.scope[data-scope]`); `global` reads **Shared** whatever the port calls it, and the cones and roles follow.
|
|
446
|
+
The count (“N of M memories”) counts the memories in the picked scope.
|
|
447
|
+
With a search or tag filter that hides everything it says “No memories match.”, otherwise “No memories yet.”, “No shared memories yet.” or “No memories for {scope} yet.”
|
|
448
|
+
Each memory is a row (`li.row[data-id]`) that opens in place: open rows (`data-open`) take the accent's subtle background and turn their chevron down, and show the body with **Edit** and **Forget** (`data-action="edit"`, `data-action="forget"`).
|
|
449
|
+
Under the summary, the tag is a subtle badge (`swc-badge[data-tag]`: User indigo, Feedback purple, Project seafoam), then the scope and, for free text outside an entry, “From MEMORY.md” (`.from`).
|
|
450
|
+
Below 560 px of panel width, the search takes its own line over the two pickers.
|
|
451
|
+
The search field is the shared one from `search.ts` (`sp-search[data-action=search]`), as in Files.
|
|
452
|
+
|
|
453
|
+
#### `<slicc-monitor>`
|
|
454
|
+
|
|
455
|
+
The live monitor: vitals (`.vital[data-id]`), each a meter when it has a `ratio` (budget, fullest context) and otherwise a value, its unit and delta on one line and a sparkline from its `series` (spend, active agents, processes, process memory).
|
|
456
|
+
Each card keeps its place whatever it shows: the value line never wraps (a unit or delta that doesn't fit is left out whole rather than cut, and stays in the line's `title` and its text; a vital's optional `hint` replaces that tooltip, such as the exact reset time behind "resets in 4 days"), a card without a `series` keeps the sparkline's room, a meter keeps its description's line, and a card left alone on the last row spans it, so short and long values, 0% and 100%, and missing data move nothing below them.
|
|
457
|
+
Then alerts (an alert's optional `command` shows after its detail, led by its `commandLabel` or "Run", as unwrapped code with **Copy**, `[data-command]` and `data-action="copy-command"`), and the sections (the agent's Tasks, Processes and Recent problems, then terminals, tabs, pending changes and tray followers; cones and sprinkles have their own panels; any other section an agent sends, such as an older agent's `agents` or `sprinkles`, renders the same way).
|
|
458
|
+
Each section (`li.section[data-id]`) shows its count and an attention badge, and opens its surface with **Open** where one exists; the counts line up in one column whether or not a section has **Open**, and a section without rows keeps its name in line with the others (`agents`, `terminals` and `processes` → terminal, `tabs`, `changes`).
|
|
459
|
+
A section with rows expands into them with its toggle (`.toggle`, `aria-expanded`; `data-open` on the section): collapsed by default, expanded while a row is `warn` or `error`, until the user toggles it.
|
|
460
|
+
Each row (`li.row[data-status]`, `data-id` from the row's `id`) shows a status light (blue active, gray idle, orange warn, red error, with the status spoken), the name, the meta line and its badges, indented by `depth` (`data-depth`, up to 6).
|
|
461
|
+
|
|
462
|
+
#### `<slicc-freezer>`
|
|
463
|
+
|
|
464
|
+
Every conversation in the session, from `frozen()`: cones, scoops and agent runs, each with a kind badge, live ones first with a Live light, then the newest first.
|
|
465
|
+
A live row offers **Open**, which selects that agent; a stopped one offers **Thaw** and **Remove**, which deletes its working folders after a confirmation.
|
|
466
|
+
A thawed scoop or agent run keeps its row, which says what it was thawed as.
|
|
467
|
+
**Delete cone**, in the agents list (or <kbd>Delete</kbd> on a cone's row) and the header's cone picker, stops a cone after a confirmation; `/freeze` does it at once.
|
|
468
|
+
Either way its conversation stays here, and a row without a `kind` counts as a cone.
|
|
469
|
+
|
|
470
|
+
#### `<slicc-sprinkle>`
|
|
471
|
+
|
|
472
|
+
A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit, even when it loads out of view (the frame measures itself on load, after icons and fonts arrive, and on every resize).
|
|
473
|
+
A dip has a tab-like handle: drag it into the dock (or onto a rail, or use its open-as-panel button) and it lives on as a sprinkle panel, with a note in the chat where it was; which dips are out is saved in `slicc-ui.dips`.
|
|
474
|
+
It runs in a sandboxed frame (`allow-scripts` only) with SLICC's sprinkle theme (the `--s2-*` tokens and `.sprinkle-*` classes), the app's fonts, and Lucide icons (`<i data-lucide>`, `LucideIcons.render()`).
|
|
475
|
+
It talks back through `slicc.lick({ action, data })`, which reaches the owning cone as a `sprinkle` lick, and, when the port has `call`, reads files and keeps state (see [Sprinkle bridge](#sprinkle-bridge)).
|
|
476
|
+
Panel sprinkles sit at the top of the right rail with their Lucide icon.
|
|
477
|
+
On a guest's page (the model's `seat` set), every sprinkle, inline or panel, is view only: its frame is inert, it says "View only on this device", and its licks and `setState` calls go nowhere.
|
|
478
|
+
|
|
479
|
+
#### `<slicc-network>`
|
|
480
|
+
|
|
481
|
+
Network: the route and its health in plain words with the bios `detail`, a line on browser automation (`data-browser`: through the SLICC extension, through slicc on this computer (`proxy`, or a linked device on this computer), through a linked device's browser, that slicc on this computer has no browser to control when it carries the traffic and `via` is `null`, or not available with how to get it) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, an **Internet traffic** picker (`data-action="exit"`, in `[data-exit-picker]` with the exit's kind) when the port has `setExit`, the route line saying where traffic goes, and under the picker a warning that traffic waits while the chosen device isn't connected (`data-exit="device"`, `data-held`, with **Send directly**, `data-action="exit-direct"`, which calls `setExit(null)`) or, when `route` isn't `tailnet`, the Tailscale exit node in use (`data-exit="node"`), a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, a **Linked devices** section (`data-links`: `linked`, `connecting`, `reconnecting`, `offline`, `none` or `permission`; each device row has a **Prompting** or **Watching** badge, `data-controlling`, while it controls the agent, and **Allow remote control**, `sp-switch[data-action="remote-control"]`, behind a confirm when turned on, or `data-control="fixed"` for a device on this computer; with `access`, an **Allow** group of **View**, **Control** and **Terminal** switches, `data-action="access-view"`, `access-control` and `access-terminal`, each confirmed when turned on, Terminal's saying it gives a root shell, and Control disabled while View is off; a device that asks to follow shows **Asks to follow** and **Allow**, `data-action="allow-follow"`; a **Terminal open** badge, `data-terminals`, counts its terminals; with `followUrl`, the join details add **Open on another device** with a QR code and the link, `data-join="follow"` and `data-copy="follow-url"`) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and the commands to run slicc, each in a `.command[data-command]` block with a copy button: `curl -fsSL https://get.sliccy.ai/slicc | sh` (its button is `data-action="copy-command"`), `slicc`, and, with Node, `npx sliccy@next`; folded away when the route already reaches the whole web (`proxy`, `extension`, `link` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line says "Through slicc on this computer" for a `local` device, or names a remote device and its `policy`, or says it's waiting for it, and `data-route` is `link`, whatever `route` says), and recent failures with host, error and time.
|
|
482
|
+
When the model has a `tray` port, a **Sync** section (`data-sync` with the connection) follows the connection: the tray's state, the float's name, role and runtime, the join link, masked (`…/join/••••••`) until **Show** (`data-action="reveal-tray-join"`, which turns into **Hide**), with **Copy** (`data-action="copy-tray-join"`), the followers (`li[data-follower]`), and **Disconnect** (`data-action="tray-disconnect"`, confirmed) or **Reconnect** (`data-action="tray-reconnect"`).
|
|
483
|
+
When the status has `leaders`, a **Leaders** section (`section[data-leaders]` with the count) follows Sync, since it's the other direction of the same tray link: Sync is this SLICC's own tray and who follows it, Leaders the SLICCs it follows.
|
|
484
|
+
Each leader is an `li[data-leader]` (the id) with `data-state`: its name, its hub as a quiet second line and a status light with the state in words (Connecting, Reconnecting, Connected, Disconnected, Failed, Unlinked); `failed` adds its `error` as plain text (`[data-leader-error]`, "Couldn’t reach its hub." without one) and **Retry** (`data-action="retry-leader"`), and `unlinked` says "{name} stopped sharing with this SLICC."
|
|
485
|
+
(`[data-unlinked]`) without actions.
|
|
486
|
+
**Disconnect** (`data-action="disconnect-leader"`) runs at once, without a confirm, since it's reversible: the row stays as Disconnected (a neutral light), with **Reconnect** (`data-action="reconnect-leader"`) and **Forget** and no Disconnect or Retry.
|
|
487
|
+
**Forget** (`data-action="forget-leader"`) asks "Forget {name}?"
|
|
488
|
+
("To follow it again, you’ll need a new invite from it.") with a negative confirm, and the row goes.
|
|
489
|
+
Each shows only when the port has its method; while one runs, the row's buttons are disabled, and a failure shows in the row (`data-error="leader"`).
|
|
490
|
+
When the model has a `guests` port, a **Guests** section (`section[data-guests]` with the count) follows Leaders: **Invite a guest** (`data-action="invite-guest"`, accent, disabled while no cone is live; frozen cones are left out of the invite too), or, while `unavailable()` (or the last `unavailable` event, when the port has no method) is a sentence, that sentence instead, subdued (`[data-guests-unavailable]`).
|
|
491
|
+
Each seat is an `li[data-seat]` (the id) with `data-connected` (`true` or `false`): its label, "on {cone}" as a quiet second line (`[data-seat-cone]`), "Expires in {n} hours" under a day, "Expires in {n} days" after, or "No expiry" (`[data-expiry]`), a status light with **Connected** or **Not connected**, a count badge (`data-pending`) reading "{n} waiting", whose accessible name is "{n} waiting for review" when `pending` is above 0, and **Revoke** (`data-action="revoke-guest"`), which asks "Revoke {label}?"
|
|
492
|
+
("Their link stops working at once.
|
|
493
|
+
To let them back in, invite them again.") with a negative confirm; while it runs, the rows' buttons are disabled, and a failure shows in the row (`data-error="guest"`).
|
|
494
|
+
With no seats it says "No guests yet."
|
|
495
|
+
(`[data-guests-empty]`).
|
|
496
|
+
Invite opens `<slicc-guest-invite>`, a modal: **Cone** (`#guest-cone`, the active cone, or a selected scoop's cone, else the first), **Name** (`#guest-name`, required, trimmed, at most 40 characters, "Shown to you and in the cone, never set by the guest."), **Link expires** (`#guest-expiry`: 1 hour, 1 day, 7 days by default, or 30 days), **Their messages** (`#guest-messages`: You approve each one, The automatic check decides, {cone} decides, {scoop} decides for each of the cone's scoops, or No review, which are `user`, `agent`, `cone`, `scoop:<name>` and `off`) and **Their tool calls** (`#guest-tools`: `user`, `agent` and `scoop:<name>` only), both `user` by default, each with the chosen option's meaning under it.
|
|
497
|
+
When the port has `gates()`, each picker offers exactly its list, in that order (`scoop:*` as one choice per scoop of the chosen cone, a concrete `scoop:<name>` only when the cone has that scoop, `off` for tool calls reading "Tool calls run without review."), and starts on `user` when the list has it, else on its first choice; a list with no choice left for the cone offers `user`.
|
|
498
|
+
**Create link** (`data-action="create-guest-link"`) calls `mint(cone, { label, expiresAt, gates })`; while it runs the form is busy and Escape does nothing, and a rejection shows its message inline (`data-error="guest"`).
|
|
499
|
+
On success the dialog shows the URL once (`[data-guest-invite="link"]`): a read-only field (`data-copy="guest-url"`) with **Copy** (`data-action="copy-guest-url"`), "This link is shown only once.
|
|
500
|
+
Copy it now." and **Done** (`data-action="done-guest"`).
|
|
501
|
+
The URL is never stored or shown again: closing the dialog drops it.
|
|
502
|
+
The header's **Network** indicator (`data-network`, `data-health`) opens it: a status light, green for `ok`, yellow for `limited` and red for `failing`, with the state in its text and label and `detail` as its tooltip, which adds "Tailscale is connected." while the tailnet runs; on a phone, a cloud icon for the state.
|
|
503
|
+
While a linked device asks to follow, controls the agent, watches it or has a terminal open, an indicator (`data-remote`, `ask`, `prompt`, `terminal` or `watch`) sits before it, **{name} asks to follow**, **Controlled by**, **Terminal open from** or **Watched by** the device's name (or the number of devices; in that order of precedence), says who does what in its tooltip and opens the panel too.
|
|
504
|
+
Offered when the model has a `network` port, a `guests` port, or both.
|
|
505
|
+
With `guests` and no `network`, it shows only the Guests section (no connection, Sync, Leaders, Tailscale, Linked devices, Get the whole web or failures), still titled Network with its rail icon, and the header's Network and remote indicators stay hidden; the tray's connection chip is then a plain status.
|
|
506
|
+
|
|
507
|
+
#### `<slicc-tray>`
|
|
508
|
+
|
|
509
|
+
The tray items in the header, each opening a panel when that panel is in `panels`: the connection (`#chip`, a status light with the float's name and followers, `data-surface="network"`) opens Network at its **Sync** section, and the budget (`#usage`, `data-surface="monitor"`) opens the Monitor.
|
|
510
|
+
Without the panel an item is a plain status with its label.
|
|
511
|
+
|
|
512
|
+
#### `<slicc-confirm>`
|
|
513
|
+
|
|
514
|
+
The confirmation dialog `confirm()` opens.
|
|
515
|
+
Not for direct use.
|
|
516
|
+
|
|
517
|
+
#### `<slicc-prompt>`
|
|
518
|
+
|
|
519
|
+
The name dialog `prompt()` opens.
|
|
520
|
+
Not for direct use.
|
|
521
|
+
|
|
522
|
+
#### `<slicc-guest-invite>`
|
|
523
|
+
|
|
524
|
+
The invite dialog of the Network panel's Guests section; its gate pickers follow the `guests` port's `gates()` when it has one.
|
|
525
|
+
Not for direct use.
|
|
526
|
+
|
|
527
|
+
#### `<slicc-file-tree>`
|
|
528
|
+
|
|
529
|
+
Generic.
|
|
530
|
+
[`@pierre/trees`](https://www.npmjs.com/package/@pierre/trees) through its `web-components` entry: `paths` (folders end in `/`), `gitStatus`, `expanded`, `query` (shows only the paths that match it; empty shows all), `reveal(path, focus)`.
|
|
531
|
+
It has no search field of its own.
|
|
532
|
+
Fires `file-open` on click and on <kbd>Enter</kbd>, and `tree-search` (`detail.key`) when a letter or digit is typed in it, for the panel's search field.
|
|
533
|
+
|
|
534
|
+
#### `<slicc-code-view>`
|
|
535
|
+
|
|
536
|
+
Generic.
|
|
537
|
+
A highlighted file through [`@pierre/diffs`](https://www.npmjs.com/package/@pierre/diffs): `path`, `contents`, `color`.
|
|
538
|
+
|
|
539
|
+
#### `<slicc-diff-view>`
|
|
540
|
+
|
|
541
|
+
Generic.
|
|
542
|
+
A diff through `@pierre/diffs`: `path`, `oldText`, `newText` (`null` for an added or deleted file), `color`, `diff-style` (`unified` or `split`).
|
|
543
|
+
|
|
544
|
+
Files and diffs open in one editor group next to the chat.
|
|
545
|
+
Panels ask for them with `open-file` and `open-diff` events (`{ path }`), which `<slicc-app>` handles, or call `app.open('file' | 'diff', path)`.
|
|
546
|
+
|
|
547
|
+
Keys: <kbd>Alt</kbd>+<kbd>1</kbd>… opens and focuses a panel, <kbd>F6</kbd> and <kbd>Shift</kbd>+<kbd>F6</kbd> move between groups, <kbd>Ctrl</kbd>+<kbd>]</kbd> and <kbd>Ctrl</kbd>+<kbd>[</kbd> between tabs, <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd> switches the theme.
|
|
548
|
+
In terminal and browser tab strips, <kbd>←</kbd> and <kbd>→</kbd> switch tabs.
|
|
549
|
+
In the agents and changes lists, arrows, <kbd>Home</kbd>, <kbd>End</kbd> and <kbd>Enter</kbd>; in the file tree, its own arrow keys and <kbd>Enter</kbd> to open.
|
|
550
|
+
In the composer, <kbd>Enter</kbd> sends, <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line and <kbd>Esc</kbd> stops a reply.
|
|
172
551
|
|
|
173
552
|
### Confirmations
|
|
174
553
|
|
|
175
|
-
Destructive actions ask first: reverting one change or all, deleting a frozen cone, forgetting a memory, disconnecting an account or the tray, and closing a terminal.
|
|
554
|
+
Destructive actions ask first: reverting one change or all, deleting a frozen cone, forgetting a memory, disconnecting an account or the tray, and closing a terminal.
|
|
555
|
+
`confirm({ title, body, action, variant, cancel, trigger })`, exported from `dist/slicc-ui.js`, opens a modal alert dialog (1.12 `sp-alert-dialog`, as Gen2 has no dialog yet) inside `<slicc-app>`'s themed shadow root and resolves `true` on the action and `false` on Cancel or <kbd>Esc</kbd>.
|
|
556
|
+
`cancel` renames Cancel, for example “Don’t allow” next to “Allow” in a permission prompt.
|
|
557
|
+
Focus starts on Cancel and goes back to `trigger` (default: the focused element) when it closes.
|
|
558
|
+
`variant` is `destructive` (default, a negative action button) for actions that lose data for good, or `confirmation` (accent) for ones you can take back, like disconnecting.
|
|
559
|
+
Title the dialog with a question, say what can’t be undone in `body`, and name the action with a verb.
|
|
176
560
|
|
|
177
561
|
```js
|
|
178
562
|
import { confirm } from './dist/slicc-ui.js';
|
|
@@ -180,7 +564,10 @@ import { confirm } from './dist/slicc-ui.js';
|
|
|
180
564
|
if (await confirm({ title: 'Delete harbor?', body: 'Its scoops and messages are deleted. You can’t undo this.', action: 'Delete' })) remove();
|
|
181
565
|
```
|
|
182
566
|
|
|
183
|
-
`prompt({ title, label, action, value, trigger, submit })` asks for one line of text in the same kind of modal dialog, with a 1.12 `sp-textfield` (Gen2 has no text field yet).
|
|
567
|
+
`prompt({ title, label, action, value, trigger, submit })` asks for one line of text in the same kind of modal dialog, with a 1.12 `sp-textfield` (Gen2 has no text field yet).
|
|
568
|
+
Focus starts in the field, <kbd>Enter</kbd> or the action submits, and <kbd>Esc</kbd> or Cancel resolves `null`.
|
|
569
|
+
The action stays disabled while the trimmed value is empty.
|
|
570
|
+
With `submit`, the dialog awaits it with the action pending and resolves the trimmed value once it does; if it throws, the dialog stays open and shows the error under the field.
|
|
184
571
|
|
|
185
572
|
```js
|
|
186
573
|
import { prompt } from './dist/slicc-ui.js';
|
|
@@ -190,12 +577,16 @@ const name = await prompt({ title: 'New cone', label: 'Name', action: 'Create',
|
|
|
190
577
|
|
|
191
578
|
### Secret rules check
|
|
192
579
|
|
|
193
|
-
`secretRules(command, cwd)`, exported from `dist/slicc-ui.js` (`@ai-ecoverse/slicc-spectrum/ui`), is the check behind the sudo dialog's secret-rules warning.
|
|
580
|
+
`secretRules(command, cwd)`, exported from `dist/slicc-ui.js` (`@ai-ecoverse/slicc-spectrum/ui`), is the check behind the sudo dialog's secret-rules warning.
|
|
581
|
+
It takes a sudo request's command (without `sudo` and its options) and working folder, and returns `{ certain, maybe }`:
|
|
194
582
|
|
|
195
|
-
- `certain` is an absolute path when the command names a file or folder holding SLICC's secret rules (`/etc/fnox`, `/etc/fnox/config.toml`, `/etc/slicc/secrets-policy.toml`), a folder above them it changes as a whole (`/`, `/etc`, `/etc/slicc`), a glob or brace that matches one, or a file named like them (`fnox`, `secrets-policy`) anywhere, as `/tmp/fnox/config.toml`.
|
|
583
|
+
- `certain` is an absolute path when the command names a file or folder holding SLICC's secret rules (`/etc/fnox`, `/etc/fnox/config.toml`, `/etc/slicc/secrets-policy.toml`), a folder above them it changes as a whole (`/`, `/etc`, `/etc/slicc`), a glob or brace that matches one, or a file named like them (`fnox`, `secrets-policy`) anywhere, as `/tmp/fnox/config.toml`.
|
|
584
|
+
It sees through quotes, backslashes, `$'…'` and `$"…"`, `cd` and `pushd` (also inside `bash -c`), relative paths and `..`, extglobs, `/proc/self/root` and `/proc/self/cwd`, option clusters like `-t/etc/fnox`, and comma lists.
|
|
196
585
|
- `maybe` is the token when the command may reach one of them and the check can't tell where it leads: an expansion (`$X`, `$(…)`, backticks), a `~user/..`, `~+` or `~-` path, a `/proc/<pid>/cwd` or `/dev/fd` path, or a command over 4096 characters (its first 40 followed by `…`).
|
|
197
586
|
|
|
198
|
-
Both are `null` for a clean command, and at most one is set.
|
|
587
|
+
Both are `null` for a clean command, and at most one is set.
|
|
588
|
+
The dialog shows "This changes secret rules" for `certain` and "This may change secret rules" for `maybe`, and offers no Always allow for either.
|
|
589
|
+
An embedder that records "always" decisions itself, such as slicc-bios, must refuse "always" (answer "once" at most) whenever either is set, so a decision sent without the dialog can't add a rule that runs it unasked.
|
|
199
590
|
|
|
200
591
|
```js
|
|
201
592
|
import { secretRules } from './dist/slicc-ui.js';
|
|
@@ -213,68 +604,348 @@ A sprinkle's frame gets `window.slicc` with `name` and `lick(event)`. When the m
|
|
|
213
604
|
- `slicc.readFile(path)` resolves with the file's text.
|
|
214
605
|
- `slicc.exists(path)` resolves with a boolean.
|
|
215
606
|
- `slicc.getState()` returns the sprinkle's saved value, or `null`, synchronously: the frame is built with it, so `var saved = slicc.getState()` works on load.
|
|
216
|
-
- `slicc.setState(value)` updates that value at once and resolves when it is saved.
|
|
607
|
+
- `slicc.setState(value)` updates that value at once and resolves when it is saved.
|
|
608
|
+
State is JSON: what `JSON.stringify` drops or changes is gone (a `Map` becomes `{}`, `NaN` becomes `null`), and a value it can't serialize, like a cycle or a function, rejects and leaves state as it was.
|
|
609
|
+
It outlives the frame: reopening the panel or rebuilding the sprinkle starts with it.
|
|
217
610
|
|
|
218
|
-
Paths are the agent's: `/shared/…` and `/home/…`, nothing else.
|
|
611
|
+
Paths are the agent's: `/shared/…` and `/home/…`, nothing else.
|
|
612
|
+
A refusal or a missing file rejects with an `Error`.
|
|
613
|
+
There is no `exec`: a sprinkle that needs a command asks its cone with `slicc.lick()`.
|
|
614
|
+
Without `call`, none of the four exist, so guard them (`typeof slicc.readFile === 'function'`) and fall back.
|
|
219
615
|
|
|
220
616
|
### Panel command
|
|
221
617
|
|
|
222
|
-
`panelCommand(app, argv, caller)`, exported from `dist/slicc-ui.js`, runs `panel` for an agent or a terminal, so the agent can arrange the UI.
|
|
618
|
+
`panelCommand(app, argv, caller)`, exported from `dist/slicc-ui.js`, runs `panel` for an agent or a terminal, so the agent can arrange the UI.
|
|
619
|
+
It returns `{ stdout, stderr, code }`.
|
|
620
|
+
A leading `panel` in `argv` is dropped.
|
|
621
|
+
`caller` is `{ agentId, cone?, readOnly? }`: `agentId` is the calling cone or scoop, or `null` for the user at a terminal; `cone` is the caller's cone when the agent already knows it (otherwise the `parentId` chain of `agentId` finds it).
|
|
223
622
|
|
|
224
|
-
- `panel list [--json]` lists the panels the caller can reach, open or not, with their state (`active`, `shown`, `open` or `closed`) and why one is busy.
|
|
623
|
+
- `panel list [--json]` lists the panels the caller can reach, open or not, with their state (`active`, `shown`, `open` or `closed`) and why one is busy.
|
|
624
|
+
`--json` prints `PanelInfo`s: `id`, `title`, `kind` (`surface`, `chat`, `terminal`, `sprinkle`, `file`, `diff`), home `side`, `agent`, `open`, `visible`, `active` and `busy`.
|
|
225
625
|
- `panel open <id> [place] [--section <name>]` shows a panel without moving keyboard focus. `panel focus <id> [place] [--section <name>]` also gives it focus.
|
|
226
626
|
- `panel close <id>`, `panel move <id> <place>` (a closed panel opens there) and `panel reset` (the default layout).
|
|
227
|
-
- `panel tabs` and `--tab <tab>` are the agent's: it lists the SLICC tabs it can send the command to, and sends it to the one named instead of the one showing the caller's cone.
|
|
627
|
+
- `panel tabs` and `--tab <tab>` are the agent's: it lists the SLICC tabs it can send the command to, and sends it to the one named instead of the one showing the caller's cone.
|
|
628
|
+
It strips `--tab` before `panelCommand` runs; the help text names both so `panel --help` tells the whole story.
|
|
228
629
|
- A place is `left`, `center` or `right` (a group on that side as the layout is now, with the chats' column as the center, or a new one beside its neighbours), or `--left`, `--right`, `--above`, `--below` or `--within <id>`.
|
|
229
|
-
- Ids are the surface ids (`files`, `changes`, `terminal`, `monitor`, `settings`, …), `chat:<agent>`, `terminal:<id>`, `sprinkle:<id>`, `file:<path>` and `diff:<path>`.
|
|
630
|
+
- Ids are the surface ids (`files`, `changes`, `terminal`, `monitor`, `settings`, …), `chat:<agent>`, `terminal:<id>`, `sprinkle:<id>`, `file:<path>` and `diff:<path>`.
|
|
631
|
+
`chat` is the caller's cone's chat (without a caller, the selected thread's), `terminal` the active terminal, else the latest open one, else the newest, and a sprinkle can be named by its name alone.
|
|
230
632
|
|
|
231
633
|
Exit codes: 0 done, 1 no such panel, 2 usage (with the help text), 5 refused. 3 and 4 are left to the agent's transport.
|
|
232
634
|
|
|
233
|
-
With an `agentId` or a `cone`, the caller reaches the shared surfaces, the chats of its cone and its scoops, its cone's sprinkles and the built-in ones; anything else is reported as missing.
|
|
635
|
+
With an `agentId` or a `cone`, the caller reaches the shared surfaces, the chats of its cone and its scoops, its cone's sprinkles and the built-in ones; anything else is reported as missing.
|
|
636
|
+
With `readOnly` (a page that only follows), everything but `list` and `--help` is refused.
|
|
637
|
+
The command refuses (code 5) to close a terminal (its shell would stop), the last chat, or a busy panel, and to move or reset while a panel is busy.
|
|
638
|
+
A panel element tells it is busy with a `busyReason` string: Settings while signing in or entering a key, Install / Update while a package installs, a chat while a question waits.
|
|
639
|
+
For an agent, it also refuses to reopen a panel the user closed in the last 60 seconds (or to reset the layout then), and while the user is typing (a key in a text field in the last 3 seconds, or a focused field with text), `focus` only shows the panel.
|
|
640
|
+
On a phone, `move` changes nothing.
|
|
641
|
+
`app.panels` (`Panels`) is the controller under the command, with `list()`, `find(id)`, `open(id, { place, section })`, `focus`, `close`, `move(id, place)` and `reset()`, each returning `{ code, message }` and without the agent-only rules.
|
|
642
|
+
Agent changes are saved with the layout.
|
|
234
643
|
|
|
235
644
|
### Model
|
|
236
645
|
|
|
237
|
-
`SliccModel` (`src/model/types.ts`) has one port per future backend, so adapters can replace the dummy without UI changes.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
The
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
646
|
+
`SliccModel` (`src/model/types.ts`) has one port per future backend, so adapters can replace the dummy without UI changes.
|
|
647
|
+
Every port is subscribable with `on(type, listener)`, which returns an unsubscribe function.
|
|
648
|
+
|
|
649
|
+
#### `agent`
|
|
650
|
+
|
|
651
|
+
Later backed by: `@ai-ecoverse/slicc-agent`.
|
|
652
|
+
Cones and scoops, the active agent, conversations, `send` (with attachments, as a send, a steer or a queued follow-up) and `stop`, the queue, suggestions, answers to questions, permission decisions, compaction, clearing (`clear(agentId)` starts a new conversation: on a cone, a port with `freeze` first puts the old one in the Freezer as its own row, which thaws into a cone of its own; a scoop's is just reset), the model per agent and new scoops.
|
|
653
|
+
The optional `rewind(agentId, messageId)` drops the user turn before that message and everything after it, and resolves with the turn's prompt (text and attachments) for the composer, or `null`.
|
|
654
|
+
The optional `hold()` returns an `AgentHold` (`{ reason: waiting | locked, held }`) while tool results wait for the vault (slicc-agent's `connection.vault`: `waiting` while the vault port doesn't answer, `locked` while it needs a passkey), or `null`, with a `hold` event when it changes.
|
|
655
|
+
The optional `drop(agentId)` removes a scoop: it stops and its working folder is deleted, while its conversation and reports are kept, and `createCone(name)` resolves with a new cone; the dummy rejects a cone name that is taken.
|
|
656
|
+
`freeze(agentId)` is optional too: without it, **Delete cone** and `/freeze` don't show.
|
|
657
|
+
`frozen()` lists the Freezer's rows; a `FrozenCone` may carry `kind` (`cone`, `scoop` or `agent`), `live` for a conversation that's still running, and `thawedAs`, the id of the cone a scoop or agent run was thawed into.
|
|
658
|
+
`thaw(id)` brings a cone back, or forks a scoop or agent run into a new cone, and `discard(id)` removes a row.
|
|
659
|
+
Names don't have to be unique, and an optional `Agent.title` names a cone's conversation; without it, the first user message does.
|
|
660
|
+
Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`.
|
|
661
|
+
The optional `toolLabel(agentId, callIds)` resolves with a short label for a cluster of tool runs (`callIds` in order), such as "Compare the docs with the live API", or `null`; the agent dedupes, caches and persists it.
|
|
662
|
+
Spectrum asks once a cluster forms and again each time it grows, once per set of ids, and shows the label at the end of the strip.
|
|
663
|
+
The optional `readOnly()` returns a line, or `null`, with a `readOnly` event when it changes: while it's a non-empty string, such as when this follower runs a different major version from its owner, every surface is watch-only just as a follower without Control is (no composer, no cone or conversation actions, no pickers, no Stop, no Accept or Revert in Changes, inline sprinkles view only, no model settling), and the line takes the place of the follow-based watch-only note under the chat.
|
|
664
|
+
|
|
665
|
+
#### `files`
|
|
666
|
+
|
|
667
|
+
Later backed by: slicc-kernel's OPFS.
|
|
668
|
+
`list`, `read`, `write`, `remove`, and pending changes with their before and after text (`accept`, `revert`).
|
|
669
|
+
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()`.
|
|
670
|
+
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).
|
|
671
|
+
`mountFolder(path)` inserts the picked folder at a mount point that waits for one; the optional `needsFolder()` lists those mount points.
|
|
672
|
+
The port emits `mounts` and then `files` after every mount and eject, including those made from the shell, and `mounts` whenever `needsFolder()` changes.
|
|
673
|
+
|
|
674
|
+
#### `terminals`
|
|
675
|
+
|
|
676
|
+
Later backed by: slicc-kernel's terminal API.
|
|
677
|
+
Terminal sessions; `backend(id)` returns a `TerminalBackend` for `<slicc-terminal>`.
|
|
678
|
+
|
|
679
|
+
#### `browser`
|
|
680
|
+
|
|
681
|
+
Later backed by: `@ai-ecoverse/slicc-cdp`.
|
|
682
|
+
Tabs, the active tab, `open`, `navigate`, `close`, `screenshot`.
|
|
683
|
+
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.
|
|
684
|
+
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.
|
|
685
|
+
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.
|
|
686
|
+
**Stop** calls `agent.stop(agentId)`.
|
|
687
|
+
|
|
688
|
+
#### `settings`
|
|
689
|
+
|
|
690
|
+
Later backed by: local storage.
|
|
691
|
+
Theme, model, thinking level, composer and diff preferences, the models on offer, and accounts.
|
|
692
|
+
A model can say it is its provider's `default`.
|
|
693
|
+
Emit `settings` or `accounts` when the models on offer change.
|
|
694
|
+
|
|
695
|
+
#### `changes` (optional)
|
|
696
|
+
|
|
697
|
+
Later backed by: `@ai-ecoverse/slicc-agent`.
|
|
698
|
+
Pending changes from git instead of the file system: `changes()`, `accept(path)` (stage), `revert(path)` (discard), and a `changes` event, as on `files`.
|
|
699
|
+
When present, the UI reads changes only from this port, including the rail's count.
|
|
700
|
+
A `Change` may carry `repo`, its repository root; put the active agent's repository first.
|
|
701
|
+
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.
|
|
702
|
+
The optional `commands()` adds more copyable commands under that text, in order, without repeating its own.
|
|
703
|
+
|
|
704
|
+
#### `memory`
|
|
705
|
+
|
|
706
|
+
Later backed by: `@ai-ecoverse/slicc-agent`.
|
|
707
|
+
Memories with scope, section, tag and body; `save` and `remove`.
|
|
708
|
+
`source: 'notes'` marks free text the panel shows as an entry titled after its section; saving it makes it an entry.
|
|
709
|
+
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 `global` and each cone.
|
|
710
|
+
Either way the panel puts its own **Everyone** (`*`, every memory) first and labels `global` **Shared**.
|
|
711
|
+
The panel re-reads `scopes()` on `memories` and `agents`, so emit `memories` when the list changes.
|
|
712
|
+
A scope that disappears falls back to Everyone.
|
|
713
|
+
A memory remembered while Everyone is picked goes to `global`.
|
|
714
|
+
|
|
715
|
+
#### `monitor`
|
|
716
|
+
|
|
717
|
+
Later backed by: all of them.
|
|
718
|
+
A snapshot of vitals, alerts and sections, re-emitted as the system changes, and `resync`.
|
|
719
|
+
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).
|
|
720
|
+
|
|
721
|
+
#### `sprinkles`
|
|
722
|
+
|
|
723
|
+
Later backed by: `@ai-ecoverse/slicc-agent`.
|
|
724
|
+
Sprinkles (`inline` ones show as dips in the chat, not in the rail) and `send`, which turns a sprinkle's lick into a lick on its cone.
|
|
725
|
+
The rail groups sprinkles by their `agentId`: the selected cone's come first, then the other cones' in the agents list's order, each group set apart by a line (`.rail .cones`) and each button carrying `data-cone`.
|
|
726
|
+
A sprinkle of another cone reads `{cone name} / {title}` in its tooltip, its label and the tab it opens; the name comes from the agents list, so give each sprinkle an `id` that stays stable when a cone is renamed, such as `{coneId}/{name}`.
|
|
727
|
+
The optional `call(id, method, args)` serves the [sprinkle bridge](#sprinkle-bridge) for sprinkle `id`: `readFile` and `exists` (`[path]`) resolve with a string and a boolean, `getState` (`[]`) with the saved value or `null`, and `setState` (`[value]`) once it is saved.
|
|
728
|
+
The adapter owns path policy and keeps state per sprinkle; `<slicc-sprinkle>` awaits `getState` before it builds a frame, mirrors what the frame saves, and forwards only these four methods from its own frame.
|
|
729
|
+
The dummy keeps state in memory and maps `/shared/x` to `/home/x` in its files, refusing anything outside `/home`.
|
|
730
|
+
|
|
731
|
+
#### `tray`
|
|
732
|
+
|
|
733
|
+
Later backed by: the tray protocol.
|
|
734
|
+
Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`.
|
|
735
|
+
`spent`, `rate` and `budget` are optional: the budget chip's tooltip and label leave out the spend while `spent` is 0 or missing, and the rate while `rate` is, and the chip itself hides without `budget`, or while it's at 0% with nothing spent.
|
|
736
|
+
|
|
737
|
+
#### `updates` (optional)
|
|
738
|
+
|
|
739
|
+
Later backed by: BIOS installers.
|
|
740
|
+
`list()` returns `UpdateItem` rows for BIOS, kernel, agent, grammars, global pnpm packages, skills and UI.
|
|
741
|
+
`ready()` explicitly reports agent readiness, independently of agent activity.
|
|
742
|
+
Emit `items` whenever rows or readiness change.
|
|
743
|
+
Use `queued` for a row that waits for another component before it starts; it reads "Waiting" and doesn't count as updating.
|
|
744
|
+
Use `starting` for an installed row that is only starting up: it reads "Starting" without a checked line, and the header shows a neutral "Starting" only when nothing is failed, updating or ready.
|
|
745
|
+
A `failed` row with an installed `from` version reads "Update failed".
|
|
746
|
+
`act(id, action)` resolves when dispatched or rejects with a displayed error; actions are `retry`, `update-now`, `restart-agent` and `reload`.
|
|
747
|
+
Missing ports preserve existing adapters and report disconnected information in the panel.
|
|
748
|
+
Optional `packages()` returns `PackageItem` rows for optional tools, each with a short catalog `id`, its npm `package` (shown in the name's tooltip and at the top of its Install log), `label`, a one-sentence `description`, the `commands` it provides, `requires` (other package ids, read as "Uses Python" while it's installed, or "Installs Python too" on a row that isn't installed yet; an installed row leaves out a requirement that isn't installed, since its warning says so), optional `notes` (short plain-text lines shown muted under the row, such as "1 untested dependency: …"; leave it out when there's nothing to say), optional `warnings` (lines with an alert icon, `data-warnings`, such as "Needs Python, which isn't installed.
|
|
749
|
+
Install Python again to use it."; the dummy adds that one when a required package is removed), `state` (`available`, `queued`, `installing`, `installed`, `outdated`, `updating`, `removing` or `failed`), the installed `version`, the `offered` version and its `size` in bytes, `progress` counted in packages (or `null` for an indeterminate bar), `error`, `actions` (`install`, `update`, `remove`, `retry`, `unpin`) and an optional `log`.
|
|
750
|
+
A row with `origin: 'user'` (ids such as `user:agent:<name>`) goes into Your packages, `section[data-section="user-packages"]`, which only shows when there is one; its `pin` holds `base` (the version seven ships, or `null` for a package seven doesn't ship), the user's `range` and the `project` folder, such as `/opt/agent`.
|
|
751
|
+
An installed pin reads "Pinned" with `0.24.0 · seven ships 0.25.0 · in /opt/agent` ("the version seven ships" when they match), and an addition reads "Added" with `^1.2.0 → 1.2.4 · in /opt/agent`; `.meta[data-pin]` is `pinned` or `added`.
|
|
752
|
+
A pin that is `removing` reads "Unpinning"; other states keep their usual lights.
|
|
753
|
+
Offer `unpin` for pins and `remove` for additions.
|
|
754
|
+
Emit `packages` whenever they change.
|
|
755
|
+
`actPackage(id, action)` keeps the row's button pending until it settles; it shows any confirm itself, resolves without a change when that confirm is cancelled, and rejects with a displayed error on a real failure.
|
|
756
|
+
Hooks: `section[data-section="packages"]`, `li[data-id][data-state]`, buttons with `data-action`, `[data-requires]`, `[data-notes]` and `[data-error]`.
|
|
757
|
+
Real installer wiring belongs to ai-ecoverse/slicc-bios#70.
|
|
758
|
+
|
|
759
|
+
#### `network` (optional)
|
|
760
|
+
|
|
761
|
+
Later backed by: BIOS.
|
|
762
|
+
`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).
|
|
763
|
+
Emit `network` when it changes.
|
|
764
|
+
The optional `check()` tests the route again.
|
|
765
|
+
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).
|
|
766
|
+
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.
|
|
767
|
+
A rejected call shows its message in the section.
|
|
768
|
+
Peers are never named beyond the exit-node list.
|
|
769
|
+
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).
|
|
770
|
+
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`.
|
|
771
|
+
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).
|
|
772
|
+
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.
|
|
773
|
+
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**.
|
|
774
|
+
`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()`).
|
|
775
|
+
An optional `exit` (`NetworkExit`: `null`, `{ kind: 'tailnet', node }` with a Tailscale node id or `'auto'`, or `{ kind: 'link', id }`) is the default exit.
|
|
776
|
+
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.
|
|
777
|
+
Without `setRemoteControl(id, allowed)`, `control` is a line saying whether it's allowed; with it, **Allow remote control** is a switch.
|
|
778
|
+
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)`.
|
|
779
|
+
Turning View off should turn Control off too.
|
|
780
|
+
`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.
|
|
781
|
+
Their failures show in the section.
|
|
782
|
+
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.
|
|
783
|
+
`unlinked` means the leader removed this SLICC and bios already forgot it: send it in one status, then leave it out.
|
|
784
|
+
`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.
|
|
785
|
+
Without the port, the indicator doesn't show, and neither does the Network panel unless the model has a `guests` port.
|
|
786
|
+
|
|
787
|
+
#### `notices` (optional)
|
|
788
|
+
|
|
789
|
+
Later backed by: BIOS.
|
|
790
|
+
`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 }`).
|
|
791
|
+
Title, body and labels are text only.
|
|
792
|
+
Notices use `role="status"` and never take focus.
|
|
793
|
+
An action is a secondary button that stays pending until `act(id, action)` settles; a rejection shows its message under the body.
|
|
794
|
+
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.
|
|
795
|
+
Emit `notices` whenever they change.
|
|
796
|
+
A notice with `dismissible: false` has no close button and stays until the port drops it.
|
|
797
|
+
Hooks: `[data-notice="<id>"]`, buttons with `data-action`, `[data-dismiss]` and `[data-error]`.
|
|
798
|
+
|
|
799
|
+
#### `tabs` (optional)
|
|
800
|
+
|
|
801
|
+
Later backed by: BIOS.
|
|
802
|
+
`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`).
|
|
803
|
+
Emit `tabs` when it changes.
|
|
804
|
+
An owner shows nothing.
|
|
805
|
+
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.
|
|
806
|
+
While connecting, an empty chat says "Connecting to the tab running SLICC…" (`[data-connecting]`).
|
|
807
|
+
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.
|
|
808
|
+
Reload this tab." with **Reload** (`data-action="reload"`, calls `reload()`), "Another tab runs an older SLICC.
|
|
809
|
+
Reload the other SLICC tab.", or "Chrome paused the tab running SLICC.
|
|
810
|
+
Switch to it to continue.".
|
|
811
|
+
An `alone` tab shows only the brand and "SLICC is open in another tab."
|
|
812
|
+
(`[data-tabs="alone"]`).
|
|
813
|
+
|
|
814
|
+
#### `approvals` (optional)
|
|
815
|
+
|
|
816
|
+
Later backed by: BIOS.
|
|
817
|
+
`list()` returns waiting `Approval`s; emit `approvals` when the list changes.
|
|
818
|
+
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).
|
|
819
|
+
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`).
|
|
820
|
+
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).
|
|
821
|
+
Read-only commands count only by bare name or from `/bin` and `/usr/bin`.
|
|
822
|
+
`/proc/<pid>/root` counts as `/` and `/proc/self/cwd` as `cwd`.
|
|
823
|
+
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.
|
|
824
|
+
Other symlinks aren't followed.
|
|
825
|
+
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.
|
|
826
|
+
For half a second after a request shows, only Deny works, so a double-click can't approve it.
|
|
827
|
+
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.
|
|
828
|
+
Pass `decide` in every tab: the first decision wins.
|
|
829
|
+
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.
|
|
830
|
+
Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it.
|
|
831
|
+
Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "It waits for the owner's approval.
|
|
832
|
+
Denied automatically at {time}."
|
|
833
|
+
A guest request has `kind` `guest-message` (`text`, optional `attachments` of `{ name, mimeType, size }`) or `guest-tool` (`tool`, `summary`), plus `requester` (`kind` `guest`), `agentId`, optional `agentName`, `createdAt`, `expiresAt`, `always` (offer Always allow, a policy default the owner can change) and an optional `verdict` (`{ decision: 'allow' | 'deny' | 'unsure', reason }`, the automatic check's view).
|
|
834
|
+
Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); then "The automatic check would allow this: {reason}", "…deny this: {reason}" or "The automatic check wasn’t sure: {reason}" (`[data-verdict]`, the decision in the attribute).
|
|
835
|
+
It offers **Deny** and **Allow once**, and, when `always` is `true`, **Always allow** (`always`, which calls `decide(id, 'always')`) with the hint "Always allow lets {name} send messages to {agentName ?? 'this cone'} without asking again." or "Always allow lets {agentName ?? 'this cone'} run exactly this {tool} call for {name} without asking again.".
|
|
836
|
+
`decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo.
|
|
837
|
+
Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out".
|
|
838
|
+
Without `decide`, a guest request's notice reads "Waiting for approval of a guest’s message" with "{name}'s message to {agentName} waits for the owner's approval.", or "Waiting for approval of a guest’s tool call" with "{agentName} wants to run {tool} for {name}, which waits for the owner's approval.", then "Denied automatically at {time}."
|
|
839
|
+
|
|
840
|
+
#### `vault` (optional)
|
|
841
|
+
|
|
842
|
+
Later backed by: BIOS.
|
|
843
|
+
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).
|
|
844
|
+
`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`.
|
|
845
|
+
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.
|
|
846
|
+
`audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`).
|
|
847
|
+
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()`.
|
|
848
|
+
They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`).
|
|
849
|
+
Values go to the vault only through `put`, and never through another port.
|
|
850
|
+
`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.
|
|
851
|
+
The passkey lock (unlock and protect) happens in the vault's own popup, which only a click in that frame can open.
|
|
852
|
+
`notices`, `acknowledge` and the `notice` event are still typed, and ignored.
|
|
853
|
+
|
|
854
|
+
#### `guests` (optional)
|
|
855
|
+
|
|
856
|
+
Later backed by: the tray hub.
|
|
857
|
+
Guest seats on a cone.
|
|
858
|
+
`list()` returns the `GuestSeat`s (`id`, `label`, `agentId`, the cone, `createdAt`, `expiresAt` or `null`, `gates`, `connected` and `pending`, how many of their messages or tool calls wait for review); a revoked or expired seat leaves it at once.
|
|
859
|
+
Emit `seats` when it changes.
|
|
860
|
+
`mint(agentId, { label, expiresAt, gates })` resolves with the `seat` and its `url`, which the panel shows once and never keeps, and `revoke(id)` ends a seat; both reject with an `Error` whose message is a sentence to show.
|
|
861
|
+
The hub caps `expiresAt` at 30 days.
|
|
862
|
+
`gates` (`{ messages, tools }`) are fixed at mint, so changing them means revoking and minting again; each is a `GuestGate`: `user` (a person with Control approves each one), `cone` (the shared cone itself decides, messages only unless `gates()` offers it for tools), `agent` (the automatic check decides by the owner's intent and asks a person when unsure), `scoop:<name>` (that scoop decides) or `off` (no review: messages go straight in as follow-ups, and tool calls run without review; messages only unless `gates()` offers it for tools).
|
|
863
|
+
The optional `unavailable()` returns a sentence when seats can't be minted here (no tray hub), or `null`; emit `unavailable` with it when it changes.
|
|
864
|
+
The optional `gates()` returns the policy's allowed choices, `{ messages, tools }`, each a list of `GuestGate`s in display order, where `scoop:*` means each of the cone's scoops; the owner can change them, and without it the invite offers the lists above.
|
|
865
|
+
`mint` always gets a concrete gate, never `scoop:*`.
|
|
866
|
+
With this port the Network panel is offered even without a `network` port (on a follower, say), showing only Guests.
|
|
867
|
+
The dummy's `?network=off&guests=on` gives that panel, on the follow page too.
|
|
868
|
+
|
|
869
|
+
#### `seat` (optional)
|
|
870
|
+
|
|
871
|
+
Later backed by: slicc-agent.
|
|
872
|
+
Only on a guest's page: the `Seat` (`id`, `label`, `agentId`, the shared cone, `expiresAt` or `null`, and `gates`) the guest joined with.
|
|
873
|
+
While it's set, `<slicc-chat>` and `<slicc-composer>` are in guest mode.
|
|
874
|
+
The guest's model lists only the cone, its `messages()` are the cone's transcript, `send(cone, text)` proposes a message, and every other method rejects with `EACCES`; the UI calls none of them.
|
|
875
|
+
|
|
876
|
+
#### `follow` (optional)
|
|
877
|
+
|
|
878
|
+
Later backed by: BIOS.
|
|
879
|
+
Only on a follower page, a SLICC that shows another SLICC's agent over a link.
|
|
880
|
+
`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`).
|
|
881
|
+
Emit `follow` when it changes.
|
|
882
|
+
Connected without `view` means waiting to be allowed.
|
|
883
|
+
`leave()` forgets the link and the optional `retry()` tries again from `failed`.
|
|
884
|
+
Pass only the surfaces a follower has to `<slicc-app>` (agents, chat, terminal and settings) and leave out `network`.
|
|
885
|
+
|
|
886
|
+
The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`.
|
|
887
|
+
Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot.
|
|
888
|
+
Dummy Update now and Retry simulate download and link progress; Restart agent and Reload only change fixture state, never restart a real process or reload the browser.
|
|
889
|
+
`createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'off' })` (or `?packages=` on `/ui/`, off by default) adds invented optional packages in every state: Install and Retry finish after a short wait and install missing requirements too, `fail` makes them fail, `user` adds a pinned agent-kit and two additions (one with a warning) that Unpin and Remove take out of the list, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`).
|
|
890
|
+
`updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`.
|
|
891
|
+
`createDummyModel({ mounts: 'mount' | 'cancel' | 'fail' | 'needs' })` (or `?mounts=` on `/ui/`) turns on the optional mount members, which are off by default: `mountFolder` mounts invented files at `/mnt/photos`, resolves with `null`, or rejects, as `model.files.mount.scenario` says; `needs` also lists `/mnt/photos` in `needsFolder()` until a folder is inserted there.
|
|
892
|
+
`createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc on this computer as a linked device (`route: 'link'`, with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port.
|
|
893
|
+
`model.network.setScenario()` switches it and Check again re-emits after a short wait.
|
|
894
|
+
`createDummyModel({ tailnet: 'off' | 'needs-login' | 'running' | 'failed' })` (or `?tailnet=` on `/ui/`, off by default) adds an invented tailnet: an auth key starting with `tskey-` signs in, `mast-pi` and `keel-router` are online exit nodes and `buoy-laptop` is offline, and Retry on `failed` connects.
|
|
895
|
+
While the dummy tailnet runs with an exit node, the route is `tailnet` and the health `ok`.
|
|
896
|
+
`createDummyModel({ links: 'none' | 'preparing' | 'linked' | 'reconnecting' | 'exit' | 'prompt' | 'denied' | 'remote' | 'followers' })` (or `?links=` on `/ui/`, off by default) adds invented linked devices and the exit members: `linked` has quay-studio (on this computer) and pier-nuc, which can be exits and have a Chrome each, and dock-builder, which can't; quay-studio shares `sketches` and pier-nuc `proj` and a read-only `archive`, which **Mount** attaches to the dummy files at `/mnt/<source>` (with `mountInfo` naming the source) and **Eject** or the Files strip detaches; with no other browser automation, the dummy reports `via: 'link'`; `exit` sends traffic through quay-studio; `reconnecting` holds traffic for pier-nuc (health `failing`, and `ok` while the exit device is connected, both without a `detail`, as bios reports them); `remote` has pier-nuc allowed and prompting, quay-studio watching, and an offline reef-laptop that only ever connected for control; `prompt` and `denied` wait for Local Network Access, and Try again links quay-studio.
|
|
897
|
+
New join URL changes the URL, and `model.network.setLinks()` switches scenarios.
|
|
898
|
+
`createDummyModel({ leaders: 'connected' | 'failed' | 'unlinked' | 'disconnected' | 'two' })` (or `?leaders=` on `/ui/`, off by default) adds invented leaders and their members: cove-studio connected, marsh-desk failed with an error, cove-studio unlinked (gone on the next status), cove-studio disconnected, or both cove-studio and marsh-desk; Retry and Reconnect move a leader to connecting, then connected, Disconnect makes it disconnected and Forget drops it, each after a short wait.
|
|
899
|
+
`networkFixtures`, `tailnetFixtures`, `linksFixtures`, `leadersFixtures`, `joinUrl` and `DummyNetwork` are exported from `/dummy`.
|
|
900
|
+
`createDummyModel({ guests: 'on' | 'none' | 'unavailable' | 'fail' | 'policy' })` (or `?guests=` on `/ui/`, off by default) adds an invented `guests` port: `on` has Seat 1 (Mara) on sliccy, connected with 2 waiting and 5 days left, and Seat 2 (Ole) on harbor, not connected and without expiry; `none` has no seats; `unavailable` has none and says guests need the tray hub, so minting rejects; `fail` makes `mint` reject with a sentence; and `policy` has the `on` seats and a `gates()` (`guestsPolicy`) that also allows tools `off`.
|
|
901
|
+
`mint` adds a seat after a short wait and resolves with an invented `https://www.example.com/guest/…` URL, `revoke` drops the seat, and `model.guests.setScenario()` switches scenarios.
|
|
902
|
+
`guestsFixtures`, `guestsPolicy`, `guestsUnavailable`, `guestsRefused` and `DummyGuests` are exported from `/dummy`.
|
|
903
|
+
`createDummyModel({ changes: 'files' | 'git' | 'nogit' })` (or `?changes=` on `/ui/`, default `files`) adds a git `changes` port: `git` lists the pending changes under `/workspace/harbor` and `/workspace/skills` with their `repo` and no author, and `nogit` reports git as unavailable.
|
|
904
|
+
`DummyChanges` is exported from `/dummy`.
|
|
905
|
+
`createDummyModel({ notices: 'storage' | 'mixed' | 'fail' | 'off' })` (or `?notices=` on `/ui/`, off by default) adds an invented storage warning with Try again, which clears it after a short wait (`mixed` adds an informative notice without actions, and `fail` makes Try again fail).
|
|
906
|
+
`noticeFixtures` and `DummyNotices` are exported from `/dummy`.
|
|
907
|
+
`createDummyModel({ browser: 'idle' | 'driving' | 'off' })` (or `?browser=` on `/ui/`, default `idle`) sets the dummy browser: `idle` has a history of invented actions (about 180 on harbor's tab, enough for Show more) and no driven tab, `driving` has harbor and quiet-otter driving two tabs, with harbor's last click still running and frames every 500 ms while watched, and `off` has no tabs.
|
|
908
|
+
Stopping an agent releases its tabs.
|
|
909
|
+
Like bios, `actions()` keeps the latest 500 per tab from the last 24 hours, 5,000 in total (`actionLimits`).
|
|
910
|
+
`DummyBrowser`, `actionLimits`, `browserHistory` and `page` are exported from `/dummy`.
|
|
911
|
+
`createDummyModel({ sprinkles: 'builtin' | 'cones' })` (or `?sprinkles=` on `/ui/`, default `builtin`) adds, with `cones`, invented sprinkles of other cones: a Board in harbor and one in tide-tables, and a Changelog in release-notes.
|
|
912
|
+
`createDummyModel({ tabs: 'owner' | 'follower' | 'connecting' | 'stalled' | 'newer' | 'older' | 'alone' | 'off' })` (or `?tabs=` on `/ui/`, off by default) adds a tabs port in that state; `model.tabs.setScenario()` switches it, and Reload connects again and becomes a follower after a short wait.
|
|
913
|
+
`tabsFixtures` and `DummyTabs` are exported from `/dummy`.
|
|
914
|
+
|
|
915
|
+
`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.
|
|
916
|
+
Leave counts in `model.follow.left` (the `/ui/` page then drops `?follow=`), and `model.follow.setScenario()` switches it.
|
|
917
|
+
`followFixtures` and `DummyFollow` are exported from `/dummy`.
|
|
918
|
+
`?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.
|
|
919
|
+
|
|
920
|
+
`createGuestModel({ seat: 'review' | 'reject' | 'off' | 'expiring', history: 'on' | 'none' | 'twin' })` (the `/guest/` page, with `?seat=`, `?history=` and `?color=`, shows `<slicc-chat>` alone) is a guest's model on sliccy for Seat 2 (Mara): `review` gates messages through the host, so a sent message waits in `queue()` (id `g1`, `g2`, …) and is then declined, leaving the notice "It was declined." with its text, `reject` gates them through the automatic check, so `send` rejects with "Messages from guests need a review, and this agent can't review them yet.", `off` takes them as follow-ups that sliccy answers, and `expiring` reviews with a link that ends in a few hours.
|
|
921
|
+
Its invented transcript has the host's, Mara's and Seat 3 (Jonas)'s messages, tool runs, an inline sprinkle, an answered and an open question, a pending permission request and an error with Retry; `none` starts empty, and `twin` adds a message from another seat with Mara's label.
|
|
922
|
+
Every other method, and every other port's, rejects with `EACCES` and is recorded in `model.refused`.
|
|
923
|
+
`createGuestModel`, `DummyGuestAgent` and its fixtures are exported from `/dummy`.
|
|
924
|
+
|
|
925
|
+
`createDummyModel({ readonly: 'version' })` (`?readonly=version` on `/ui/`) makes the agent port read-only with an invented line, "Watch only.
|
|
926
|
+
The SLICC you follow runs a newer version.
|
|
927
|
+
Update this one to send prompts.", and shows a warning notice without a close button (`[data-notice="readonly"]`) in place of any other notices.
|
|
928
|
+
`model.agent.setReadOnly(text)` changes it, and `null` ends it.
|
|
268
929
|
|
|
269
930
|
`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`).
|
|
270
931
|
|
|
271
|
-
`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.
|
|
932
|
+
`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.
|
|
933
|
+
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.
|
|
272
934
|
|
|
273
|
-
`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."
|
|
935
|
+
`createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' | 'secrets' | 'guest' | 'guest-follower' | 'guest-always' })` (`?approvals=` on `/ui/`) adds waiting sudo requests: one from an agent, one from a terminal user, three in a row (the last as `www-data`), one without `decide`, one whose `decide` fails, one that expires in 24 seconds, or two whose first is answered in another tab or withdrawn after a few seconds, or one from an agent that copies a file over `/etc/fnox/config.toml`, or (`guest`) three guest requests: a message from Seat 2 (Mara) with attachments and an allow verdict, a tool call with a deny verdict, and a message whose `decide` rejects with "Someone else already answered this."
|
|
936
|
+
(`guest-follower`: the same without `decide`), or (`guest-always`) one guest tool call from Seat 2 (Mara) to `read_file` with `always: true`.
|
|
937
|
+
Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
|
|
274
938
|
|
|
275
|
-
`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).
|
|
939
|
+
`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).
|
|
940
|
+
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.
|
|
941
|
+
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.
|
|
942
|
+
`model.vault.fail(code, message)` makes the next op fail.
|
|
943
|
+
Its buttons are plain Spectrum buttons that answer at once: Unlock unlocks, and created buttons are in `model.vault.buttons`.
|
|
944
|
+
`vaultFixtures`, `DummyVault` and `DummyVaultButton` are exported from `/dummy`.
|
|
945
|
+
`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.
|
|
276
946
|
|
|
277
|
-
On phones the update status and the tray connection show only their status light (the tray's budget is hidden) and the network indicator a cloud icon, with the text in their labels and tooltips; the agent picker keeps room for the cone's name, cut with an ellipsis when long, and the bottom rail scrolls when needed.
|
|
947
|
+
On phones the update status and the tray connection show only their status light (the tray's budget is hidden) and the network indicator a cloud icon, with the text in their labels and tooltips; the agent picker keeps room for the cone's name, cut with an ellipsis when long, and the bottom rail scrolls when needed.
|
|
948
|
+
The update status comes first so progress and failures remain reachable on narrow screens.
|
|
278
949
|
|
|
279
950
|
### Kernel model
|
|
280
951
|
|
|
@@ -292,24 +963,49 @@ app.surfaces = surfaces.filter((item) => item.id === 'files' || item.id === 'ter
|
|
|
292
963
|
app.model = createKernelModel({ kernel, root, storage: localStorage, files: { skip: ['/node_modules'] } });
|
|
293
964
|
```
|
|
294
965
|
|
|
295
|
-
- **`KernelFiles`** lists the OPFS root as the tree, reads and writes files through the OPFS API, and removes them recursively.
|
|
296
|
-
|
|
297
|
-
|
|
966
|
+
- **`KernelFiles`** lists the OPFS root as the tree, reads and writes files through the OPFS API, and removes them recursively.
|
|
967
|
+
Directories in `skip` show up but aren't scanned; paths in `hide` (and everything under them) don't show up at all.
|
|
968
|
+
`start()` rescans on every `FileSystemObserver` record, or every `interval` ms (default 2000) where the browser can't observe OPFS, and emits `files` and a `file` per changed file, so open tabs follow what the terminal writes.
|
|
969
|
+
Without an agent there are no pending changes.
|
|
970
|
+
- **`KernelTerminals`** opens `bash -i` (or `argv`) in `cwd` (default `/home`) as `user` (default the kernel's, `root`) through `kernelBackend`, one session per terminal, as many as the user opens.
|
|
971
|
+
`open` (default 1) is how many it starts with.
|
|
972
|
+
- Everything else is idle (`IdleAgent`, `IdleBrowser`, `IdleMemory`, `IdleMonitor`, `IdleSprinkles`, `IdleTray`, from `idleModel(storage)`): empty lists, and actions that need a backend throw.
|
|
973
|
+
Settings are kept in `storage` under `slicc-agent.settings` (exported as `settingsKey`), the agent's key, so the theme chosen in the app is the one the page boots with; when that key is missing they are read once from the older `slicc-ui.settings`.
|
|
974
|
+
Leave their surfaces out of `surfaces`.
|
|
298
975
|
|
|
299
976
|
Each section is its own port, so an embedder can mix real and dummy per section, for example `{ ...createDummyModel(), files: new KernelFiles(root) }`.
|
|
300
977
|
|
|
301
978
|
`<slicc-file-view>` has **Edit**: the file opens in a plain text area, and **Save** or `Mod+S` writes it through `files.write`; `Escape` cancels.
|
|
302
979
|
|
|
303
|
-
The `kitchen-sink` cone holds every kind of message, content and lick in one conversation, for design work and screenshots.
|
|
980
|
+
The `kitchen-sink` cone holds every kind of message, content and lick in one conversation, for design work and screenshots.
|
|
981
|
+
Its clusters carry invented labels, which the dummy's `toolLabel` answers after a short pause, keyed by the title of the cluster's first call; one is long enough to be cut.
|
|
982
|
+
Each reply to it is paced to take about 20 seconds, so its streaming states can be looked at.
|
|
983
|
+
A prompt that says search or grep runs a search.
|
|
984
|
+
Its tool turn has two clusters and a standalone run with URL, file and plain commands in every state, and two dummy browser tabs carry invented `data:` favicons.
|
|
985
|
+
The dummy's fixtures are invented: a small forecast API called harbor, with cones, scoops, a conversation per agent, pending changes (one of them binary, without a diff), browser tabs and accounts.
|
|
986
|
+
Its only sprinkles are SLICC's own: `welcome` (the onboarding wizard, as a dip in the sliccy cone) and `suggestions`, copied from `slicc` (`packages/vfs-root/shared/sprinkles/`, commit `aa29784`) with their comments stripped, as is the sprinkle theme in `src/app/sprinkle-theme.css`.
|
|
987
|
+
`suggestions` reads its stream from the invented `/home/.gelatiere/suggestions.json` through the bridge; there is no `/home/.welcomed`, so `welcome` starts its wizard.
|
|
988
|
+
Its replies are scripted by keyword (debug or investigate, tests, fix or add, open or docs, files, anything else), and their tool calls act on the other ports: an edit writes a file and shows up as a change, a browse opens a tab.
|
|
989
|
+
Debugging runs seven commands in a row, so its cluster grows past five while it streams.
|
|
304
990
|
|
|
305
991
|
### Distribution
|
|
306
992
|
|
|
307
|
-
The UI is ESM with code splitting: `slicc-ui.js`, `slicc-dummy.js` and `chunk-*.js` next to them in `dist/`, with no bare imports, so the folder can be served as plain files from OPFS. dockview's ESM build has no CSS; the build takes it from dockview's UMD bundle.
|
|
993
|
+
The UI is ESM with code splitting: `slicc-ui.js`, `slicc-dummy.js` and `chunk-*.js` next to them in `dist/`, with no bare imports, so the folder can be served as plain files from OPFS. dockview's ESM build has no CSS; the build takes it from dockview's UMD bundle.
|
|
994
|
+
`@pierre/diffs` highlights with Shiki.
|
|
995
|
+
`dist/` carries the grammars SLICC needs most (CSS, diff, HTML, JavaScript, JSON, JSX, Markdown, Python, shell, SQL, TOML, TSX, TypeScript, XML, YAML); every other grammar and theme stays out of `dist/` and loads the first time a file needs it.
|
|
996
|
+
It loads from `grammarBase` first, if set, and then from `https://cdn.jsdelivr.net/npm/@shikijs/` (pinned to the installed version, CORS and CORP headers set).
|
|
997
|
+
For offline use, install the optional peer dependencies `@shikijs/langs` and `@shikijs/themes` (pinned to the same version; plain relative ESM, no bare imports), serve them, and point `grammarBase` (on `<slicc-app>`, or `setGrammarBase()`) at the folder holding `langs/` and `themes/`, for example `/node_modules/@shikijs/`.
|
|
998
|
+
The build fails if the peer versions drift from the installed ones.
|
|
999
|
+
`SLICC_GRAMMAR_BASE` points the build at another CDN.
|
|
1000
|
+
`dist/` is about 55 files and 5.5 MB of JavaScript.
|
|
1001
|
+
The build fails if React is bundled; `@pierre/trees` lists React as a peer dependency, so npm installs it, but the `web-components` entry runs on Preact.
|
|
1002
|
+
Types are in `dist/types/ui.d.ts`, `dist/types/dummy.d.ts` and `dist/types/kernel.d.ts`, exported as `@ai-ecoverse/slicc-spectrum/ui`, `/dummy` and `/kernel`.
|
|
308
1003
|
|
|
309
1004
|
### dockview notes
|
|
310
1005
|
|
|
311
1006
|
- Keyboard navigation (`keyboardNavigation`) needs dockview-enterprise, so `<slicc-dock>` implements <kbd>F6</kbd> and <kbd>Ctrl</kbd>+<kbd>]</kbd>/<kbd>[</kbd> itself.
|
|
312
|
-
- A floating group with a border (`--dv-floating-border`, which dockview's own light theme sets) grows by twice the border width on every save and restore.
|
|
1007
|
+
- A floating group with a border (`--dv-floating-border`, which dockview's own light theme sets) grows by twice the border width on every save and restore.
|
|
1008
|
+
The theme draws the edge with a box shadow instead.
|
|
313
1009
|
|
|
314
1010
|
## Development
|
|
315
1011
|
|
|
@@ -319,19 +1015,32 @@ npm run lint
|
|
|
319
1015
|
npm test
|
|
320
1016
|
```
|
|
321
1017
|
|
|
322
|
-
`npm test` builds `dist/` and runs the integration tests in headless Chromium over raw CDP, through the [harness from slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness), against a fake backend (`test/integration/page/fake-backend.js`).
|
|
1018
|
+
`npm test` builds `dist/` and runs the integration tests in headless Chromium over raw CDP, through the [harness from slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness), against a fake backend (`test/integration/page/fake-backend.js`).
|
|
1019
|
+
It writes V8 coverage to `coverage/` and CPU profiles, screenshots and console logs to `artifacts/`.
|
|
1020
|
+
The test page is served cross-origin isolated (COOP/COEP), as slicc-kernel requires.
|
|
1021
|
+
`test/integration/kernel.test.mjs` runs `bash -i` end to end on `@ai-ecoverse/slicc-kernel` (a pinned dev dependency), with `@ai-ecoverse/wasm-bash` and `@ai-ecoverse/wasm-coreutils` installed into OPFS by the page (`/kernel.html`).
|
|
1022
|
+
`npm start` serves the test page on port 8080, and the UI at `/ui/`.
|
|
1023
|
+
`test/integration/kernel-ui.test.mjs` boots `<slicc-app>` on the kernel model (`/kernel-ui/`): a command in the terminal, its file in the tree, an edit in a file tab read back in the terminal, a second terminal, and a reload.
|
|
1024
|
+
`test/integration/ui.test.mjs` covers the shell, the layout (moving, closing, floating, reopening and reloading), streaming a reply with a tool call, stopping it, switching themes and the keyboard, and saves screenshots to `artifacts/ui/`.
|
|
323
1025
|
|
|
324
1026
|
The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web), which also provides the `slicc-lint-comments`, `slicc-no-unit-tests` and `slicc-diff-cover` commands used by `npm run lint` and the pre-commit hook.
|
|
325
1027
|
|
|
326
|
-
Unit tests live in `test/unit/`, which is gitignored.
|
|
1028
|
+
Unit tests live in `test/unit/`, which is gitignored.
|
|
1029
|
+
The pre-commit hook runs them in Node against happy-dom under monocart and fails the commit unless the staged lines in `src/` are fully covered (`slicc-diff-cover`, which runs `diff-cover --fail-under 100` and needs [uv](https://docs.astral.sh/uv/)).
|
|
327
1030
|
|
|
328
1031
|
## Design review
|
|
329
1032
|
|
|
330
|
-
[`.github/workflows/design-review.yml`](.github/workflows/design-review.yml) has Claude review every ready, same-repo pull request visually, against Spectrum 2 and general design guidelines, and leave a comment with screenshots.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
- **
|
|
334
|
-
|
|
1033
|
+
[`.github/workflows/design-review.yml`](.github/workflows/design-review.yml) has Claude review every ready, same-repo pull request visually, against Spectrum 2 and general design guidelines, and leave a comment with screenshots.
|
|
1034
|
+
It also runs on `workflow_dispatch` with `pr_number`; fork pull requests and drafts are skipped.
|
|
1035
|
+
|
|
1036
|
+
- **Setup** (`CERT_RUNNER`): main and the pull request are each built and served by `test/integration/chrome.mjs`, and each is open in a Chrome with a window under Xvfb, so hover works: main on CDP 9301 (`:8301/ui/?delay=0`), the pull request on CDP 9302 (`:8302`).
|
|
1037
|
+
Claude drives both with `playwright-core`, shoots the same states side by side (light and dark, desktop and a 420×860 mobile viewport), and reads spectrum.adobe.com, seeded with every URL in its sitemap.
|
|
1038
|
+
- **What it checks:** tokens only, never colour alone, contrast in dark, focus, copy, typography, icons, layout at 420, and the `data-*` hooks embedders test on.
|
|
1039
|
+
The prompt carries the setup pitfalls too: wait for fonts, blur focus, compare at the same scroll anchor, and use a real mobile viewport.
|
|
1040
|
+
- **The comment:** Claude writes `comment.md` and its screenshots, uploaded as the run's `design-review-comment-<pr>` and `design-review-shots-<pr>` artifacts.
|
|
1041
|
+
A separate `post` job on a fresh runner, the only one with the Cloudflare token, puts the screenshots in the R2 bucket slicc uses for PR screenshots (`slicc-pr-screenshots`, under `slicc-spectrum/pr-<n>/<run>/`) and posts the comment with them inline from its public URL.
|
|
1042
|
+
A screenshot that doesn't upload is linked to the artifact instead.
|
|
1043
|
+
The token is the org secret `CLOUDFLARE_API_TOKEN`; `CLOUDFLARE_ACCOUNT_ID`, `R2_BUCKET` and `R2_PUBLIC_BASE_URL` default to slicc's values.
|
|
335
1044
|
|
|
336
1045
|
## License
|
|
337
1046
|
|