@ai-ecoverse/slicc-spectrum 1.96.1 → 1.97.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
@@ -303,6 +303,7 @@ Between the header and the transcript, a `<slicc-notices>` shows the model's not
303
303
  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.
304
304
  An empty chat says "No messages yet.
305
305
  Ask sliccy something." with the agent's name, and only "No messages yet." while watch-only.
306
+ While the agent port's `starting()` returns a line, the chat has no empty state (`[data-empty]`): the embedder's notice says what's going on.
306
307
  With the model's `seat` set, the chat is a guest's page: the header shows only the cone's name (`[data-seat-cone]`) and its status, with no pickers, meter or New conversation; the guest's own messages (`origin` `guest` whose `seat` is the seat's `id`, or, without a `seat`, whose `from` is its `label`) read as the reader's own, with no name, while other guests keep "From {label}" and the host's messages read "From the host"; an open question or a permission request takes the normal card border, without its buttons, form, countdown or pile (no `data-pending`), and says "The host answers this."
307
308
  (`[data-host-answers]`, subdued); once answered, it shows the answer as everyone sees it; a reviewed guest message's outcome (a notice titled "It was approved.", "Nobody approved this in time.", "It was declined." or "It was withdrawn.", with the message as its text) reads as the quoted message, cut to one line, then the outcome (`.marker[data-ended]`); an approved one shows only "It was approved.", since the delivered message follows right after it; inline sprinkles inert with "View only on this device", error cards have no action and there's no suggested follow-up; the models aren't settled, and files can't be dropped.
308
309
  An empty guest chat says "No messages yet.
@@ -345,6 +346,11 @@ There's no row of key hints: the keys are the field's accessible description ("E
345
346
  The field's status icon, which is decorative, follows the input: the AI logo when empty, a pencil while typing, a brush for a moment after <kbd>Backspace</kbd> or <kbd>Delete</kbd>, a microphone while dictating; it stays the AI logo while a reply streams, and it swaps without animation.
346
347
  Drafts are kept per agent.
347
348
  On a follower while the link reconnects, the field is disabled and says so; the draft stays.
349
+ While the agent port's `starting()` returns a line (no agent yet, but one is on its way), the composer stays usable: its placeholder is that line, there are no `/` or `@` popups, and sending, attachments included, adds a row above the field that reads "Sent when the agent starts" (`li.queued[data-mode="starting"]`), which can be dropped (`data-action="unqueue"`); a row with text and files says how many files go with it (`[data-files]`).
350
+ Whenever the model changes or its agent port fires `starting` or `agents`, and the current model's port has no line (`null`, or no `starting` at all) and `agent.list()` has an agent, the composer sends the rows in order to that model's active agent (else the first one) through `agent.send`, the first like a normal send and the rest with `mode: 'queue'`, and the rows go; if the line clears with no agent, the rows stay until one comes.
351
+ A line that comes back closes an open `/` or `@` popup.
352
+ The rows are kept for the page (for the `<slicc-app>` the composer is in, else for the document), not for the model, so a model swap (an embedder that puts a new `SliccModel` on `<slicc-app>` once the agent is there), a layout switch and a dock rebuild keep them; a page reload drops them.
353
+ 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.
348
354
  While the agent holds tool results for the vault (`agent.hold()`), a status line above it says "Waiting for the vault" or "Unlock the vault to continue", with how many results are held, and offers the vault's **Unlock** button when locked and the model has a `vault`; if the browser blocks the vault window, the line says to allow pop-ups.
349
355
  With the model's `seat` set it's a guest's composer: <kbd>Enter</kbd> and the send button send through `agent.send(cone, text)`, <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line and <kbd>↑</kbd>/<kbd>↓</kbd> walk the guest's own messages; there's no steer, queue, Stop, attachments (the field is `collapsed`, so its + is hidden and inert, and pasted files are ignored), `/` commands, `@` mentions, + menu or push to talk, and it never reads files; its description is "Enter sends, Shift+Enter adds a new line."
350
356
  A rejected send shows the rejection's message above the field (`[data-send-error]`, `role="alert"`) and puts the draft back, ahead of anything typed since.
@@ -705,6 +711,7 @@ Assistant messages stream: the same message is re-emitted as its parts grow and
705
711
  The optional `toolLabel(agentId, callIds)` resolves with a short label for a cluster of tool runs (`callIds` in order), such as "Compare the docs with the live API", or `null`; the agent dedupes, caches and persists it.
706
712
  Spectrum asks once a cluster forms and again each time it grows, once per set of ids, and shows the label at the end of the strip.
707
713
  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.
714
+ 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.
708
715
 
709
716
  #### `files`
710
717
 
@@ -980,6 +987,10 @@ The SLICC you follow runs a newer version.
980
987
  Update this one to send prompts.", and shows a warning notice without a close button (`[data-notice="readonly"]`) in place of any other notices.
981
988
  `model.agent.setReadOnly(text)` changes it, and `null` ends it.
982
989
 
990
+ `createDummyModel({ agent: 'starting' | 'waiting' })` (`?agent=` on `/ui/`, default `ready`) starts with no agents while `starting()` returns "Seven is installing its agent.", and shows an info notice without a close button (`[data-notice="starting"]`) that goes with the line; `<slicc-app>` opens the chat with no agent (the `chat` panel, as `show('chat')` does) when it starts while `starting()` has a line and its layout has no chat; once an agent is active, that agent's chat takes the `chat` panel's place in the dock, focused if focus was in it, and the `chat` panel closes, also when a saved layout brings it back.
991
+ With `starting`, after 100 ticks of the dummy clock (3 s at the default delay) the usual cones arrive and the line clears; `waiting` waits for `model.agent.arrive()`, which does the same.
992
+ `model.agent.setStarting(line)` changes the line (and `null` clears it with no agent), and either one cancels the scheduled arrival.
993
+
983
994
  `createDummyModel({ import: 'v6' | 'again' | 'fail' | 'cancel-check' })` (`?import=` on `/ui/`, off by default) adds `importProfile`, `importCancel` and `importing` to the agent: any zip previews an invented export from www.sliccy.ai (3 cones, 14 scoops, 2 frozen, Anthropic and OpenAI to sign in to again, 2 mounts and a flagged `files/cones/cone/.env`), and the import ticks to 14 in a few steps and adds 3 frozen cones with `v6: true` and the report `/root/v6-import-2026-10-18.md`; a second preview says when, and **Import again** adds fresh copies. `again` starts with an earlier import, `fail` rejects with "That zip isn’t a SLICC 6 profile export.", and `cancel-check` checks slowly. `DummyImport` and its fixtures (`importPreview`, `importCones`, `importReport`, …) are exported from `/dummy`.
984
995
 
985
996
  The kitchen sink's “docs-pass failed” card is an expired-session error for the invented `ai-adobe` account (Adobe AI, harbor-ops@example.com, `expired`, OAuth): **Log in** goes `signing-in`, then `connected` after a short wait, and the dummy records each `connect` in `settings.connects` (`{ id, gesture }`, whether `navigator.userActivation.isActive` was true at the call); inbox-triage's “Tickets connector” card names `mcp:tickets`, a token account, so its **Log in** opens Settings at that row; `createDummyModel({ login: 'fail' })` (`?login=fail` on `/ui/`) makes OAuth sign-ins reject with “The sign-in window was closed before it finished.”
@@ -1507,6 +1507,9 @@ var DummyAgent = class extends Emitter {
1507
1507
  #frozen = [];
1508
1508
  #hold = null;
1509
1509
  #readOnly = null;
1510
+ #starting = null;
1511
+ #absent = [];
1512
+ #arrival = 0;
1510
1513
  #ice = /* @__PURE__ */ new Map();
1511
1514
  #suggestions = /* @__PURE__ */ new Map();
1512
1515
  #runs = /* @__PURE__ */ new Map();
@@ -1985,6 +1988,34 @@ var DummyAgent = class extends Emitter {
1985
1988
  this.#readOnly = text || null;
1986
1989
  this.emit("readOnly", this.#readOnly);
1987
1990
  }
1991
+ starting() {
1992
+ return this.#starting;
1993
+ }
1994
+ setStarting(line) {
1995
+ this.#arrival++;
1996
+ this.#starting = line || null;
1997
+ this.emit("starting", this.#starting);
1998
+ }
1999
+ withhold(line, ticks2) {
2000
+ this.#absent = [...this.#absent, ...this.#agents];
2001
+ this.#agents = [];
2002
+ this.#active = "";
2003
+ this.#changed();
2004
+ this.setStarting(line);
2005
+ if (ticks2 === void 0) return;
2006
+ const arrival = this.#arrival;
2007
+ void this.#clock.sleep(ticks2).then(() => {
2008
+ if (arrival === this.#arrival) this.arrive();
2009
+ });
2010
+ }
2011
+ arrive() {
2012
+ this.#agents = [...this.#agents, ...this.#absent];
2013
+ this.#absent = [];
2014
+ this.#active = this.#agents[0]?.id ?? "";
2015
+ this.#changed();
2016
+ this.emit("active", this.#active);
2017
+ this.setStarting(null);
2018
+ }
1988
2019
  frozen() {
1989
2020
  const live = this.#agents.filter((agent) => !agent.frozen).map((agent) => {
1990
2021
  const messages = this.#messages.get(agent.id);
@@ -4467,6 +4498,7 @@ var DummyNetwork = class extends Emitter {
4467
4498
 
4468
4499
  // src/dummy/notices.ts
4469
4500
  var readOnlyText = "Watch only. The SLICC you follow runs a newer version. Update this one to send prompts.";
4501
+ var startingText = "Seven is installing its agent.";
4470
4502
  var noticeFixtures = {
4471
4503
  storage: {
4472
4504
  id: "storage",
@@ -4489,6 +4521,14 @@ var noticeFixtures = {
4489
4521
  body: "The SLICC you follow moved to a newer version. Until you update SLICC on this device, it can only watch.",
4490
4522
  actions: [],
4491
4523
  dismissible: false
4524
+ },
4525
+ starting: {
4526
+ id: "starting",
4527
+ tone: "info",
4528
+ title: "Seven is installing its agent",
4529
+ body: "The chat starts once the agent is installed. Messages you send now wait for it.",
4530
+ actions: [],
4531
+ dismissible: false
4492
4532
  }
4493
4533
  };
4494
4534
  var DummyNotices = class extends Emitter {
@@ -4499,7 +4539,7 @@ var DummyNotices = class extends Emitter {
4499
4539
  super();
4500
4540
  this.#scenario = scenario;
4501
4541
  this.#time = time;
4502
- this.#notices = scenario === "mixed" ? [noticeFixtures.storage, noticeFixtures.welcome] : scenario === "readonly" ? [noticeFixtures.readonly] : [noticeFixtures.storage];
4542
+ this.#notices = scenario === "mixed" ? [noticeFixtures.storage, noticeFixtures.welcome] : scenario === "readonly" || scenario === "starting" ? [noticeFixtures[scenario]] : [noticeFixtures.storage];
4503
4543
  }
4504
4544
  list() {
4505
4545
  return this.#notices;
@@ -4515,13 +4555,19 @@ var DummyNotices = class extends Emitter {
4515
4555
  this.#remove(id);
4516
4556
  }
4517
4557
  readOnly(on) {
4518
- const shown = this.#notices.some((notice) => notice.id === "readonly");
4558
+ this.#toggle("readonly", on);
4559
+ }
4560
+ starting(on) {
4561
+ this.#toggle("starting", on);
4562
+ }
4563
+ #toggle(id, on) {
4564
+ const shown = this.#notices.some((notice) => notice.id === id);
4519
4565
  if (on === shown) return;
4520
4566
  if (on) {
4521
- this.#notices = [noticeFixtures.readonly, ...this.#notices];
4567
+ this.#notices = [noticeFixtures[id], ...this.#notices];
4522
4568
  this.emit("notices", this.#notices);
4523
4569
  } else {
4524
- this.#remove("readonly");
4570
+ this.#remove(id);
4525
4571
  }
4526
4572
  }
4527
4573
  #remove(id) {
@@ -6050,6 +6096,15 @@ function importPort(scenario, agent, time) {
6050
6096
  agent.importCancel = port.importCancel.bind(port);
6051
6097
  agent.importing = port.importing.bind(port);
6052
6098
  }
6099
+ function noticesPort(agent, time, notices, readonly, arrival) {
6100
+ if (readonly !== "off") agent.setReadOnly(readOnlyText);
6101
+ if (arrival !== "ready") agent.withhold(startingText, arrival === "starting" ? 100 : void 0);
6102
+ const notice = readonly !== "off" ? "readonly" : arrival !== "ready" ? "starting" : notices;
6103
+ const port = notice === "off" ? null : new DummyNotices(notice, time);
6104
+ if (port && readonly !== "off") agent.on("readOnly", (text) => port.readOnly(Boolean(text)));
6105
+ if (port && arrival !== "ready") agent.on("starting", (line) => port.starting(Boolean(line)));
6106
+ return port;
6107
+ }
6053
6108
  function createDummyModel({
6054
6109
  delay = 30,
6055
6110
  storage = null,
@@ -6074,6 +6129,7 @@ function createDummyModel({
6074
6129
  transport,
6075
6130
  hold = "off",
6076
6131
  readonly = "off",
6132
+ agent: arrival = "ready",
6077
6133
  browser: scenario = "idle",
6078
6134
  sprinkles: sprinkles2 = "builtin",
6079
6135
  monitor = "dummy",
@@ -6103,12 +6159,7 @@ function createDummyModel({
6103
6159
  };
6104
6160
  importPort(imports, agent, time);
6105
6161
  if (hold !== "off") agent.setHold({ reason: hold, held: 2 });
6106
- if (readonly !== "off") agent.setReadOnly(readOnlyText);
6107
- const notice = readonly !== "off" ? "readonly" : notices;
6108
- const noticePort = notice === "off" ? null : new DummyNotices(notice, time);
6109
- if (noticePort && readonly !== "off") {
6110
- agent.on("readOnly", (text) => noticePort.readOnly(Boolean(text)));
6111
- }
6162
+ const noticePort = noticesPort(agent, time, notices, readonly, arrival);
6112
6163
  if (scenario === "driving") void browser.run(drive);
6113
6164
  const base = {
6114
6165
  agent,