@ai-ecoverse/slicc-spectrum 1.37.1 → 1.39.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 CHANGED
@@ -134,7 +134,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
134
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. |
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. 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 chat**, which calls `clear` and puts focus back in the composer; on a cone, when the port can freeze, its tooltip says the old chat goes to the freezer. |
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. |
138
138
  | `<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. |
139
139
  | `<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. |
140
140
  | `<slicc-file-view>` | One file (`path`), highlighted, with its pending change and a link to the diff. |
@@ -146,9 +146,9 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
146
146
  | `<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. Also available through View and the rails. |
147
147
  | `<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”. |
148
148
  | `<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). |
149
- | `<slicc-freezer>` | Frozen cones, archived with their scoops, and the chats New chat put away: thaw to bring one back, delete, or freeze the active cone (also `/freeze`). |
149
+ | `<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. |
150
150
  | `<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. |
151
- | `<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, not available with how to get it, or `declined`, muted: off for this session, reload to be asked again) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, **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, 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; on a phone, a cloud icon for the state. Offered only when the model has a `network` port. |
151
+ | `<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, not available with how to get it, or `declined`, muted: off for this session, reload to be asked again) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, **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, 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. Offered only when the model has a `network` port. |
152
152
  | `<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. |
153
153
  | `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
154
154
  | `<slicc-prompt>` | The name dialog `prompt()` opens. Not for direct use. |
@@ -195,7 +195,7 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
195
195
 
196
196
  | Port | Later backed by | |
197
197
  | :--- | :--- | :--- |
198
- | `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 chat: on a cone, a port with `freeze` first puts the old chat in the freezer as its own entry, which thaws into a cone of its own; a scoop's chat 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, and names a thawed cone `<name> (earlier)`, then `(earlier 2)` and so on, when its name is taken. `freeze(agentId)` is optional too: without it, `/freeze` and the Freezer's freeze button don't show. Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`. |
198
+ | `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. Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`. |
199
199
  | `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. |
200
200
  | `terminals` | slicc-kernel's terminal API | Terminal sessions; `backend(id)` returns a `TerminalBackend` for `<slicc-terminal>`. |
201
201
  | `browser` | `@ai-ecoverse/slicc-cdp` | Tabs, the active tab, `open`, `navigate`, `close`, `screenshot`. |
@@ -206,9 +206,9 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
206
206
  | `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`. |
207
207
  | `tray` | the tray protocol | Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`. |
208
208
  | `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. Real installer wiring belongs to ai-ecoverse/slicc-bios#70. |
209
- | `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`proxy` for slicc-node, `extension` for the Chrome extension relay, `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?, declined? }`, `via` being `extension`, `proxy` or `null` when browser automation isn't available, and `declined: true` once the user declined browser control for the session). Emit `network` when it changes. The optional `check()` tests the route again. Without the port, the indicator and the Network panel don't show. |
209
+ | `network` (optional) | BIOS | `status()` returns the `NetworkStatus`: `route` (`proxy` for slicc-node, `extension` for the Chrome extension relay, `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?, declined? }`, `via` being `extension`, `proxy` or `null` when browser automation isn't available, and `declined: true` once the user declined browser control for the session). 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. |
210
210
 
211
- 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. `updateFixtures` 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. `networkFixtures` 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`.
211
+ 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. `updateFixtures` 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. `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`.
212
212
 
213
213
  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.
214
214
 
@@ -265,6 +265,7 @@ var DummyAgent = class extends Emitter {
265
265
  #next = 1;
266
266
  #cones = 1;
267
267
  #chats = 1;
268
+ #thaws = 1;
268
269
  constructor(agents2, conversations2, effects, clock2, queues2 = {}, frozen2 = []) {
269
270
  super();
270
271
  this.#clock = clock2;
@@ -579,6 +580,7 @@ var DummyAgent = class extends Emitter {
579
580
  const first = messages.find((message) => message.role === "user");
580
581
  this.#frozen.unshift({
581
582
  id,
583
+ kind: "cone",
582
584
  name: agent.name,
583
585
  title: first?.role === "user" ? first.text.slice(0, 80) : agent.name,
584
586
  model: agent.model,
@@ -685,7 +687,21 @@ var DummyAgent = class extends Emitter {
685
687
  });
686
688
  }
687
689
  frozen() {
688
- return this.#frozen.map((cone) => ({ ...cone }));
690
+ const live = this.#agents.filter((agent) => !agent.frozen).map((agent) => {
691
+ const messages = this.#messages.get(agent.id);
692
+ const first = messages.find((message) => message.role === "user");
693
+ return {
694
+ id: agent.id,
695
+ kind: agent.kind,
696
+ live: true,
697
+ name: agent.name,
698
+ title: first?.role === "user" ? first.text.slice(0, 80) : agent.name,
699
+ model: agent.model,
700
+ messages: messages.length,
701
+ frozenAt: messages.at(-1)?.createdAt ?? 0
702
+ };
703
+ });
704
+ return [...live, ...this.#frozen.map((cone) => ({ ...cone }))];
689
705
  }
690
706
  freeze(agentId) {
691
707
  const agent = this.#agent(agentId);
@@ -698,6 +714,7 @@ var DummyAgent = class extends Emitter {
698
714
  const first = messages.find((message) => message.role === "user");
699
715
  this.#frozen.unshift({
700
716
  id: agentId,
717
+ kind: "cone",
701
718
  name: agent.name,
702
719
  title: first?.role === "user" ? first.text.slice(0, 80) : agent.name,
703
720
  model: agent.model,
@@ -728,16 +745,25 @@ var DummyAgent = class extends Emitter {
728
745
  this.#changed();
729
746
  return { ...agent };
730
747
  }
731
- this.#ice.delete(id);
732
- this.#frozen = this.#frozen.filter((cone2) => cone2.id !== id);
733
- const cone = ice[0].agent;
734
- const taken = (name) => this.#agents.some((agent) => agent.kind === "cone" && agent.name === name);
735
- if (taken(cone.name)) {
736
- const base = cone.name;
737
- let n = 1;
738
- cone.name = `${base} (earlier)`;
739
- while (taken(cone.name)) cone.name = `${base} (earlier ${++n})`;
748
+ const row = this.#frozen.find((cone) => cone.id === id);
749
+ if (row.kind === "scoop" || row.kind === "agent") {
750
+ const [{ agent, messages }] = ice;
751
+ const fork = {
752
+ ...agent,
753
+ id: `${id}-thaw-${this.#thaws++}`,
754
+ kind: "cone",
755
+ parentId: null
756
+ };
757
+ this.#agents.push(fork);
758
+ this.#messages.set(fork.id, structuredClone(messages));
759
+ this.#queues.set(fork.id, []);
760
+ row.thawedAs = fork.id;
761
+ this.emit("frozen", this.frozen());
762
+ this.select(fork.id);
763
+ return { ...fork };
740
764
  }
765
+ this.#ice.delete(id);
766
+ this.#frozen = this.#frozen.filter((cone) => cone.id !== id);
741
767
  for (const { agent, messages } of ice) {
742
768
  this.#agents.push(agent);
743
769
  this.#messages.set(agent.id, messages);
@@ -1818,6 +1844,7 @@ var frozen = [
1818
1844
  {
1819
1845
  cone: {
1820
1846
  id: "cone-onboarding",
1847
+ kind: "cone",
1821
1848
  name: "onboarding",
1822
1849
  title: "Write the onboarding guide for new contributors",
1823
1850
  model: "claude-sonnet-5-5",
@@ -1832,6 +1859,7 @@ var frozen = [
1832
1859
  {
1833
1860
  cone: {
1834
1861
  id: "cone-kv-spike",
1862
+ kind: "cone",
1835
1863
  name: "kv-spike",
1836
1864
  title: "Spike: can the cache move to KV without a cold start?",
1837
1865
  model: "claude-opus-5-5",
@@ -1846,6 +1874,7 @@ var frozen = [
1846
1874
  {
1847
1875
  cone: {
1848
1876
  id: "cone-q3-report",
1877
+ kind: "cone",
1849
1878
  name: "q3-report",
1850
1879
  title: "Summarize Q3 uptime and incidents",
1851
1880
  model: "claude-haiku-4-5",
@@ -1856,6 +1885,36 @@ var frozen = [
1856
1885
  "Summarize Q3 uptime and incidents",
1857
1886
  "Uptime 99.96%. Two incidents: an expired certificate and an upstream outage."
1858
1887
  )
1888
+ },
1889
+ {
1890
+ cone: {
1891
+ id: "scoop-tide-check",
1892
+ kind: "scoop",
1893
+ name: "tide-check",
1894
+ title: "Check the tide tables against the harbor feed",
1895
+ model: "claude-haiku-4-5",
1896
+ messages: 9,
1897
+ frozenAt: now - 1 * day
1898
+ },
1899
+ messages: frozenConversation(
1900
+ "Check the tide tables against the harbor feed",
1901
+ "They match, except the 3 October low tide, which the feed lists 4 minutes later."
1902
+ )
1903
+ },
1904
+ {
1905
+ cone: {
1906
+ id: "run-nightly-digest",
1907
+ kind: "agent",
1908
+ name: "nightly-digest",
1909
+ title: "Write the nightly digest of merged pull requests",
1910
+ model: "claude-sonnet-5-5",
1911
+ messages: 6,
1912
+ frozenAt: now - 3 * day
1913
+ },
1914
+ messages: frozenConversation(
1915
+ "Write the nightly digest of merged pull requests",
1916
+ "Four pull requests merged: two fixes, a docs change and a dependency bump."
1917
+ )
1859
1918
  }
1860
1919
  ];
1861
1920
  var memories = [
@@ -3785,29 +3844,97 @@ function networkFixtures(scenario, now2 = Date.now()) {
3785
3844
  browser: { via: null }
3786
3845
  };
3787
3846
  }
3847
+ var exitNodes = [
3848
+ { id: "nKeel1CNTRL", name: "keel-router", online: true },
3849
+ { id: "nMast2CNTRL", name: "mast-pi", online: true },
3850
+ { id: "nBuoy3CNTRL", name: "buoy-laptop", online: false }
3851
+ ];
3852
+ function tailnetFixtures(scenario) {
3853
+ const base = { exitNode: null, exitNodes: [], autoExitNode: false, shieldsUp: true };
3854
+ if (scenario === "needs-login") {
3855
+ return { ...base, state: "needs-login", loginUrl: "https://login.tailnet.example/a/7f3c2e" };
3856
+ }
3857
+ if (scenario === "running") {
3858
+ return {
3859
+ ...base,
3860
+ state: "running",
3861
+ node: { name: "slicc-harbor", addresses: ["fd7a:115c:a1e0::7", "100.64.12.7"] },
3862
+ exitNodes,
3863
+ peers: 4
3864
+ };
3865
+ }
3866
+ if (scenario === "failed") {
3867
+ return {
3868
+ ...base,
3869
+ state: "failed",
3870
+ detail: "The coordination server didn\u2019t answer. Check the connection and try again."
3871
+ };
3872
+ }
3873
+ return { ...base, state: "off" };
3874
+ }
3788
3875
  var DummyNetwork = class extends Emitter {
3789
3876
  scenario;
3790
3877
  #status;
3791
3878
  #clock;
3792
- constructor(scenario, clock2) {
3879
+ #tailnet;
3880
+ constructor(scenario, clock2, tailnet) {
3793
3881
  super();
3794
3882
  this.scenario = scenario;
3795
- this.#status = networkFixtures(scenario);
3796
3883
  this.#clock = clock2;
3884
+ this.#tailnet = tailnet ? tailnetFixtures(tailnet) : void 0;
3885
+ this.#status = this.#with(networkFixtures(scenario));
3886
+ }
3887
+ #with(status) {
3888
+ return this.#tailnet ? { ...status, tailnet: this.#tailnet } : status;
3889
+ }
3890
+ #set(tailnet) {
3891
+ this.#tailnet = tailnet;
3892
+ this.#status = this.#with(this.#status);
3893
+ this.emit("network", this.#status);
3797
3894
  }
3798
3895
  status() {
3799
3896
  return this.#status;
3800
3897
  }
3801
3898
  setScenario(scenario) {
3802
3899
  this.scenario = scenario;
3803
- this.#status = networkFixtures(scenario);
3900
+ this.#status = this.#with(networkFixtures(scenario));
3804
3901
  this.emit("network", this.#status);
3805
3902
  }
3806
3903
  async check() {
3807
3904
  await this.#clock.sleep(4);
3808
- this.#status = networkFixtures(this.scenario);
3905
+ if (this.#tailnet?.state === "failed") this.#tailnet = tailnetFixtures("running");
3906
+ this.#status = this.#with(networkFixtures(this.scenario));
3809
3907
  this.emit("network", this.#status);
3810
3908
  }
3909
+ async setTailnet(on) {
3910
+ await this.#clock.sleep(4);
3911
+ this.#set(tailnetFixtures(on ? "needs-login" : "off"));
3912
+ }
3913
+ async submitAuthKey(key) {
3914
+ if (!key.startsWith("tskey-")) {
3915
+ throw new Error("That isn\u2019t a Tailscale auth key. Auth keys start with tskey-.");
3916
+ }
3917
+ this.#set({ ...tailnetFixtures("off"), state: "starting", detail: "Joining your tailnet\u2026" });
3918
+ await this.#clock.sleep(8);
3919
+ this.#set(tailnetFixtures("running"));
3920
+ }
3921
+ async setExitNode(id) {
3922
+ await this.#clock.sleep(4);
3923
+ const tailnet = this.#tailnet ?? tailnetFixtures("running");
3924
+ const nodes = tailnet.exitNodes ?? [];
3925
+ if (id === "auto") {
3926
+ const node2 = nodes.find((item) => item.online);
3927
+ this.#set({ ...tailnet, autoExitNode: true, exitNode: node2?.name ?? null });
3928
+ return;
3929
+ }
3930
+ const node = nodes.find((item) => item.id === id);
3931
+ if (id && !node?.online) throw new Error("That exit node is offline.");
3932
+ this.#set({ ...tailnet, autoExitNode: false, exitNode: node?.name ?? null });
3933
+ }
3934
+ async logoutTailnet() {
3935
+ await this.#clock.sleep(4);
3936
+ this.#set(tailnetFixtures("needs-login"));
3937
+ }
3811
3938
  };
3812
3939
 
3813
3940
  // src/dummy/sprinkles.ts
@@ -4257,6 +4384,7 @@ function createDummyModel({
4257
4384
  updates = "current",
4258
4385
  mounts = "off",
4259
4386
  network = "limited",
4387
+ tailnet,
4260
4388
  changes = "files"
4261
4389
  } = {}) {
4262
4390
  const time = clock(delay);
@@ -4292,7 +4420,7 @@ function createDummyModel({
4292
4420
  sprinkles: new DummySprinkles(sprinkles, agent, files3),
4293
4421
  tray: new DummyTray(tray, time),
4294
4422
  updates: new DummyUpdates(updates, time),
4295
- ...network === "off" ? {} : { network: new DummyNetwork(network, time) },
4423
+ ...network === "off" ? {} : { network: new DummyNetwork(network, time, tailnet) },
4296
4424
  ...changes === "files" ? {} : { changes: new DummyChanges(files3, changes === "git") }
4297
4425
  };
4298
4426
  return { ...base, monitor: new DummyMonitor(base) };
@@ -4303,6 +4431,7 @@ export {
4303
4431
  DummyUpdates,
4304
4432
  createDummyModel,
4305
4433
  networkFixtures,
4434
+ tailnetFixtures,
4306
4435
  updateFixtures
4307
4436
  };
4308
4437
  //# sourceMappingURL=slicc-dummy.js.map