@ai-ecoverse/slicc-spectrum 1.51.0 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/dist/slicc-dummy.js +173 -19
- package/dist/slicc-dummy.js.map +4 -4
- package/dist/slicc-ui.js +3972 -2861
- package/dist/slicc-ui.js.map +4 -4
- package/dist/types/app/follow.d.ts +60 -0
- package/dist/types/app/network.d.ts +10 -2
- package/dist/types/app/qr.d.ts +6 -0
- package/dist/types/dummy/follow.d.ts +16 -0
- package/dist/types/dummy/index.d.ts +4 -1
- package/dist/types/dummy/network.d.ts +5 -2
- package/dist/types/dummy.d.ts +2 -0
- package/dist/types/model/types.d.ts +30 -0
- package/dist/types/ui.d.ts +2 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -131,13 +131,14 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
|
|
|
131
131
|
|
|
132
132
|
| Element | |
|
|
133
133
|
| :--- | :--- |
|
|
134
|
-
| `<slicc-app>` | The shell: header with the agent picker, status items, the update status, the network indicator, the tray, the View menu and the theme switch, rails, and a `<slicc-dock>`. Saves the dock layout per screen class to `storage` (default `localStorage`) under `layoutKey`. While connected it sets `overscroll-behavior` to `none !important` on the page's `html` and `body`, so only scroll areas inside panels rubber-band. When the last app on the page is removed, the page's own inline values come back. |
|
|
134
|
+
| `<slicc-app>` | The shell: header with the agent picker, status items, the update status, the network indicator, the tray, the View menu and the theme switch, rails, and a `<slicc-dock>`. Saves the dock layout per screen class to `storage` (default `localStorage`) under `layoutKey`. While connected it sets `overscroll-behavior` to `none !important` on the page's `html` and `body`, so only scroll areas inside panels rubber-band. When the last app on the page is removed, the page's own inline values come back. With a `follow` port, the header shows a `<slicc-follow>` chip, and while the follower connects, waits to be allowed, is unlinked or fails, a `<slicc-follow page>` (`.alone[data-follow]` with the page kind) takes the place of the panels; without Terminal access, the terminal surface and New terminal aren't offered, and open terminals close when it's taken away. |
|
|
135
135
|
| `<slicc-dock>` | Generic. [dockview-core](https://dockview.dev/) 8 with a theme mapped to Spectrum tokens, in its own shadow root. Panels come from `factories` (`name → (params) => HTMLElement`). `open`, `close`, `has`, `content`, `focusPanel`, `cycleGroup`, `cycleTab`, `toJSON`, `restore`, `clear`, and the full `api`. Tabs have a context menu (close, close others, float, maximize). Events: `layout-change`, `active-panel-change`, `panel-close`. |
|
|
136
136
|
| `<slicc-agents>` | Cones with their scoops, status, context fill and unread count. A cone's row has a second line with its conversation title, as does its item in the header's cone picker: `Agent.title`, or else the first line of the first user message, or "No messages yet". When cones share a name, their accessible names and the Delete cone, Freezer and confirmation labels add the title, as in "Delete cone sliccy (Hello from Bedrock)". Selecting one makes it the active agent. |
|
|
137
|
-
| `<slicc-chat>` | One thread's conversation (its `agent`, or the active agent if unset), by day: user messages (with attachments, tagged as steered or follow-up by `delivered`, or delegated from another cone), assistant messages (Markdown with tables, task lists, quotes, code and images; thinking; tool calls, grouped into a collapsible cluster from three in a row, each with its input (a shell command after `$`), an optional `meta` such as a timeout, an edit's `diff` shown as a diff view, and an `image`, such as a screenshot, shown as a thumbnail under the closed card and in full when open; media galleries with images, video and audio; action cards for tools, pull requests and status; plans and checklists; inline diffs; question prompts; delegations; link previews; errors with a fix-it action; a content-filter stop offers **Drop the last turn**, which rewinds that turn and puts its prompt back into the composer; when the turns right before it failed too, the button reads **Drop N failed turns** and rewinds to before the first of them, putting the latest prompt back), standalone tool results, system messages (compaction markers, error cards, notices) and licks, SLICC's events into a cone, on every channel; a lick's `severity` (`warn` or `error`) turns its channel badge into a notice or negative one with an alert icon. Permission requests are allowed or denied in place. The header shows the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false`, the thinking picker, the context meter and **New conversation**, which calls `clear` and puts focus back in the composer; on a cone, when the port can freeze, its tooltip says the old conversation stays in the Freezer. Between the header and the transcript, a `<slicc-notices>` shows the model's notices. |
|
|
137
|
+
| `<slicc-chat>` | One thread's conversation (its `agent`, or the active agent if unset), by day: user messages (with attachments, tagged as steered or follow-up by `delivered`, or delegated from another cone), assistant messages (Markdown with tables, task lists, quotes, code and images; thinking; tool calls, grouped into a collapsible cluster from three in a row, each with its input (a shell command after `$`), an optional `meta` such as a timeout, an edit's `diff` shown as a diff view, and an `image`, such as a screenshot, shown as a thumbnail under the closed card and in full when open; media galleries with images, video and audio; action cards for tools, pull requests and status; plans and checklists; inline diffs; question prompts; delegations; link previews; errors with a fix-it action; a content-filter stop offers **Drop the last turn**, which rewinds that turn and puts its prompt back into the composer; when the turns right before it failed too, the button reads **Drop N failed turns** and rewinds to before the first of them, putting the latest prompt back), standalone tool results, system messages (compaction markers, error cards, notices) and licks, SLICC's events into a cone, on every channel; a lick's `severity` (`warn` or `error`) turns its channel badge into a notice or negative one with an alert icon. Permission requests are allowed or denied in place. The header shows the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false`, the thinking picker, the context meter and **New conversation**, which calls `clear` and puts focus back in the composer; on a cone, when the port can freeze, its tooltip says the old conversation stays in the Freezer. Between the header and the transcript, a `<slicc-notices>` shows the model's notices. On a follower without Control, a note (`p[data-follow="watch"]`) takes the composer's place, the header shows the model's name instead of the pickers and New conversation, and there's no suggested follow-up. |
|
|
138
138
|
| `<slicc-notices>` | The model's `notices` as in-line alerts with their actions and a close button, after a banner from the `tabs` port when another tab is paused or runs a different SLICC, and a warning for each waiting approval when the `approvals` port has no `decide` (`[data-approval-notice]`); renders nothing without any. |
|
|
139
139
|
| `<slicc-approvals>` | The model's waiting `approvals` as a modal alert dialog, one at a time, oldest first; `<slicc-app>` mounts it. Renders nothing without `approvals.decide`. |
|
|
140
|
-
| `<slicc-
|
|
140
|
+
| `<slicc-follow>` | The model's `follow` state. By default a header chip (`#chip[data-follow]` with the link state: **Connecting to**, **Following**, **Reconnecting to**, **Unlinked from** or **Can’t reach** the owner; `compact` keeps only a follow icon and the light) whose popover says what this device can do (`[data-can]`), that the conversation may be out of date while reconnecting, and with Control that sending waits (`[data-reconnecting]`), and has **Leave** (`data-action="leave"`, behind a confirm unless unlinked). With `page`, the full page for `connecting`, `waiting` (connected without View: "Waiting for {owner} to allow this device"), `unlinked` and `failed` (`[data-follow-page]`), with **Try again** (`data-action="retry"`) when failed and the port has `retry`, and **Leave**. A rejected call shows its message (`[data-error]`). Renders nothing without the port. |
|
|
141
|
+
| `<slicc-composer>` | The message box. <kbd>Enter</kbd> sends, or steers a running reply: the message joins after the current tool round, without interrupting; <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>Enter</kbd> queues a follow-up that runs after the reply and can be withdrawn; <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line; <kbd>Tab</kbd> takes the suggested follow-up; <kbd>↑</kbd>/<kbd>↓</kbd> at the start or end walk this agent's history; <kbd>Esc</kbd> stops. `/` completes commands (`/clear`, `/compact`, `/model`, `/thinking`, `/scoop`, `/stop`, `/freeze` when the port can freeze, `/theme`) and their arguments, `@` mentions agents and files. Attach from the computer, from SLICC's files, a browser screenshot or a shared secret, or paste and drop files (25 MB each). Queued messages wait above it, to send now or drop. Model and thinking pickers, and dictation where the browser has speech recognition. Drafts are kept per agent. On a follower while the link reconnects, the field is disabled and says so; the draft stays. |
|
|
141
142
|
| `<slicc-files>` | The file system as a `<slicc-file-tree>`, with pending changes as git status. Opening a file opens it in a tab. With `files.mountFolder`, **Mount a folder…** (`data-action="mount-folder"`) mounts a local folder, then selects and expands it in the tree; with `files.mounts`, a **Mounted** strip lists the mount points, each with **Eject** (`data-action="eject"`, `data-path` the mount point) when the port has `eject`. With `files.needsFolder`, a **Needs a folder** section lists the mount points waiting for a folder, each with **Insert folder…** (`data-action="insert-folder"`, `data-id` the mount point), which calls `mountFolder(path)`. A failed mount, insert or eject shows its message under the bar. |
|
|
142
143
|
| `<slicc-file-view>` | One file (`path`), highlighted, with its pending change and a link to the diff. |
|
|
143
144
|
| `<slicc-changes>` | Pending changes with their author. Opens the diff, accepts or reverts one change or all of them. Changes with a `repo` show their folder relative to it, and no author when `agentId` is `null`; they group under a header per repository (its path and count), even when there is only one, in the order the port returns them. In a diff panel narrower than 480 px, Accept and Revert (`data-action="accept"`, `data-action="revert"`) show only their icons. Revert and Revert all ask with a destructive confirm that says the uncommitted (or, without a repo, pending) changes can't be undone; a failed revert shows its message under the bar. When the port's `unavailable()` returns a string, the panel shows it instead of the list, with each `backtick` span as inline code in the text's color and, below, each command once with a **Copy** button (`data-command`, `data-action="copy-command"`). |
|
|
@@ -150,7 +151,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
|
|
|
150
151
|
| `<slicc-monitor>` | The live monitor: vitals with sparklines (active agents, spend, budget, fullest context), alerts, and the topology (cones and scoops, terminals, tabs, pending changes, sprinkles, tray followers). |
|
|
151
152
|
| `<slicc-freezer>` | 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. A live row offers **Open**, which selects that agent; a stopped one offers **Thaw** and **Remove**, which deletes its working folders after a confirmation. A thawed scoop or agent run keeps its row, which says what it was thawed as. **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. Either way its conversation stays here, and a row without a `kind` counts as a cone. |
|
|
152
153
|
| `<slicc-sprinkle>` | A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit. 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`. 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()`). 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)). Panel sprinkles sit at the top of the right rail with their Lucide icon. |
|
|
153
|
-
| `<slicc-network>` | 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-node, through a linked device's browser, that slicc-node has no browser attached when `route` is `proxy` 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) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and `npx @ai-ecoverse/slicc-node` with a copy button (`data-action="copy-command"`), folded away when the route already reaches the whole web (`proxy`, `extension` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line names the 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. 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. While a linked device controls the agent, an indicator (`data-remote`, `prompt` or `watch`) sits before it, **Controlled by** or **Watched by** the device's name (or the number of devices;
|
|
154
|
+
| `<slicc-network>` | 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-node, through a linked device's browser, that slicc-node has no browser attached when `route` is `proxy` 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 `npx @ai-ecoverse/slicc-node` with a copy button (`data-action="copy-command"`), folded away when the route already reaches the whole web (`proxy`, `extension` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line names the 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. 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. 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. Offered only when the model has a `network` port. |
|
|
154
155
|
| `<slicc-tray>` | The tray indicator in the header: connection, the float's name, followers and budget; its panel adds role, runtime, spend, the follower list, the join link, and disconnect or reconnect. |
|
|
155
156
|
| `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
|
|
156
157
|
| `<slicc-prompt>` | The name dialog `prompt()` opens. Not for direct use. |
|
|
@@ -208,12 +209,15 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
|
|
|
208
209
|
| `sprinkles` | `@ai-ecoverse/slicc-agent` | 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. 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. 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. The dummy keeps state in memory and maps `/shared/x` to `/home/x` in its files, refusing anything outside `/home`. |
|
|
209
210
|
| `tray` | the tray protocol | Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`. |
|
|
210
211
|
| `updates` (optional) | BIOS installers | `list()` returns `UpdateItem` rows for BIOS, kernel, agent, grammars, global pnpm packages, skills and UI. `ready()` explicitly reports agent readiness, independently of agent activity. Emit `items` whenever rows or readiness change. Use `queued` for a row that waits for another component before it starts; it reads "Waiting" and doesn't count as updating. 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. A `failed` row with an installed `from` version reads "Update failed". `act(id, action)` resolves when dispatched or rejects with a displayed error; actions are `retry`, `update-now`, `restart-agent` and `reload`. Missing ports preserve existing adapters and report disconnected information in the panel. 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. 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`) and an optional `log`. Emit `packages` whenever they change. `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. Hooks: `section[data-section="packages"]`, `li[data-id][data-state]`, buttons with `data-action`, `[data-requires]`, `[data-notes]` and `[data-error]`. Real installer wiring belongs to ai-ecoverse/slicc-bios#70. |
|
|
211
|
-
| `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`proxy` for slicc-node, `extension` for the Chrome extension relay, `tailnet` while a Tailscale exit node carries the traffic, `page` for the page's own fetch, limited by CORS, or `null`), `health` (`ok`, `limited` or `failing`, decided by the port), a one-line `detail`, recent `failures` (`{ url, error, at }`, newest first), an optional `extensionUrl` and an optional `browser` (`{ via, detail? }`, `via` being `extension`, `proxy`, `link` when linked devices' browsers carry it (the line names the browser and its device, or counts them when there are several, counting connected devices only), or `null` when browser automation isn't available; a legacy `declined` flag is ignored, since there is no consent prompt any more). Emit `network` when it changes. The optional `check()` tests the route again. An optional `tailnet` (`TailnetStatus`) shows the Tailscale section: `state` (`off`, `loading`, `needs-login`, `starting`, `running` or `failed`, a status light), a plain-language `detail` (progress, or the failure reason in an error box with **Retry** through `check()`), a `loginUrl` (**Sign in to Tailscale**, a new tab), the `node` (`{ name, addresses }`; the name and the first 100.x address, each with a Copy button), `exitNode` (the name in use or `null`), `exitNodes` (`{ id, name, online }`), `autoExitNode`, `shieldsUp` (a muted line when `true`, a warning when `false`) and `peers` (a count). The optional `setTailnet(on)` adds the **Use Tailscale** switch (`data-action="tailnet"`); `submitAuthKey(key)` adds an auth-key field to `needs-login` (a password field without a `name`, cleared on submit, never kept in component state); `setExitNode(id | 'auto' | null)` adds the **Exit node** picker (None, Automatic, and each node, offline ones disabled) to `running`, with a line saying where traffic goes; `logoutTailnet()` adds **Sign out**, behind a confirm. A rejected call shows its message in the section. Peers are never named beyond the exit-node list. An optional `links` (`NetworkLinks`) shows **Linked devices**, slicc CLIs linked over WebRTC: `joinUrl` and `joinCommand` (each with Copy, `data-copy="join-url"` and `data-copy="join-command"`; both `null` shows "Preparing a join URL…"), `devices` and `permission`. A `LinkedDevice` has an `id`, a display `name
|
|
212
|
+
| `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`proxy` for slicc-node, `extension` for the Chrome extension relay, `tailnet` while a Tailscale exit node carries the traffic, `page` for the page's own fetch, limited by CORS, or `null`), `health` (`ok`, `limited` or `failing`, decided by the port), a one-line `detail`, recent `failures` (`{ url, error, at }`, newest first), an optional `extensionUrl` and an optional `browser` (`{ via, detail? }`, `via` being `extension`, `proxy`, `link` when linked devices' browsers carry it (the line names the browser and its device, or counts them when there are several, counting connected devices only), or `null` when browser automation isn't available; a legacy `declined` flag is ignored, since there is no consent prompt any more). Emit `network` when it changes. The optional `check()` tests the route again. An optional `tailnet` (`TailnetStatus`) shows the Tailscale section: `state` (`off`, `loading`, `needs-login`, `starting`, `running` or `failed`, a status light), a plain-language `detail` (progress, or the failure reason in an error box with **Retry** through `check()`), a `loginUrl` (**Sign in to Tailscale**, a new tab), the `node` (`{ name, addresses }`; the name and the first 100.x address, each with a Copy button), `exitNode` (the name in use or `null`), `exitNodes` (`{ id, name, online }`), `autoExitNode`, `shieldsUp` (a muted line when `true`, a warning when `false`) and `peers` (a count). The optional `setTailnet(on)` adds the **Use Tailscale** switch (`data-action="tailnet"`); `submitAuthKey(key)` adds an auth-key field to `needs-login` (a password field without a `name`, cleared on submit, never kept in component state); `setExitNode(id | 'auto' | null)` adds the **Exit node** picker (None, Automatic, and each node, offline ones disabled) to `running`, with a line saying where traffic goes; `logoutTailnet()` adds **Sign out**, behind a confirm. A rejected call shows its message in the section. Peers are never named beyond the exit-node list. An optional `links` (`NetworkLinks`) shows **Linked devices**, slicc CLIs linked over WebRTC: `joinUrl` and `joinCommand` (each with Copy, `data-copy="join-url"` and `data-copy="join-command"`; both `null` shows "Preparing a join URL…"), an optional `followUrl` (the page URL with `#follow=` and the join URL, shown with a QR code), `devices` and `permission`. A `LinkedDevice` has an `id`, a display `name` (a leading `slicc` is shown as `SLICC`), the kernel `host` and `address`, a `mode` (`local` for a CLI on this computer attached over CDP, or `remote`), `offers` (`net`, `http`, `ssh`; `ssh` adds a copyable `ssh <host>`), a one-line `policy`, `exit` (whether it can carry internet traffic), a `state` (`connecting`, `connected`, `reconnecting` or `offline`, for a device that linked before and isn't there now; every device that ever linked is listed, so one that was refused can be allowed), optional `control` (`{ allowed, fixed? }`; `fixed` for one on this computer, always allowed), optional `controlling` (`prompt` or `watch` while a CLI prompts or watches the agent) optional `browsers` (`{ runtime, name }`, listed in the row as `data-browsers`, with the runtime such as `pier-nuc/chrome` that the 9222 facade drives), optional `kind` (`cli`, or `seven` for another SLICC that follows this one, "Follower" in the row), optional `access` (`{ view?, control?, terminal?, fixed? }`: only the keys present get a switch, so a CLI sends `{ control }`; `fixed` lists them as always allowed from this computer; it replaces `control`), optional `asking` (a follower that waits to be allowed) and optional `terminals` (how many terminals it has open here). Each device is an `li[data-device]` with `data-state`, `data-mode` and `data-kind`, and `data-asking` while it asks; once there are devices, the join details fold into **Link another device**. `permission` is `prompt` (an informative line asking to choose Allow in Chrome's Local Network Access prompt) or `denied` (a warning, with **Try again**, `data-action="retry-links"`, through `retryLinks()`). An optional `exit` (`NetworkExit`: `null`, `{ kind: 'tailnet', node }` with a Tailscale node id or `'auto'`, or `{ kind: 'link', id }`) is the default exit. The optional `setExit(exit)` replaces the Tailscale **Exit node** picker with one **Internet traffic** picker in Connection: Directly, then Tailscale (Automatic and its nodes) and Linked devices (those with `exit: true`), offline ones disabled unless chosen; a chosen device that isn't connected shows that traffic waits, since the exit fails closed. Without `setRemoteControl(id, allowed)`, `control` is a line saying whether it's allowed; with it, **Allow remote control** is a switch. Without `setAccess(id, 'view' \| 'control' \| 'terminal', allowed)`, `access` is a line saying what's allowed; with it, the switches call it, and **Allow** on a device that asks calls `setAccess(id, 'view', true)`. Turning View off should turn Control off too. `unlink(id)` adds **Unlink** (`data-action="unlink"`) to each device and `rotateJoinUrl()` adds **New join URL** (`data-action="rotate-join-url"`), both behind a confirm. Their failures show in the section. Without the port, the indicator and the Network panel don't show. |
|
|
212
213
|
| `notices` (optional) | BIOS | `list()` returns `Notice`s for quiet, dismissible in-line notices at the top of every chat panel, above the transcript, in list order: `id`, `tone` (`info` reads as informative, `warning` as a notice), `title`, a plain-text `body` and `actions` (`{ id, label }`). Title, body and labels are text only. Notices use `role="status"` and never take focus. An action is a secondary button that stays pending until `act(id, action)` settles; a rejection shows its message under the body. The close button, labelled "Dismiss: <title>", hides the notice at once and calls `dismiss(id)` synchronously; it shows again only if the port drops the notice from `list()` and brings it back later. Emit `notices` whenever they change. Hooks: `[data-notice="<id>"]`, buttons with `data-action`, `[data-dismiss]` and `[data-error]`. |
|
|
213
214
|
| `tabs` (optional) | BIOS | `state()` returns this tab's `TabsState`: `role` (`owner` runs SLICC; `follower` shows the owner tab's SLICC; `connecting` while the owner boots or hands over; `alone` without SharedWorker), `stalled` (the owner stopped answering) and `skew` (`newer` or `older` when the owner runs another SLICC version, else `null`). Emit `tabs` when it changes. An owner shows nothing. A follower gets a quiet header light, "Another tab" (`[data-tabs][data-role]`, with `data-stalled` and `data-skew`; a dot on phones), info while connecting, and a notice while stalled or skewed, with its explanation as the tooltip. While connecting, an empty chat says "Connecting to the tab running SLICC…" (`[data-connecting]`). Skew, or else a stall, shows a warning banner at the top of every chat panel (`[data-tabs-notice="skew"\|"stalled"]`, `data-skew`): "Another tab runs a newer SLICC. Reload this tab." with **Reload** (`data-action="reload"`, calls `reload()`), "Another tab runs an older SLICC. Reload the other SLICC tab.", or "Chrome paused the tab running SLICC. Switch to it to continue.". An `alone` tab shows only the brand and "SLICC is open in another tab." (`[data-tabs="alone"]`). |
|
|
214
215
|
| `approvals` (optional) | BIOS | `list()` returns waiting `Approval`s; emit `approvals` when the list changes. A `sudo` approval has `id`, `requester` (`kind` `agent` or `user`, `name`, optional `agentId` and `where`, such as a terminal's title), `command` (without `sudo`; bios joins the kernel's argv shell-quoted), optional `target` (the user to run as; shown as `sudo -u {target}` unless `root`), optional `cwd`, `createdAt`, `expiresAt` (epoch ms) and `always` (offer Always allow, which covers the requester's user and this exact command). With `decide(id, 'once' \| 'always' \| 'deny')`, which resolves once the decision is recorded and then drops the request from `list()`, the tab shows a warning alert dialog (`dialog[data-approval][data-kind][data-requester]`): "Allow sudo?", "{name} wants to run this with sudo[ in {where}]:" for an agent or "You ran this with sudo[ in {where}] (as {name}):" for a user, the command, "In {cwd}", 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`). For half a second after a request shows, only Deny works, so a double-click can't approve it. A rejected `decide` shows its message in the dialog. Pass `decide` in every tab: the first decision wins. When a request ends without a decision in this tab, emit `ended` with `{ id, reason }` and drop it from `list()`: `answered` (another tab decided) shows "Answered in another tab", `expired` shows "Sudo request timed out", each with the command and **Close** (`data-action="close"`, `dialog[data-gone]`), and `withdrawn` (the requester went away) closes the dialog silently. Without `ended`, a request that leaves the list reads as expired past `expiresAt` and answered before it. Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "Answer it in the tab running SLICC. Denied automatically at {time}." |
|
|
216
|
+
| `follow` (optional) | BIOS | Only on a follower page, a SLICC that shows another SLICC's agent over a link. `state()` returns the `FollowState`: `owner` (its display name), `link` (`connecting` for the first connection, `connected`, `reconnecting` after a connection dropped, `unlinked` when the owner unlinked this device, or `failed`), `access` (`{ view, control, terminal }`, what the owner allows this device), an optional `name` (this device's name on the owner, for the waiting page) and an optional `error` (shown when `failed`). Emit `follow` when it changes. Connected without `view` means waiting to be allowed. `leave()` forgets the link and the optional `retry()` tries again from `failed`. Pass only the surfaces a follower has to `<slicc-app>` (agents, chat, terminal and settings) and leave out `network`. |
|
|
215
217
|
|
|
216
|
-
The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`. Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot. 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. `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | '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, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`). `updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`. `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. `createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc-node (with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port. `model.network.setScenario()` switches it and Check again re-emits after a short wait. `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. While the dummy tailnet runs with an exit node, the route is `tailnet` and the health `ok`. `createDummyModel({ links: 'none' | 'preparing' | 'linked' | 'reconnecting' | 'exit' | 'prompt' | 'denied' | 'remote' })` (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; 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. New join URL changes the URL, and `model.network.setLinks()` switches scenarios. `networkFixtures`, `tailnetFixtures`, `linksFixtures`, `joinUrl` and `DummyNetwork` are exported from `/dummy`. `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. `DummyChanges` is exported from `/dummy`. `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). `noticeFixtures` and `DummyNotices` are exported from `/dummy`. `createDummyModel({ browser: 'idle' | 'driving' | 'off' })` (or `?browser=` on `/ui/`, default `idle`) sets the dummy browser: `idle` has a history of invented actions 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. Stopping an agent releases its tabs. `DummyBrowser`, `browserHistory` and `page` are exported from `/dummy`. `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. `tabsFixtures` and `DummyTabs` are exported from `/dummy`.
|
|
218
|
+
The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`. Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot. 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. `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | '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, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`). `updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`. `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. `createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc-node (with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port. `model.network.setScenario()` switches it and Check again re-emits after a short wait. `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. While the dummy tailnet runs with an exit node, the route is `tailnet` and the health `ok`. `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; 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. New join URL changes the URL, and `model.network.setLinks()` switches scenarios. `networkFixtures`, `tailnetFixtures`, `linksFixtures`, `joinUrl` and `DummyNetwork` are exported from `/dummy`. `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. `DummyChanges` is exported from `/dummy`. `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). `noticeFixtures` and `DummyNotices` are exported from `/dummy`. `createDummyModel({ browser: 'idle' | 'driving' | 'off' })` (or `?browser=` on `/ui/`, default `idle`) sets the dummy browser: `idle` has a history of invented actions 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. Stopping an agent releases its tabs. `DummyBrowser`, `browserHistory` and `page` are exported from `/dummy`. `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. `tabsFixtures` and `DummyTabs` are exported from `/dummy`.
|
|
219
|
+
|
|
220
|
+
`createDummyModel({ follow: 'connecting' | 'waiting' | 'watch' | 'control' | 'full' | 'reconnecting' | 'unlinked' | 'failed' })` (`?follow=` on `/ui/`, which then leaves out the network port and offers only agents, chat, terminal and settings) makes the page a follower of SLICC on quay-studio: connecting, waiting to be allowed, View only, View and Control, everything, reconnecting, unlinked, or failed, where Try again connects with everything allowed. Leave counts in `model.follow.left` (the `/ui/` page then drops `?follow=`), and `model.follow.setScenario()` switches it. `followFixtures` and `DummyFollow` are exported from `/dummy`. `?links=followers` lists quay-studio (a CLI on this computer, Control always allowed), pier-nuc (a remote CLI, prompting), bay-laptop (a follower with everything allowed, watching, a terminal open), reef-phone (View only, watching) and tide-tablet (asking to follow); every links scenario has a follow link.
|
|
217
221
|
|
|
218
222
|
`createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' })` (`?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. Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
|
|
219
223
|
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -324,6 +324,56 @@ var DummyChanges = class {
|
|
|
324
324
|
}
|
|
325
325
|
};
|
|
326
326
|
|
|
327
|
+
// src/dummy/follow.ts
|
|
328
|
+
var owner = "SLICC on quay-studio";
|
|
329
|
+
var name = "SLICC on reef-phone";
|
|
330
|
+
function followFixtures(scenario) {
|
|
331
|
+
const none = { view: false, control: false, terminal: false };
|
|
332
|
+
const full = { view: true, control: true, terminal: true };
|
|
333
|
+
const base = { owner, name };
|
|
334
|
+
if (scenario === "connecting") return { ...base, link: "connecting", access: none };
|
|
335
|
+
if (scenario === "waiting") return { ...base, link: "connected", access: none };
|
|
336
|
+
if (scenario === "watch") return { ...base, link: "connected", access: { ...none, view: true } };
|
|
337
|
+
if (scenario === "control")
|
|
338
|
+
return { ...base, link: "connected", access: { ...full, terminal: false } };
|
|
339
|
+
if (scenario === "reconnecting") return { ...base, link: "reconnecting", access: full };
|
|
340
|
+
if (scenario === "unlinked") return { ...base, link: "unlinked", access: none };
|
|
341
|
+
if (scenario === "failed")
|
|
342
|
+
return {
|
|
343
|
+
...base,
|
|
344
|
+
link: "failed",
|
|
345
|
+
access: none,
|
|
346
|
+
error: `Couldn\u2019t reach ${owner}. It may be closed or offline.`
|
|
347
|
+
};
|
|
348
|
+
return { ...base, link: "connected", access: full };
|
|
349
|
+
}
|
|
350
|
+
var DummyFollow = class extends Emitter {
|
|
351
|
+
#state;
|
|
352
|
+
#time;
|
|
353
|
+
left = 0;
|
|
354
|
+
constructor(scenario, time) {
|
|
355
|
+
super();
|
|
356
|
+
this.#state = followFixtures(scenario);
|
|
357
|
+
this.#time = time;
|
|
358
|
+
}
|
|
359
|
+
state() {
|
|
360
|
+
return this.#state;
|
|
361
|
+
}
|
|
362
|
+
setScenario(scenario) {
|
|
363
|
+
this.#state = followFixtures(scenario);
|
|
364
|
+
this.emit("follow", this.#state);
|
|
365
|
+
}
|
|
366
|
+
async leave() {
|
|
367
|
+
await this.#time.sleep(4);
|
|
368
|
+
this.left++;
|
|
369
|
+
}
|
|
370
|
+
async retry() {
|
|
371
|
+
this.setScenario("connecting");
|
|
372
|
+
await this.#time.sleep(20);
|
|
373
|
+
this.setScenario("full");
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
327
377
|
// src/dummy/script.ts
|
|
328
378
|
var units = "/workspace/harbor/src/lib/units.ts";
|
|
329
379
|
var kelvin = `
|
|
@@ -899,11 +949,11 @@ var DummyAgent = class extends Emitter {
|
|
|
899
949
|
}
|
|
900
950
|
async ready() {
|
|
901
951
|
}
|
|
902
|
-
createScoop(parentId,
|
|
952
|
+
createScoop(parentId, name2) {
|
|
903
953
|
const parent = this.#agent(parentId);
|
|
904
954
|
const agent = {
|
|
905
955
|
id: this.#id("scoop"),
|
|
906
|
-
name,
|
|
956
|
+
name: name2,
|
|
907
957
|
kind: "scoop",
|
|
908
958
|
parentId: parent?.kind === "cone" ? parent.id : parent?.parentId ?? null,
|
|
909
959
|
status: "idle",
|
|
@@ -918,7 +968,7 @@ var DummyAgent = class extends Emitter {
|
|
|
918
968
|
id: this.#id("m"),
|
|
919
969
|
role: "lick",
|
|
920
970
|
channel: "scoop-idle",
|
|
921
|
-
title:
|
|
971
|
+
title: name2,
|
|
922
972
|
text: "New scoop, waiting for its first task",
|
|
923
973
|
createdAt: Date.now()
|
|
924
974
|
});
|
|
@@ -937,8 +987,8 @@ var DummyAgent = class extends Emitter {
|
|
|
937
987
|
if (this.#active === agentId) this.select(agent.parentId ?? this.#agents[0].id);
|
|
938
988
|
this.#changed();
|
|
939
989
|
}
|
|
940
|
-
async createCone(
|
|
941
|
-
const trimmed =
|
|
990
|
+
async createCone(name2) {
|
|
991
|
+
const trimmed = name2.trim();
|
|
942
992
|
if (!trimmed) throw new Error("A cone needs a name");
|
|
943
993
|
if (this.#agents.some((agent2) => agent2.kind === "cone" && agent2.name === trimmed)) {
|
|
944
994
|
throw new Error(`A cone named ${trimmed} already exists`);
|
|
@@ -2419,8 +2469,8 @@ function tone(seconds = 0.6, frequency = 440, rate = 8e3) {
|
|
|
2419
2469
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2420
2470
|
return `data:audio/wav;base64,${btoa(binary)}`;
|
|
2421
2471
|
}
|
|
2422
|
-
function tool(id,
|
|
2423
|
-
return { id, name, title, input, output, status: "done", paths: [], ...extra };
|
|
2472
|
+
function tool(id, name2, title, input, output, extra = {}) {
|
|
2473
|
+
return { id, name: name2, title, input, output, status: "done", paths: [], ...extra };
|
|
2424
2474
|
}
|
|
2425
2475
|
var markdown = [
|
|
2426
2476
|
"## The forecast API in one page",
|
|
@@ -4129,12 +4179,100 @@ var linkedDevices = [
|
|
|
4129
4179
|
function joinUrl(rotation = 0) {
|
|
4130
4180
|
return `https://join.slicc.example/l/${rotation ? `r${rotation}-` : ""}7Hq2kd9FXw3m`;
|
|
4131
4181
|
}
|
|
4182
|
+
function followUrl(rotation = 0) {
|
|
4183
|
+
return `https://seven.slicc.example/#follow=${joinUrl(rotation)}`;
|
|
4184
|
+
}
|
|
4185
|
+
var followers = [
|
|
4186
|
+
{
|
|
4187
|
+
id: "link-quay",
|
|
4188
|
+
name: "slicc on quay-studio",
|
|
4189
|
+
host: "quay-studio.slicc.internal",
|
|
4190
|
+
address: "198.18.57.11",
|
|
4191
|
+
mode: "local",
|
|
4192
|
+
offers: ["net", "http", "ssh"],
|
|
4193
|
+
exit: true,
|
|
4194
|
+
state: "connected",
|
|
4195
|
+
kind: "cli",
|
|
4196
|
+
access: { control: true, fixed: true }
|
|
4197
|
+
},
|
|
4198
|
+
{
|
|
4199
|
+
id: "link-pier",
|
|
4200
|
+
name: "slicc on pier-nuc",
|
|
4201
|
+
host: "pier-nuc.slicc.internal",
|
|
4202
|
+
address: "198.18.57.12",
|
|
4203
|
+
mode: "remote",
|
|
4204
|
+
offers: ["net", "ssh"],
|
|
4205
|
+
exit: true,
|
|
4206
|
+
state: "connected",
|
|
4207
|
+
kind: "cli",
|
|
4208
|
+
access: { control: true },
|
|
4209
|
+
controlling: "prompt"
|
|
4210
|
+
},
|
|
4211
|
+
{
|
|
4212
|
+
id: "link-bay",
|
|
4213
|
+
name: "SLICC on bay-laptop",
|
|
4214
|
+
host: "",
|
|
4215
|
+
address: "",
|
|
4216
|
+
mode: "remote",
|
|
4217
|
+
offers: [],
|
|
4218
|
+
exit: false,
|
|
4219
|
+
state: "connected",
|
|
4220
|
+
kind: "seven",
|
|
4221
|
+
access: { view: true, control: true, terminal: true },
|
|
4222
|
+
controlling: "watch",
|
|
4223
|
+
terminals: 1
|
|
4224
|
+
},
|
|
4225
|
+
{
|
|
4226
|
+
id: "link-reef",
|
|
4227
|
+
name: "SLICC on reef-phone",
|
|
4228
|
+
host: "",
|
|
4229
|
+
address: "",
|
|
4230
|
+
mode: "remote",
|
|
4231
|
+
offers: [],
|
|
4232
|
+
exit: false,
|
|
4233
|
+
state: "connected",
|
|
4234
|
+
kind: "seven",
|
|
4235
|
+
access: { view: true, control: false, terminal: false },
|
|
4236
|
+
controlling: "watch"
|
|
4237
|
+
},
|
|
4238
|
+
{
|
|
4239
|
+
id: "link-tide",
|
|
4240
|
+
name: "SLICC on tide-tablet",
|
|
4241
|
+
host: "",
|
|
4242
|
+
address: "",
|
|
4243
|
+
mode: "remote",
|
|
4244
|
+
offers: [],
|
|
4245
|
+
exit: false,
|
|
4246
|
+
state: "connected",
|
|
4247
|
+
kind: "seven",
|
|
4248
|
+
asking: true,
|
|
4249
|
+
access: { view: false, control: false, terminal: false }
|
|
4250
|
+
}
|
|
4251
|
+
];
|
|
4252
|
+
function grant(device, key, allowed) {
|
|
4253
|
+
const access = { ...device.access, [key]: allowed };
|
|
4254
|
+
if (key === "view" && !allowed && "control" in access) access.control = false;
|
|
4255
|
+
const next = { ...device, access, asking: device.asking && !access.view };
|
|
4256
|
+
if (!access.view && !access.control) next.controlling = void 0;
|
|
4257
|
+
else if (!access.control && next.controlling === "prompt") next.controlling = "watch";
|
|
4258
|
+
if (!access.terminal) next.terminals = void 0;
|
|
4259
|
+
return next;
|
|
4260
|
+
}
|
|
4132
4261
|
function linksFixtures(scenario, rotation = 0) {
|
|
4133
4262
|
const url = joinUrl(rotation);
|
|
4134
|
-
const base = {
|
|
4263
|
+
const base = {
|
|
4264
|
+
joinUrl: url,
|
|
4265
|
+
joinCommand: `npx sliccy ${url} follow`,
|
|
4266
|
+
followUrl: followUrl(rotation),
|
|
4267
|
+
permission: null
|
|
4268
|
+
};
|
|
4135
4269
|
if (scenario === "preparing") {
|
|
4136
|
-
return {
|
|
4270
|
+
return {
|
|
4271
|
+
links: { ...base, joinUrl: null, joinCommand: null, followUrl: null, devices: [] },
|
|
4272
|
+
exit: null
|
|
4273
|
+
};
|
|
4137
4274
|
}
|
|
4275
|
+
if (scenario === "followers") return { links: { ...base, devices: followers }, exit: null };
|
|
4138
4276
|
if (scenario === "none") return { links: { ...base, devices: [] }, exit: null };
|
|
4139
4277
|
if (scenario === "prompt") {
|
|
4140
4278
|
return { links: { ...base, devices: [], permission: "prompt" }, exit: null };
|
|
@@ -4185,6 +4323,7 @@ var DummyNetwork = class extends Emitter {
|
|
|
4185
4323
|
setExit;
|
|
4186
4324
|
unlink;
|
|
4187
4325
|
setRemoteControl;
|
|
4326
|
+
setAccess;
|
|
4188
4327
|
rotateJoinUrl;
|
|
4189
4328
|
retryLinks;
|
|
4190
4329
|
constructor(scenario, clock2, tailnet, links) {
|
|
@@ -4197,6 +4336,7 @@ var DummyNetwork = class extends Emitter {
|
|
|
4197
4336
|
this.setExit = (exit) => this.#setExit(exit);
|
|
4198
4337
|
this.unlink = (id) => this.#unlink(id);
|
|
4199
4338
|
this.setRemoteControl = (id, allowed) => this.#control(id, allowed);
|
|
4339
|
+
this.setAccess = (id, key, allowed) => this.#access(id, key, allowed);
|
|
4200
4340
|
this.rotateJoinUrl = () => this.#rotate();
|
|
4201
4341
|
this.retryLinks = () => this.#retry();
|
|
4202
4342
|
}
|
|
@@ -4268,6 +4408,15 @@ var DummyNetwork = class extends Emitter {
|
|
|
4268
4408
|
this.#links = { ...links, devices };
|
|
4269
4409
|
this.#emit();
|
|
4270
4410
|
}
|
|
4411
|
+
async #access(id, key, allowed) {
|
|
4412
|
+
await this.#clock.sleep(4);
|
|
4413
|
+
const links = this.#links;
|
|
4414
|
+
const devices = links.devices.map(
|
|
4415
|
+
(device) => device.id === id ? grant(device, key, allowed) : device
|
|
4416
|
+
);
|
|
4417
|
+
this.#links = { ...links, devices };
|
|
4418
|
+
this.#emit();
|
|
4419
|
+
}
|
|
4271
4420
|
async #unlink(id) {
|
|
4272
4421
|
await this.#clock.sleep(4);
|
|
4273
4422
|
const links = this.#links;
|
|
@@ -4282,7 +4431,8 @@ var DummyNetwork = class extends Emitter {
|
|
|
4282
4431
|
this.#links = {
|
|
4283
4432
|
...this.#links,
|
|
4284
4433
|
joinUrl: url,
|
|
4285
|
-
joinCommand: `npx sliccy ${url} follow
|
|
4434
|
+
joinCommand: `npx sliccy ${url} follow`,
|
|
4435
|
+
followUrl: followUrl(this.#rotation)
|
|
4286
4436
|
};
|
|
4287
4437
|
this.#emit();
|
|
4288
4438
|
}
|
|
@@ -4546,8 +4696,8 @@ var FakeShell = class {
|
|
|
4546
4696
|
const names = [...entries].filter(
|
|
4547
4697
|
([candidate]) => candidate.startsWith(prefix) && !candidate.slice(prefix.length).includes("/")
|
|
4548
4698
|
).map(([candidate, kind]) => {
|
|
4549
|
-
const
|
|
4550
|
-
return kind === "directory" ? `\x1B[1;34m${
|
|
4699
|
+
const name2 = candidate.slice(prefix.length);
|
|
4700
|
+
return kind === "directory" ? `\x1B[1;34m${name2}\x1B[0m` : name2;
|
|
4551
4701
|
});
|
|
4552
4702
|
if (path !== "/" && !entries.has(path))
|
|
4553
4703
|
this.#print(`ls: cannot access '${target}': No such file or directory`);
|
|
@@ -4600,12 +4750,12 @@ var FakeShell = class {
|
|
|
4600
4750
|
}
|
|
4601
4751
|
}
|
|
4602
4752
|
async #run(line) {
|
|
4603
|
-
const [
|
|
4604
|
-
if (!
|
|
4753
|
+
const [name2, ...args] = line.trim().split(/\s+/);
|
|
4754
|
+
if (!name2) return;
|
|
4605
4755
|
this.#history.push(line);
|
|
4606
|
-
const command = this.#commands[
|
|
4756
|
+
const command = this.#commands[name2];
|
|
4607
4757
|
if (command) await command(args);
|
|
4608
|
-
else this.#print(`bash: ${
|
|
4758
|
+
else this.#print(`bash: ${name2}: command not found`);
|
|
4609
4759
|
}
|
|
4610
4760
|
#key(char) {
|
|
4611
4761
|
if (this.#escape) {
|
|
@@ -4652,8 +4802,8 @@ var FakeShell = class {
|
|
|
4652
4802
|
}
|
|
4653
4803
|
resize() {
|
|
4654
4804
|
}
|
|
4655
|
-
signal(
|
|
4656
|
-
if (
|
|
4805
|
+
signal(name2) {
|
|
4806
|
+
if (name2 === "SIGHUP") {
|
|
4657
4807
|
this.#exit();
|
|
4658
4808
|
return;
|
|
4659
4809
|
}
|
|
@@ -5067,6 +5217,7 @@ function createDummyModel({
|
|
|
5067
5217
|
notices = "off",
|
|
5068
5218
|
tabs: tabs2 = "off",
|
|
5069
5219
|
approvals = "off",
|
|
5220
|
+
follow = "off",
|
|
5070
5221
|
browser: scenario = "idle"
|
|
5071
5222
|
} = {}) {
|
|
5072
5223
|
const time = clock(delay);
|
|
@@ -5118,7 +5269,8 @@ function createDummyModel({
|
|
|
5118
5269
|
...changes === "files" ? {} : { changes: new DummyChanges(files3, changes === "git") },
|
|
5119
5270
|
...notices === "off" ? {} : { notices: new DummyNotices(notices, time) },
|
|
5120
5271
|
...tabs2 === "off" ? {} : { tabs: new DummyTabs(tabs2, time) },
|
|
5121
|
-
...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time) }
|
|
5272
|
+
...approvals === "off" ? {} : { approvals: new DummyApprovals(approvals, time) },
|
|
5273
|
+
...follow === "off" ? {} : { follow: new DummyFollow(follow, time) }
|
|
5122
5274
|
};
|
|
5123
5275
|
return { ...base, monitor: new DummyMonitor(base) };
|
|
5124
5276
|
}
|
|
@@ -5126,6 +5278,7 @@ export {
|
|
|
5126
5278
|
DummyApprovals,
|
|
5127
5279
|
DummyBrowser,
|
|
5128
5280
|
DummyChanges,
|
|
5281
|
+
DummyFollow,
|
|
5129
5282
|
DummyNetwork,
|
|
5130
5283
|
DummyNotices,
|
|
5131
5284
|
DummyTabs,
|
|
@@ -5133,6 +5286,7 @@ export {
|
|
|
5133
5286
|
approvalFixtures,
|
|
5134
5287
|
browserHistory,
|
|
5135
5288
|
createDummyModel,
|
|
5289
|
+
followFixtures,
|
|
5136
5290
|
joinUrl,
|
|
5137
5291
|
linksFixtures,
|
|
5138
5292
|
networkFixtures,
|