@ai-ecoverse/slicc-spectrum 1.1.0 → 1.2.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.
Files changed (122) hide show
  1. package/README.md +29 -6
  2. package/dist/{chunk-4RGMMAR4.js → chunk-4GQ6THC2.js} +3 -3
  3. package/dist/{chunk-H5WU2XYY.js → chunk-5XWUZJTT.js} +2 -2
  4. package/dist/chunk-64K5VONJ.js +11 -0
  5. package/dist/chunk-64K5VONJ.js.map +7 -0
  6. package/dist/{chunk-J45KH7MF.js → chunk-6JNYNXUQ.js} +2 -2
  7. package/dist/chunk-6JYCQXDO.js +8 -0
  8. package/dist/chunk-6JYCQXDO.js.map +7 -0
  9. package/dist/{chunk-ZTPYBVZL.js → chunk-6N3GZNXH.js} +3 -3
  10. package/dist/chunk-6ODADG7T.js +8 -0
  11. package/dist/chunk-6ODADG7T.js.map +7 -0
  12. package/dist/chunk-6T3O5K6R.js +8 -0
  13. package/dist/{chunk-MR7SU4GJ.js → chunk-7PY7B3ZW.js} +2 -2
  14. package/dist/{chunk-Q4AC2O6Z.js → chunk-AXQFWXYZ.js} +8 -2
  15. package/dist/chunk-BDOI3WEE.js +8 -0
  16. package/dist/chunk-BDOI3WEE.js.map +7 -0
  17. package/dist/chunk-BHAGWMDD.js +8 -0
  18. package/dist/chunk-BHAGWMDD.js.map +7 -0
  19. package/dist/chunk-C7X653JJ.js +8 -0
  20. package/dist/chunk-CGS66GRP.js +11 -0
  21. package/dist/chunk-CGS66GRP.js.map +7 -0
  22. package/dist/chunk-CONYKGKV.js +11 -0
  23. package/dist/chunk-CONYKGKV.js.map +7 -0
  24. package/dist/chunk-CU7BC2RI.js +11 -0
  25. package/dist/chunk-CU7BC2RI.js.map +7 -0
  26. package/dist/chunk-EX57SDII.js +8 -0
  27. package/dist/chunk-EX57SDII.js.map +7 -0
  28. package/dist/chunk-GKXQPI4G.js +8 -0
  29. package/dist/chunk-GKXQPI4G.js.map +7 -0
  30. package/dist/{chunk-FN3Y2AYJ.js → chunk-I2NMYJ25.js} +3 -3
  31. package/dist/chunk-I2NMYJ25.js.map +7 -0
  32. package/dist/chunk-I4OYHPAU.js +11 -0
  33. package/dist/chunk-I4OYHPAU.js.map +7 -0
  34. package/dist/{chunk-MTF6JFSL.js → chunk-J3QOBXZJ.js} +2 -2
  35. package/dist/chunk-JBYBW627.js +8 -0
  36. package/dist/chunk-JBYBW627.js.map +7 -0
  37. package/dist/chunk-JWTAKXQS.js +8 -0
  38. package/dist/chunk-JWTAKXQS.js.map +7 -0
  39. package/dist/chunk-KMP5HO3E.js +11 -0
  40. package/dist/chunk-KMP5HO3E.js.map +7 -0
  41. package/dist/chunk-KWPU2PY3.js +14 -0
  42. package/dist/chunk-KWPU2PY3.js.map +7 -0
  43. package/dist/chunk-KYBD6ZRT.js +8 -0
  44. package/dist/chunk-KYBD6ZRT.js.map +7 -0
  45. package/dist/chunk-LBXW6XNL.js +18 -0
  46. package/dist/chunk-LBXW6XNL.js.map +7 -0
  47. package/dist/chunk-LDR4U5KX.js +8 -0
  48. package/dist/chunk-LDR4U5KX.js.map +7 -0
  49. package/dist/chunk-LII3WY6Y.js +11 -0
  50. package/dist/chunk-LII3WY6Y.js.map +7 -0
  51. package/dist/chunk-LY7WVBCB.js +10 -0
  52. package/dist/chunk-LY7WVBCB.js.map +7 -0
  53. package/dist/chunk-MJVAL555.js +8 -0
  54. package/dist/chunk-MJVAL555.js.map +7 -0
  55. package/dist/chunk-NLDFGCIL.js +11 -0
  56. package/dist/chunk-NLDFGCIL.js.map +7 -0
  57. package/dist/{chunk-SZ33GAVH.js → chunk-OFQ56DAN.js} +5 -5
  58. package/dist/chunk-S5EGO4NA.js +11 -0
  59. package/dist/chunk-S5EGO4NA.js.map +7 -0
  60. package/dist/{chunk-24WRGUXG.js → chunk-T2LAT2D6.js} +2 -2
  61. package/dist/chunk-TAZBKRHT.js +10 -0
  62. package/dist/chunk-TAZBKRHT.js.map +7 -0
  63. package/dist/chunk-TK4ZJ7LA.js +5346 -0
  64. package/dist/chunk-TK4ZJ7LA.js.map +7 -0
  65. package/dist/chunk-UCIBH53C.js +11 -0
  66. package/dist/chunk-UCIBH53C.js.map +7 -0
  67. package/dist/chunk-XZM23TK4.js +11 -0
  68. package/dist/chunk-XZM23TK4.js.map +7 -0
  69. package/dist/chunk-YPQKCWWG.js +19 -0
  70. package/dist/chunk-YPQKCWWG.js.map +7 -0
  71. package/dist/chunk-YX6SI5VA.js +11 -0
  72. package/dist/chunk-YX6SI5VA.js.map +7 -0
  73. package/dist/slicc-dummy.js +1475 -57
  74. package/dist/slicc-dummy.js.map +4 -4
  75. package/dist/slicc-terminal.js +7 -5344
  76. package/dist/slicc-terminal.js.map +4 -4
  77. package/dist/slicc-ui.js +56839 -17869
  78. package/dist/slicc-ui.js.map +4 -4
  79. package/dist/types/app/app.d.ts +5 -11
  80. package/dist/types/app/base.d.ts +9 -1
  81. package/dist/types/app/browser.d.ts +25 -0
  82. package/dist/types/app/changes.d.ts +24 -0
  83. package/dist/types/app/chat.d.ts +2 -14
  84. package/dist/types/app/composer.d.ts +69 -0
  85. package/dist/types/app/files.d.ts +56 -0
  86. package/dist/types/app/freezer.d.ts +11 -0
  87. package/dist/types/app/markdown.d.ts +12 -3
  88. package/dist/types/app/memory.d.ts +39 -0
  89. package/dist/types/app/messages.d.ts +21 -0
  90. package/dist/types/app/monitor.d.ts +11 -0
  91. package/dist/types/app/panels.d.ts +11 -3
  92. package/dist/types/app/settings.d.ts +21 -0
  93. package/dist/types/app/spectrum.d.ts +18 -0
  94. package/dist/types/app/sprinkle.d.ts +18 -0
  95. package/dist/types/app/terminals.d.ts +25 -0
  96. package/dist/types/app/tray.d.ts +25 -0
  97. package/dist/types/components/code-view.d.ts +47 -0
  98. package/dist/types/components/file-tree.d.ts +18 -0
  99. package/dist/types/dummy/agent.d.ts +21 -3
  100. package/dist/types/dummy/extras.d.ts +8 -0
  101. package/dist/types/dummy/fixtures.d.ts +2 -1
  102. package/dist/types/dummy/memory.d.ts +9 -0
  103. package/dist/types/dummy/monitor.d.ts +9 -0
  104. package/dist/types/dummy/script.d.ts +8 -2
  105. package/dist/types/dummy/showcase.d.ts +6 -0
  106. package/dist/types/dummy/sprinkles.d.ts +9 -0
  107. package/dist/types/dummy/tray.d.ts +10 -0
  108. package/dist/types/model/types.d.ts +280 -8
  109. package/dist/types/ui.d.ts +11 -3
  110. package/package.json +6 -1
  111. package/dist/chunk-T5QFSUYQ.js +0 -8
  112. /package/dist/{chunk-4RGMMAR4.js.map → chunk-4GQ6THC2.js.map} +0 -0
  113. /package/dist/{chunk-FN3Y2AYJ.js.map → chunk-5XWUZJTT.js.map} +0 -0
  114. /package/dist/{chunk-J45KH7MF.js.map → chunk-6JNYNXUQ.js.map} +0 -0
  115. /package/dist/{chunk-ZTPYBVZL.js.map → chunk-6N3GZNXH.js.map} +0 -0
  116. /package/dist/{chunk-H5WU2XYY.js.map → chunk-6T3O5K6R.js.map} +0 -0
  117. /package/dist/{chunk-MR7SU4GJ.js.map → chunk-7PY7B3ZW.js.map} +0 -0
  118. /package/dist/{chunk-Q4AC2O6Z.js.map → chunk-AXQFWXYZ.js.map} +0 -0
  119. /package/dist/{chunk-T5QFSUYQ.js.map → chunk-C7X653JJ.js.map} +0 -0
  120. /package/dist/{chunk-MTF6JFSL.js.map → chunk-J3QOBXZJ.js.map} +0 -0
  121. /package/dist/{chunk-SZ33GAVH.js.map → chunk-OFQ56DAN.js.map} +0 -0
  122. /package/dist/{chunk-24WRGUXG.js.map → chunk-T2LAT2D6.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Emitter
3
3
  } from "./chunk-T5NHW4KC.js";
4
- import "./chunk-Q4AC2O6Z.js";
4
+ import "./chunk-AXQFWXYZ.js";
5
5
 
6
6
  // src/dummy/script.ts
7
7
  var units = "/workspace/harbor/src/lib/units.ts";
@@ -56,6 +56,18 @@ function edit() {
56
56
  await files3.write(units, before + kelvin, agentId);
57
57
  }
58
58
  },
59
+ {
60
+ type: "part",
61
+ part: {
62
+ type: "diff",
63
+ path: units,
64
+ before: "export function toFahrenheit(celsius: number): number {\n return Math.round((celsius * 9) / 5 + 32);\n}\n",
65
+ after: `export function toFahrenheit(celsius: number): number {
66
+ return Math.round((celsius * 9) / 5 + 32);
67
+ }
68
+ ${kelvin}`
69
+ }
70
+ },
59
71
  {
60
72
  type: "text",
61
73
  text: "Added `toKelvin()` to `src/lib/units.ts`. It rounds to two decimals, like the API\u2019s other values. The change is in **Changes**, ready for review."
@@ -125,36 +137,110 @@ function chat(prompt) {
125
137
  }
126
138
  ];
127
139
  }
128
- function script(prompt) {
140
+ function ask() {
141
+ return [
142
+ { type: "thinking", text: "Two ways to do this. Ask before picking one." },
143
+ {
144
+ type: "part",
145
+ part: {
146
+ type: "question",
147
+ question: {
148
+ id: `q-${Math.random().toString(36).slice(2, 8)}`,
149
+ kind: "choice",
150
+ question: "Where should the cache live once the TTL fix ships?",
151
+ options: ["Keep it in memory", "Move it to KV", "Decide after the load test"],
152
+ state: "open"
153
+ }
154
+ }
155
+ }
156
+ ];
157
+ }
158
+ function look(attachments) {
159
+ const names = attachments.map((attachment) => attachment.name).join(", ");
160
+ const images = attachments.filter((attachment) => attachment.kind === "image").length;
161
+ return [
162
+ {
163
+ type: "tool",
164
+ name: "read_file",
165
+ title: `Read ${attachments.length === 1 ? "the attachment" : `${attachments.length} attachments`}`,
166
+ input: names,
167
+ output: attachments.map(
168
+ (attachment) => `${attachment.name} (${attachment.mimeType}, ${attachment.size} bytes)`
169
+ ).join("\n"),
170
+ paths: [],
171
+ ticks: 10
172
+ },
173
+ {
174
+ type: "text",
175
+ text: `Got **${names}**${images ? `, including ${images === 1 ? "an image" : `${images} images`}` : ""}. I\u2019ll keep ${attachments.length === 1 ? "it" : "them"} in mind for the next step.`
176
+ }
177
+ ];
178
+ }
179
+ var suggestions = [
180
+ [/\btests?\b/, "Open a pull request with the fix"],
181
+ [/\b(fix|edit|add|change)\b/, "Run the tests"],
182
+ [/\b(open|browse|page|docs)\b/, "Summarize the page"],
183
+ [/\b(files?|read|show|project)\b/, "Add a Kelvin helper to units.ts"]
184
+ ];
185
+ function script(prompt, attachments = []) {
129
186
  const text = prompt.toLowerCase();
130
- if (/\btests?\b/.test(text)) return test();
131
- if (/\b(fix|edit|add|change)\b/.test(text)) return edit();
132
- if (/\b(open|browse|page|docs)\b/.test(text)) return browse();
133
- if (/\b(files?|read|show|project)\b/.test(text)) return files();
134
- return chat(prompt);
187
+ const suggestion = suggestions.find(([pattern]) => pattern.test(text))?.[1] ?? "Run the tests";
188
+ if (attachments.length > 0) return { steps: look(attachments), suggestion };
189
+ if (/\b(ask|choose|decide)\b/.test(text)) return { steps: ask(), suggestion };
190
+ if (/\btests?\b/.test(text)) return { steps: test(), suggestion };
191
+ if (/\b(fix|edit|add|change)\b/.test(text)) return { steps: edit(), suggestion };
192
+ if (/\b(open|browse|page|docs)\b/.test(text)) return { steps: browse(), suggestion };
193
+ if (/\b(files?|read|show|project)\b/.test(text)) return { steps: files(), suggestion };
194
+ return { steps: chat(prompt), suggestion };
135
195
  }
136
196
 
137
197
  // src/dummy/agent.ts
138
198
  function chunks(text) {
139
199
  return text.match(/\S+\s*|\s+/g) ?? [];
140
200
  }
201
+ function outgoing(input) {
202
+ return typeof input === "string" ? { text: input } : input;
203
+ }
141
204
  var DummyAgent = class extends Emitter {
142
205
  #agents;
143
206
  #messages = /* @__PURE__ */ new Map();
207
+ #queues = /* @__PURE__ */ new Map();
208
+ #frozen = [];
209
+ #ice = /* @__PURE__ */ new Map();
210
+ #suggestions = /* @__PURE__ */ new Map();
144
211
  #runs = /* @__PURE__ */ new Map();
145
212
  #active;
146
213
  #clock;
147
214
  #effects;
148
215
  #next = 1;
149
- constructor(agents2, conversations2, effects, clock2) {
216
+ constructor(agents2, conversations2, effects, clock2, queues2 = {}, frozen2 = []) {
150
217
  super();
151
218
  this.#clock = clock2;
152
219
  this.#effects = effects;
153
220
  this.#agents = agents2.map((agent) => ({ ...agent }));
154
221
  for (const agent of this.#agents) {
155
222
  this.#messages.set(agent.id, structuredClone([...conversations2[agent.id] ?? []]));
223
+ this.#queues.set(agent.id, structuredClone([...queues2[agent.id] ?? []]));
156
224
  }
157
225
  this.#active = this.#agents[0]?.id ?? "";
226
+ for (const { cone, messages } of frozen2) {
227
+ this.#frozen.push({ ...cone });
228
+ this.#ice.set(cone.id, [
229
+ {
230
+ agent: {
231
+ id: cone.id,
232
+ name: cone.name,
233
+ kind: "cone",
234
+ parentId: null,
235
+ status: "idle",
236
+ model: cone.model,
237
+ contextFill: 0.2,
238
+ unread: 0
239
+ },
240
+ messages: structuredClone([...messages])
241
+ }
242
+ ]);
243
+ }
158
244
  }
159
245
  list() {
160
246
  return this.#agents.map((agent) => ({ ...agent }));
@@ -168,6 +254,9 @@ var DummyAgent = class extends Emitter {
168
254
  #changed() {
169
255
  this.emit("agents", this.list());
170
256
  }
257
+ #id(prefix) {
258
+ return `${prefix}-${this.#next++}`;
259
+ }
171
260
  select(id) {
172
261
  const agent = this.#agent(id);
173
262
  if (!agent) return;
@@ -179,6 +268,15 @@ var DummyAgent = class extends Emitter {
179
268
  messages(agentId) {
180
269
  return [...this.#messages.get(agentId) ?? []];
181
270
  }
271
+ queue(agentId) {
272
+ return [...this.#queues.get(agentId) ?? []];
273
+ }
274
+ busy(agentId) {
275
+ return this.#runs.has(agentId);
276
+ }
277
+ suggestion(agentId) {
278
+ return this.#suggestions.get(agentId) ?? null;
279
+ }
182
280
  #post(agentId, message) {
183
281
  this.#messages.get(agentId)?.push(message);
184
282
  this.emit("message", { agentId, message });
@@ -197,17 +295,10 @@ var DummyAgent = class extends Emitter {
197
295
  this.emit("message", { agentId, message: reply });
198
296
  }
199
297
  }
200
- async #step(agentId, reply, step, run) {
201
- if (step.type !== "tool") {
202
- this.#status(agentId, "thinking");
203
- const part = { type: step.type, text: "" };
204
- reply.parts.push(part);
205
- await this.#stream(agentId, reply, part, step.text, run);
206
- return;
207
- }
298
+ async #tool(agentId, reply, step, run) {
208
299
  this.#status(agentId, "working");
209
- const tool = {
210
- id: `t-${this.#next++}`,
300
+ const tool2 = {
301
+ id: this.#id("t"),
211
302
  name: step.name,
212
303
  title: step.title,
213
304
  input: step.input,
@@ -215,50 +306,263 @@ var DummyAgent = class extends Emitter {
215
306
  status: "running",
216
307
  paths: step.paths
217
308
  };
218
- reply.parts.push({ type: "tool", tool });
309
+ reply.parts.push({ type: "tool", tool: tool2 });
219
310
  this.emit("message", { agentId, message: reply });
220
311
  await this.#clock.sleep(step.ticks);
221
- const done = run.stopped ? { status: "error", output: "Stopped" } : { status: "done", output: step.output };
312
+ const done = run.stopped ? { status: "cancelled", output: "Stopped" } : { status: "done", output: step.output };
222
313
  if (!run.stopped) await step.effect?.({ ...this.#effects, agentId });
223
- Object.assign(tool, done);
314
+ Object.assign(tool2, done);
224
315
  this.emit("message", { agentId, message: reply });
225
316
  }
226
- async send(agentId, text) {
317
+ async #step(agentId, reply, step, run) {
318
+ if (step.type === "tool") return this.#tool(agentId, reply, step, run);
319
+ if (step.type === "part") {
320
+ await this.#clock.sleep(2);
321
+ if (run.stopped) return;
322
+ reply.parts.push(structuredClone(step.part));
323
+ this.emit("message", { agentId, message: reply });
324
+ return;
325
+ }
326
+ this.#status(agentId, "thinking");
327
+ const part = { type: step.type, text: "" };
328
+ reply.parts.push(part);
329
+ await this.#stream(agentId, reply, part, step.text, run);
330
+ }
331
+ #enqueue(agentId, input) {
332
+ const message = {
333
+ id: this.#id("q"),
334
+ role: "user",
335
+ text: input.text.trim(),
336
+ createdAt: Date.now(),
337
+ queued: true,
338
+ mode: "queue",
339
+ ...input.attachments?.length ? { attachments: input.attachments } : {}
340
+ };
341
+ this.#queues.set(agentId, [...this.queue(agentId), message]);
342
+ this.emit("messages", agentId);
343
+ }
344
+ unqueue(agentId, messageId) {
345
+ this.#queues.set(
346
+ agentId,
347
+ this.queue(agentId).filter((message) => message.id !== messageId)
348
+ );
349
+ this.emit("messages", agentId);
350
+ }
351
+ async send(agentId, input) {
227
352
  const agent = this.#agent(agentId);
228
- const prompt = text.trim();
229
- if (!agent || !prompt) return;
353
+ const message = outgoing(input);
354
+ const prompt = message.text.trim();
355
+ if (!agent || !prompt && !message.attachments?.length) return;
356
+ if (this.busy(agentId) && message.mode !== "steer") {
357
+ this.#enqueue(agentId, message);
358
+ return;
359
+ }
230
360
  this.stop(agentId);
361
+ this.#suggestions.delete(agentId);
231
362
  const run = { stopped: false };
232
363
  this.#runs.set(agentId, run);
233
364
  this.#post(agentId, {
234
- id: `m-${this.#next++}`,
365
+ id: this.#id("m"),
235
366
  role: "user",
236
367
  text: prompt,
237
- createdAt: Date.now()
368
+ createdAt: Date.now(),
369
+ ...message.mode === "steer" ? { mode: "steer" } : {},
370
+ ...message.attachments?.length ? { attachments: message.attachments } : {}
238
371
  });
239
372
  const reply = {
240
- id: `m-${this.#next++}`,
373
+ id: this.#id("m"),
241
374
  role: "assistant",
242
375
  parts: [],
243
376
  status: "streaming",
244
- createdAt: Date.now()
377
+ createdAt: Date.now(),
378
+ model: agent.model
245
379
  };
246
380
  this.#post(agentId, reply);
247
- for (const step of script(prompt)) {
381
+ const { steps, suggestion } = script(prompt, message.attachments ?? []);
382
+ for (const step of steps) {
248
383
  if (run.stopped) break;
249
384
  await this.#step(agentId, reply, step, run);
250
385
  }
251
386
  reply.status = run.stopped ? "stopped" : "done";
387
+ reply.usage = {
388
+ input: 1200 + prompt.length * 4,
389
+ output: 180 + reply.parts.length * 60,
390
+ cost: 4e-3
391
+ };
252
392
  if (this.#runs.get(agentId) === run) this.#runs.delete(agentId);
253
393
  agent.contextFill = Math.min(0.95, Math.round((agent.contextFill + 0.03) * 100) / 100);
254
394
  if (agentId !== this.#active) agent.unread += 1;
395
+ if (!run.stopped) this.#suggestions.set(agentId, suggestion);
255
396
  this.emit("message", { agentId, message: reply });
256
397
  agent.status = "idle";
257
398
  this.#changed();
399
+ await this.#drain(agentId);
400
+ }
401
+ async #drain(agentId) {
402
+ if (this.busy(agentId)) return;
403
+ const [next, ...rest] = this.queue(agentId);
404
+ if (!next) return;
405
+ this.#queues.set(agentId, rest);
406
+ this.emit("messages", agentId);
407
+ await this.send(agentId, { text: next.text, attachments: next.attachments });
258
408
  }
259
409
  stop(agentId) {
260
410
  const run = this.#runs.get(agentId);
261
411
  if (run) run.stopped = true;
412
+ this.#runs.delete(agentId);
413
+ }
414
+ #find(agentId, test2) {
415
+ return this.#messages.get(agentId)?.find(test2);
416
+ }
417
+ answer(agentId, questionId, answer) {
418
+ const message = this.#find(
419
+ agentId,
420
+ (candidate) => candidate.role === "assistant" && candidate.parts.some((part) => part.type === "question" && part.question.id === questionId)
421
+ );
422
+ if (!message) return;
423
+ for (const part of message.parts) {
424
+ if (part.type === "question" && part.question.id === questionId) {
425
+ part.question.state = "answered";
426
+ part.question.answer = answer;
427
+ }
428
+ }
429
+ this.emit("message", { agentId, message });
430
+ }
431
+ resolveLick(agentId, messageId, state) {
432
+ const message = this.#find(agentId, (candidate) => candidate.id === messageId);
433
+ if (message?.role !== "lick") return;
434
+ message.state = state;
435
+ this.emit("message", { agentId, message });
436
+ }
437
+ compact(agentId) {
438
+ const agent = this.#agent(agentId);
439
+ if (!agent) return;
440
+ const message = {
441
+ id: this.#id("m"),
442
+ role: "system",
443
+ kind: "compaction",
444
+ title: "Context compacted",
445
+ text: "Summarizing the conversation so far\u2026",
446
+ trigger: "manual",
447
+ state: "summarizing",
448
+ createdAt: Date.now()
449
+ };
450
+ this.#post(agentId, message);
451
+ void this.#clock.sleep(20).then(() => {
452
+ Object.assign(message, {
453
+ state: "summarized",
454
+ text: "The earlier conversation was summarized to free up context."
455
+ });
456
+ agent.contextFill = 0.06;
457
+ this.emit("message", { agentId, message });
458
+ this.#changed();
459
+ });
460
+ }
461
+ clear(agentId) {
462
+ if (!this.#messages.has(agentId)) return;
463
+ this.stop(agentId);
464
+ this.#messages.set(agentId, []);
465
+ this.#suggestions.delete(agentId);
466
+ this.emit("messages", agentId);
467
+ }
468
+ setModel(agentId, model) {
469
+ const agent = this.#agent(agentId);
470
+ if (!agent) return;
471
+ agent.model = model;
472
+ this.#changed();
473
+ }
474
+ createScoop(parentId, name) {
475
+ const parent = this.#agent(parentId);
476
+ const agent = {
477
+ id: this.#id("scoop"),
478
+ name,
479
+ kind: "scoop",
480
+ parentId: parent?.kind === "cone" ? parent.id : parent?.parentId ?? null,
481
+ status: "idle",
482
+ model: parent?.model ?? "claude-haiku-4-5",
483
+ contextFill: 0,
484
+ unread: 0
485
+ };
486
+ this.#agents.push(agent);
487
+ this.#messages.set(agent.id, []);
488
+ if (parent) {
489
+ this.#post(parentId, {
490
+ id: this.#id("m"),
491
+ role: "lick",
492
+ channel: "scoop-idle",
493
+ title: name,
494
+ text: "New scoop, waiting for its first task",
495
+ createdAt: Date.now()
496
+ });
497
+ }
498
+ this.#changed();
499
+ return { ...agent };
500
+ }
501
+ lick(agentId, channel, title, text, body) {
502
+ if (!this.#agent(agentId)) return;
503
+ this.#post(agentId, {
504
+ id: this.#id("m"),
505
+ role: "lick",
506
+ channel,
507
+ title,
508
+ text,
509
+ createdAt: Date.now(),
510
+ ...body ? { body } : {}
511
+ });
512
+ }
513
+ frozen() {
514
+ return this.#frozen.map((cone) => ({ ...cone }));
515
+ }
516
+ freeze(agentId) {
517
+ const agent = this.#agent(agentId);
518
+ if (agent?.kind !== "cone") return;
519
+ this.stop(agentId);
520
+ const family = this.#agents.filter(
521
+ (candidate) => candidate.id === agentId || candidate.parentId === agentId
522
+ );
523
+ const messages = this.#messages.get(agentId) ?? [];
524
+ const first = messages.find((message) => message.role === "user");
525
+ this.#frozen.unshift({
526
+ id: agentId,
527
+ name: agent.name,
528
+ title: first?.role === "user" ? first.text.slice(0, 80) : agent.name,
529
+ model: agent.model,
530
+ messages: messages.length,
531
+ frozenAt: Date.now()
532
+ });
533
+ this.#ice.set(
534
+ agentId,
535
+ family.map((member) => ({
536
+ agent: { ...member, status: "idle" },
537
+ messages: this.#messages.get(member.id)
538
+ }))
539
+ );
540
+ for (const member of family) this.stop(member.id);
541
+ this.#agents = this.#agents.filter((candidate) => !family.includes(candidate));
542
+ for (const member of family) this.#messages.delete(member.id);
543
+ if (this.#active === agentId) this.#active = this.#agents[0]?.id ?? "";
544
+ this.emit("frozen", this.frozen());
545
+ this.emit("active", this.#active);
546
+ this.#changed();
547
+ }
548
+ thaw(id) {
549
+ const ice = this.#ice.get(id);
550
+ if (!ice) return null;
551
+ this.#ice.delete(id);
552
+ this.#frozen = this.#frozen.filter((cone) => cone.id !== id);
553
+ for (const { agent, messages } of ice) {
554
+ this.#agents.push(agent);
555
+ this.#messages.set(agent.id, messages);
556
+ this.#queues.set(agent.id, []);
557
+ }
558
+ this.emit("frozen", this.frozen());
559
+ this.select(id);
560
+ return { ...ice[0].agent };
561
+ }
562
+ discard(id) {
563
+ this.#ice.delete(id);
564
+ this.#frozen = this.#frozen.filter((cone) => cone.id !== id);
565
+ this.emit("frozen", this.frozen());
262
566
  }
263
567
  };
264
568
 
@@ -281,6 +585,7 @@ function page(tab) {
281
585
  }
282
586
  function titleFor(url) {
283
587
  const { host, pathname } = new URL(url);
588
+ if (!host) return "New tab";
284
589
  return pathname === "/" ? host : `${host}${pathname}`;
285
590
  }
286
591
  var DummyBrowser = class extends Emitter {
@@ -365,6 +670,210 @@ function clock(delay) {
365
670
  };
366
671
  }
367
672
 
673
+ // src/dummy/extras.ts
674
+ var day = 864e5;
675
+ var now = Date.UTC(2026, 9, 5, 9, 0);
676
+ function frozenConversation(topic, reply) {
677
+ return [
678
+ { id: `f-${topic}-1`, role: "user", text: topic, createdAt: now - 3 * day },
679
+ {
680
+ id: `f-${topic}-2`,
681
+ role: "assistant",
682
+ status: "done",
683
+ createdAt: now - 3 * day + 6e4,
684
+ parts: [{ type: "text", text: reply }]
685
+ }
686
+ ];
687
+ }
688
+ var frozen = [
689
+ {
690
+ cone: {
691
+ id: "cone-onboarding",
692
+ name: "onboarding",
693
+ title: "Write the onboarding guide for new contributors",
694
+ model: "claude-sonnet-5-5",
695
+ messages: 42,
696
+ frozenAt: now - 2 * day
697
+ },
698
+ messages: frozenConversation(
699
+ "Write the onboarding guide for new contributors",
700
+ "The guide is in `/workspace/harbor/docs/onboarding.md`, with setup, tests and the release checklist."
701
+ )
702
+ },
703
+ {
704
+ cone: {
705
+ id: "cone-kv-spike",
706
+ name: "kv-spike",
707
+ title: "Spike: can the cache move to KV without a cold start?",
708
+ model: "claude-opus-5-5",
709
+ messages: 118,
710
+ frozenAt: now - 6 * day
711
+ },
712
+ messages: frozenConversation(
713
+ "Spike: can the cache move to KV without a cold start?",
714
+ "Yes, with a 5-minute in-memory layer in front. Reads from KV add 12 ms at the 95th percentile."
715
+ )
716
+ },
717
+ {
718
+ cone: {
719
+ id: "cone-q3-report",
720
+ name: "q3-report",
721
+ title: "Summarize Q3 uptime and incidents",
722
+ model: "claude-haiku-4-5",
723
+ messages: 17,
724
+ frozenAt: now - 21 * day
725
+ },
726
+ messages: frozenConversation(
727
+ "Summarize Q3 uptime and incidents",
728
+ "Uptime 99.96%. Two incidents: an expired certificate and an upstream outage."
729
+ )
730
+ }
731
+ ];
732
+ var memories = [
733
+ {
734
+ id: "mem-1",
735
+ scope: "global",
736
+ section: "Preferences",
737
+ title: "Lead with the result",
738
+ body: "Keep replies short. Lead with the result, then the evidence.",
739
+ tag: "feedback",
740
+ updatedAt: now - 5 * day
741
+ },
742
+ {
743
+ id: "mem-2",
744
+ scope: "global",
745
+ section: "Preferences",
746
+ title: "Tests before \u201Cfixed\u201D",
747
+ body: "Run the tests before saying something is fixed, and quote the summary line.",
748
+ tag: "feedback",
749
+ updatedAt: now - 9 * day
750
+ },
751
+ {
752
+ id: "mem-3",
753
+ scope: "global",
754
+ section: "About the user",
755
+ title: "Works in UTC",
756
+ body: "Schedules, timestamps and cron expressions are in UTC unless stated otherwise.",
757
+ tag: "user",
758
+ updatedAt: now - 30 * day
759
+ },
760
+ {
761
+ id: "mem-4",
762
+ scope: "global",
763
+ section: "Projects",
764
+ title: "harbor",
765
+ body: "Forecast API, deployed as a worker. Cache bugs show up around midnight UTC. Staging deploys are frozen on Fridays.",
766
+ tag: "project",
767
+ updatedAt: now - day
768
+ },
769
+ {
770
+ id: "mem-5",
771
+ scope: "global",
772
+ section: "Projects",
773
+ title: "release-notes",
774
+ body: "Drafted every Friday from merged pull requests, grouped as features, fixes and chores.",
775
+ tag: "project",
776
+ updatedAt: now - 4 * day
777
+ },
778
+ {
779
+ id: "mem-6",
780
+ scope: "cone-harbor",
781
+ section: "Conventions",
782
+ title: "Cache keys",
783
+ body: "Cache keys are `city:UTC-date`. Never key on local time.",
784
+ tag: "project",
785
+ updatedAt: now - 2 * day
786
+ },
787
+ {
788
+ id: "mem-7",
789
+ scope: "cone-harbor",
790
+ section: "Conventions",
791
+ title: "Retries",
792
+ body: "Retry upstream calls three times with exponential backoff starting at 250 ms.",
793
+ tag: null,
794
+ updatedAt: now - 2 * day
795
+ },
796
+ {
797
+ id: "mem-8",
798
+ scope: "cone-release",
799
+ section: "Style",
800
+ title: "Past tense, no ticket numbers",
801
+ body: "One sentence per change, in the past tense, without ticket numbers.",
802
+ tag: "feedback",
803
+ updatedAt: now - 7 * day
804
+ }
805
+ ];
806
+ var sprinkleStyle = `
807
+ body { margin: 0; padding: 16px; font: 13px/1.45 var(--font, system-ui, sans-serif); color: var(--fg); background: var(--bg); }
808
+ h1 { font-size: 15px; margin: 0 0 12px; }
809
+ ul { list-style: none; padding: 0; margin: 0 0 12px; display: grid; gap: 6px; }
810
+ li { display: flex; gap: 8px; align-items: center; padding: 6px 8px; border: 1px solid var(--line); border-radius: 6px; }
811
+ li .state { margin-left: auto; color: var(--muted); }
812
+ li.done .state { color: var(--positive); }
813
+ button { font: inherit; padding: 4px 12px; border-radius: 14px; border: 1px solid var(--line); background: var(--bg); color: var(--fg); cursor: pointer; }
814
+ button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
815
+ .actions { display: flex; gap: 8px; }
816
+ p.note { color: var(--muted); }
817
+ `;
818
+ var sprinkleScript = `
819
+ document.addEventListener('click', (event) => {
820
+ const button = event.target.closest('[data-event]');
821
+ if (!button) return;
822
+ parent.postMessage({ type: 'sprinkle-event', event: button.dataset.event, detail: button.dataset.detail ?? null }, '*');
823
+ const note = document.querySelector('.note');
824
+ if (note) note.textContent = 'Sent \u201C' + button.textContent.trim() + '\u201D to the agent.';
825
+ });
826
+ `;
827
+ function page2(body) {
828
+ return `<!doctype html><html><head><meta charset="utf-8"><style>${sprinkleStyle}</style></head><body>${body}<script>${sprinkleScript}<\/script></body></html>`;
829
+ }
830
+ var sprinkles = [
831
+ {
832
+ id: "release-board",
833
+ name: "release-board",
834
+ title: "Release board",
835
+ icon: "sp-icon-checkmark-circle",
836
+ agentId: "cone-release",
837
+ html: page2(`<h1>harbor 1.9.0</h1>
838
+ <ul>
839
+ <li class="done">Retry pull request merged <span class="state">done</span></li>
840
+ <li class="done">Release notes drafted <span class="state">done</span></li>
841
+ <li>Docs updated to Celsius <span class="state">in review</span></li>
842
+ <li>Staging smoke test <span class="state">waiting</span></li>
843
+ </ul>
844
+ <div class="actions"><button class="primary" data-event="ship" data-detail="1.9.0">Ship 1.9.0</button><button data-event="hold">Hold</button></div>
845
+ <p class="note"></p>`)
846
+ },
847
+ {
848
+ id: "loose-ends",
849
+ name: "loose-ends",
850
+ title: "Loose ends",
851
+ icon: "sp-icon-filter",
852
+ agentId: "cone-sliccy",
853
+ html: page2(`<h1>Loose ends</h1>
854
+ <ul>
855
+ <li>Reply to the vendor about Thursday <button data-event="done" data-detail="vendor">Done</button></li>
856
+ <li>Renew the staging certificate <button data-event="done" data-detail="certificate">Done</button></li>
857
+ <li>Move the cache to KV <button data-event="done" data-detail="kv">Done</button></li>
858
+ </ul>
859
+ <p class="note"></p>`)
860
+ }
861
+ ];
862
+ var tray = {
863
+ name: "sliccstart",
864
+ kind: "sliccstart",
865
+ connection: "live",
866
+ role: "leader",
867
+ followers: [
868
+ { id: "f-phone", name: "Phone", device: "phone", since: now - 36e5 },
869
+ { id: "f-laptop", name: "Second browser", device: "browser", since: now - 6e5 }
870
+ ],
871
+ spent: 23.67,
872
+ rate: 0.59,
873
+ budget: { percent: 32, window: "weekly", resets: "resets in 4 days" },
874
+ joinUrl: "https://www.example.com/join/7Q4D-K9TX"
875
+ };
876
+
368
877
  // src/dummy/files.ts
369
878
  function parents(path) {
370
879
  const parts = path.split("/").slice(1, -1);
@@ -390,10 +899,10 @@ var DummyFiles = class extends Emitter {
390
899
  #mkdirs(path) {
391
900
  for (const directory of parents(path)) this.#directories.add(directory);
392
901
  }
393
- #put(path, text, now) {
902
+ #put(path, text, now2) {
394
903
  this.#mkdirs(path);
395
904
  this.#contents.set(path, text);
396
- this.#modified.set(path, now);
905
+ this.#modified.set(path, now2);
397
906
  }
398
907
  #entries() {
399
908
  const directories2 = [...this.#directories].map(
@@ -465,6 +974,672 @@ var DummyFiles = class extends Emitter {
465
974
  }
466
975
  };
467
976
 
977
+ // src/dummy/showcase.ts
978
+ var start = Date.UTC(2026, 9, 4, 16, 0);
979
+ var at = (minutes) => start + minutes * 6e4;
980
+ function picture(label, from, to, width = 640, height = 400) {
981
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${from}"/><stop offset="1" stop-color="${to}"/></linearGradient></defs><rect width="${width}" height="${height}" fill="url(#g)"/><text x="50%" y="50%" font-family="sans-serif" font-size="${Math.round(height / 9)}" fill="#ffffff" text-anchor="middle" dominant-baseline="middle">${label}</text></svg>`;
982
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
983
+ }
984
+ function tone(seconds = 0.6, frequency = 440, rate = 8e3) {
985
+ const samples = Math.round(seconds * rate);
986
+ const bytes = new Uint8Array(44 + samples);
987
+ const view = new DataView(bytes.buffer);
988
+ const text = (offset, value) => {
989
+ for (let i = 0; i < value.length; i++) bytes[offset + i] = value.charCodeAt(i);
990
+ };
991
+ text(0, "RIFF");
992
+ view.setUint32(4, 36 + samples, true);
993
+ text(8, "WAVEfmt ");
994
+ view.setUint32(16, 16, true);
995
+ view.setUint16(20, 1, true);
996
+ view.setUint16(22, 1, true);
997
+ view.setUint32(24, rate, true);
998
+ view.setUint32(28, rate, true);
999
+ view.setUint16(32, 1, true);
1000
+ view.setUint16(34, 8, true);
1001
+ text(36, "data");
1002
+ view.setUint32(40, samples, true);
1003
+ for (let i = 0; i < samples; i++) {
1004
+ const fade = Math.min(1, (samples - i) / (rate / 10));
1005
+ bytes[44 + i] = 128 + Math.round(60 * fade * Math.sin(2 * Math.PI * frequency * i / rate));
1006
+ }
1007
+ let binary = "";
1008
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1009
+ return `data:audio/wav;base64,${btoa(binary)}`;
1010
+ }
1011
+ function tool(id, name, title, input, output, extra = {}) {
1012
+ return { id, name, title, input, output, status: "done", paths: [], ...extra };
1013
+ }
1014
+ var markdown = [
1015
+ "## The forecast API in one page",
1016
+ "",
1017
+ "harbor answers `GET /forecast?city=\u2026` with the day\u2019s **high**, **low** and a *short summary*, and caches per city for an hour. ~~XML output~~ is gone.",
1018
+ "",
1019
+ "### How a request flows",
1020
+ "",
1021
+ "1. The worker parses the city.",
1022
+ "2. The cache answers if it has a fresh entry:",
1023
+ " - same UTC day",
1024
+ " - younger than one hour",
1025
+ "3. Otherwise the upstream call runs, with three retries.",
1026
+ "",
1027
+ "> Upstream is slow at the top of the hour. Retries back off at 250, 500 and 1000 ms.",
1028
+ "",
1029
+ "```ts",
1030
+ "export async function forecast(city: string): Promise<Forecast> {",
1031
+ " const cached = cache.get(city);",
1032
+ " if (cached) return cached;",
1033
+ " return cache.set(city, await retry(() => upstream(city)));",
1034
+ "}",
1035
+ "```",
1036
+ "",
1037
+ "| Route | Cache | Typical latency |",
1038
+ "| --- | --- | ---: |",
1039
+ "| `/forecast` | 1 hour, per city | 38 ms |",
1040
+ "| `/health` | none | 2 ms |",
1041
+ "| `/forecast` (miss) | \u2014 | 410 ms |",
1042
+ "",
1043
+ "- [x] TTL fix merged",
1044
+ "- [ ] Move the cache to KV",
1045
+ "",
1046
+ "Details are in the [API reference](https://api.example.com/docs/v2/forecast). A long path that must wrap: `/workspace/harbor/node_modules/.cache/wrangler/state/v3/cache/miniflare-CacheObject/7c1f0e3b9a5d4e2f8b6a1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f.sqlite`."
1047
+ ].join("\n");
1048
+ var toolShapes = [
1049
+ tool("k-t1", "bash", "Count the routes", "rg -c 'pathname ===' src", "src/index.ts:2"),
1050
+ tool(
1051
+ "k-t2",
1052
+ "read_file",
1053
+ "Read the router",
1054
+ "/workspace/harbor/src/index.ts",
1055
+ "import { forecast } from './routes/forecast.ts';\n\u2026",
1056
+ {
1057
+ paths: ["/workspace/harbor/src/index.ts"]
1058
+ }
1059
+ ),
1060
+ tool(
1061
+ "k-t3",
1062
+ "write_file",
1063
+ "Create the KV binding notes",
1064
+ "/workspace/harbor/docs/kv.md",
1065
+ "Wrote 18 lines",
1066
+ {
1067
+ paths: ["/workspace/harbor/docs/kv.md"]
1068
+ }
1069
+ ),
1070
+ tool(
1071
+ "k-t4",
1072
+ "edit_file",
1073
+ "Rename the cache key",
1074
+ "/workspace/harbor/src/lib/cache.ts",
1075
+ 'No match for "cityKey(" in src/lib/cache.ts',
1076
+ {
1077
+ status: "error",
1078
+ paths: ["/workspace/harbor/src/lib/cache.ts"]
1079
+ }
1080
+ ),
1081
+ tool(
1082
+ "k-t5",
1083
+ "grep",
1084
+ "Find TODOs",
1085
+ 'TODO --glob "src/**"',
1086
+ "src/routes/forecast.ts:14: TODO: cache negative results\nsrc/lib/units.ts:3: TODO: round half to even"
1087
+ ),
1088
+ tool(
1089
+ "k-t6",
1090
+ "web_fetch",
1091
+ "Fetch the API changelog",
1092
+ "https://api.example.com/changelog.json",
1093
+ '{ "latest": "2.4.1", "breaking": false }'
1094
+ ),
1095
+ tool(
1096
+ "k-t7",
1097
+ "browser",
1098
+ "Screenshot the preview",
1099
+ "screenshot http://localhost:8787/",
1100
+ "Captured 1280\xD7800",
1101
+ {
1102
+ image: picture("localhost:8787", "#3b63fb", "#0a7a43", 640, 400)
1103
+ }
1104
+ ),
1105
+ tool(
1106
+ "k-t8",
1107
+ "scoop_scoop",
1108
+ "Start a scoop for the docs",
1109
+ "name: docs-pass\ntask: Update the forecast page to Celsius",
1110
+ "Scoop docs-pass is running"
1111
+ ),
1112
+ tool(
1113
+ "k-t9",
1114
+ "feed_scoop",
1115
+ "Hand the screenshots to docs-pass",
1116
+ "scoop_name: docs-pass\nmessage: Use the new screenshots in /tmp/shots",
1117
+ "Delivered"
1118
+ ),
1119
+ tool(
1120
+ "k-t10",
1121
+ "send_message",
1122
+ "Tell quiet-otter the tests pass",
1123
+ "to: quiet-otter\nmessage: CI is green, merge when ready",
1124
+ "Delivered"
1125
+ ),
1126
+ tool(
1127
+ "k-t11",
1128
+ "drop_scoop",
1129
+ "Wrap up amber-heron",
1130
+ "scoop_name: amber-heron",
1131
+ "Dropped amber-heron; its notes are in /shared/notes"
1132
+ ),
1133
+ tool(
1134
+ "k-t12",
1135
+ "update_global_memory",
1136
+ "Remember the cache rule",
1137
+ "Cache keys are per city and per UTC day.",
1138
+ "Memory updated (3 sections, 412 characters)"
1139
+ ),
1140
+ tool("k-t13", "bash", "Deploy to staging", "wrangler deploy --env staging", "Stopped", {
1141
+ status: "cancelled"
1142
+ }),
1143
+ tool("k-t14", "bash", "Watch the logs", "wrangler tail --env staging", "", { status: "running" })
1144
+ ];
1145
+ var licks = [
1146
+ [
1147
+ "webhook",
1148
+ "GitHub \xB7 pull_request.opened",
1149
+ "example/harbor#58 Add retries for upstream timeouts",
1150
+ '{\n "action": "opened",\n "number": 58,\n "repository": { "full_name": "example/harbor" }\n}',
1151
+ {}
1152
+ ],
1153
+ [
1154
+ "cron",
1155
+ "Every weekday 09:00",
1156
+ "Morning summary for the harbor cone",
1157
+ void 0,
1158
+ { count: 3 }
1159
+ ],
1160
+ [
1161
+ "sprinkle",
1162
+ "Release board",
1163
+ "Button \u201CShip 1.9.0\u201D clicked",
1164
+ '{ "button": "ship", "version": "1.9.0" }',
1165
+ {}
1166
+ ],
1167
+ [
1168
+ "fswatch",
1169
+ "/workspace/harbor/src",
1170
+ "2 files changed",
1171
+ "modified /workspace/harbor/src/lib/cache.ts\ncreated /workspace/harbor/src/lib/retry.ts",
1172
+ {}
1173
+ ],
1174
+ [
1175
+ "session-reload",
1176
+ "Session restored",
1177
+ "The page reloaded; 4 cones resumed from storage",
1178
+ void 0,
1179
+ {}
1180
+ ],
1181
+ [
1182
+ "navigate",
1183
+ "api.example.com",
1184
+ "A page offered a SLICC handoff: open the forecast docs",
1185
+ '{ "verb": "open", "target": "https://api.example.com/docs/v2/forecast" }',
1186
+ {}
1187
+ ],
1188
+ [
1189
+ "discovery",
1190
+ "api.example.com",
1191
+ "Found an ai-catalog.json with 3 tools",
1192
+ "https://api.example.com/.well-known/ai-catalog.json",
1193
+ {}
1194
+ ],
1195
+ ["upgrade", "SLICC updated", "6.4.0 \u2192 6.5.0", void 0, {}],
1196
+ [
1197
+ "workflow",
1198
+ "release-notes",
1199
+ "Step 2 of 4 finished: collect merged pull requests",
1200
+ void 0,
1201
+ {}
1202
+ ],
1203
+ ["bash", "Job 3", "npm run build exited with status 0 after 41 s", void 0, {}],
1204
+ ["jshd", "collect.jsh", "Script finished: 12 pull requests since v1.8.0", void 0, {}],
1205
+ ["preview", "Preview bridge", "Connected from localhost:8787", void 0, {}],
1206
+ [
1207
+ "cherry",
1208
+ "Host page",
1209
+ "Event \u201Ccart-updated\u201D from shop.example.com",
1210
+ '{ "items": 3, "total": 42.5 }',
1211
+ {}
1212
+ ],
1213
+ ["scoop-notify", "docs-pass", "Finished: the forecast page now shows Celsius", void 0, {}],
1214
+ ["scoop-idle", "amber-heron", "Idle for 30 minutes", void 0, {}],
1215
+ ["scoop-wait", "quiet-otter", "The wait for CI resolved: all checks passed", void 0, {}],
1216
+ [
1217
+ "sudo-request",
1218
+ "docs-pass",
1219
+ "Wants to push to example/harbor (branch docs/celsius)",
1220
+ "git push origin docs/celsius",
1221
+ { state: "pending" }
1222
+ ],
1223
+ [
1224
+ "sudo-request",
1225
+ "quiet-otter",
1226
+ "Wants to read secrets: UPSTREAM_TOKEN",
1227
+ void 0,
1228
+ { state: "confirmed" }
1229
+ ],
1230
+ [
1231
+ "sudo-request",
1232
+ "amber-heron",
1233
+ "Wants to run: rm -rf /workspace/harbor/node_modules",
1234
+ void 0,
1235
+ { state: "dismissed" }
1236
+ ]
1237
+ ];
1238
+ var attachmentsMessage = {
1239
+ id: "k-u2",
1240
+ role: "user",
1241
+ text: "Here is the mockup, my notes and the old spec.",
1242
+ createdAt: at(4),
1243
+ attachments: [
1244
+ {
1245
+ id: "k-a1",
1246
+ name: "forecast-card.png",
1247
+ kind: "image",
1248
+ mimeType: "image/png",
1249
+ size: 48211,
1250
+ url: picture("Mockup", "#f5a623", "#d0021b", 480, 320)
1251
+ },
1252
+ {
1253
+ id: "k-a2",
1254
+ name: "notes.txt",
1255
+ kind: "text",
1256
+ mimeType: "text/plain",
1257
+ size: 214,
1258
+ text: "Show \xB0C first.\nRound to whole degrees.\nKeep the summary under 60 characters."
1259
+ },
1260
+ {
1261
+ id: "k-a3",
1262
+ name: "spec-v1.pdf",
1263
+ kind: "file",
1264
+ mimeType: "application/pdf",
1265
+ size: 1204331,
1266
+ path: "/home/user/downloads/spec-v1.pdf"
1267
+ },
1268
+ {
1269
+ id: "k-a4",
1270
+ name: "UPSTREAM_TOKEN",
1271
+ kind: "secret",
1272
+ mimeType: "application/octet-stream",
1273
+ size: 0
1274
+ },
1275
+ {
1276
+ id: "k-a5",
1277
+ name: "recording.mov",
1278
+ kind: "file",
1279
+ mimeType: "video/quicktime",
1280
+ size: 98304e3,
1281
+ error: "Too large: attachments are limited to 25 MB"
1282
+ }
1283
+ ]
1284
+ };
1285
+ var showcaseAgent = {
1286
+ id: "cone-kitchen",
1287
+ name: "kitchen-sink",
1288
+ kind: "cone",
1289
+ parentId: null,
1290
+ status: "idle",
1291
+ model: "claude-opus-5-5",
1292
+ contextFill: 0.47,
1293
+ unread: 0
1294
+ };
1295
+ function opening() {
1296
+ return [
1297
+ {
1298
+ id: "k-u1",
1299
+ role: "user",
1300
+ text: "Give me the full tour of harbor, everything you can show.",
1301
+ createdAt: at(0)
1302
+ },
1303
+ {
1304
+ id: "k-m1",
1305
+ role: "assistant",
1306
+ status: "done",
1307
+ createdAt: at(1),
1308
+ model: "claude-opus-5-5",
1309
+ usage: { input: 18204, output: 912, cost: 0.31 },
1310
+ parts: [
1311
+ {
1312
+ type: "thinking",
1313
+ text: "Start with the shape of the API, then show the pieces one kind at a time."
1314
+ },
1315
+ { type: "text", text: markdown }
1316
+ ]
1317
+ },
1318
+ attachmentsMessage,
1319
+ {
1320
+ id: "k-m2",
1321
+ role: "assistant",
1322
+ status: "done",
1323
+ createdAt: at(5),
1324
+ parts: [
1325
+ {
1326
+ type: "text",
1327
+ text: "Mockup, notes and spec received. The recording was too large to attach. Here are the current screens and the alert sound:"
1328
+ },
1329
+ {
1330
+ type: "media",
1331
+ items: [
1332
+ {
1333
+ kind: "image",
1334
+ src: picture("Forecast card", "#3b63fb", "#7a5af8"),
1335
+ alt: "The forecast card in light mode"
1336
+ },
1337
+ {
1338
+ kind: "image",
1339
+ src: picture("Dark card", "#1d1d1d", "#3b63fb"),
1340
+ alt: "The forecast card in dark mode"
1341
+ },
1342
+ {
1343
+ kind: "image",
1344
+ src: picture("Phone", "#0a7a43", "#12b886", 360, 640),
1345
+ alt: "The forecast card on a phone"
1346
+ },
1347
+ {
1348
+ kind: "video",
1349
+ src: "",
1350
+ poster: picture("\u25B6 Walkthrough", "#292929", "#717171"),
1351
+ alt: "A walkthrough of the forecast page"
1352
+ },
1353
+ { kind: "audio", src: tone(), alt: "The storm alert sound" }
1354
+ ]
1355
+ }
1356
+ ]
1357
+ },
1358
+ { id: "k-u3", role: "user", text: "Show me every tool you used.", createdAt: at(8) },
1359
+ {
1360
+ id: "k-m3",
1361
+ role: "assistant",
1362
+ status: "done",
1363
+ createdAt: at(9),
1364
+ parts: [
1365
+ { type: "text", text: "Every tool from today\u2019s run, in order:" },
1366
+ ...toolShapes.map((call) => ({ type: "tool", tool: call }))
1367
+ ]
1368
+ },
1369
+ {
1370
+ id: "k-tool1",
1371
+ role: "tool",
1372
+ createdAt: at(12),
1373
+ tool: tool(
1374
+ "k-t15",
1375
+ "bash",
1376
+ "You ran a command",
1377
+ "git log --oneline -3",
1378
+ "a1b2c3d fix: expire entries from the previous day\ne4f5a6b feat: retry upstream timeouts\n0c9d8e7 chore: update wrangler"
1379
+ )
1380
+ }
1381
+ ];
1382
+ }
1383
+ function work() {
1384
+ return [
1385
+ {
1386
+ id: "k-m4",
1387
+ role: "assistant",
1388
+ status: "done",
1389
+ createdAt: at(14),
1390
+ parts: [
1391
+ { type: "text", text: "Here is where things stand." },
1392
+ {
1393
+ type: "card",
1394
+ card: {
1395
+ variant: "pr",
1396
+ number: 58,
1397
+ title: "Add retries for upstream timeouts",
1398
+ branch: "fix/retry-upstream",
1399
+ checks: "4 of 4 checks passed",
1400
+ additions: 42,
1401
+ deletions: 7,
1402
+ files: 3,
1403
+ tone: "positive"
1404
+ }
1405
+ },
1406
+ {
1407
+ type: "card",
1408
+ card: {
1409
+ variant: "tool",
1410
+ title: "Visual regression suite",
1411
+ badge: "bash",
1412
+ status: "2 snapshots changed",
1413
+ files: ["forecast-card.png", "forecast-card-dark.png"],
1414
+ tone: "notice"
1415
+ }
1416
+ },
1417
+ {
1418
+ type: "card",
1419
+ card: {
1420
+ variant: "light",
1421
+ title: "Staging is healthy",
1422
+ detail: "/health answered in 2 ms",
1423
+ tone: "informative"
1424
+ }
1425
+ },
1426
+ {
1427
+ type: "plan",
1428
+ items: [
1429
+ "Merge the retry pull request",
1430
+ "Move the cache to KV",
1431
+ "Update the forecast docs to Celsius",
1432
+ "Tag 1.9.0"
1433
+ ]
1434
+ },
1435
+ {
1436
+ type: "check",
1437
+ items: [
1438
+ { text: "TTL fix merged" },
1439
+ { text: "Retries reviewed", tone: "informative" },
1440
+ { text: "Docs pass running", tone: "notice" },
1441
+ { text: "KV migration not started", tone: "negative" }
1442
+ ]
1443
+ },
1444
+ {
1445
+ type: "diff",
1446
+ path: "/workspace/harbor/src/lib/units.ts",
1447
+ before: "export function toCelsius(f: number): number {\n return ((f - 32) * 5) / 9;\n}\n",
1448
+ after: "export function toCelsius(f: number): number {\n return Math.round(((f - 32) * 5) / 9);\n}\n"
1449
+ },
1450
+ {
1451
+ type: "link",
1452
+ url: "https://api.example.com/docs/v2/forecast",
1453
+ title: "Forecast API reference",
1454
+ description: "Parameters, units and rate limits for /v2/forecast."
1455
+ },
1456
+ {
1457
+ type: "delegation",
1458
+ kind: "scoop",
1459
+ scoop: "docs-pass",
1460
+ text: "Started a scoop to update the docs"
1461
+ },
1462
+ {
1463
+ type: "delegation",
1464
+ kind: "feed",
1465
+ scoop: "docs-pass",
1466
+ text: "Handed over the new screenshots"
1467
+ },
1468
+ {
1469
+ type: "delegation",
1470
+ kind: "sprinkle",
1471
+ scoop: "release-board",
1472
+ text: "Opened the release board sprinkle"
1473
+ },
1474
+ { type: "delegation", kind: "drop", scoop: "amber-heron", text: "Wrapped up amber-heron" }
1475
+ ]
1476
+ },
1477
+ {
1478
+ id: "k-m5",
1479
+ role: "assistant",
1480
+ status: "done",
1481
+ createdAt: at(18),
1482
+ parts: [
1483
+ { type: "text", text: "Three decisions from earlier:" },
1484
+ {
1485
+ type: "question",
1486
+ question: {
1487
+ id: "k-q1",
1488
+ kind: "yes-no",
1489
+ question: "Tag 1.9.0 after the merge?",
1490
+ options: [],
1491
+ state: "answered",
1492
+ answer: "Yes"
1493
+ }
1494
+ },
1495
+ {
1496
+ type: "question",
1497
+ question: {
1498
+ id: "k-q2",
1499
+ kind: "text",
1500
+ question: "What should the release be called?",
1501
+ options: [],
1502
+ state: "inert"
1503
+ }
1504
+ },
1505
+ {
1506
+ type: "question",
1507
+ question: {
1508
+ id: "k-q3",
1509
+ kind: "choice",
1510
+ question: "Where should the cache live?",
1511
+ options: ["Keep it in memory", "Move it to KV", "Decide after the load test"],
1512
+ state: "open"
1513
+ }
1514
+ }
1515
+ ]
1516
+ }
1517
+ ];
1518
+ }
1519
+ function events() {
1520
+ return [
1521
+ {
1522
+ id: "k-s1",
1523
+ role: "system",
1524
+ kind: "compaction",
1525
+ title: "Context compacted",
1526
+ text: "58 earlier messages were summarized.",
1527
+ trigger: "threshold",
1528
+ state: "summarized",
1529
+ createdAt: at(1440)
1530
+ },
1531
+ {
1532
+ id: "k-s2",
1533
+ role: "system",
1534
+ kind: "compaction",
1535
+ title: "Compaction fell back",
1536
+ text: "The summary failed, so the oldest 20 messages were dropped instead.",
1537
+ trigger: "overflow",
1538
+ state: "fallback",
1539
+ createdAt: at(1441)
1540
+ },
1541
+ ...licks.map(
1542
+ ([channel, title, text, body, extra], index2) => ({
1543
+ id: `k-l${index2 + 1}`,
1544
+ role: "lick",
1545
+ channel,
1546
+ title,
1547
+ text,
1548
+ createdAt: at(1445 + index2),
1549
+ ...body ? { body } : {},
1550
+ ...extra
1551
+ })
1552
+ ),
1553
+ {
1554
+ id: "k-u4",
1555
+ role: "user",
1556
+ text: "Actually, stop the deploy, staging is frozen today.",
1557
+ mode: "steer",
1558
+ createdAt: at(1470)
1559
+ },
1560
+ {
1561
+ id: "k-m6",
1562
+ role: "assistant",
1563
+ status: "stopped",
1564
+ createdAt: at(1470),
1565
+ parts: [{ type: "text", text: "Stopping the staging deploy and leaving" }]
1566
+ },
1567
+ {
1568
+ id: "k-u5",
1569
+ role: "user",
1570
+ text: "Update the forecast page to Celsius and use the new screenshots.",
1571
+ from: "harbor",
1572
+ createdAt: at(1472)
1573
+ },
1574
+ {
1575
+ id: "k-m7",
1576
+ role: "assistant",
1577
+ status: "error",
1578
+ createdAt: at(1475),
1579
+ parts: [
1580
+ {
1581
+ type: "thinking",
1582
+ text: "The model rejected the request because the context is too large."
1583
+ },
1584
+ {
1585
+ type: "error",
1586
+ message: "The model returned 413: the request is larger than its context window.",
1587
+ action: "change-model"
1588
+ }
1589
+ ]
1590
+ },
1591
+ {
1592
+ id: "k-s3",
1593
+ role: "system",
1594
+ kind: "error",
1595
+ title: "docs-pass failed",
1596
+ text: "The Adobe session expired. Log in again to continue.",
1597
+ action: "login",
1598
+ createdAt: at(1476)
1599
+ },
1600
+ {
1601
+ id: "k-s4",
1602
+ role: "system",
1603
+ kind: "error",
1604
+ title: "Rate limited",
1605
+ text: "Anthropic answered 429. Retrying in 30 seconds.",
1606
+ action: "retry",
1607
+ createdAt: at(1477)
1608
+ },
1609
+ {
1610
+ id: "k-s5",
1611
+ role: "system",
1612
+ kind: "notice",
1613
+ title: "Model changed",
1614
+ text: "This cone now uses Claude Opus 5.5.",
1615
+ createdAt: at(1478)
1616
+ }
1617
+ ];
1618
+ }
1619
+ function showcase() {
1620
+ return [...opening(), ...work(), ...events()];
1621
+ }
1622
+ function showcaseQueue() {
1623
+ return [
1624
+ {
1625
+ id: "k-q-1",
1626
+ role: "user",
1627
+ text: "Then open a pull request for the docs.",
1628
+ createdAt: at(1480),
1629
+ queued: true,
1630
+ mode: "queue"
1631
+ },
1632
+ {
1633
+ id: "k-q-2",
1634
+ role: "user",
1635
+ text: "And post the link in the release channel.",
1636
+ createdAt: at(1481),
1637
+ queued: true,
1638
+ mode: "queue"
1639
+ }
1640
+ ];
1641
+ }
1642
+
468
1643
  // src/dummy/fixtures.ts
469
1644
  var cacheBefore = `export interface Entry<T> {
470
1645
  value: T;
@@ -743,6 +1918,7 @@ var agents = [
743
1918
  contextFill: 0.21,
744
1919
  unread: 0
745
1920
  },
1921
+ showcaseAgent,
746
1922
  {
747
1923
  id: "cone-harbor",
748
1924
  name: "harbor",
@@ -804,21 +1980,23 @@ var agents = [
804
1980
  unread: 0
805
1981
  }
806
1982
  ];
807
- var start = Date.UTC(2026, 9, 5, 8, 30);
808
- var at = (minutes) => start + minutes * 6e4;
1983
+ var start2 = Date.UTC(2026, 9, 5, 8, 30);
1984
+ var at2 = (minutes) => start2 + minutes * 6e4;
1985
+ var queues = { "cone-kitchen": showcaseQueue() };
809
1986
  var conversations = {
1987
+ "cone-kitchen": showcase(),
810
1988
  "cone-sliccy": [
811
1989
  {
812
1990
  id: "m-s1",
813
1991
  role: "user",
814
1992
  text: "What is on my plate today?",
815
- createdAt: at(0)
1993
+ createdAt: at2(0)
816
1994
  },
817
1995
  {
818
1996
  id: "m-s2",
819
1997
  role: "assistant",
820
1998
  status: "done",
821
- createdAt: at(1),
1999
+ createdAt: at2(1),
822
2000
  parts: [
823
2001
  {
824
2002
  type: "tool",
@@ -842,23 +2020,23 @@ var conversations = {
842
2020
  "cone-harbor": [
843
2021
  {
844
2022
  id: "m-h1",
845
- role: "event",
846
- kind: "webhook",
2023
+ role: "lick",
2024
+ channel: "webhook",
847
2025
  title: "GitHub \xB7 issue opened",
848
2026
  text: "example/harbor#57: Forecast stays on yesterday after midnight UTC",
849
- createdAt: at(2)
2027
+ createdAt: at2(2)
850
2028
  },
851
2029
  {
852
2030
  id: "m-h2",
853
2031
  role: "user",
854
2032
  text: "The forecast endpoint serves stale data after midnight UTC. Find out why and fix it.",
855
- createdAt: at(3)
2033
+ createdAt: at2(3)
856
2034
  },
857
2035
  {
858
2036
  id: "m-h3",
859
2037
  role: "assistant",
860
2038
  status: "done",
861
- createdAt: at(4),
2039
+ createdAt: at2(4),
862
2040
  parts: [
863
2041
  {
864
2042
  type: "thinking",
@@ -920,11 +2098,11 @@ var conversations = {
920
2098
  },
921
2099
  {
922
2100
  id: "m-h4",
923
- role: "event",
924
- kind: "scoop",
2101
+ role: "lick",
2102
+ channel: "scoop-notify",
925
2103
  title: "quiet-otter",
926
2104
  text: "Started: add retries with backoff around the upstream fetch",
927
- createdAt: at(9)
2105
+ createdAt: at2(9)
928
2106
  }
929
2107
  ],
930
2108
  "scoop-otter": [
@@ -932,13 +2110,13 @@ var conversations = {
932
2110
  id: "m-o1",
933
2111
  role: "user",
934
2112
  text: "Wrap the upstream fetch in src/routes/forecast.ts with three retries and exponential backoff.",
935
- createdAt: at(9)
2113
+ createdAt: at2(9)
936
2114
  },
937
2115
  {
938
2116
  id: "m-o2",
939
2117
  role: "assistant",
940
2118
  status: "done",
941
- createdAt: at(10),
2119
+ createdAt: at2(10),
942
2120
  parts: [
943
2121
  {
944
2122
  type: "tool",
@@ -973,17 +2151,17 @@ var conversations = {
973
2151
  "cone-release": [
974
2152
  {
975
2153
  id: "m-r1",
976
- role: "event",
977
- kind: "cron",
2154
+ role: "lick",
2155
+ channel: "cron",
978
2156
  title: "Every Friday 09:00",
979
2157
  text: "Draft the release notes for example/harbor",
980
- createdAt: at(-1440)
2158
+ createdAt: at2(-1440)
981
2159
  },
982
2160
  {
983
2161
  id: "m-r2",
984
2162
  role: "assistant",
985
2163
  status: "done",
986
- createdAt: at(-1438),
2164
+ createdAt: at2(-1438),
987
2165
  parts: [
988
2166
  {
989
2167
  type: "tool",
@@ -1007,17 +2185,19 @@ var conversations = {
1007
2185
  "cone-triage": [
1008
2186
  {
1009
2187
  id: "m-t1",
1010
- role: "event",
2188
+ role: "system",
1011
2189
  kind: "compaction",
2190
+ trigger: "threshold",
2191
+ state: "summarized",
1012
2192
  title: "Context compacted",
1013
2193
  text: "41 earlier messages were summarized to stay under the context limit.",
1014
- createdAt: at(-30)
2194
+ createdAt: at2(-30)
1015
2195
  },
1016
2196
  {
1017
2197
  id: "m-t2",
1018
2198
  role: "assistant",
1019
2199
  status: "done",
1020
- createdAt: at(-20),
2200
+ createdAt: at2(-20),
1021
2201
  parts: [
1022
2202
  {
1023
2203
  type: "text",
@@ -1027,11 +2207,12 @@ var conversations = {
1027
2207
  },
1028
2208
  {
1029
2209
  id: "m-t3",
1030
- role: "event",
2210
+ role: "system",
1031
2211
  kind: "error",
2212
+ action: "login",
1032
2213
  title: "Mail connector",
1033
2214
  text: "The connector returned 401. Reconnect the account in Settings.",
1034
- createdAt: at(-19)
2215
+ createdAt: at2(-19)
1035
2216
  }
1036
2217
  ]
1037
2218
  };
@@ -1062,6 +2243,177 @@ var terminals = [
1062
2243
  { id: "term-1", title: "bash", cwd: "/workspace/harbor", agentId: null }
1063
2244
  ];
1064
2245
 
2246
+ // src/dummy/memory.ts
2247
+ var DummyMemory = class extends Emitter {
2248
+ #memories;
2249
+ #next = 1;
2250
+ constructor(memories2) {
2251
+ super();
2252
+ this.#memories = memories2.map((memory2) => ({ ...memory2 }));
2253
+ }
2254
+ list() {
2255
+ return this.#memories.map((memory2) => ({ ...memory2 }));
2256
+ }
2257
+ save(draft) {
2258
+ const memory2 = {
2259
+ ...draft,
2260
+ id: draft.id ?? `mem-new-${this.#next++}`,
2261
+ updatedAt: Date.now()
2262
+ };
2263
+ const index2 = this.#memories.findIndex((candidate) => candidate.id === memory2.id);
2264
+ if (index2 < 0) this.#memories.push(memory2);
2265
+ else this.#memories[index2] = memory2;
2266
+ this.emit("memories", this.list());
2267
+ return { ...memory2 };
2268
+ }
2269
+ remove(id) {
2270
+ this.#memories = this.#memories.filter((memory2) => memory2.id !== id);
2271
+ this.emit("memories", this.list());
2272
+ }
2273
+ };
2274
+
2275
+ // src/dummy/monitor.ts
2276
+ var statuses = {
2277
+ idle: "idle",
2278
+ thinking: "active",
2279
+ working: "active",
2280
+ waiting: "warn",
2281
+ error: "error"
2282
+ };
2283
+ function wave(seed, length = 24) {
2284
+ return Array.from(
2285
+ { length },
2286
+ (_, i) => Math.round(40 + 25 * Math.sin((i + seed) / 3) + 10 * Math.cos(i * seed / 5))
2287
+ );
2288
+ }
2289
+ var DummyMonitor = class extends Emitter {
2290
+ #model;
2291
+ #tick = 0;
2292
+ constructor(model) {
2293
+ super();
2294
+ this.#model = model;
2295
+ const update = () => this.resync();
2296
+ model.agent.on("agents", update);
2297
+ model.terminals.on("terminals", update);
2298
+ model.browser.on("tabs", update);
2299
+ model.files.on("changes", update);
2300
+ model.tray.on("status", update);
2301
+ }
2302
+ snapshot() {
2303
+ const { agent, terminals: terminals2, browser, files: files3, sprinkles: sprinkles2, tray: tray2 } = this.#model;
2304
+ const listed = agent.list();
2305
+ const agents2 = listed.filter((candidate) => candidate.kind === "cone").flatMap((cone) => [cone, ...listed.filter((candidate) => candidate.parentId === cone.id)]);
2306
+ const active = agents2.filter((candidate) => candidate.status !== "idle").length;
2307
+ const status = tray2.status();
2308
+ const fill = Math.max(...agents2.map((candidate) => candidate.contextFill), 0);
2309
+ const agentRows = agents2.map(
2310
+ (candidate) => ({
2311
+ name: candidate.kind === "scoop" ? `\u21B3 ${candidate.name}` : candidate.name,
2312
+ meta: `${candidate.status} \xB7 ${Math.round(candidate.contextFill * 100)}% context`,
2313
+ status: statuses[candidate.status],
2314
+ badges: candidate.unread ? [`${candidate.unread} unread`] : []
2315
+ })
2316
+ );
2317
+ return {
2318
+ updatedAt: Date.now(),
2319
+ vitals: [
2320
+ {
2321
+ id: "agents",
2322
+ label: "Active agents",
2323
+ value: String(active),
2324
+ unit: `of ${agents2.length}`,
2325
+ series: wave(this.#tick + 1)
2326
+ },
2327
+ {
2328
+ id: "spend",
2329
+ label: "Spend",
2330
+ value: `$${status.spent.toFixed(2)}`,
2331
+ delta: `$${status.rate.toFixed(2)}/h`,
2332
+ series: wave(this.#tick + 7)
2333
+ },
2334
+ {
2335
+ id: "budget",
2336
+ label: `Budget (${status.budget.window})`,
2337
+ value: `${status.budget.percent}%`,
2338
+ unit: status.budget.resets,
2339
+ ratio: status.budget.percent / 100
2340
+ },
2341
+ {
2342
+ id: "context",
2343
+ label: "Fullest context",
2344
+ value: `${Math.round(fill * 100)}%`,
2345
+ ratio: fill
2346
+ }
2347
+ ],
2348
+ alerts: [
2349
+ ...agents2.filter((candidate) => candidate.contextFill >= 0.6).map((candidate) => ({
2350
+ id: `fill-${candidate.id}`,
2351
+ title: `${candidate.name} is ${Math.round(candidate.contextFill * 100)}% full`,
2352
+ detail: "Compact it soon to keep replies fast.",
2353
+ severity: "warn"
2354
+ })),
2355
+ ...status.connection === "live" ? [] : [
2356
+ {
2357
+ id: "tray",
2358
+ title: `Tray ${status.connection}`,
2359
+ detail: "Followers can\u2019t see this session.",
2360
+ severity: "error"
2361
+ }
2362
+ ]
2363
+ ],
2364
+ sections: [
2365
+ { id: "agents", label: "Cones and scoops", rows: agentRows },
2366
+ {
2367
+ id: "terminals",
2368
+ label: "Terminals",
2369
+ rows: terminals2.list().map((terminal) => ({
2370
+ name: terminal.title,
2371
+ meta: terminal.cwd,
2372
+ status: "active"
2373
+ }))
2374
+ },
2375
+ {
2376
+ id: "tabs",
2377
+ label: "Browser tabs",
2378
+ rows: browser.list().map((tab) => ({
2379
+ name: tab.title,
2380
+ meta: new URL(tab.url).host || tab.url,
2381
+ status: tab.status === "loading" ? "active" : "idle",
2382
+ badges: tab.agentId ? ["agent"] : []
2383
+ }))
2384
+ },
2385
+ {
2386
+ id: "changes",
2387
+ label: "Pending changes",
2388
+ rows: files3.changes().map((change) => ({ name: change.path, meta: change.status, status: "warn" }))
2389
+ },
2390
+ {
2391
+ id: "sprinkles",
2392
+ label: "Sprinkles",
2393
+ rows: sprinkles2.list().map((sprinkle) => ({
2394
+ name: sprinkle.title,
2395
+ meta: sprinkle.name,
2396
+ status: "idle"
2397
+ }))
2398
+ },
2399
+ {
2400
+ id: "followers",
2401
+ label: "Tray followers",
2402
+ rows: status.followers.map((follower) => ({
2403
+ name: follower.name,
2404
+ meta: follower.device,
2405
+ status: "active"
2406
+ }))
2407
+ }
2408
+ ]
2409
+ };
2410
+ }
2411
+ resync() {
2412
+ this.#tick += 1;
2413
+ this.emit("snapshot", this.snapshot());
2414
+ }
2415
+ };
2416
+
1065
2417
  // src/dummy/settings.ts
1066
2418
  var key = "slicc-ui.settings";
1067
2419
  var DummySettings = class extends Emitter {
@@ -1116,6 +2468,33 @@ var DummySettings = class extends Emitter {
1116
2468
  }
1117
2469
  };
1118
2470
 
2471
+ // src/dummy/sprinkles.ts
2472
+ var DummySprinkles = class extends Emitter {
2473
+ #sprinkles;
2474
+ #agent;
2475
+ constructor(sprinkles2, agent) {
2476
+ super();
2477
+ this.#sprinkles = sprinkles2.map((sprinkle) => ({ ...sprinkle }));
2478
+ this.#agent = agent;
2479
+ }
2480
+ list() {
2481
+ return this.#sprinkles.map((sprinkle) => ({ ...sprinkle }));
2482
+ }
2483
+ send(id, payload) {
2484
+ const sprinkle = this.#sprinkles.find((candidate) => candidate.id === id);
2485
+ if (!sprinkle) return;
2486
+ const data = payload ?? {};
2487
+ const what = data.event ? `\u201C${data.event}\u201D${data.detail ? ` (${data.detail})` : ""}` : "an event";
2488
+ this.#agent.lick(
2489
+ sprinkle.agentId,
2490
+ "sprinkle",
2491
+ sprinkle.title,
2492
+ `Sent ${what}`,
2493
+ JSON.stringify(payload, null, 2)
2494
+ );
2495
+ }
2496
+ };
2497
+
1119
2498
  // src/dummy/shell.ts
1120
2499
  var encoder = new TextEncoder();
1121
2500
  var decoder = new TextDecoder();
@@ -1368,13 +2747,48 @@ var DummyTerminals = class extends Emitter {
1368
2747
  }
1369
2748
  };
1370
2749
 
2750
+ // src/dummy/tray.ts
2751
+ var DummyTray = class extends Emitter {
2752
+ #status;
2753
+ #followers;
2754
+ #clock;
2755
+ constructor(status, clock2) {
2756
+ super();
2757
+ this.#status = structuredClone(status);
2758
+ this.#followers = structuredClone(status.followers);
2759
+ this.#clock = clock2;
2760
+ }
2761
+ status() {
2762
+ return structuredClone(this.#status);
2763
+ }
2764
+ #set(patch) {
2765
+ this.#status = { ...this.#status, ...patch };
2766
+ this.emit("status", this.status());
2767
+ }
2768
+ reconnect() {
2769
+ this.#set({ connection: "reconnecting" });
2770
+ void this.#clock.sleep(15).then(() => this.#set({ connection: "live", followers: structuredClone(this.#followers) }));
2771
+ }
2772
+ disconnect() {
2773
+ this.#set({ connection: "offline", followers: [] });
2774
+ }
2775
+ };
2776
+
1371
2777
  // src/dummy/index.ts
1372
2778
  function createDummyModel({ delay = 30, storage = null } = {}) {
1373
2779
  const time = clock(delay);
1374
2780
  const files3 = new DummyFiles(files2, directories, pending, time);
1375
2781
  const browser = new DummyBrowser(tabs, time);
1376
- return {
1377
- agent: new DummyAgent(agents, conversations, { files: files3, browser }, time),
2782
+ const agent = new DummyAgent(
2783
+ agents,
2784
+ conversations,
2785
+ { files: files3, browser },
2786
+ time,
2787
+ queues,
2788
+ frozen
2789
+ );
2790
+ const base = {
2791
+ agent,
1378
2792
  files: files3,
1379
2793
  browser,
1380
2794
  terminals: new DummyTerminals(terminals, files3, time),
@@ -1384,8 +2798,12 @@ function createDummyModel({ delay = 30, storage = null } = {}) {
1384
2798
  accounts,
1385
2799
  storage,
1386
2800
  time
1387
- )
2801
+ ),
2802
+ memory: new DummyMemory(memories),
2803
+ sprinkles: new DummySprinkles(sprinkles, agent),
2804
+ tray: new DummyTray(tray, time)
1388
2805
  };
2806
+ return { ...base, monitor: new DummyMonitor(base) };
1389
2807
  }
1390
2808
  export {
1391
2809
  createDummyModel