@ai-ecoverse/slicc-spectrum 1.39.0 → 1.40.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
@@ -133,7 +133,7 @@ Text is Adobe Clean Spectrum VF, falling back to Adobe Clean, and code is Spectr
133
133
  | :--- | :--- |
134
134
  | `<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. When the last app on the page is removed, the page's own inline values come back. |
135
135
  | `<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`. |
136
- | `<slicc-agents>` | Cones with their scoops, status, context fill and unread count. Selecting one makes it the active agent. |
136
+ | `<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. |
137
137
  | `<slicc-chat>` | One thread's conversation (its `agent`, or the active agent if unset), by day: user messages (with attachments, tagged as steered or follow-up by `delivered`, or delegated from another cone), assistant messages (Markdown with tables, task lists, quotes, code and images; thinking; tool calls, grouped into a collapsible cluster from three in a row, each with its input (a shell command after `$`), an optional `meta` such as a timeout, an edit's `diff` shown as a diff view, and an `image`, such as a screenshot, shown as a thumbnail under the closed card and in full when open; 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; 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), standalone tool results, system messages (compaction markers, error cards, notices) and licks, SLICC's events into a cone, on every channel; 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 shows the status, a model picker that names each model's provider and leaves out classifiers and models with `tools: false`, 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. |
138
138
  | `<slicc-composer>` | The message box. <kbd>Enter</kbd> sends, or steers a running reply: the message joins after the current tool round, without interrupting; <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>Enter</kbd> queues a follow-up that runs after the reply and can be withdrawn; <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line; <kbd>Tab</kbd> takes the suggested follow-up; <kbd>↑</kbd>/<kbd>↓</kbd> at the start or end walk this agent's history; <kbd>Esc</kbd> stops. `/` completes commands (`/clear`, `/compact`, `/model`, `/thinking`, `/scoop`, `/stop`, `/freeze` when the port can freeze, `/theme`) and their arguments, `@` mentions agents and files. Attach from the computer, from SLICC's files, a browser screenshot or a shared secret, or paste and drop files (25 MB each). Queued messages wait above it, to send now or drop. Model and thinking pickers, and dictation where the browser has speech recognition. Drafts are kept per agent. |
139
139
  | `<slicc-files>` | The file system as a `<slicc-file-tree>`, with pending changes as git status. Opening a file opens it in a tab. 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, each with **Eject** (`data-action="eject"`, `data-path` the mount point) when the port has `eject`. 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)`. A failed mount, insert or eject shows its message under the bar. |
@@ -195,7 +195,7 @@ Paths are the agent's: `/shared/…` and `/home/…`, nothing else. A refusal or
195
195
 
196
196
  | Port | Later backed by | |
197
197
  | :--- | :--- | :--- |
198
- | `agent` | `@ai-ecoverse/slicc-agent` | Cones and scoops, the active agent, conversations, `send` (with attachments, as a send, a steer or a queued follow-up) and `stop`, the queue, suggestions, answers to questions, permission decisions, compaction, clearing (`clear(agentId)` starts a new conversation: on a cone, a port with `freeze` first puts the old one in the Freezer as its own row, which thaws into a cone of its own; a scoop's is just reset), the model per agent and new scoops. The optional `rewind(agentId, messageId)` drops the user turn before that message and everything after it, and resolves with the turn's prompt (text and attachments) for the composer, or `null`. The optional `drop(agentId)` removes a scoop: it stops and its working folder is deleted, while its conversation and reports are kept, and `createCone(name)` resolves with a new cone; the dummy rejects a cone name that is taken. `freeze(agentId)` is optional too: without it, **Delete cone** and `/freeze` don't show. `frozen()` lists the Freezer's rows; a `FrozenCone` may carry `kind` (`cone`, `scoop` or `agent`), `live` for a conversation that's still running, and `thawedAs`, the id of the cone a scoop or agent run was thawed into. `thaw(id)` brings a cone back, or forks a scoop or agent run into a new cone, and `discard(id)` removes a row. Names don't have to be unique. Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`. |
198
+ | `agent` | `@ai-ecoverse/slicc-agent` | Cones and scoops, the active agent, conversations, `send` (with attachments, as a send, a steer or a queued follow-up) and `stop`, the queue, suggestions, answers to questions, permission decisions, compaction, clearing (`clear(agentId)` starts a new conversation: on a cone, a port with `freeze` first puts the old one in the Freezer as its own row, which thaws into a cone of its own; a scoop's is just reset), the model per agent and new scoops. The optional `rewind(agentId, messageId)` drops the user turn before that message and everything after it, and resolves with the turn's prompt (text and attachments) for the composer, or `null`. The optional `drop(agentId)` removes a scoop: it stops and its working folder is deleted, while its conversation and reports are kept, and `createCone(name)` resolves with a new cone; the dummy rejects a cone name that is taken. `freeze(agentId)` is optional too: without it, **Delete cone** and `/freeze` don't show. `frozen()` lists the Freezer's rows; a `FrozenCone` may carry `kind` (`cone`, `scoop` or `agent`), `live` for a conversation that's still running, and `thawedAs`, the id of the cone a scoop or agent run was thawed into. `thaw(id)` brings a cone back, or forks a scoop or agent run into a new cone, and `discard(id)` removes a row. Names don't have to be unique, and an optional `Agent.title` names a cone's conversation; without it, the first user message does. Assistant messages stream: the same message is re-emitted as its parts grow and its tool calls go from `running` to `done`. |
199
199
  | `files` | slicc-kernel's OPFS | `list`, `read`, `write`, `remove`, and pending changes with their before and after text (`accept`, `revert`). The optional `mountFolder()` asks for a local folder and resolves with its mount point (for example `/mnt/photos`), or `null` when the user cancels; the panel calls it synchronously in the click, so the port can call `showDirectoryPicker()`. The optional `mounts()` lists the mount points, and `eject(path)` unmounts one (the folder stays on disk). `mountFolder(path)` inserts the picked folder at a mount point that waits for one; the optional `needsFolder()` lists those mount points. The port emits `mounts` and then `files` after every mount and eject, including those made from the shell, and `mounts` whenever `needsFolder()` changes. |
200
200
  | `terminals` | slicc-kernel's terminal API | Terminal sessions; `backend(id)` returns a `TerminalBackend` for `<slicc-terminal>`. |
201
201
  | `browser` | `@ai-ecoverse/slicc-cdp` | Tabs, the active tab, `open`, `navigate`, `close`, `screenshot`. |
package/dist/slicc-ui.js CHANGED
@@ -10019,6 +10019,46 @@ var SliccFileView = class extends ThemedElement {
10019
10019
  }
10020
10020
  };
10021
10021
 
10022
+ // src/app/names.ts
10023
+ var untitled = "No messages yet";
10024
+ function short(text2, max2 = 60) {
10025
+ return text2.length > max2 ? `${text2.slice(0, max2 - 1).trimEnd()}\u2026` : text2;
10026
+ }
10027
+ function agentTitle(model, agent) {
10028
+ if (agent.title !== void 0) return agent.title.trim();
10029
+ const first2 = model.agent.messages(agent.id).find((message) => message.role === "user");
10030
+ return first2?.role === "user" ? first2.text.trim().split("\n")[0].trim() : "";
10031
+ }
10032
+ function twinned(name, id, rows) {
10033
+ return rows.some((row) => row.id !== id && row.name === name);
10034
+ }
10035
+ function withTitle(name, title) {
10036
+ return `${name} (${short(title) || untitled.toLowerCase()})`;
10037
+ }
10038
+ function coneLabel(model, agent) {
10039
+ const cones = model.agent.list().filter((other) => other.kind === "cone");
10040
+ if (agent.kind !== "cone" || !twinned(agent.name, agent.id, cones)) return agent.name;
10041
+ return withTitle(agent.name, agentTitle(model, agent));
10042
+ }
10043
+ function watchTitles(model, update) {
10044
+ const seen = /* @__PURE__ */ new Map();
10045
+ const check2 = (id) => {
10046
+ const agent = model.agent.list().find((candidate) => candidate.id === id);
10047
+ if (agent?.kind !== "cone") return;
10048
+ const title = agentTitle(model, agent);
10049
+ if (seen.get(id) === title) return;
10050
+ seen.set(id, title);
10051
+ update();
10052
+ };
10053
+ const stops = [
10054
+ model.agent.on("messages", check2),
10055
+ model.agent.on("message", ({ agentId }) => check2(agentId))
10056
+ ];
10057
+ return () => {
10058
+ for (const stop of stops) stop();
10059
+ };
10060
+ }
10061
+
10022
10062
  // src/app/freezer.ts
10023
10063
  function ago(at, now = Date.now()) {
10024
10064
  const days = Math.floor((now - at) / 864e5);
@@ -10029,7 +10069,7 @@ function ago(at, now = Date.now()) {
10029
10069
  }
10030
10070
  async function deleteCone(model, agent, trigger2) {
10031
10071
  const ok = await confirm({
10032
- title: `Delete cone ${agent.name}?`,
10072
+ title: `Delete cone ${coneLabel(model, agent)}?`,
10033
10073
  body: "It stops, with its scoops. Its conversation stays in the Freezer, where you can thaw it.",
10034
10074
  action: "Delete cone",
10035
10075
  variant: "confirmation",
@@ -10109,7 +10149,7 @@ var SliccFreezer = class extends ModelElement {
10109
10149
  }
10110
10150
  async #remove(cone, trigger2) {
10111
10151
  const ok = await confirm({
10112
- title: `Remove ${cone.name} from the Freezer?`,
10152
+ title: `Remove ${this.#named(cone)} from the Freezer?`,
10113
10153
  body: "Its working folders are deleted. The conversation stays in the session database. You can\u2019t undo this.",
10114
10154
  action: "Remove",
10115
10155
  variant: "destructive",
@@ -10117,6 +10157,10 @@ var SliccFreezer = class extends ModelElement {
10117
10157
  });
10118
10158
  if (ok) this.model?.agent.discard(cone.id);
10119
10159
  }
10160
+ #named(cone) {
10161
+ const rows = this.model?.agent.frozen() ?? [];
10162
+ return twinned(cone.name, cone.id, rows) ? withTitle(cone.name, cone.title) : cone.name;
10163
+ }
10120
10164
  #thawedAs(id) {
10121
10165
  const model = this.model;
10122
10166
  return model.agent.list().find((agent) => agent.id === id)?.name ?? model.agent.frozen().find((row) => row.id === id)?.name ?? id;
@@ -10124,10 +10168,10 @@ var SliccFreezer = class extends ModelElement {
10124
10168
  #actions(cone) {
10125
10169
  const model = this.model;
10126
10170
  if (cone.live) {
10127
- return b`<swc-action-button size="s" quiet data-action="open" accessible-label=${`Open ${cone.name}`} @click=${() => model.agent.select(cone.id)}>Open</swc-action-button>`;
10171
+ return b`<swc-action-button size="s" quiet data-action="open" accessible-label=${`Open ${this.#named(cone)}`} @click=${() => model.agent.select(cone.id)}>Open</swc-action-button>`;
10128
10172
  }
10129
- return b`<swc-action-button size="s" quiet data-action="thaw" accessible-label=${`Thaw ${cone.name}`} @click=${() => model.agent.thaw(cone.id)}>Thaw</swc-action-button>
10130
- <swc-action-button size="s" quiet data-action="remove" accessible-label=${`Remove ${cone.name}`} @click=${(event2) => this.#remove(cone, event2.currentTarget)}>Remove</swc-action-button>`;
10173
+ return b`<swc-action-button size="s" quiet data-action="thaw" accessible-label=${`Thaw ${this.#named(cone)}`} @click=${() => model.agent.thaw(cone.id)}>Thaw</swc-action-button>
10174
+ <swc-action-button size="s" quiet data-action="remove" accessible-label=${`Remove ${this.#named(cone)}`} @click=${(event2) => this.#remove(cone, event2.currentTarget)}>Remove</swc-action-button>`;
10131
10175
  }
10132
10176
  #card(cone) {
10133
10177
  const model = this.model;
@@ -10186,7 +10230,7 @@ var SliccAgents = class extends ModelElement {
10186
10230
  li {
10187
10231
  display: grid;
10188
10232
  grid-template-columns: minmax(0, 1fr) auto auto auto;
10189
- grid-template-areas: 'name unread fill drop' 'status unread fill drop' 'error error error error';
10233
+ grid-template-areas: 'name unread fill drop' 'title unread fill drop' 'status unread fill drop' 'error error error error';
10190
10234
  align-items: center;
10191
10235
  column-gap: var(--swc-spacing-100);
10192
10236
  padding: var(--swc-spacing-75) var(--swc-spacing-200);
@@ -10215,6 +10259,14 @@ var SliccAgents = class extends ModelElement {
10215
10259
  .cone .name {
10216
10260
  font-weight: var(--swc-bold-font-weight);
10217
10261
  }
10262
+ .title {
10263
+ grid-area: title;
10264
+ overflow: hidden;
10265
+ text-overflow: ellipsis;
10266
+ white-space: nowrap;
10267
+ font-size: var(--swc-font-size-75);
10268
+ color: var(--swc-neutral-subdued-content-color-default);
10269
+ }
10218
10270
  .unread {
10219
10271
  --swc-badge-height: var(--swc-spacing-300);
10220
10272
  --swc-badge-padding-block: var(--swc-spacing-50);
@@ -10268,7 +10320,8 @@ var SliccAgents = class extends ModelElement {
10268
10320
  subscribe(model) {
10269
10321
  return [
10270
10322
  model.agent.on("agents", () => this.requestUpdate()),
10271
- model.agent.on("active", () => this.requestUpdate())
10323
+ model.agent.on("active", () => this.requestUpdate()),
10324
+ watchTitles(model, () => this.requestUpdate())
10272
10325
  ];
10273
10326
  }
10274
10327
  #select(id) {
@@ -10366,7 +10419,7 @@ var SliccAgents = class extends ModelElement {
10366
10419
  #deleteButton(agent) {
10367
10420
  if (!this.#deletable(agent)) return A;
10368
10421
  const id = `delete-${agent.id}`;
10369
- const label = `Delete cone ${agent.name}`;
10422
+ const label = `Delete cone ${coneLabel(this.model, agent)}`;
10370
10423
  return b`<swc-action-button
10371
10424
  id=${id}
10372
10425
  class="drop"
@@ -10385,7 +10438,9 @@ var SliccAgents = class extends ModelElement {
10385
10438
  #row(agent, active) {
10386
10439
  const selected = agent.id === active;
10387
10440
  const error = this.errors[agent.id];
10388
- const label = `${agent.name}: ${statusLabel[agent.status]}, ${percent(agent.contextFill)} context${agent.unread ? `, ${agent.unread} unread` : ""}${error ? `. ${error}` : ""}`;
10441
+ const model = this.model;
10442
+ const title = agent.kind === "cone" ? agentTitle(model, agent) : null;
10443
+ const label = `${coneLabel(model, agent)}: ${statusLabel[agent.status]}, ${percent(agent.contextFill)} context${agent.unread ? `, ${agent.unread} unread` : ""}${error ? `. ${error}` : ""}`;
10389
10444
  return b`<li
10390
10445
  role="option"
10391
10446
  class=${agent.kind}
@@ -10398,6 +10453,7 @@ var SliccAgents = class extends ModelElement {
10398
10453
  @click=${() => this.#select(agent.id)}
10399
10454
  >
10400
10455
  <span class="name">${agent.name}</span>
10456
+ ${title === null ? A : b`<span class="title">${title || untitled}</span>`}
10401
10457
  ${agent.unread ? b`<swc-badge class="unread" size="s" variant="accent">${agent.unread}</swc-badge>` : A}
10402
10458
  <span class="fill">${percent(agent.contextFill)}</span>
10403
10459
  ${statusLight(agent.status)}
@@ -15320,6 +15376,13 @@ var SliccApp = class extends ThemedElement {
15320
15376
  sp-picker {
15321
15377
  min-width: calc(var(--swc-spacing-400) * 8);
15322
15378
  }
15379
+ header sp-menu-item [slot='description'] {
15380
+ display: block;
15381
+ max-inline-size: calc(var(--swc-spacing-1000) * 4);
15382
+ overflow: hidden;
15383
+ text-overflow: ellipsis;
15384
+ white-space: nowrap;
15385
+ }
15323
15386
  slicc-dock {
15324
15387
  height: 100%;
15325
15388
  }
@@ -15344,6 +15407,7 @@ var SliccApp = class extends ThemedElement {
15344
15407
  update();
15345
15408
  }),
15346
15409
  changesOf(model).on("changes", update),
15410
+ watchTitles(model, update),
15347
15411
  model.terminals.on("terminals", () => {
15348
15412
  this.#prune();
15349
15413
  update();
@@ -15759,7 +15823,7 @@ var SliccApp = class extends ThemedElement {
15759
15823
  ${this.#offers("chat") ? i17(
15760
15824
  cones.map((agent) => agent.id).join(" "),
15761
15825
  b`<sp-picker size="s" label="Agent" value=${this.#cone()} @change=${this.#pick}>
15762
- ${cones.map((agent) => b`<sp-menu-item value=${agent.id}>${agent.name}</sp-menu-item>`)}
15826
+ ${cones.map((agent) => b`<sp-menu-item value=${agent.id}>${agent.name}<span slot="description">${agentTitle(this.model, agent) || untitled}</span></sp-menu-item>`)}
15763
15827
  ${this.model?.agent.createCone ? b`<sp-menu-divider></sp-menu-divider><sp-menu-item value=${newCone} data-action="new-cone"><swc-icon-add slot="icon"></swc-icon-add>New cone</sp-menu-item>` : A}
15764
15828
  ${this.#deletable() ? b`${this.model?.agent.createCone ? A : b`<sp-menu-divider></sp-menu-divider>`}<sp-menu-item value=${deleteActiveCone} data-action="delete-cone"><swc-icon-delete slot="icon"></swc-icon-delete>Delete cone</sp-menu-item>` : A}
15765
15829
  ${this.#offers("agents") ? b`<sp-menu-divider></sp-menu-divider><sp-menu-item value=${allAgents}>Show all agents</sp-menu-item>` : A}