@ai-ecoverse/slicc-spectrum 1.0.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.
- package/README.md +86 -3
- package/dist/chunk-364Y2XOJ.js +28 -0
- package/dist/chunk-364Y2XOJ.js.map +7 -0
- package/dist/chunk-4GQ6THC2.js +18 -0
- package/dist/chunk-4GQ6THC2.js.map +7 -0
- package/dist/chunk-5OVGH7HO.js +28 -0
- package/dist/chunk-5OVGH7HO.js.map +7 -0
- package/dist/chunk-5XWUZJTT.js +8 -0
- package/dist/chunk-5XWUZJTT.js.map +7 -0
- package/dist/chunk-64K5VONJ.js +11 -0
- package/dist/chunk-64K5VONJ.js.map +7 -0
- package/dist/chunk-6A6NIESE.js +9 -0
- package/dist/chunk-6A6NIESE.js.map +7 -0
- package/dist/chunk-6JNYNXUQ.js +180 -0
- package/dist/chunk-6JNYNXUQ.js.map +7 -0
- package/dist/chunk-6JYCQXDO.js +8 -0
- package/dist/chunk-6JYCQXDO.js.map +7 -0
- package/dist/chunk-6N3GZNXH.js +181 -0
- package/dist/chunk-6N3GZNXH.js.map +7 -0
- package/dist/chunk-6ODADG7T.js +8 -0
- package/dist/chunk-6ODADG7T.js.map +7 -0
- package/dist/chunk-6T3O5K6R.js +8 -0
- package/dist/chunk-6T3O5K6R.js.map +7 -0
- package/dist/chunk-7PY7B3ZW.js +59 -0
- package/dist/chunk-7PY7B3ZW.js.map +7 -0
- package/dist/chunk-AXQFWXYZ.js +19 -0
- package/dist/chunk-AXQFWXYZ.js.map +7 -0
- package/dist/chunk-BDOI3WEE.js +8 -0
- package/dist/chunk-BDOI3WEE.js.map +7 -0
- package/dist/chunk-BHAGWMDD.js +8 -0
- package/dist/chunk-BHAGWMDD.js.map +7 -0
- package/dist/chunk-C7X653JJ.js +8 -0
- package/dist/chunk-C7X653JJ.js.map +7 -0
- package/dist/chunk-CGS66GRP.js +11 -0
- package/dist/chunk-CGS66GRP.js.map +7 -0
- package/dist/chunk-CONYKGKV.js +11 -0
- package/dist/chunk-CONYKGKV.js.map +7 -0
- package/dist/chunk-CU7BC2RI.js +11 -0
- package/dist/chunk-CU7BC2RI.js.map +7 -0
- package/dist/chunk-EN3XGF2M.js +26 -0
- package/dist/chunk-EN3XGF2M.js.map +7 -0
- package/dist/chunk-ER3HU7OE.js +22 -0
- package/dist/chunk-ER3HU7OE.js.map +7 -0
- package/dist/chunk-EX57SDII.js +8 -0
- package/dist/chunk-EX57SDII.js.map +7 -0
- package/dist/chunk-GKXQPI4G.js +8 -0
- package/dist/chunk-GKXQPI4G.js.map +7 -0
- package/dist/chunk-HBUX7E54.js +234 -0
- package/dist/chunk-HBUX7E54.js.map +7 -0
- package/dist/chunk-I2NMYJ25.js +17 -0
- package/dist/chunk-I2NMYJ25.js.map +7 -0
- package/dist/chunk-I4OYHPAU.js +11 -0
- package/dist/chunk-I4OYHPAU.js.map +7 -0
- package/dist/chunk-IBQ4O4NO.js +13 -0
- package/dist/chunk-IBQ4O4NO.js.map +7 -0
- package/dist/chunk-J3QOBXZJ.js +1093 -0
- package/dist/chunk-J3QOBXZJ.js.map +7 -0
- package/dist/chunk-JASEW37G.js +411 -0
- package/dist/chunk-JASEW37G.js.map +7 -0
- package/dist/chunk-JBYBW627.js +8 -0
- package/dist/chunk-JBYBW627.js.map +7 -0
- package/dist/chunk-JWTAKXQS.js +8 -0
- package/dist/chunk-JWTAKXQS.js.map +7 -0
- package/dist/chunk-KMP5HO3E.js +11 -0
- package/dist/chunk-KMP5HO3E.js.map +7 -0
- package/dist/chunk-KWPU2PY3.js +14 -0
- package/dist/chunk-KWPU2PY3.js.map +7 -0
- package/dist/chunk-KYBD6ZRT.js +8 -0
- package/dist/chunk-KYBD6ZRT.js.map +7 -0
- package/dist/chunk-LBXW6XNL.js +18 -0
- package/dist/chunk-LBXW6XNL.js.map +7 -0
- package/dist/chunk-LDR4U5KX.js +8 -0
- package/dist/chunk-LDR4U5KX.js.map +7 -0
- package/dist/chunk-LII3WY6Y.js +11 -0
- package/dist/chunk-LII3WY6Y.js.map +7 -0
- package/dist/chunk-LY7WVBCB.js +10 -0
- package/dist/chunk-LY7WVBCB.js.map +7 -0
- package/dist/chunk-MJVAL555.js +8 -0
- package/dist/chunk-MJVAL555.js.map +7 -0
- package/dist/chunk-NLDFGCIL.js +11 -0
- package/dist/chunk-NLDFGCIL.js.map +7 -0
- package/dist/chunk-OFQ56DAN.js +2638 -0
- package/dist/chunk-OFQ56DAN.js.map +7 -0
- package/dist/chunk-S5EGO4NA.js +11 -0
- package/dist/chunk-S5EGO4NA.js.map +7 -0
- package/dist/chunk-T2LAT2D6.js +168 -0
- package/dist/chunk-T2LAT2D6.js.map +7 -0
- package/dist/chunk-T5NHW4KC.js +23 -0
- package/dist/chunk-T5NHW4KC.js.map +7 -0
- package/dist/chunk-TAZBKRHT.js +10 -0
- package/dist/chunk-TAZBKRHT.js.map +7 -0
- package/dist/chunk-TK4ZJ7LA.js +5346 -0
- package/dist/chunk-TK4ZJ7LA.js.map +7 -0
- package/dist/chunk-TS7SL372.js +687 -0
- package/dist/chunk-TS7SL372.js.map +7 -0
- package/dist/chunk-UCIBH53C.js +11 -0
- package/dist/chunk-UCIBH53C.js.map +7 -0
- package/dist/chunk-XZM23TK4.js +11 -0
- package/dist/chunk-XZM23TK4.js.map +7 -0
- package/dist/chunk-Y4Z7K443.js +64 -0
- package/dist/chunk-Y4Z7K443.js.map +7 -0
- package/dist/chunk-YPQKCWWG.js +19 -0
- package/dist/chunk-YPQKCWWG.js.map +7 -0
- package/dist/chunk-YX6SI5VA.js +11 -0
- package/dist/chunk-YX6SI5VA.js.map +7 -0
- package/dist/chunk-ZMBN6APP.js +11 -0
- package/dist/chunk-ZMBN6APP.js.map +7 -0
- package/dist/fonts/source-code-pro-latin-400-normal.woff2 +0 -0
- package/dist/fonts/source-code-pro-latin-600-normal.woff2 +0 -0
- package/dist/slicc-dummy.js +2811 -0
- package/dist/slicc-dummy.js.map +7 -0
- package/dist/slicc-terminal.js +7 -5369
- package/dist/slicc-terminal.js.map +4 -4
- package/dist/slicc-ui.js +61411 -0
- package/dist/slicc-ui.js.map +7 -0
- package/dist/types/app/agents.d.ts +11 -0
- package/dist/types/app/app.d.ts +34 -0
- package/dist/types/app/base.d.ts +35 -0
- package/dist/types/app/browser.d.ts +25 -0
- package/dist/types/app/changes.d.ts +24 -0
- package/dist/types/app/chat.d.ts +11 -0
- package/dist/types/app/composer.d.ts +69 -0
- package/dist/types/app/files.d.ts +56 -0
- package/dist/types/app/fonts.d.ts +3 -0
- package/dist/types/app/freezer.d.ts +11 -0
- package/dist/types/app/markdown.d.ts +33 -0
- package/dist/types/app/memory.d.ts +39 -0
- package/dist/types/app/messages.d.ts +21 -0
- package/dist/types/app/monitor.d.ts +11 -0
- package/dist/types/app/panels.d.ts +39 -0
- package/dist/types/app/settings.d.ts +21 -0
- package/dist/types/app/spectrum.d.ts +37 -0
- package/dist/types/app/sprinkle.d.ts +18 -0
- package/dist/types/app/terminals.d.ts +25 -0
- package/dist/types/app/tray.d.ts +25 -0
- package/dist/types/components/code-view.d.ts +47 -0
- package/dist/types/components/dock.d.ts +24 -0
- package/dist/types/components/file-tree.d.ts +18 -0
- package/dist/types/components/sheets.d.ts +1 -0
- package/dist/types/dummy/agent.d.ts +32 -0
- package/dist/types/dummy/browser.d.ts +15 -0
- package/dist/types/dummy/clock.d.ts +5 -0
- package/dist/types/dummy/extras.d.ts +8 -0
- package/dist/types/dummy/files.d.ts +14 -0
- package/dist/types/dummy/fixtures.d.ts +12 -0
- package/dist/types/dummy/index.d.ts +6 -0
- package/dist/types/dummy/memory.d.ts +9 -0
- package/dist/types/dummy/monitor.d.ts +9 -0
- package/dist/types/dummy/script.d.ts +29 -0
- package/dist/types/dummy/settings.d.ts +13 -0
- package/dist/types/dummy/shell.d.ts +16 -0
- package/dist/types/dummy/showcase.d.ts +6 -0
- package/dist/types/dummy/sprinkles.d.ts +9 -0
- package/dist/types/dummy/terminals.d.ts +15 -0
- package/dist/types/dummy/tray.d.ts +10 -0
- package/dist/types/dummy.d.ts +2 -0
- package/dist/types/model/emitter.d.ts +9 -0
- package/dist/types/model/types.d.ts +455 -0
- package/dist/types/ui.d.ts +23 -0
- package/package.json +34 -11
|
@@ -0,0 +1,2811 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Emitter
|
|
3
|
+
} from "./chunk-T5NHW4KC.js";
|
|
4
|
+
import "./chunk-AXQFWXYZ.js";
|
|
5
|
+
|
|
6
|
+
// src/dummy/script.ts
|
|
7
|
+
var units = "/workspace/harbor/src/lib/units.ts";
|
|
8
|
+
var kelvin = `
|
|
9
|
+
export function toKelvin(celsius: number): number {
|
|
10
|
+
return Math.round((celsius + 273.15) * 100) / 100;
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
function test() {
|
|
14
|
+
return [
|
|
15
|
+
{ type: "thinking", text: "Run the whole suite first, then look at anything that fails." },
|
|
16
|
+
{
|
|
17
|
+
type: "tool",
|
|
18
|
+
name: "bash",
|
|
19
|
+
title: "Run the tests",
|
|
20
|
+
input: "npm test",
|
|
21
|
+
output: "> harbor@0.0.0 test\n> node --test test/\n\n\u2714 converts both ways (0.6ms)\n\u2714 expires entries from the previous day (0.4ms)\n\u2714 retries upstream timeouts (3.1ms)\n\u2139 tests 3\n\u2139 pass 3\n\u2139 fail 0",
|
|
22
|
+
paths: [],
|
|
23
|
+
ticks: 40
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: "All **3 tests pass**, including the new one for the midnight rollover. Nothing else to fix."
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
function edit() {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
type: "thinking",
|
|
35
|
+
text: "The conversion helpers live in units.ts. Add the missing one next to them."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "tool",
|
|
39
|
+
name: "read_file",
|
|
40
|
+
title: "Read the unit helpers",
|
|
41
|
+
input: units,
|
|
42
|
+
output: "export function toCelsius(fahrenheit: number): number { \u2026 }\nexport function toFahrenheit(celsius: number): number { \u2026 }",
|
|
43
|
+
paths: [units],
|
|
44
|
+
ticks: 10
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "tool",
|
|
48
|
+
name: "edit_file",
|
|
49
|
+
title: "Add toKelvin()",
|
|
50
|
+
input: units,
|
|
51
|
+
output: "Appended 4 lines",
|
|
52
|
+
paths: [units],
|
|
53
|
+
ticks: 20,
|
|
54
|
+
effect: async ({ files: files3, agentId }) => {
|
|
55
|
+
const before = await files3.read(units);
|
|
56
|
+
await files3.write(units, before + kelvin, agentId);
|
|
57
|
+
}
|
|
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
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
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."
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
function browse() {
|
|
78
|
+
const url = "https://api.example.com/docs/v2/units";
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
type: "tool",
|
|
82
|
+
name: "browser",
|
|
83
|
+
title: "Open the units reference",
|
|
84
|
+
input: `navigate ${url}`,
|
|
85
|
+
output: "Loaded api.example.com/docs/v2/units (200, 412 ms)",
|
|
86
|
+
paths: [],
|
|
87
|
+
ticks: 25,
|
|
88
|
+
effect: ({ browser, agentId }) => browser.open(url, agentId)
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: "The reference page is open in the **Browser** panel. Temperatures are documented in Celsius, with Fahrenheit behind `?units=imperial`."
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
function files() {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
type: "tool",
|
|
100
|
+
name: "bash",
|
|
101
|
+
title: "List the project",
|
|
102
|
+
input: "ls /workspace/harbor/src",
|
|
103
|
+
output: "index.ts legacy lib routes",
|
|
104
|
+
paths: ["/workspace/harbor/src"],
|
|
105
|
+
ticks: 8
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "tool",
|
|
109
|
+
name: "read_file",
|
|
110
|
+
title: "Read the README",
|
|
111
|
+
input: "/workspace/harbor/README.md",
|
|
112
|
+
output: "# harbor\n\nA small forecast API that sits in front of the upstream weather service and caches per city.",
|
|
113
|
+
paths: ["/workspace/harbor/README.md"],
|
|
114
|
+
ticks: 8
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: "harbor has four parts:\n\n- `index.ts`: the worker entry and routing\n- `routes/forecast.ts`: the forecast handler\n- `lib/`: cache, retry and unit helpers\n- `legacy/`: the XML output, marked for removal"
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
function chat(prompt) {
|
|
123
|
+
return [
|
|
124
|
+
{ type: "thinking", text: "Short question. Answer directly, then offer the next step." },
|
|
125
|
+
{
|
|
126
|
+
type: "tool",
|
|
127
|
+
name: "bash",
|
|
128
|
+
title: "Check the working tree",
|
|
129
|
+
input: "git status --short",
|
|
130
|
+
output: " M src/lib/cache.ts\n M src/routes/forecast.ts\n A src/lib/retry.ts\n D src/legacy/xml.ts",
|
|
131
|
+
paths: [],
|
|
132
|
+
ticks: 12
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: "text",
|
|
136
|
+
text: `On \u201C${prompt.slice(0, 60)}\u201D: there are **4 uncommitted changes** in harbor, from me and quiet-otter. Review them in Changes, or ask me to run the tests, edit a file or open a page.`
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
}
|
|
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 = []) {
|
|
186
|
+
const text = prompt.toLowerCase();
|
|
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 };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/dummy/agent.ts
|
|
198
|
+
function chunks(text) {
|
|
199
|
+
return text.match(/\S+\s*|\s+/g) ?? [];
|
|
200
|
+
}
|
|
201
|
+
function outgoing(input) {
|
|
202
|
+
return typeof input === "string" ? { text: input } : input;
|
|
203
|
+
}
|
|
204
|
+
var DummyAgent = class extends Emitter {
|
|
205
|
+
#agents;
|
|
206
|
+
#messages = /* @__PURE__ */ new Map();
|
|
207
|
+
#queues = /* @__PURE__ */ new Map();
|
|
208
|
+
#frozen = [];
|
|
209
|
+
#ice = /* @__PURE__ */ new Map();
|
|
210
|
+
#suggestions = /* @__PURE__ */ new Map();
|
|
211
|
+
#runs = /* @__PURE__ */ new Map();
|
|
212
|
+
#active;
|
|
213
|
+
#clock;
|
|
214
|
+
#effects;
|
|
215
|
+
#next = 1;
|
|
216
|
+
constructor(agents2, conversations2, effects, clock2, queues2 = {}, frozen2 = []) {
|
|
217
|
+
super();
|
|
218
|
+
this.#clock = clock2;
|
|
219
|
+
this.#effects = effects;
|
|
220
|
+
this.#agents = agents2.map((agent) => ({ ...agent }));
|
|
221
|
+
for (const agent of this.#agents) {
|
|
222
|
+
this.#messages.set(agent.id, structuredClone([...conversations2[agent.id] ?? []]));
|
|
223
|
+
this.#queues.set(agent.id, structuredClone([...queues2[agent.id] ?? []]));
|
|
224
|
+
}
|
|
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
|
+
}
|
|
244
|
+
}
|
|
245
|
+
list() {
|
|
246
|
+
return this.#agents.map((agent) => ({ ...agent }));
|
|
247
|
+
}
|
|
248
|
+
active() {
|
|
249
|
+
return this.#active;
|
|
250
|
+
}
|
|
251
|
+
#agent(id) {
|
|
252
|
+
return this.#agents.find((agent) => agent.id === id);
|
|
253
|
+
}
|
|
254
|
+
#changed() {
|
|
255
|
+
this.emit("agents", this.list());
|
|
256
|
+
}
|
|
257
|
+
#id(prefix) {
|
|
258
|
+
return `${prefix}-${this.#next++}`;
|
|
259
|
+
}
|
|
260
|
+
select(id) {
|
|
261
|
+
const agent = this.#agent(id);
|
|
262
|
+
if (!agent) return;
|
|
263
|
+
this.#active = id;
|
|
264
|
+
agent.unread = 0;
|
|
265
|
+
this.emit("active", id);
|
|
266
|
+
this.#changed();
|
|
267
|
+
}
|
|
268
|
+
messages(agentId) {
|
|
269
|
+
return [...this.#messages.get(agentId) ?? []];
|
|
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
|
+
}
|
|
280
|
+
#post(agentId, message) {
|
|
281
|
+
this.#messages.get(agentId)?.push(message);
|
|
282
|
+
this.emit("message", { agentId, message });
|
|
283
|
+
}
|
|
284
|
+
#status(agentId, status) {
|
|
285
|
+
const agent = this.#agent(agentId);
|
|
286
|
+
if (!agent || agent.status === status) return;
|
|
287
|
+
agent.status = status;
|
|
288
|
+
this.#changed();
|
|
289
|
+
}
|
|
290
|
+
async #stream(agentId, reply, part, text, run) {
|
|
291
|
+
for (const chunk of chunks(text)) {
|
|
292
|
+
await this.#clock.sleep();
|
|
293
|
+
if (run.stopped) return;
|
|
294
|
+
part.text += chunk;
|
|
295
|
+
this.emit("message", { agentId, message: reply });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async #tool(agentId, reply, step, run) {
|
|
299
|
+
this.#status(agentId, "working");
|
|
300
|
+
const tool2 = {
|
|
301
|
+
id: this.#id("t"),
|
|
302
|
+
name: step.name,
|
|
303
|
+
title: step.title,
|
|
304
|
+
input: step.input,
|
|
305
|
+
output: "",
|
|
306
|
+
status: "running",
|
|
307
|
+
paths: step.paths
|
|
308
|
+
};
|
|
309
|
+
reply.parts.push({ type: "tool", tool: tool2 });
|
|
310
|
+
this.emit("message", { agentId, message: reply });
|
|
311
|
+
await this.#clock.sleep(step.ticks);
|
|
312
|
+
const done = run.stopped ? { status: "cancelled", output: "Stopped" } : { status: "done", output: step.output };
|
|
313
|
+
if (!run.stopped) await step.effect?.({ ...this.#effects, agentId });
|
|
314
|
+
Object.assign(tool2, done);
|
|
315
|
+
this.emit("message", { agentId, message: reply });
|
|
316
|
+
}
|
|
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) {
|
|
352
|
+
const agent = this.#agent(agentId);
|
|
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
|
+
}
|
|
360
|
+
this.stop(agentId);
|
|
361
|
+
this.#suggestions.delete(agentId);
|
|
362
|
+
const run = { stopped: false };
|
|
363
|
+
this.#runs.set(agentId, run);
|
|
364
|
+
this.#post(agentId, {
|
|
365
|
+
id: this.#id("m"),
|
|
366
|
+
role: "user",
|
|
367
|
+
text: prompt,
|
|
368
|
+
createdAt: Date.now(),
|
|
369
|
+
...message.mode === "steer" ? { mode: "steer" } : {},
|
|
370
|
+
...message.attachments?.length ? { attachments: message.attachments } : {}
|
|
371
|
+
});
|
|
372
|
+
const reply = {
|
|
373
|
+
id: this.#id("m"),
|
|
374
|
+
role: "assistant",
|
|
375
|
+
parts: [],
|
|
376
|
+
status: "streaming",
|
|
377
|
+
createdAt: Date.now(),
|
|
378
|
+
model: agent.model
|
|
379
|
+
};
|
|
380
|
+
this.#post(agentId, reply);
|
|
381
|
+
const { steps, suggestion } = script(prompt, message.attachments ?? []);
|
|
382
|
+
for (const step of steps) {
|
|
383
|
+
if (run.stopped) break;
|
|
384
|
+
await this.#step(agentId, reply, step, run);
|
|
385
|
+
}
|
|
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
|
+
};
|
|
392
|
+
if (this.#runs.get(agentId) === run) this.#runs.delete(agentId);
|
|
393
|
+
agent.contextFill = Math.min(0.95, Math.round((agent.contextFill + 0.03) * 100) / 100);
|
|
394
|
+
if (agentId !== this.#active) agent.unread += 1;
|
|
395
|
+
if (!run.stopped) this.#suggestions.set(agentId, suggestion);
|
|
396
|
+
this.emit("message", { agentId, message: reply });
|
|
397
|
+
agent.status = "idle";
|
|
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 });
|
|
408
|
+
}
|
|
409
|
+
stop(agentId) {
|
|
410
|
+
const run = this.#runs.get(agentId);
|
|
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());
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// src/dummy/browser.ts
|
|
570
|
+
function escape(text) {
|
|
571
|
+
return text.replace(/[<>&"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
572
|
+
}
|
|
573
|
+
function hue(text) {
|
|
574
|
+
let sum = 0;
|
|
575
|
+
for (const char of text) sum = (sum * 31 + char.charCodeAt(0)) % 360;
|
|
576
|
+
return sum;
|
|
577
|
+
}
|
|
578
|
+
function page(tab) {
|
|
579
|
+
const h = hue(new URL(tab.url).host);
|
|
580
|
+
const rows = [0, 1, 2, 3, 4, 5].map(
|
|
581
|
+
(i) => `<rect x="64" y="${220 + i * 36}" width="${560 - i % 3 * 90}" height="12" rx="6" fill="#d6d6d6"/>`
|
|
582
|
+
).join("");
|
|
583
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800"><rect width="1280" height="800" fill="#ffffff"/><rect width="1280" height="64" fill="hsl(${h} 45% 32%)"/><text x="64" y="40" font-family="sans-serif" font-size="20" fill="#ffffff">${escape(new URL(tab.url).host)}</text><text x="64" y="160" font-family="sans-serif" font-size="36" fill="#222222">${escape(tab.title)}</text>${rows}<rect x="720" y="200" width="496" height="280" rx="12" fill="hsl(${h} 40% 92%)"/></svg>`;
|
|
584
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
585
|
+
}
|
|
586
|
+
function titleFor(url) {
|
|
587
|
+
const { host, pathname } = new URL(url);
|
|
588
|
+
if (!host) return "New tab";
|
|
589
|
+
return pathname === "/" ? host : `${host}${pathname}`;
|
|
590
|
+
}
|
|
591
|
+
var DummyBrowser = class extends Emitter {
|
|
592
|
+
#tabs;
|
|
593
|
+
#active;
|
|
594
|
+
#clock;
|
|
595
|
+
#next = 1;
|
|
596
|
+
constructor(tabs2, clock2) {
|
|
597
|
+
super();
|
|
598
|
+
this.#clock = clock2;
|
|
599
|
+
this.#tabs = tabs2.map((tab) => ({ ...tab }));
|
|
600
|
+
this.#active = this.#tabs[0]?.id ?? null;
|
|
601
|
+
}
|
|
602
|
+
list() {
|
|
603
|
+
return this.#tabs.map((tab) => ({ ...tab }));
|
|
604
|
+
}
|
|
605
|
+
active() {
|
|
606
|
+
return this.#active;
|
|
607
|
+
}
|
|
608
|
+
#changed() {
|
|
609
|
+
this.emit("tabs", this.list());
|
|
610
|
+
this.emit("active", this.#active);
|
|
611
|
+
}
|
|
612
|
+
activate(id) {
|
|
613
|
+
if (!this.#tabs.some((tab) => tab.id === id)) return;
|
|
614
|
+
this.#active = id;
|
|
615
|
+
this.emit("active", id);
|
|
616
|
+
}
|
|
617
|
+
#load(id) {
|
|
618
|
+
void this.#clock.sleep(15).then(() => {
|
|
619
|
+
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
620
|
+
if (!tab) return;
|
|
621
|
+
tab.status = "complete";
|
|
622
|
+
tab.title = titleFor(tab.url);
|
|
623
|
+
this.#changed();
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
open(url, agentId = null) {
|
|
627
|
+
const tab = {
|
|
628
|
+
id: `tab-new-${this.#next++}`,
|
|
629
|
+
title: url,
|
|
630
|
+
url,
|
|
631
|
+
status: "loading",
|
|
632
|
+
agentId
|
|
633
|
+
};
|
|
634
|
+
this.#tabs.push(tab);
|
|
635
|
+
this.#active = tab.id;
|
|
636
|
+
this.#changed();
|
|
637
|
+
this.#load(tab.id);
|
|
638
|
+
return { ...tab };
|
|
639
|
+
}
|
|
640
|
+
navigate(id, url) {
|
|
641
|
+
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
642
|
+
if (!tab) return;
|
|
643
|
+
tab.url = url;
|
|
644
|
+
tab.title = url;
|
|
645
|
+
tab.status = "loading";
|
|
646
|
+
this.#changed();
|
|
647
|
+
this.#load(id);
|
|
648
|
+
}
|
|
649
|
+
close(id) {
|
|
650
|
+
const index2 = this.#tabs.findIndex((tab) => tab.id === id);
|
|
651
|
+
if (index2 < 0) return;
|
|
652
|
+
this.#tabs.splice(index2, 1);
|
|
653
|
+
if (this.#active === id)
|
|
654
|
+
this.#active = this.#tabs[Math.min(index2, this.#tabs.length - 1)]?.id ?? null;
|
|
655
|
+
this.#changed();
|
|
656
|
+
}
|
|
657
|
+
async screenshot(id) {
|
|
658
|
+
await this.#clock.sleep(2);
|
|
659
|
+
const tab = this.#tabs.find((candidate) => candidate.id === id);
|
|
660
|
+
if (!tab) throw new Error(`No tab ${id}`);
|
|
661
|
+
return page(tab);
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/dummy/clock.ts
|
|
666
|
+
function clock(delay) {
|
|
667
|
+
return {
|
|
668
|
+
delay,
|
|
669
|
+
sleep: (ticks = 1) => new Promise((resolve2) => setTimeout(resolve2, delay * ticks))
|
|
670
|
+
};
|
|
671
|
+
}
|
|
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
|
+
|
|
877
|
+
// src/dummy/files.ts
|
|
878
|
+
function parents(path) {
|
|
879
|
+
const parts = path.split("/").slice(1, -1);
|
|
880
|
+
return parts.map((_, i) => `/${parts.slice(0, i + 1).join("/")}`);
|
|
881
|
+
}
|
|
882
|
+
var DummyFiles = class extends Emitter {
|
|
883
|
+
#contents = /* @__PURE__ */ new Map();
|
|
884
|
+
#directories = /* @__PURE__ */ new Set();
|
|
885
|
+
#modified = /* @__PURE__ */ new Map();
|
|
886
|
+
#changes = /* @__PURE__ */ new Map();
|
|
887
|
+
#clock;
|
|
888
|
+
constructor(contents, directories2, changes, clock2) {
|
|
889
|
+
super();
|
|
890
|
+
this.#clock = clock2;
|
|
891
|
+
for (const directory of directories2) this.#mkdirs(`${directory}/`);
|
|
892
|
+
for (const [path, text] of Object.entries(contents)) this.#put(path, text, 0);
|
|
893
|
+
for (const change of changes) this.#changes.set(change.path, { ...change });
|
|
894
|
+
for (const change of changes) {
|
|
895
|
+
if (change.after === null) this.#contents.delete(change.path);
|
|
896
|
+
else this.#put(change.path, change.after, 0);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
#mkdirs(path) {
|
|
900
|
+
for (const directory of parents(path)) this.#directories.add(directory);
|
|
901
|
+
}
|
|
902
|
+
#put(path, text, now2) {
|
|
903
|
+
this.#mkdirs(path);
|
|
904
|
+
this.#contents.set(path, text);
|
|
905
|
+
this.#modified.set(path, now2);
|
|
906
|
+
}
|
|
907
|
+
#entries() {
|
|
908
|
+
const directories2 = [...this.#directories].map(
|
|
909
|
+
(path) => ({ path, kind: "directory", size: 0, modified: 0 })
|
|
910
|
+
);
|
|
911
|
+
const files3 = [...this.#contents].map(
|
|
912
|
+
([path, text]) => ({
|
|
913
|
+
path,
|
|
914
|
+
kind: "file",
|
|
915
|
+
size: new TextEncoder().encode(text).length,
|
|
916
|
+
modified: this.#modified.get(path)
|
|
917
|
+
})
|
|
918
|
+
);
|
|
919
|
+
return [...directories2, ...files3].sort((a, b) => a.path.localeCompare(b.path));
|
|
920
|
+
}
|
|
921
|
+
async list() {
|
|
922
|
+
await this.#clock.sleep();
|
|
923
|
+
return this.#entries();
|
|
924
|
+
}
|
|
925
|
+
async read(path) {
|
|
926
|
+
await this.#clock.sleep();
|
|
927
|
+
const text = this.#contents.get(path);
|
|
928
|
+
if (text === void 0) throw new Error(`ENOENT: ${path}`);
|
|
929
|
+
return text;
|
|
930
|
+
}
|
|
931
|
+
#record(path, after, agentId) {
|
|
932
|
+
const previous = this.#changes.get(path);
|
|
933
|
+
const before = previous ? previous.before : this.#contents.get(path) ?? null;
|
|
934
|
+
if (before === after) {
|
|
935
|
+
this.#changes.delete(path);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const status = before === null ? "added" : after === null ? "deleted" : "modified";
|
|
939
|
+
this.#changes.set(path, { path, status, before, after, agentId });
|
|
940
|
+
}
|
|
941
|
+
#announce(path) {
|
|
942
|
+
this.emit("files", this.#entries());
|
|
943
|
+
this.emit("file", path);
|
|
944
|
+
this.emit("changes", this.changes());
|
|
945
|
+
}
|
|
946
|
+
async write(path, text, agentId = null) {
|
|
947
|
+
await this.#clock.sleep();
|
|
948
|
+
this.#record(path, text, agentId);
|
|
949
|
+
this.#put(path, text, Date.now());
|
|
950
|
+
this.#announce(path);
|
|
951
|
+
}
|
|
952
|
+
async remove(path, agentId = null) {
|
|
953
|
+
await this.#clock.sleep();
|
|
954
|
+
if (!this.#contents.has(path)) throw new Error(`ENOENT: ${path}`);
|
|
955
|
+
this.#record(path, null, agentId);
|
|
956
|
+
this.#contents.delete(path);
|
|
957
|
+
this.#announce(path);
|
|
958
|
+
}
|
|
959
|
+
changes() {
|
|
960
|
+
return [...this.#changes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
961
|
+
}
|
|
962
|
+
accept(path) {
|
|
963
|
+
this.#changes.delete(path);
|
|
964
|
+
this.emit("changes", this.changes());
|
|
965
|
+
}
|
|
966
|
+
async revert(path) {
|
|
967
|
+
const change = this.#changes.get(path);
|
|
968
|
+
if (!change) return;
|
|
969
|
+
await this.#clock.sleep();
|
|
970
|
+
this.#changes.delete(path);
|
|
971
|
+
if (change.before === null) this.#contents.delete(path);
|
|
972
|
+
else this.#put(path, change.before, Date.now());
|
|
973
|
+
this.#announce(path);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
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
|
+
|
|
1643
|
+
// src/dummy/fixtures.ts
|
|
1644
|
+
var cacheBefore = `export interface Entry<T> {
|
|
1645
|
+
value: T;
|
|
1646
|
+
storedAt: number;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const TTL = 60 * 60 * 1000;
|
|
1650
|
+
|
|
1651
|
+
export class ForecastCache<T> {
|
|
1652
|
+
#entries = new Map<string, Entry<T>>();
|
|
1653
|
+
|
|
1654
|
+
get(key: string, now = Date.now()): T | undefined {
|
|
1655
|
+
const entry = this.#entries.get(key);
|
|
1656
|
+
if (!entry) return undefined;
|
|
1657
|
+
const day = new Date(entry.storedAt).getDate();
|
|
1658
|
+
if (day !== new Date(now).getDate()) return entry.value;
|
|
1659
|
+
if (now - entry.storedAt > TTL) return undefined;
|
|
1660
|
+
return entry.value;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
set(key: string, value: T, now = Date.now()): void {
|
|
1664
|
+
this.#entries.set(key, { value, storedAt: now });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
`;
|
|
1668
|
+
var cacheAfter = `export interface Entry<T> {
|
|
1669
|
+
value: T;
|
|
1670
|
+
storedAt: number;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
const TTL = 60 * 60 * 1000;
|
|
1674
|
+
|
|
1675
|
+
export class ForecastCache<T> {
|
|
1676
|
+
#entries = new Map<string, Entry<T>>();
|
|
1677
|
+
|
|
1678
|
+
get(key: string, now = Date.now()): T | undefined {
|
|
1679
|
+
const entry = this.#entries.get(key);
|
|
1680
|
+
if (!entry) return undefined;
|
|
1681
|
+
if (dayOf(entry.storedAt) !== dayOf(now)) {
|
|
1682
|
+
this.#entries.delete(key);
|
|
1683
|
+
return undefined;
|
|
1684
|
+
}
|
|
1685
|
+
if (now - entry.storedAt > TTL) return undefined;
|
|
1686
|
+
return entry.value;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
set(key: string, value: T, now = Date.now()): void {
|
|
1690
|
+
this.#entries.set(key, { value, storedAt: now });
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function dayOf(time: number): string {
|
|
1695
|
+
return new Date(time).toISOString().slice(0, 10);
|
|
1696
|
+
}
|
|
1697
|
+
`;
|
|
1698
|
+
var retry = `export async function retry<T>(task: () => Promise<T>, attempts = 3, wait = 250): Promise<T> {
|
|
1699
|
+
let last: unknown;
|
|
1700
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1701
|
+
try {
|
|
1702
|
+
return await task();
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
last = error;
|
|
1705
|
+
await new Promise((resolve) => setTimeout(resolve, wait * 2 ** attempt));
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
throw last;
|
|
1709
|
+
}
|
|
1710
|
+
`;
|
|
1711
|
+
var legacyXml = `export function toXml(forecast: { city: string; high: number; low: number }): string {
|
|
1712
|
+
return \`<forecast city="\${forecast.city}"><high>\${forecast.high}</high><low>\${forecast.low}</low></forecast>\`;
|
|
1713
|
+
}
|
|
1714
|
+
`;
|
|
1715
|
+
var forecastBefore = `import { ForecastCache } from '../lib/cache.ts';
|
|
1716
|
+
import { toCelsius } from '../lib/units.ts';
|
|
1717
|
+
|
|
1718
|
+
const cache = new ForecastCache<Forecast>();
|
|
1719
|
+
|
|
1720
|
+
export interface Forecast {
|
|
1721
|
+
city: string;
|
|
1722
|
+
high: number;
|
|
1723
|
+
low: number;
|
|
1724
|
+
summary: string;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
export async function forecast(city: string, upstream: typeof fetch = fetch): Promise<Forecast> {
|
|
1728
|
+
const cached = cache.get(city);
|
|
1729
|
+
if (cached) return cached;
|
|
1730
|
+
const response = await upstream(\`https://api.example.com/v2/forecast?city=\${encodeURIComponent(city)}\`);
|
|
1731
|
+
const body = await response.json();
|
|
1732
|
+
const result = {
|
|
1733
|
+
city,
|
|
1734
|
+
high: toCelsius(body.daily.max),
|
|
1735
|
+
low: toCelsius(body.daily.min),
|
|
1736
|
+
summary: body.daily.summary,
|
|
1737
|
+
};
|
|
1738
|
+
cache.set(city, result);
|
|
1739
|
+
return result;
|
|
1740
|
+
}
|
|
1741
|
+
`;
|
|
1742
|
+
var forecastAfter = forecastBefore.replace(
|
|
1743
|
+
"import { toCelsius } from '../lib/units.ts';",
|
|
1744
|
+
"import { retry } from '../lib/retry.ts';\nimport { toCelsius } from '../lib/units.ts';"
|
|
1745
|
+
).replace("const response = await upstream(", "const response = await retry(() =>\n upstream(").replace("(city)}`);", "(city)}`)\n );");
|
|
1746
|
+
var units2 = `export function toCelsius(fahrenheit: number): number {
|
|
1747
|
+
return Math.round(((fahrenheit - 32) * 5) / 9);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
export function toFahrenheit(celsius: number): number {
|
|
1751
|
+
return Math.round((celsius * 9) / 5 + 32);
|
|
1752
|
+
}
|
|
1753
|
+
`;
|
|
1754
|
+
var unitsTest = `import assert from 'node:assert/strict';
|
|
1755
|
+
import { test } from 'node:test';
|
|
1756
|
+
import { toCelsius, toFahrenheit } from '../src/lib/units.ts';
|
|
1757
|
+
|
|
1758
|
+
test('converts both ways', () => {
|
|
1759
|
+
assert.equal(toCelsius(212), 100);
|
|
1760
|
+
assert.equal(toFahrenheit(0), 32);
|
|
1761
|
+
});
|
|
1762
|
+
`;
|
|
1763
|
+
var index = `import { forecast } from './routes/forecast.ts';
|
|
1764
|
+
|
|
1765
|
+
export default {
|
|
1766
|
+
async fetch(request: Request): Promise<Response> {
|
|
1767
|
+
const url = new URL(request.url);
|
|
1768
|
+
if (url.pathname === '/health') return new Response('ok');
|
|
1769
|
+
const city = url.searchParams.get('city');
|
|
1770
|
+
if (!city) return Response.json({ error: 'city is required' }, { status: 400 });
|
|
1771
|
+
return Response.json(await forecast(city));
|
|
1772
|
+
},
|
|
1773
|
+
};
|
|
1774
|
+
`;
|
|
1775
|
+
var readme = `# harbor
|
|
1776
|
+
|
|
1777
|
+
A small forecast API that sits in front of the upstream weather service and caches per city.
|
|
1778
|
+
|
|
1779
|
+
## Development
|
|
1780
|
+
|
|
1781
|
+
\`\`\`sh
|
|
1782
|
+
npm install
|
|
1783
|
+
npm test
|
|
1784
|
+
npm run dev
|
|
1785
|
+
\`\`\`
|
|
1786
|
+
|
|
1787
|
+
The worker listens on \`http://localhost:8787\`. Try \`/forecast?city=Lisbon\`.
|
|
1788
|
+
`;
|
|
1789
|
+
var pkg = `{
|
|
1790
|
+
"name": "harbor",
|
|
1791
|
+
"private": true,
|
|
1792
|
+
"type": "module",
|
|
1793
|
+
"scripts": {
|
|
1794
|
+
"dev": "wrangler dev",
|
|
1795
|
+
"test": "node --test test/"
|
|
1796
|
+
},
|
|
1797
|
+
"devDependencies": {
|
|
1798
|
+
"wrangler": "4.40.0"
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
`;
|
|
1802
|
+
var skill = `---
|
|
1803
|
+
name: release-notes
|
|
1804
|
+
description: Draft release notes from merged pull requests since the last tag.
|
|
1805
|
+
---
|
|
1806
|
+
|
|
1807
|
+
1. Run \`collect.jsh\` with the repository and the previous tag.
|
|
1808
|
+
2. Group the pull requests by label: features, fixes, chores.
|
|
1809
|
+
3. Write one sentence per change, in the past tense, without ticket numbers.
|
|
1810
|
+
`;
|
|
1811
|
+
var collect = `const [repo, since] = process.argv.slice(2);
|
|
1812
|
+
const pulls = await gh(\`repos/\${repo}/pulls?state=closed&per_page=100\`);
|
|
1813
|
+
for (const pull of pulls.filter((p) => p.merged_at > since)) {
|
|
1814
|
+
console.log(\`\${pull.number}\\t\${pull.labels.map((l) => l.name).join(',')}\\t\${pull.title}\`);
|
|
1815
|
+
}
|
|
1816
|
+
`;
|
|
1817
|
+
var memory = `# Shared memory
|
|
1818
|
+
|
|
1819
|
+
## Preferences
|
|
1820
|
+
- Keep replies short. Lead with the result, then the evidence.
|
|
1821
|
+
- Run the tests before saying something is fixed.
|
|
1822
|
+
|
|
1823
|
+
## Projects
|
|
1824
|
+
- harbor: forecast API, deployed as a worker. Cache bugs show up around midnight UTC.
|
|
1825
|
+
- release-notes: drafted every Friday from merged pull requests.
|
|
1826
|
+
`;
|
|
1827
|
+
var standup = `# Standup, 4 October
|
|
1828
|
+
|
|
1829
|
+
- harbor: retries for upstream timeouts are in review.
|
|
1830
|
+
- Docs: the forecast endpoint page still shows Fahrenheit examples.
|
|
1831
|
+
- Next: move the cache to KV once the TTL fix lands.
|
|
1832
|
+
`;
|
|
1833
|
+
var sample = `{
|
|
1834
|
+
"city": "Lisbon",
|
|
1835
|
+
"daily": { "max": 77, "min": 61, "summary": "Clear, light wind from the north" }
|
|
1836
|
+
}
|
|
1837
|
+
`;
|
|
1838
|
+
var bashrc = `export PS1='\\u@slicc:\\w\\$ '
|
|
1839
|
+
alias ll='ls -la'
|
|
1840
|
+
`;
|
|
1841
|
+
var files2 = {
|
|
1842
|
+
"/workspace/harbor/README.md": readme,
|
|
1843
|
+
"/workspace/harbor/package.json": pkg,
|
|
1844
|
+
"/workspace/harbor/wrangler.toml": 'name = "harbor"\nmain = "src/index.ts"\ncompatibility_date = "2026-09-01"\n',
|
|
1845
|
+
"/workspace/harbor/.gitignore": "node_modules/\n.wrangler/\n",
|
|
1846
|
+
"/workspace/harbor/src/index.ts": index,
|
|
1847
|
+
"/workspace/harbor/src/routes/forecast.ts": forecastBefore,
|
|
1848
|
+
"/workspace/harbor/src/lib/cache.ts": cacheBefore,
|
|
1849
|
+
"/workspace/harbor/src/lib/units.ts": units2,
|
|
1850
|
+
"/workspace/harbor/src/legacy/xml.ts": legacyXml,
|
|
1851
|
+
"/workspace/harbor/test/units.test.ts": unitsTest,
|
|
1852
|
+
"/workspace/skills/release-notes/SKILL.md": skill,
|
|
1853
|
+
"/workspace/skills/release-notes/scripts/collect.jsh": collect,
|
|
1854
|
+
"/shared/MEMORY.md": memory,
|
|
1855
|
+
"/shared/notes/2026-10-04-standup.md": standup,
|
|
1856
|
+
"/home/user/.bashrc": bashrc,
|
|
1857
|
+
"/tmp/forecast-sample.json": sample
|
|
1858
|
+
};
|
|
1859
|
+
var directories = ["/sessions", "/home/user/downloads"];
|
|
1860
|
+
var pending = [
|
|
1861
|
+
{
|
|
1862
|
+
path: "/workspace/harbor/src/lib/cache.ts",
|
|
1863
|
+
status: "modified",
|
|
1864
|
+
before: cacheBefore,
|
|
1865
|
+
after: cacheAfter,
|
|
1866
|
+
agentId: "cone-harbor"
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
path: "/workspace/harbor/src/lib/retry.ts",
|
|
1870
|
+
status: "added",
|
|
1871
|
+
before: null,
|
|
1872
|
+
after: retry,
|
|
1873
|
+
agentId: "scoop-otter"
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
path: "/workspace/harbor/src/routes/forecast.ts",
|
|
1877
|
+
status: "modified",
|
|
1878
|
+
before: forecastBefore,
|
|
1879
|
+
after: forecastAfter,
|
|
1880
|
+
agentId: "scoop-otter"
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
path: "/workspace/harbor/src/legacy/xml.ts",
|
|
1884
|
+
status: "deleted",
|
|
1885
|
+
before: legacyXml,
|
|
1886
|
+
after: null,
|
|
1887
|
+
agentId: "cone-harbor"
|
|
1888
|
+
}
|
|
1889
|
+
];
|
|
1890
|
+
var models = [
|
|
1891
|
+
{ id: "claude-opus-5-5", label: "Claude Opus 5.5", provider: "Anthropic" },
|
|
1892
|
+
{ id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5", provider: "Anthropic" },
|
|
1893
|
+
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5", provider: "Anthropic" },
|
|
1894
|
+
{ id: "local-small", label: "Local (ort-llama)", provider: "This browser" }
|
|
1895
|
+
];
|
|
1896
|
+
var defaults = {
|
|
1897
|
+
color: "system",
|
|
1898
|
+
model: "claude-sonnet-5-5",
|
|
1899
|
+
thinking: "medium",
|
|
1900
|
+
sendOnEnter: true,
|
|
1901
|
+
showThinking: true,
|
|
1902
|
+
diffStyle: "unified"
|
|
1903
|
+
};
|
|
1904
|
+
var accounts = [
|
|
1905
|
+
{ id: "anthropic", provider: "Anthropic", identity: "API key ending 3f2a", status: "connected" },
|
|
1906
|
+
{ id: "github", provider: "GitHub", identity: "@example-dev", status: "connected" },
|
|
1907
|
+
{ id: "adobe", provider: "Adobe", identity: "dev@example.com", status: "expired" },
|
|
1908
|
+
{ id: "openai", provider: "OpenAI", identity: "", status: "disconnected" }
|
|
1909
|
+
];
|
|
1910
|
+
var agents = [
|
|
1911
|
+
{
|
|
1912
|
+
id: "cone-sliccy",
|
|
1913
|
+
name: "sliccy",
|
|
1914
|
+
kind: "cone",
|
|
1915
|
+
parentId: null,
|
|
1916
|
+
status: "idle",
|
|
1917
|
+
model: "claude-sonnet-5-5",
|
|
1918
|
+
contextFill: 0.21,
|
|
1919
|
+
unread: 0
|
|
1920
|
+
},
|
|
1921
|
+
showcaseAgent,
|
|
1922
|
+
{
|
|
1923
|
+
id: "cone-harbor",
|
|
1924
|
+
name: "harbor",
|
|
1925
|
+
kind: "cone",
|
|
1926
|
+
parentId: null,
|
|
1927
|
+
status: "idle",
|
|
1928
|
+
model: "claude-opus-5-5",
|
|
1929
|
+
contextFill: 0.38,
|
|
1930
|
+
unread: 0
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
id: "scoop-otter",
|
|
1934
|
+
name: "quiet-otter",
|
|
1935
|
+
kind: "scoop",
|
|
1936
|
+
parentId: "cone-harbor",
|
|
1937
|
+
status: "working",
|
|
1938
|
+
model: "claude-haiku-4-5",
|
|
1939
|
+
contextFill: 0.12,
|
|
1940
|
+
unread: 0
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
id: "scoop-heron",
|
|
1944
|
+
name: "amber-heron",
|
|
1945
|
+
kind: "scoop",
|
|
1946
|
+
parentId: "cone-harbor",
|
|
1947
|
+
status: "idle",
|
|
1948
|
+
model: "claude-haiku-4-5",
|
|
1949
|
+
contextFill: 0.04,
|
|
1950
|
+
unread: 0
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
id: "cone-release",
|
|
1954
|
+
name: "release-notes",
|
|
1955
|
+
kind: "cone",
|
|
1956
|
+
parentId: null,
|
|
1957
|
+
status: "idle",
|
|
1958
|
+
model: "claude-sonnet-5-5",
|
|
1959
|
+
contextFill: 0.09,
|
|
1960
|
+
unread: 1
|
|
1961
|
+
},
|
|
1962
|
+
{
|
|
1963
|
+
id: "cone-triage",
|
|
1964
|
+
name: "inbox-triage",
|
|
1965
|
+
kind: "cone",
|
|
1966
|
+
parentId: null,
|
|
1967
|
+
status: "waiting",
|
|
1968
|
+
model: "claude-sonnet-5-5",
|
|
1969
|
+
contextFill: 0.62,
|
|
1970
|
+
unread: 2
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
id: "scoop-wren",
|
|
1974
|
+
name: "tidal-wren",
|
|
1975
|
+
kind: "scoop",
|
|
1976
|
+
parentId: "cone-sliccy",
|
|
1977
|
+
status: "idle",
|
|
1978
|
+
model: "claude-haiku-4-5",
|
|
1979
|
+
contextFill: 0.02,
|
|
1980
|
+
unread: 0
|
|
1981
|
+
}
|
|
1982
|
+
];
|
|
1983
|
+
var start2 = Date.UTC(2026, 9, 5, 8, 30);
|
|
1984
|
+
var at2 = (minutes) => start2 + minutes * 6e4;
|
|
1985
|
+
var queues = { "cone-kitchen": showcaseQueue() };
|
|
1986
|
+
var conversations = {
|
|
1987
|
+
"cone-kitchen": showcase(),
|
|
1988
|
+
"cone-sliccy": [
|
|
1989
|
+
{
|
|
1990
|
+
id: "m-s1",
|
|
1991
|
+
role: "user",
|
|
1992
|
+
text: "What is on my plate today?",
|
|
1993
|
+
createdAt: at2(0)
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
id: "m-s2",
|
|
1997
|
+
role: "assistant",
|
|
1998
|
+
status: "done",
|
|
1999
|
+
createdAt: at2(1),
|
|
2000
|
+
parts: [
|
|
2001
|
+
{
|
|
2002
|
+
type: "tool",
|
|
2003
|
+
tool: {
|
|
2004
|
+
id: "t-s1",
|
|
2005
|
+
name: "read_file",
|
|
2006
|
+
title: "Read the standup notes",
|
|
2007
|
+
input: "/shared/notes/2026-10-04-standup.md",
|
|
2008
|
+
output: standup,
|
|
2009
|
+
status: "done",
|
|
2010
|
+
paths: ["/shared/notes/2026-10-04-standup.md"]
|
|
2011
|
+
}
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
type: "text",
|
|
2015
|
+
text: "Three things carry over from yesterday:\n\n1. **harbor**: the retry change is in review, and the midnight cache bug is still open.\n2. **Docs**: the forecast page still shows Fahrenheit examples.\n3. **release-notes** has a draft waiting for you.\n\nThe `harbor` cone is the one to start with."
|
|
2016
|
+
}
|
|
2017
|
+
]
|
|
2018
|
+
}
|
|
2019
|
+
],
|
|
2020
|
+
"cone-harbor": [
|
|
2021
|
+
{
|
|
2022
|
+
id: "m-h1",
|
|
2023
|
+
role: "lick",
|
|
2024
|
+
channel: "webhook",
|
|
2025
|
+
title: "GitHub \xB7 issue opened",
|
|
2026
|
+
text: "example/harbor#57: Forecast stays on yesterday after midnight UTC",
|
|
2027
|
+
createdAt: at2(2)
|
|
2028
|
+
},
|
|
2029
|
+
{
|
|
2030
|
+
id: "m-h2",
|
|
2031
|
+
role: "user",
|
|
2032
|
+
text: "The forecast endpoint serves stale data after midnight UTC. Find out why and fix it.",
|
|
2033
|
+
createdAt: at2(3)
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
id: "m-h3",
|
|
2037
|
+
role: "assistant",
|
|
2038
|
+
status: "done",
|
|
2039
|
+
createdAt: at2(4),
|
|
2040
|
+
parts: [
|
|
2041
|
+
{
|
|
2042
|
+
type: "thinking",
|
|
2043
|
+
text: "Stale after midnight points at the cache expiry. Check how the cache compares days."
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
type: "tool",
|
|
2047
|
+
tool: {
|
|
2048
|
+
id: "t-h1",
|
|
2049
|
+
name: "bash",
|
|
2050
|
+
title: "Search for the cache expiry",
|
|
2051
|
+
input: 'rg -n "getDate|TTL" src',
|
|
2052
|
+
output: "src/lib/cache.ts:6:const TTL = 60 * 60 * 1000;\nsrc/lib/cache.ts:14: const day = new Date(entry.storedAt).getDate();\nsrc/lib/cache.ts:16: if (now - entry.storedAt > TTL) return undefined;",
|
|
2053
|
+
status: "done",
|
|
2054
|
+
paths: ["/workspace/harbor/src/lib/cache.ts"]
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
type: "tool",
|
|
2059
|
+
tool: {
|
|
2060
|
+
id: "t-h2",
|
|
2061
|
+
name: "read_file",
|
|
2062
|
+
title: "Read the cache",
|
|
2063
|
+
input: "/workspace/harbor/src/lib/cache.ts",
|
|
2064
|
+
output: cacheBefore,
|
|
2065
|
+
status: "done",
|
|
2066
|
+
paths: ["/workspace/harbor/src/lib/cache.ts"]
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
type: "tool",
|
|
2071
|
+
tool: {
|
|
2072
|
+
id: "t-h3",
|
|
2073
|
+
name: "edit_file",
|
|
2074
|
+
title: "Expire entries from the previous day",
|
|
2075
|
+
input: "/workspace/harbor/src/lib/cache.ts",
|
|
2076
|
+
output: "Replaced 1 block (4 lines \u2192 7 lines), added dayOf()",
|
|
2077
|
+
status: "done",
|
|
2078
|
+
paths: ["/workspace/harbor/src/lib/cache.ts"]
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
type: "tool",
|
|
2083
|
+
tool: {
|
|
2084
|
+
id: "t-h4",
|
|
2085
|
+
name: "bash",
|
|
2086
|
+
title: "Run the tests",
|
|
2087
|
+
input: "npm test",
|
|
2088
|
+
output: "> harbor@0.0.0 test\n> node --test test/\n\n\u2714 converts both ways (0.6ms)\n\u2714 expires entries from the previous day (0.4ms)\n\u2139 tests 2\n\u2139 pass 2\n\u2139 fail 0",
|
|
2089
|
+
status: "done",
|
|
2090
|
+
paths: []
|
|
2091
|
+
}
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
type: "text",
|
|
2095
|
+
text: "Found it. `ForecastCache.get` compared **local** calendar days, and when the day changed it returned the old entry instead of dropping it:\n\n```ts\nif (day !== new Date(now).getDate()) return entry.value;\n```\n\nI changed it to compare UTC dates and delete the entry, so the first request after midnight goes upstream. The tests pass.\n\n| | Before | After |\n| --- | --- | --- |\n| Day compared | local `getDate()` | UTC `toISOString()` |\n| On a new day | returns stale entry | deletes and refetches |\n\nquiet-otter is adding retries for upstream timeouts in a scoop."
|
|
2096
|
+
}
|
|
2097
|
+
]
|
|
2098
|
+
},
|
|
2099
|
+
{
|
|
2100
|
+
id: "m-h4",
|
|
2101
|
+
role: "lick",
|
|
2102
|
+
channel: "scoop-notify",
|
|
2103
|
+
title: "quiet-otter",
|
|
2104
|
+
text: "Started: add retries with backoff around the upstream fetch",
|
|
2105
|
+
createdAt: at2(9)
|
|
2106
|
+
}
|
|
2107
|
+
],
|
|
2108
|
+
"scoop-otter": [
|
|
2109
|
+
{
|
|
2110
|
+
id: "m-o1",
|
|
2111
|
+
role: "user",
|
|
2112
|
+
text: "Wrap the upstream fetch in src/routes/forecast.ts with three retries and exponential backoff.",
|
|
2113
|
+
createdAt: at2(9)
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
id: "m-o2",
|
|
2117
|
+
role: "assistant",
|
|
2118
|
+
status: "done",
|
|
2119
|
+
createdAt: at2(10),
|
|
2120
|
+
parts: [
|
|
2121
|
+
{
|
|
2122
|
+
type: "tool",
|
|
2123
|
+
tool: {
|
|
2124
|
+
id: "t-o1",
|
|
2125
|
+
name: "write_file",
|
|
2126
|
+
title: "Add retry()",
|
|
2127
|
+
input: "/workspace/harbor/src/lib/retry.ts",
|
|
2128
|
+
output: "Wrote 12 lines",
|
|
2129
|
+
status: "done",
|
|
2130
|
+
paths: ["/workspace/harbor/src/lib/retry.ts"]
|
|
2131
|
+
}
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
type: "tool",
|
|
2135
|
+
tool: {
|
|
2136
|
+
id: "t-o2",
|
|
2137
|
+
name: "edit_file",
|
|
2138
|
+
title: "Use retry() for the upstream call",
|
|
2139
|
+
input: "/workspace/harbor/src/routes/forecast.ts",
|
|
2140
|
+
output: "Replaced 1 block",
|
|
2141
|
+
status: "done",
|
|
2142
|
+
paths: ["/workspace/harbor/src/routes/forecast.ts"]
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
{ type: "text", text: "Both edits are in. Running the tests next." }
|
|
2146
|
+
]
|
|
2147
|
+
}
|
|
2148
|
+
],
|
|
2149
|
+
"scoop-heron": [],
|
|
2150
|
+
"scoop-wren": [],
|
|
2151
|
+
"cone-release": [
|
|
2152
|
+
{
|
|
2153
|
+
id: "m-r1",
|
|
2154
|
+
role: "lick",
|
|
2155
|
+
channel: "cron",
|
|
2156
|
+
title: "Every Friday 09:00",
|
|
2157
|
+
text: "Draft the release notes for example/harbor",
|
|
2158
|
+
createdAt: at2(-1440)
|
|
2159
|
+
},
|
|
2160
|
+
{
|
|
2161
|
+
id: "m-r2",
|
|
2162
|
+
role: "assistant",
|
|
2163
|
+
status: "done",
|
|
2164
|
+
createdAt: at2(-1438),
|
|
2165
|
+
parts: [
|
|
2166
|
+
{
|
|
2167
|
+
type: "tool",
|
|
2168
|
+
tool: {
|
|
2169
|
+
id: "t-r1",
|
|
2170
|
+
name: "bash",
|
|
2171
|
+
title: "Collect merged pull requests",
|
|
2172
|
+
input: "collect.jsh example/harbor v1.8.0",
|
|
2173
|
+
output: "41 fix Return 400 when the city is missing\n43 feature Add /health\n44 chore Update wrangler to 4.40.0",
|
|
2174
|
+
status: "done",
|
|
2175
|
+
paths: ["/workspace/skills/release-notes/scripts/collect.jsh"]
|
|
2176
|
+
}
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
type: "text",
|
|
2180
|
+
text: "## harbor 1.9.0\n\n**Features**\n- Added a `/health` endpoint.\n\n**Fixes**\n- Requests without a city now get a 400 instead of a 500.\n\n**Chores**\n- Updated wrangler to 4.40.0."
|
|
2181
|
+
}
|
|
2182
|
+
]
|
|
2183
|
+
}
|
|
2184
|
+
],
|
|
2185
|
+
"cone-triage": [
|
|
2186
|
+
{
|
|
2187
|
+
id: "m-t1",
|
|
2188
|
+
role: "system",
|
|
2189
|
+
kind: "compaction",
|
|
2190
|
+
trigger: "threshold",
|
|
2191
|
+
state: "summarized",
|
|
2192
|
+
title: "Context compacted",
|
|
2193
|
+
text: "41 earlier messages were summarized to stay under the context limit.",
|
|
2194
|
+
createdAt: at2(-30)
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
id: "m-t2",
|
|
2198
|
+
role: "assistant",
|
|
2199
|
+
status: "done",
|
|
2200
|
+
createdAt: at2(-20),
|
|
2201
|
+
parts: [
|
|
2202
|
+
{
|
|
2203
|
+
type: "text",
|
|
2204
|
+
text: "Two messages need a decision from you:\n\n- A vendor asks to move Thursday\u2019s call to Friday.\n- The staging certificate expires in 6 days. Should I open a ticket?"
|
|
2205
|
+
}
|
|
2206
|
+
]
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
id: "m-t3",
|
|
2210
|
+
role: "system",
|
|
2211
|
+
kind: "error",
|
|
2212
|
+
action: "login",
|
|
2213
|
+
title: "Mail connector",
|
|
2214
|
+
text: "The connector returned 401. Reconnect the account in Settings.",
|
|
2215
|
+
createdAt: at2(-19)
|
|
2216
|
+
}
|
|
2217
|
+
]
|
|
2218
|
+
};
|
|
2219
|
+
var tabs = [
|
|
2220
|
+
{
|
|
2221
|
+
id: "tab-preview",
|
|
2222
|
+
title: "harbor \xB7 localhost",
|
|
2223
|
+
url: "http://localhost:8787/forecast?city=Lisbon",
|
|
2224
|
+
status: "complete",
|
|
2225
|
+
agentId: "cone-harbor"
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
id: "tab-docs",
|
|
2229
|
+
title: "Forecast API reference",
|
|
2230
|
+
url: "https://api.example.com/docs/v2/forecast",
|
|
2231
|
+
status: "complete",
|
|
2232
|
+
agentId: null
|
|
2233
|
+
},
|
|
2234
|
+
{
|
|
2235
|
+
id: "tab-pull",
|
|
2236
|
+
title: "Add retries for upstream timeouts \xB7 Pull request #58",
|
|
2237
|
+
url: "https://git.example.com/example/harbor/pull/58",
|
|
2238
|
+
status: "complete",
|
|
2239
|
+
agentId: "scoop-otter"
|
|
2240
|
+
}
|
|
2241
|
+
];
|
|
2242
|
+
var terminals = [
|
|
2243
|
+
{ id: "term-1", title: "bash", cwd: "/workspace/harbor", agentId: null }
|
|
2244
|
+
];
|
|
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
|
+
|
|
2417
|
+
// src/dummy/settings.ts
|
|
2418
|
+
var key = "slicc-ui.settings";
|
|
2419
|
+
var DummySettings = class extends Emitter {
|
|
2420
|
+
#settings;
|
|
2421
|
+
#accounts;
|
|
2422
|
+
#models;
|
|
2423
|
+
#storage;
|
|
2424
|
+
#clock;
|
|
2425
|
+
constructor(defaults2, models2, accounts2, storage, clock2) {
|
|
2426
|
+
super();
|
|
2427
|
+
this.#storage = storage;
|
|
2428
|
+
this.#clock = clock2;
|
|
2429
|
+
this.#models = models2;
|
|
2430
|
+
this.#accounts = accounts2.map((account) => ({ ...account }));
|
|
2431
|
+
this.#settings = { ...defaults2, ...this.#load() };
|
|
2432
|
+
}
|
|
2433
|
+
#load() {
|
|
2434
|
+
try {
|
|
2435
|
+
return JSON.parse(this.#storage?.getItem(key) ?? "{}");
|
|
2436
|
+
} catch {
|
|
2437
|
+
return {};
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
get() {
|
|
2441
|
+
return { ...this.#settings };
|
|
2442
|
+
}
|
|
2443
|
+
update(patch) {
|
|
2444
|
+
this.#settings = { ...this.#settings, ...patch };
|
|
2445
|
+
this.#storage?.setItem(key, JSON.stringify(this.#settings));
|
|
2446
|
+
this.emit("settings", this.get());
|
|
2447
|
+
}
|
|
2448
|
+
models() {
|
|
2449
|
+
return this.#models;
|
|
2450
|
+
}
|
|
2451
|
+
accounts() {
|
|
2452
|
+
return this.#accounts.map((account) => ({ ...account }));
|
|
2453
|
+
}
|
|
2454
|
+
#set(id, patch) {
|
|
2455
|
+
this.#accounts = this.#accounts.map(
|
|
2456
|
+
(account) => account.id === id ? { ...account, ...patch } : account
|
|
2457
|
+
);
|
|
2458
|
+
this.emit("accounts", this.accounts());
|
|
2459
|
+
}
|
|
2460
|
+
async connect(id) {
|
|
2461
|
+
await this.#clock.sleep(10);
|
|
2462
|
+
const account = this.#accounts.find((candidate) => candidate.id === id);
|
|
2463
|
+
const identity = account?.identity || `${id}-user@example.com`;
|
|
2464
|
+
this.#set(id, { status: "connected", identity });
|
|
2465
|
+
}
|
|
2466
|
+
disconnect(id) {
|
|
2467
|
+
this.#set(id, { status: "disconnected" });
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
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
|
+
|
|
2498
|
+
// src/dummy/shell.ts
|
|
2499
|
+
var encoder = new TextEncoder();
|
|
2500
|
+
var decoder = new TextDecoder();
|
|
2501
|
+
var home = "/home/user";
|
|
2502
|
+
function resolve(cwd, target) {
|
|
2503
|
+
const path = target === "~" || target.startsWith("~/") ? home + target.slice(1) : target;
|
|
2504
|
+
const parts = (path.startsWith("/") ? path : `${cwd}/${path}`).split("/");
|
|
2505
|
+
const out = [];
|
|
2506
|
+
for (const part of parts) {
|
|
2507
|
+
if (part === "..") out.pop();
|
|
2508
|
+
else if (part && part !== ".") out.push(part);
|
|
2509
|
+
}
|
|
2510
|
+
return `/${out.join("/")}`;
|
|
2511
|
+
}
|
|
2512
|
+
function shorten(cwd) {
|
|
2513
|
+
return cwd === home || cwd.startsWith(`${home}/`) ? `~${cwd.slice(home.length)}` : cwd;
|
|
2514
|
+
}
|
|
2515
|
+
var FakeShell = class {
|
|
2516
|
+
#sink;
|
|
2517
|
+
#files;
|
|
2518
|
+
#clock;
|
|
2519
|
+
#line = "";
|
|
2520
|
+
#escape = false;
|
|
2521
|
+
#busy = false;
|
|
2522
|
+
#interrupted = false;
|
|
2523
|
+
#history = [];
|
|
2524
|
+
#commands;
|
|
2525
|
+
cwd;
|
|
2526
|
+
closed = false;
|
|
2527
|
+
constructor(sink, files3, cwd, clock2) {
|
|
2528
|
+
this.#sink = sink;
|
|
2529
|
+
this.#files = files3;
|
|
2530
|
+
this.#clock = clock2;
|
|
2531
|
+
this.cwd = cwd;
|
|
2532
|
+
this.#commands = {
|
|
2533
|
+
help: () => this.#print("Commands: cat cd clear date echo exit git help ls npm pwd whoami"),
|
|
2534
|
+
pwd: () => this.#print(this.cwd),
|
|
2535
|
+
whoami: () => this.#print("user"),
|
|
2536
|
+
date: () => this.#print((/* @__PURE__ */ new Date()).toUTCString()),
|
|
2537
|
+
echo: (args) => this.#print(args.join(" ")),
|
|
2538
|
+
clear: () => this.#out("\x1B[2J\x1B[H"),
|
|
2539
|
+
exit: () => this.#exit(),
|
|
2540
|
+
cd: (args) => this.#cd(args[0] ?? "~"),
|
|
2541
|
+
ls: (args) => this.#ls(args.filter((arg) => !arg.startsWith("-"))[0] ?? "."),
|
|
2542
|
+
cat: (args) => this.#cat(args),
|
|
2543
|
+
git: (args) => this.#git(args[0]),
|
|
2544
|
+
npm: (args) => this.#npm(args[0])
|
|
2545
|
+
};
|
|
2546
|
+
this.#prompt();
|
|
2547
|
+
}
|
|
2548
|
+
#out(text) {
|
|
2549
|
+
this.#sink.output(encoder.encode(text));
|
|
2550
|
+
}
|
|
2551
|
+
#print(text) {
|
|
2552
|
+
this.#out(`${text.replace(/\n/g, "\r\n")}\r
|
|
2553
|
+
`);
|
|
2554
|
+
}
|
|
2555
|
+
#prompt() {
|
|
2556
|
+
this.#out(`\x1B[1;32muser@slicc\x1B[0m:\x1B[1;34m${shorten(this.cwd)}\x1B[0m$ `);
|
|
2557
|
+
}
|
|
2558
|
+
#exit() {
|
|
2559
|
+
this.closed = true;
|
|
2560
|
+
this.#sink.exit(0);
|
|
2561
|
+
}
|
|
2562
|
+
async #entries() {
|
|
2563
|
+
return new Map((await this.#files.list()).map((entry) => [entry.path, entry.kind]));
|
|
2564
|
+
}
|
|
2565
|
+
async #cd(target) {
|
|
2566
|
+
const path = resolve(this.cwd, target);
|
|
2567
|
+
if (path === "/" || (await this.#entries()).get(path) === "directory") this.cwd = path;
|
|
2568
|
+
else this.#print(`bash: cd: ${target}: No such file or directory`);
|
|
2569
|
+
}
|
|
2570
|
+
async #ls(target) {
|
|
2571
|
+
const path = resolve(this.cwd, target);
|
|
2572
|
+
const entries = await this.#entries();
|
|
2573
|
+
const prefix = path === "/" ? "/" : `${path}/`;
|
|
2574
|
+
const names = [...entries].filter(
|
|
2575
|
+
([candidate]) => candidate.startsWith(prefix) && !candidate.slice(prefix.length).includes("/")
|
|
2576
|
+
).map(([candidate, kind]) => {
|
|
2577
|
+
const name = candidate.slice(prefix.length);
|
|
2578
|
+
return kind === "directory" ? `\x1B[1;34m${name}\x1B[0m` : name;
|
|
2579
|
+
});
|
|
2580
|
+
if (path !== "/" && !entries.has(path))
|
|
2581
|
+
this.#print(`ls: cannot access '${target}': No such file or directory`);
|
|
2582
|
+
else if (names.length > 0) this.#print(names.join(" "));
|
|
2583
|
+
}
|
|
2584
|
+
async #cat(args) {
|
|
2585
|
+
for (const arg of args) {
|
|
2586
|
+
const text = await this.#files.read(resolve(this.cwd, arg)).catch(() => null);
|
|
2587
|
+
if (text === null) this.#print(`cat: ${arg}: No such file or directory`);
|
|
2588
|
+
else this.#out(text.replace(/\n/g, "\r\n"));
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
#git(sub) {
|
|
2592
|
+
if (sub !== "status") {
|
|
2593
|
+
this.#print("usage: git status");
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
const changes = this.#files.changes();
|
|
2597
|
+
this.#print("On branch main");
|
|
2598
|
+
if (changes.length === 0) {
|
|
2599
|
+
this.#print("nothing to commit, working tree clean");
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
this.#print("Changes not staged for commit:");
|
|
2603
|
+
const colors = { added: 32, modified: 33, deleted: 31 };
|
|
2604
|
+
for (const change of changes) {
|
|
2605
|
+
this.#print(` \x1B[${colors[change.status]}m${change.status}: ${change.path}\x1B[0m`);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
async #npm(sub) {
|
|
2609
|
+
if (sub !== "test") {
|
|
2610
|
+
this.#print("usage: npm test");
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
const lines = [
|
|
2614
|
+
"> harbor@0.0.0 test",
|
|
2615
|
+
"> node --test test/",
|
|
2616
|
+
"",
|
|
2617
|
+
"\x1B[32m\u2714\x1B[0m converts both ways \x1B[90m(0.6ms)\x1B[0m",
|
|
2618
|
+
"\x1B[32m\u2714\x1B[0m expires entries from the previous day \x1B[90m(0.4ms)\x1B[0m",
|
|
2619
|
+
"\x1B[32m\u2714\x1B[0m retries upstream timeouts \x1B[90m(3.1ms)\x1B[0m",
|
|
2620
|
+
"\u2139 tests 3",
|
|
2621
|
+
"\u2139 pass 3",
|
|
2622
|
+
"\u2139 fail 0"
|
|
2623
|
+
];
|
|
2624
|
+
for (const line of lines) {
|
|
2625
|
+
await this.#clock.sleep(2);
|
|
2626
|
+
if (this.closed || this.#interrupted) return;
|
|
2627
|
+
this.#print(line);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
async #run(line) {
|
|
2631
|
+
const [name, ...args] = line.trim().split(/\s+/);
|
|
2632
|
+
if (!name) return;
|
|
2633
|
+
this.#history.push(line);
|
|
2634
|
+
const command = this.#commands[name];
|
|
2635
|
+
if (command) await command(args);
|
|
2636
|
+
else this.#print(`bash: ${name}: command not found`);
|
|
2637
|
+
}
|
|
2638
|
+
#key(char) {
|
|
2639
|
+
if (this.#escape) {
|
|
2640
|
+
this.#escape = !/[A-Za-z~]/.test(char) || char === "[";
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
if (char === "\x1B") this.#escape = true;
|
|
2644
|
+
else if (char === "\r") this.#enter();
|
|
2645
|
+
else if (char === "\x7F") this.#backspace();
|
|
2646
|
+
else if (char === "") this.signal("SIGINT");
|
|
2647
|
+
else if (char === "\f") {
|
|
2648
|
+
this.#out("\x1B[2J\x1B[H");
|
|
2649
|
+
this.#prompt();
|
|
2650
|
+
this.#out(this.#line);
|
|
2651
|
+
} else if (char >= " ") {
|
|
2652
|
+
this.#line += char;
|
|
2653
|
+
this.#out(char);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
#backspace() {
|
|
2657
|
+
if (!this.#line) return;
|
|
2658
|
+
this.#line = [...this.#line].slice(0, -1).join("");
|
|
2659
|
+
this.#out("\b \b");
|
|
2660
|
+
}
|
|
2661
|
+
#enter() {
|
|
2662
|
+
const line = this.#line;
|
|
2663
|
+
this.#line = "";
|
|
2664
|
+
this.#out("\r\n");
|
|
2665
|
+
this.#busy = true;
|
|
2666
|
+
this.#interrupted = false;
|
|
2667
|
+
void this.#run(line).then(() => {
|
|
2668
|
+
this.#busy = false;
|
|
2669
|
+
if (!this.closed) this.#prompt();
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
history() {
|
|
2673
|
+
return this.#history;
|
|
2674
|
+
}
|
|
2675
|
+
write(data) {
|
|
2676
|
+
if (this.closed) return;
|
|
2677
|
+
const text = decoder.decode(data);
|
|
2678
|
+
if (!this.#busy) for (const char of text) this.#key(char);
|
|
2679
|
+
else if (text.includes("")) this.signal("SIGINT");
|
|
2680
|
+
}
|
|
2681
|
+
resize() {
|
|
2682
|
+
}
|
|
2683
|
+
signal(name) {
|
|
2684
|
+
if (name === "SIGHUP") {
|
|
2685
|
+
this.#exit();
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
this.#line = "";
|
|
2689
|
+
this.#interrupted = this.#busy;
|
|
2690
|
+
this.#out("^C\r\n");
|
|
2691
|
+
if (!this.#busy) this.#prompt();
|
|
2692
|
+
}
|
|
2693
|
+
close() {
|
|
2694
|
+
this.closed = true;
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
function shellBackend(files3, cwd, clock2) {
|
|
2698
|
+
return { open: (sink) => new FakeShell(sink, files3, cwd, clock2) };
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/dummy/terminals.ts
|
|
2702
|
+
var DummyTerminals = class extends Emitter {
|
|
2703
|
+
#terminals;
|
|
2704
|
+
#backends = /* @__PURE__ */ new Map();
|
|
2705
|
+
#files;
|
|
2706
|
+
#clock;
|
|
2707
|
+
#next;
|
|
2708
|
+
constructor(terminals2, files3, clock2) {
|
|
2709
|
+
super();
|
|
2710
|
+
this.#files = files3;
|
|
2711
|
+
this.#clock = clock2;
|
|
2712
|
+
this.#terminals = terminals2.map((terminal) => ({ ...terminal }));
|
|
2713
|
+
this.#next = this.#terminals.length + 1;
|
|
2714
|
+
}
|
|
2715
|
+
list() {
|
|
2716
|
+
return this.#terminals.map((terminal) => ({ ...terminal }));
|
|
2717
|
+
}
|
|
2718
|
+
open({
|
|
2719
|
+
cwd = "/workspace/harbor",
|
|
2720
|
+
agentId = null
|
|
2721
|
+
} = {}) {
|
|
2722
|
+
const terminal = {
|
|
2723
|
+
id: `term-${this.#next}`,
|
|
2724
|
+
title: `bash ${this.#next}`,
|
|
2725
|
+
cwd,
|
|
2726
|
+
agentId
|
|
2727
|
+
};
|
|
2728
|
+
this.#next += 1;
|
|
2729
|
+
this.#terminals.push(terminal);
|
|
2730
|
+
this.emit("terminals", this.list());
|
|
2731
|
+
return { ...terminal };
|
|
2732
|
+
}
|
|
2733
|
+
close(id) {
|
|
2734
|
+
this.#terminals = this.#terminals.filter((terminal) => terminal.id !== id);
|
|
2735
|
+
this.#backends.delete(id);
|
|
2736
|
+
this.emit("terminals", this.list());
|
|
2737
|
+
}
|
|
2738
|
+
backend(id) {
|
|
2739
|
+
const terminal = this.#terminals.find((candidate) => candidate.id === id);
|
|
2740
|
+
if (!terminal) throw new Error(`No terminal ${id}`);
|
|
2741
|
+
let backend = this.#backends.get(id);
|
|
2742
|
+
if (!backend) {
|
|
2743
|
+
backend = shellBackend(this.#files, terminal.cwd, this.#clock);
|
|
2744
|
+
this.#backends.set(id, backend);
|
|
2745
|
+
}
|
|
2746
|
+
return backend;
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
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
|
+
|
|
2777
|
+
// src/dummy/index.ts
|
|
2778
|
+
function createDummyModel({ delay = 30, storage = null } = {}) {
|
|
2779
|
+
const time = clock(delay);
|
|
2780
|
+
const files3 = new DummyFiles(files2, directories, pending, time);
|
|
2781
|
+
const browser = new DummyBrowser(tabs, 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,
|
|
2792
|
+
files: files3,
|
|
2793
|
+
browser,
|
|
2794
|
+
terminals: new DummyTerminals(terminals, files3, time),
|
|
2795
|
+
settings: new DummySettings(
|
|
2796
|
+
defaults,
|
|
2797
|
+
models,
|
|
2798
|
+
accounts,
|
|
2799
|
+
storage,
|
|
2800
|
+
time
|
|
2801
|
+
),
|
|
2802
|
+
memory: new DummyMemory(memories),
|
|
2803
|
+
sprinkles: new DummySprinkles(sprinkles, agent),
|
|
2804
|
+
tray: new DummyTray(tray, time)
|
|
2805
|
+
};
|
|
2806
|
+
return { ...base, monitor: new DummyMonitor(base) };
|
|
2807
|
+
}
|
|
2808
|
+
export {
|
|
2809
|
+
createDummyModel
|
|
2810
|
+
};
|
|
2811
|
+
//# sourceMappingURL=slicc-dummy.js.map
|