@ai-ecoverse/slicc-spectrum 1.73.2 → 1.74.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
@@ -136,7 +136,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
136
136
  | :--- | :--- |
137
137
  | `<slicc-app>` | 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>`. Saves the dock layout per screen class to `storage` (default `localStorage`) under `layoutKey`. While connected it sets `overscroll-behavior` to `none !important` on the page's `html` and `body`, so only scroll areas inside panels rubber-band. 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. When the last app on the page is removed, the page's own inline values and `theme-color` contents come back. With a `follow` port, the header shows a `<slicc-follow>` chip, and while the follower connects, waits to be allowed, is unlinked or fails, a `<slicc-follow page>` (`.alone[data-follow]` with the page kind) takes the place of the panels; without Terminal access, the terminal surface and New terminal aren't offered, and open terminals close when it's taken away. |
138
138
  | `<slicc-dock>` | Generic. [dockview-core](https://dockview.dev/) 8 with a theme mapped to Spectrum tokens, in its own shadow root. Panels come from `factories` (`name → (params) => HTMLElement`). `open`, `close`, `has`, `content`, `focusPanel`, `cycleGroup`, `cycleTab`, `toJSON`, `restore`, `clear`, and the full `api`. Tabs have a context menu (close, close others, float, maximize). Events: `layout-change`, `active-panel-change`, `panel-close`. |
139
- | `<slicc-agents>` | Cones with their scoops, status, context fill and unread count. A cone's row has a second line with its conversation title, as does its item in the header's cone picker: `Agent.title`, or else the first line of the first user message, or "No messages yet". When cones share a name, their accessible names and the Delete cone, Freezer and confirmation labels add the title, as in "Delete cone sliccy (Hello from Bedrock)". Selecting one makes it the active agent. |
139
+ | `<slicc-agents>` | A tree (`role="tree"`) of cones, with their scoops hidden under them. A row's first line starts with one circle: its unread count when there are unread messages (`9+` from ten up, white on the accent fill), otherwise a dot coloured by status. Then comes the name; the context fill, when it's high, sits in the top right corner. Every row has up to two lines starting with its status in words (`.state`, such as "Working"), then its conversation title, so the status stays visible when the unread count takes the circle; the accessible name says the status and the exact unread count too. A cone's row also has a ribbon along its bottom with one fixed-width segment per scoop, coloured by status; it's `aria-hidden`, and the cone's `aria-description` sums it up, as in "2 scoops: 1 working, 1 idle". On hover, on keyboard focus of the row, or when <kbd>Tab</kbd> from the selected row reaches its drop or delete button, a strip the full height of the row's right edge holds that button over the text; the context fill stays on top of it, and it hides again once a pointer that clicked it leaves. A click, <kbd>Enter</kbd> or <kbd>Space</kbd> on a cone expands or collapses it, as do <kbd>→</kbd> and <kbd>←</kbd> (<kbd>→</kbd> on an expanded cone moves to its first scoop, <kbd>←</kbd> on a scoop to its cone); only the focused cone is expanded, and its scoops list below it. A cone with scoops carries `aria-expanded`, and `aria-controls` names its scoop rows while they show. Expanding and collapsing have no motion. **New cone** sits at the bottom. The conversation title is also shown in the header's cone picker: `Agent.title`, or else the first line of the first user message, or "No messages yet". When cones share a name, their accessible names and the Delete cone, Freezer and confirmation labels add the title, as in "Delete cone sliccy (Hello from Bedrock)". Selecting one makes it the active agent. |
140
140
  | `<slicc-chat>` | One thread's conversation (its `agent`, or the active agent if unset), by day. A message's time shows only when it is at least two minutes newer than the last time shown, and the first one after each day separator always shows; a hidden time stays for screen readers (`time.sr` with `datetime`, or `.sr` on licks and cards), and a meta row left empty isn't rendered. User messages (with attachments and a quiet timestamp, `time.when`, by that rule; the reader's own carry no name or delivery tag, while `data-mode` and `data-delivered` still say how they went; a message from another cone, a follower or a guest says who sent it, and an agent's request carries its name and an Agent badge; a message from another agent, an agent's request or a message `from` a cone (`origin` `agent`, or `from` set without `origin` `follower` or `guest`), is `[data-remote]`: a subtle indigo bubble and a 🤖 pill on its bottom edge, `aria-hidden` since the byline names the sender; people, the reader included, never get it), assistant messages (with the time by that rule, and the model only on the first reply and whenever it changes from the previous one, `.meta .model` with the usage as its tooltip; without a model the usage is the time's tooltip, and with neither it is the reply's own (`.reply[title]`); the turn always carries the usage as its `aria-description`; the cone's name isn't repeated; Markdown with tables, task lists, quotes, code and images; thinking; tool runs, one icon per run (`<slicc-tool-runs>`): three or more runs in a row form a cluster, a strip of icon buttons (`data-action="open-run"`, `data-target` the call's id, named by its `title` and status, `aria-expanded`) that each open one run's card below it, and on the right the model's label for the run, a button (`data-action="open-cluster"`, `aria-expanded`, named "Show all runs" while it has no text) that opens every card; the strip's tooltip and the group's accessible name sum it up, such as "3 commands, 1 read. Done"; prose breaks a run, hidden thinking and source links don't, and a standalone tool message or a done reply with only tool calls continues the run before it on the same day, its `data-id` moving onto its first card (`.tool[data-id]`); a single run is a strip of one, its icon and its `title` as the label, which opens with <kbd>Enter</kbd> or <kbd>Space</kbd> like the strip's buttons. A strip and its cards share one width, the column less a wide margin on each side, so they read as centred, and their edges line up. Icons fill from the start of a space kept for five, so a run that grows doesn't move its strip or its label; past five, the strip shows the newest five and scrolls back to the older ones, with <kbd>Tab</kbd> or a swipe, and returns to the newest when a run is added or the strip comes back into view. The label sits at the end and is cut with an ellipsis before it reaches that space. Under 480 px of column width the label takes a row of its own under the icons, which then show as many whole icons as fit. A cluster's label comes from the agent port's optional `toolLabel(agentId, callIds)`, asked once for each agent port, agent and set of runs; while a growing run waits for its new label, it keeps its last one, and without `toolLabel` the label is empty. A command's icon is the favicon of the site it names when an open browser tab already has a `data:` favicon for it (nothing is fetched; otherwise the globe), else the file browser's icon for the file it names, else the code icon; other tools show their kind's icon. 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. 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; 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, with their time by the two-minute rule; a lick's `severity` (`warn` or `error`) turns its channel badge into a notice or negative one with an alert icon. Permission requests are allowed or denied in place. The header is one row: the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false` (with no model on offer, **Set up a model**, `data-action="set-up-model"`, opens Settings), the thinking picker, the context meter and **New conversation**, which calls `clear` and puts focus back in the composer; on a cone, when the port can freeze, its tooltip says the old conversation stays in the Freezer. It never wraps: when it's short of room, the model picker's label is cut with an ellipsis, and under 560px wide (`header[data-compact]`) New conversation is icon-only, with `accessible-label` and its tooltip. The pickers re-read `models()` on `settings`, `accounts` and `agents`. When a live agent's model, or `Settings.model`, is no longer offered and its provider (the id before `/`) isn't a connected account, it moves to the offered chat model with `default: true`, else the first one; frozen agents are left alone, and so is everything on a follower unless it has Control and is connected. The chat re-runs this on `follow` too, and `<slicc-app>` does it as well, when it binds, on those events and on `follow`, so a new cone that starts on a provider that isn't set up moves at once. The transcript pads its end, and its `scroll-padding-block-end`, by the composer's measured height, so at the bottom the last message sits above the composer and a focused element scrolls clear of it. While a reply streams, its status icon follows what it is doing: a pencil for an edit, a page for a read, a magnifier for a search, an eye for the browser and an hourglass for any other tool; thinking and text keep the AI logo. Between the header and the transcript, a `<slicc-notices>` shows the model's notices. On a follower without Control, a note (`p[data-follow="watch"]`) takes the composer's place, the header shows the model's name instead of the pickers and New conversation, and there's no suggested follow-up. |
141
141
  | `<slicc-notices>` | The model's `notices` as in-line alerts with their actions and a close button, after a banner from the `tabs` port when another tab is paused or runs a different SLICC, and a warning for each waiting approval when the `approvals` port has no `decide` (`[data-approval-notice]`); renders nothing without any. |
142
142
  | `<slicc-approvals>` | The model's waiting `approvals` as a modal alert dialog, one at a time, oldest first; `<slicc-app>` mounts it. Renders nothing without `approvals.decide`. A request that `secretRules()` flags shows a warning (`[data-secret-rules]`, the path or token) and no **Always allow (**Always allow in this directory**, with the cwd in its hint, when the approval has `alwaysScope: 'cwd'` and a `cwd`)**. Focus starts on Deny and goes back, after Deny, Always allow, Allow once, <kbd>Esc</kbd> or Close, to what had it when the dialog opened; inside a terminal, to that `<slicc-terminal>` through its own `focus()`, even when it remounted meanwhile. |
@@ -156,7 +156,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
156
156
  | `<slicc-memory>` | What agents remember, grouped by section: search, filter by scope and tag, expand, edit, add and forget. 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. The count (“N of M memories”) counts the memories in the picked scope. 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.” 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"`). 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`). Below 560 px of panel width, the search takes its own line over the two pickers. The search field is the shared one from `search.ts` (`sp-search[data-action=search]`), as in Files. |
157
157
  | `<slicc-monitor>` | The live monitor: vitals (`.vital[data-id]`), each a meter when it has a `ratio` (budget, fullest context) and otherwise a value, its unit and delta on one line and a sparkline from its `series` (spend, active agents, processes, process memory). 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. 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). 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`). 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. 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). |
158
158
  | `<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. |
159
- | `<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. |
159
+ | `<slicc-sprinkle>` | A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit, even when it loads out of view (the frame measures itself on load, after icons and fonts arrive, and on every resize). 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. |
160
160
  | `<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 on this computer (`proxy`, or a linked device on this computer), through a linked device's browser, that slicc on this computer has no browser to control when it carries the traffic 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 the commands to run slicc, each in a `.command[data-command]` block with a copy button: `curl -fsSL https://get.sliccy.ai/slicc | sh` (its button is `data-action="copy-command"`), `slicc`, and, with Node, `npx sliccy@next`; folded away when the route already reaches the whole web (`proxy`, `extension`, `link` or `tailnet`, whose line names `tailnet.exitNode`) or a linked device is the exit (`exit.kind` `link`: the route line says "Through slicc on this computer" for a `local` device, or names a remote 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, masked (`…/join/••••••`) until **Show** (`data-action="reveal-tray-join"`, which turns into **Hide**), 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"`). When the status has `leaders`, a **Leaders** section (`section[data-leaders]` with the count) follows Sync, since it's the other direction of the same tray link: Sync is this SLICC's own tray and who follows it, Leaders the SLICCs it follows. Each leader is an `li[data-leader]` (the id) with `data-state`: its name, its hub as a quiet second line and a status light with the state in words (Connecting, Reconnecting, Connected, Disconnected, Failed, Unlinked); `failed` adds its `error` as plain text (`[data-leader-error]`, "Couldn’t reach its hub." without one) and **Retry** (`data-action="retry-leader"`), and `unlinked` says "{name} stopped sharing with this SLICC." (`[data-unlinked]`) without actions. **Disconnect** (`data-action="disconnect-leader"`) runs at once, without a confirm, since it's reversible: the row stays as Disconnected (a neutral light), with **Reconnect** (`data-action="reconnect-leader"`) and **Forget** and no Disconnect or Retry. **Forget** (`data-action="forget-leader"`) asks "Forget {name}?" ("To follow it again, you’ll need a new invite from it.") with a negative confirm, and the row goes. Each shows only when the port has its method; while one runs, the row's buttons are disabled, and a failure shows in the row (`data-error="leader"`). 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. |
161
161
  | `<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. |
162
162
  | `<slicc-confirm>` | The confirmation dialog `confirm()` opens. Not for direct use. |
package/dist/slicc-ui.js CHANGED
@@ -10646,7 +10646,7 @@ function watchTitles(model, update) {
10646
10646
  const seen = /* @__PURE__ */ new Map();
10647
10647
  const check3 = (id) => {
10648
10648
  const agent = model.agent.list().find((candidate) => candidate.id === id);
10649
- if (agent?.kind !== "cone") return;
10649
+ if (!agent) return;
10650
10650
  const title = agentTitle(model, agent);
10651
10651
  if (seen.get(id) === title) return;
10652
10652
  seen.set(id, title);
@@ -11406,11 +11406,25 @@ async function createCone(model, trigger2) {
11406
11406
  if (created) port.select(created.id);
11407
11407
  return created;
11408
11408
  }
11409
+ var statusOrder = ["working", "thinking", "waiting", "error", "idle"];
11410
+ function scoopSummary(scoops) {
11411
+ if (scoops.length === 0) return "";
11412
+ const counts = statusOrder.map((status2) => [status2, scoops.filter((scoop) => scoop.status === status2).length]).filter(([, count]) => count > 0).map(([status2, count]) => `${count} ${statusLabel[status2].toLowerCase()}`);
11413
+ return `${scoops.length} scoop${scoops.length === 1 ? "" : "s"}: ${counts.join(", ")}`;
11414
+ }
11415
+ function unreadCount(unread) {
11416
+ return unread > 9 ? "9+" : String(unread);
11417
+ }
11409
11418
  function fillShown(fill2) {
11410
11419
  return fill2 >= 0.5;
11411
11420
  }
11412
11421
  var SliccAgents = class extends ModelElement {
11413
11422
  static properties = { ...ModelElement.properties, errors: { state: true } };
11423
+ #expanded = null;
11424
+ #pointer = false;
11425
+ #modality = (event2) => {
11426
+ this.#pointer = event2.type === "pointerdown";
11427
+ };
11414
11428
  constructor() {
11415
11429
  super();
11416
11430
  this.errors = {};
@@ -11419,16 +11433,22 @@ var SliccAgents = class extends ModelElement {
11419
11433
  shared,
11420
11434
  i`
11421
11435
  :host {
11422
- display: block;
11436
+ display: flex;
11437
+ flex-direction: column;
11423
11438
  height: 100%;
11424
- overflow-y: auto;
11439
+ overflow: hidden;
11425
11440
  background: var(--swc-background-layer-1-color);
11426
11441
  color: var(--swc-neutral-content-color-default);
11427
11442
  font-size: var(--swc-font-size-100);
11428
11443
  }
11444
+ .list {
11445
+ flex: 1;
11446
+ min-block-size: 0;
11447
+ overflow-y: auto;
11448
+ }
11429
11449
  .actions {
11430
11450
  display: flex;
11431
- padding: var(--swc-spacing-75) var(--swc-spacing-100) 0;
11451
+ padding: var(--swc-spacing-75) var(--swc-spacing-100);
11432
11452
  }
11433
11453
  ul {
11434
11454
  list-style: none;
@@ -11436,31 +11456,67 @@ var SliccAgents = class extends ModelElement {
11436
11456
  padding: var(--swc-spacing-75) 0;
11437
11457
  }
11438
11458
  li {
11459
+ --row-background: var(--swc-background-layer-1-color);
11460
+ --row-head: 1lh;
11461
+ --row-description: calc(2 * 1.4 * var(--swc-font-size-75));
11462
+ position: relative;
11439
11463
  display: grid;
11440
- grid-template-columns: minmax(0, 1fr) auto auto auto;
11441
- grid-template-areas: 'name unread fill drop' 'title unread fill drop' 'status unread fill drop' 'error error error error';
11464
+ grid-template-columns: minmax(0, 1fr) auto;
11465
+ grid-template-areas: 'head fill' 'description description' 'ribbon ribbon' 'error error';
11466
+ grid-template-rows: var(--row-head) var(--row-description) var(--swc-spacing-100);
11442
11467
  align-items: center;
11443
11468
  column-gap: var(--swc-spacing-100);
11444
11469
  padding: var(--swc-spacing-75) var(--swc-spacing-200);
11470
+ background: var(--row-background);
11445
11471
  cursor: pointer;
11446
11472
  }
11447
11473
  li.scoop {
11448
- grid-template-areas: 'name unread fill drop' 'status unread fill drop' 'error error error error';
11449
11474
  padding-inline-start: calc(var(--swc-spacing-200) + var(--swc-spacing-300));
11450
11475
  }
11451
11476
  li:hover {
11452
- background: var(--swc-gray-100);
11477
+ --row-background: var(--swc-gray-100);
11453
11478
  }
11454
11479
  li[aria-selected='true'] {
11455
- background: var(--swc-gray-200);
11456
- box-shadow: inset var(--swc-spacing-50) 0 0 var(--swc-accent-visual-color);
11480
+ --row-background: var(--swc-gray-200);
11457
11481
  }
11458
11482
  li:focus-visible {
11459
11483
  outline: var(--swc-focus-indicator-thickness) solid var(--swc-focus-indicator-color);
11460
11484
  outline-offset: calc(-1 * var(--swc-focus-indicator-thickness));
11461
11485
  }
11486
+ .head {
11487
+ grid-area: head;
11488
+ display: flex;
11489
+ align-items: center;
11490
+ gap: var(--swc-spacing-75);
11491
+ min-inline-size: 0;
11492
+ }
11493
+ .head > * {
11494
+ flex: none;
11495
+ }
11496
+ .marker {
11497
+ flex: none;
11498
+ display: inline-flex;
11499
+ align-items: center;
11500
+ justify-content: center;
11501
+ inline-size: var(--swc-spacing-200);
11502
+ block-size: var(--swc-spacing-200);
11503
+ border-radius: var(--swc-corner-radius-full);
11504
+ font-size: var(--swc-font-size-25);
11505
+ font-variant-numeric: tabular-nums;
11506
+ line-height: 1;
11507
+ }
11508
+ .marker.unread {
11509
+ box-sizing: border-box;
11510
+ inline-size: auto;
11511
+ min-inline-size: var(--swc-spacing-200);
11512
+ padding-inline: var(--swc-spacing-50);
11513
+ background: var(--swc-accent-background-color-default);
11514
+ color: var(--swc-white);
11515
+ font-weight: var(--swc-bold-font-weight);
11516
+ }
11462
11517
  .name {
11463
- grid-area: name;
11518
+ flex: 0 1 auto;
11519
+ min-inline-size: 0;
11464
11520
  overflow: hidden;
11465
11521
  text-overflow: ellipsis;
11466
11522
  white-space: nowrap;
@@ -11469,29 +11525,58 @@ var SliccAgents = class extends ModelElement {
11469
11525
  font-weight: var(--swc-bold-font-weight);
11470
11526
  }
11471
11527
  .title {
11472
- grid-area: title;
11528
+ grid-area: description;
11529
+ display: -webkit-box;
11530
+ -webkit-box-orient: vertical;
11531
+ -webkit-line-clamp: 2;
11473
11532
  overflow: hidden;
11474
- text-overflow: ellipsis;
11475
- white-space: nowrap;
11533
+ overflow-wrap: anywhere;
11534
+ line-height: 1.4;
11476
11535
  font-size: var(--swc-font-size-75);
11477
11536
  color: var(--swc-neutral-subdued-content-color-default);
11478
11537
  }
11479
- .unread {
11480
- --swc-badge-height: var(--swc-spacing-300);
11481
- --swc-badge-padding-block: var(--swc-spacing-50);
11482
- --swc-badge-padding-inline: var(--swc-spacing-75);
11483
- --swc-badge-corner-radius: var(--swc-spacing-100);
11484
- --swc-badge-line-height: 1;
11485
- grid-area: unread;
11486
- align-self: center;
11538
+ .state {
11539
+ color: var(--swc-neutral-content-color-default);
11540
+ }
11541
+ .ribbon {
11542
+ grid-area: ribbon;
11543
+ display: flex;
11544
+ align-items: center;
11545
+ gap: var(--swc-spacing-50);
11546
+ overflow: hidden;
11547
+ }
11548
+ .status,
11549
+ .segment {
11550
+ background: var(--swc-neutral-visual-color);
11551
+ }
11552
+ .status[data-status='thinking'],
11553
+ .status[data-status='working'],
11554
+ .segment[data-status='thinking'],
11555
+ .segment[data-status='working'] {
11556
+ background: var(--swc-informative-visual-color);
11557
+ }
11558
+ .status[data-status='waiting'],
11559
+ .segment[data-status='waiting'] {
11560
+ background: var(--swc-notice-visual-color);
11561
+ }
11562
+ .status[data-status='error'],
11563
+ .segment[data-status='error'] {
11564
+ background: var(--swc-negative-visual-color);
11565
+ }
11566
+ .segment {
11567
+ flex: none;
11568
+ inline-size: var(--swc-spacing-200);
11569
+ block-size: var(--swc-spacing-50);
11570
+ border-radius: var(--swc-corner-radius-full);
11487
11571
  }
11488
11572
  .fill {
11489
11573
  grid-area: fill;
11574
+ position: relative;
11575
+ z-index: 1;
11576
+ align-self: start;
11490
11577
  font-size: var(--swc-font-size-75);
11491
11578
  font-variant-numeric: tabular-nums;
11492
11579
  color: var(--swc-neutral-subdued-content-color-default);
11493
- min-width: 4ch;
11494
- text-align: end;
11495
11580
  }
11496
11581
  .fill[data-variant='notice'] {
11497
11582
  color: var(--swc-notice-color-1000);
@@ -11501,18 +11586,20 @@ var SliccAgents = class extends ModelElement {
11501
11586
  color: var(--swc-negative-color-1000);
11502
11587
  font-weight: var(--swc-bold-font-weight);
11503
11588
  }
11504
- swc-status-light {
11505
- grid-area: status;
11506
- justify-self: start;
11507
- }
11508
- .drop {
11509
- grid-area: drop;
11510
- margin-block: calc(-1 * var(--swc-spacing-100));
11511
- visibility: hidden;
11589
+ .drop-cell {
11590
+ position: absolute;
11591
+ inset-block: 0;
11592
+ inset-inline-end: 0;
11593
+ display: flex;
11594
+ align-items: center;
11595
+ padding-inline: var(--swc-spacing-100);
11596
+ background: var(--row-background);
11597
+ opacity: 0;
11512
11598
  }
11513
- li:hover .drop,
11514
- li:focus-within .drop {
11515
- visibility: visible;
11599
+ li:hover .drop-cell,
11600
+ li:focus-visible .drop-cell,
11601
+ .drop-cell[data-shown] {
11602
+ opacity: 1;
11516
11603
  }
11517
11604
  .error {
11518
11605
  grid-area: error;
@@ -11529,23 +11616,46 @@ var SliccAgents = class extends ModelElement {
11529
11616
  white-space: nowrap;
11530
11617
  }
11531
11618
  @media (hover: none), (pointer: coarse) {
11532
- .drop {
11533
- visibility: visible;
11619
+ .drop-cell {
11620
+ opacity: 1;
11534
11621
  }
11535
11622
  }
11536
11623
  `
11537
11624
  ];
11538
11625
  subscribe(model) {
11626
+ const focused2 = this.#focused(model);
11627
+ this.#expanded = focused2?.kind === "scoop" ? focused2.parentId : null;
11539
11628
  return [
11540
11629
  model.agent.on("agents", () => this.requestUpdate()),
11541
- model.agent.on("active", () => this.requestUpdate()),
11630
+ model.agent.on("active", () => {
11631
+ const next = this.#focused(model);
11632
+ if (next) this.#expanded = next.kind === "cone" ? next.id : next.parentId;
11633
+ this.requestUpdate();
11634
+ }),
11542
11635
  watchTitles(model, () => this.requestUpdate()),
11543
- ...model.follow ? [model.follow.on("follow", () => this.requestUpdate())] : []
11636
+ ...model.follow ? [model.follow.on("follow", () => this.requestUpdate())] : [],
11637
+ this.#watchPointer()
11544
11638
  ];
11545
11639
  }
11640
+ #watchPointer() {
11641
+ const types = ["pointerdown", "keydown"];
11642
+ for (const type of types) globalThis.addEventListener(type, this.#modality, true);
11643
+ return () => {
11644
+ for (const type of types) globalThis.removeEventListener(type, this.#modality, true);
11645
+ };
11646
+ }
11647
+ #focused(model) {
11648
+ return model.agent.list().find((agent) => agent.id === model.agent.active());
11649
+ }
11546
11650
  #select(id) {
11547
11651
  this.model?.agent.select(id);
11548
11652
  }
11653
+ #pick(agent) {
11654
+ const open = agent.kind === "cone" && this.#expanded !== agent.id;
11655
+ this.#select(agent.id);
11656
+ if (agent.kind === "cone") this.#expanded = open ? agent.id : null;
11657
+ this.requestUpdate();
11658
+ }
11549
11659
  focus() {
11550
11660
  this.focusOn('li[tabindex="0"]');
11551
11661
  }
@@ -11564,21 +11674,44 @@ var SliccAgents = class extends ModelElement {
11564
11674
  if (event2.key in moves) {
11565
11675
  event2.preventDefault();
11566
11676
  items[Math.max(0, Math.min(items.length - 1, moves[event2.key]))]?.focus();
11567
- } else if (event2.key === "Enter" || event2.key === " ") {
11677
+ return;
11678
+ }
11679
+ const agent = this.model?.agent.list().find((item2) => item2.id === items[index]?.dataset.id);
11680
+ if (!agent) return;
11681
+ if (event2.key === "Enter" || event2.key === " ") {
11682
+ event2.preventDefault();
11683
+ this.#pick(agent);
11684
+ } else if (event2.key === "ArrowRight" && this.#scoops(agent).length > 0) {
11568
11685
  event2.preventDefault();
11569
- const id = items[index]?.dataset.id;
11570
- if (id) this.#select(id);
11686
+ if (this.#expanded === agent.id) items[index + 1].focus();
11687
+ else this.#pick(agent);
11688
+ } else if (event2.key === "ArrowLeft" && agent.kind === "scoop") {
11689
+ event2.preventDefault();
11690
+ items.find((item2) => item2.dataset.id === agent.parentId)?.focus();
11691
+ } else if (event2.key === "ArrowLeft" && this.#expanded === agent.id) {
11692
+ event2.preventDefault();
11693
+ this.#pick(agent);
11571
11694
  } else if (event2.key === "Delete") {
11572
- const agent = this.model?.agent.list().find((item2) => item2.id === items[index]?.dataset.id);
11573
- if (agent && this.#droppable(agent)) {
11695
+ if (this.#droppable(agent)) {
11574
11696
  event2.preventDefault();
11575
11697
  void this.#drop(agent, items[index]);
11576
- } else if (agent && this.#deletable(agent)) {
11698
+ } else if (this.#deletable(agent)) {
11577
11699
  event2.preventDefault();
11578
11700
  void this.#deleteCone(agent, items[index]);
11579
11701
  }
11580
11702
  }
11581
11703
  }
11704
+ #scoops(agent) {
11705
+ if (agent.kind !== "cone") return [];
11706
+ return this.model.agent.list().filter((other) => other.parentId === agent.id);
11707
+ }
11708
+ #focusin(event2) {
11709
+ const cell = event2.target.closest(".drop-cell");
11710
+ cell?.toggleAttribute("data-shown", !this.#pointer);
11711
+ }
11712
+ #focusout(event2) {
11713
+ event2.target.closest(".drop-cell")?.removeAttribute("data-shown");
11714
+ }
11582
11715
  #droppable(agent) {
11583
11716
  return agent.kind === "scoop" && Boolean(this.model?.agent.drop) && !watchOnly(this.model);
11584
11717
  }
@@ -11616,69 +11749,89 @@ var SliccAgents = class extends ModelElement {
11616
11749
  await this.updateComplete;
11617
11750
  this.focus();
11618
11751
  }
11619
- #dropButton(agent) {
11752
+ #dropButton(agent, selected) {
11620
11753
  if (!this.#droppable(agent)) return A;
11621
11754
  const id = `drop-${agent.id}`;
11622
11755
  const label = `Drop scoop ${agent.name}`;
11623
- return b`<swc-action-button
11624
- id=${id}
11625
- class="drop"
11626
- size="l"
11627
- quiet
11628
- tabindex="-1"
11629
- data-action="drop"
11630
- accessible-label=${label}
11631
- @click=${(event2) => {
11756
+ return b`<span class="drop-cell"
11757
+ ><swc-action-button
11758
+ id=${id}
11759
+ class="drop"
11760
+ size="l"
11761
+ quiet
11762
+ tabindex=${selected ? "0" : "-1"}
11763
+ data-action="drop"
11764
+ accessible-label=${label}
11765
+ @click=${(event2) => {
11632
11766
  event2.stopPropagation();
11633
11767
  void this.#drop(agent, event2.currentTarget);
11634
11768
  }}
11635
- ><swc-icon-close slot="icon"></swc-icon-close></swc-action-button
11636
- ><swc-tooltip for=${id} placement="start">${label}</swc-tooltip>`;
11769
+ ><swc-icon-close slot="icon"></swc-icon-close></swc-action-button
11770
+ ><swc-tooltip for=${id} placement="start">${label}</swc-tooltip></span
11771
+ >`;
11637
11772
  }
11638
- #deleteButton(agent) {
11773
+ #deleteButton(agent, selected) {
11639
11774
  if (!this.#deletable(agent)) return A;
11640
11775
  const id = `delete-${agent.id}`;
11641
11776
  const label = `Delete cone ${coneLabel(this.model, agent)}`;
11642
- return b`<swc-action-button
11643
- id=${id}
11644
- class="drop"
11645
- size="l"
11646
- quiet
11647
- tabindex="-1"
11648
- data-action="delete-cone"
11649
- accessible-label=${label}
11650
- @click=${(event2) => {
11777
+ return b`<span class="drop-cell"
11778
+ ><swc-action-button
11779
+ id=${id}
11780
+ class="drop"
11781
+ size="l"
11782
+ quiet
11783
+ tabindex=${selected ? "0" : "-1"}
11784
+ data-action="delete-cone"
11785
+ accessible-label=${label}
11786
+ @click=${(event2) => {
11651
11787
  event2.stopPropagation();
11652
11788
  void this.#deleteCone(agent, event2.currentTarget);
11653
11789
  }}
11654
- ><swc-icon-delete slot="icon"></swc-icon-delete></swc-action-button
11655
- ><swc-tooltip for=${id} placement="start">${label}</swc-tooltip>`;
11790
+ ><swc-icon-delete slot="icon"></swc-icon-delete></swc-action-button
11791
+ ><swc-tooltip for=${id} placement="start">${label}</swc-tooltip></span
11792
+ >`;
11793
+ }
11794
+ #ribbon(scoops) {
11795
+ if (scoops.length === 0) return A;
11796
+ return b`<span class="ribbon" aria-hidden="true">${scoops.map(
11797
+ (scoop) => b`<span class="segment" data-status=${scoop.status}></span>`
11798
+ )}</span>`;
11656
11799
  }
11657
11800
  #row(agent, active) {
11658
11801
  const selected = agent.id === active;
11659
11802
  const error = this.errors[agent.id];
11660
11803
  const model = this.model;
11661
- const title = agent.kind === "cone" ? agentTitle(model, agent) : null;
11804
+ const title = agentTitle(model, agent);
11805
+ const scoops = this.#scoops(agent);
11806
+ const open = this.#expanded === agent.id;
11662
11807
  const unread = selected ? 0 : agent.unread;
11663
11808
  const label = `${coneLabel(model, agent)}: ${statusLabel[agent.status]}, ${percent(agent.contextFill)} context${unread ? `, ${unread} unread` : ""}${error ? `. ${error}` : ""}`;
11809
+ const summary = scoopSummary(scoops);
11664
11810
  return b`<li
11665
- role="option"
11811
+ role="treeitem"
11812
+ id=${`row-${agent.id}`}
11666
11813
  class=${agent.kind}
11667
11814
  data-id=${agent.id}
11815
+ aria-level=${agent.kind === "cone" ? "1" : "2"}
11668
11816
  aria-selected=${selected ? "true" : "false"}
11817
+ aria-expanded=${scoops.length ? String(open) : A}
11818
+ aria-controls=${open && scoops.length ? scoops.map((scoop) => `row-${scoop.id}`).join(" ") : A}
11669
11819
  aria-label=${label}
11820
+ aria-description=${summary || A}
11670
11821
  tabindex=${selected ? "0" : "-1"}
11671
11822
  aria-keyshortcuts=${this.#droppable(agent) || this.#deletable(agent) ? "Delete" : A}
11672
- title=${label}
11673
- @click=${() => this.#select(agent.id)}
11823
+ title=${summary ? `${label}. ${summary}` : label}
11824
+ @click=${() => this.#pick(agent)}
11674
11825
  >
11675
- <span class="name">${agent.name}</span>
11676
- ${title === null ? A : b`<span class="title">${title || untitled}</span>`}
11677
- ${unread ? b`<swc-badge class="unread" size="s" variant="accent">${unread}</swc-badge>` : A}
11826
+ <div class="head">
11827
+ ${unread ? b`<span class="marker unread" aria-hidden="true">${unreadCount(unread)}</span>` : b`<span class="marker status" data-status=${agent.status} aria-hidden="true"></span>`}
11828
+ <span class="name">${agent.name}</span>
11829
+ </div>
11678
11830
  ${fillShown(agent.contextFill) ? b`<span class="fill" data-variant=${meterVariant2(agent.contextFill)}>${percent(agent.contextFill)}</span>` : A}
11679
- ${statusLight(agent.status)}
11680
- ${this.#dropButton(agent)}
11681
- ${this.#deleteButton(agent)}
11831
+ <span class="title"><span class="state">${statusLabel[agent.status]}</span> · ${title || untitled}</span>
11832
+ ${this.#ribbon(scoops)}
11833
+ ${this.#dropButton(agent, selected)}
11834
+ ${this.#deleteButton(agent, selected)}
11682
11835
  ${error ? b`<span class="error">${error}</span>` : A}
11683
11836
  </li>`;
11684
11837
  }
@@ -11696,12 +11849,15 @@ var SliccAgents = class extends ModelElement {
11696
11849
  </div>`;
11697
11850
  }
11698
11851
  render() {
11699
- const agents = ordered(this.model?.agent.list() ?? []);
11852
+ const list5 = this.model?.agent.list() ?? [];
11700
11853
  const active = this.model?.agent.active() ?? "";
11701
11854
  const errors = Object.values(this.errors);
11702
- return b`${this.#actions()}<ul role="listbox" aria-label="Cones and scoops" @keydown=${this.#keydown}>
11703
- ${agents.map((agent) => this.#row(agent, active))}
11704
- </ul>
11855
+ const rows = ordered(list5).filter(
11856
+ (agent) => agent.kind === "cone" || agent.parentId === this.#expanded
11857
+ );
11858
+ return b`<div class="list"><ul role="tree" aria-label="Cones and scoops" @keydown=${this.#keydown} @focusin=${this.#focusin} @focusout=${this.#focusout}>
11859
+ ${rows.map((agent) => this.#row(agent, active))}
11860
+ </ul></div>${this.#actions()}
11705
11861
  <div class="sr" role="status">${errors.at(-1) ?? ""}</div>`;
11706
11862
  }
11707
11863
  };
@@ -41110,12 +41266,13 @@ var bridge = `(() => {
41110
41266
  }
41111
41267
  node.replaceWith(svg);
41112
41268
  }
41269
+ size();
41113
41270
  };
41114
41271
  const fonts = (list) => {
41115
41272
  for (const font of list) {
41116
41273
  const face = new FontFace(font.family, font.data, { weight: String(font.weight) });
41117
41274
  document.fonts.add(face);
41118
- face.load().catch(() => {});
41275
+ face.load().then(size, () => {});
41119
41276
  }
41120
41277
  };
41121
41278
  addEventListener('message', (event) => {
@@ -41131,6 +41288,7 @@ var bridge = `(() => {
41131
41288
  };
41132
41289
  document.addEventListener('DOMContentLoaded', () => {
41133
41290
  window.LucideIcons.render();
41291
+ size();
41134
41292
  new ResizeObserver(size).observe(document.body);
41135
41293
  });
41136
41294
  post({ type: 'slicc-ready' });