@luckydraw/cumulus 0.31.65 → 1.0.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/CHANGELOG.md +6 -555
- package/LICENSE +150 -0
- package/README.md +161 -17
- package/dist/gateway/adapters/webchat.d.ts +2 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +22 -2
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/config.d.ts +17 -2
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +10 -3
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/daemon.d.ts +3 -1
- package/dist/gateway/daemon.d.ts.map +1 -1
- package/dist/gateway/daemon.js +128 -39
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/namespaces.d.ts +34 -0
- package/dist/gateway/namespaces.d.ts.map +1 -1
- package/dist/gateway/namespaces.js +58 -0
- package/dist/gateway/namespaces.js.map +1 -1
- package/dist/gateway/server.d.ts +8 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +150 -41
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/setup.d.ts +32 -0
- package/dist/gateway/setup.d.ts.map +1 -1
- package/dist/gateway/setup.js +23 -3
- package/dist/gateway/setup.js.map +1 -1
- package/dist/gateway/static/widget.js +897 -611
- package/dist/lib/gateway.d.ts +30 -8
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +36 -11
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/history.d.ts +22 -0
- package/dist/lib/history.d.ts.map +1 -1
- package/dist/lib/history.js +59 -21
- package/dist/lib/history.js.map +1 -1
- package/dist/lib/huggingface-provider.d.ts.map +1 -1
- package/dist/lib/huggingface-provider.js +11 -3
- package/dist/lib/huggingface-provider.js.map +1 -1
- package/dist/lib/license.d.ts +76 -0
- package/dist/lib/license.d.ts.map +1 -0
- package/dist/lib/license.js +141 -0
- package/dist/lib/license.js.map +1 -0
- package/docs/agentic-harness-primer.md +283 -0
- package/docs/conditional-continuation.md +167 -0
- package/docs/web-app-agent-guide.md +520 -0
- package/examples/web-app-agent/README.md +187 -0
- package/examples/web-app-agent/agent/mcp-shim.js +105 -0
- package/examples/web-app-agent/gateway.config.example.json +52 -0
- package/examples/web-app-agent/package.json +13 -0
- package/examples/web-app-agent/public/agent/bridge-mount.js +75 -0
- package/examples/web-app-agent/public/agent/chat-client.js +104 -0
- package/examples/web-app-agent/public/agent/commands.js +250 -0
- package/examples/web-app-agent/public/agent/device-thread.js +48 -0
- package/examples/web-app-agent/public/agent/panel.css +107 -0
- package/examples/web-app-agent/public/agent/panel.js +369 -0
- package/examples/web-app-agent/public/app.js +250 -0
- package/examples/web-app-agent/public/index.html +111 -0
- package/examples/web-app-agent/server.js +242 -0
- package/package.json +7 -3
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/* The chat panel — a slim bar pinned bottom-center that grows upward into a
|
|
2
|
+
chat window. Vanilla JS, no framework, themed entirely through the app's own
|
|
3
|
+
CSS variables so it follows light/dark for free.
|
|
4
|
+
|
|
5
|
+
Mounted in its OWN root (#agent-root), a sibling of the app's DOM. Nothing
|
|
6
|
+
the app re-renders can unmount it. If you use React/Vue/Svelte, this is the
|
|
7
|
+
one piece to rewrite in your framework — the other files stay as they are.
|
|
8
|
+
|
|
9
|
+
window.AgentPanel = { init(), setState(s), confirm(req), notify(text), reset() } */
|
|
10
|
+
(function () {
|
|
11
|
+
'use strict';
|
|
12
|
+
var LS_KEY = 'demoapp-agent-v1';
|
|
13
|
+
var root, barEl, winEl, logEl, inputEl, dotEl;
|
|
14
|
+
var prefs = { expanded: false, draft: '' };
|
|
15
|
+
var streaming = false;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
Object.assign(prefs, JSON.parse(localStorage.getItem(LS_KEY) || '{}'));
|
|
19
|
+
} catch {
|
|
20
|
+
/* no stored prefs, or storage unavailable — defaults stand */
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function savePrefs() {
|
|
24
|
+
try {
|
|
25
|
+
localStorage.setItem(
|
|
26
|
+
LS_KEY,
|
|
27
|
+
JSON.stringify({ expanded: prefs.expanded, draft: inputEl ? inputEl.value : prefs.draft })
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
/* storage full or unavailable — prefs just don't persist */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ---- minimal markdown (code -> link -> bold -> italic) ------------------ */
|
|
35
|
+
function esc(s) {
|
|
36
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function inline(s) {
|
|
40
|
+
var out = '';
|
|
41
|
+
// tokenize inline code first so nothing inside it is touched
|
|
42
|
+
s.split(/(`[^`]*`)/).forEach(function (part) {
|
|
43
|
+
if (part.charAt(0) === '`' && part.charAt(part.length - 1) === '`' && part.length > 1) {
|
|
44
|
+
out += '<code>' + esc(part.slice(1, -1)) + '</code>';
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
var t = esc(part);
|
|
48
|
+
t = t.replace(
|
|
49
|
+
/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,
|
|
50
|
+
'<a href="$2" target="_blank" rel="noopener">$1</a>'
|
|
51
|
+
);
|
|
52
|
+
t = t.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
|
|
53
|
+
t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<i>$2</i>');
|
|
54
|
+
out += t;
|
|
55
|
+
});
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function splitCells(line) {
|
|
60
|
+
// split on unescaped pipes only — models emit \| inside table cells
|
|
61
|
+
var cells = [],
|
|
62
|
+
cur = '';
|
|
63
|
+
for (var i = 0; i < line.length; i++) {
|
|
64
|
+
var ch = line.charAt(i);
|
|
65
|
+
if (ch === '\\' && line.charAt(i + 1) === '|') {
|
|
66
|
+
cur += '\\|';
|
|
67
|
+
i++;
|
|
68
|
+
} else if (ch === '|') {
|
|
69
|
+
cells.push(cur);
|
|
70
|
+
cur = '';
|
|
71
|
+
} else cur += ch;
|
|
72
|
+
}
|
|
73
|
+
cells.push(cur);
|
|
74
|
+
if (cells.length && !cells[0].trim()) cells.shift();
|
|
75
|
+
if (cells.length && !cells[cells.length - 1].trim()) cells.pop();
|
|
76
|
+
return cells.map(function (c) {
|
|
77
|
+
return c.trim();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderMarkdown(text) {
|
|
82
|
+
var lines = String(text).split('\n');
|
|
83
|
+
var html = '',
|
|
84
|
+
i = 0;
|
|
85
|
+
while (i < lines.length) {
|
|
86
|
+
var line = lines[i];
|
|
87
|
+
if (/^```/.test(line)) {
|
|
88
|
+
var code = [];
|
|
89
|
+
i++;
|
|
90
|
+
while (i < lines.length && !/^```/.test(lines[i])) code.push(lines[i++]);
|
|
91
|
+
i++;
|
|
92
|
+
html += '<pre>' + esc(code.join('\n')) + '</pre>';
|
|
93
|
+
} else if (
|
|
94
|
+
/^\s*\|.*\|/.test(line) ||
|
|
95
|
+
(/\|/.test(line) && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1] || ''))
|
|
96
|
+
) {
|
|
97
|
+
var rows = [];
|
|
98
|
+
while (i < lines.length && /\|/.test(lines[i])) rows.push(lines[i++]);
|
|
99
|
+
if (rows.length >= 2 && /^[\s:|-]+$/.test(rows[1])) {
|
|
100
|
+
html +=
|
|
101
|
+
'<table><thead><tr>' +
|
|
102
|
+
splitCells(rows[0])
|
|
103
|
+
.map(function (c) {
|
|
104
|
+
return '<th>' + inline(c) + '</th>';
|
|
105
|
+
})
|
|
106
|
+
.join('') +
|
|
107
|
+
'</tr></thead><tbody>';
|
|
108
|
+
rows.slice(2).forEach(function (r) {
|
|
109
|
+
html +=
|
|
110
|
+
'<tr>' +
|
|
111
|
+
splitCells(r)
|
|
112
|
+
.map(function (c) {
|
|
113
|
+
return '<td>' + inline(c) + '</td>';
|
|
114
|
+
})
|
|
115
|
+
.join('') +
|
|
116
|
+
'</tr>';
|
|
117
|
+
});
|
|
118
|
+
html += '</tbody></table>';
|
|
119
|
+
} else {
|
|
120
|
+
rows.forEach(function (r) {
|
|
121
|
+
html += '<p>' + inline(r) + '</p>';
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} else if (/^\s*[-*] /.test(line)) {
|
|
125
|
+
html += '<ul>';
|
|
126
|
+
while (i < lines.length && /^\s*[-*] /.test(lines[i]))
|
|
127
|
+
html += '<li>' + inline(lines[i++].replace(/^\s*[-*] /, '')) + '</li>';
|
|
128
|
+
html += '</ul>';
|
|
129
|
+
} else if (/^#{1,4} /.test(line)) {
|
|
130
|
+
html += '<h4>' + inline(line.replace(/^#{1,4} /, '')) + '</h4>';
|
|
131
|
+
i++;
|
|
132
|
+
} else if (!line.trim()) {
|
|
133
|
+
i++;
|
|
134
|
+
} else {
|
|
135
|
+
var para = [];
|
|
136
|
+
while (
|
|
137
|
+
i < lines.length &&
|
|
138
|
+
lines[i].trim() &&
|
|
139
|
+
!/^(```|#{1,4} |\s*[-*] )/.test(lines[i]) &&
|
|
140
|
+
!/\|.*\|/.test(lines[i])
|
|
141
|
+
)
|
|
142
|
+
para.push(lines[i++]);
|
|
143
|
+
html += '<p>' + para.map(inline).join('<br>') + '</p>';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return html;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ---- DOM --------------------------------------------------------------- */
|
|
150
|
+
function el(tag, cls, parent) {
|
|
151
|
+
var e = document.createElement(tag);
|
|
152
|
+
if (cls) e.className = cls;
|
|
153
|
+
if (parent) parent.appendChild(e);
|
|
154
|
+
return e;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setExpanded(on) {
|
|
158
|
+
prefs.expanded = on;
|
|
159
|
+
root.classList.toggle('open', on);
|
|
160
|
+
savePrefs();
|
|
161
|
+
if (on) {
|
|
162
|
+
inputEl.focus();
|
|
163
|
+
scrollLog();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function scrollLog() {
|
|
168
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function addMsg(role, content, asMarkdown) {
|
|
172
|
+
var m = el('div', 'agmsg ' + role, logEl);
|
|
173
|
+
if (asMarkdown) m.innerHTML = renderMarkdown(content);
|
|
174
|
+
else m.textContent = content;
|
|
175
|
+
scrollLog();
|
|
176
|
+
return m;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function setLoading(on) {
|
|
180
|
+
// House rule: async containers expose their state so browser tests never
|
|
181
|
+
// have to guess. See RULES.md "Explicit Loading States".
|
|
182
|
+
logEl.setAttribute('data-loading', on ? 'true' : 'false');
|
|
183
|
+
logEl.setAttribute('aria-busy', on ? 'true' : 'false');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ---- sending ------------------------------------------------------------ */
|
|
187
|
+
function send() {
|
|
188
|
+
var text = inputEl.value.trim();
|
|
189
|
+
if (!text || streaming || !window.AgentChat || !window.AgentChat.live) return;
|
|
190
|
+
inputEl.value = '';
|
|
191
|
+
savePrefs();
|
|
192
|
+
setExpanded(true);
|
|
193
|
+
addMsg('user', text);
|
|
194
|
+
|
|
195
|
+
// A fresh describeView rides along with every turn. This is the whole
|
|
196
|
+
// reason the agent knows what the human is looking at.
|
|
197
|
+
if (window.AgentBridge) {
|
|
198
|
+
try {
|
|
199
|
+
window.AgentBridge.sendContext();
|
|
200
|
+
} catch {
|
|
201
|
+
/* bridge not mounted yet — the next context push will carry it */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
streaming = true;
|
|
206
|
+
setLoading(true);
|
|
207
|
+
var activity = el('div', 'agactivity', logEl);
|
|
208
|
+
activity.textContent = 'thinking…';
|
|
209
|
+
var bubble = addMsg('assistant', '');
|
|
210
|
+
var acc = '';
|
|
211
|
+
|
|
212
|
+
window.AgentChat.send(text, {
|
|
213
|
+
onToken: function (t) {
|
|
214
|
+
acc += t;
|
|
215
|
+
bubble.textContent = acc;
|
|
216
|
+
scrollLog();
|
|
217
|
+
},
|
|
218
|
+
// Tool activity is liveness. A turn can run for minutes with no text
|
|
219
|
+
// tokens at all while the model works — show that, or it reads as hung.
|
|
220
|
+
onSegment: function (seg) {
|
|
221
|
+
if (!seg || !seg.type) return;
|
|
222
|
+
if (seg.type === 'thinking') activity.textContent = 'thinking…';
|
|
223
|
+
else if (seg.type === 'tool_use')
|
|
224
|
+
activity.textContent = '⚙ ' + (seg.name || seg.tool || 'tool') + '…';
|
|
225
|
+
else if (seg.type === 'tool_result') activity.textContent = '⚙ done, composing…';
|
|
226
|
+
},
|
|
227
|
+
onError: function (err) {
|
|
228
|
+
streaming = false;
|
|
229
|
+
setLoading(false);
|
|
230
|
+
activity.remove();
|
|
231
|
+
bubble.className = 'agmsg error';
|
|
232
|
+
bubble.textContent = '✕ ' + ((err && (err.error || err.message)) || 'agent unavailable');
|
|
233
|
+
},
|
|
234
|
+
onDone: function () {
|
|
235
|
+
streaming = false;
|
|
236
|
+
setLoading(false);
|
|
237
|
+
activity.remove();
|
|
238
|
+
bubble.innerHTML = renderMarkdown(acc);
|
|
239
|
+
scrollLog();
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* ---- public ------------------------------------------------------------- */
|
|
245
|
+
window.AgentPanel = {
|
|
246
|
+
init: function () {
|
|
247
|
+
if (root) return;
|
|
248
|
+
root = el('div', null, document.body);
|
|
249
|
+
root.id = 'agent-root';
|
|
250
|
+
|
|
251
|
+
// minimized home bar
|
|
252
|
+
barEl = el('div', 'agbar', root);
|
|
253
|
+
barEl.setAttribute('data-testid', 'agent-bar');
|
|
254
|
+
el('span', 'agstar', barEl).textContent = '✦';
|
|
255
|
+
var barInput = el('input', 'agbarin', barEl);
|
|
256
|
+
barInput.placeholder = 'Ask about your notes…';
|
|
257
|
+
barInput.setAttribute('data-testid', 'agent-bar-input');
|
|
258
|
+
barInput.addEventListener('focus', function () {
|
|
259
|
+
setExpanded(true);
|
|
260
|
+
inputEl.value = barInput.value || inputEl.value;
|
|
261
|
+
barInput.value = '';
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// expanded window
|
|
265
|
+
winEl = el('div', 'agwin', root);
|
|
266
|
+
winEl.setAttribute('data-testid', 'agent-panel');
|
|
267
|
+
var head = el('div', 'aghead', winEl);
|
|
268
|
+
el('span', 'agstar', head).textContent = '✦';
|
|
269
|
+
el('span', 'agtitle', head).textContent = 'Assistant';
|
|
270
|
+
dotEl = el('span', 'agdot closed', head);
|
|
271
|
+
dotEl.title = 'bridge: connecting';
|
|
272
|
+
dotEl.setAttribute('data-testid', 'agent-bridge-state');
|
|
273
|
+
var min = el('button', 'agmin', head);
|
|
274
|
+
min.textContent = '─';
|
|
275
|
+
min.title = 'Minimize';
|
|
276
|
+
min.setAttribute('data-testid', 'agent-collapse');
|
|
277
|
+
min.onclick = function () {
|
|
278
|
+
setExpanded(false);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
logEl = el('div', 'aglog', winEl);
|
|
282
|
+
logEl.setAttribute('data-testid', 'agent-log');
|
|
283
|
+
setLoading(false);
|
|
284
|
+
|
|
285
|
+
var inrow = el('div', 'aginrow', winEl);
|
|
286
|
+
inputEl = el('textarea', 'again', inrow);
|
|
287
|
+
inputEl.rows = 2;
|
|
288
|
+
inputEl.placeholder = 'Ask about your notes, or tell me what to change…';
|
|
289
|
+
inputEl.setAttribute('data-testid', 'agent-input');
|
|
290
|
+
inputEl.value = prefs.draft || '';
|
|
291
|
+
var sendBtn = el('button', 'agsend', inrow);
|
|
292
|
+
sendBtn.textContent = '↑';
|
|
293
|
+
sendBtn.title = 'Send';
|
|
294
|
+
sendBtn.setAttribute('data-testid', 'agent-send');
|
|
295
|
+
sendBtn.onclick = send;
|
|
296
|
+
|
|
297
|
+
inputEl.addEventListener('keydown', function (ev) {
|
|
298
|
+
if (ev.key === 'Enter' && !ev.shiftKey) {
|
|
299
|
+
ev.preventDefault();
|
|
300
|
+
send();
|
|
301
|
+
}
|
|
302
|
+
if (ev.key === 'Escape') setExpanded(false);
|
|
303
|
+
});
|
|
304
|
+
inputEl.addEventListener('input', savePrefs);
|
|
305
|
+
|
|
306
|
+
if (prefs.expanded) setExpanded(true);
|
|
307
|
+
|
|
308
|
+
// reload survival — the thread outlives the tab, so replay prior turns
|
|
309
|
+
if (window.AgentChat && window.AgentChat.live) {
|
|
310
|
+
window.AgentChat.history().then(function (msgs) {
|
|
311
|
+
(msgs || []).slice(-50).forEach(function (m) {
|
|
312
|
+
var role = m.role === 'user' ? 'user' : 'assistant';
|
|
313
|
+
var text = typeof m.content === 'string' ? m.content : m.text || '';
|
|
314
|
+
if (text) addMsg(role, text, role === 'assistant');
|
|
315
|
+
});
|
|
316
|
+
scrollLog();
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
setState: function (s) {
|
|
322
|
+
if (!dotEl) return;
|
|
323
|
+
dotEl.className = 'agdot ' + s;
|
|
324
|
+
dotEl.title = 'bridge: ' + s;
|
|
325
|
+
dotEl.setAttribute('data-state', s);
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/* Export-tier confirm chip — the human in the loop. */
|
|
329
|
+
confirm: function (req) {
|
|
330
|
+
window.AgentPanel.init();
|
|
331
|
+
setExpanded(true);
|
|
332
|
+
var chip = el('div', 'agconfirm', logEl);
|
|
333
|
+
chip.setAttribute('data-testid', 'agent-confirm');
|
|
334
|
+
el('div', 'agconfirmtext', chip).textContent = req.summary || req.command + ' wants to run';
|
|
335
|
+
var row = el('div', 'agconfirmrow', chip);
|
|
336
|
+
var ok = el('button', 'agbtn primary', row);
|
|
337
|
+
ok.textContent = 'Allow';
|
|
338
|
+
ok.setAttribute('data-testid', 'agent-confirm-accept');
|
|
339
|
+
var no = el('button', 'agbtn', row);
|
|
340
|
+
no.textContent = 'Decline';
|
|
341
|
+
no.setAttribute('data-testid', 'agent-confirm-decline');
|
|
342
|
+
function settle(label) {
|
|
343
|
+
row.remove();
|
|
344
|
+
el('div', 'agconfirmdone', chip).textContent = label;
|
|
345
|
+
}
|
|
346
|
+
ok.onclick = function () {
|
|
347
|
+
settle('✓ allowed');
|
|
348
|
+
req.accept();
|
|
349
|
+
};
|
|
350
|
+
no.onclick = function () {
|
|
351
|
+
settle('✕ declined');
|
|
352
|
+
req.decline();
|
|
353
|
+
};
|
|
354
|
+
scrollLog();
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
notify: function (text) {
|
|
358
|
+
window.AgentPanel.init();
|
|
359
|
+
addMsg('system', text);
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/* logout teardown — lets a later login re-init cleanly */
|
|
363
|
+
reset: function () {
|
|
364
|
+
if (root && root.parentElement) root.remove();
|
|
365
|
+
root = barEl = winEl = logEl = inputEl = dotEl = null;
|
|
366
|
+
streaming = false;
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
})();
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/* The demo app itself — a notes list in localStorage.
|
|
2
|
+
Nothing here is agent-specific EXCEPT window.HostApp and the login flow that
|
|
3
|
+
fetches the agent config.
|
|
4
|
+
|
|
5
|
+
window.HostApp is the adapter: the app's own actions, exposed as functions.
|
|
6
|
+
The command registry calls these, never the DOM. That is what keeps
|
|
7
|
+
validation, persistence, and re-render working identically whether a human
|
|
8
|
+
or the model is driving — and it means your commands survive a UI rewrite. */
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
var STORE = 'demoapp.notes';
|
|
13
|
+
var state = { notes: [], filter: '' };
|
|
14
|
+
|
|
15
|
+
function load() {
|
|
16
|
+
try {
|
|
17
|
+
var raw = JSON.parse(localStorage.getItem(STORE) || 'null');
|
|
18
|
+
if (raw && Array.isArray(raw.notes)) state.notes = raw.notes;
|
|
19
|
+
} catch {
|
|
20
|
+
/* nothing stored, or storage unavailable — seed the demo notes below */
|
|
21
|
+
}
|
|
22
|
+
if (!state.notes.length) {
|
|
23
|
+
state.notes = [
|
|
24
|
+
{
|
|
25
|
+
id: 1,
|
|
26
|
+
text: 'Read the README in this folder',
|
|
27
|
+
done: false,
|
|
28
|
+
created: new Date().toISOString(),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 2,
|
|
32
|
+
text: 'Ask the assistant what is on this list',
|
|
33
|
+
done: false,
|
|
34
|
+
created: new Date().toISOString(),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 3,
|
|
38
|
+
text: 'Set up the gateway namespace',
|
|
39
|
+
done: true,
|
|
40
|
+
created: new Date().toISOString(),
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function persist() {
|
|
47
|
+
try {
|
|
48
|
+
localStorage.setItem(STORE, JSON.stringify({ notes: state.notes }));
|
|
49
|
+
} catch {
|
|
50
|
+
/* storage full or unavailable — notes stay in memory for this session */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nextId() {
|
|
55
|
+
return (
|
|
56
|
+
state.notes.reduce(function (m, n) {
|
|
57
|
+
return Math.max(m, n.id);
|
|
58
|
+
}, 0) + 1
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function visibleNotes() {
|
|
63
|
+
var f = state.filter.trim().toLowerCase();
|
|
64
|
+
if (!f) return state.notes.slice();
|
|
65
|
+
return state.notes.filter(function (n) {
|
|
66
|
+
return n.text.toLowerCase().indexOf(f) >= 0;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ---- render ------------------------------------------------------------- */
|
|
71
|
+
function render() {
|
|
72
|
+
var list = document.getElementById('note-list');
|
|
73
|
+
if (!list) return;
|
|
74
|
+
var notes = visibleNotes();
|
|
75
|
+
list.innerHTML = '';
|
|
76
|
+
if (!notes.length) {
|
|
77
|
+
var empty = document.createElement('li');
|
|
78
|
+
empty.className = 'empty';
|
|
79
|
+
empty.textContent = state.filter ? 'No notes match "' + state.filter + '".' : 'No notes yet.';
|
|
80
|
+
empty.setAttribute('data-testid', 'note-list-empty');
|
|
81
|
+
list.appendChild(empty);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
notes.forEach(function (n) {
|
|
85
|
+
var li = document.createElement('li');
|
|
86
|
+
if (n.done) li.className = 'done';
|
|
87
|
+
li.setAttribute('data-testid', 'note-' + n.id);
|
|
88
|
+
|
|
89
|
+
var box = document.createElement('input');
|
|
90
|
+
box.type = 'checkbox';
|
|
91
|
+
box.checked = !!n.done;
|
|
92
|
+
box.setAttribute('data-testid', 'note-toggle-' + n.id);
|
|
93
|
+
box.onchange = function () {
|
|
94
|
+
HostApp.setDone(n.id, box.checked);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
var text = document.createElement('span');
|
|
98
|
+
text.className = 'text';
|
|
99
|
+
text.textContent = n.text;
|
|
100
|
+
|
|
101
|
+
var id = document.createElement('span');
|
|
102
|
+
id.className = 'id';
|
|
103
|
+
id.textContent = '#' + n.id;
|
|
104
|
+
|
|
105
|
+
li.appendChild(box);
|
|
106
|
+
li.appendChild(text);
|
|
107
|
+
li.appendChild(id);
|
|
108
|
+
list.appendChild(li);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ---- the adapter the agent drives --------------------------------------- */
|
|
113
|
+
var HostApp = {
|
|
114
|
+
getState: function () {
|
|
115
|
+
return { notes: state.notes.slice(), filter: state.filter };
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
visibleNotes: visibleNotes,
|
|
119
|
+
|
|
120
|
+
setFilter: function (text) {
|
|
121
|
+
state.filter = String(text || '');
|
|
122
|
+
var box = document.getElementById('filter');
|
|
123
|
+
if (box) box.value = state.filter;
|
|
124
|
+
render();
|
|
125
|
+
return state.filter;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
createNote: function (text) {
|
|
129
|
+
var note = {
|
|
130
|
+
id: nextId(),
|
|
131
|
+
text: String(text),
|
|
132
|
+
done: false,
|
|
133
|
+
created: new Date().toISOString(),
|
|
134
|
+
};
|
|
135
|
+
state.notes.push(note);
|
|
136
|
+
persist();
|
|
137
|
+
render();
|
|
138
|
+
return note;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
setDone: function (id, done) {
|
|
142
|
+
var note = state.notes.find(function (n) {
|
|
143
|
+
return n.id === Number(id);
|
|
144
|
+
});
|
|
145
|
+
if (!note) return null;
|
|
146
|
+
note.done = !!done;
|
|
147
|
+
persist();
|
|
148
|
+
render();
|
|
149
|
+
return note;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
deleteAllDone: function () {
|
|
153
|
+
var before = state.notes.length;
|
|
154
|
+
state.notes = state.notes.filter(function (n) {
|
|
155
|
+
return !n.done;
|
|
156
|
+
});
|
|
157
|
+
persist();
|
|
158
|
+
render();
|
|
159
|
+
return before - state.notes.length;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
window.HostApp = HostApp;
|
|
163
|
+
|
|
164
|
+
/* ---- login + agent config ----------------------------------------------- */
|
|
165
|
+
async function startAgent() {
|
|
166
|
+
// 404 = no key configured on the server; the app simply runs agent-dark.
|
|
167
|
+
var resp = await fetch('/api/agent-config');
|
|
168
|
+
if (!resp.ok) {
|
|
169
|
+
console.info('[agent] not configured (' + resp.status + ') — running without the assistant');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
window.__AGENT_CONFIG__ = await resp.json();
|
|
173
|
+
if (window.AgentStart) window.AgentStart();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function showApp() {
|
|
177
|
+
document.getElementById('login').hidden = true;
|
|
178
|
+
document.getElementById('app').hidden = false;
|
|
179
|
+
render();
|
|
180
|
+
startAgent();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function wire() {
|
|
184
|
+
load();
|
|
185
|
+
|
|
186
|
+
document.getElementById('login-submit').onclick = async function () {
|
|
187
|
+
var pw = document.getElementById('password').value;
|
|
188
|
+
var err = document.getElementById('login-error');
|
|
189
|
+
err.textContent = '';
|
|
190
|
+
var resp = await fetch('/api/login', {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ password: pw }),
|
|
194
|
+
});
|
|
195
|
+
if (!resp.ok) {
|
|
196
|
+
err.textContent = 'Wrong password.';
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
showApp();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
document.getElementById('password').addEventListener('keydown', function (ev) {
|
|
203
|
+
if (ev.key === 'Enter') document.getElementById('login-submit').click();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
document.getElementById('logout').onclick = async function () {
|
|
207
|
+
await fetch('/api/logout', { method: 'POST' });
|
|
208
|
+
if (window.AgentStop) window.AgentStop();
|
|
209
|
+
document.getElementById('app').hidden = true;
|
|
210
|
+
document.getElementById('login').hidden = false;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
document.getElementById('add-note').onclick = function () {
|
|
214
|
+
var box = document.getElementById('new-note');
|
|
215
|
+
var text = box.value.trim();
|
|
216
|
+
if (!text) return;
|
|
217
|
+
HostApp.createNote(text);
|
|
218
|
+
box.value = '';
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
document.getElementById('new-note').addEventListener('keydown', function (ev) {
|
|
222
|
+
if (ev.key === 'Enter') document.getElementById('add-note').click();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
document.getElementById('filter').addEventListener('input', function (ev) {
|
|
226
|
+
HostApp.setFilter(ev.target.value);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
document.getElementById('clear-filter').onclick = function () {
|
|
230
|
+
HostApp.setFilter('');
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// A live session survives reload. Ask the session probe, not the key
|
|
234
|
+
// endpoint: probing the key endpoint while logged out is a 401, which the
|
|
235
|
+
// browser reports as a console error on every visit by an anonymous user.
|
|
236
|
+
fetch('/api/session')
|
|
237
|
+
.then(function (r) {
|
|
238
|
+
return r.json();
|
|
239
|
+
})
|
|
240
|
+
.then(function (s) {
|
|
241
|
+
if (s.authenticated) showApp();
|
|
242
|
+
})
|
|
243
|
+
.catch(function () {
|
|
244
|
+
/* server unreachable — stay on the login screen */
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
|
|
249
|
+
else wire();
|
|
250
|
+
})();
|