@ai-ecoverse/slicc-spectrum 1.23.3 → 1.24.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
@@ -147,7 +147,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
147
147
  | `<slicc-memory>` | What agents remember, globally or per cone, grouped by section: search, filter by tag, expand, edit, add and forget. |
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: thaw to bring one back, delete, or freeze the active cone (also `/freeze`). |
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()`). Its only way back is `slicc.lick({ action, data })`, which reaches the owning cone as a `sprinkle` lick. Panel sprinkles sit at the top of the right rail with their Lucide icon. |
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
151
  | `<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. |
152
152
  | `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
153
153
  | `<slicc-prompt>` | The name dialog `prompt()` opens. Not for direct use. |
@@ -177,6 +177,17 @@ import { prompt } from './dist/slicc-ui.js';
177
177
  const name = await prompt({ title: 'New cone', label: 'Name', action: 'Create', submit: (name) => model.agent.createCone(name).then(() => {}) });
178
178
  ```
179
179
 
180
+ ### Sprinkle bridge
181
+
182
+ A sprinkle's frame gets `window.slicc` with `name` and `lick(event)`. When the model's `sprinkles` port has `call`, it also gets:
183
+
184
+ - `slicc.readFile(path)` resolves with the file's text.
185
+ - `slicc.exists(path)` resolves with a boolean.
186
+ - `slicc.getState()` returns the sprinkle's saved value, or `null`, synchronously: the frame is built with it, so `var saved = slicc.getState()` works on load.
187
+ - `slicc.setState(value)` updates that value at once and resolves when it is saved. State is JSON: what `JSON.stringify` drops or changes is gone (a `Map` becomes `{}`, `NaN` becomes `null`), and a value it can't serialize, like a cycle or a function, rejects and leaves state as it was. It outlives the frame: reopening the panel or rebuilding the sprinkle starts with it.
188
+
189
+ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or a missing file rejects with an `Error`. There is no `exec`: a sprinkle that needs a command asks its cone with `slicc.lick()`. Without `call`, none of the four exist, so guard them (`typeof slicc.readFile === 'function'`) and fall back.
190
+
180
191
  ### Model
181
192
 
182
193
  `SliccModel` (`src/model/types.ts`) has one port per future backend, so adapters can replace the dummy without UI changes. Every port is subscribable with `on(type, listener)`, which returns an unsubscribe function.
@@ -190,7 +201,7 @@ const name = await prompt({ title: 'New cone', label: 'Name', action: 'Create',
190
201
  | `settings` | local storage | Theme, model, thinking level, composer and diff preferences, the models on offer, and accounts. |
191
202
  | `memory` | `@ai-ecoverse/slicc-agent` | Memories with scope, section, tag and body; `save` and `remove`. |
192
203
  | `monitor` | all of them | A snapshot of vitals, alerts and sections, re-emitted as the system changes, and `resync`. |
193
- | `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. |
204
+ | `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`. |
194
205
  | `tray` | the tray protocol | Connection, role, float kind, followers, spend and budget; `reconnect` and `disconnect`. |
195
206
  | `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. `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. |
196
207
 
@@ -222,7 +233,7 @@ Each section is its own port, so an embedder can mix real and dummy per section,
222
233
 
223
234
  `<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.
224
235
 
225
- 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`. Without file access, `suggestions` shows its empty state. 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.
236
+ 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.
226
237
 
227
238
  ### Distribution
228
239
 
@@ -2861,6 +2861,34 @@ var sample = `{
2861
2861
  var bashrc = `export PS1='\\u@slicc:\\w\\$ '
2862
2862
  alias ll='ls -la'
2863
2863
  `;
2864
+ var suggestions2 = `${JSON.stringify(
2865
+ [
2866
+ {
2867
+ id: "sug-units",
2868
+ kind: "skill",
2869
+ title: "Unit conversions",
2870
+ body: "You converted Fahrenheit to Celsius by hand in three chats this week. A skill can do it for you.",
2871
+ skill: "unit-converter",
2872
+ install: "skill install unit-converter"
2873
+ },
2874
+ {
2875
+ id: "sug-notes",
2876
+ kind: "use-case",
2877
+ title: "Release notes from the week",
2878
+ body: "You write release notes for harbor every Friday. Let the cone draft them from the commits.",
2879
+ prompt: "Draft release notes for harbor from the commits since last Friday."
2880
+ },
2881
+ {
2882
+ id: "sug-cache",
2883
+ kind: "tip",
2884
+ title: "Name your cache keys",
2885
+ body: "Forecast cache keys mix the city and the units. Splitting them makes stale entries easier to find."
2886
+ }
2887
+ ],
2888
+ null,
2889
+ 2
2890
+ )}
2891
+ `;
2864
2892
  var files2 = {
2865
2893
  "/workspace/harbor/README.md": readme,
2866
2894
  "/workspace/harbor/package.json": pkg,
@@ -2877,6 +2905,7 @@ var files2 = {
2877
2905
  "/shared/MEMORY.md": memory,
2878
2906
  "/shared/notes/2026-10-04-standup.md": standup,
2879
2907
  "/home/user/.bashrc": bashrc,
2908
+ "/home/.gelatiere/suggestions.json": suggestions2,
2880
2909
  "/tmp/forecast-sample.json": sample
2881
2910
  };
2882
2911
  var directories = ["/sessions", "/home/user/downloads"];
@@ -3472,13 +3501,24 @@ var DummyMonitor = class extends Emitter {
3472
3501
  };
3473
3502
 
3474
3503
  // src/dummy/sprinkles.ts
3504
+ function homePath(path) {
3505
+ const text = String(path);
3506
+ const mapped = text.startsWith("/shared/") ? `/home/${text.slice("/shared/".length)}` : text;
3507
+ const parts = mapped.split("/");
3508
+ if (!mapped.startsWith("/home/") || parts.some((part) => part === "." || part === ".."))
3509
+ throw new Error(`EACCES: ${text} is outside /home`);
3510
+ return mapped;
3511
+ }
3475
3512
  var DummySprinkles = class extends Emitter {
3476
3513
  #sprinkles;
3477
3514
  #agent;
3478
- constructor(sprinkles2, agent) {
3515
+ #files;
3516
+ #state = /* @__PURE__ */ new Map();
3517
+ constructor(sprinkles2, agent, files3) {
3479
3518
  super();
3480
3519
  this.#sprinkles = sprinkles2.map((sprinkle) => ({ ...sprinkle }));
3481
3520
  this.#agent = agent;
3521
+ this.#files = files3;
3482
3522
  }
3483
3523
  list() {
3484
3524
  return this.#sprinkles.map((sprinkle) => ({ ...sprinkle }));
@@ -3495,6 +3535,16 @@ var DummySprinkles = class extends Emitter {
3495
3535
  JSON.stringify(data.data ?? null, null, 2)
3496
3536
  );
3497
3537
  }
3538
+ async call(id, method, args) {
3539
+ if (method === "getState") return structuredClone(this.#state.get(id) ?? null);
3540
+ if (method === "setState") {
3541
+ this.#state.set(id, structuredClone(args[0] ?? null));
3542
+ return void 0;
3543
+ }
3544
+ const path = homePath(args[0]);
3545
+ if (method === "exists") return (await this.#files.list()).some((entry) => entry.path === path);
3546
+ return this.#files.read(path);
3547
+ }
3498
3548
  };
3499
3549
 
3500
3550
  // src/dummy/shell.ts
@@ -3917,7 +3967,7 @@ function createDummyModel({
3917
3967
  time
3918
3968
  ),
3919
3969
  memory: new DummyMemory(memories),
3920
- sprinkles: new DummySprinkles(sprinkles, agent),
3970
+ sprinkles: new DummySprinkles(sprinkles, agent, files3),
3921
3971
  tray: new DummyTray(tray, time),
3922
3972
  updates: new DummyUpdates(updates, time)
3923
3973
  };