@luckydraw/cumulus 0.31.66 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +12 -556
  2. package/LICENSE +150 -0
  3. package/README.md +27 -8
  4. package/dist/gateway/adapters/webchat.d.ts +15 -0
  5. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  6. package/dist/gateway/adapters/webchat.js +78 -5
  7. package/dist/gateway/adapters/webchat.js.map +1 -1
  8. package/dist/gateway/config.d.ts +17 -2
  9. package/dist/gateway/config.d.ts.map +1 -1
  10. package/dist/gateway/config.js +10 -3
  11. package/dist/gateway/config.js.map +1 -1
  12. package/dist/gateway/daemon.d.ts +3 -1
  13. package/dist/gateway/daemon.d.ts.map +1 -1
  14. package/dist/gateway/daemon.js +128 -39
  15. package/dist/gateway/daemon.js.map +1 -1
  16. package/dist/gateway/namespaces.d.ts +34 -0
  17. package/dist/gateway/namespaces.d.ts.map +1 -1
  18. package/dist/gateway/namespaces.js +58 -0
  19. package/dist/gateway/namespaces.js.map +1 -1
  20. package/dist/gateway/server.d.ts +8 -0
  21. package/dist/gateway/server.d.ts.map +1 -1
  22. package/dist/gateway/server.js +150 -41
  23. package/dist/gateway/server.js.map +1 -1
  24. package/dist/gateway/setup.d.ts +32 -0
  25. package/dist/gateway/setup.d.ts.map +1 -1
  26. package/dist/gateway/setup.js +23 -3
  27. package/dist/gateway/setup.js.map +1 -1
  28. package/dist/gateway/static/blex-render.js +341 -0
  29. package/dist/gateway/static/chat.html +1 -0
  30. package/dist/gateway/static/widget.js +1009 -738
  31. package/dist/lib/gateway.d.ts +30 -8
  32. package/dist/lib/gateway.d.ts.map +1 -1
  33. package/dist/lib/gateway.js +36 -11
  34. package/dist/lib/gateway.js.map +1 -1
  35. package/dist/lib/history.d.ts +22 -0
  36. package/dist/lib/history.d.ts.map +1 -1
  37. package/dist/lib/history.js +59 -21
  38. package/dist/lib/history.js.map +1 -1
  39. package/dist/lib/huggingface-provider.d.ts.map +1 -1
  40. package/dist/lib/huggingface-provider.js +11 -3
  41. package/dist/lib/huggingface-provider.js.map +1 -1
  42. package/dist/lib/license.d.ts +76 -0
  43. package/dist/lib/license.d.ts.map +1 -0
  44. package/dist/lib/license.js +141 -0
  45. package/dist/lib/license.js.map +1 -0
  46. package/docs/agentic-harness-primer.md +283 -0
  47. package/docs/conditional-continuation.md +167 -0
  48. package/docs/web-app-agent-guide.md +559 -0
  49. package/examples/web-app-agent/README.md +334 -0
  50. package/examples/web-app-agent/agent/mcp-shim.js +105 -0
  51. package/examples/web-app-agent/gateway.config.example.json +70 -0
  52. package/examples/web-app-agent/package.json +13 -0
  53. package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
  54. package/examples/web-app-agent/public/agent/bridge-mount.js +91 -0
  55. package/examples/web-app-agent/public/agent/chat-client.js +104 -0
  56. package/examples/web-app-agent/public/agent/commands.js +256 -0
  57. package/examples/web-app-agent/public/agent/device-thread.js +48 -0
  58. package/examples/web-app-agent/public/agent/panel.css +113 -0
  59. package/examples/web-app-agent/public/agent/panel.js +392 -0
  60. package/examples/web-app-agent/public/app.js +250 -0
  61. package/examples/web-app-agent/public/index.html +126 -0
  62. package/examples/web-app-agent/server.js +379 -0
  63. package/package.json +7 -3
@@ -0,0 +1,91 @@
1
+ /* Bridge mount — wires the cumulus-owned BridgeClient to this app's registry.
2
+
3
+ The BridgeClient is served straight out of the cumulus package (see
4
+ server.js). Don't fork it: it is the seam contract, and the gateway on the
5
+ other end is versioned with it.
6
+
7
+ Because the agent config arrives only after login, this is a function the
8
+ app calls (AgentStart) rather than something that runs at load. AgentStop
9
+ tears it down on logout so a second login re-mounts cleanly.
10
+
11
+ KNOWN LIMIT of the version stamp: the import below is a static specifier, so
12
+ client.js — and protocol.js, which client.js imports in turn — are fetched
13
+ unstamped. Both ship from the cumulus package and change only on upgrade, and
14
+ the server sends `no-cache, must-revalidate` on them, so the browser is
15
+ correct; an edge that overrides that is the exposure. If you put a CDN in
16
+ front of this app, exclude /agent/ from it. */
17
+ import { BridgeClient } from './bridge-client/client.js';
18
+
19
+ let bridge = null;
20
+
21
+ window.AgentStart = function () {
22
+ if (bridge) return; // already live this page-load
23
+ const cfg = window.__AGENT_CONFIG__;
24
+ if (!cfg || !cfg.API_KEY || !window.AgentRegistry || !window.agentDeviceThread) return;
25
+
26
+ window.agentDeviceThread(cfg); // THREAD_ID -> <your-namespace>-v-<deviceId>
27
+ console.info('[agent] thread: ' + cfg.THREAD_ID);
28
+
29
+ if (!document.getElementById('agent-css')) {
30
+ const link = document.createElement('link');
31
+ link.id = 'agent-css';
32
+ link.rel = 'stylesheet';
33
+ // Built here, so the server's HTML stamp cannot see it — it takes its hash
34
+ // from the map index.html publishes. Bare URL if that map is absent.
35
+ link.href =
36
+ typeof window.agentAsset === 'function'
37
+ ? window.agentAsset('/agent/panel.css')
38
+ : '/agent/panel.css';
39
+ document.head.appendChild(link);
40
+ }
41
+
42
+ window.AgentChat.init(cfg);
43
+ window.AgentPanel.init();
44
+
45
+ // Rich blocks, loaded same-origin from /agent/blex/. Fire-and-forget: the panel renders
46
+ // plain markdown until these arrive, and keeps doing so if they never do.
47
+ if (window.AgentBlex) window.AgentBlex.load(cfg);
48
+
49
+ bridge = new BridgeClient({
50
+ url: cfg.BRIDGE_URL,
51
+ thread: cfg.THREAD_ID,
52
+ apiKey: cfg.API_KEY,
53
+
54
+ registry: {
55
+ manifest: window.AgentRegistry.manifest(),
56
+ execute: (command, params) => Promise.resolve(window.AgentRegistry.call(command, params)),
57
+ },
58
+
59
+ // Recomputed on EVERY sendContext, never cached — this is what keeps the
60
+ // agent looking at the same screen the human is.
61
+ describeView: () => {
62
+ const r = window.AgentRegistry.callNow('app.describeView');
63
+ return r.ok ? r.data : { error: r.summary };
64
+ },
65
+
66
+ // Export-tier commands land here instead of executing. Returning without
67
+ // calling accept() or decline() means the gateway times the call out —
68
+ // which is the safe outcome.
69
+ onConfirmRequest: req => window.AgentPanel.confirm(req),
70
+
71
+ onStateChange: s => {
72
+ console.info('[agent] bridge ' + s);
73
+ window.AgentPanel.setState(s);
74
+ },
75
+ });
76
+
77
+ bridge.connect();
78
+ window.AgentBridge = bridge;
79
+ };
80
+
81
+ window.AgentStop = function () {
82
+ if (!bridge) return;
83
+ bridge.close();
84
+ bridge = null;
85
+ window.AgentBridge = null;
86
+ if (window.AgentPanel) window.AgentPanel.reset();
87
+ };
88
+
89
+ // The config may already be present if the session check finished before this
90
+ // module executed.
91
+ if (window.__AGENT_CONFIG__) window.AgentStart();
@@ -0,0 +1,104 @@
1
+ /* Gateway chat client. Speaks the cumulus HTTP API:
2
+
3
+ POST /api/thread/:name/message -> SSE stream: token | segment | error | done
4
+ GET /api/thread/:name/history -> prior turns, for reload survival
5
+
6
+ Both authenticate with the scoped key in the X-API-Key header. Note there is
7
+ no thread-creation call: posting to a thread that does not exist creates it.
8
+ That is exactly what the demo-mode visitor cap gates, so a 402 here means the
9
+ gateway is unlicensed and at capacity.
10
+
11
+ window.AgentChat = {
12
+ init(cfg) -> boolean, live,
13
+ send(message, handlers) -> { cancel() }
14
+ handlers: onToken(text), onSegment(seg), onError(err), onDone(payload)
15
+ history() -> Promise<array> ([] when unavailable)
16
+ } */
17
+ (function () {
18
+ 'use strict';
19
+ var origin = null,
20
+ thread = null,
21
+ apiKey = null;
22
+
23
+ async function streamSSE(resp, handlers, cancelled) {
24
+ var reader = resp.body.getReader();
25
+ var decoder = new TextDecoder();
26
+ var buf = '';
27
+ while (true) {
28
+ var chunk = await reader.read();
29
+ if (chunk.done || cancelled.is) break;
30
+ buf += decoder.decode(chunk.value, { stream: true });
31
+ var idx;
32
+ while ((idx = buf.indexOf('\n\n')) >= 0) {
33
+ var raw = buf.slice(0, idx);
34
+ buf = buf.slice(idx + 2);
35
+ var event = 'message',
36
+ data = '';
37
+ raw.split('\n').forEach(function (line) {
38
+ if (line.indexOf('event:') === 0) event = line.slice(6).trim();
39
+ else if (line.indexOf('data:') === 0) data += line.slice(5).trim();
40
+ });
41
+ if (!data) continue;
42
+ var payload;
43
+ try {
44
+ payload = JSON.parse(data);
45
+ } catch {
46
+ continue;
47
+ }
48
+ if (event === 'token' && handlers.onToken) handlers.onToken(payload.text || '');
49
+ else if (event === 'segment' && handlers.onSegment) handlers.onSegment(payload);
50
+ else if (event === 'error' && handlers.onError) handlers.onError(payload);
51
+ else if (event === 'done' && handlers.onDone) handlers.onDone(payload);
52
+ }
53
+ }
54
+ }
55
+
56
+ window.AgentChat = {
57
+ live: false,
58
+
59
+ init: function (cfg) {
60
+ origin = cfg.GATEWAY_URL;
61
+ thread = cfg.THREAD_ID;
62
+ apiKey = cfg.API_KEY;
63
+ this.live = !!(origin && thread && apiKey);
64
+ return this.live;
65
+ },
66
+
67
+ send: function (message, handlers) {
68
+ var cancelled = { is: false };
69
+ fetch(origin + '/api/thread/' + encodeURIComponent(thread) + '/message', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
72
+ body: JSON.stringify({ message: message }),
73
+ })
74
+ .then(function (resp) {
75
+ if (resp.status === 402)
76
+ throw new Error('Gateway is in demo mode and at its visitor limit');
77
+ if (!resp.ok) throw new Error('Gateway returned ' + resp.status);
78
+ return streamSSE(resp, handlers, cancelled);
79
+ })
80
+ .catch(function (err) {
81
+ if (!cancelled.is && handlers.onError)
82
+ handlers.onError({ error: String(err.message || err) });
83
+ });
84
+ return {
85
+ cancel: function () {
86
+ cancelled.is = true;
87
+ },
88
+ };
89
+ },
90
+
91
+ history: async function () {
92
+ try {
93
+ var resp = await fetch(origin + '/api/thread/' + encodeURIComponent(thread) + '/history', {
94
+ headers: { 'X-API-Key': apiKey },
95
+ });
96
+ if (!resp.ok) return [];
97
+ var j = await resp.json();
98
+ return Array.isArray(j) ? j : j.messages || j.items || [];
99
+ } catch {
100
+ return [];
101
+ }
102
+ },
103
+ };
104
+ })();
@@ -0,0 +1,256 @@
1
+ /* The command registry — your app's capability surface.
2
+
3
+ This is the file you actually write for your own app. Everything else in
4
+ this directory is plumbing you copy once and forget.
5
+
6
+ The registry is the ONLY thing the model can do to your app. Whatever isn't
7
+ registered here doesn't exist, no matter how the visitor phrases it. And
8
+ commands act through window.HostApp — the app's own adapter over its own
9
+ actions — never by poking the DOM, so persistence, validation, and re-render
10
+ keep working no matter who is driving.
11
+
12
+ Frozen contract:
13
+ command { name, description, params, risk, execute(params) }
14
+ risk "export" — the ONLY gated tier. ALWAYS routed through the
15
+ confirm chip by the gateway; the model cannot bypass
16
+ it, and neither can this file.
17
+ "read" — answers a question, changes nothing
18
+ "display" — changes what's on screen only, trivially undoable
19
+ "mutate" — changes stored data
20
+ The last three are ADVISORY: they dispatch on arrival. The tier
21
+ rides in the tool description the model sees ("[mutate] …") so it
22
+ can weigh the call, but nothing stops it. Tier by "must a human
23
+ see this first?", not by "is this irreversible?" — if yes, it is
24
+ "export". A command that reads as guarded and isn't fails
25
+ silently: it just runs.
26
+ execute -> { summary, data?, affected? } (may be async)
27
+ call() never throws — failures become { ok: false, summary }
28
+
29
+ Write descriptions for a reader who cannot see your UI. The description is
30
+ the entire basis on which the model decides to call the thing. */
31
+ (function () {
32
+ 'use strict';
33
+ var defs = new Map();
34
+
35
+ function register(def) {
36
+ defs.set(def.name, def);
37
+ }
38
+
39
+ async function call(name, params) {
40
+ var def = defs.get(name);
41
+ if (!def) return { ok: false, summary: 'Unknown command: ' + name };
42
+ if (!window.HostApp) return { ok: false, summary: 'App not ready' };
43
+ try {
44
+ var r = await def.execute(params || {});
45
+ return Object.assign({ ok: true }, r);
46
+ } catch (e) {
47
+ return { ok: false, summary: name + ' failed: ' + ((e && e.message) || e) };
48
+ }
49
+ }
50
+
51
+ /* Synchronous variant, for hooks that need a value immediately — the
52
+ bridge's describeView is called during send() and cannot await. Only valid
53
+ for commands whose execute() is synchronous. */
54
+ function callNow(name, params) {
55
+ var def = defs.get(name);
56
+ if (!def || !window.HostApp) return { ok: false, summary: 'Unavailable: ' + name };
57
+ try {
58
+ var r = def.execute(params || {});
59
+ if (r && typeof r.then === 'function')
60
+ return { ok: false, summary: name + ' is async — use call()' };
61
+ return Object.assign({ ok: true }, r);
62
+ } catch (e) {
63
+ return { ok: false, summary: name + ' failed: ' + ((e && e.message) || e) };
64
+ }
65
+ }
66
+
67
+ window.AgentRegistry = {
68
+ register: register,
69
+ call: call,
70
+ callNow: callNow,
71
+ list: function () {
72
+ return Array.from(defs.keys());
73
+ },
74
+ manifest: function () {
75
+ return Array.from(defs.values()).map(function (d) {
76
+ return { name: d.name, description: d.description, risk: d.risk, input_schema: d.params };
77
+ });
78
+ },
79
+ };
80
+
81
+ var NO_PARAMS = { type: 'object', properties: {} };
82
+ function app() {
83
+ return window.HostApp;
84
+ }
85
+
86
+ /* ---- the agent's eyes -------------------------------------------------- */
87
+
88
+ register({
89
+ name: 'app.describe',
90
+ risk: 'read',
91
+ description:
92
+ 'Static knowledge about this app: what it is, what a note is, and what you can do here. ' +
93
+ 'Call once per conversation for grounding. For what is on screen right now, use app.describeView.',
94
+ params: NO_PARAMS,
95
+ execute: function () {
96
+ return {
97
+ summary: 'Demo Notes — a single-list note taker.',
98
+ data: {
99
+ app: 'Demo Notes',
100
+ concepts: {
101
+ note: 'A short piece of text with a "done" flag and a created timestamp. Notes have integer ids.',
102
+ filter:
103
+ 'A text box above the list. When non-empty, only notes containing that text are shown. It is a view setting, not a deletion.',
104
+ },
105
+ recipes: [
106
+ 'To find something: call notes.list and read it. Do NOT set the filter just to answer a question — the filter changes what the human sees.',
107
+ 'To highlight a subset for the human: notes.setFilter.',
108
+ 'To tick something off: notes.setDone with done=true, not notes.delete.',
109
+ ],
110
+ },
111
+ };
112
+ },
113
+ });
114
+
115
+ register({
116
+ name: 'app.describeView',
117
+ risk: 'read',
118
+ description:
119
+ 'What is on the screen right now: the active filter, how many notes are visible, and the visible notes themselves. ' +
120
+ 'Recomputed on every call — never cached. Use this before acting so you act on what the human is actually looking at.',
121
+ params: NO_PARAMS,
122
+ execute: function () {
123
+ var state = app().getState();
124
+ var visible = app().visibleNotes();
125
+ return {
126
+ summary:
127
+ visible.length +
128
+ ' of ' +
129
+ state.notes.length +
130
+ ' notes visible' +
131
+ (state.filter ? ' (filter: "' + state.filter + '")' : ''),
132
+ data: {
133
+ filter: state.filter,
134
+ totalNotes: state.notes.length,
135
+ visibleNotes: visible,
136
+ },
137
+ };
138
+ },
139
+ });
140
+
141
+ register({
142
+ name: 'notes.list',
143
+ risk: 'read',
144
+ description:
145
+ 'Every note, ignoring the on-screen filter. { done?: boolean } narrows to done or not-done notes. ' +
146
+ 'This is how you answer questions — it changes nothing the human can see.',
147
+ params: {
148
+ type: 'object',
149
+ properties: { done: { type: 'boolean', description: 'Only notes with this done state.' } },
150
+ },
151
+ execute: function (p) {
152
+ var notes = app().getState().notes;
153
+ if (typeof p.done === 'boolean')
154
+ notes = notes.filter(function (n) {
155
+ return !!n.done === p.done;
156
+ });
157
+ return { summary: notes.length + ' note(s)', data: { notes: notes } };
158
+ },
159
+ });
160
+
161
+ /* ---- display: changes the view, not the data --------------------------- */
162
+
163
+ register({
164
+ name: 'notes.setFilter',
165
+ risk: 'display',
166
+ description:
167
+ 'Set the on-screen filter text so the human sees a subset. { text: string } — pass "" to clear it. ' +
168
+ 'This hides nothing permanently and deletes nothing. Use it to SHOW someone something, not to look something up yourself.',
169
+ params: {
170
+ type: 'object',
171
+ properties: {
172
+ text: { type: 'string', description: 'Filter text; empty string clears the filter.' },
173
+ },
174
+ required: ['text'],
175
+ },
176
+ execute: function (p) {
177
+ app().setFilter(String(p.text || ''));
178
+ var visible = app().visibleNotes();
179
+ return {
180
+ summary: p.text
181
+ ? 'Filtered to "' + p.text + '" — ' + visible.length + ' shown'
182
+ : 'Filter cleared',
183
+ data: { visibleCount: visible.length },
184
+ affected: ['filter'],
185
+ };
186
+ },
187
+ });
188
+
189
+ /* ---- mutate: changes stored data, but reversibly ----------------------- */
190
+
191
+ register({
192
+ name: 'notes.create',
193
+ risk: 'mutate',
194
+ description: 'Add a note. { text: string }. Returns the new note including its id.',
195
+ params: {
196
+ type: 'object',
197
+ properties: { text: { type: 'string', description: 'The note body.' } },
198
+ required: ['text'],
199
+ },
200
+ execute: function (p) {
201
+ var text = String(p.text || '').trim();
202
+ if (!text) throw new Error('text is required');
203
+ var note = app().createNote(text);
204
+ return {
205
+ summary: 'Created note #' + note.id,
206
+ data: { note: note },
207
+ affected: ['note:' + note.id],
208
+ };
209
+ },
210
+ });
211
+
212
+ register({
213
+ name: 'notes.setDone',
214
+ risk: 'mutate',
215
+ description:
216
+ 'Tick a note off or un-tick it. { id: number, done?: boolean (default true) }. ' +
217
+ 'Get ids from notes.list — never guess one.',
218
+ params: {
219
+ type: 'object',
220
+ properties: {
221
+ id: { type: 'number', description: 'Note id from notes.list.' },
222
+ done: { type: 'boolean', description: 'Default true.' },
223
+ },
224
+ required: ['id'],
225
+ },
226
+ execute: function (p) {
227
+ var note = app().setDone(Number(p.id), p.done !== false);
228
+ if (!note) throw new Error('no note with id ' + p.id);
229
+ return {
230
+ summary: 'Note #' + note.id + (note.done ? ' marked done' : ' reopened'),
231
+ data: { note: note },
232
+ affected: ['note:' + note.id],
233
+ };
234
+ },
235
+ });
236
+
237
+ /* ---- export: irreversible. The gateway stops for a human here. ---------- */
238
+
239
+ register({
240
+ name: 'notes.deleteAllDone',
241
+ risk: 'export',
242
+ description:
243
+ 'Permanently delete every note already marked done. This cannot be undone. ' +
244
+ 'The human confirms with a chip before it runs — say what you are about to delete first, ' +
245
+ 'and prefer notes.list so they can see the count.',
246
+ params: NO_PARAMS,
247
+ execute: function () {
248
+ var removed = app().deleteAllDone();
249
+ return {
250
+ summary: 'Deleted ' + removed + ' completed note(s)',
251
+ data: { removed: removed },
252
+ affected: ['notes'],
253
+ };
254
+ },
255
+ });
256
+ })();
@@ -0,0 +1,48 @@
1
+ /* Per-visitor thread identity.
2
+
3
+ The thread name IS the capability. A scoped key cannot enumerate threads, so
4
+ knowing the name is what grants access to a conversation — which means the
5
+ name must be unguessable and must never be minted server-side and broadcast.
6
+
7
+ This mints 16 hex characters (64 bits) once per browser, keeps it in
8
+ localStorage, and pins THREAD_ID = <base>-<deviceId>. The base comes from the
9
+ server (this file never names your app). Send "<ns>-v" — a sub-namespace
10
+ under "<ns>" — so visitor threads get their own gateway config (cheap model,
11
+ tight prompt) while the base "<ns>" thread stays yours for working on the app.
12
+
13
+ Do not shorten the id. 8 hex characters is 32 bits, which is brute-forceable
14
+ against a live gateway. */
15
+ (function () {
16
+ 'use strict';
17
+ var KEY = 'agent.deviceId'; // app-neutral: localStorage is origin-partitioned
18
+
19
+ window.agentDeviceThread = function (cfg) {
20
+ if (!cfg || !cfg.THREAD_ID) return null;
21
+ if (/-[0-9a-f]{16,}$/.test(cfg.THREAD_ID)) return cfg.THREAD_ID; // already suffixed
22
+
23
+ var id = null;
24
+ try {
25
+ id = localStorage.getItem(KEY);
26
+ } catch {
27
+ /* private mode — fall through and mint an ephemeral id */
28
+ }
29
+
30
+ if (!id || !/^[0-9a-f]{16,}$/.test(id)) {
31
+ var bytes = new Uint8Array(8); // 8 bytes -> 16 hex chars
32
+ crypto.getRandomValues(bytes);
33
+ id = Array.prototype.map
34
+ .call(bytes, function (b) {
35
+ return ('0' + b.toString(16)).slice(-2);
36
+ })
37
+ .join('');
38
+ try {
39
+ localStorage.setItem(KEY, id);
40
+ } catch {
41
+ /* ignore — the visitor gets a fresh thread each load */
42
+ }
43
+ }
44
+
45
+ cfg.THREAD_ID = cfg.THREAD_ID + '-' + id;
46
+ return cfg.THREAD_ID;
47
+ };
48
+ })();
@@ -0,0 +1,113 @@
1
+ /* Agent panel styling. Everything is expressed through the host app's CSS
2
+ variables (--panel, --panel2, --panel3, --line, --text, --muted, --accent),
3
+ so the panel follows your theme — including light/dark — with no work.
4
+ Define those six variables in your app and this file needs no edits. */
5
+
6
+ #agent-root {
7
+ position: fixed;
8
+ z-index: 900;
9
+ left: 50%;
10
+ bottom: 0;
11
+ transform: translateX(-50%);
12
+ width: min(560px, 100vw - 32px);
13
+ font-size: 13.5px;
14
+ }
15
+
16
+ @media (max-width: 720px) {
17
+ #agent-root { width: calc(100vw - 16px); bottom: env(safe-area-inset-bottom, 0px); }
18
+ #agent-root .agbar { margin-bottom: 8px; padding: 8px 12px; }
19
+ #agent-root.open .agwin { height: min(520px, 68dvh); margin-bottom: 8px; }
20
+ }
21
+
22
+ /* --- minimized home bar --- */
23
+ #agent-root .agbar {
24
+ display: flex; align-items: center; gap: 8px;
25
+ margin-bottom: 10px; padding: 9px 14px;
26
+ background: var(--panel2); border: 1px solid var(--line); border-radius: 22px;
27
+ box-shadow: 0 4px 18px rgba(0, 0, 0, .25);
28
+ cursor: text;
29
+ }
30
+ #agent-root.open .agbar { display: none; }
31
+ #agent-root .agstar { color: var(--accent); flex: none; }
32
+ #agent-root .agbarin {
33
+ flex: 1; background: none; border: none; outline: none;
34
+ color: var(--text); font: inherit;
35
+ }
36
+ #agent-root .agbarin::placeholder { color: var(--muted); }
37
+
38
+ /* --- expanded window (grows upward from the bar's slot) --- */
39
+ #agent-root .agwin { display: none; }
40
+ #agent-root.open .agwin {
41
+ display: flex; flex-direction: column;
42
+ margin-bottom: 16px;
43
+ height: min(520px, 72vh);
44
+ background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
45
+ box-shadow: 0 10px 34px rgba(0, 0, 0, .35);
46
+ overflow: hidden;
47
+ }
48
+ #agent-root .aghead {
49
+ display: flex; align-items: center; gap: 8px; flex: none;
50
+ padding: 10px 14px; border-bottom: 1px solid var(--line);
51
+ }
52
+ #agent-root .agtitle { font-weight: 600; }
53
+ #agent-root .agdot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
54
+ #agent-root .agdot.open { background: #47c98a; }
55
+ #agent-root .agdot.connecting { background: #f2b13e; }
56
+ #agent-root .agdot.closed { background: #f2665e; }
57
+ #agent-root .agmin {
58
+ margin-left: auto; background: none; border: none; color: var(--muted);
59
+ cursor: pointer; font: inherit; padding: 2px 6px; border-radius: 6px;
60
+ }
61
+ #agent-root .agmin:hover { background: var(--panel3); color: var(--text); }
62
+
63
+ /* --- log --- */
64
+ #agent-root .aglog { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 10px; }
65
+ #agent-root .agmsg { max-width: 88%; padding: 8px 12px; border-radius: 10px; line-height: 1.45; overflow-wrap: break-word; }
66
+ #agent-root .agmsg.user { align-self: flex-end; background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
67
+ #agent-root .agmsg.assistant { align-self: flex-start; background: var(--panel2); color: var(--text); border-bottom-left-radius: 4px; white-space: pre-wrap; }
68
+ #agent-root .agmsg.assistant p,
69
+ #agent-root .agmsg.assistant ul,
70
+ #agent-root .agmsg.assistant h4,
71
+ #agent-root .agmsg.assistant table,
72
+ #agent-root .agmsg.assistant pre { white-space: normal; margin: 0 0 8px; }
73
+ #agent-root .agmsg.assistant > :last-child { margin-bottom: 0; }
74
+ #agent-root .agmsg.assistant ul { padding-left: 18px; }
75
+ #agent-root .agmsg.system { align-self: center; color: var(--muted); font-size: 12px; background: none; }
76
+ #agent-root .agmsg.error { align-self: flex-start; background: rgba(242, 102, 94, .12); color: #f2665e; }
77
+ #agent-root .agmsg pre { background: var(--panel3); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; overflow-x: auto; font-size: 12px; }
78
+ #agent-root .agmsg code { background: var(--panel3); border-radius: 4px; padding: 1px 5px; font-size: 12.5px; }
79
+ #agent-root .agmsg table { border-collapse: collapse; font-size: 12.5px; }
80
+ #agent-root .agmsg th, #agent-root .agmsg td { border: 1px solid var(--line); padding: 4px 8px; text-align: left; }
81
+
82
+ /* Blex blocks. The container is what the shared renderer (blex-render.js) fills;
83
+ .blex-fallback is the raw fence, shown when nothing claims the container —
84
+ a denied type, or blex failing to load. Both beat a blank rectangle. */
85
+ #agent-root .blex-block-container { margin: 6px 0; border-radius: 8px; overflow: hidden; max-width: 100%; }
86
+ #agent-root .blex-fallback { margin: 0; padding: 8px 10px; background: var(--panel3); border: 1px solid var(--line); border-radius: 8px; font-size: 11.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; opacity: .85; overflow-x: auto; }
87
+
88
+ #agent-root .agactivity { align-self: flex-start; color: var(--muted); font-size: 12px; padding: 0 4px; animation: agpulse 1.4s ease-in-out infinite; }
89
+ @keyframes agpulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
90
+ /* Browser tests set data-test-mode to kill animation timing races. */
91
+ [data-test-mode="true"] #agent-root .agactivity { animation: none; }
92
+
93
+ /* --- export-tier confirm chip --- */
94
+ #agent-root .agconfirm { align-self: stretch; background: var(--panel2); border: 1px solid #f2b13e88; border-radius: 10px; padding: 10px 12px; }
95
+ #agent-root .agconfirmrow { display: flex; gap: 8px; margin-top: 8px; }
96
+ #agent-root .agconfirmdone { color: var(--muted); margin-top: 6px; font-size: 12px; }
97
+ #agent-root .agbtn {
98
+ font: inherit; padding: 5px 14px; border-radius: 8px; cursor: pointer;
99
+ background: var(--panel3); color: var(--text); border: 1px solid var(--line);
100
+ }
101
+ #agent-root .agbtn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
102
+
103
+ /* --- composer row --- */
104
+ #agent-root .aginrow { display: flex; gap: 8px; flex: none; padding: 10px 12px; border-top: 1px solid var(--line); }
105
+ #agent-root .again {
106
+ flex: 1; resize: none; background: var(--panel2); border: 1px solid var(--line);
107
+ border-radius: 10px; padding: 8px 10px; color: var(--text); font: inherit; outline: none;
108
+ }
109
+ #agent-root .again:focus { border-color: var(--accent); }
110
+ #agent-root .agsend {
111
+ align-self: flex-end; width: 34px; height: 34px; border-radius: 50%;
112
+ background: var(--accent); color: #fff; border: none; cursor: pointer; font-size: 15px;
113
+ }