@ai-ecoverse/slicc-spectrum 1.35.0 → 1.37.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,12 +134,12 @@ 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, and an edit's `diff` shown as a diff view; 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 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. |
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. |
141
- | `<slicc-changes>` | Pending changes with their author. Opens the diff, accepts or reverts one change or all of them. |
142
- | `<slicc-diff-panel>` | The diff of one pending change (`path`), unified or split, with accept and revert. |
141
+ | `<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`; with more than one repository they group under a header per repository (its path and count), in the order the port returns them. 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 code and, below, each command once with a **Copy** button (`data-command`, `data-action="copy-command"`). |
142
+ | `<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. |
143
143
  | `<slicc-terminals>` | Terminal sessions in tabs, each a `<slicc-terminal>` on the session's `TerminalBackend`. Sessions stay alive while hidden; `exit` closes the tab. |
144
144
  | `<slicc-browser>` | Browser tabs with the agent driving each, an address bar, reload, and the active tab's screenshot. |
145
145
  | `<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. |
@@ -148,7 +148,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
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
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`). |
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, or not available with how to get it) 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`, **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. |
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. |
@@ -162,7 +162,7 @@ Keys: <kbd>Alt</kbd>+<kbd>1</kbd>… opens and focuses a panel, <kbd>F6</kbd> an
162
162
 
163
163
  ### Confirmations
164
164
 
165
- Destructive actions ask first: reverting one change or all, deleting a frozen cone, forgetting a memory, disconnecting an account or the tray, and closing a terminal. `confirm({ title, body, action, variant, trigger })`, exported from `dist/slicc-ui.js`, opens a modal alert dialog (1.12 `sp-alert-dialog`, as Gen2 has no dialog yet) inside `<slicc-app>`'s themed shadow root and resolves `true` on the action and `false` on Cancel or <kbd>Esc</kbd>. Focus starts on Cancel and goes back to `trigger` (default: the focused element) when it closes. `variant` is `destructive` (default, a negative action button) for actions that lose data for good, or `confirmation` (accent) for ones you can take back, like disconnecting. Title the dialog with a question, say what can’t be undone in `body`, and name the action with a verb.
165
+ Destructive actions ask first: reverting one change or all, deleting a frozen cone, forgetting a memory, disconnecting an account or the tray, and closing a terminal. `confirm({ title, body, action, variant, cancel, trigger })`, exported from `dist/slicc-ui.js`, opens a modal alert dialog (1.12 `sp-alert-dialog`, as Gen2 has no dialog yet) inside `<slicc-app>`'s themed shadow root and resolves `true` on the action and `false` on Cancel or <kbd>Esc</kbd>. `cancel` renames Cancel, for example “Don’t allow” next to “Allow” in a permission prompt. Focus starts on Cancel and goes back to `trigger` (default: the focused element) when it closes. `variant` is `destructive` (default, a negative action button) for actions that lose data for good, or `confirmation` (accent) for ones you can take back, like disconnecting. Title the dialog with a question, say what can’t be undone in `body`, and name the action with a verb.
166
166
 
167
167
  ```js
168
168
  import { confirm } from './dist/slicc-ui.js';
@@ -200,16 +200,17 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
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`. |
202
202
  | `settings` | local storage | Theme, model, thinking level, composer and diff preferences, the models on offer, and accounts. |
203
+ | `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. |
203
204
  | `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`. |
204
205
  | `monitor` | all of them | A snapshot of vitals, alerts and sections, re-emitted as the system changes, and `resync`. |
205
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`. |
206
207
  | `tray` | the tray protocol | Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`. |
207
- | `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. 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. |
208
- | `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? }`, `via` being `extension`, `proxy` or `null` when browser automation isn't available). Emit `network` when it changes. The optional `check()` tests the route again. Without the port, the indicator and the Network panel don't show. |
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
210
 
210
- The dummy supports `createDummyModel({ updates: 'boot' | '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`.
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
212
 
212
- On phones the tray and the network indicator use compact indicators (their full status remains available on activation), the agent picker shrinks to fit, and the bottom rail scrolls when needed. The update status comes first so progress and failures remain reachable on narrow screens.
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.
213
214
 
214
215
  ### Kernel model
215
216
 
@@ -235,7 +236,7 @@ Each section is its own port, so an embedder can mix real and dummy per section,
235
236
 
236
237
  `<slicc-file-view>` has **Edit**: the file opens in a plain text area, and **Save** or `Mod+S` writes it through `files.write`; `Escape` cancels.
237
238
 
238
- The `kitchen-sink` cone holds every kind of message, content and lick in one conversation, for design work and screenshots. The dummy's fixtures are invented: a small forecast API called harbor, with cones, scoops, a conversation per agent, pending changes, browser tabs and accounts. Its only sprinkles are SLICC's own: `welcome` (the onboarding wizard, as a dip in the sliccy cone) and `suggestions`, copied from `slicc` (`packages/vfs-root/shared/sprinkles/`, commit `aa29784`) with their comments stripped, as is the sprinkle theme in `src/app/sprinkle-theme.css`. `suggestions` reads its stream from the invented `/home/.gelatiere/suggestions.json` through the bridge; there is no `/home/.welcomed`, so `welcome` starts its wizard. Its replies are scripted by keyword (tests, fix or add, open or docs, files, anything else), and their tool calls act on the other ports: an edit writes a file and shows up as a change, a browse opens a tab.
239
+ The `kitchen-sink` cone holds every kind of message, content and lick in one conversation, for design work and screenshots. The dummy's fixtures are invented: a small forecast API called harbor, with cones, scoops, a conversation per agent, pending changes (one of them binary, without a diff), browser tabs and accounts. Its only sprinkles are SLICC's own: `welcome` (the onboarding wizard, as a dip in the sliccy cone) and `suggestions`, copied from `slicc` (`packages/vfs-root/shared/sprinkles/`, commit `aa29784`) with their comments stripped, as is the sprinkle theme in `src/app/sprinkle-theme.css`. `suggestions` reads its stream from the invented `/home/.gelatiere/suggestions.json` through the bridge; there is no `/home/.welcomed`, so `welcome` starts its wizard. Its replies are scripted by keyword (tests, fix or add, open or docs, files, anything else), and their tool calls act on the other ports: an edit writes a file and shows up as a change, a browse opens a tab.
239
240
 
240
241
  ### Distribution
241
242
 
@@ -7,6 +7,38 @@ import {
7
7
  } from "./chunk-T5NHW4KC.js";
8
8
  import "./chunk-AXQFWXYZ.js";
9
9
 
10
+ // src/dummy/changes.ts
11
+ var repos = ["/workspace/harbor", "/workspace/skills"];
12
+ 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.";
13
+ function repoOf(path) {
14
+ return repos.find((repo) => path.startsWith(`${repo}/`));
15
+ }
16
+ var DummyChanges = class {
17
+ #files;
18
+ #git;
19
+ constructor(files3, git) {
20
+ this.#files = files3;
21
+ this.#git = git;
22
+ }
23
+ on = (type, listener) => this.#files.on(type, () => listener(this.changes()));
24
+ changes() {
25
+ if (!this.#git) return [];
26
+ return this.#files.changes().flatMap((change) => {
27
+ const repo = repoOf(change.path);
28
+ return repo ? [{ ...change, agentId: null, repo }] : [];
29
+ });
30
+ }
31
+ accept(path) {
32
+ this.#files.accept(path);
33
+ }
34
+ revert(path) {
35
+ return this.#files.revert(path);
36
+ }
37
+ unavailable() {
38
+ return this.#git ? null : noGit;
39
+ }
40
+ };
41
+
10
42
  // src/dummy/script.ts
11
43
  var units = "/workspace/harbor/src/lib/units.ts";
12
44
  var kelvin = `
@@ -1987,8 +2019,8 @@ var DummyFiles = class extends Emitter {
1987
2019
  for (const [path, text] of Object.entries(contents)) this.#put(path, text, 0);
1988
2020
  for (const change of changes) this.#changes.set(change.path, { ...change });
1989
2021
  for (const change of changes) {
1990
- if (change.after === null) this.#contents.delete(change.path);
1991
- else this.#put(change.path, change.after, 0);
2022
+ if (change.status === "deleted") this.#contents.delete(change.path);
2023
+ else if (change.after !== null) this.#put(change.path, change.after, 0);
1992
2024
  }
1993
2025
  }
1994
2026
  #mkdirs(path) {
@@ -2063,8 +2095,8 @@ var DummyFiles = class extends Emitter {
2063
2095
  if (!change) return;
2064
2096
  await this.#clock.sleep();
2065
2097
  this.#changes.delete(path);
2066
- if (change.before === null) this.#contents.delete(path);
2067
- else this.#put(path, change.before, Date.now());
2098
+ if (change.before !== null) this.#put(path, change.before, Date.now());
2099
+ else if (change.status === "added") this.#contents.delete(path);
2068
2100
  this.#announce(path);
2069
2101
  }
2070
2102
  async #mountFolder(mount, point) {
@@ -3008,6 +3040,7 @@ var files2 = {
3008
3040
  "/workspace/harbor/src/lib/cache.ts": cacheBefore,
3009
3041
  "/workspace/harbor/src/lib/units.ts": units2,
3010
3042
  "/workspace/harbor/src/legacy/xml.ts": legacyXml,
3043
+ "/workspace/harbor/static/radar.png": "\x89PNG\r\n\n",
3011
3044
  "/workspace/harbor/test/units.test.ts": unitsTest,
3012
3045
  "/workspace/skills/release-notes/SKILL.md": skill,
3013
3046
  "/workspace/skills/release-notes/scripts/collect.jsh": collect,
@@ -3053,8 +3086,23 @@ var pending = [
3053
3086
  before: legacyXml,
3054
3087
  after: null,
3055
3088
  agentId: "cone-harbor"
3089
+ },
3090
+ {
3091
+ path: "/workspace/harbor/static/radar.png",
3092
+ status: "modified",
3093
+ before: null,
3094
+ after: null,
3095
+ agentId: "scoop-otter"
3056
3096
  }
3057
3097
  ];
3098
+ var skillEdit = {
3099
+ path: "/workspace/skills/release-notes/SKILL.md",
3100
+ status: "modified",
3101
+ before: skill,
3102
+ after: `${skill}4. Link each change to its pull request.
3103
+ `,
3104
+ agentId: null
3105
+ };
3058
3106
  var models = [
3059
3107
  { id: "claude-opus-5-5", label: "Claude Opus 5.5", provider: "Anthropic", images: true },
3060
3108
  { id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5", provider: "Anthropic", images: true },
@@ -3553,11 +3601,13 @@ var DummyMonitor = class extends Emitter {
3553
3601
  model.agent.on("agents", update);
3554
3602
  model.terminals.on("terminals", update);
3555
3603
  model.browser.on("tabs", update);
3556
- model.files.on("changes", update);
3604
+ const changes = model.changes ?? model.files;
3605
+ changes.on("changes", update);
3557
3606
  model.tray.on("status", update);
3558
3607
  }
3559
3608
  snapshot() {
3560
3609
  const { agent, terminals: terminals2, browser, files: files3, sprinkles: sprinkles2, tray: tray2 } = this.#model;
3610
+ const changes = this.#model.changes ?? files3;
3561
3611
  const listed = agent.list();
3562
3612
  const agents2 = listed.filter((candidate) => candidate.kind === "cone").flatMap((cone) => [cone, ...listed.filter((candidate) => candidate.parentId === cone.id)]);
3563
3613
  const active = agents2.filter((candidate) => candidate.status !== "idle").length;
@@ -3642,7 +3692,11 @@ var DummyMonitor = class extends Emitter {
3642
3692
  {
3643
3693
  id: "changes",
3644
3694
  label: "Pending changes",
3645
- rows: files3.changes().map((change) => ({ name: change.path, meta: change.status, status: "warn" }))
3695
+ rows: (changes.unavailable?.() ? [] : changes.changes()).map((change) => ({
3696
+ name: change.path,
3697
+ meta: change.status,
3698
+ status: "warn"
3699
+ }))
3646
3700
  },
3647
3701
  {
3648
3702
  id: "sprinkles",
@@ -4120,6 +4174,8 @@ function updateFixtures(scenario) {
4120
4174
  });
4121
4175
  items[4].state = "checking";
4122
4176
  Object.assign(items[5], { state: "queued", from: null });
4177
+ } else if (scenario === "starting") {
4178
+ agent.state = "starting";
4123
4179
  } else if (scenario === "available" || scenario === "restart") {
4124
4180
  Object.assign(agent, {
4125
4181
  state: scenario === "available" ? "available" : "ready",
@@ -4146,7 +4202,7 @@ var DummyUpdates = class extends Emitter {
4146
4202
  constructor(scenario, clock2) {
4147
4203
  super();
4148
4204
  this.#items = updateFixtures(scenario);
4149
- this.#ready = scenario !== "boot";
4205
+ this.#ready = scenario !== "boot" && scenario !== "starting";
4150
4206
  this.#clock = clock2;
4151
4207
  }
4152
4208
  list() {
@@ -4157,7 +4213,7 @@ var DummyUpdates = class extends Emitter {
4157
4213
  }
4158
4214
  setScenario(scenario) {
4159
4215
  this.#items = updateFixtures(scenario);
4160
- this.#ready = scenario !== "boot";
4216
+ this.#ready = scenario !== "boot" && scenario !== "starting";
4161
4217
  this.emit("items", this.#items);
4162
4218
  }
4163
4219
  #patch(id, patch) {
@@ -4200,11 +4256,13 @@ function createDummyModel({
4200
4256
  storage = null,
4201
4257
  updates = "current",
4202
4258
  mounts = "off",
4203
- network = "limited"
4259
+ network = "limited",
4260
+ changes = "files"
4204
4261
  } = {}) {
4205
4262
  const time = clock(delay);
4206
4263
  const mount = mounts === "off" ? null : { scenario: mounts, point: mountPoint, files: mounted };
4207
- const files3 = new DummyFiles(files2, directories, pending, time, mount);
4264
+ const pending2 = changes === "git" ? [...pending, skillEdit] : pending;
4265
+ const files3 = new DummyFiles(files2, directories, pending2, time, mount);
4208
4266
  const browser = new DummyBrowser(tabs, time);
4209
4267
  const agent = new DummyAgent(
4210
4268
  agents,
@@ -4234,11 +4292,13 @@ function createDummyModel({
4234
4292
  sprinkles: new DummySprinkles(sprinkles, agent, files3),
4235
4293
  tray: new DummyTray(tray, time),
4236
4294
  updates: new DummyUpdates(updates, time),
4237
- ...network === "off" ? {} : { network: new DummyNetwork(network, time) }
4295
+ ...network === "off" ? {} : { network: new DummyNetwork(network, time) },
4296
+ ...changes === "files" ? {} : { changes: new DummyChanges(files3, changes === "git") }
4238
4297
  };
4239
4298
  return { ...base, monitor: new DummyMonitor(base) };
4240
4299
  }
4241
4300
  export {
4301
+ DummyChanges,
4242
4302
  DummyNetwork,
4243
4303
  DummyUpdates,
4244
4304
  createDummyModel,