@ai-ecoverse/slicc-spectrum 1.99.3 → 1.101.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
@@ -356,6 +356,7 @@ They're also kept in `sessionStorage` (`slicc-spectrum.starting`, so per origin
356
356
  Only a reload of the same tab brings them back (the navigation's type is `reload`); any other load, such as a duplicated tab that got a copy of the storage, clears them, so nothing is sent twice.
357
357
  After a reload the rows come back in order with their text; attachments can't be restored, so a row that had any isn't sent on its own.
358
358
  A row with text and lost files is held (`[data-held]`): it shows an alert icon instead of the clock, a second, muted line "Not sent: tides.csv wasn’t kept after reload." (or "Not sent: 2 files weren’t kept after reload.", `[data-lost]`), **Put back** (`data-action="restore-starting"`), which puts its text into the field (after any draft, on a new line), focuses it and drops the row so the files can be attached again, and **Dismiss**.
359
+ After **Put back**, a muted line under the field with a paperclip icon (`[data-reattach]`) names what to attach: "Attach tides.csv again.", "Attach tides.csv and notes.txt again." and, with more than three, "Attach these 4 files again: …"; it's a quiet button (`data-action="reattach"`) that opens the file picker. It follows the listed files by name: attaching tides.csv leaves "Attach notes.txt again.", other files don't change it, and removing a listed file brings it back; it hides while all of them are attached, and goes once the field is cleared or the message is sent. The list is kept with each agent's draft, and a file is listed once.
359
360
  A row that had only attachments shows the alert icon, its file names and "Not kept after reload", and reads "Not sent. Attach the files again." (`[data-unsent]`) with **Dismiss**.
360
361
  Neither kind is sent on arrival or holds the other rows back, which still go in order; they stay until they're put back or dismissed.
361
362
  Text typed but not sent while it starts is kept for the page the same way, and stays in the field once the agent arrives.
@@ -418,6 +419,7 @@ The Browser rail button carries a dot (`data-driven`) while a tab is in use.
418
419
  #### `<slicc-settings>`
419
420
 
420
421
  Theme, the model and thinking level for new cones, composer and diff preferences (push to talk only where the browser has speech recognition), and accounts to connect, reconnect or disconnect.
422
+ When the `updates` port has `setDeveloper`, an Updates row has the **Developer mode** switch (`sp-switch[data-setting=developer]`, `checked` from `developer()` on every `items`), described as "Check npm for newer releases that aren't certified yet, and offer them as betas in Install / Update."; while `setDeveloper(on)` is pending it is disabled with `aria-busy` and a progress circle, and a rejection shows its message under it (`[data-error]`).
421
423
  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.
422
424
  Accounts with `group: 'mcp'` (MCP servers) get their own **MCP servers** heading (`.accounts[data-group=mcp]`).
423
425
  An `api-key` account asks for its `secret`: “{provider} API key” by default, “{provider} token” for `secret: 'token'`; when it is `expired` it reads **API key rejected** or **Token rejected**.
@@ -472,11 +474,18 @@ It closes, clearing the field, when the request leaves `waiting` or while the va
472
474
  #### `<slicc-updates>`
473
475
 
474
476
  Install / Update: component states, version changes, last checks, download and link counts, action buttons, failures with Retry, and collapsible install logs behind a quiet chevron, which stay open across a layout switch.
477
+ A row with a pending version change (`from → to`, or a package's `version → offered`) and release notes shows a What’s new disclosure after the row's actions (in package rows, after the command chips), next to the Install log, `details[data-releases]` with `summary[data-action="whats-new"]`, which stays open across a layout switch like the log. It lists every release after the installed version up to the new one, newest first, each as `.release[data-version]` with its version linking to the release page (`a[data-action="release-notes"]`, named "Kernel 1.13.0 release notes") and its notes rendered as markdown in strict mode (`markdown(text, { strict: true })`: raw HTML tags are dropped and their text kept as plain text, only `https:` links stay links, and an image becomes a link with its alt text, so nothing loads; the notes' headings sit one level under the release's, through `level`). Notes over 8 KB (8,192 characters) per release are cut at the last line break before that, with "…" and a Read the full notes link (`[data-cut]`, named "Read the full notes for Kernel 1.13.0"); a release whose `url` isn't `https:` shows its version as plain text. The `<!-- cert-verify -->` block is dropped, and a block that starts with "certified" shows as a subtle Certified badge (`[data-certified]`); a heading line naming the version is dropped too, since the link already shows it. Without notes in the range there is no disclosure.
478
+ An up-to-date row (no pending change) links its installed release with Release notes next to the version; an installed optional package without one links its npm page instead (`a[data-action="npm-page"]`, named "npm page for Biome 2.2.4"). Pins and additions in Your packages show no link. Every link opens in a new tab.
475
479
  An install shows one bar for update rows and package rows alike: it fills 0–50% while downloading and 50–100% while linking (`value` against `max-value="100"`). While the row runs it never goes back, not when pnpm's total grows and not when progress drops out between passes. While a download reports `bytes` with a total above 0, the first half fills by bytes and the value label reads "10.0 MB of 40.0 MB". Otherwise, and always while linking, the label falls back to the phase's "n of m" count. The bar starts at 0 again once the row settles.
476
480
  Opens at boot until the agent is ready without failures.
477
481
  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.
478
482
  Every automatic close is saved to the layout.
479
483
  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.
484
+ With a port that has `setDeveloper`, a row's `npm` check shows under its version: "npm: no newer release", "Checking npm…", its `reason` when the check failed (the state light stays), "{installed} (certified) · {latest} on npm, not offered: {reason}" (a certified update keeps its target, "6.6.0 (certified) → 6.7.0 · …", and so does a beta row, "6.13.5 beta · certified 6.6.0 → 6.7.0"), or, when `offered`, "{installed} (certified) · {latest} on npm, not certified" with "12 packages change in /opt/agent." (the folder in the label's parentheses; "1 package changes …"), followed by "@slicc/kernel-bridge and @slicc/vault stay at their certified versions." for `gated` ("… stays at its certified version." for one) and **Install beta**; `article[data-npm]` is the state.
485
+ A row with `beta` has a notice **Beta** badge by its name (`[data-beta]`), a notice "Beta installed" light while it is `current` or `installed` ("Up to date" means the certified version only; Ready to apply, Failed and the running states keep their words), reads "{version} beta · certified {certified}" and offers **Back to certified** next to its other actions; Install beta shows only while `npm` is `offered`.
486
+ While `npm` is `offered`, What’s new covers the releases from the installed version up to npm's `latest`, so an offered beta lists its own notes.
487
+ Install beta and Back to certified are secondary buttons and call `act`; without `setDeveloper` (a mirrored port) neither shows.
488
+ For now bios sets these on the agent row only; the other rows follow with bios.
480
489
  Packages the user added with pnpm in `/opt/user`, or pinned, show up in their own Your packages section below it, under "Packages you added with pnpm in /opt/user. Seven’s updates keep them until you remove them." (plus "Pinned versions stay until you unpin them." while a row offers `unpin`), with a neutral Pinned light (a hold, not a success) or a positive Added light, their version against seven's, and Unpin or Remove.
481
490
  Also available through View and the rails.
482
491
 
@@ -486,8 +495,8 @@ What agents remember, grouped by section: search, filter by scope and tag, expan
486
495
  The scope picker starts on **Everyone** (`*`), which lists every memory with its scope on it (`.scope[data-scope]`); `global` reads **Shared** whatever the port calls it, and the cones and roles follow.
487
496
  The count (“N of M memories”) counts the memories in the picked scope.
488
497
  With a search or tag filter that hides everything it says “No memories match.”, otherwise “No memories yet.”, “No shared memories yet.” or “No memories for {scope} yet.”
489
- Each memory is a row (`li.row[data-id]`, divided from the next by a rule) with one structure open or closed: the title line (`.head`, the disclosure button) keeps its chevron, title and marks where they are, and the text below it (`.content`) shows two lines closed and grows into the full text in place when the row opens, so opening only adds height below. It stays open across a layout switch. Open rows (`data-open`) take no fill, so nothing reads as selected; the chevron turns down. Height and chevron move like Spectrum's accordion (`--swc-animation-duration-100`, ease in and out), and not at all under `prefers-reduced-motion`. A click on a closed row's text opens it too.
490
- An open row ends in an action row at the trailing end: **Edit** and **Forget** (`data-action="edit"`, `data-action="forget"`), Forget in the negative content colour and asking first with the destructive confirm.
498
+ Each memory is a row (`li.row[data-id]`, divided from the next by one straight hairline, a square-ended `--swc-border-width-100` rule in the neutral border colour that spans the row, even where the row's hover fill is rounded) with one structure open or closed: the title line (`.head`, the disclosure button) keeps its chevron, title and marks where they are, and the text below it (`.content`) shows two whole lines closed (the clamp leaves room for one paragraph gap, so a second paragraph's first line is never half cut and doesn't move when the row opens) and grows into the full text in place when the row opens, so opening only adds height below. It stays open across a layout switch. Open rows (`data-open`) take no fill, so nothing reads as selected; the chevron turns down. Height and chevron move like Spectrum's accordion (`--swc-animation-duration-100`, ease in and out), and not at all under `prefers-reduced-motion`. A click on a closed row's text opens it too.
499
+ An open row ends in an action row: first its tag and scope in words next to their icons (`.meta [data-label=tag]`, `[data-label=scope]`, such as “Project” and “harbor only”), since touch has no tooltips, while a closed row shows only the icons; the open row hides its title-line tag and scope icons (`visibility: hidden`, so the title keeps its place) instead of showing them twice; then at the trailing end **Edit** and **Forget** (`data-action="edit"`, `data-action="forget"`), Forget in the negative content colour and asking first with the destructive confirm.
491
500
  At the end of the title line, small icons (`.mark`, `role="img"` with an `aria-label` and a tooltip) carry the metadata: the tag (`.tag[data-tag]`: User a file with a person, Feedback a comment, Project a briefcase), “From MEMORY.md” for free text outside an entry (`.from`), and under Everyone the scope (`.scope[data-scope]`: Shared a group of people, a cone a person, a role a person with a gear, labelled “Shared” or “{name} only”).
492
501
  The tag is left out where the list already says it: under a section heading that names it (Projects, User, Feedback) and while the tag filter picks it.
493
502
  Below 560 px of panel width, the search takes its own line over the two pickers.
@@ -806,7 +815,10 @@ Emit `items` whenever rows or readiness change.
806
815
  Use `queued` for a row that waits for another component before it starts; it reads "Waiting" and doesn't count as updating.
807
816
  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.
808
817
  A `failed` row with an installed `from` version reads "Update failed".
809
- `act(id, action)` resolves when dispatched or rejects with a displayed error; actions are `retry`, `update-now`, `restart-agent` and `reload`.
818
+ `act(id, action)` resolves when dispatched or rejects with a displayed error; actions are `retry`, `update-now`, `restart-agent`, `reload`, `install-beta` and `back-to-certified` (the embedder asks for confirmation itself).
819
+ A row's and a package's optional `releases` (`UpdateRelease[]`) lists the GitHub releases the embedder knows for it, each with its `version`, the release page `url` and optional `notes` (the release body, semantic-release markdown as GitHub returns it). Send every release after the installed version up to the new one, so the panel can show the whole range, plus the installed one for its link; spectrum orders them by version and never fetches them itself. Leave it out, or leave out `notes`, when there's nothing to show.
820
+ Optional `developer()` (false by default) and `setDeveloper(on)`, on the owner's page only, turn developer mode on and off; `setDeveloper` resolves once settled or rejects with a displayed error, and the port emits `items` when either changes.
821
+ A row's optional `beta` (`{ version, certified }`) is set while a beta is installed, and its optional `npm` (`{ state, latest?, reason?, changes?: { count, gated } }`, `state` one of `checking`, `current`, `offered`, `not-offered` or `failed`) is `null` while developer mode is off.
810
822
  Missing ports preserve existing adapters and report disconnected information in the panel.
811
823
  Optional `packages()` returns `PackageItem` rows for optional tools, each with a short catalog `id`, its npm `package` (shown in the name's tooltip and at the top of its Install log), `label`, a one-sentence `description`, the `commands` it provides, `requires` (other package ids, read as "Uses Python" while it's installed, or "Installs Python too" on a row that isn't installed yet; an installed row leaves out a requirement that isn't installed, since its warning says so), optional `notes` (short plain-text lines shown muted under the row, such as "1 untested dependency: …"; leave it out when there's nothing to say), optional `warnings` (lines with an alert icon, `data-warnings`, such as "Needs Python, which isn't installed.
812
824
  Install Python again to use it."; the dummy adds that one when a required package is removed), `state` (`available`, `queued`, `installing`, `installed`, `outdated`, `updating`, `removing` or `failed`), the installed `version`, the `offered` version and its `size` in bytes, `progress` (`InstallProgress`, or `null` for an indeterminate bar), `error`, `actions` (`install`, `update`, `remove`, `retry`, `unpin`) and an optional `log`.
@@ -867,6 +879,7 @@ An action is a secondary button that stays pending until `act(id, action)` settl
867
879
  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.
868
880
  Emit `notices` whenever they change.
869
881
  A notice with `dismissible: false` has no close button and stays until the port drops it.
882
+ An optional `session: true` marks a notice the embedder keeps for the session; the panel shows it like any other, and other fields pass through.
870
883
  Hooks: `[data-notice="<id>"]`, buttons with `data-action`, `[data-dismiss]` and `[data-error]`.
871
884
 
872
885
  #### `tabs` (optional)
@@ -960,9 +973,11 @@ Pass only the surfaces a follower has to `<slicc-app>` (agents, chat, terminal a
960
973
 
961
974
  The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current' | 'available' | 'restart' | 'failure' })`, also selectable with `?updates=` on `/ui/`.
962
975
  Fixtures stay stable for inspection; `model.updates.setScenario('current')` completes boot.
976
+ The dummy's kernel offers three releases from 1.12.0 to 1.13.0, two of them certified, the agent one release to 0.25.0, and the syntax grammars only their installed release; its uv package offers two releases, Python links its release and the other installed packages their npm pages.
963
977
  Dummy Update now and Retry simulate download and link progress, and a dummy package install reports half its `size` in bytes on the way; Restart agent and Reload only change fixture state, never restart a real process or reload the browser.
964
978
  `createDummyModel({ packages: 'mixed' | 'empty' | 'fail' | 'user' | 'elsewhere' | 'off' })` (or `?packages=` on `/ui/`, off by default) adds invented optional packages in every state: Install and Retry finish after a short wait and install missing requirements too, `fail` makes them fail, `user` adds a pinned agent-kit and two additions (one with a warning) that Unpin and Remove take out of the list, `elsewhere` has git installed for cone with pnpm and TypeScript there at an older version, whose Update keeps it in place, and Biome both from the panel and for cone, whose Remove leaves cone's copy, and removing Python while uv is installed asks through the `confirm` option (the `/ui/` page passes spectrum's `confirm`).
965
- `updateFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`.
979
+ `createDummyModel({ beta: 'offered' | 'installed' | 'blocked' | 'failed' | 'off' })` (or `?beta=` on `/ui/`, off by default) turns developer mode on with an invented agent at 6.6.0 (certified) and 6.13.5 on npm, 12 packages and nothing gated: `offered` offers Install beta, `installed` runs the beta, `blocked` isn't offered because it needs a newer kernel, and `failed` is a failed beta install with Retry and Install beta; `setDeveloper` toggles it after a short wait (checking npm again when turned on), and Install beta and Back to certified simulate a short install.
980
+ `updateFixtures`, `betaFixtures`, `packageFixtures` and `DummyUpdates` are exported from `/dummy`.
966
981
  `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.
967
982
  `createDummyModel({ network: 'ok' | 'limited' | 'failing' | 'off' })` (or `?network=` on `/ui/`, default `limited`) sets the dummy network: slicc on this computer as a linked device (`route: 'link'`, with browser automation through it), the page fetch with CORS failures, or no route, all invented; `off` leaves out the port.
968
983
  `model.network.setScenario()` switches it and Check again re-emits after a short wait.
@@ -1032,6 +1047,7 @@ Its buttons are plain Spectrum buttons that answer at once: Unlock unlocks, and
1032
1047
 
1033
1048
  On phones the update status and the tray connection show only their status light (the tray's budget is hidden) and the network indicator a cloud icon, with the text in their labels and tooltips; the agent picker keeps room for the cone's name, cut with an ellipsis when long, and the bottom rail scrolls when needed.
1034
1049
  The update status comes first so progress and failures remain reachable on narrow screens.
1050
+ While a row has `beta`, the update status shows a notice **Beta** badge (`[data-updates] [data-beta]`) after its light, or alone when nothing else applies, at every size; its tooltip and label add "{label} runs beta {version}, not certified", with the label's folder left out ("Agent runs beta 6.13.5, not certified").
1035
1051
 
1036
1052
  ### Kernel model
1037
1053
 
@@ -1741,8 +1741,8 @@ var DummyAgent = class extends Emitter {
1741
1741
  this.emit("message", { agentId, message: reply });
1742
1742
  const steers = run.steers.splice(0);
1743
1743
  for (const steer of steers) this.#post(agentId, this.#user(steer, "steer"));
1744
- const latest = steers.at(-1);
1745
- ({ steps, suggestion } = script(latest.text.trim(), latest.attachments ?? []));
1744
+ const latest2 = steers.at(-1);
1745
+ ({ steps, suggestion } = script(latest2.text.trim(), latest2.attachments ?? []));
1746
1746
  run.pace = this.#pace(agentId, steps);
1747
1747
  reply = this.#reply(agent.model);
1748
1748
  this.#post(agentId, reply);
@@ -4944,6 +4944,65 @@ var DummyTray = class extends Emitter {
4944
4944
 
4945
4945
  // src/dummy/updates.ts
4946
4946
  var checkedAt = Date.UTC(2026, 9, 8, 8, 42);
4947
+ var tag = (repo, version, notes2) => ({
4948
+ version,
4949
+ url: `https://github.com/harbor-demo/${repo}/releases/tag/v${version}`,
4950
+ ...notes2 ? { notes: notes2 } : {}
4951
+ });
4952
+ var certified = (name2, version) => `
4953
+
4954
+ <!-- cert-verify -->
4955
+ certified @harbor/${name2}@${version} sha256=0f3c9a1e (covers ${version}; downstream: PASS)
4956
+ <!-- /cert-verify -->
4957
+ `;
4958
+ var notes = (repo, version, sections) => `# [${version}](https://github.com/harbor-demo/${repo}/compare/v${version}) (2026-10-07)
4959
+
4960
+ ${sections}`;
4961
+ var releaseFixtures = {
4962
+ kernel: [
4963
+ tag(
4964
+ "harbor-kernel",
4965
+ "1.13.0",
4966
+ notes(
4967
+ "harbor-kernel",
4968
+ "1.13.0",
4969
+ "### Features\n\n* **fs:** watch a folder and get one event per batch of changes\n* **jobs:** `wait -n` returns the first job that finishes"
4970
+ ) + certified("harbor-kernel", "1.13.0")
4971
+ ),
4972
+ tag(
4973
+ "harbor-kernel",
4974
+ "1.12.2",
4975
+ notes(
4976
+ "harbor-kernel",
4977
+ "1.12.2",
4978
+ "### Bug Fixes\n\n* **net:** a slow DNS answer no longer stalls the next request"
4979
+ ) + certified("harbor-kernel", "1.12.2")
4980
+ ),
4981
+ tag(
4982
+ "harbor-kernel",
4983
+ "1.12.1",
4984
+ notes(
4985
+ "harbor-kernel",
4986
+ "1.12.1",
4987
+ "### Bug Fixes\n\n* **tty:** resizing a terminal mid-line keeps the cursor on its column"
4988
+ )
4989
+ ),
4990
+ tag("harbor-kernel", "1.12.0")
4991
+ ],
4992
+ agent: [
4993
+ tag(
4994
+ "harbor-agent",
4995
+ "0.25.0",
4996
+ notes(
4997
+ "harbor-agent",
4998
+ "0.25.0",
4999
+ "### Features\n\n* **tools:** the agent can read a tide table from a CSV file\n\n### Bug Fixes\n\n* **chat:** a long answer keeps its code blocks intact when it is cut"
5000
+ )
5001
+ ),
5002
+ tag("harbor-agent", "0.24.0")
5003
+ ],
5004
+ grammars: [tag("harbor-grammars", "4.5.0")]
5005
+ };
4947
5006
  function updateFixtures(scenario) {
4948
5007
  const components = [
4949
5008
  ["bios", "BIOS packages", "0.8.2"],
@@ -4963,7 +5022,8 @@ function updateFixtures(scenario) {
4963
5022
  to: version,
4964
5023
  checkedAt,
4965
5024
  error: null,
4966
- actions: []
5025
+ actions: [],
5026
+ ...releaseFixtures[kind] ? { releases: releaseFixtures[kind] } : {}
4967
5027
  }));
4968
5028
  const agent = items[2];
4969
5029
  if (scenario === "boot") {
@@ -5001,7 +5061,89 @@ function updateFixtures(scenario) {
5001
5061
  }
5002
5062
  return items;
5003
5063
  }
5004
- var offer = (id, label2, description, commands, offered, size, requires) => ({
5064
+ var pin = "6.6.0";
5065
+ var latest = "6.13.5";
5066
+ var betaReleases = [
5067
+ tag(
5068
+ "harbor-agent",
5069
+ "6.13.5",
5070
+ notes(
5071
+ "harbor-agent",
5072
+ "6.13.5",
5073
+ "### Bug Fixes\n\n* **tools:** a tide table with a blank row no longer stops the import"
5074
+ )
5075
+ ),
5076
+ tag(
5077
+ "harbor-agent",
5078
+ "6.10.0",
5079
+ notes(
5080
+ "harbor-agent",
5081
+ "6.10.0",
5082
+ "### Features\n\n* **chat:** answers can quote a buoy reading with its time and place"
5083
+ )
5084
+ ),
5085
+ tag(
5086
+ "harbor-agent",
5087
+ "6.6.0",
5088
+ notes("harbor-agent", "6.6.0", "### Bug Fixes\n\n* **chat:** a pasted chart keeps its axes") + certified("harbor-agent", "6.6.0")
5089
+ )
5090
+ ];
5091
+ var offered = { state: "offered", latest, changes: { count: 12, gated: [] } };
5092
+ var blocked = {
5093
+ state: "not-offered",
5094
+ latest,
5095
+ reason: "it needs kernel 1.14.0 or later"
5096
+ };
5097
+ var betaFailed = "Couldn\u2019t install the beta: pnpm stopped with an error, shown in the install log. Retry.";
5098
+ function betaActions(item) {
5099
+ return [
5100
+ ...item.beta ? ["back-to-certified"] : [],
5101
+ ...item.npm?.state === "offered" ? ["install-beta"] : []
5102
+ ];
5103
+ }
5104
+ function withNpm(items, npm) {
5105
+ return items.map((item) => {
5106
+ if (item.kind !== "agent") return item;
5107
+ const next = { ...item, npm };
5108
+ const kept = item.actions.filter(
5109
+ (action) => action !== "install-beta" && action !== "back-to-certified"
5110
+ );
5111
+ return { ...next, actions: [...kept, ...betaActions(next)] };
5112
+ });
5113
+ }
5114
+ function betaFixtures(items, scenario) {
5115
+ return items.map((item) => {
5116
+ if (item.kind !== "agent") return item;
5117
+ const base = {
5118
+ ...item,
5119
+ from: pin,
5120
+ to: pin,
5121
+ beta: null,
5122
+ npm: offered,
5123
+ releases: betaReleases
5124
+ };
5125
+ const next = scenario === "installed" ? {
5126
+ ...base,
5127
+ from: latest,
5128
+ to: latest,
5129
+ beta: { version: latest, certified: pin },
5130
+ npm: { state: "current", latest }
5131
+ } : scenario === "blocked" ? { ...base, npm: blocked } : scenario === "failed" ? {
5132
+ ...base,
5133
+ state: "failed",
5134
+ to: latest,
5135
+ error: betaFailed,
5136
+ log: `pnpm add @slicc/demo-agent@${latest}
5137
+ ERR_PNPM_FETCH_TIMEOUT Request timed out after 30000ms`
5138
+ } : base;
5139
+ const extra = betaActions(next);
5140
+ return {
5141
+ ...next,
5142
+ actions: [...scenario === "failed" ? ["retry"] : item.actions, ...extra]
5143
+ };
5144
+ });
5145
+ }
5146
+ var offer = (id, label2, description, commands, offered2, size, requires) => ({
5005
5147
  id,
5006
5148
  package: `@harbor/${id}-wasm`,
5007
5149
  label: label2,
@@ -5010,14 +5152,14 @@ var offer = (id, label2, description, commands, offered, size, requires) => ({
5010
5152
  ...requires ? { requires } : {},
5011
5153
  state: "available",
5012
5154
  version: null,
5013
- offered,
5155
+ offered: offered2,
5014
5156
  size,
5015
5157
  progress: null,
5016
5158
  error: null,
5017
5159
  actions: ["install"]
5018
5160
  });
5019
5161
  var fetchFailed = "The download stopped. Check the connection and try again.";
5020
- var yours = (project, name2, description, version, pin, change = {}) => ({
5162
+ var yours = (project, name2, description, version, pin2, change = {}) => ({
5021
5163
  id: `user:${project}:${name2}`,
5022
5164
  package: name2,
5023
5165
  label: name2.replace(/^@[^/]+\//, ""),
@@ -5028,9 +5170,9 @@ var yours = (project, name2, description, version, pin, change = {}) => ({
5028
5170
  offered: null,
5029
5171
  progress: null,
5030
5172
  error: null,
5031
- actions: [pin.base === null ? "remove" : "unpin"],
5173
+ actions: [pin2.base === null ? "remove" : "unpin"],
5032
5174
  origin: "user",
5033
- pin: { ...pin, project: `/opt/${project}` },
5175
+ pin: { ...pin2, project: `/opt/${project}` },
5034
5176
  ...change
5035
5177
  });
5036
5178
  var userPackages = () => [
@@ -5118,7 +5260,28 @@ function packageFixtures(scenario) {
5118
5260
  notes: ["1 untested dependency: @harbor/wasi-sockets@0.4.2-1"],
5119
5261
  actions: ["remove"]
5120
5262
  });
5121
- patch("uv", { state: "outdated", version: "0.8.1", actions: ["update", "remove"] });
5263
+ patch("python", {
5264
+ releases: [
5265
+ { version: "3.13.1", url: "https://github.com/harbor-demo/python-wasm/releases/tag/v3.13.1" }
5266
+ ]
5267
+ });
5268
+ patch("uv", {
5269
+ state: "outdated",
5270
+ version: "0.8.1",
5271
+ actions: ["update", "remove"],
5272
+ releases: [
5273
+ {
5274
+ version: "0.9.2",
5275
+ url: "https://github.com/harbor-demo/uv-wasm/releases/tag/v0.9.2",
5276
+ notes: "## 0.9.2\n\n### Bug Fixes\n\n* `uv run` finds the project in a parent folder again"
5277
+ },
5278
+ {
5279
+ version: "0.9.0",
5280
+ url: "https://github.com/harbor-demo/uv-wasm/releases/tag/v0.9.0",
5281
+ notes: "## 0.9.0\n\n### Features\n\n* `uv add` keeps the comments in `pyproject.toml`"
5282
+ }
5283
+ ]
5284
+ });
5122
5285
  patch("esbuild", {
5123
5286
  state: "installing",
5124
5287
  actions: [],
@@ -5158,9 +5321,16 @@ var DummyUpdates = class extends Emitter {
5158
5321
  #packages = [];
5159
5322
  #fail = false;
5160
5323
  #confirm;
5324
+ #beta;
5325
+ #developer = true;
5161
5326
  constructor(scenario, clock2, options = {}) {
5162
5327
  super();
5163
- this.#items = updateFixtures(scenario);
5328
+ this.#beta = options.beta;
5329
+ this.#items = this.#fixtures(scenario);
5330
+ if (options.beta) {
5331
+ this.developer = () => this.#developer;
5332
+ this.setDeveloper = (on) => this.#setDeveloper(on);
5333
+ }
5164
5334
  this.#ready = scenario !== "boot" && scenario !== "starting";
5165
5335
  this.#clock = clock2;
5166
5336
  this.#confirm = options.confirm;
@@ -5290,8 +5460,32 @@ Done in 3.8s`
5290
5460
  ready() {
5291
5461
  return this.#ready;
5292
5462
  }
5463
+ #fixtures(scenario) {
5464
+ const items = updateFixtures(scenario);
5465
+ if (!this.#beta) return items;
5466
+ const fixed = betaFixtures(items, this.#beta);
5467
+ return this.#developer ? fixed : withNpm(fixed, null);
5468
+ }
5469
+ async #setDeveloper(on) {
5470
+ await this.#clock.sleep(4);
5471
+ this.#developer = on;
5472
+ this.#npm(on ? { state: "checking" } : null);
5473
+ if (!on) return;
5474
+ await this.#clock.sleep(4);
5475
+ const agent = this.#items.find((item) => item.kind === "agent");
5476
+ this.#npm(this.#check(agent.from));
5477
+ }
5478
+ #check(version) {
5479
+ if (!this.#developer) return null;
5480
+ if (version === latest) return { state: "current", latest };
5481
+ return this.#beta === "blocked" ? blocked : offered;
5482
+ }
5483
+ #npm(npm) {
5484
+ this.#items = withNpm(this.#items, npm);
5485
+ this.emit("items", this.#items);
5486
+ }
5293
5487
  setScenario(scenario) {
5294
- this.#items = updateFixtures(scenario);
5488
+ this.#items = this.#fixtures(scenario);
5295
5489
  this.#ready = scenario !== "boot" && scenario !== "starting";
5296
5490
  this.emit("items", this.#items);
5297
5491
  }
@@ -5302,7 +5496,9 @@ Done in 3.8s`
5302
5496
  async act(id, action) {
5303
5497
  const item = this.#items.find((candidate) => candidate.id === id);
5304
5498
  if (!item?.actions.includes(action)) throw new Error(`Update action unavailable: ${id}`);
5305
- if (action === "update-now" || action === "retry") {
5499
+ const back = action === "back-to-certified";
5500
+ const beta = back || action === "install-beta" || action === "retry" && item.to === latest;
5501
+ if (action === "update-now" || action === "retry" || beta) {
5306
5502
  this.#patch(id, {
5307
5503
  state: "downloading",
5308
5504
  progress: { phase: "download", done: 0, total: 95 },
@@ -5318,13 +5514,30 @@ Done in 3.8s`
5318
5514
  });
5319
5515
  await this.#clock.sleep(2);
5320
5516
  }
5517
+ if (beta) {
5518
+ const version = back ? pin : latest;
5519
+ const next = {
5520
+ ...this.#items.find((candidate) => candidate.id === id),
5521
+ state: "ready",
5522
+ from: version,
5523
+ to: version,
5524
+ progress: null,
5525
+ error: null,
5526
+ checkedAt,
5527
+ beta: back ? null : { version, certified: pin },
5528
+ npm: this.#check(version)
5529
+ };
5530
+ this.#patch(id, { ...next, actions: ["restart-agent", ...betaActions(next)] });
5531
+ return;
5532
+ }
5321
5533
  const apply = action === "update-now" && ["agent", "bios", "kernel", "ui"].includes(item.kind);
5322
5534
  this.#patch(id, {
5323
5535
  state: apply ? "ready" : "installed",
5324
5536
  from: apply ? item.from : item.to,
5325
5537
  progress: null,
5326
5538
  checkedAt,
5327
- actions: apply ? [item.kind === "agent" ? "restart-agent" : "reload"] : []
5539
+ actions: apply ? [item.kind === "agent" ? "restart-agent" : "reload"] : [],
5540
+ ...apply ? {} : { actions: betaActions(item) }
5328
5541
  });
5329
5542
  }
5330
5543
  };
@@ -5431,7 +5644,7 @@ function refused(settings) {
5431
5644
  if (bad)
5432
5645
  throw vaultFailure("EINVAL", `The vault refused ${bad[0]}. Check that value and try again.`);
5433
5646
  }
5434
- function accountRecords(now2 = Date.now(), pin) {
5647
+ function accountRecords(now2 = Date.now(), pin2) {
5435
5648
  return [
5436
5649
  {
5437
5650
  name: "MCP_DOCS_TOKEN",
@@ -5441,7 +5654,7 @@ function accountRecords(now2 = Date.now(), pin) {
5441
5654
  source: "settings",
5442
5655
  sourceDevice: null,
5443
5656
  server: "docs",
5444
- ...pin ? { url: pin, pinned: true } : {},
5657
+ ...pin2 ? { url: pin2, pinned: true } : {},
5445
5658
  domains: ["docs.example"],
5446
5659
  rules: [{ domain: "docs.example", header: "Authorization", methods: [], paths: [] }],
5447
5660
  exec: false,
@@ -6022,11 +6235,11 @@ var DummyVault = class extends Emitter {
6022
6235
  await this.#time.sleep(5);
6023
6236
  this.#unlocked();
6024
6237
  for (const name2 of names) {
6025
- const pin = this.#list.audit.find(
6238
+ const pin2 = this.#list.audit.find(
6026
6239
  (entry) => entry.type === "endpoints" && entry.pinned?.name === name2
6027
6240
  );
6028
- if (pin?.type === "endpoints" && pin.pinned)
6029
- this.#log(name2, { type: "endpoints", unpinned: pin.pinned });
6241
+ if (pin2?.type === "endpoints" && pin2.pinned)
6242
+ this.#log(name2, { type: "endpoints", unpinned: pin2.pinned });
6030
6243
  }
6031
6244
  this.emit("list", this.#list);
6032
6245
  }
@@ -6063,20 +6276,20 @@ var DummyVault = class extends Emitter {
6063
6276
  password: !!options.password,
6064
6277
  ...pick ? { pick: [...pick] } : {}
6065
6278
  });
6066
- const offered = kind === "keepass" ? [...candidates, duplicate] : candidates;
6067
- if (!pick) return { stage: "pick", candidates: offered.map((item) => ({ ...item })) };
6279
+ const offered2 = kind === "keepass" ? [...candidates, duplicate] : candidates;
6280
+ if (!pick) return { stage: "pick", candidates: offered2.map((item) => ({ ...item })) };
6068
6281
  const chosen = new Map(
6069
6282
  pick.map(
6070
6283
  (item) => typeof item === "string" ? [item, "secret"] : [item.name, item.kind]
6071
6284
  )
6072
6285
  );
6073
- const taken = offered.filter((item) => chosen.has(item.name) && !item.refused);
6286
+ const taken = offered2.filter((item) => chosen.has(item.name) && !item.refused);
6074
6287
  return {
6075
6288
  stage: "done",
6076
6289
  imported: taken.map((item) => this.#seal(item.name, `import:${kind}`, chosen.get(item.name))),
6077
- skipped: offered.filter((item) => !taken.includes(item)).map((item) => item.name),
6290
+ skipped: offered2.filter((item) => !taken.includes(item)).map((item) => item.name),
6078
6291
  reasons: Object.fromEntries(
6079
- offered.flatMap((item) => item.refused ? [[item.name, item.refused]] : [])
6292
+ offered2.flatMap((item) => item.refused ? [[item.name, item.refused]] : [])
6080
6293
  )
6081
6294
  };
6082
6295
  }
@@ -6167,6 +6380,7 @@ function createDummyModel({
6167
6380
  updates = "current",
6168
6381
  packages = "off",
6169
6382
  confirm,
6383
+ beta = "off",
6170
6384
  mounts = "off",
6171
6385
  network = "limited",
6172
6386
  tailnet,
@@ -6247,6 +6461,7 @@ function createDummyModel({
6247
6461
  tray: new DummyTray(tray, time),
6248
6462
  updates: new DummyUpdates(updates, time, {
6249
6463
  ...packages === "off" ? {} : { packages },
6464
+ ...beta === "off" ? {} : { beta },
6250
6465
  confirm
6251
6466
  }),
6252
6467
  ...network === "off" ? {} : { network: new DummyNetwork(network, time, tailnet, links, files3, leaders) },
@@ -6284,6 +6499,7 @@ export {
6284
6499
  DummyVaultButton,
6285
6500
  actionLimits,
6286
6501
  approvalFixtures,
6502
+ betaFixtures,
6287
6503
  browserHistory,
6288
6504
  createDummyModel,
6289
6505
  createGuestModel,