@ai-ecoverse/slicc-spectrum 2.0.2 → 2.1.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
@@ -307,6 +307,7 @@ When a live agent's model, or `Settings.model`, is no longer offered and its pro
307
307
  The chat re-runs this on `follow` and `readOnly` too, and `<slicc-app>` does it as well, when it binds, on those events and on `follow` and `readOnly`, so a new cone that starts on a provider that isn't set up moves at once.
308
308
  The transcript pads its end, and its `scroll-padding-block-end`, by the composer's measured height plus the height its frosted layer fades out over (`--composer-fade`, 72 px, 0 under `prefers-reduced-transparency` and `forced-colors` and without a composer), so at the bottom the last line sits a line above the glass, and a focused element scrolls clear of it.
309
309
  The log follows new content only while it sits within one line (its `line-height`) of the bottom. Scrolling up by wheel, touch, keys or the scrollbar stops that, and the log then keeps its place, whatever re-renders or ticks; content that grows above the view, such as a tool card opening, doesn't move what's in view either. New output while the reader is scrolled up shows **Jump to latest** (`data-action="jump-latest"`), floating over the log's bottom edge above the composer, with the number of new items, "Jump to latest (2 new)", once there are any; it, or scrolling back to the bottom, resumes following, and so does the reader's own send. While it follows, a log that gets shorter (a notice above it, a taller composer, a shorter panel) or a latest card that grows (a lick opened, a sprinkle loading) doesn't scroll that card's top out of view: a card that fits shows whole, and a taller one keeps its top at the top of the log, with the rest under the composer, instead of its end above the composer's padding; a new item, a streaming reply and Jump to latest still go to the very bottom.
310
+ With the agent port's optional `hasOlder(agentId)`, the log pages back through the conversation: while it is less than a screen from its top (which includes a log that doesn't fill the view) and `hasOlder` is true, it calls `older(agentId, id)` with the id of the first message in `messages()`, one load at a time per agent, and renders the earlier messages from `messages()` by id once the port emits `messages`, never from what `older` resolves with. The first turn in view keeps its place: its offset is measured before the render and put back before the next paint (scroll anchoring does nothing at the very top), and the log's follow bookkeeping moves with it, so the jump never reads as the reader scrolling. It loads until the log is more than a screen above the view or the start is reached; an empty answer, or one that leaves the first message as it was, is the start, and the chat doesn't ask again for that agent until its first message changes or it is shown again. While there is more, the top of the log reads "Loading earlier messages…" (`[data-older=loading]`, `aria-busy` while a load runs), and at the start it shows nothing; a rejected `older` shows "Couldn’t load earlier messages." with **Retry** (`[data-older=failed]`, `role="alert"`, `data-action="retry-older"`) instead of trying again. A load that ends after another agent is shown touches only its own conversation. Without `hasOlder` (an older agent), it never calls `older`.
310
311
  While a reply streams, its status icon follows what it is doing: a pencil for an edit, a page for a read, a magnifier for a search, an eye for the browser and an hourglass for any other tool; thinking and text keep the AI logo.
311
312
  Between the header and the transcript, a `<slicc-notices>` shows the model's notices.
312
313
  On a follower without Control, or while the agent port's `readOnly()` returns a line, a note (`p[data-follow="watch"]`, that line first, else the follow-based one) takes the composer's place, the header shows the model's name instead of the pickers and New conversation, and there's no suggested follow-up.
@@ -327,9 +328,10 @@ The model's `notices` as in-line alerts with their actions and a close button (n
327
328
  The model's waiting `approvals` as a modal alert dialog, one at a time, oldest first; `<slicc-app>` mounts it.
328
329
  Renders nothing without `approvals.decide`.
329
330
  **Always allow** (**Always allow in this directory**, with the cwd in its hint, when the approval has `alwaysScope: 'cwd'` and a `cwd`) shows when `offersAlways(approval)` is true, and the element's `decide()` sends `always` only when it holds for the live approval with that id in `list()`, so a stale dialog can't send it after the request lost it. For a sudo request, `offersAlways` is true only when `always` is true and the verdict has a level 1, 2 or 3: a level-less sudo (an older agent) never offers Always allow, even when `always` is true. Guest requests still offer Always allow whenever `always` is true. Tray requests never do.
330
- The command (`[data-command]`, or a guest tool call's `[data-summary]`) wraps in full; past four lines the dialog shows the first four and **Show the full command** (as in the chat, `data-action="full-command"` with `data-shown`), and shown, it scrolls inside its own box of at most `min(16lh, 50dvh)`. Always allow, its hint and Allow once wait until the end of the command has been in view (a zero-size `[data-end]` after its last line), and so does the element's `decide()` for anything but `deny`; once seen, the end stays seen while the request waits, and hiding the command again hides them again. Until then a subdued line (`[data-allow-hint]`) says "Show the full command to allow it." or, shown, "Scroll to the end of the command to allow it." The dialog keeps one width, so only its height changes. A closed request shows its command whole.
331
+ The command (`[data-command]`, or a guest tool call's `[data-summary]`) shows whole and wraps, in its own box of at most `min(16lh, 50dvh)` that scrolls past that; the dialog has no **Show the full command**. Always allow and Allow once stay disabled until the end of the command has been in view (a zero-size `[data-end]` after its last line), which is at once when the command fits its box, and so does the element's `decide()` for anything but `deny`; once seen, the end stays seen while the request waits. While the end is out of view a subdued line (`[data-allow-hint]`) says "Scroll to the end of the command to allow it." The dialog keeps one width. A closed request shows its command whole.
332
+ While a request shows, its buttons stay put: Deny sits at the start of the row and Allow once at its end, so Always allow appearing or going moves neither, and the verdict, its note, the allow hint and Always allow's hint (`[data-always-hint]`, shown whenever Always allow is offered) share one box of fixed height (`[data-notes]`, `min(9lh, 30dvh)`, which scrolls when they overflow it; the level block fills its free space), so nothing above the buttons changes height as the verdict and its reason arrive. "{n} more requests are waiting." (`[data-more]`) has its line kept even while none wait.
331
333
  Control characters in the sudo command, target, requester name and place, and cwd are shown: C0 controls use Unicode Control Pictures (including ␍ for CR, ␊ for LF and ␉ for TAB), DEL uses ␡, and C1, format, default-ignorable characters and line/paragraph separators use uppercase `U+XXXX` notation (at least four digits). LF keeps its line break after ␊ in the command and shows only ␊ in paragraphs; ordinary spaces stay spaces. Each shown control has a chip with its character name or notation as its accessible label and title; Control Pictures also show a readable abbreviation or notation. Joiners and variation selectors use the same chips. The clamped and full views show the same symbols.
332
- Sudo and guest requests show the verdict under the command, in a live region (`.judge[aria-live=polite]`). A level verdict is a block (`.judged[data-verdict][data-level]`): level 1 "Routine." (`routine`, positive subtle background), level 2 "Changes permissions." (`permissions`, notice) or level 3 "Could harm the system." (`harm`, negative, with the alert-triangle icon), then the reason, then "It fits what you asked for." or "It isn’t clear it serves what you asked for." when `aligned` is set. While `judging` it reads "Checking…" (`checking`); with no verdict and a `verdictError`, "No verdict: {verdictError}" (`none`). A verdict with only a `decision` keeps the line "The automatic check would {decision} this: {reason}" or "The automatic check wasn’t sure: {reason}" (`p.verdict[data-verdict]`, the decision in the attribute). The reason and `verdictError` show control characters as chips, as in the command, and are cut at 300 characters. A sudo request without Always allow says "Always allow isn’t offered for permission changes." at level 2 and "Always allow appears only with a level 1 verdict." with no verdict (`[data-verdict-note]`). No level adds a confirm step: Deny and Allow once work at every level.
334
+ Sudo and guest requests show the verdict under the command, in a live region (`.judge[aria-live=polite]`) inside `[data-notes]`. A level verdict is a block (`.judged[data-verdict][data-level]`): level 1 "Routine." (`routine`, positive subtle background), level 2 "Changes permissions." (`permissions`, notice) or level 3 "Could harm the system." (`harm`, negative, with the alert-triangle icon), then the reason, then "It fits what you asked for." or "It isn’t clear it serves what you asked for." when `aligned` is set. While `judging` it reads "Checking…" (`checking`); with no verdict and a `verdictError`, "No verdict: {verdictError}" (`none`). A verdict with only a `decision` keeps the line "The automatic check would {decision} this: {reason}" or "The automatic check wasn’t sure: {reason}" (`p.verdict[data-verdict]`, the decision in the attribute). The reason and `verdictError` show control characters as chips, as in the command, and are cut at 300 characters. A sudo request without Always allow says "Always allow isn’t offered for permission changes." at level 2 and "Always allow appears only with a level 1 verdict." with no verdict (`[data-verdict-note]`). No level adds a confirm step: Deny and Allow once work at every level.
333
335
  Focus starts on Deny (on Close once `decide` rejects) and goes back, after Deny, Always allow, Allow once, <kbd>Esc</kbd> or Close, to what had it when the dialog opened; inside a terminal, to that `<slicc-terminal>` through its own `focus()`, even when it remounted meanwhile.
334
336
  Guest requests (`guest-message`, `guest-tool`) get their own copy, the guest's text as plain text (`[data-guest-text]`), attachments (`[data-attachments]`), the automatic check's verdict (`[data-verdict]`), and Deny and Allow once, plus **Always allow** (`data-action="always"`) with a hint saying what it covers when the approval's `always` is `true`; the dialog body carries `[data-approval-kind]` and fills the dialog's width at every size.
335
337
  Tray requests (`tray`) get their own title and one sentence (`#asking`), and only **Deny** (`deny`, focused) and **Allow once** (`once`), never Always allow.
@@ -764,6 +766,7 @@ The optional `toolLabel(agentId, callIds)` resolves with a short label for a run
764
766
  Spectrum asks once a reply's status has two calls and again each time it grows, once per set of ids, and shows the label once the status is complete.
765
767
  The optional `readOnly()` returns a line, or `null`, with a `readOnly` event when it changes: while it's a non-empty string, such as when this follower runs a different major version from its owner, every surface is watch-only just as a follower without Control is (no composer, no cone or conversation actions, no pickers, no Stop, no Accept or Revert in Changes, inline sprinkles view only, no model settling), and the line takes the place of the follow-based watch-only note under the chat.
766
768
  The optional `starting()` returns a line, or `null`, with a `starting` event when it changes: while it's a non-empty string, there's no agent yet but one is on its way (such as while seven installs its first agent, or replaces one), the chat drops its empty state and the composer queues what's sent until an agent is there (see `<slicc-composer>`); without it, or at `null`, nothing changes.
769
+ The optional `hasOlder(agentId)` says whether `older(agentId, before)` can load messages before the first one in `messages()`; `older` resolves once at least one earlier message has arrived in `messages()`, emitting `messages`, or with `[]` at the start, and the chat pages back with them (see `<slicc-chat>`).
767
770
 
768
771
  #### `files`
769
772
 
@@ -946,7 +949,7 @@ Later backed by: BIOS.
946
949
  A `sudo` approval has `id`, `requester` (`kind` `agent` or `user`, `name`, optional `agentId` and `where`, such as a terminal's title), `command` (without `sudo`; bios joins the kernel's argv shell-quoted), optional `target` (the user to run as; shown as `sudo -u {target}` unless `root`), optional `cwd` and `verdict` (`{ level?: 1 | 2 | 3, aligned?: boolean, decision?: 'allow' | 'deny' | 'unsure', reason }`, the judge's view: `level` 1 routine, 2 changes permissions, 3 could harm the system; `aligned`, whether it serves what the owner asked for; `decision`, the older shape, which may be missing), optional `judging` (`true` while the judge runs) and `verdictError` (why there is no verdict), `createdAt`, `expiresAt` (epoch ms) and `always` (for a sudo request with a level verdict, offer Always allow, which covers the requester's user and this exact command; without a level it is ignored).
947
950
  With `decide(id, 'once' | 'always' | 'deny')`, which resolves once the decision is recorded and then drops the request from `list()`, the tab shows a warning alert dialog (`dialog[data-approval][data-kind][data-requester]`): "Allow sudo?", "{name} wants to run this with sudo[ in {where}]:" for an agent or "You ran this with sudo[ in {where}] (as {name}):" for a user, the command, "In {cwd}", the verdict, what Always allow does, "Denied automatically in m:ss." and how many more wait, with **Deny** (`data-action="deny"`, focused; Escape also denies), **Always allow** (`always`) when offered and **Allow once** (`once`).
948
951
  A sudo request without a level verdict never offers Always allow, even when `always` is `true` (`decide(id, 'always')` is ignored for it); only Deny and Allow once show. With a level verdict, `always` alone decides whether Always allow is offered.
949
- For half a second after a request shows, only Deny works, so a double-click can't approve it.
952
+ For half a second (`arming`, in ms) after a request shows and after any change to its buttons (one appearing or going, being enabled or disabled, changing its label or moving), no button click counts, Deny's included: a mouse or touch click counts only when its press started after that half second, and a keyboard click only when it comes after it, so a double-click or a quick second key press can't answer the next request and a click aimed before Always allow appeared can't land on it. `decide()` refuses anything but `deny` for that half second too; <kbd>Escape</kbd> denies at once.
950
953
  A rejected `decide` shows its message in the dialog (`[data-error]`, `dialog[data-rejected]`) in place of the countdown, with only **Close** (`data-action="close"`, focused; Escape also closes), which moves on to the next waiting request or closes the dialog.
951
954
  Pass `decide` in every tab: the first decision wins.
952
955
  When a request ends without a decision in this tab, emit `ended` with `{ id, reason }` and drop it from `list()`: `answered` (another tab decided) shows "Answered in another tab", `expired` shows "Sudo request timed out", each with the command and **Close** (`data-action="close"`, `dialog[data-gone]`), and `withdrawn` (the requester went away) closes the dialog silently.
@@ -954,7 +957,7 @@ Without `ended`, a request that leaves the list reads as expired past `expiresAt
954
957
  Without `decide`, each request is a warning notice in the chat panels: "Waiting for sudo approval", who asks and the command, and "It waits for the owner's approval.
955
958
  Denied automatically at {time}."
956
959
  A guest request has `kind` `guest-message` (`text`, optional `attachments` of `{ name, mimeType, size }`) or `guest-tool` (`tool`, `summary`, optional `args`, the call's arguments as text, which the embedder pretty-prints, and `argsTruncated`, `true` when only a prefix of them came), plus `requester` (`kind` `guest`), `agentId`, optional `agentName`, `createdAt`, `expiresAt`, `always` (offer Always allow, a policy default the owner can change), an optional `verdict` (the same shape as a sudo approval's), and optional `judging` and `verdictError`, shown in the same verdict block.
957
- Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); when the summary is blank, the block shows the tool's name on its first line and then `args` instead, under the same four-line and end-of-command rules, and with no `args` either, "This request has no details." (`[data-id="no-details"]`) and Allow as usual; with a blank summary and `argsTruncated`, the dialog never offers Allow once or Always allow and `decide()` takes only `deny`, saying "This request is too long to show in full, so it can’t be allowed." (`[data-allow-hint]`) while the args still show; then the verdict, as for sudo.
960
+ Its dialog (`[data-approval-kind]` on the body) reads "Allow a guest’s message?" with "{name} wants to send this to {agentName ?? 'this cone'}:", the text as plain text in a scrolling box (`[data-guest-text]`, never Markdown or HTML) and the attachments as name · type · size (`[data-attachments]`), or "Allow a guest’s tool call?" with "{agentName ?? 'This cone'} wants to run {tool} for {name}:" and the summary (`[data-summary]`); when the summary is blank, the block shows the tool's name on its first line and then `args` instead, under the same end-of-command rule, and with no `args` either, "This request has no details." (`[data-id="no-details"]`) and Allow as usual; with a blank summary and `argsTruncated`, the dialog never enables Allow once or Always allow and `decide()` takes only `deny`, saying "This request is too long to show in full, so it can’t be allowed." (`[data-allow-hint]`) while the args still show; then the verdict, as for sudo.
958
961
  It offers **Deny** and **Allow once**, and, when `always` is `true`, **Always allow** (`always`, which calls `decide(id, 'always')`) with the hint "Always allow lets {name} send messages to {agentName ?? 'this cone'} without asking again." or "Always allow lets {agentName ?? 'this cone'} run exactly this {tool} call for {name} without asking again.".
959
962
  `decide` gets no `reason` from the UI; a rejected `decide` shows its message inline (`[data-error]`) with only **Close**, as for sudo.
960
963
  Ended guest requests read "Answered elsewhere", "Guest message timed out" or "Guest tool call timed out".
@@ -1098,6 +1101,7 @@ Import offers five invented candidates (one replaces a secret, one is a model ke
1098
1101
  Its buttons are plain Spectrum buttons that answer at once: Unlock unlocks, and created buttons are in `model.vault.buttons`.
1099
1102
  `vaultFixtures`, `DummyVault` and `DummyVaultButton` are exported from `/dummy`.
1100
1103
  `createDummyModel({ hold: 'waiting' | 'locked' | 'off' })` (`?hold=` on `/ui/`) makes the agent hold two tool results; a `locked` hold clears when the dummy vault unlocks, and `model.agent.setHold(hold)` changes it.
1104
+ `createDummyModel({ older: 'paged' })` (`?older=paged` on `/ui/`, off by default) holds back all but the last six messages of every conversation, and the agent's `older` brings them back four at a time after a short wait (`model.agent.olderTicks`, 10 ticks); `model.agent.paginate(keep, size, ids?)` holds them back again for those agents (all by default), `failOlder(times)` makes the next loads reject, and `olderCalls` lists the agents each load was for. `hasOlder` is false wherever nothing is held back.
1101
1105
 
1102
1106
  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.
1103
1107
  On tablets the agent picker gives up room before the View menu does, so the header keeps the View menu and its last button in view from 640 to 1199 px, the tray's chips stay on one line each, the budget chip leaves out its window and a tray that isn't live shows only its state.