@ai-ecoverse/slicc-spectrum 1.86.1 → 1.86.2

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
@@ -219,6 +219,7 @@ The sans stack (`--swc-sans-font-family-stack`, also used by the 1.12 components
219
219
 
220
220
  The shell: header with the agent picker, status items, the update status, the network indicator, the tray, the View menu and the theme switch, rails, and a `<slicc-dock>`.
221
221
  Saves the dock layout per screen class to `storage` (default `localStorage`) under `layoutKey`.
222
+ The dock builds new panel content whenever it lays panels out again (a switch between phone, tablet and desktop, or a layout reset), so what the user opens in a panel (sections, cones, rows, folders, disclosures) lives outside the element, per model and panel (`view-state.ts`, in memory only): a rebuilt panel shows it again, and a click on the outgoing panel while the new one mounts reaches the new one too.
222
223
  While connected it sets `overscroll-behavior` to `none !important` on the page's `html` and `body`, so only scroll areas inside panels rubber-band.
223
224
  It sets the page's `color-scheme` to its resolved theme, and when the page has a `theme-color` meta per `prefers-color-scheme`, gives every one of them the content of the meta for that theme, so the page and the installed app's title bar follow the app's setting instead of the OS.
224
225
  When the last app on the page is removed, the page's own inline values and `theme-color` contents come back.
@@ -240,7 +241,7 @@ A tree (`role="tree"`) of cones, with their scoops listed under them.
240
241
  A cone with scoops starts with a quiet disclosure chevron (`data-action="toggle-cone"`, `swc-icon-chevron-right`, turned down when open and mirrored right to left); it's `aria-hidden` and out of the Tab order, since the row's `aria-expanded` carries the state.
241
242
  The chevron only expands and collapses; a click on the row, <kbd>Enter</kbd> or <kbd>Space</kbd> selects the agent and makes it the active one, which also expands its cone.
242
243
  <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.
243
- Several cones can be open at once, and the active agent's cone is open again whenever the panel mounts, so on a phone a cone you tapped still shows its scoops when you come back to Agents.
244
+ 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.
244
245
  When the active scoop's cone is collapsed, the cone row takes the Tab stop.
245
246
  A row's first line starts with one circle: its unread count when there are unread messages (`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), otherwise a dot coloured by status.
246
247
  Then comes the name; the context fill, when it's high, sits in the top right corner.
@@ -272,6 +273,7 @@ A command's icon is the favicon of the site it names when an open browser tab al
272
273
  A running run pulses (still, with a dashed ring, under `prefers-reduced-motion`), a done or cancelled one is grey and a failed one red, each with a corner glyph.
273
274
  The card (`.tool[data-tool][data-status][data-kind]`, a `details.run` under a pill and a `div[role=region]` named by its `title` and status under a strip) has no header of its own, no chevron, title, tool name or status: its input (a shell command after `$`), an edit's `diff` shown as a diff view, its output wrapped rather than cut off, an `image`, such as a screenshot, an optional `meta` such as a timeout, a standalone tool message's time ("Ran at 16:12", `.ran`, by the two-minute rule), and its `notes` (`{ severity, text }`, such as "Exited with code 5" or "Full output in /tmp/…") as lines under the output, `li[data-note][data-severity]`: `error` with an alert icon in the negative colour, `warn` with an alert icon and `info` with an info icon, both subdued; media galleries with images, video and audio; action cards for tools, pull requests and status; plans and checklists; inline diffs; question prompts (as decision cards, below); delegations; link previews; errors with a fix-it action, as a grey card (`.error-card[role=alert]`, `data-id` on system errors) inset by a wide margin on each side, a narrower one under 480 px: a red alert icon, the title in bold, the detail subdued and the action on the right, or under the detail below 360 px, announced as "Error: " and the title; a content-filter stop offers **Drop the last turn**, which rewinds that turn and puts its prompt back into the composer; when the turns right before it failed too, the button reads **Drop N failed turns** and rewinds to before the first of them, putting the latest prompt back), system messages (error cards; compactions and notices, rewinds and model changes included, as full-width dividers with a line on each side, like the day: a compaction shows only its title, its `text` kept for screen readers, one that lost messages (`state: 'fallback'`, by default "Compaction failed, transcript truncated", or `discarded`, "Context discarded") an alert icon in the negative colour, and a notice its title and text on one line, `role="note"`; a divider that wraps keeps a short line on each side) and licks, SLICC's events into a cone, on every channel, on the right (`.lick-turn`): a byline with the title and the time by the two-minute rule, then a dashed box (`.lick[data-id][data-channel]`) with the text and the count (×3); emoji pills on its bottom edge (`.reactions .reaction`, `aria-hidden`, each named by its `title`) give the channel (⏰ Schedule, 🪝 Webhook, ✨ Sprinkle, …) and the outcome (👍 Allowed, 👎 Denied, also said in the box's hidden text).
274
275
  A lick with a body is a `details.lick` whose head (`summary.lick-head`, with a quiet chevron that turns when open) toggles it with a click, <kbd>Enter</kbd> or <kbd>Space</kbd>; clicking the body doesn't close it.
276
+ Open licks and open tool runs hold for every view of the chat on the same model, so a layout switch keeps them.
275
277
  A lick's `severity` (`warn` or `error`) adds an alert icon to its byline, a coloured edge to its box and a spoken prefix.
276
278
  Pending permission requests (`sudo-request` licks with `state: 'pending'`) and open questions are decision cards in the centre column (`.decision-turn[data-pending]`, `data-id` the lick's or the question's), inset like a tool cluster (a wide margin on each side, a narrower one under 480 px), with their buttons on the right: **Deny**, then **Allow** (`data-action` `deny` and `allow`), or the question's options.
277
279
  A request with `expiresAt` counts down as m:ss; the count is hidden from screen readers, which hear its end time, then "Less than a minute left" and "Timed out", so the log's live region doesn't speak every second; a timed-out request (`data-expired`) loses its buttons.
@@ -341,6 +343,7 @@ A quiet line under it (`[data-seat-note]`) says who reviews ("The host reviews w
341
343
  #### `<slicc-files>`
342
344
 
343
345
  The file system as a `<slicc-file-tree>`, with pending changes as git status.
346
+ It starts with the folders of the pending changes open; once the user opens or closes a folder, every view of Files on the same model shows the same folders open, a rebuilt one included.
344
347
  Opening a file opens it in a tab.
345
348
  With `files.mountFolder`, **Mount a folder…** (`data-action="mount-folder"`) mounts a local folder, then selects and expands it in the tree; with `files.mounts`, a **Mounted** strip lists the mount points (with `files.mountInfo`, each mount with a `source` reads `/mnt/proj · ref-node/proj`, `data-source` on its `.path`), each with **Eject** (`data-action="eject"`, `data-path` the mount point) when the port has `eject`.
346
349
  With `files.needsFolder`, a **Needs a folder** section lists the mount points waiting for a folder, each with **Insert folder…** (`data-action="insert-folder"`, `data-id` the mount point), which calls `mountFolder(path)`.
@@ -382,6 +385,7 @@ It opens on the most recently driven tab (`.focused`), or on the tab overview: c
382
385
  The focused tab has **All tabs** (`data-action="all-tabs"`), a tab picker (a list of tabs at 720 px and wider), the URL with **Copy** (`data-action="copy-url"`) and **Open** (`data-action="open-tab"`, activates the tab), who's using it (`.driving[data-driving=agent|user|none]`; a controlled tab without an agent reads **In use** and has no Stop) with a small **Stop <agent>** (`data-action="stop-agent"`) in the same row, which keeps its height with or without an agent, a view-only preview (`figure[data-view]`: `live` while watching with a frame under 3 s old, `shot` with "Updated N s ago", `none`) and the recent actions (`li[data-action-id][data-kind][data-status]`, failures with their error inline), the tab's own filtered first, then the latest 50, with **Show more** (`data-action="more-actions"`) adding 50 earlier ones each time up to all the port has ("Showing the latest N of M"); the list scrolls and takes focus.
383
386
  Actions with a URL (`open`, `goto`, `request`) expand (`data-action="action-url"`, `aria-expanded`) to show the full URL, or the request line, under the shortened one.
384
387
  Below 480 px they fold into **Recent actions (n)**, a chevron disclosure counting all of them, with the last one showing.
388
+ The tab the user opened (or **All tabs**), expanded URLs and an open **Recent actions** hold for every view of the panel on the same model, so a layout switch keeps them; a closed tab lets go, and the panel opens on the most recently driven tab again.
385
389
  Labels, URLs and errors are text only.
386
390
  With no tabs and `browser.via` `null` in the network status, it says browser control isn't connected (`data-browser="none"`) with the extension link and the commands to install and run slicc (`curl -fsSL https://get.sliccy.ai/slicc | sh`, then `slicc`, or `npx sliccy@next` with Node).
387
391
  The Browser rail button carries a dot (`data-driven`) while a tab is in use.
@@ -419,6 +423,7 @@ Then **Rules SLICC refused** (`[data-refused]`), where the rules live (`[data-po
419
423
  The section's `data-vault` is `locked`, `unlocked` or `loading`.
420
424
  A popup the browser blocked shows how to allow it (`[data-popup=blocked]`).
421
425
  Widened-rule notices aren't shown.
426
+ An open **Removed** list (`details[data-removed]`) stays open across a layout switch.
422
427
 
423
428
  #### `<slicc-vault-button>`
424
429
 
@@ -433,7 +438,7 @@ It closes, clearing the field, when the request leaves `waiting` or while the va
433
438
 
434
439
  #### `<slicc-updates>`
435
440
 
436
- Install / Update: component states, version changes, last checks, download and link counts, action buttons, failures with Retry, and collapsible install logs.
441
+ Install / Update: component states, version changes, last checks, download and link counts, action buttons, failures with Retry, and collapsible install logs, which stay open across a layout switch.
437
442
  Opens at boot until the agent is ready without failures.
438
443
  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.
439
444
  Every automatic close is saved to the layout.
@@ -447,7 +452,7 @@ What agents remember, grouped by section: search, filter by scope and tag, expan
447
452
  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.
448
453
  The count (“N of M memories”) counts the memories in the picked scope.
449
454
  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.”
450
- Each memory is a row (`li.row[data-id]`) that opens in place: open rows (`data-open`) take the accent's subtle background and turn their chevron down, and show the body with **Edit** and **Forget** (`data-action="edit"`, `data-action="forget"`).
455
+ Each memory is a row (`li.row[data-id]`) that opens in place, and stays open across a layout switch: open rows (`data-open`) take the accent's subtle background and turn their chevron down, and show the body with **Edit** and **Forget** (`data-action="edit"`, `data-action="forget"`).
451
456
  Under the summary, the tag is a subtle badge (`swc-badge[data-tag]`: User indigo, Feedback purple, Project seafoam), then the scope and, for free text outside an entry, “From MEMORY.md” (`.from`).
452
457
  Below 560 px of panel width, the search takes its own line over the two pickers.
453
458
  The search field is the shared one from `search.ts` (`sp-search[data-action=search]`), as in Files.
@@ -458,7 +463,7 @@ The live monitor: vitals (`.vital[data-id]`), each a meter when it has a `ratio`
458
463
  Each card keeps its place whatever it shows: the value line never wraps (a unit or delta that doesn't fit is left out whole rather than cut, and stays in the line's `title` and its text; a vital's optional `hint` replaces that tooltip, such as the exact reset time behind "resets in 4 days"), a card without a `series` keeps the sparkline's room, a meter keeps its description's line, and a card left alone on the last row spans it, so short and long values, 0% and 100%, and missing data move nothing below them.
459
464
  Then alerts (an alert's optional `command` shows after its detail, led by its `commandLabel` or "Run", as unwrapped code with **Copy**, `[data-command]` and `data-action="copy-command"`), and the sections (the agent's Tasks, Processes and Recent problems, then terminals, tabs, pending changes and tray followers; cones and sprinkles have their own panels; any other section an agent sends, such as an older agent's `agents` or `sprinkles`, renders the same way).
460
465
  Each section (`li.section[data-id]`) shows its count and an attention badge, and opens its surface with **Open** where one exists; the counts line up in one column whether or not a section has **Open**, and a section without rows keeps its name in line with the others (`agents`, `terminals` and `processes` → terminal, `tabs`, `changes`).
461
- A section with rows expands into them with its toggle (`.toggle`, `aria-expanded`; `data-open` on the section): collapsed by default, expanded while a row is `warn` or `error`, until the user toggles it.
466
+ A section with rows expands into them with its toggle (`.toggle`, `aria-expanded`; `data-open` on the section): collapsed by default, expanded while a row is `warn` or `error`, until the user toggles it; that choice holds for every view of the monitor on the same model, so a layout switch keeps it.
462
467
  Each row (`li.row[data-status]`, `data-id` from the row's `id`) shows a status light (blue active, gray idle, orange warn, red error, with the status spoken), the name, the meta line and its badges, indented by `depth` (`data-depth`, up to 6).
463
468
 
464
469
  #### `<slicc-freezer>`
@@ -504,6 +509,7 @@ The URL is never stored or shown again: closing the dialog drops it.
504
509
  The header's **Network** indicator (`data-network`, `data-health`) opens it: a status light, green for `ok`, yellow for `limited` and red for `failing`, with the state in its text and label and `detail` as its tooltip, which adds "Tailscale is connected." while the tailnet runs; on a phone, a cloud icon for the state.
505
510
  While a linked device asks to follow, controls the agent, watches it or has a terminal open, an indicator (`data-remote`, `ask`, `prompt`, `terminal` or `watch`) sits before it, **{name} asks to follow**, **Controlled by**, **Terminal open from** or **Watched by** the device's name (or the number of devices; in that order of precedence), says who does what in its tooltip and opens the panel too.
506
511
  Offered when the model has a `network` port, a `guests` port, or both.
512
+ **Link another device** and **Other ways to reach the whole web** stay open across a layout switch once opened.
507
513
  With `guests` and no `network`, it shows only the Guests section (no connection, Sync, Leaders, Tailscale, Linked devices, Get the whole web or failures), still titled Network with its rail icon, and the header's Network and remote indicators stay hidden; the tray's connection chip is then a plain status.
508
514
 
509
515
  #### `<slicc-tray>`
@@ -530,6 +536,7 @@ Not for direct use.
530
536
 
531
537
  Generic.
532
538
  [`@pierre/trees`](https://www.npmjs.com/package/@pierre/trees) through its `web-components` entry: `paths` (folders end in `/`), `gitStatus`, `expanded`, `query` (shows only the paths that match it; empty shows all), `reveal(path, focus)`.
539
+ `expanded` also opens and closes the folders of a mounted tree, a `tree-expand` event (`detail.expanded`) reports the open folders when the user changes them, and the tree keeps them when it's moved, such as into a floating group.
533
540
  It has no search field of its own.
534
541
  Fires `file-open` on click and on <kbd>Enter</kbd>, and `tree-search` (`detail.key`) when a letter or digit is typed in it, for the panel's search field.
535
542
 
package/dist/slicc-ui.js CHANGED
@@ -10052,6 +10052,87 @@ function search(options) {
10052
10052
  ></sp-search>`;
10053
10053
  }
10054
10054
 
10055
+ // src/app/view-state.ts
10056
+ var stores = /* @__PURE__ */ new WeakMap();
10057
+ var loose = {};
10058
+ function slotOf(model, key2, initial) {
10059
+ const owner = model ?? loose;
10060
+ const slots = stores.get(owner) ?? /* @__PURE__ */ new Map();
10061
+ stores.set(owner, slots);
10062
+ const found = slots.get(key2);
10063
+ if (found) return found;
10064
+ const slot = { value: initial(), listeners: /* @__PURE__ */ new Set() };
10065
+ slots.set(key2, slot);
10066
+ return slot;
10067
+ }
10068
+ var ViewState = class {
10069
+ #host;
10070
+ #key;
10071
+ #initial;
10072
+ #watched = null;
10073
+ #connected = false;
10074
+ #listener = () => this.#host.requestUpdate();
10075
+ constructor(host4, key2, initial) {
10076
+ this.#host = host4;
10077
+ this.#key = key2;
10078
+ this.#initial = initial;
10079
+ host4.addController(this);
10080
+ }
10081
+ #slot() {
10082
+ const model = this.#host.model;
10083
+ return slotOf(model, this.#key, () => this.#initial(model));
10084
+ }
10085
+ get value() {
10086
+ return this.#slot().value;
10087
+ }
10088
+ set value(value) {
10089
+ const slot = this.#slot();
10090
+ if (Object.is(slot.value, value)) return;
10091
+ slot.value = value;
10092
+ for (const listener of [...slot.listeners]) listener();
10093
+ }
10094
+ #watch() {
10095
+ const slot = this.#slot();
10096
+ if (slot === this.#watched) return;
10097
+ this.#unwatch();
10098
+ slot.listeners.add(this.#listener);
10099
+ this.#watched = slot;
10100
+ }
10101
+ #unwatch() {
10102
+ this.#watched?.listeners.delete(this.#listener);
10103
+ this.#watched = null;
10104
+ }
10105
+ hostConnected() {
10106
+ this.#connected = true;
10107
+ this.#watch();
10108
+ }
10109
+ hostUpdate() {
10110
+ if (this.#connected) this.#watch();
10111
+ }
10112
+ hostDisconnected() {
10113
+ this.#connected = false;
10114
+ this.#unwatch();
10115
+ }
10116
+ };
10117
+ var ViewSet = class extends ViewState {
10118
+ constructor(host4, key2, initial = () => []) {
10119
+ super(host4, key2, (model) => new Set(initial(model)));
10120
+ }
10121
+ has(id) {
10122
+ return this.value.has(id);
10123
+ }
10124
+ toggle(id, open = !this.has(id)) {
10125
+ if (open === this.has(id)) return;
10126
+ const next = new Set(this.value);
10127
+ if (open) next.add(id);
10128
+ else next.delete(id);
10129
+ this.value = next;
10130
+ }
10131
+ details(id) {
10132
+ return (event2) => this.toggle(id, event2.currentTarget.open);
10133
+ }
10134
+ };
10135
+
10055
10136
  // src/app/files.ts
10056
10137
  function relative(path) {
10057
10138
  return path.replace(/^\//, "");
@@ -10203,6 +10284,8 @@ var SliccFiles = class extends ModelElement {
10203
10284
  #entries = [];
10204
10285
  #pending = false;
10205
10286
  #reveal = null;
10287
+ #folders = new ViewState(this, "files", () => null);
10288
+ #applied = null;
10206
10289
  constructor() {
10207
10290
  super();
10208
10291
  this.count = 0;
@@ -10358,7 +10441,8 @@ var SliccFiles = class extends ModelElement {
10358
10441
  const tree = this.#tree();
10359
10442
  const changes2 = this.model ? changesOf(this.model).changes() : [];
10360
10443
  if (!tree) return;
10361
- if (tree.paths.length === 0) tree.expanded = ancestors(changes2.map((change) => change.path));
10444
+ if (tree.paths.length === 0)
10445
+ tree.expanded = this.#folders.value ?? ancestors(changes2.map((change) => change.path));
10362
10446
  tree.paths = treePaths(this.#entries);
10363
10447
  tree.gitStatus = treeStatus(changes2);
10364
10448
  this.#show();
@@ -10528,8 +10612,20 @@ var SliccFiles = class extends ModelElement {
10528
10612
  @tree-search=${this.#seed}
10529
10613
  @tree-matches=${this.#matches}
10530
10614
  @tree-search-close=${this.#closed}
10615
+ @tree-expand=${this.#expand}
10531
10616
  ></slicc-file-tree>`;
10532
10617
  }
10618
+ #expand(event2) {
10619
+ this.#applied = event2.detail.expanded;
10620
+ this.#folders.value = event2.detail.expanded;
10621
+ }
10622
+ updated() {
10623
+ const folders = this.#folders.value;
10624
+ const tree = this.#tree();
10625
+ if (!folders || folders === this.#applied || !tree?.paths.length) return;
10626
+ this.#applied = folders;
10627
+ tree.expanded = folders;
10628
+ }
10533
10629
  };
10534
10630
  var SliccFileView = class extends ThemedElement {
10535
10631
  static properties = {
@@ -10995,14 +11091,19 @@ function attention(section) {
10995
11091
  var SliccMonitor = class extends ModelElement {
10996
11092
  static properties = {
10997
11093
  ...ModelElement.properties,
10998
- expanded: { state: true },
10999
11094
  copied: { state: true }
11000
11095
  };
11096
+ #sections = new ViewState(this, "monitor", () => ({}));
11001
11097
  constructor() {
11002
11098
  super();
11003
- this.expanded = {};
11004
11099
  this.copied = null;
11005
11100
  }
11101
+ get expanded() {
11102
+ return this.#sections.value;
11103
+ }
11104
+ set expanded(value) {
11105
+ this.#sections.value = value;
11106
+ }
11006
11107
  static styles = [
11007
11108
  shared,
11008
11109
  panelCss,
@@ -11460,7 +11561,8 @@ function fillShown(fill2) {
11460
11561
  }
11461
11562
  var SliccAgents = class extends ModelElement {
11462
11563
  static properties = { ...ModelElement.properties, errors: { state: true } };
11463
- #expanded = /* @__PURE__ */ new Set();
11564
+ #expanded = new ViewSet(this, "agents");
11565
+ #revealed = new ViewState(this, "agents:revealed", () => "");
11464
11566
  #pointer = false;
11465
11567
  #modality = (event2) => {
11466
11568
  this.#pointer = event2.type === "pointerdown";
@@ -11731,16 +11833,16 @@ var SliccAgents = class extends ModelElement {
11731
11833
  #reveal(model) {
11732
11834
  const focused2 = model.agent.list().find((agent) => agent.id === model.agent.active());
11733
11835
  const cone = focused2?.kind === "cone" ? focused2.id : focused2?.parentId;
11734
- if (cone) this.#expanded.add(cone);
11836
+ if (!cone || focused2?.id === this.#revealed.value) return;
11837
+ this.#revealed.value = focused2?.id;
11838
+ this.#expanded.toggle(cone, true);
11735
11839
  }
11736
11840
  #select(agent) {
11737
11841
  this.model?.agent.select(agent.id);
11738
11842
  if (agent.kind === "cone") this.#toggle(agent, true);
11739
11843
  }
11740
11844
  #toggle(agent, open = !this.#expanded.has(agent.id)) {
11741
- if (open) this.#expanded.add(agent.id);
11742
- else this.#expanded.delete(agent.id);
11743
- this.requestUpdate();
11845
+ this.#expanded.toggle(agent.id, open);
11744
11846
  }
11745
11847
  focus() {
11746
11848
  this.focusOn('li[tabindex="0"]');
@@ -13783,6 +13885,7 @@ var SliccNetwork = class extends ModelElement {
13783
13885
  guestError: { state: true },
13784
13886
  guestsOff: { state: true }
13785
13887
  };
13888
+ #open = new ViewSet(this, "network");
13786
13889
  #leaderId = "";
13787
13890
  #guestId = "";
13788
13891
  constructor() {
@@ -14500,7 +14603,7 @@ var SliccNetwork = class extends ModelElement {
14500
14603
  </div>
14501
14604
  ${this.#permission(port, links)}
14502
14605
  ${links.devices.length ? b`<ul aria-labelledby="links">${links.devices.map((device) => this.#device(port, status2, device))}</ul>
14503
- <details><summary>Link another device</summary><div>${this.#join(port, links)}</div></details>` : this.#join(port, links)}
14606
+ <details ?open=${this.#open.has("link")} @toggle=${this.#open.details("link")}><summary>Link another device</summary><div>${this.#join(port, links)}</div></details>` : this.#join(port, links)}
14504
14607
  ${this.linksError ? b`<div class="error" role="alert" data-error="links"><swc-icon-alert-triangle size="s" aria-hidden="true"></swc-icon-alert-triangle><span>${this.linksError}</span></div>` : A}
14505
14608
  </section>`;
14506
14609
  }
@@ -14699,7 +14802,7 @@ var SliccNetwork = class extends ModelElement {
14699
14802
  }
14700
14803
  #whole(status2) {
14701
14804
  if (wholeWeb.has(routeOf(status2))) {
14702
- return b`<details><summary>Other ways to reach the whole web</summary><div>${this.#ways(status2)}</div></details>`;
14805
+ return b`<details ?open=${this.#open.has("whole")} @toggle=${this.#open.details("whole")}><summary>Other ways to reach the whole web</summary><div>${this.#ways(status2)}</div></details>`;
14703
14806
  }
14704
14807
  return b`<section aria-labelledby="whole">
14705
14808
  <h2 id="whole">Get the whole web</h2>
@@ -31832,7 +31935,7 @@ function lick(message, handlers, stamp = true, pile = flatPile) {
31832
31935
  b`<div class="lick-frame"><div class="lick" data-id=${message.id} data-channel=${message.channel} data-severity=${message.severity ?? A}><div class="lick-head">${head2}</div></div>${reaction}</div>`
31833
31936
  );
31834
31937
  }
31835
- const details = b`<details class="lick" data-id=${message.id} data-channel=${message.channel} data-severity=${message.severity ?? A}>
31938
+ const details = b`<details class="lick" data-id=${message.id} data-channel=${message.channel} data-severity=${message.severity ?? A} ?open=${handlers.licks?.has(message.id) ?? false} @toggle=${handlers.licks?.details(message.id) ?? A}>
31836
31939
  <summary class="lick-head">${head2}<swc-icon-chevron-right class="chevron" size="s" aria-hidden="true"></swc-icon-chevron-right></summary>
31837
31940
  <pre class="output">${message.body}</pre>
31838
31941
  </details>`;
@@ -32865,6 +32968,7 @@ var SliccUpdates = class extends ModelElement {
32865
32968
  busy: { state: true },
32866
32969
  problems: { state: true }
32867
32970
  };
32971
+ #logs = new ViewSet(this, "updates");
32868
32972
  constructor() {
32869
32973
  super();
32870
32974
  this.pending = /* @__PURE__ */ new Set();
@@ -33244,7 +33348,7 @@ var SliccUpdates = class extends ModelElement {
33244
33348
  <span>${error}</span>
33245
33349
  </div>` : A}
33246
33350
  ${this.#actions(item2)}
33247
- ${item2.log ? b`<details><summary>Install log</summary><pre>${item2.log}</pre></details>` : A}
33351
+ ${item2.log ? b`<details ?open=${this.#logs.has(`item:${item2.id}`)} @toggle=${this.#logs.details(`item:${item2.id}`)}><summary>Install log</summary><pre>${item2.log}</pre></details>` : A}
33248
33352
  </article>`;
33249
33353
  }
33250
33354
  #packageMeta(item2, all2) {
@@ -33318,7 +33422,7 @@ var SliccUpdates = class extends ModelElement {
33318
33422
  <swc-icon-alert-triangle size="s" aria-hidden="true"></swc-icon-alert-triangle>
33319
33423
  <span>${error}</span>
33320
33424
  </div>` : A}
33321
- ${item2.log ? b`<details><summary>Install log</summary><pre><span class="package">${item2.package}</span>\n${item2.log}</pre></details>` : A}
33425
+ ${item2.log ? b`<details ?open=${this.#logs.has(`package:${item2.id}`)} @toggle=${this.#logs.details(`package:${item2.id}`)}><summary>Install log</summary><pre><span class="package">${item2.package}</span>\n${item2.log}</pre></details>` : A}
33322
33426
  </li>`;
33323
33427
  }
33324
33428
  #list(items, all2, port) {
@@ -35271,7 +35375,9 @@ var SliccBrowser = class extends ModelElement {
35271
35375
  #last = null;
35272
35376
  #shown = actionPage;
35273
35377
  #anchor = null;
35274
- #expanded = /* @__PURE__ */ new Set();
35378
+ #expanded = new ViewSet(this, "browser");
35379
+ #recent = new ViewState(this, "browser:recent", () => false);
35380
+ #chosen = new ViewState(this, "browser:tab", () => void 0);
35275
35381
  constructor() {
35276
35382
  super();
35277
35383
  this.address = "";
@@ -35742,7 +35848,6 @@ var SliccBrowser = class extends ModelElement {
35742
35848
  this.#shots.clear();
35743
35849
  this.#frames.clear();
35744
35850
  this.#pulse.clear();
35745
- this.#expanded.clear();
35746
35851
  this.#shown = actionPage;
35747
35852
  const tabs = model.browser.list();
35748
35853
  this.#driven = new Set(tabs.filter((tab) => tab.controlled).map((tab) => tab.id));
@@ -35805,7 +35910,10 @@ var SliccBrowser = class extends ModelElement {
35805
35910
  this.#driven = driven;
35806
35911
  const ids = new Set(tabs.map((tab) => tab.id));
35807
35912
  for (const id of this.#frames.keys()) if (!ids.has(id)) this.#frames.delete(id);
35808
- if (this.#focus && !ids.has(this.#focus)) this.#show(null);
35913
+ if (this.#focus && !ids.has(this.#focus)) {
35914
+ this.#show(null);
35915
+ this.#chosen.value = void 0;
35916
+ }
35809
35917
  this.#stale();
35810
35918
  this.requestUpdate();
35811
35919
  }
@@ -35859,11 +35967,22 @@ var SliccBrowser = class extends ModelElement {
35859
35967
  return shot ?? null;
35860
35968
  }
35861
35969
  #show(id) {
35970
+ this.#chosen.value = id;
35971
+ this.#visit(id);
35972
+ this.requestUpdate();
35973
+ }
35974
+ #visit(id) {
35862
35975
  this.#focus = id;
35863
35976
  this.#copied = null;
35864
35977
  this.#shown = actionPage;
35865
35978
  if (id) this.#pulse.delete(id);
35866
- this.requestUpdate();
35979
+ }
35980
+ willUpdate(changed) {
35981
+ super.willUpdate(changed);
35982
+ const chosen = this.#chosen.value;
35983
+ if (chosen === void 0 || chosen === this.#focus) return;
35984
+ if (chosen === null || this.model?.browser.list().some((tab) => tab.id === chosen))
35985
+ this.#visit(chosen);
35867
35986
  }
35868
35987
  #focused() {
35869
35988
  return this.model?.browser.list().find((tab) => tab.id === this.#focus);
@@ -36021,8 +36140,7 @@ var SliccBrowser = class extends ModelElement {
36021
36140
  this.requestUpdate();
36022
36141
  }
36023
36142
  #toggle(id) {
36024
- if (!this.#expanded.delete(id)) this.#expanded.add(id);
36025
- this.requestUpdate();
36143
+ this.#expanded.toggle(id);
36026
36144
  }
36027
36145
  #what(action) {
36028
36146
  const url = linked.has(action.kind) ? action.value : void 0;
@@ -36052,7 +36170,7 @@ var SliccBrowser = class extends ModelElement {
36052
36170
  if (this.#width >= narrowWidth)
36053
36171
  return b`<h3>Recent actions</h3>${more}<ol class="actions" aria-label="Recent actions" tabindex="0">${actions2.map((action) => this.#row(action))}</ol>`;
36054
36172
  const last = actions2.at(-1);
36055
- return b`<details>
36173
+ return b`<details ?open=${this.#recent.value} @toggle=${(event2) => this.#recent.value = event2.currentTarget.open}>
36056
36174
  <summary><swc-icon-chevron-right class="chevron" size="s" aria-hidden="true"></swc-icon-chevron-right>Recent actions (${all2.length})</summary>
36057
36175
  ${more}
36058
36176
  <ol class="actions" aria-label="Earlier actions" tabindex="0">${actions2.slice(0, -1).map((action) => this.#row(action))}</ol>
@@ -37192,7 +37310,9 @@ var SliccChat = class extends ThemedElement {
37192
37310
  }
37193
37311
  `
37194
37312
  ];
37313
+ #licks = new ViewSet(this, "chat:licks");
37195
37314
  #handlers = {
37315
+ licks: this.#licks,
37196
37316
  color: "light",
37197
37317
  model: null,
37198
37318
  readOnly: false,
@@ -40051,17 +40171,22 @@ var SliccMemory = class extends ModelElement {
40051
40171
  query: { state: true },
40052
40172
  tag: { state: true },
40053
40173
  scope: { state: true },
40054
- open: { state: true },
40055
40174
  editing: { state: true }
40056
40175
  };
40176
+ #open = new ViewState(this, "memory", () => null);
40057
40177
  constructor() {
40058
40178
  super();
40059
40179
  this.query = "";
40060
40180
  this.tag = "all";
40061
40181
  this.scope = everyone;
40062
- this.open = null;
40063
40182
  this.editing = null;
40064
40183
  }
40184
+ get open() {
40185
+ return this.#open.value;
40186
+ }
40187
+ set open(value) {
40188
+ this.#open.value = value;
40189
+ }
40065
40190
  static styles = [
40066
40191
  shared,
40067
40192
  panelCss,
@@ -40708,6 +40833,7 @@ var SliccSecrets = class extends ThemedElement {
40708
40833
  picked: { state: true },
40709
40834
  again: { state: true }
40710
40835
  };
40836
+ #removed = new ViewState(this, "settings:removed", () => false);
40711
40837
  #tick;
40712
40838
  #offline = false;
40713
40839
  constructor() {
@@ -41516,7 +41642,7 @@ var SliccSecrets = class extends ThemedElement {
41516
41642
  const live = sorted.filter((secret) => secret.state !== "removed");
41517
41643
  const removed = sorted.filter((secret) => secret.state === "removed");
41518
41644
  return b`${live.length ? b`<ul class="secrets" aria-labelledby="secrets">${live.map(row)}</ul>` : A}
41519
- ${removed.length ? b`<details class="removed" data-removed>
41645
+ ${removed.length ? b`<details class="removed" data-removed ?open=${this.#removed.value} @toggle=${(event2) => this.#removed.value = event2.currentTarget.open}>
41520
41646
  <summary>Removed (${removed.length})</summary>
41521
41647
  <ul class="secrets">${removed.map(row)}</ul>
41522
41648
  </details>` : A}`;
@@ -42758,7 +42884,6 @@ ${data.html}`;
42758
42884
  };
42759
42885
 
42760
42886
  // src/app/tool-runs.ts
42761
- var opened = /* @__PURE__ */ new Map();
42762
42887
  var caches = /* @__PURE__ */ new WeakMap();
42763
42888
  function cache(port) {
42764
42889
  const found = caches.get(port);
@@ -42775,6 +42900,7 @@ var SliccToolRuns = class extends i2 {
42775
42900
  model: { attribute: false },
42776
42901
  owners: { attribute: false }
42777
42902
  };
42903
+ #opened = new ViewState(this, "chat:runs", () => /* @__PURE__ */ new Map());
42778
42904
  #icons = null;
42779
42905
  #count = 0;
42780
42906
  #resize = new ResizeObserver(() => this.#end());
@@ -42804,12 +42930,13 @@ var SliccToolRuns = class extends i2 {
42804
42930
  return this.calls.map((call) => call.id).join(" ");
42805
42931
  }
42806
42932
  get #open() {
42807
- return opened.get(this.#anchor) ?? "";
42933
+ return this.#opened.value.get(this.#anchor) ?? "";
42808
42934
  }
42809
42935
  #set(value) {
42810
- if (value) opened.set(this.#anchor, value);
42811
- else opened.delete(this.#anchor);
42812
- this.requestUpdate();
42936
+ const next = new Map(this.#opened.value);
42937
+ if (value) next.set(this.#anchor, value);
42938
+ else next.delete(this.#anchor);
42939
+ this.#opened.value = next;
42813
42940
  }
42814
42941
  #toggled(call, event2) {
42815
42942
  const open = event2.currentTarget.open;
@@ -85330,11 +85457,13 @@ var SliccFileTree = class extends HTMLElement {
85330
85457
  #expanded = [];
85331
85458
  #mount = null;
85332
85459
  #query = "";
85460
+ #chosen = [];
85461
+ #applying = false;
85333
85462
  get paths() {
85334
85463
  return this.#paths;
85335
85464
  }
85336
85465
  set paths(value) {
85337
- const expanded = this.#paths.length > 0 ? this.#expandedNow() : this.#expanded;
85466
+ const expanded = this.#paths.length > 0 ? this.#held() : this.#expanded;
85338
85467
  this.#paths = value;
85339
85468
  this.#tree?.resetPaths(value, { initialExpandedPaths: expanded });
85340
85469
  this.#research();
@@ -85352,10 +85481,15 @@ var SliccFileTree = class extends HTMLElement {
85352
85481
  return this.#query;
85353
85482
  }
85354
85483
  set query(value) {
85484
+ const was = this.#query;
85355
85485
  this.#query = value;
85356
85486
  this.#tree?.setSearch(value || null);
85487
+ if (was && !value) this.expanded = this.#chosen;
85357
85488
  this.#matched();
85358
85489
  }
85490
+ #held() {
85491
+ return this.#query ? this.#chosen : this.#expandedNow();
85492
+ }
85359
85493
  get empty() {
85360
85494
  return this.hasAttribute("data-empty");
85361
85495
  }
@@ -85375,10 +85509,12 @@ var SliccFileTree = class extends HTMLElement {
85375
85509
  }
85376
85510
  #expandedNow() {
85377
85511
  const tree = this.#tree;
85378
- return this.#paths.filter((path) => {
85379
- const item2 = path.endsWith("/") ? tree?.getItem(path) : null;
85380
- return !!item2 && "isExpanded" in item2 && item2.isExpanded();
85381
- }).map((path) => path.slice(0, -1));
85512
+ const open = (path) => {
85513
+ const item2 = tree?.getItem(path);
85514
+ return item2 && "isExpanded" in item2 ? item2.isExpanded() : null;
85515
+ };
85516
+ const shown4 = (path) => path.split("/").slice(0, -2).every((_9, i26, parts2) => open(`${parts2.slice(0, i26 + 1).join("/")}/`) !== false);
85517
+ return this.#paths.filter((path) => path.endsWith("/") && open(path) === true && shown4(path)).map((path) => path.slice(0, -1));
85382
85518
  }
85383
85519
  get gitStatus() {
85384
85520
  return this.#status;
@@ -85392,6 +85528,36 @@ var SliccFileTree = class extends HTMLElement {
85392
85528
  }
85393
85529
  set expanded(value) {
85394
85530
  this.#expanded = value;
85531
+ this.#apply();
85532
+ }
85533
+ #apply() {
85534
+ const tree = this.#tree;
85535
+ if (!tree || this.#paths.length === 0) return;
85536
+ if (this.#query) {
85537
+ this.#chosen = this.#expanded;
85538
+ return;
85539
+ }
85540
+ const wanted = new Set(this.#expanded);
85541
+ this.#applying = true;
85542
+ for (const path of this.#paths) {
85543
+ const item2 = path.endsWith("/") ? tree.getItem(path) : null;
85544
+ if (!item2 || !("isExpanded" in item2)) continue;
85545
+ const open = wanted.has(path.slice(0, -1));
85546
+ if (item2.isExpanded() === open) continue;
85547
+ if (open) item2.expand();
85548
+ else item2.collapse();
85549
+ }
85550
+ this.#applying = false;
85551
+ this.#chosen = this.#expandedNow();
85552
+ }
85553
+ #changed() {
85554
+ if (this.#applying || !this.#tree || this.#query) return;
85555
+ const expanded = this.#expandedNow();
85556
+ if (expanded.join("\n") === this.#chosen.join("\n")) return;
85557
+ this.#chosen = expanded;
85558
+ this.dispatchEvent(
85559
+ new CustomEvent("tree-expand", { detail: { expanded }, bubbles: true, composed: true })
85560
+ );
85395
85561
  }
85396
85562
  get tree() {
85397
85563
  return this.#tree;
@@ -85418,9 +85584,12 @@ var SliccFileTree = class extends HTMLElement {
85418
85584
  onSearchChange: (value) => this.#searched(value)
85419
85585
  });
85420
85586
  this.#tree.render({ containerWrapper: this.#mount });
85587
+ this.#chosen = this.#query ? this.#expanded : this.#expandedNow();
85588
+ this.#tree.subscribe(() => this.#changed());
85421
85589
  this.#matched();
85422
85590
  }
85423
85591
  disconnectedCallback() {
85592
+ if (this.#paths.length > 0) this.#expanded = this.#held();
85424
85593
  this.#tree?.cleanUp();
85425
85594
  this.#tree = null;
85426
85595
  this.#mount?.replaceChildren();