@ai-ecoverse/slicc-spectrum 1.43.1 → 1.45.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 +4 -4
- package/dist/slicc-dummy.js +238 -99
- package/dist/slicc-dummy.js.map +4 -4
- package/dist/slicc-ui.js +4955 -4086
- package/dist/slicc-ui.js.map +4 -4
- package/dist/types/app/browser.d.ts +26 -2
- package/dist/types/app/network.d.ts +1 -1
- package/dist/types/app/reach.d.ts +15 -0
- package/dist/types/dummy/browser.d.ts +16 -3
- package/dist/types/dummy/index.d.ts +4 -1
- package/dist/types/dummy.d.ts +2 -0
- package/dist/types/model/types.d.ts +28 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
|
|
|
142
142
|
| `<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"`). |
|
|
143
143
|
| `<slicc-diff-panel>` | The diff of one pending change (`path`), unified or split, with accept and revert. A change with `before` and `after` both `null` (a binary file, or text over 1 MB) shows “No diff for this file” and Open the file instead, and keeps Accept and Revert. |
|
|
144
144
|
| `<slicc-terminals>` | Terminal sessions in tabs, each a `<slicc-terminal>` on the session's `TerminalBackend`. Sessions stay alive while hidden; `exit` closes the tab. |
|
|
145
|
-
| `<slicc-browser>` |
|
|
145
|
+
| `<slicc-browser>` | What the agents' browser control is doing. It opens on the most recently driven tab (`.focused`), or on the tab overview: cards (`li[data-id]`, `data-controlled` while an agent drives the tab, `data-pulse` when another tab starts being driven, without switching to it) with a thumbnail, the agent and a small **Stop <agent>** (`data-action="stop-agent"`), and an address bar to open a URL. The focused tab has **All tabs** (`data-action="all-tabs"`), a tab picker (a list of tabs at 720 px and wider), the URL with **Copy** (`data-action="copy-url"`) and **Open** (`data-action="open-tab"`, activates the tab), who's using it (`.driving[data-driving=agent|user|none]`; a controlled tab without an agent reads **In use** and has no Stop) with **Stop**, 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), folded into **Recent actions (n)** below 480 px with the last one showing. Labels, URLs and errors are text only. With no tabs and `browser.via` `null` in the network status, it says browser control isn't connected (`data-browser="none"`) with the extension link and the slicc-node command. The Browser rail button carries a dot (`data-driven`) while a tab is in use. |
|
|
146
146
|
| `<slicc-settings>` | Theme, the model and thinking level for new cones, composer and diff preferences, and accounts to connect, reconnect or disconnect. An account whose status is `signing-in` (or a non-API-key account while its `connect` is pending) reads “Signing in…”; when the port has `cancel(id)`, its row offers Cancel (`data-action="cancel-sign-in"`), and the rejected `connect` that follows shows no error. |
|
|
147
147
|
| `<slicc-updates>` | Install / Update: component states, version changes, last checks, download and link counts, action buttons, failures with Retry, and collapsible install logs. Opens at boot until the agent is ready without failures. 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. Every automatic close is saved to the layout. 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. Also available through View and the rails. |
|
|
148
148
|
| `<slicc-memory>` | What agents remember, per scope (everyone, a cone or a role), grouped by section: search, filter by tag, expand, edit, add and forget. Free text outside an entry shows “From MEMORY.md”. |
|
|
@@ -199,18 +199,18 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
|
|
|
199
199
|
| `agent` | `@ai-ecoverse/slicc-agent` | 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. 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`. 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. `freeze(agentId)` is optional too: without it, **Delete cone** and `/freeze` don't show. `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. `thaw(id)` brings a cone back, or forks a scoop or agent run into a new cone, and `discard(id)` removes a row. Names don't have to be unique, and an optional `Agent.title` names a cone's conversation; without it, the first user message does. Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`. |
|
|
200
200
|
| `files` | slicc-kernel's OPFS | `list`, `read`, `write`, `remove`, and pending changes with their before and after text (`accept`, `revert`). The optional `mountFolder()` asks for a local folder and resolves with its mount point (for example `/mnt/photos`), or `null` when the user cancels; the panel calls it synchronously in the click, so the port can call `showDirectoryPicker()`. The optional `mounts()` lists the mount points, and `eject(path)` unmounts one (the folder stays on disk). `mountFolder(path)` inserts the picked folder at a mount point that waits for one; the optional `needsFolder()` lists those mount points. The port emits `mounts` and then `files` after every mount and eject, including those made from the shell, and `mounts` whenever `needsFolder()` changes. |
|
|
201
201
|
| `terminals` | slicc-kernel's terminal API | Terminal sessions; `backend(id)` returns a `TerminalBackend` for `<slicc-terminal>`. |
|
|
202
|
-
| `browser` | `@ai-ecoverse/slicc-cdp` | Tabs, the active tab, `open`, `navigate`, `close`, `screenshot`. |
|
|
202
|
+
| `browser` | `@ai-ecoverse/slicc-cdp` | Tabs, the active tab, `open`, `navigate`, `close`, `screenshot`. A `BrowserTab` may say it's `controlled` (an agent, or the user's terminal when `agentId` is `null`, is driving it now; `agentId` is the agent of its last action); `favicon` is ignored. The optional `actions(tabId?)` returns recent `BrowserAction`s (`{ id, tabId, agentId, kind, target?, value?, length?, status: running \| done \| failed, error?, at }`, `kind` one of `open`, `goto`, `back`, `reload`, `close`, `select`, `snapshot`, `screenshot`, `eval`, `click`, `fill`, `type`, `press`, `scroll`, `request`; a `request` has `value` `'GET https://host/path'` and may have no tab), with an `action` event per new or settled action. The optional `watch(tabId, size?)` streams `frame` events (`{ tabId, src, at }`) for a controlled tab and returns a stop function; the panel watches only the focused tab while it's controlled and visible, with the frame's CSS size. **Stop** calls `agent.stop(agentId)`. |
|
|
203
203
|
| `settings` | local storage | Theme, model, thinking level, composer and diff preferences, the models on offer, and accounts. |
|
|
204
204
|
| `changes` (optional) | `@ai-ecoverse/slicc-agent` | Pending changes from git instead of the file system: `changes()`, `accept(path)` (stage), `revert(path)` (discard), and a `changes` event, as on `files`. When present, the UI reads changes only from this port, including the rail's count. A `Change` may carry `repo`, its repository root; put the active agent's repository first. The optional `unavailable()` returns why there can be no changes (no git, no repository), which the panel shows with commands in backticks made copyable, or `null`; emit `changes` when it flips. |
|
|
205
205
|
| `memory` | `@ai-ecoverse/slicc-agent` | Memories with scope, section, tag and body; `save` and `remove`. `source: 'notes'` marks free text the panel shows as an entry titled after its section; saving it makes it an entry. The optional `scopes()` lists the scope picker's choices as `{ id, label, group }`, ungrouped first, then under “Cones” and “Roles”; without it the picker offers “Everyone” (`global`) and each cone. The panel re-reads `scopes()` on `memories` and `agents`, so emit `memories` when the list changes. A scope that disappears falls back to `global`. |
|
|
206
206
|
| `monitor` | all of them | A snapshot of vitals, alerts and sections, re-emitted as the system changes, and `resync`. |
|
|
207
207
|
| `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`. |
|
|
208
208
|
| `tray` | the tray protocol | Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`. |
|
|
209
|
-
| `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 "Needs Python", "Installs Python too", or "Needs Python (not installed)" on an installed row), `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` and `[data-error]`. Real installer wiring belongs to ai-ecoverse/slicc-bios#70. |
|
|
209
|
+
| `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 "Needs Python", "Installs Python too", or "Needs Python (not installed)" on an installed row), 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), `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. |
|
|
210
210
|
| `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` 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. Without the port, the indicator and the Network panel don't show. |
|
|
211
211
|
| `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]`. |
|
|
212
212
|
|
|
213
|
-
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`. `networkFixtures`, `tailnetFixtures` 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`.
|
|
213
|
+
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`. `networkFixtures`, `tailnetFixtures` 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`.
|
|
214
214
|
|
|
215
215
|
On phones the update status and the tray show only their status light 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. The update status comes first so progress and failures remain reachable on narrow screens.
|
|
216
216
|
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -7,6 +7,223 @@ import {
|
|
|
7
7
|
} from "./chunk-T5NHW4KC.js";
|
|
8
8
|
import "./chunk-AXQFWXYZ.js";
|
|
9
9
|
|
|
10
|
+
// src/dummy/browser.ts
|
|
11
|
+
function escape(text) {
|
|
12
|
+
return text.replace(/[<>&"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
13
|
+
}
|
|
14
|
+
function hue(text) {
|
|
15
|
+
let sum = 0;
|
|
16
|
+
for (const char of text) sum = (sum * 31 + char.charCodeAt(0)) % 360;
|
|
17
|
+
return sum;
|
|
18
|
+
}
|
|
19
|
+
function page(tab, mark) {
|
|
20
|
+
const h = hue(new URL(tab.url).host);
|
|
21
|
+
const rows = [0, 1, 2, 3, 4, 5].map(
|
|
22
|
+
(i) => `<rect x="64" y="${220 + i * 36}" width="${560 - i % 3 * 90}" height="12" rx="6" fill="#d6d6d6"/>`
|
|
23
|
+
).join("");
|
|
24
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800"><rect width="1280" height="800" fill="#ffffff"/><rect width="1280" height="64" fill="hsl(${h} 45% 32%)"/><text x="64" y="40" font-family="sans-serif" font-size="20" fill="#ffffff">${escape(new URL(tab.url).host)}</text><text x="64" y="160" font-family="sans-serif" font-size="36" fill="#222222">${escape(tab.title)}</text>${rows}<rect x="720" y="200" width="496" height="280" rx="12" fill="hsl(${h} 40% 92%)"/>${mark === void 0 ? "" : `<rect x="56" y="${212 + mark % 6 * 36}" width="${576 - mark % 3 * 90}" height="28" rx="8" fill="none" stroke="hsl(${h} 70% 45%)" stroke-width="4"/>`}</svg>`;
|
|
25
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
26
|
+
}
|
|
27
|
+
function titleFor(url) {
|
|
28
|
+
const { host, pathname } = new URL(url);
|
|
29
|
+
if (!host) return "New tab";
|
|
30
|
+
return pathname === "/" ? host : `${host}${pathname}`;
|
|
31
|
+
}
|
|
32
|
+
var second = 1e3;
|
|
33
|
+
function browserHistory(scenario, now2 = Date.now()) {
|
|
34
|
+
if (scenario === "off") return [];
|
|
35
|
+
const harbor = { tabId: "tab-preview", agentId: "cone-harbor" };
|
|
36
|
+
const otter = { tabId: "tab-pull", agentId: "scoop-otter" };
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
...otter,
|
|
40
|
+
kind: "goto",
|
|
41
|
+
value: "https://git.example.com/example/harbor/pull/58",
|
|
42
|
+
at: now2 - 300 * second
|
|
43
|
+
},
|
|
44
|
+
{ ...otter, kind: "snapshot", at: now2 - 296 * second },
|
|
45
|
+
{ ...otter, kind: "scroll", at: now2 - 290 * second },
|
|
46
|
+
{ ...otter, kind: "screenshot", at: now2 - 288 * second },
|
|
47
|
+
{ ...harbor, kind: "open", value: "http://localhost:8787/", at: now2 - 120 * second },
|
|
48
|
+
{ ...harbor, kind: "snapshot", at: now2 - 116 * second },
|
|
49
|
+
{ ...harbor, kind: "type", target: 'textbox "City"', length: 6, at: now2 - 110 * second },
|
|
50
|
+
{ ...harbor, kind: "click", target: 'button "Show forecast"', at: now2 - 106 * second },
|
|
51
|
+
{ ...harbor, kind: "snapshot", at: now2 - 100 * second },
|
|
52
|
+
{
|
|
53
|
+
...harbor,
|
|
54
|
+
kind: "click",
|
|
55
|
+
target: 'link "Tomorrow"',
|
|
56
|
+
status: "failed",
|
|
57
|
+
error: 'No element matches link "Tomorrow".',
|
|
58
|
+
at: now2 - 92 * second
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
tabId: null,
|
|
62
|
+
agentId: "cone-harbor",
|
|
63
|
+
kind: "request",
|
|
64
|
+
value: "GET https://api.example.com/v2/tides",
|
|
65
|
+
at: now2 - 80 * second
|
|
66
|
+
},
|
|
67
|
+
{ ...harbor, kind: "eval", at: now2 - 70 * second },
|
|
68
|
+
{ ...harbor, kind: "press", value: "End", at: now2 - 64 * second }
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
var drive = [
|
|
72
|
+
{ tabId: "tab-preview", agentId: "cone-harbor", kind: "scroll" },
|
|
73
|
+
{ tabId: "tab-preview", agentId: "cone-harbor", kind: "screenshot" },
|
|
74
|
+
{ tabId: "tab-preview", agentId: "cone-harbor", kind: "click", target: 'button "Compare ports"' }
|
|
75
|
+
];
|
|
76
|
+
var DummyBrowser = class extends Emitter {
|
|
77
|
+
#tabs;
|
|
78
|
+
#active;
|
|
79
|
+
#clock;
|
|
80
|
+
#next = 1;
|
|
81
|
+
#actions = [];
|
|
82
|
+
#marks = 0;
|
|
83
|
+
constructor(tabs2, clock2, {
|
|
84
|
+
history = [],
|
|
85
|
+
controlled = []
|
|
86
|
+
} = {}) {
|
|
87
|
+
super();
|
|
88
|
+
this.#clock = clock2;
|
|
89
|
+
this.#tabs = tabs2.map((tab) => ({
|
|
90
|
+
...tab,
|
|
91
|
+
...controlled.includes(tab.id) ? { controlled: true } : {}
|
|
92
|
+
}));
|
|
93
|
+
this.#active = this.#tabs[0]?.id ?? null;
|
|
94
|
+
for (const draft of history) this.#push(draft);
|
|
95
|
+
}
|
|
96
|
+
list() {
|
|
97
|
+
return this.#tabs.map((tab) => ({ ...tab }));
|
|
98
|
+
}
|
|
99
|
+
active() {
|
|
100
|
+
return this.#active;
|
|
101
|
+
}
|
|
102
|
+
actions(tabId) {
|
|
103
|
+
return this.#actions.filter((action) => tabId === void 0 || action.tabId === tabId).slice(-50).map((action) => ({ ...action }));
|
|
104
|
+
}
|
|
105
|
+
#push(draft) {
|
|
106
|
+
const action = {
|
|
107
|
+
status: "done",
|
|
108
|
+
...draft,
|
|
109
|
+
id: `action-${this.#actions.length + 1}`,
|
|
110
|
+
at: draft.at ?? Date.now()
|
|
111
|
+
};
|
|
112
|
+
this.#actions.push(action);
|
|
113
|
+
return action;
|
|
114
|
+
}
|
|
115
|
+
record(draft) {
|
|
116
|
+
const action = this.#push({ status: "running", ...draft });
|
|
117
|
+
const tab = this.#tabs.find((candidate) => candidate.id === action.tabId);
|
|
118
|
+
if (tab) {
|
|
119
|
+
tab.agentId = action.agentId;
|
|
120
|
+
if (action.agentId) tab.controlled = true;
|
|
121
|
+
this.emit("tabs", this.list());
|
|
122
|
+
}
|
|
123
|
+
this.emit("action", { ...action });
|
|
124
|
+
return action.id;
|
|
125
|
+
}
|
|
126
|
+
finish(id, error) {
|
|
127
|
+
const action = this.#actions.find((candidate) => candidate.id === id);
|
|
128
|
+
if (action?.status !== "running") return;
|
|
129
|
+
action.status = error ? "failed" : "done";
|
|
130
|
+
if (error) action.error = error;
|
|
131
|
+
this.emit("action", { ...action });
|
|
132
|
+
}
|
|
133
|
+
async run(script2) {
|
|
134
|
+
for (const [index2, draft] of script2.entries()) {
|
|
135
|
+
const id = this.record(draft);
|
|
136
|
+
if (index2 === script2.length - 1) return;
|
|
137
|
+
await this.#clock.sleep(20);
|
|
138
|
+
this.finish(id);
|
|
139
|
+
await this.#clock.sleep(10);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
release(agentId) {
|
|
143
|
+
let changed = false;
|
|
144
|
+
for (const tab of this.#tabs) {
|
|
145
|
+
if (tab.agentId !== agentId || !tab.controlled) continue;
|
|
146
|
+
tab.controlled = false;
|
|
147
|
+
changed = true;
|
|
148
|
+
}
|
|
149
|
+
for (const action of this.#actions)
|
|
150
|
+
if (action.agentId === agentId && action.status === "running")
|
|
151
|
+
this.finish(action.id, "Stopped");
|
|
152
|
+
if (changed) this.emit("tabs", this.list());
|
|
153
|
+
}
|
|
154
|
+
watch(tabId) {
|
|
155
|
+
const tab = () => this.#tabs.find((candidate) => candidate.id === tabId);
|
|
156
|
+
if (!tab()?.controlled) return () => {
|
|
157
|
+
};
|
|
158
|
+
const send = () => {
|
|
159
|
+
const now2 = tab();
|
|
160
|
+
if (!now2?.controlled) return;
|
|
161
|
+
this.emit("frame", { tabId, src: page(now2, this.#marks++), at: Date.now() });
|
|
162
|
+
};
|
|
163
|
+
const timer = setInterval(send, 500);
|
|
164
|
+
void this.#clock.sleep(0).then(send);
|
|
165
|
+
return () => clearInterval(timer);
|
|
166
|
+
}
|
|
167
|
+
#changed() {
|
|
168
|
+
this.emit("tabs", this.list());
|
|
169
|
+
this.emit("active", this.#active);
|
|
170
|
+
}
|
|
171
|
+
activate(id) {
|
|
172
|
+
if (!this.#tabs.some((tab) => tab.id === id)) return;
|
|
173
|
+
this.#active = id;
|
|
174
|
+
this.emit("active", id);
|
|
175
|
+
}
|
|
176
|
+
#load(id, action) {
|
|
177
|
+
void this.#clock.sleep(15).then(() => {
|
|
178
|
+
if (action) this.finish(action);
|
|
179
|
+
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
180
|
+
if (!tab) return;
|
|
181
|
+
tab.status = "complete";
|
|
182
|
+
tab.title = titleFor(tab.url);
|
|
183
|
+
this.#changed();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
open(url, agentId = null) {
|
|
187
|
+
const tab = {
|
|
188
|
+
id: `tab-new-${this.#next++}`,
|
|
189
|
+
title: url,
|
|
190
|
+
url,
|
|
191
|
+
status: "loading",
|
|
192
|
+
agentId
|
|
193
|
+
};
|
|
194
|
+
this.#tabs.push(tab);
|
|
195
|
+
this.#active = tab.id;
|
|
196
|
+
const action = agentId ? this.#push({ tabId: tab.id, agentId, kind: "open", value: url, status: "running" }) : void 0;
|
|
197
|
+
this.#changed();
|
|
198
|
+
if (action) this.emit("action", { ...action });
|
|
199
|
+
this.#load(tab.id, action?.id);
|
|
200
|
+
return { ...tab };
|
|
201
|
+
}
|
|
202
|
+
navigate(id, url) {
|
|
203
|
+
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
204
|
+
if (!tab) return;
|
|
205
|
+
tab.url = url;
|
|
206
|
+
tab.title = url;
|
|
207
|
+
tab.status = "loading";
|
|
208
|
+
this.#changed();
|
|
209
|
+
this.#load(id);
|
|
210
|
+
}
|
|
211
|
+
close(id) {
|
|
212
|
+
const index2 = this.#tabs.findIndex((tab) => tab.id === id);
|
|
213
|
+
if (index2 < 0) return;
|
|
214
|
+
this.#tabs.splice(index2, 1);
|
|
215
|
+
if (this.#active === id)
|
|
216
|
+
this.#active = this.#tabs[Math.min(index2, this.#tabs.length - 1)]?.id ?? null;
|
|
217
|
+
this.#changed();
|
|
218
|
+
}
|
|
219
|
+
async screenshot(id) {
|
|
220
|
+
await this.#clock.sleep(2);
|
|
221
|
+
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
222
|
+
if (!tab) throw new Error(`No tab ${id}`);
|
|
223
|
+
return page(tab);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
10
227
|
// src/dummy/changes.ts
|
|
11
228
|
var repos = ["/workspace/harbor", "/workspace/skills"];
|
|
12
229
|
var noGit = "Changes needs git, and git isn\u2019t installed. Install it with `pnpm add -g @ai-ecoverse/wasm-git`, then run `git init` in a folder under /home, or clone with slicc-node or the extension connected.";
|
|
@@ -780,102 +997,6 @@ var DummyAgent = class extends Emitter {
|
|
|
780
997
|
}
|
|
781
998
|
};
|
|
782
999
|
|
|
783
|
-
// src/dummy/browser.ts
|
|
784
|
-
function escape(text) {
|
|
785
|
-
return text.replace(/[<>&"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
786
|
-
}
|
|
787
|
-
function hue(text) {
|
|
788
|
-
let sum = 0;
|
|
789
|
-
for (const char of text) sum = (sum * 31 + char.charCodeAt(0)) % 360;
|
|
790
|
-
return sum;
|
|
791
|
-
}
|
|
792
|
-
function page(tab) {
|
|
793
|
-
const h = hue(new URL(tab.url).host);
|
|
794
|
-
const rows = [0, 1, 2, 3, 4, 5].map(
|
|
795
|
-
(i) => `<rect x="64" y="${220 + i * 36}" width="${560 - i % 3 * 90}" height="12" rx="6" fill="#d6d6d6"/>`
|
|
796
|
-
).join("");
|
|
797
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800"><rect width="1280" height="800" fill="#ffffff"/><rect width="1280" height="64" fill="hsl(${h} 45% 32%)"/><text x="64" y="40" font-family="sans-serif" font-size="20" fill="#ffffff">${escape(new URL(tab.url).host)}</text><text x="64" y="160" font-family="sans-serif" font-size="36" fill="#222222">${escape(tab.title)}</text>${rows}<rect x="720" y="200" width="496" height="280" rx="12" fill="hsl(${h} 40% 92%)"/></svg>`;
|
|
798
|
-
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
799
|
-
}
|
|
800
|
-
function titleFor(url) {
|
|
801
|
-
const { host, pathname } = new URL(url);
|
|
802
|
-
if (!host) return "New tab";
|
|
803
|
-
return pathname === "/" ? host : `${host}${pathname}`;
|
|
804
|
-
}
|
|
805
|
-
var DummyBrowser = class extends Emitter {
|
|
806
|
-
#tabs;
|
|
807
|
-
#active;
|
|
808
|
-
#clock;
|
|
809
|
-
#next = 1;
|
|
810
|
-
constructor(tabs2, clock2) {
|
|
811
|
-
super();
|
|
812
|
-
this.#clock = clock2;
|
|
813
|
-
this.#tabs = tabs2.map((tab) => ({ ...tab }));
|
|
814
|
-
this.#active = this.#tabs[0]?.id ?? null;
|
|
815
|
-
}
|
|
816
|
-
list() {
|
|
817
|
-
return this.#tabs.map((tab) => ({ ...tab }));
|
|
818
|
-
}
|
|
819
|
-
active() {
|
|
820
|
-
return this.#active;
|
|
821
|
-
}
|
|
822
|
-
#changed() {
|
|
823
|
-
this.emit("tabs", this.list());
|
|
824
|
-
this.emit("active", this.#active);
|
|
825
|
-
}
|
|
826
|
-
activate(id) {
|
|
827
|
-
if (!this.#tabs.some((tab) => tab.id === id)) return;
|
|
828
|
-
this.#active = id;
|
|
829
|
-
this.emit("active", id);
|
|
830
|
-
}
|
|
831
|
-
#load(id) {
|
|
832
|
-
void this.#clock.sleep(15).then(() => {
|
|
833
|
-
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
834
|
-
if (!tab) return;
|
|
835
|
-
tab.status = "complete";
|
|
836
|
-
tab.title = titleFor(tab.url);
|
|
837
|
-
this.#changed();
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
open(url, agentId = null) {
|
|
841
|
-
const tab = {
|
|
842
|
-
id: `tab-new-${this.#next++}`,
|
|
843
|
-
title: url,
|
|
844
|
-
url,
|
|
845
|
-
status: "loading",
|
|
846
|
-
agentId
|
|
847
|
-
};
|
|
848
|
-
this.#tabs.push(tab);
|
|
849
|
-
this.#active = tab.id;
|
|
850
|
-
this.#changed();
|
|
851
|
-
this.#load(tab.id);
|
|
852
|
-
return { ...tab };
|
|
853
|
-
}
|
|
854
|
-
navigate(id, url) {
|
|
855
|
-
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
856
|
-
if (!tab) return;
|
|
857
|
-
tab.url = url;
|
|
858
|
-
tab.title = url;
|
|
859
|
-
tab.status = "loading";
|
|
860
|
-
this.#changed();
|
|
861
|
-
this.#load(id);
|
|
862
|
-
}
|
|
863
|
-
close(id) {
|
|
864
|
-
const index2 = this.#tabs.findIndex((tab) => tab.id === id);
|
|
865
|
-
if (index2 < 0) return;
|
|
866
|
-
this.#tabs.splice(index2, 1);
|
|
867
|
-
if (this.#active === id)
|
|
868
|
-
this.#active = this.#tabs[Math.min(index2, this.#tabs.length - 1)]?.id ?? null;
|
|
869
|
-
this.#changed();
|
|
870
|
-
}
|
|
871
|
-
async screenshot(id) {
|
|
872
|
-
await this.#clock.sleep(2);
|
|
873
|
-
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
874
|
-
if (!tab) throw new Error(`No tab ${id}`);
|
|
875
|
-
return page(tab);
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
|
|
879
1000
|
// src/dummy/sprinkles/suggestions.shtml
|
|
880
1001
|
var suggestions_default = `<!DOCTYPE html>
|
|
881
1002
|
<html lang="en">
|
|
@@ -4459,7 +4580,12 @@ function packageFixtures(scenario) {
|
|
|
4459
4580
|
const patch = (id, change) => {
|
|
4460
4581
|
items[at3(id)] = { ...items[at3(id)], ...change };
|
|
4461
4582
|
};
|
|
4462
|
-
patch("python", {
|
|
4583
|
+
patch("python", {
|
|
4584
|
+
state: "installed",
|
|
4585
|
+
version: "3.13.1",
|
|
4586
|
+
notes: ["1 untested dependency: @harbor/wasi-sockets@0.4.2-1"],
|
|
4587
|
+
actions: ["remove"]
|
|
4588
|
+
});
|
|
4463
4589
|
patch("uv", { state: "outdated", version: "0.8.1", actions: ["update", "remove"] });
|
|
4464
4590
|
patch("esbuild", {
|
|
4465
4591
|
state: "installing",
|
|
@@ -4621,13 +4747,17 @@ function createDummyModel({
|
|
|
4621
4747
|
network = "limited",
|
|
4622
4748
|
tailnet,
|
|
4623
4749
|
changes = "files",
|
|
4624
|
-
notices = "off"
|
|
4750
|
+
notices = "off",
|
|
4751
|
+
browser: scenario = "idle"
|
|
4625
4752
|
} = {}) {
|
|
4626
4753
|
const time = clock(delay);
|
|
4627
4754
|
const mount = mounts === "off" ? null : { scenario: mounts, point: mountPoint, files: mounted };
|
|
4628
4755
|
const pending2 = changes === "git" ? [...pending, skillEdit] : pending;
|
|
4629
4756
|
const files3 = new DummyFiles(files2, directories, pending2, time, mount);
|
|
4630
|
-
const browser = new DummyBrowser(tabs, time
|
|
4757
|
+
const browser = new DummyBrowser(scenario === "off" ? [] : tabs, time, {
|
|
4758
|
+
history: browserHistory(scenario),
|
|
4759
|
+
controlled: scenario === "driving" ? ["tab-preview", "tab-pull"] : []
|
|
4760
|
+
});
|
|
4631
4761
|
const agent = new DummyAgent(
|
|
4632
4762
|
agents,
|
|
4633
4763
|
conversations,
|
|
@@ -4636,6 +4766,12 @@ function createDummyModel({
|
|
|
4636
4766
|
queues,
|
|
4637
4767
|
frozen
|
|
4638
4768
|
);
|
|
4769
|
+
const stop = agent.stop.bind(agent);
|
|
4770
|
+
agent.stop = (agentId) => {
|
|
4771
|
+
stop(agentId);
|
|
4772
|
+
browser.release(agentId);
|
|
4773
|
+
};
|
|
4774
|
+
if (scenario === "driving") void browser.run(drive);
|
|
4639
4775
|
const base = {
|
|
4640
4776
|
agent,
|
|
4641
4777
|
files: files3,
|
|
@@ -4666,14 +4802,17 @@ function createDummyModel({
|
|
|
4666
4802
|
return { ...base, monitor: new DummyMonitor(base) };
|
|
4667
4803
|
}
|
|
4668
4804
|
export {
|
|
4805
|
+
DummyBrowser,
|
|
4669
4806
|
DummyChanges,
|
|
4670
4807
|
DummyNetwork,
|
|
4671
4808
|
DummyNotices,
|
|
4672
4809
|
DummyUpdates,
|
|
4810
|
+
browserHistory,
|
|
4673
4811
|
createDummyModel,
|
|
4674
4812
|
networkFixtures,
|
|
4675
4813
|
noticeFixtures,
|
|
4676
4814
|
packageFixtures,
|
|
4815
|
+
page,
|
|
4677
4816
|
tailnetFixtures,
|
|
4678
4817
|
updateFixtures
|
|
4679
4818
|
};
|