@ai-ecoverse/slicc-spectrum 1.110.0 → 1.111.1
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 +29 -9
- package/dist/slicc-dummy.js +47 -0
- package/dist/slicc-dummy.js.map +3 -3
- package/dist/slicc-ui.js +477 -181
- package/dist/slicc-ui.js.map +3 -3
- package/dist/types/app/base.d.ts +1 -0
- package/dist/types/app/sprinkle.d.ts +8 -2
- package/dist/types/dummy/sprinkles.d.ts +1 -0
- package/dist/types/model/types.d.ts +21 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -255,9 +255,9 @@ The chevron only expands and collapses; a click on the row, <kbd>Enter</kbd> or
|
|
|
255
255
|
<kbd>→</kbd> expands a cone and then moves to its first scoop, <kbd>←</kbd> moves from a scoop to its cone and then collapses it, and neither changes the active agent.
|
|
256
256
|
Several cones can be open at once, and they stay open for every view of the panel on the same model, so a layout switch keeps them; when the active agent has changed since the panel last showed it, its cone opens again as the panel mounts, so on a phone a cone you tapped still shows its scoops when you come back to Agents, and a cone you closed stays closed.
|
|
257
257
|
When the active scoop's cone is collapsed, the cone row takes the Tab stop.
|
|
258
|
-
A row's first line starts with
|
|
258
|
+
A row's first line starts with a dot coloured by status, always in the same place, and, when there are unread messages, the unread count beside it (`99+` from a hundred up, at `--swc-font-size-50`, white on the accent fill, or on the negative fill when the cone is in error).
|
|
259
259
|
Then comes the name; the context fill, when it's high, sits in the top right corner.
|
|
260
|
-
Every row has up to two lines, aligned to the top under the name,
|
|
260
|
+
Every row has up to two lines of conversation title, aligned to the top under the name, with no status word, so nothing moves when the status changes; the accessible name and the tooltip say the status and the exact unread count.
|
|
261
261
|
A cone with scoops has a ribbon line below: one segment per scoop, coloured by status at 3:1 or more on every row background, then its scoops in words, naming only the busy ones, as in "2 scoops · 1 waiting" ("2 scoops" when all are idle).
|
|
262
262
|
Its `aria-description` sums up every status, as in "2 scoops: 1 working, 1 idle".
|
|
263
263
|
Rows without scoops or errors have no empty ribbon or error line.
|
|
@@ -298,7 +298,7 @@ While pending, cards leave the log for their own region between the header and t
|
|
|
298
298
|
Clicking a waiting card (not its controls), or <kbd>Enter</kbd> on it, scrolls the log to where it was asked, at the bottom of the view above the composer.
|
|
299
299
|
When a card leaves the region (answered, timed out or decided elsewhere) while it has focus, focus moves to the newest card still waiting, else the composer; a timed-out card goes back to its place in the log.
|
|
300
300
|
Under `prefers-reduced-motion`, nothing animates and the scroll is instant.
|
|
301
|
-
The header is one row: the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false` (with no model on offer, **Set up a model**, `data-action="set-up-model"`, opens Settings), 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.
|
|
301
|
+
The header is one row: a dot coloured by status (`data-agent-status`, with the status in its `aria-label` and tooltip, and no word, so the pickers never move; in forced colours it paints `CanvasText`, like the Agents markers), a model picker that names each model's provider and leaves out classifiers and models with `tools: false` (with no model on offer, **Set up a model**, `data-action="set-up-model"`, opens Settings), 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.
|
|
302
302
|
It never wraps: when it's short of room, the model picker's label is cut with an ellipsis, and under 560px wide (`header[data-compact]`) New conversation is icon-only, with `accessible-label` and its tooltip.
|
|
303
303
|
The pickers re-read `models()` on `settings`, `accounts` and `agents`.
|
|
304
304
|
When a live agent's model, or `Settings.model`, is no longer offered and its provider (the id before `/`) isn't a connected account, it moves to the offered chat model with `default: true`, else the first one; frozen agents are left alone, and so is everything on a follower unless it has Control and is connected.
|
|
@@ -560,10 +560,10 @@ A rejection shows the error card with its sentence (a check without a `preview`
|
|
|
560
560
|
|
|
561
561
|
A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit, even when it loads out of view (the frame measures itself on load, after icons and fonts arrive, and on every resize).
|
|
562
562
|
A dip has a tab-like handle: drag it into the dock (or onto a rail, or use its open-as-panel button, `data-action="detach-sprinkle"`, an extra-small action button whose tap target pads out to 24 × 24 px on a coarse pointer while it looks the same) and it lives on as a sprinkle panel, with a note in the chat where it was and a **Show** button (`data-action="show-sprinkle"`) that brings the panel forward; which dips are out is saved in `slicc-ui.dips`.
|
|
563
|
-
It runs in a sandboxed frame (`allow-scripts`
|
|
564
|
-
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
|
|
563
|
+
It runs in a sandboxed frame (`allow-scripts allow-forms`) with SLICC's sprinkle theme (the `--s2-*` tokens and `.sprinkle-*` classes), the app's fonts, and Lucide icons (`<i data-lucide>`, `LucideIcons.render()`).
|
|
564
|
+
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, keeps state and runs its own `cgi-bin/` scripts (see [Sprinkle bridge](#sprinkle-bridge)).
|
|
565
565
|
Panel sprinkles sit at the top of the right rail with their Lucide icon.
|
|
566
|
-
On a guest's page (the model's `seat` set), every sprinkle, inline or panel, is view only: its frame is inert, it says "View only on this device",
|
|
566
|
+
On a guest's page (the model's `seat` set), every sprinkle, inline or panel, is view only: its frame is inert, it says "View only on this device", its licks and `setState` calls go nowhere, and its `cgi-bin/` answers every request with a `403`.
|
|
567
567
|
|
|
568
568
|
#### `<slicc-network>`
|
|
569
569
|
|
|
@@ -714,6 +714,25 @@ A refusal or a missing file rejects with an `Error`.
|
|
|
714
714
|
There is no `exec`: a sprinkle that needs a command asks its cone with `slicc.lick()`.
|
|
715
715
|
Without `call`, none of the four exist, so guard them (`typeof slicc.readFile === 'function'`) and fall back.
|
|
716
716
|
|
|
717
|
+
#### `cgi-bin/`
|
|
718
|
+
|
|
719
|
+
A sprinkle's `fetch()` of a URL under `new URL('cgi-bin/', document.baseURI)` runs the sprinkle's own script through the port's `cgi`, so `fetch('cgi-bin/items')` answers with what `<sprinkle>/cgi-bin/items` prints, CGI/1.1 style.
|
|
720
|
+
Every other URL goes to the frame's own `fetch`, unchanged.
|
|
721
|
+
The answer is a real `Response`: `status`, `statusText`, `headers`, `url`, and a body that streams as the script prints, so `json()`, `text()` and `body.getReader()` all work; a `HEAD` and a `101`, `103`, `204`, `205` or `304` have no body.
|
|
722
|
+
The request goes as `{ method, path, query, headers, body }`, as the `Request` constructor made it (a string body gets `text/plain;charset=UTF-8`, a `FormData` its multipart boundary), with `Accept: */*` and the browser's `Accept-Language` when the sprinkle set none; `path` is what follows `cgi-bin/`, still percent-encoded, `query` has no `?`, and the body is base64.
|
|
723
|
+
A `301`, `302`, `303`, `307` or `308` with `Location` is followed as `fetch` follows redirects (a `303`, or a `301` or `302` after a `POST`, turns into a `GET` without a body), up to 20; `redirect: 'manual'` returns the redirect itself and `redirect: 'error'` rejects.
|
|
724
|
+
Aborting the `fetch` (its `signal`) or cancelling the body stops the script (`cgiClose`); a failed `cgi` or `cgiRead` rejects with a `TypeError`, as a network error does.
|
|
725
|
+
|
|
726
|
+
A form never navigates the frame: the bridge cancels every `submit` (except `method="dialog"`) and `form.submit()`.
|
|
727
|
+
A form whose action (or its submitter's `formaction`) is under `cgi-bin/` goes the same way, with its `formmethod`, and as `enctype` says: `application/x-www-form-urlencoded` by default, `multipart/form-data` or `text/plain`; a `GET` form replaces the action's query.
|
|
728
|
+
The bridge cancels the browser's own `submit` before any page handler runs and stops it there, then dispatches a copy (a `SubmitEvent` with the same `submitter`, so `isTrusted` is `false`) to the form, so stopping the copy's propagation can't navigate the frame.
|
|
729
|
+
A handler that calls `preventDefault()` on that copy, on the form or above it, or an `onsubmit` that returns `false`, keeps the bridge from sending it.
|
|
730
|
+
The form then gets a cancelable `slicc-cgi` event with `detail.response` (or `detail.error`); unless it's cancelled, an answer with `Location` rebuilds the sprinkle, with its latest state.
|
|
731
|
+
|
|
732
|
+
The element sends `cgi` with its own sprinkle id, never one from the frame, posts the head as soon as the port answers, then calls `cgiRead` until `done`.
|
|
733
|
+
A guest seat gets a `403` for every request, a frame without `call` a `404`, and a read-only, watch-only or frozen frame may `GET` and `HEAD`; any other method gets a `403` without reaching the port.
|
|
734
|
+
The port does every other check (paths, ownership, limits).
|
|
735
|
+
|
|
717
736
|
### Panel command
|
|
718
737
|
|
|
719
738
|
`panelCommand(app, argv, caller)`, exported from `dist/slicc-ui.js`, runs `panel` for an agent or a terminal, so the agent can arrange the UI.
|
|
@@ -833,8 +852,9 @@ Sprinkles (`inline` ones show as dips in the chat, not in the rail) and `send`,
|
|
|
833
852
|
The rail groups sprinkles by their `agentId`: the selected cone's come first, then the other cones' in the agents list's order, each group set apart by a line (`.rail .cones`) and each button carrying `data-cone`.
|
|
834
853
|
A sprinkle of another cone reads `{cone name} / {title}` in its tooltip, its label and the tab it opens; the name comes from the agents list, so give each sprinkle an `id` that stays stable when a cone is renamed, such as `{coneId}/{name}`.
|
|
835
854
|
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.
|
|
836
|
-
|
|
837
|
-
The
|
|
855
|
+
`cgi` (`[{ method, path, query, headers, body }]`, with `headers` as `[name, value]` pairs and `body` base64 or `null`) resolves once the script's CGI headers are in, with `{ status, statusText, headers, body, done, stream }`; `cgiRead` (`[stream]`) long-polls for `{ body, done }`, and `cgiClose` (`[stream]`) stops the script (see [`cgi-bin/`](#cgi-bin)).
|
|
856
|
+
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 the first four methods from its own frame's `slicc-call`; the `cgi` ones only through its `fetch` and forms.
|
|
857
|
+
The dummy keeps state in memory and maps `/shared/x` to `/home/x` in its files, refusing anything outside `/home`; it answers every `cgi` with a canned `200` `application/json` (`X-Slicc-Cgi: dummy`) that echoes the method, path, query, content type and body, in two chunks.
|
|
838
858
|
|
|
839
859
|
#### `tray`
|
|
840
860
|
|
|
@@ -981,7 +1001,7 @@ The vault's view for SLICC, typed after `@ai-ecoverse/slicc-vault` (spectrum re-
|
|
|
981
1001
|
Emit `status` and `list` when they change (bios re-lists after the vault's `changed` events), and `availability` when the optional `availability()` (`connecting`, `waiting`, `locked` or `ready`) changes.
|
|
982
1002
|
`audit(before?, limit?)` resolves with an older `AuditPage` (`before` is a `seq`).
|
|
983
1003
|
The optional page ops, each hiding its control when missing: `put(name, value, request?)` (a value request's `put` must use its `name`), `cancel(request)`, `remove(name, kind?)`, `restore(name, kind?)`, `import(kind, bytes, { password?, pick? })` (a `pick` stage, then `done`), `export(passphrase)` (the bundle's bytes), `lock()`, `dismissBackupReminder()` and `endpoints({ add?, remove? })`, which registers or takes back the URLs a page-sealed credential may be filled on (each an `EndpointSet`, `{ model?: [{ name, url }], mcp?: [{ server, url }] }`, a model URL bound to one record by name), checks the whole change first (`EINVAL` changes nothing) and rejects with a sentence. Settings registers them itself, never through the agent. `settings(name, kind, settings)` replaces a record's tenant settings as a whole without touching its key (the vault's `put` without a value); `put`'s options take the same `settings` on a first save.
|
|
984
|
-
They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`).
|
|
1004
|
+
They reject with a `VaultFailure`, an `Error` with an `ErrorCode` `code` (`isVaultFailure` from `/ui`) and an optional `ErrorCause` `cause`, matching slicc-vault 1.22.0’s stable refusal causes; `isPolicyPaused` still identifies only `policy-paused`.
|
|
985
1005
|
Values go to the vault only through `put`, and never through another port.
|
|
986
1006
|
`button()` returns a new `VaultButton`, `{ element, post(message), listen(fn) }` returning an unsubscribe: bios wraps the vault's `button.html` frame and its postMessage protocol, and spectrum only sends `ButtonMessage`s and receives `ButtonEvent`s.
|
|
987
1007
|
The passkey lock (unlock and protect) happens in the vault's own popup, which only a click in that frame can open.
|
package/dist/slicc-dummy.js
CHANGED
|
@@ -4908,11 +4908,29 @@ function homePath(path) {
|
|
|
4908
4908
|
throw new Error(`EACCES: ${text} is outside /home`);
|
|
4909
4909
|
return mapped;
|
|
4910
4910
|
}
|
|
4911
|
+
var base64 = (bytes) => btoa(String.fromCharCode(...bytes));
|
|
4912
|
+
var unbase64 = (text) => new TextDecoder().decode(Uint8Array.from(atob(text), (char) => char.charCodeAt(0)));
|
|
4913
|
+
function cannedCgi(request) {
|
|
4914
|
+
const given = request ?? {};
|
|
4915
|
+
const headers = Array.isArray(given.headers) ? given.headers : [];
|
|
4916
|
+
const type = headers.find(([name2]) => String(name2).toLowerCase() === "content-type");
|
|
4917
|
+
return `${JSON.stringify({
|
|
4918
|
+
ok: true,
|
|
4919
|
+
method: given.method ?? "GET",
|
|
4920
|
+
path: given.path ?? "",
|
|
4921
|
+
query: given.query ?? "",
|
|
4922
|
+
type: type?.[1] ?? null,
|
|
4923
|
+
body: typeof given.body === "string" ? unbase64(given.body) : null
|
|
4924
|
+
})}
|
|
4925
|
+
`;
|
|
4926
|
+
}
|
|
4911
4927
|
var DummySprinkles = class extends Emitter {
|
|
4912
4928
|
#sprinkles;
|
|
4913
4929
|
#agent;
|
|
4914
4930
|
#files;
|
|
4915
4931
|
#state = /* @__PURE__ */ new Map();
|
|
4932
|
+
#streams = /* @__PURE__ */ new Map();
|
|
4933
|
+
#next = 0;
|
|
4916
4934
|
constructor(sprinkles2, agent, files3) {
|
|
4917
4935
|
super();
|
|
4918
4936
|
this.#sprinkles = sprinkles2.map((sprinkle) => ({ ...sprinkle }));
|
|
@@ -4940,10 +4958,39 @@ var DummySprinkles = class extends Emitter {
|
|
|
4940
4958
|
this.#state.set(id, structuredClone(args[0] ?? null));
|
|
4941
4959
|
return void 0;
|
|
4942
4960
|
}
|
|
4961
|
+
if (method === "cgi") return this.#cgi(args[0]);
|
|
4962
|
+
if (method === "cgiRead") return this.#read(String(args[0]));
|
|
4963
|
+
if (method === "cgiClose") {
|
|
4964
|
+
this.#streams.delete(String(args[0]));
|
|
4965
|
+
return null;
|
|
4966
|
+
}
|
|
4943
4967
|
const path = homePath(args[0]);
|
|
4944
4968
|
if (method === "exists") return (await this.#files.list()).some((entry) => entry.path === path);
|
|
4945
4969
|
return this.#files.read(path);
|
|
4946
4970
|
}
|
|
4971
|
+
#cgi(request) {
|
|
4972
|
+
const bytes = new TextEncoder().encode(cannedCgi(request));
|
|
4973
|
+
const half = Math.ceil(bytes.length / 2);
|
|
4974
|
+
const stream = `dummy-cgi-${++this.#next}`;
|
|
4975
|
+
this.#streams.set(stream, bytes.subarray(half));
|
|
4976
|
+
return {
|
|
4977
|
+
status: 200,
|
|
4978
|
+
statusText: "OK",
|
|
4979
|
+
headers: [
|
|
4980
|
+
["Content-Type", "application/json"],
|
|
4981
|
+
["X-Slicc-Cgi", "dummy"]
|
|
4982
|
+
],
|
|
4983
|
+
body: base64(bytes.subarray(0, half)),
|
|
4984
|
+
done: false,
|
|
4985
|
+
stream
|
|
4986
|
+
};
|
|
4987
|
+
}
|
|
4988
|
+
#read(stream) {
|
|
4989
|
+
const rest = this.#streams.get(stream);
|
|
4990
|
+
if (rest === void 0) throw new Error(`there is no cgi-bin stream ${stream}`);
|
|
4991
|
+
this.#streams.delete(stream);
|
|
4992
|
+
return { body: base64(rest), done: true };
|
|
4993
|
+
}
|
|
4947
4994
|
};
|
|
4948
4995
|
|
|
4949
4996
|
// src/dummy/tabs.ts
|