@ai-ecoverse/slicc-spectrum 1.52.0 → 1.53.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
@@ -119,7 +119,7 @@ Things found while building this against wterm 0.5.4, recorded here, not pushed
119
119
 
120
120
  ### Layouts and rails
121
121
 
122
- The layout follows the width of `<slicc-app>`, which it reflects as its `screen` attribute: `phone` (under 640 px) has one column, `tablet` (under 1200 px) two, `desktop` three, like SLICC v6. Each screen class starts with few panels open and saves its own layout (`slicc-ui.layout.v2.<screen>`). Every surface has a home side, left, center or right. Closed panels wait as icons in the left and right rails (on a phone, in one bar at the bottom); one click puts a panel back on its side, next to its neighbours. Each thread, a cone's or a scoop's conversation, opens in its own chat panel (`chat:<agent id>`), as a tab in the same group as the other chats. Selecting a thread opens or reveals its tab, activating a chat tab selects its thread, and a thread's chat closes when the thread goes away (frozen or deleted).
122
+ The layout follows the width of `<slicc-app>`, which it reflects as its `screen` attribute: `phone` (under 640 px) has one column, `tablet` (under 1200 px) two, `desktop` three, like SLICC v6. Each screen class starts with few panels open and saves its own layout (`slicc-ui.layout.v2.<screen>`). Every surface has a home side, left, center or right. Every panel has an icon in the left or right rail (on a phone, in one bar at the bottom, with the rest in its More menu). The icon stays when its panel is open; while the panel is visible it shows selected (`.selected`, `data-visible`, labelled "{Title}, shown"). One click puts a closed panel back on its side, next to its neighbours, or brings an open one to the front. Each thread, a cone's or a scoop's conversation, opens in its own chat panel (`chat:<agent id>`), as a tab in the same group as the other chats. Selecting a thread opens or reveals its tab, activating a chat tab selects its thread, and a thread's chat closes when the thread goes away (frozen or deleted).
123
123
 
124
124
  `surfaces` on `<slicc-app>` (default: every surface, exported as `surfaces`) is what the app offers: the panels it can open, the View menu, the rails and `Alt+1…9`. A saved layout with a panel it doesn't offer is dropped for the default. An embedder can pass a subset with its own `open` per screen class. The agent picker and the tray show only when `chat` is offered, the update status only with `updates`, the network indicator only with `network` and a `network` port, and the settings button only with `settings`. Elements with `slot="status"` show in the header as compact single-line items, before the update status, and are cut with an ellipsis when long; a `hidden` one takes no space. There are no strips under the header. On a phone the status items are hidden.
125
125
 
@@ -151,8 +151,8 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
151
151
  | `<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). |
152
152
  | `<slicc-freezer>` | Every conversation in the session, from `frozen()`: cones, scoops and agent runs, each with a kind badge, live ones first with a Live light, then the newest first. A live row offers **Open**, which selects that agent; a stopped one offers **Thaw** and **Remove**, which deletes its working folders after a confirmation. A thawed scoop or agent run keeps its row, which says what it was thawed as. **Delete cone**, in the agents list (or <kbd>Delete</kbd> on a cone's row) and the header's cone picker, stops a cone after a confirmation; `/freeze` does it at once. Either way its conversation stays here, and a row without a `kind` counts as a cone. |
153
153
  | `<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. |
154
- | `<slicc-network>` | Network: the route and its health in plain words with the bios `detail`, a line on browser automation (`data-browser`: through the SLICC extension, through slicc-node, through a linked device's browser, that slicc-node has no browser attached when `route` is `proxy` and `via` is `null`, or not available with how to get it) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, an **Internet traffic** picker (`data-action="exit"`, in `[data-exit-picker]` with the exit's kind) when the port has `setExit`, the route line saying where traffic goes, and under the picker a warning that traffic waits while the chosen device isn't connected (`data-exit="device"`, `data-held`, with **Send directly**, `data-action="exit-direct"`, which calls `setExit(null)`) or, when `route` isn't `tailnet`, the Tailscale exit node in use (`data-exit="node"`), a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, a **Linked devices** section (`data-links`: `linked`, `connecting`, `reconnecting`, `offline`, `none` or `permission`; each device row has a **Prompting** or **Watching** badge, `data-controlling`, while it controls the agent, and **Allow remote control**, `sp-switch[data-action="remote-control"]`, behind a confirm when turned on, or `data-control="fixed"` for a device on this computer; with `access`, an **Allow** group of **View**, **Control** and **Terminal** switches, `data-action="access-view"`, `access-control` and `access-terminal`, each confirmed when turned on, Terminal's saying it gives a root shell, and Control disabled while View is off; a device that asks to follow shows **Asks to follow** and **Allow**, `data-action="allow-follow"`; a **Terminal open** badge, `data-terminals`, counts its terminals; with `followUrl`, the join details add **Open on another device** with a QR code and the link, `data-join="follow"` and `data-copy="follow-url"`) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and `npx @ai-ecoverse/slicc-node` with a copy button (`data-action="copy-command"`), folded away when the route already reaches the whole web (`proxy`, `extension` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line names the device and its `policy`, or says it's waiting for it, and `data-route` is `link`, whatever `route` says), and recent failures with host, error and time. 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. 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. Offered only when the model has a `network` port. |
155
- | `<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. |
154
+ | `<slicc-network>` | Network: the route and its health in plain words with the bios `detail`, a line on browser automation (`data-browser`: through the SLICC extension, through slicc-node, through a linked device's browser, that slicc-node has no browser attached when `route` is `proxy` and `via` is `null`, or not available with how to get it) when the status has `browser`, **Check again** (`data-action="check"`) when the port has `check`, an **Internet traffic** picker (`data-action="exit"`, in `[data-exit-picker]` with the exit's kind) when the port has `setExit`, the route line saying where traffic goes, and under the picker a warning that traffic waits while the chosen device isn't connected (`data-exit="device"`, `data-held`, with **Send directly**, `data-action="exit-direct"`, which calls `setExit(null)`) or, when `route` isn't `tailnet`, the Tailscale exit node in use (`data-exit="node"`), a **Tailscale** section (`data-tailnet` with the state) when the status has `tailnet`, a **Linked devices** section (`data-links`: `linked`, `connecting`, `reconnecting`, `offline`, `none` or `permission`; each device row has a **Prompting** or **Watching** badge, `data-controlling`, while it controls the agent, and **Allow remote control**, `sp-switch[data-action="remote-control"]`, behind a confirm when turned on, or `data-control="fixed"` for a device on this computer; with `access`, an **Allow** group of **View**, **Control** and **Terminal** switches, `data-action="access-view"`, `access-control` and `access-terminal`, each confirmed when turned on, Terminal's saying it gives a root shell, and Control disabled while View is off; a device that asks to follow shows **Asks to follow** and **Allow**, `data-action="allow-follow"`; a **Terminal open** badge, `data-terminals`, counts its terminals; with `followUrl`, the join details add **Open on another device** with a QR code and the link, `data-join="follow"` and `data-copy="follow-url"`) when the status has `links`, **Get the whole web** with the Chrome extension link (`data-action="install-extension"`, when `extensionUrl` is set) and `npx @ai-ecoverse/slicc-node` with a copy button (`data-action="copy-command"`), folded away when the route already reaches the whole web (`proxy`, `extension` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line names the device and its `policy`, or says it's waiting for it, and `data-route` is `link`, whatever `route` says), and recent failures with host, error and time. When the model has a `tray` port, a **Sync** section (`data-sync` with the connection) follows the connection: the tray's state, the float's name, role and runtime, the join link with **Copy** (`data-action="copy-tray-join"`), the followers (`li[data-follower]`), and **Disconnect** (`data-action="tray-disconnect"`, confirmed) or **Reconnect** (`data-action="tray-reconnect"`). 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. 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. Offered only when the model has a `network` port. |
155
+ | `<slicc-tray>` | The tray items in the header, each opening a panel when that panel is in `panels`: the connection (`#chip`, a status light with the float's name and followers, `data-surface="network"`) opens Network at its **Sync** section, and the budget (`#usage`, `data-surface="monitor"`) opens the Monitor. Without the panel an item is a plain status with its label. |
156
156
  | `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
157
157
  | `<slicc-prompt>` | The name dialog `prompt()` opens. Not for direct use. |
158
158
  | `<slicc-file-tree>` | Generic. [`@pierre/trees`](https://www.npmjs.com/package/@pierre/trees) through its `web-components` entry: `paths` (folders end in `/`), `gitStatus`, `expanded`, `reveal(path, focus)`. Fires `file-open` on click and on <kbd>Enter</kbd>. |
@@ -221,7 +221,7 @@ The dummy supports `createDummyModel({ updates: 'boot' | 'starting' | 'current'
221
221
 
222
222
  `createDummyModel({ approvals: 'agent' | 'user' | 'many' | 'follower' | 'fail' | 'soon' | 'elsewhere' | 'withdrawn' })` (`?approvals=` on `/ui/`) adds waiting sudo requests: one from an agent, one from a terminal user, three in a row (the last as `www-data`), one without `decide`, one whose `decide` fails, one that expires in 24 seconds, or two whose first is answered in another tab or withdrawn after a few seconds. Decisions are recorded in `model.approvals.decisions`, and `model.approvals.end(id, reason)` ends a request.
223
223
 
224
- On phones the update status and the tray show only their status light 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. The update status comes first so progress and failures remain reachable on narrow screens.
224
+ 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. The update status comes first so progress and failures remain reachable on narrow screens.
225
225
 
226
226
  ### Kernel model
227
227
 
package/dist/slicc-ui.js CHANGED
@@ -11895,6 +11895,122 @@ async function copyText(text2) {
11895
11895
  }
11896
11896
  }
11897
11897
 
11898
+ // src/app/tray.ts
11899
+ var connectionVariant = {
11900
+ live: "positive",
11901
+ connecting: "info",
11902
+ reconnecting: "info",
11903
+ stalled: "notice",
11904
+ offline: "neutral",
11905
+ error: "negative"
11906
+ };
11907
+ var connectionLabel = {
11908
+ live: "Live",
11909
+ connecting: "Connecting",
11910
+ reconnecting: "Reconnecting",
11911
+ stalled: "Stalled",
11912
+ offline: "Offline",
11913
+ error: "Error"
11914
+ };
11915
+ function budgetVariant(percent2) {
11916
+ const variant = meterVariant(percent2);
11917
+ return variant === "informative" ? "neutral" : variant;
11918
+ }
11919
+ function budgetLine(status2) {
11920
+ return `Spent $${status2.spent.toFixed(2)} \xB7 $${status2.rate.toFixed(2)}/h. ${status2.budget.percent}% of the ${status2.budget.window} budget, ${status2.budget.resets}.`;
11921
+ }
11922
+ var SliccTray = class extends ModelElement {
11923
+ static properties = {
11924
+ ...ModelElement.properties,
11925
+ panels: { attribute: false }
11926
+ };
11927
+ constructor() {
11928
+ super();
11929
+ this.panels = ["network", "monitor"];
11930
+ }
11931
+ static styles = [
11932
+ shared,
11933
+ i`
11934
+ :host {
11935
+ display: inline-flex;
11936
+ align-items: center;
11937
+ gap: var(--swc-spacing-75);
11938
+ }
11939
+ .chip > span {
11940
+ display: inline-flex;
11941
+ align-items: center;
11942
+ gap: var(--swc-spacing-75);
11943
+ }
11944
+ span.chip,
11945
+ span.usage {
11946
+ display: inline-flex;
11947
+ align-items: center;
11948
+ padding-inline: var(--swc-spacing-100);
11949
+ }
11950
+ swc-status-light {
11951
+ align-self: center;
11952
+ }
11953
+ .chip .followers {
11954
+ display: inline-flex;
11955
+ align-items: center;
11956
+ gap: var(--swc-spacing-50);
11957
+ }
11958
+ :host([compact]) .chip .detail,
11959
+ :host([compact]) .usage {
11960
+ display: none;
11961
+ }
11962
+ .muted {
11963
+ color: var(--swc-neutral-subdued-content-color-default);
11964
+ }
11965
+ `
11966
+ ];
11967
+ subscribe(model) {
11968
+ return [model.tray.on("status", () => this.requestUpdate())];
11969
+ }
11970
+ #open(id, section) {
11971
+ this.dispatchEvent(
11972
+ new CustomEvent("show-surface", {
11973
+ detail: section ? { id, section } : { id },
11974
+ bubbles: true,
11975
+ composed: true
11976
+ })
11977
+ );
11978
+ }
11979
+ #item(id, surface2, label, content, open) {
11980
+ return this.panels.includes(surface2) ? b`<swc-action-button id=${id} class=${id} size="s" quiet data-surface=${surface2} accessible-label=${label} @click=${open}>${content}</swc-action-button>` : b`<span id=${id} class=${id} tabindex="0" role="img" aria-label=${label}>${content}</span>`;
11981
+ }
11982
+ render() {
11983
+ const model = this.model;
11984
+ if (!model) return b``;
11985
+ const status2 = model.tray.status();
11986
+ const connection = connectionLabel[status2.connection];
11987
+ const followers = `${status2.followers.length} ${status2.followers.length === 1 ? "follower" : "followers"}`;
11988
+ const light = status2.connection === "live" ? status2.name : `${status2.name}, ${connection}`;
11989
+ const budget = `Budget ${status2.budget.percent}%`;
11990
+ return b`${this.#item(
11991
+ "chip",
11992
+ "network",
11993
+ `Tray ${status2.name}: ${connection}, ${followers}`,
11994
+ b`<span>
11995
+ <swc-status-light size="s" variant=${connectionVariant[status2.connection]}>
11996
+ <span class="detail">${light}</span>
11997
+ </swc-status-light>
11998
+ <span class="detail muted followers"><swc-icon-user-group size="s"></swc-icon-user-group>${status2.followers.length}</span>
11999
+ </span>`,
12000
+ () => this.#open("network", "sync")
12001
+ )}
12002
+ <swc-tooltip for="chip" placement="bottom">${`${connection}, ${followers}`}</swc-tooltip>
12003
+ ${this.#item(
12004
+ "usage",
12005
+ "monitor",
12006
+ `${budget}. ${budgetLine(status2)}`,
12007
+ b`<swc-status-light size="s" variant=${budgetVariant(status2.budget.percent)}>${budget}</swc-status-light>`,
12008
+ () => this.#open("monitor")
12009
+ )}
12010
+ <swc-tooltip for="usage" placement="bottom">${budgetLine(status2)}</swc-tooltip>`;
12011
+ }
12012
+ };
12013
+
11898
12014
  // src/app/network.ts
11899
12015
  var networkHealth = {
11900
12016
  ok: ["positive", "Full access"],
@@ -12139,7 +12255,8 @@ var SliccNetwork = class extends ModelElement {
12139
12255
  busy: { state: true },
12140
12256
  tailnetError: { state: true },
12141
12257
  tailnetCopied: { state: true },
12142
- linksError: { state: true }
12258
+ linksError: { state: true },
12259
+ syncError: { state: true }
12143
12260
  };
12144
12261
  constructor() {
12145
12262
  super();
@@ -12151,6 +12268,7 @@ var SliccNetwork = class extends ModelElement {
12151
12268
  this.tailnetError = "";
12152
12269
  this.tailnetCopied = "";
12153
12270
  this.linksError = "";
12271
+ this.syncError = "";
12154
12272
  }
12155
12273
  static styles = [
12156
12274
  r(link_default),
@@ -12181,6 +12299,7 @@ var SliccNetwork = class extends ModelElement {
12181
12299
  display: grid;
12182
12300
  gap: var(--swc-spacing-200);
12183
12301
  align-content: start;
12302
+ scroll-margin-top: var(--swc-spacing-300);
12184
12303
  }
12185
12304
  h2 {
12186
12305
  margin: 0;
@@ -12424,7 +12543,21 @@ var SliccNetwork = class extends ModelElement {
12424
12543
  this.error = "";
12425
12544
  this.tailnetError = "";
12426
12545
  this.linksError = "";
12427
- return model.network ? [model.network.on("network", () => this.requestUpdate())] : [];
12546
+ this.syncError = "";
12547
+ const update = () => this.requestUpdate();
12548
+ return [
12549
+ model.tray.on("status", update),
12550
+ ...model.network ? [model.network.on("network", update)] : []
12551
+ ];
12552
+ }
12553
+ reveal(section) {
12554
+ void this.updateComplete.then(() => {
12555
+ const target = this.renderRoot.querySelector(
12556
+ `section[aria-labelledby="${section}"]`
12557
+ );
12558
+ target?.querySelector("swc-button, swc-action-button")?.focus({ focusVisible: false });
12559
+ target?.scrollIntoView({ block: "start" });
12560
+ });
12428
12561
  }
12429
12562
  focus() {
12430
12563
  this.focusWith(
@@ -12787,6 +12920,35 @@ var SliccNetwork = class extends ModelElement {
12787
12920
  ${this.error ? b`<div class="error" role="alert"><swc-icon-alert-triangle size="s" aria-hidden="true"></swc-icon-alert-triangle><span>${this.error}</span></div>` : A}
12788
12921
  </section>`;
12789
12922
  }
12923
+ async #disconnect(tray, name, trigger2) {
12924
+ const ok = await confirm({
12925
+ title: `Disconnect from ${name}?`,
12926
+ body: "This SLICC stops syncing with the tray until you reconnect.",
12927
+ action: "Disconnect",
12928
+ variant: "confirmation",
12929
+ trigger: trigger2
12930
+ });
12931
+ if (ok) tray.disconnect();
12932
+ }
12933
+ #sync(tray) {
12934
+ const status2 = tray.status();
12935
+ return b`<section aria-labelledby="sync" data-sync=${status2.connection}>
12936
+ <div class="head">
12937
+ <h2 id="sync">Sync</h2>
12938
+ <swc-status-light variant=${connectionVariant[status2.connection]}>${connectionLabel[status2.connection]}</swc-status-light>
12939
+ </div>
12940
+ <dl class="node">
12941
+ <dt>Name</dt><dd>${status2.name}</dd>
12942
+ <dt>Role</dt><dd>${status2.role}</dd>
12943
+ <dt>Runs in</dt><dd>${status2.kind}</dd>
12944
+ ${status2.joinUrl ? this.#copyRow("tray-join", "Join link", status2.joinUrl, "syncError", "join link") : A}
12945
+ </dl>
12946
+ <h3 id="sync-followers">Followers</h3>
12947
+ ${status2.followers.length ? b`<ul aria-labelledby="sync-followers" data-followers>${status2.followers.map((follower) => b`<li data-follower=${follower.id}>${follower.name} <span class="muted">${follower.device}</span></li>`)}</ul>` : b`<p class="muted" data-followers>No one else is connected.</p>`}
12948
+ <div class="actions">${status2.connection === "offline" ? b`<swc-button size="m" variant="secondary" data-action="tray-reconnect" @click=${() => tray.reconnect()}>Reconnect</swc-button>` : b`<swc-button size="m" variant="secondary" data-action="tray-disconnect" @click=${(event2) => this.#disconnect(tray, status2.name, event2.currentTarget)}>Disconnect</swc-button>`}</div>
12949
+ ${this.syncError ? b`<div class="error" role="alert" data-error="sync"><swc-icon-alert-triangle size="s" aria-hidden="true"></swc-icon-alert-triangle><span>${this.syncError}</span></div>` : A}
12950
+ </section>`;
12951
+ }
12790
12952
  #ways(status2) {
12791
12953
  return reach({
12792
12954
  intro: "Install the SLICC Chrome extension, or run slicc-node on this computer. Either one reaches every site.",
@@ -12827,6 +12989,7 @@ var SliccNetwork = class extends ModelElement {
12827
12989
  const status2 = networkStatus(port);
12828
12990
  return b`<div class="page" tabindex="-1">
12829
12991
  ${this.#state(port, status2)}
12992
+ ${this.#sync(this.model.tray)}
12830
12993
  ${this.#tailnetSection(port, status2)}
12831
12994
  ${this.#linksSection(port, status2)}
12832
12995
  ${this.#whole(status2)}
@@ -13087,14 +13250,9 @@ function defaultLayout(dock, screen, agent, items = surfaces, terminals = []) {
13087
13250
  const chat = chats(dock)[0];
13088
13251
  if (chat) dock.api.getPanel(chat)?.api.setActive();
13089
13252
  }
13090
- function closed(dock, items = surfaces) {
13091
- const grouped = {
13092
- chat: () => chats(dock),
13093
- terminal: () => terminalPanels(dock)
13094
- };
13095
- return items.filter(
13096
- (item2) => item2.id in grouped ? grouped[item2.id]().length === 0 : !dock.has(item2.id)
13097
- );
13253
+ function shown(dock, id) {
13254
+ const ids = id === "chat" ? chats(dock) : id === "terminal" ? terminalPanels(dock) : [id];
13255
+ return ids.some((panel) => dock.api.getPanel(panel)?.api.isVisible === true);
13098
13256
  }
13099
13257
  function sprinkleSurface(sprinkle) {
13100
13258
  return {
@@ -21150,8 +21308,8 @@ var SliccUpdates = class extends ModelElement {
21150
21308
  #checked(at) {
21151
21309
  if (at === null) return b`<span>Not checked yet</span>`;
21152
21310
  const date = new Date(at);
21153
- const shown = date.toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" });
21154
- return b`<span>Checked <time datetime=${date.toISOString()}>${shown}</time></span>`;
21311
+ const shown2 = date.toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" });
21312
+ return b`<span>Checked <time datetime=${date.toISOString()}>${shown2}</time></span>`;
21155
21313
  }
21156
21314
  #item(item2) {
21157
21315
  const [variant, state2] = states[item2.state];
@@ -21208,14 +21366,14 @@ var SliccUpdates = class extends ModelElement {
21208
21366
  #package(item2, all2, act) {
21209
21367
  const light = packageStates[item2.state];
21210
21368
  const busy = this.busy.get(item2.id);
21211
- const shown = busy ? [busy] : act ? item2.actions : [];
21369
+ const shown2 = busy ? [busy] : act ? item2.actions : [];
21212
21370
  const error = this.problems.get(item2.id) ?? item2.error;
21213
21371
  return b`<li data-id=${item2.id} data-state=${item2.state}>
21214
21372
  <div class="name">
21215
21373
  <h3 title=${item2.package}>${item2.label}</h3>
21216
21374
  ${light ? b`<swc-status-light size="s" variant=${light[0]}>${light[1]}</swc-status-light>` : A}
21217
21375
  </div>
21218
- <div class="buttons">${shown.map(
21376
+ <div class="buttons">${shown2.map(
21219
21377
  (action) => b`<swc-button
21220
21378
  size="s"
21221
21379
  variant="secondary"
@@ -21286,7 +21444,7 @@ var allAgents = "slicc:all-agents";
21286
21444
  var newCone = "slicc:new-cone";
21287
21445
  var deleteActiveCone = "slicc:delete-cone";
21288
21446
  var railPitch = 52;
21289
- var phoneRail = ["agents", "files", "changes", "terminal", "browser"];
21447
+ var phoneRail = ["chat", "agents", "files", "changes", "terminal", "browser"];
21290
21448
  function railCapacity(width, count) {
21291
21449
  if (!width) return count;
21292
21450
  const slots = Math.max(1, Math.floor((width - 16 + 4) / railPitch));
@@ -21461,6 +21619,16 @@ var SliccApp = class extends ThemedElement {
21461
21619
  .rail:empty {
21462
21620
  display: none;
21463
21621
  }
21622
+ .rail swc-action-button.selected {
21623
+ --swc-action-button-background-color-default: var(--swc-neutral-background-color-selected-default);
21624
+ --swc-action-button-background-color-hover: var(--swc-neutral-background-color-selected-hover);
21625
+ --swc-action-button-background-color-down: var(--swc-neutral-background-color-selected-down);
21626
+ --swc-action-button-background-color-focus: var(--swc-neutral-background-color-selected-key-focus);
21627
+ --swc-action-button-content-color-default: var(--swc-gray-25);
21628
+ --swc-action-button-content-color-hover: var(--swc-gray-25);
21629
+ --swc-action-button-content-color-down: var(--swc-gray-25);
21630
+ --swc-action-button-content-color-focus: var(--swc-gray-25);
21631
+ }
21464
21632
  .badged {
21465
21633
  position: relative;
21466
21634
  display: inline-flex;
@@ -21779,6 +21947,7 @@ var SliccApp = class extends ThemedElement {
21779
21947
  }
21780
21948
  #activated(event2) {
21781
21949
  const { id } = event2.detail;
21950
+ this.requestUpdate();
21782
21951
  this.terminalPanels.activated(id);
21783
21952
  const agent = chatAgent(id);
21784
21953
  if (agent && agent !== this.model?.agent.active()) this.model?.agent.select(agent);
@@ -21788,7 +21957,11 @@ var SliccApp = class extends ThemedElement {
21788
21957
  defaultLayout(this.dock, this.screen, this.#active(), this.#available(), terminals);
21789
21958
  this.#save();
21790
21959
  }
21791
- show(id) {
21960
+ #showSurface(event2) {
21961
+ const { id, section } = event2.detail;
21962
+ this.show(id, section);
21963
+ }
21964
+ show(id, section) {
21792
21965
  const agent = this.#active();
21793
21966
  if (id === "chat" && agent && this.#offers("chat")) {
21794
21967
  openChat(this.dock, agent, this.screen);
@@ -21808,6 +21981,8 @@ var SliccApp = class extends ThemedElement {
21808
21981
  openSurface(this.dock, item2, this.screen);
21809
21982
  if (id === "updates") this.dock.api.getPanel(id)?.api.updateParameters({ boot: false });
21810
21983
  this.dock.focusPanel(id);
21984
+ if (section)
21985
+ this.dock.content(id)?.reveal?.(section);
21811
21986
  }
21812
21987
  #sprinkle(id) {
21813
21988
  return this.model?.sprinkles.list().find((candidate) => candidate.id === id);
@@ -21840,8 +22015,9 @@ var SliccApp = class extends ThemedElement {
21840
22015
  #railButton(item2, side) {
21841
22016
  const changes = item2.id === "changes" ? this.#changes() : 0;
21842
22017
  const driven = item2.id === "browser" && !!this.model?.browser.list().some((tab) => tab.controlled);
21843
- const label = changes ? `Open ${item2.title}, ${changeCount(changes)}` : driven ? `Open ${item2.title}, a tab is in use` : `Open ${item2.title}`;
21844
- const button = u16`<swc-action-button id=${`rail-${item2.id}`} quiet size="m" accessible-label=${label} data-surface=${item2.id} @click=${() => this.show(item2.id)}>${railIcon(item2.icon)}</swc-action-button>`;
22018
+ const visible2 = shown(this.dock, item2.id);
22019
+ const label = `${visible2 ? `${item2.title}, shown` : `Open ${item2.title}`}${changes ? `, ${changeCount(changes)}` : driven ? ", a tab is in use" : ""}`;
22020
+ const button = u16`<swc-action-button id=${`rail-${item2.id}`} class=${visible2 ? "selected" : ""} quiet size="m" accessible-label=${label} data-surface=${item2.id} ?data-visible=${visible2} @click=${() => this.show(item2.id)}>${railIcon(item2.icon)}</swc-action-button>`;
21845
22021
  const tooltip = b`<swc-tooltip for=${`rail-${item2.id}`} placement=${{ left: "end", right: "start", bottom: "top" }[side]}>${item2.title}</swc-tooltip>`;
21846
22022
  return changes ? b`<span class="badged">${button}<swc-badge size="s" variant="neutral" aria-hidden="true">${changes}</swc-badge></span>${tooltip}` : driven ? b`<span class="badged">${button}<span class="dot" data-driven aria-hidden="true"></span></span>${tooltip}` : b`${button}${tooltip}`;
21847
22023
  }
@@ -21862,20 +22038,20 @@ var SliccApp = class extends ThemedElement {
21862
22038
  #rail(items, side) {
21863
22039
  const primary = side === "bottom" ? items.filter((item2) => phoneRail.includes(item2.id)) : items;
21864
22040
  const fit = primary.length < items.length ? Math.min(primary.length, railCapacity(this.#width, Infinity)) : railCapacity(this.#width, items.length);
21865
- const shown = side === "bottom" ? primary.slice(0, fit) : items;
21866
- return b`<nav class=${`rail ${side}`} aria-label=${`Closed panels, ${side}`} @dragover=${this.#railOver} @dragleave=${this.#railLeave} @drop=${this.#railDrop}>${shown.map(
22041
+ const shown2 = side === "bottom" ? primary.slice(0, fit) : items;
22042
+ return b`<nav class=${`rail ${side}`} aria-label=${`Panels, ${side}`} @dragover=${this.#railOver} @dragleave=${this.#railLeave} @drop=${this.#railDrop}>${shown2.map(
21867
22043
  (item2) => this.#railButton(item2, side)
21868
- )}${this.#more(items.filter((item2) => !shown.includes(item2)))}</nav>`;
22044
+ )}${this.#more(items.filter((item2) => !shown2.includes(item2)))}</nav>`;
21869
22045
  }
21870
22046
  #rails() {
21871
22047
  const dock = this.renderRoot.querySelector("slicc-dock");
21872
- const sprinkles = (this.model?.sprinkles.list() ?? []).filter((sprinkle) => !sprinkle.inline || this.dips.has(sprinkle.id)).map(sprinkleSurface).filter((item2) => !dock?.has(item2.id));
21873
- const shut = dock && this.#started ? [...sprinkles, ...closed(dock, this.#available())] : [];
22048
+ const sprinkles = (this.model?.sprinkles.list() ?? []).filter((sprinkle) => !sprinkle.inline || this.dips.has(sprinkle.id)).map(sprinkleSurface);
22049
+ const all2 = dock && this.#started ? [...sprinkles, ...this.#available()] : [];
21874
22050
  const phone = this.screen === "phone";
21875
22051
  return {
21876
- left: this.#rail(phone ? [] : shut.filter((item2) => item2.side !== "right"), "left"),
21877
- right: this.#rail(phone ? [] : shut.filter((item2) => item2.side === "right"), "right"),
21878
- bottom: phone ? this.#rail(shut, "bottom") : A
22052
+ left: this.#rail(phone ? [] : all2.filter((item2) => item2.side !== "right"), "left"),
22053
+ right: this.#rail(phone ? [] : all2.filter((item2) => item2.side === "right"), "right"),
22054
+ bottom: phone ? this.#rail(all2, "bottom") : A
21879
22055
  };
21880
22056
  }
21881
22057
  newTerminal() {
@@ -22047,7 +22223,7 @@ var SliccApp = class extends ThemedElement {
22047
22223
  ${this.#updatesIndicator()}
22048
22224
  ${this.#remoteIndicator()}
22049
22225
  ${this.#networkIndicator()}
22050
- ${this.#offers("chat") ? b`<slicc-tray .model=${this.model} ?compact=${this.screen === "phone"}></slicc-tray>` : A}
22226
+ ${this.#offers("chat") ? b`<slicc-tray .model=${this.model} .panels=${["network", "monitor"].filter((id) => this.#offers(id))} ?compact=${this.screen === "phone"} @show-surface=${this.#showSurface}></slicc-tray>` : A}
22051
22227
  <sp-action-menu size="s" quiet label="View" @change=${this.#view}>
22052
22228
  <swc-icon-view-grid slot="icon"></swc-icon-view-grid>
22053
22229
  <span slot="label" class="view-label">View</span>
@@ -22081,7 +22257,7 @@ var SliccApp = class extends ThemedElement {
22081
22257
  @open-file=${(event2) => this.#request("file", event2)}
22082
22258
  @open-diff=${(event2) => this.#request("diff", event2)}
22083
22259
  @new-terminal=${() => this.newTerminal()}
22084
- @show-surface=${(event2) => this.show(event2.detail.id)}
22260
+ @show-surface=${this.#showSurface}
22085
22261
  ></slicc-dock>
22086
22262
  ${rails.right}
22087
22263
  </main>
@@ -26463,8 +26639,8 @@ var SliccMemory = class extends ModelElement {
26463
26639
  const scopes = this.#scopes;
26464
26640
  const scope = this.scope;
26465
26641
  const option2 = (item2) => b`<sp-menu-item value=${item2.id}>${item2.label}</sp-menu-item>`;
26466
- const shown = visible(all2, scope, this.tag, this.query);
26467
- return b`<div class="bar"><span>${shown.length} of ${all2.filter((memory) => memory.scope === scope).length} memories</span><span class="spacer"></span>
26642
+ const shown2 = visible(all2, scope, this.tag, this.query);
26643
+ return b`<div class="bar"><span>${shown2.length} of ${all2.filter((memory) => memory.scope === scope).length} memories</span><span class="spacer"></span>
26468
26644
  <swc-action-button size="s" quiet @click=${() => this.editing = "new"}><swc-icon-add slot="icon"></swc-icon-add>Remember</swc-action-button>
26469
26645
  </div>
26470
26646
  <div class="tools">
@@ -26486,7 +26662,7 @@ var SliccMemory = class extends ModelElement {
26486
26662
  </div>
26487
26663
  <div class="list">
26488
26664
  ${this.editing === "new" ? b`<div class="row">${this.#form({ scope })}</div>` : A}
26489
- ${shown.length ? sections(shown).map(
26665
+ ${shown2.length ? sections(shown2).map(
26490
26666
  ([section, items]) => b`<h3>${section}</h3>${items.map((memory) => this.#row(memory))}`
26491
26667
  ) : b`<div class="note">${all2.length ? "No memories match." : "No memories yet."}</div>`}
26492
26668
  </div>`;
@@ -27659,164 +27835,6 @@ ${data.html}`;
27659
27835
  }
27660
27836
  };
27661
27837
 
27662
- // src/app/tray.ts
27663
- var connectionVariant = {
27664
- live: "positive",
27665
- connecting: "info",
27666
- reconnecting: "info",
27667
- stalled: "notice",
27668
- offline: "neutral",
27669
- error: "negative"
27670
- };
27671
- var connectionLabel = {
27672
- live: "Live",
27673
- connecting: "Connecting",
27674
- reconnecting: "Reconnecting",
27675
- stalled: "Stalled",
27676
- offline: "Offline",
27677
- error: "Error"
27678
- };
27679
- var SliccTray = class extends ModelElement {
27680
- static properties = {
27681
- ...ModelElement.properties,
27682
- copied: { state: true }
27683
- };
27684
- constructor() {
27685
- super();
27686
- this.copied = false;
27687
- }
27688
- static styles = [
27689
- shared,
27690
- i`
27691
- :host {
27692
- display: inline-block;
27693
- }
27694
- .chip > span {
27695
- display: inline-flex;
27696
- align-items: center;
27697
- gap: var(--swc-spacing-75);
27698
- }
27699
- .chip swc-status-light {
27700
- align-self: center;
27701
- }
27702
- .chip .followers {
27703
- display: inline-flex;
27704
- align-items: center;
27705
- gap: var(--swc-spacing-50);
27706
- }
27707
- :host([compact]) .chip .detail {
27708
- display: none;
27709
- }
27710
- .muted {
27711
- color: var(--swc-neutral-subdued-content-color-default);
27712
- }
27713
- .panel {
27714
- display: grid;
27715
- gap: var(--swc-spacing-200);
27716
- width: calc(var(--swc-spacing-400) * 12);
27717
- max-width: 100%;
27718
- padding: var(--swc-spacing-300);
27719
- box-sizing: border-box;
27720
- font-size: var(--swc-font-size-100);
27721
- color: var(--swc-neutral-content-color-default);
27722
- }
27723
- dl {
27724
- display: grid;
27725
- grid-template-columns: auto 1fr;
27726
- gap: var(--swc-spacing-75) var(--swc-spacing-200);
27727
- margin: 0;
27728
- }
27729
- dt {
27730
- color: var(--swc-neutral-subdued-content-color-default);
27731
- }
27732
- dd {
27733
- margin: 0;
27734
- }
27735
- h3 {
27736
- margin: 0 0 var(--swc-spacing-75);
27737
- font-size: inherit;
27738
- font-weight: var(--swc-bold-font-weight);
27739
- }
27740
- ul {
27741
- list-style: none;
27742
- padding: 0;
27743
- margin: 0;
27744
- }
27745
- swc-meter {
27746
- width: 100%;
27747
- }
27748
- .actions {
27749
- display: flex;
27750
- flex-wrap: wrap;
27751
- gap: var(--swc-spacing-100);
27752
- }
27753
- `
27754
- ];
27755
- subscribe(model) {
27756
- return [model.tray.on("status", () => this.requestUpdate())];
27757
- }
27758
- async #disconnect(name, trigger2) {
27759
- const body4 = "This SLICC stops syncing with the tray until you reconnect.";
27760
- const title = `Disconnect from ${name}?`;
27761
- if (await confirm({ title, body: body4, action: "Disconnect", variant: "confirmation", trigger: trigger2 }))
27762
- this.model?.tray.disconnect();
27763
- }
27764
- async copy() {
27765
- const url = this.model?.tray.status().joinUrl ?? "";
27766
- await globalThis.navigator?.clipboard?.writeText(url).catch(() => {
27767
- });
27768
- this.copied = true;
27769
- }
27770
- render() {
27771
- const model = this.model;
27772
- if (!model) return b``;
27773
- const status2 = model.tray.status();
27774
- const connection = connectionLabel[status2.connection];
27775
- const followers = `${status2.followers.length} ${status2.followers.length === 1 ? "follower" : "followers"}`;
27776
- const light = status2.connection === "live" ? status2.name : `${status2.name}, ${connection}`;
27777
- return b`<swc-action-button
27778
- id="chip"
27779
- class="chip"
27780
- size="s"
27781
- quiet
27782
- accessible-label=${`Tray ${status2.name}: ${connection}, ${followers}`}
27783
- >
27784
- <span>
27785
- <swc-status-light size="s" variant=${connectionVariant[status2.connection]}>
27786
- <span class="detail">${light}</span>
27787
- </swc-status-light>
27788
- <span class="detail muted followers"><swc-icon-user-group size="s"></swc-icon-user-group>${status2.followers.length}</span>
27789
- </span>
27790
- </swc-action-button>
27791
- <swc-tooltip for="chip" placement="bottom">${`${connection}, ${followers}`}</swc-tooltip>
27792
- <swc-popover for="chip" placement="bottom-end" accessible-label="Tray" @swc-open=${() => {
27793
- this.copied = false;
27794
- }}>
27795
- <div class="panel">
27796
- <dl>
27797
- <dt>Name</dt><dd>${status2.name}</dd>
27798
- <dt>Connection</dt><dd>${connection}</dd>
27799
- <dt>Role</dt><dd>${status2.role}</dd>
27800
- <dt>Runs in</dt><dd>${status2.kind}</dd>
27801
- <dt>Spent</dt><dd>$${status2.spent.toFixed(2)} · $${status2.rate.toFixed(2)}/h</dd>
27802
- </dl>
27803
- <swc-meter size="s" value=${status2.budget.percent} variant=${meterVariant(status2.budget.percent)}>
27804
- <span slot="label">Budget (${status2.budget.window})</span>
27805
- <span slot="description">${status2.budget.resets}</span>
27806
- </swc-meter>
27807
- <div>
27808
- <h3>Followers</h3>
27809
- ${status2.followers.length ? b`<ul>${status2.followers.map((follower) => b`<li>${follower.name} <span class="muted">${follower.device}</span></li>`)}</ul>` : b`<div class="muted">No one else is connected.</div>`}
27810
- </div>
27811
- <div class="actions">
27812
- <swc-action-button size="s" @click=${() => this.copy()}>${this.copied ? "Copied" : "Copy join link"}</swc-action-button>
27813
- ${status2.connection === "offline" ? b`<swc-action-button size="s" @click=${() => model.tray.reconnect()}>Reconnect</swc-action-button>` : b`<swc-action-button size="s" @click=${(event2) => this.#disconnect(status2.name, event2.currentTarget)}>Disconnect</swc-action-button>`}
27814
- </div>
27815
- </div>
27816
- </swc-popover>`;
27817
- }
27818
- };
27819
-
27820
27838
  // node_modules/@pierre/diffs/dist/constants.js
27821
27839
  var DIFFS_TAG_NAME = "diffs-container";
27822
27840
  var DIFFS_DEVELOPMENT_BUILD = (() => {