@llui/agent 0.0.48 → 0.0.50

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 (67) hide show
  1. package/dist/client/agentAttention.d.ts +129 -0
  2. package/dist/client/agentAttention.d.ts.map +1 -0
  3. package/dist/client/agentAttention.js +156 -0
  4. package/dist/client/agentAttention.js.map +1 -0
  5. package/dist/client/agentChat.d.ts +100 -0
  6. package/dist/client/agentChat.d.ts.map +1 -0
  7. package/dist/client/agentChat.js +84 -0
  8. package/dist/client/agentChat.js.map +1 -0
  9. package/dist/client/agentLog.d.ts +17 -0
  10. package/dist/client/agentLog.d.ts.map +1 -1
  11. package/dist/client/agentLog.js +18 -0
  12. package/dist/client/agentLog.js.map +1 -1
  13. package/dist/client/diff-render.d.ts +68 -0
  14. package/dist/client/diff-render.d.ts.map +1 -0
  15. package/dist/client/diff-render.js +141 -0
  16. package/dist/client/diff-render.js.map +1 -0
  17. package/dist/client/effect-handler.d.ts +29 -0
  18. package/dist/client/effect-handler.d.ts.map +1 -1
  19. package/dist/client/effect-handler.js +39 -0
  20. package/dist/client/effect-handler.js.map +1 -1
  21. package/dist/client/effects.d.ts +43 -0
  22. package/dist/client/effects.d.ts.map +1 -1
  23. package/dist/client/effects.js.map +1 -1
  24. package/dist/client/factory.d.ts +21 -0
  25. package/dist/client/factory.d.ts.map +1 -1
  26. package/dist/client/factory.js +15 -2
  27. package/dist/client/factory.js.map +1 -1
  28. package/dist/client/index.d.ts +4 -0
  29. package/dist/client/index.d.ts.map +1 -1
  30. package/dist/client/index.js +3 -0
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/ws-client.d.ts +9 -0
  33. package/dist/client/ws-client.d.ts.map +1 -1
  34. package/dist/client/ws-client.js +120 -0
  35. package/dist/client/ws-client.js.map +1 -1
  36. package/dist/protocol.d.ts +103 -3
  37. package/dist/protocol.d.ts.map +1 -1
  38. package/dist/protocol.js.map +1 -1
  39. package/dist/server/cloudflare/durable-object.d.ts +41 -0
  40. package/dist/server/cloudflare/durable-object.d.ts.map +1 -1
  41. package/dist/server/cloudflare/durable-object.js +46 -0
  42. package/dist/server/cloudflare/durable-object.js.map +1 -1
  43. package/dist/server/cloudflare/index.d.ts +10 -3
  44. package/dist/server/cloudflare/index.d.ts.map +1 -1
  45. package/dist/server/cloudflare/index.js +10 -3
  46. package/dist/server/cloudflare/index.js.map +1 -1
  47. package/dist/server/core.d.ts +11 -1
  48. package/dist/server/core.d.ts.map +1 -1
  49. package/dist/server/core.js +1 -0
  50. package/dist/server/core.js.map +1 -1
  51. package/dist/server/lap/narrate.d.ts +31 -0
  52. package/dist/server/lap/narrate.d.ts.map +1 -0
  53. package/dist/server/lap/narrate.js +70 -0
  54. package/dist/server/lap/narrate.js.map +1 -0
  55. package/dist/server/lap/router.d.ts.map +1 -1
  56. package/dist/server/lap/router.js +6 -0
  57. package/dist/server/lap/router.js.map +1 -1
  58. package/dist/server/lap/wait-for-user-input.d.ts +13 -0
  59. package/dist/server/lap/wait-for-user-input.d.ts.map +1 -0
  60. package/dist/server/lap/wait-for-user-input.js +53 -0
  61. package/dist/server/lap/wait-for-user-input.js.map +1 -0
  62. package/dist/server/ws/pairing-registry.d.ts +101 -0
  63. package/dist/server/ws/pairing-registry.d.ts.map +1 -1
  64. package/dist/server/ws/pairing-registry.js +160 -0
  65. package/dist/server/ws/pairing-registry.js.map +1 -1
  66. package/package.json +7 -5
  67. package/styles/agent-panel.css +153 -0
@@ -13,9 +13,18 @@ import { rpc as rpcHelper, waitForConfirm as waitForConfirmHelper, waitForChange
13
13
  * the buffer currently holds.
14
14
  */
15
15
  const RECENT_LOG_CAP = 100;
16
+ /**
17
+ * Per-tid cap on the user-input buffer (submissions received with no
18
+ * waiter parked). Eight messages covers a typical "user types a few
19
+ * follow-ups while Claude is mid-tool-call" gap without leaking memory
20
+ * if no agent ever drains them. Overflow drops oldest (newer messages
21
+ * are more contextually relevant).
22
+ */
23
+ const USER_INPUT_BUFFER_CAP = 8;
16
24
  export class InMemoryPairingRegistry {
17
25
  pairings = new Map();
18
26
  onLogAppend;
27
+ userInputStorage;
19
28
  /**
20
29
  * Per-tid ring buffer of recent log entries. Populated as the
21
30
  * registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.
@@ -25,6 +34,7 @@ export class InMemoryPairingRegistry {
25
34
  recentLog = new Map();
26
35
  constructor(opts = {}) {
27
36
  this.onLogAppend = opts.onLogAppend ?? null;
37
+ this.userInputStorage = opts.userInputStorage ?? null;
28
38
  }
29
39
  /**
30
40
  * Read the most recent `n` log entries for a tid, newest-first. Returns
@@ -49,10 +59,38 @@ export class InMemoryPairingRegistry {
49
59
  subscribers: new Set(),
50
60
  closeHandlers: new Set(),
51
61
  closed: false,
62
+ userInputBuffer: [],
63
+ userInputWaiters: [],
52
64
  };
53
65
  this.pairings.set(tid, p);
54
66
  conn.onFrame((frame) => this.dispatch(tid, frame));
55
67
  conn.onClose(() => this.handleClose(tid));
68
+ // Best-effort restore of any persisted buffer. The promise is NOT
69
+ // awaited so the synchronous register() contract is preserved;
70
+ // submissions arriving in-flight queue up in the in-memory buffer
71
+ // and the restored entries simply prepend (oldest-first) when the
72
+ // read resolves. If the read rejects, we treat it as an empty
73
+ // restore — the conversation continues, just without any messages
74
+ // from before the eviction.
75
+ if (this.userInputStorage) {
76
+ const storage = this.userInputStorage;
77
+ storage.read(tid).then((entries) => {
78
+ if (!entries || entries.length === 0)
79
+ return;
80
+ const live = this.pairings.get(tid);
81
+ if (!live || live !== p || live.closed)
82
+ return; // pairing already closed/replaced
83
+ // Restored entries are older than anything that arrived in
84
+ // the meantime. Prepend, then re-cap to USER_INPUT_BUFFER_CAP.
85
+ live.userInputBuffer.unshift(...entries);
86
+ if (live.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {
87
+ live.userInputBuffer.splice(0, live.userInputBuffer.length - USER_INPUT_BUFFER_CAP);
88
+ }
89
+ }, () => {
90
+ // Storage failure — log and continue. The conversation works
91
+ // without persistence; we just lost any pre-eviction messages.
92
+ });
93
+ }
56
94
  }
57
95
  unregister(tid) {
58
96
  this.handleClose(tid);
@@ -124,6 +162,25 @@ export class InMemoryPairingRegistry {
124
162
  this.onLogAppend?.(tid, frame.entry);
125
163
  return;
126
164
  }
165
+ if (frame.t === 'user-input-submitted') {
166
+ // FIFO delivery to a parked waiter, else buffer.
167
+ // Shifting the head keeps "first parked" semantics; the parked
168
+ // promise's cancel tears down its own timer before resolving.
169
+ const waiter = p.userInputWaiters.shift();
170
+ if (waiter) {
171
+ waiter.cancel();
172
+ waiter.resolve({ status: 'submitted', text: frame.text, at: frame.at });
173
+ return;
174
+ }
175
+ p.userInputBuffer.push({ text: frame.text, at: frame.at });
176
+ if (p.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {
177
+ // Drop oldest. Newer messages are more contextually relevant
178
+ // for an agent picking up a stale conversation.
179
+ p.userInputBuffer.splice(0, p.userInputBuffer.length - USER_INPUT_BUFFER_CAP);
180
+ }
181
+ this.persistUserInputBuffer(tid, p.userInputBuffer);
182
+ return;
183
+ }
127
184
  // Iterate over a snapshot because subscribers may self-remove
128
185
  // mid-iteration by returning true.
129
186
  const snapshot = Array.from(p.subscribers);
@@ -155,15 +212,118 @@ export class InMemoryPairingRegistry {
155
212
  waitForChange(tid, path, timeoutMs) {
156
213
  return waitForChangeHelper(this, tid, path, timeoutMs);
157
214
  }
215
+ waitForUserInput(tid, timeoutMs) {
216
+ const p = this.pairings.get(tid);
217
+ // Unknown / closed pairing → resolve as timeout immediately. The
218
+ // LAP layer above us has already gated on `isPaired`, so this is
219
+ // the rare race where the pairing closed between the gate and the
220
+ // wait call. Returning timeout (instead of rejecting) keeps the
221
+ // public response shape simple — agents only need to handle two
222
+ // outcomes.
223
+ if (!p || p.closed) {
224
+ return Promise.resolve({ status: 'timeout' });
225
+ }
226
+ // Buffered submission already waiting → resolve synchronously.
227
+ const buffered = p.userInputBuffer.shift();
228
+ if (buffered) {
229
+ this.persistUserInputBuffer(tid, p.userInputBuffer);
230
+ return Promise.resolve({ status: 'submitted', text: buffered.text, at: buffered.at });
231
+ }
232
+ return new Promise((resolve) => {
233
+ let settled = false;
234
+ const settle = (value) => {
235
+ if (settled)
236
+ return;
237
+ settled = true;
238
+ resolve(value);
239
+ };
240
+ const timer = setTimeout(() => {
241
+ const idx = p.userInputWaiters.findIndex((w) => w === waiter);
242
+ if (idx !== -1)
243
+ p.userInputWaiters.splice(idx, 1);
244
+ settle({ status: 'timeout' });
245
+ }, timeoutMs);
246
+ const waiter = {
247
+ resolve: (value) => {
248
+ // dispatch() / handleClose() both call this. The `settled`
249
+ // guard makes it idempotent — exactly one resolution survives,
250
+ // regardless of arrival order.
251
+ settle(value);
252
+ },
253
+ cancel: () => {
254
+ clearTimeout(timer);
255
+ unsubClose();
256
+ },
257
+ };
258
+ p.userInputWaiters.push(waiter);
259
+ // Pairing close before resolution: clean up and resolve as
260
+ // timeout. handleClose sweeps the waiter queue and calls
261
+ // `waiter.resolve({ status: 'timeout' })` directly, so this
262
+ // close subscription is belt-and-braces — covers any path where
263
+ // the registry's close cascade doesn't run (e.g. a custom
264
+ // PairingConnection signalling close in an unusual order).
265
+ const unsubClose = this.onClose(tid, () => {
266
+ const idx = p.userInputWaiters.findIndex((w) => w === waiter);
267
+ if (idx !== -1)
268
+ p.userInputWaiters.splice(idx, 1);
269
+ clearTimeout(timer);
270
+ settle({ status: 'timeout' });
271
+ });
272
+ });
273
+ }
158
274
  /** @deprecated Use `send(tid, frame)` directly; semantics are identical. */
159
275
  notify(tid, frame) {
160
276
  this.send(tid, frame);
161
277
  }
278
+ /**
279
+ * Fire-and-forget write-through to the optional storage adapter.
280
+ * Snapshots the buffer (defensive copy — the in-memory array can
281
+ * mutate again before the adapter's write resolves) and ignores
282
+ * rejections. Called on every buffer mutation; cheap when the
283
+ * adapter is unset (no-ops) and best-effort when wired.
284
+ */
285
+ persistUserInputBuffer(tid, buffer) {
286
+ if (!this.userInputStorage)
287
+ return;
288
+ // Defensive copy so the persisted snapshot is the buffer at the
289
+ // moment of mutation, not whatever the buffer holds when the
290
+ // storage adapter actually serializes the write.
291
+ const snapshot = buffer.slice();
292
+ void this.userInputStorage.write(tid, snapshot).catch(() => {
293
+ /* best-effort; storage failures don't wedge the conversation */
294
+ });
295
+ }
162
296
  handleClose(tid) {
163
297
  const p = this.pairings.get(tid);
164
298
  if (!p || p.closed)
165
299
  return;
166
300
  p.closed = true;
301
+ // Tear down user-input waiters BEFORE running closeHandlers.
302
+ // Each waiter's `cancel` clears its timer + close-subscription;
303
+ // resolving as `timeout` afterward is idempotent (the waiter's
304
+ // `settled` guard short-circuits if its own close handler already
305
+ // resolved it). Order matters: cancel first so the resolution
306
+ // path can't re-enter handleClose via the close-subscription.
307
+ const waiters = p.userInputWaiters.slice();
308
+ p.userInputWaiters.length = 0;
309
+ for (const w of waiters) {
310
+ try {
311
+ w.cancel();
312
+ }
313
+ catch {
314
+ // best-effort; resolution still proceeds
315
+ }
316
+ w.resolve({ status: 'timeout' });
317
+ }
318
+ // Drop buffered inputs — once the pairing is closed, an agent
319
+ // resuming on a fresh tid shouldn't see stale messages from the
320
+ // previous session. Same retention contract as recentLog.
321
+ p.userInputBuffer.length = 0;
322
+ if (this.userInputStorage) {
323
+ void this.userInputStorage.clear(tid).catch(() => {
324
+ /* best-effort; storage failures don't block the close path */
325
+ });
326
+ }
167
327
  for (const h of Array.from(p.closeHandlers)) {
168
328
  try {
169
329
  h();
@@ -1 +1 @@
1
- {"version":3,"file":"pairing-registry.js","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js'\nimport {\n rpc as rpcHelper,\n waitForConfirm as waitForConfirmHelper,\n waitForChange as waitForChangeHelper,\n type RpcOptions,\n type RpcError,\n} from './rpc.js'\n\nexport type { RpcOptions, RpcError }\n\n/**\n * Thin abstraction over a single paired WebSocket. Consumed by the\n * registry implementations; runtime-specific adapters (`ws`-lib,\n * `WebSocketPair`, `Deno.upgradeWebSocket`, `Bun.serve` upgrade) build\n * one of these and pass it to `registry.register()`.\n */\nexport interface PairingConnection {\n send(frame: ServerFrame): void\n onFrame(handler: (f: ClientFrame) => void): void\n onClose(handler: () => void): void\n close(): void\n}\n\n/**\n * A per-call frame subscriber. Return `true` to remove this\n * subscriber (one-shot), or `false` to keep receiving. The registry\n * dispatches every inbound `ClientFrame` to every active subscriber\n * for the given `tid`; subscribers filter by `frame.t` + identifiers\n * (correlation id, confirm id, state path) to find the one that\n * belongs to their request.\n */\nexport type FrameSubscriber = (frame: ClientFrame) => boolean\n\n/**\n * Registry of live browser pairings. Pure routing + hello cache —\n * request-lifecycle state (in-flight RPC promises, confirm waits,\n * long-polls) lives in the LAP handlers that need it, not here.\n *\n * Two implementations ship today:\n * - `InMemoryPairingRegistry` for long-lived server processes\n * (Node, Bun, Deno, Deno Deploy).\n * - A Cloudflare Durable Object implementation (see\n * `server/cloudflare`) for stateless Worker runtimes.\n *\n * Other runtimes can implement this interface the same way; the\n * contract is intentionally small.\n */\nexport interface PairingRegistry {\n // ── Routing primitives ─────────────────────────────────────────\n register(tid: string, conn: PairingConnection): void\n unregister(tid: string): void\n isPaired(tid: string): boolean\n getHello(tid: string): HelloFrame | null\n /** Send a frame. No-op when the pairing is absent or closed. */\n send(tid: string, frame: ServerFrame): void\n /**\n * Subscribe to frames from the paired browser. Returns an\n * unsubscribe function. A subscriber can remove itself mid-dispatch\n * by returning `true` from its callback — useful for one-shot\n * request/response correlation.\n */\n subscribe(tid: string, handler: FrameSubscriber): () => void\n /**\n * Observe the pairing closing (WebSocket drop, `unregister`, etc.).\n * Handlers registered before close fire; handlers registered after\n * close fire synchronously. Returns an unsubscribe function.\n */\n onClose(tid: string, handler: () => void): () => void\n\n /**\n * Read the most recent `n` log entries for a tid (newest first).\n * Backed by an in-memory ring buffer populated as the registry\n * sees `log-append` frames; capped per-tid to bound memory across\n * long-lived sessions. Drained on close. Returns an empty array\n * for unknown tids.\n */\n getRecentLog(tid: string, n: number): LogEntry[]\n\n // ── Request/response helpers ───────────────────────────────────\n // These are part of the contract (LAP handlers call them directly)\n // but implementations almost always delegate to the free helpers in\n // `./rpc.ts`, which are built on the routing primitives above. The\n // Cloudflare Durable Object registry uses the same helpers; the\n // split exists so the routing surface is small enough to implement\n // across stateful boundaries (DO storage, WebSocket hibernation),\n // while the correlation logic lives once in a runtime-neutral file.\n\n /**\n * Send a typed rpc frame and await its matching reply. See\n * `./rpc.ts::rpc` for the full contract.\n */\n rpc(tid: string, tool: string, args: unknown, opts?: RpcOptions): Promise<unknown>\n /** See `./rpc.ts::waitForConfirm`. */\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }>\n /** See `./rpc.ts::waitForChange`. */\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }>\n}\n\ntype Pairing = {\n conn: PairingConnection\n hello: HelloFrame | null\n subscribers: Set<FrameSubscriber>\n closeHandlers: Set<() => void>\n closed: boolean\n}\n\n/**\n * Single-process in-memory registry. Correct for Node/Bun/Deno/Deno\n * Deploy — anywhere the server process can hold a long-lived\n * WebSocket. Not suitable for stateless Worker isolates; use the\n * Durable Object registry for Cloudflare.\n */\n/**\n * Per-tid cap on the recent-log ring buffer. Sized to cover a few\n * minutes of agent activity at typical dispatch rates without\n * growing unboundedly for long-lived sessions. Reads via\n * `getRecentLog` clamp to this; agents asking for more get whatever\n * the buffer currently holds.\n */\nconst RECENT_LOG_CAP = 100\n\nexport class InMemoryPairingRegistry implements PairingRegistry {\n private pairings = new Map<string, Pairing>()\n private onLogAppend: ((tid: string, entry: LogEntry) => void) | null\n /**\n * Per-tid ring buffer of recent log entries. Populated as the\n * registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.\n * The agent reads this via `describe_recent_actions` to introspect\n * its own activity history with stateDiffs intact.\n */\n private recentLog = new Map<string, LogEntry[]>()\n\n constructor(\n opts: {\n onLogAppend?: (tid: string, entry: LogEntry) => void\n } = {},\n ) {\n this.onLogAppend = opts.onLogAppend ?? null\n }\n\n /**\n * Read the most recent `n` log entries for a tid, newest-first. Returns\n * an empty array when the tid is unknown or has no recorded activity.\n * Drained from the in-memory ring buffer; entries older than\n * RECENT_LOG_CAP have already been trimmed.\n */\n getRecentLog(tid: string, n: number): LogEntry[] {\n const buf = this.recentLog.get(tid)\n if (!buf || buf.length === 0) return []\n const count = Math.min(Math.max(0, Math.floor(n)), buf.length)\n if (count === 0) return []\n // Buffer is append-order; return the tail reversed so newest is first.\n return buf.slice(-count).reverse()\n }\n\n register(tid: string, conn: PairingConnection): void {\n const p: Pairing = {\n conn,\n hello: null,\n subscribers: new Set(),\n closeHandlers: new Set(),\n closed: false,\n }\n this.pairings.set(tid, p)\n conn.onFrame((frame) => this.dispatch(tid, frame))\n conn.onClose(() => this.handleClose(tid))\n }\n\n unregister(tid: string): void {\n this.handleClose(tid)\n }\n\n isPaired(tid: string): boolean {\n const p = this.pairings.get(tid)\n return !!p && !p.closed\n }\n\n getHello(tid: string): HelloFrame | null {\n return this.pairings.get(tid)?.hello ?? null\n }\n\n send(tid: string, frame: ServerFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n try {\n p.conn.send(frame)\n } catch {\n // Connection may have dropped between isPaired() and send(); no-op.\n }\n }\n\n subscribe(tid: string, handler: FrameSubscriber): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return () => {}\n p.subscribers.add(handler)\n return () => {\n p.subscribers.delete(handler)\n }\n }\n\n onClose(tid: string, handler: () => void): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) {\n // Already closed — fire synchronously so callers don't hang\n // waiting for a close that already happened.\n queueMicrotask(handler)\n return () => {}\n }\n p.closeHandlers.add(handler)\n return () => {\n p.closeHandlers.delete(handler)\n }\n }\n\n private dispatch(tid: string, frame: ClientFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n // hello and log-append are registry-owned side effects — handled\n // here so no per-call subscriber has to pick them up.\n if (frame.t === 'hello') {\n p.hello = frame\n return\n }\n if (frame.t === 'log-append') {\n // Push into the ring buffer for `describe_recent_actions`,\n // capped to RECENT_LOG_CAP. The audit-sink callback runs\n // alongside; both are independent observers of the same\n // log-append stream.\n let buf = this.recentLog.get(tid)\n if (!buf) {\n buf = []\n this.recentLog.set(tid, buf)\n }\n buf.push(frame.entry)\n if (buf.length > RECENT_LOG_CAP) {\n buf.splice(0, buf.length - RECENT_LOG_CAP)\n }\n this.onLogAppend?.(tid, frame.entry)\n return\n }\n // Iterate over a snapshot because subscribers may self-remove\n // mid-iteration by returning true.\n const snapshot = Array.from(p.subscribers)\n for (const sub of snapshot) {\n try {\n if (sub(frame)) p.subscribers.delete(sub)\n } catch {\n // One bad subscriber shouldn't break the others.\n p.subscribers.delete(sub)\n }\n }\n }\n\n // ── Convenience wrappers ───────────────────────────────────────\n // The following methods delegate to the free-function helpers in\n // `./rpc.ts`. They're here so the in-memory registry remains a\n // one-stop testing surface (spy on `registry.rpc`, etc.) without\n // couping the `PairingRegistry` interface to request-lifecycle\n // details. External implementations (e.g. the Cloudflare Durable\n // Object registry) are NOT required to provide these; the LAP\n // handlers always go through the free helpers.\n\n rpc(tid: string, tool: string, args: unknown, opts: RpcOptions = {}): Promise<unknown> {\n return rpcHelper(this, tid, tool, args, opts)\n }\n\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }> {\n return waitForConfirmHelper(this, tid, confirmId, timeoutMs)\n }\n\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }> {\n return waitForChangeHelper(this, tid, path, timeoutMs)\n }\n\n /** @deprecated Use `send(tid, frame)` directly; semantics are identical. */\n notify(tid: string, frame: ServerFrame): void {\n this.send(tid, frame)\n }\n\n private handleClose(tid: string): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n p.closed = true\n for (const h of Array.from(p.closeHandlers)) {\n try {\n h()\n } catch {\n // Swallow — handlers run best-effort.\n }\n }\n p.closeHandlers.clear()\n p.subscribers.clear()\n this.pairings.delete(tid)\n // Drop the recent-log ring buffer — once the pairing is gone,\n // `describe_recent_actions` will reject anyway (paused/revoked\n // gates run before the registry lookup), but holding the entries\n // would leak memory across reconnects.\n this.recentLog.delete(tid)\n }\n}\n\n/**\n * Back-compat alias for the prior class name. New code should use\n * `InMemoryPairingRegistry`. Removed in a future major.\n *\n * @deprecated Use `InMemoryPairingRegistry` directly.\n */\nexport const WsPairingRegistry = InMemoryPairingRegistry\nexport type WsPairingRegistry = InMemoryPairingRegistry\n"]}
1
+ {"version":3,"file":"pairing-registry.js","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js'\nimport {\n rpc as rpcHelper,\n waitForConfirm as waitForConfirmHelper,\n waitForChange as waitForChangeHelper,\n type RpcOptions,\n type RpcError,\n} from './rpc.js'\n\nexport type { RpcOptions, RpcError }\n\n/**\n * Resolution shape for `waitForUserInput`. Mirrors `LapWaitForUserInputResponse`\n * one-for-one — declared here so the interface stays expressible without\n * importing the LAP layer's protocol types upward.\n */\nexport type UserInputResolution =\n | { status: 'submitted'; text: string; at: number }\n | { status: 'timeout' }\n\n/**\n * Optional persistence adapter for the per-tid user-input buffer.\n *\n * Most runtimes (Node, Bun, Deno, Deno Deploy) don't need this — the\n * in-memory registry survives for the lifetime of the process, which\n * is also the lifetime of the WS pairing. Buffered submissions either\n * get drained by an agent's `wait_for_user_input` call or are\n * irrelevant when the pairing eventually closes.\n *\n * Cloudflare Durable Objects are the motivating case. A DO process\n * can be evicted (deploys, idle eviction, runtime restarts) while a\n * WS pairing is paused mid-conversation; the next request rebuilds\n * a fresh DO with a fresh `InMemoryPairingRegistry`. Wiring this\n * adapter to the DO's `state.storage` makes buffered submissions\n * survive eviction.\n *\n * Parked waiters (Promise resolvers from `waitForUserInput`) CAN'T be\n * persisted — they live in JS memory only. After eviction + wake the\n * agent's LAP client times out on its parked HTTP request and retries\n * via the same long-poll loop; the retry sees the restored buffer.\n *\n * Calls are best-effort: the registry doesn't await them on the hot\n * path. A storage outage causes lost messages on eviction but never\n * wedges a live conversation.\n */\nexport interface UserInputStorage {\n /**\n * Read any persisted buffer for this tid. Called from `register()`\n * when the registry sees a fresh pairing — the returned entries\n * seed the in-memory buffer so subsequent `waitForUserInput` calls\n * find them.\n *\n * Returning an empty array (or rejecting) for an unknown tid is\n * normal — a fresh DO has nothing to restore.\n */\n read(tid: string): Promise<Array<{ text: string; at: number }>>\n /**\n * Persist the current buffer for this tid. Called whenever the\n * buffer mutates (push on `user-input-submitted`, shift on\n * `waitForUserInput` drain). Receives the FULL buffer, not just\n * the delta — simpler contract, idempotent writes, and the buffer\n * is small (capped at USER_INPUT_BUFFER_CAP).\n */\n write(tid: string, buffer: Array<{ text: string; at: number }>): Promise<void>\n /**\n * Drop persisted buffer for this tid. Called from `handleClose()`\n * so an evicted-then-restarted DO doesn't see stale messages from\n * a session that ended.\n */\n clear(tid: string): Promise<void>\n}\n\n/**\n * Thin abstraction over a single paired WebSocket. Consumed by the\n * registry implementations; runtime-specific adapters (`ws`-lib,\n * `WebSocketPair`, `Deno.upgradeWebSocket`, `Bun.serve` upgrade) build\n * one of these and pass it to `registry.register()`.\n */\nexport interface PairingConnection {\n send(frame: ServerFrame): void\n onFrame(handler: (f: ClientFrame) => void): void\n onClose(handler: () => void): void\n close(): void\n}\n\n/**\n * A per-call frame subscriber. Return `true` to remove this\n * subscriber (one-shot), or `false` to keep receiving. The registry\n * dispatches every inbound `ClientFrame` to every active subscriber\n * for the given `tid`; subscribers filter by `frame.t` + identifiers\n * (correlation id, confirm id, state path) to find the one that\n * belongs to their request.\n */\nexport type FrameSubscriber = (frame: ClientFrame) => boolean\n\n/**\n * Registry of live browser pairings. Pure routing + hello cache —\n * request-lifecycle state (in-flight RPC promises, confirm waits,\n * long-polls) lives in the LAP handlers that need it, not here.\n *\n * Two implementations ship today:\n * - `InMemoryPairingRegistry` for long-lived server processes\n * (Node, Bun, Deno, Deno Deploy).\n * - A Cloudflare Durable Object implementation (see\n * `server/cloudflare`) for stateless Worker runtimes.\n *\n * Other runtimes can implement this interface the same way; the\n * contract is intentionally small.\n */\nexport interface PairingRegistry {\n // ── Routing primitives ─────────────────────────────────────────\n register(tid: string, conn: PairingConnection): void\n unregister(tid: string): void\n isPaired(tid: string): boolean\n getHello(tid: string): HelloFrame | null\n /** Send a frame. No-op when the pairing is absent or closed. */\n send(tid: string, frame: ServerFrame): void\n /**\n * Subscribe to frames from the paired browser. Returns an\n * unsubscribe function. A subscriber can remove itself mid-dispatch\n * by returning `true` from its callback — useful for one-shot\n * request/response correlation.\n */\n subscribe(tid: string, handler: FrameSubscriber): () => void\n /**\n * Observe the pairing closing (WebSocket drop, `unregister`, etc.).\n * Handlers registered before close fire; handlers registered after\n * close fire synchronously. Returns an unsubscribe function.\n */\n onClose(tid: string, handler: () => void): () => void\n\n /**\n * Read the most recent `n` log entries for a tid (newest first).\n * Backed by an in-memory ring buffer populated as the registry\n * sees `log-append` frames; capped per-tid to bound memory across\n * long-lived sessions. Drained on close. Returns an empty array\n * for unknown tids.\n */\n getRecentLog(tid: string, n: number): LogEntry[]\n\n /**\n * Long-poll for the next user-input submission from the paired\n * runtime. The registry buffers a small number of submissions\n * received with no waiter parked (so a user typing before Claude\n * reaches the tool call doesn't lose the message); when a waiter\n * parks with a non-empty buffer it resolves immediately with the\n * oldest buffered submission. When the buffer is empty, the waiter\n * sleeps until a `user-input-submitted` frame arrives, the WS\n * pairing closes, or `timeoutMs` elapses.\n *\n * FIFO delivery: each submission is consumed by exactly one waiter.\n * Multiple parked waiters form a queue; submissions are dispatched\n * in arrival order to the head of the waiter queue.\n */\n waitForUserInput(tid: string, timeoutMs: number): Promise<UserInputResolution>\n\n // ── Request/response helpers ───────────────────────────────────\n // These are part of the contract (LAP handlers call them directly)\n // but implementations almost always delegate to the free helpers in\n // `./rpc.ts`, which are built on the routing primitives above. The\n // Cloudflare Durable Object registry uses the same helpers; the\n // split exists so the routing surface is small enough to implement\n // across stateful boundaries (DO storage, WebSocket hibernation),\n // while the correlation logic lives once in a runtime-neutral file.\n\n /**\n * Send a typed rpc frame and await its matching reply. See\n * `./rpc.ts::rpc` for the full contract.\n */\n rpc(tid: string, tool: string, args: unknown, opts?: RpcOptions): Promise<unknown>\n /** See `./rpc.ts::waitForConfirm`. */\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }>\n /** See `./rpc.ts::waitForChange`. */\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }>\n}\n\ntype Pairing = {\n conn: PairingConnection\n hello: HelloFrame | null\n subscribers: Set<FrameSubscriber>\n closeHandlers: Set<() => void>\n closed: boolean\n /**\n * Buffered user-input submissions awaiting a parked waiter.\n * Bounded to USER_INPUT_BUFFER_CAP to prevent unbounded memory\n * growth if Claude never calls `wait_for_user_input` while the\n * user keeps typing. Buffer overflow drops the OLDEST entry —\n * fresher messages are more likely to be relevant.\n */\n userInputBuffer: Array<{ text: string; at: number }>\n /**\n * Waiters parked on `waitForUserInput`. The shape carries both the\n * outer promise's resolver (typed for the union, so `handleClose`\n * can resolve as `timeout` if the pairing dies) and a `cancel`\n * that tears down the waiter's per-call timer + close subscription.\n */\n userInputWaiters: Array<UserInputWaiter>\n}\n\ntype UserInputWaiter = {\n resolve: (value: UserInputResolution) => void\n cancel: () => void\n}\n\n/**\n * Single-process in-memory registry. Correct for Node/Bun/Deno/Deno\n * Deploy — anywhere the server process can hold a long-lived\n * WebSocket. Not suitable for stateless Worker isolates; use the\n * Durable Object registry for Cloudflare.\n */\n/**\n * Per-tid cap on the recent-log ring buffer. Sized to cover a few\n * minutes of agent activity at typical dispatch rates without\n * growing unboundedly for long-lived sessions. Reads via\n * `getRecentLog` clamp to this; agents asking for more get whatever\n * the buffer currently holds.\n */\nconst RECENT_LOG_CAP = 100\n\n/**\n * Per-tid cap on the user-input buffer (submissions received with no\n * waiter parked). Eight messages covers a typical \"user types a few\n * follow-ups while Claude is mid-tool-call\" gap without leaking memory\n * if no agent ever drains them. Overflow drops oldest (newer messages\n * are more contextually relevant).\n */\nconst USER_INPUT_BUFFER_CAP = 8\n\nexport class InMemoryPairingRegistry implements PairingRegistry {\n private pairings = new Map<string, Pairing>()\n private onLogAppend: ((tid: string, entry: LogEntry) => void) | null\n private userInputStorage: UserInputStorage | null\n /**\n * Per-tid ring buffer of recent log entries. Populated as the\n * registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.\n * The agent reads this via `describe_recent_actions` to introspect\n * its own activity history with stateDiffs intact.\n */\n private recentLog = new Map<string, LogEntry[]>()\n\n constructor(\n opts: {\n onLogAppend?: (tid: string, entry: LogEntry) => void\n /**\n * Optional adapter for persisting the user-input buffer across\n * runtime restarts (Cloudflare DO eviction, mainly). See\n * `UserInputStorage` for the contract. Omit on Node/Bun/Deno —\n * those runtimes don't need it.\n */\n userInputStorage?: UserInputStorage\n } = {},\n ) {\n this.onLogAppend = opts.onLogAppend ?? null\n this.userInputStorage = opts.userInputStorage ?? null\n }\n\n /**\n * Read the most recent `n` log entries for a tid, newest-first. Returns\n * an empty array when the tid is unknown or has no recorded activity.\n * Drained from the in-memory ring buffer; entries older than\n * RECENT_LOG_CAP have already been trimmed.\n */\n getRecentLog(tid: string, n: number): LogEntry[] {\n const buf = this.recentLog.get(tid)\n if (!buf || buf.length === 0) return []\n const count = Math.min(Math.max(0, Math.floor(n)), buf.length)\n if (count === 0) return []\n // Buffer is append-order; return the tail reversed so newest is first.\n return buf.slice(-count).reverse()\n }\n\n register(tid: string, conn: PairingConnection): void {\n const p: Pairing = {\n conn,\n hello: null,\n subscribers: new Set(),\n closeHandlers: new Set(),\n closed: false,\n userInputBuffer: [],\n userInputWaiters: [],\n }\n this.pairings.set(tid, p)\n conn.onFrame((frame) => this.dispatch(tid, frame))\n conn.onClose(() => this.handleClose(tid))\n // Best-effort restore of any persisted buffer. The promise is NOT\n // awaited so the synchronous register() contract is preserved;\n // submissions arriving in-flight queue up in the in-memory buffer\n // and the restored entries simply prepend (oldest-first) when the\n // read resolves. If the read rejects, we treat it as an empty\n // restore — the conversation continues, just without any messages\n // from before the eviction.\n if (this.userInputStorage) {\n const storage = this.userInputStorage\n storage.read(tid).then(\n (entries) => {\n if (!entries || entries.length === 0) return\n const live = this.pairings.get(tid)\n if (!live || live !== p || live.closed) return // pairing already closed/replaced\n // Restored entries are older than anything that arrived in\n // the meantime. Prepend, then re-cap to USER_INPUT_BUFFER_CAP.\n live.userInputBuffer.unshift(...entries)\n if (live.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {\n live.userInputBuffer.splice(0, live.userInputBuffer.length - USER_INPUT_BUFFER_CAP)\n }\n },\n () => {\n // Storage failure — log and continue. The conversation works\n // without persistence; we just lost any pre-eviction messages.\n },\n )\n }\n }\n\n unregister(tid: string): void {\n this.handleClose(tid)\n }\n\n isPaired(tid: string): boolean {\n const p = this.pairings.get(tid)\n return !!p && !p.closed\n }\n\n getHello(tid: string): HelloFrame | null {\n return this.pairings.get(tid)?.hello ?? null\n }\n\n send(tid: string, frame: ServerFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n try {\n p.conn.send(frame)\n } catch {\n // Connection may have dropped between isPaired() and send(); no-op.\n }\n }\n\n subscribe(tid: string, handler: FrameSubscriber): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return () => {}\n p.subscribers.add(handler)\n return () => {\n p.subscribers.delete(handler)\n }\n }\n\n onClose(tid: string, handler: () => void): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) {\n // Already closed — fire synchronously so callers don't hang\n // waiting for a close that already happened.\n queueMicrotask(handler)\n return () => {}\n }\n p.closeHandlers.add(handler)\n return () => {\n p.closeHandlers.delete(handler)\n }\n }\n\n private dispatch(tid: string, frame: ClientFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n // hello and log-append are registry-owned side effects — handled\n // here so no per-call subscriber has to pick them up.\n if (frame.t === 'hello') {\n p.hello = frame\n return\n }\n if (frame.t === 'log-append') {\n // Push into the ring buffer for `describe_recent_actions`,\n // capped to RECENT_LOG_CAP. The audit-sink callback runs\n // alongside; both are independent observers of the same\n // log-append stream.\n let buf = this.recentLog.get(tid)\n if (!buf) {\n buf = []\n this.recentLog.set(tid, buf)\n }\n buf.push(frame.entry)\n if (buf.length > RECENT_LOG_CAP) {\n buf.splice(0, buf.length - RECENT_LOG_CAP)\n }\n this.onLogAppend?.(tid, frame.entry)\n return\n }\n if (frame.t === 'user-input-submitted') {\n // FIFO delivery to a parked waiter, else buffer.\n // Shifting the head keeps \"first parked\" semantics; the parked\n // promise's cancel tears down its own timer before resolving.\n const waiter = p.userInputWaiters.shift()\n if (waiter) {\n waiter.cancel()\n waiter.resolve({ status: 'submitted', text: frame.text, at: frame.at })\n return\n }\n p.userInputBuffer.push({ text: frame.text, at: frame.at })\n if (p.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {\n // Drop oldest. Newer messages are more contextually relevant\n // for an agent picking up a stale conversation.\n p.userInputBuffer.splice(0, p.userInputBuffer.length - USER_INPUT_BUFFER_CAP)\n }\n this.persistUserInputBuffer(tid, p.userInputBuffer)\n return\n }\n // Iterate over a snapshot because subscribers may self-remove\n // mid-iteration by returning true.\n const snapshot = Array.from(p.subscribers)\n for (const sub of snapshot) {\n try {\n if (sub(frame)) p.subscribers.delete(sub)\n } catch {\n // One bad subscriber shouldn't break the others.\n p.subscribers.delete(sub)\n }\n }\n }\n\n // ── Convenience wrappers ───────────────────────────────────────\n // The following methods delegate to the free-function helpers in\n // `./rpc.ts`. They're here so the in-memory registry remains a\n // one-stop testing surface (spy on `registry.rpc`, etc.) without\n // couping the `PairingRegistry` interface to request-lifecycle\n // details. External implementations (e.g. the Cloudflare Durable\n // Object registry) are NOT required to provide these; the LAP\n // handlers always go through the free helpers.\n\n rpc(tid: string, tool: string, args: unknown, opts: RpcOptions = {}): Promise<unknown> {\n return rpcHelper(this, tid, tool, args, opts)\n }\n\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }> {\n return waitForConfirmHelper(this, tid, confirmId, timeoutMs)\n }\n\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }> {\n return waitForChangeHelper(this, tid, path, timeoutMs)\n }\n\n waitForUserInput(tid: string, timeoutMs: number): Promise<UserInputResolution> {\n const p = this.pairings.get(tid)\n // Unknown / closed pairing → resolve as timeout immediately. The\n // LAP layer above us has already gated on `isPaired`, so this is\n // the rare race where the pairing closed between the gate and the\n // wait call. Returning timeout (instead of rejecting) keeps the\n // public response shape simple — agents only need to handle two\n // outcomes.\n if (!p || p.closed) {\n return Promise.resolve({ status: 'timeout' })\n }\n\n // Buffered submission already waiting → resolve synchronously.\n const buffered = p.userInputBuffer.shift()\n if (buffered) {\n this.persistUserInputBuffer(tid, p.userInputBuffer)\n return Promise.resolve({ status: 'submitted', text: buffered.text, at: buffered.at })\n }\n\n return new Promise<UserInputResolution>((resolve) => {\n let settled = false\n const settle = (value: UserInputResolution): void => {\n if (settled) return\n settled = true\n resolve(value)\n }\n\n const timer = setTimeout(() => {\n const idx = p.userInputWaiters.findIndex((w) => w === waiter)\n if (idx !== -1) p.userInputWaiters.splice(idx, 1)\n settle({ status: 'timeout' })\n }, timeoutMs)\n\n const waiter: UserInputWaiter = {\n resolve: (value) => {\n // dispatch() / handleClose() both call this. The `settled`\n // guard makes it idempotent — exactly one resolution survives,\n // regardless of arrival order.\n settle(value)\n },\n cancel: () => {\n clearTimeout(timer)\n unsubClose()\n },\n }\n p.userInputWaiters.push(waiter)\n\n // Pairing close before resolution: clean up and resolve as\n // timeout. handleClose sweeps the waiter queue and calls\n // `waiter.resolve({ status: 'timeout' })` directly, so this\n // close subscription is belt-and-braces — covers any path where\n // the registry's close cascade doesn't run (e.g. a custom\n // PairingConnection signalling close in an unusual order).\n const unsubClose = this.onClose(tid, () => {\n const idx = p.userInputWaiters.findIndex((w) => w === waiter)\n if (idx !== -1) p.userInputWaiters.splice(idx, 1)\n clearTimeout(timer)\n settle({ status: 'timeout' })\n })\n })\n }\n\n /** @deprecated Use `send(tid, frame)` directly; semantics are identical. */\n notify(tid: string, frame: ServerFrame): void {\n this.send(tid, frame)\n }\n\n /**\n * Fire-and-forget write-through to the optional storage adapter.\n * Snapshots the buffer (defensive copy — the in-memory array can\n * mutate again before the adapter's write resolves) and ignores\n * rejections. Called on every buffer mutation; cheap when the\n * adapter is unset (no-ops) and best-effort when wired.\n */\n private persistUserInputBuffer(tid: string, buffer: Array<{ text: string; at: number }>): void {\n if (!this.userInputStorage) return\n // Defensive copy so the persisted snapshot is the buffer at the\n // moment of mutation, not whatever the buffer holds when the\n // storage adapter actually serializes the write.\n const snapshot = buffer.slice()\n void this.userInputStorage.write(tid, snapshot).catch(() => {\n /* best-effort; storage failures don't wedge the conversation */\n })\n }\n\n private handleClose(tid: string): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n p.closed = true\n // Tear down user-input waiters BEFORE running closeHandlers.\n // Each waiter's `cancel` clears its timer + close-subscription;\n // resolving as `timeout` afterward is idempotent (the waiter's\n // `settled` guard short-circuits if its own close handler already\n // resolved it). Order matters: cancel first so the resolution\n // path can't re-enter handleClose via the close-subscription.\n const waiters = p.userInputWaiters.slice()\n p.userInputWaiters.length = 0\n for (const w of waiters) {\n try {\n w.cancel()\n } catch {\n // best-effort; resolution still proceeds\n }\n w.resolve({ status: 'timeout' })\n }\n // Drop buffered inputs — once the pairing is closed, an agent\n // resuming on a fresh tid shouldn't see stale messages from the\n // previous session. Same retention contract as recentLog.\n p.userInputBuffer.length = 0\n if (this.userInputStorage) {\n void this.userInputStorage.clear(tid).catch(() => {\n /* best-effort; storage failures don't block the close path */\n })\n }\n for (const h of Array.from(p.closeHandlers)) {\n try {\n h()\n } catch {\n // Swallow — handlers run best-effort.\n }\n }\n p.closeHandlers.clear()\n p.subscribers.clear()\n this.pairings.delete(tid)\n // Drop the recent-log ring buffer — once the pairing is gone,\n // `describe_recent_actions` will reject anyway (paused/revoked\n // gates run before the registry lookup), but holding the entries\n // would leak memory across reconnects.\n this.recentLog.delete(tid)\n }\n}\n\n/**\n * Back-compat alias for the prior class name. New code should use\n * `InMemoryPairingRegistry`. Removed in a future major.\n *\n * @deprecated Use `InMemoryPairingRegistry` directly.\n */\nexport const WsPairingRegistry = InMemoryPairingRegistry\nexport type WsPairingRegistry = InMemoryPairingRegistry\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/agent",
3
- "version": "0.0.48",
3
+ "version": "0.0.50",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -31,21 +31,23 @@
31
31
  "./codecs": {
32
32
  "types": "./dist/codecs.d.ts",
33
33
  "import": "./dist/codecs.js"
34
- }
34
+ },
35
+ "./styles/agent-panel.css": "./styles/agent-panel.css"
35
36
  },
36
37
  "files": [
37
- "dist"
38
+ "dist",
39
+ "styles"
38
40
  ],
39
41
  "dependencies": {
40
42
  "ws": "^8.18.0"
41
43
  },
42
44
  "peerDependencies": {
43
- "@llui/dom": "^0.0.35"
45
+ "@llui/dom": "^0.0.36"
44
46
  },
45
47
  "devDependencies": {
46
48
  "@types/node": "^22.0.0",
47
49
  "@types/ws": "^8.5.13",
48
- "@llui/dom": "0.0.35"
50
+ "@llui/dom": "0.0.36"
49
51
  },
50
52
  "description": "LLui Agent — LAP server + browser client runtime for driving LLui apps from LLM clients",
51
53
  "keywords": [
@@ -0,0 +1,153 @@
1
+ /*
2
+ * @llui/agent default styles
3
+ *
4
+ * Opt-in stylesheet hosts can import as a starting point. Provides:
5
+ *
6
+ * - the `.agent-flash` keyframe animation (the default class
7
+ * `agentAttention.flashClass(path)` returns when a region is in
8
+ * the spotlight)
9
+ * - a small set of CSS custom properties for tuning colors/timing
10
+ * without forking the keyframes
11
+ * - prefers-reduced-motion fallback so users who opt out of motion
12
+ * get an instant flash + quick fade instead of the pulse animation
13
+ * - data-attribute hooks that the namespace prop bags emit
14
+ * (`[data-scope=agent-attention]`, `[data-scope=agent-chat]`,
15
+ * `[data-scope=agent-log]`) so hosts can target them directly
16
+ * without writing extra class hooks
17
+ *
18
+ * Import:
19
+ * import '@llui/agent/styles/agent-panel.css'
20
+ *
21
+ * Or copy + extend. The values here aim for "obvious enough that the
22
+ * panel works on first paint"; production apps will want to override
23
+ * the custom properties to match their design tokens.
24
+ */
25
+
26
+ :root {
27
+ /*
28
+ * Spotlight color. Default: a translucent saturated blue that reads
29
+ * on both light + dark backgrounds without blocking content
30
+ * underneath. Apps with their own accent should set
31
+ * --llui-agent-flash-color: var(--brand-accent-translucent);
32
+ * to integrate.
33
+ */
34
+ --llui-agent-flash-color: rgba(59, 130, 246, 0.35);
35
+ /*
36
+ * Spotlight outline color (the brief border that traces the region's
37
+ * shape on flash). Slightly more opaque than the fill so the edge
38
+ * pops without looking heavy.
39
+ */
40
+ --llui-agent-flash-outline: rgba(59, 130, 246, 0.6);
41
+ /*
42
+ * Animation duration. Pair with `agentAttention`'s `flashDurationMs`:
43
+ * the JS auto-clear should fire shortly AFTER the CSS animation ends
44
+ * so the user sees the full pulse before state cleanup. The default
45
+ * 600ms here matches `agentAttention`'s 600ms reducer default.
46
+ */
47
+ --llui-agent-flash-duration: 600ms;
48
+ --llui-agent-flash-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
49
+ }
50
+
51
+ /*
52
+ * Spotlight pulse. Three-phase keyframe:
53
+ * 0% — background instantly applied + outline visible
54
+ * 30% — fully saturated (the "look here" peak)
55
+ * 100% — back to transparent (smoothly faded out)
56
+ *
57
+ * Border is pulled in via box-shadow rather than a real border so the
58
+ * highlight doesn't shift layout (a real border would push siblings).
59
+ * The `outline` property would also work but doesn't follow
60
+ * border-radius on every browser; box-shadow does.
61
+ */
62
+ @keyframes llui-agent-flash {
63
+ 0% {
64
+ background-color: var(--llui-agent-flash-color);
65
+ box-shadow: 0 0 0 2px var(--llui-agent-flash-outline);
66
+ }
67
+ 30% {
68
+ background-color: var(--llui-agent-flash-color);
69
+ box-shadow: 0 0 0 2px var(--llui-agent-flash-outline);
70
+ }
71
+ 100% {
72
+ background-color: transparent;
73
+ box-shadow: 0 0 0 0 transparent;
74
+ }
75
+ }
76
+
77
+ .agent-flash {
78
+ /* `forwards` keeps the end-state (transparent) so JS can re-trigger
79
+ the animation cleanly when the next dispatch lands on the same
80
+ region without an interim repaint flicker. */
81
+ animation: llui-agent-flash var(--llui-agent-flash-duration) var(--llui-agent-flash-easing)
82
+ forwards;
83
+ /* Round the spotlight to whatever the host's element already uses;
84
+ no shape coupling here. */
85
+ border-radius: inherit;
86
+ }
87
+
88
+ /*
89
+ * Reduced motion: skip the keyframe pulse, do a 1-frame highlight that
90
+ * fades over a shorter window. Same visual cue (here's where the
91
+ * action landed) without animating.
92
+ */
93
+ @media (prefers-reduced-motion: reduce) {
94
+ @keyframes llui-agent-flash {
95
+ 0% {
96
+ background-color: var(--llui-agent-flash-color);
97
+ }
98
+ 100% {
99
+ background-color: transparent;
100
+ }
101
+ }
102
+ .agent-flash {
103
+ animation-duration: 200ms;
104
+ }
105
+ }
106
+
107
+ /*
108
+ * Activity log: per-entry kind colour hints. The bag's
109
+ * `entryItem(id)` returns `'data-kind'` as a reactive accessor —
110
+ * spread into the host's row element and these styles light up.
111
+ *
112
+ * These are mild; hosts are expected to override for design
113
+ * consistency. The defaults exist so a panel works at all without
114
+ * extra CSS.
115
+ */
116
+ [data-scope='agent-log'] [data-part='entry'] {
117
+ /* Slim left border carrying the kind colour; doesn't shift layout. */
118
+ border-left: 3px solid transparent;
119
+ padding-left: 0.5rem;
120
+ }
121
+ [data-scope='agent-log'] [data-part='entry'][data-kind='dispatched'] {
122
+ border-left-color: rgba(34, 197, 94, 0.7); /* green: action landed */
123
+ }
124
+ [data-scope='agent-log'] [data-part='entry'][data-kind='proposed'] {
125
+ border-left-color: rgba(234, 179, 8, 0.8); /* amber: awaiting confirm */
126
+ }
127
+ [data-scope='agent-log'] [data-part='entry'][data-kind='blocked'],
128
+ [data-scope='agent-log'] [data-part='entry'][data-kind='rejected'] {
129
+ border-left-color: rgba(239, 68, 68, 0.8); /* red: stop */
130
+ }
131
+ [data-scope='agent-log'] [data-part='entry'][data-kind='error'] {
132
+ border-left-color: rgba(239, 68, 68, 1); /* red: failure */
133
+ }
134
+ [data-scope='agent-log'] [data-part='entry'][data-kind='read'] {
135
+ border-left-color: rgba(148, 163, 184, 0.5); /* slate: passive */
136
+ }
137
+ [data-scope='agent-log'] [data-part='entry'][data-kind='user-input'] {
138
+ border-left-color: rgba(99, 102, 241, 0.7); /* indigo: user voice */
139
+ }
140
+ [data-scope='agent-log'] [data-part='entry'][data-kind='narrate'] {
141
+ border-left-color: rgba(168, 85, 247, 0.6); /* purple: agent voice */
142
+ font-style: italic;
143
+ }
144
+
145
+ /*
146
+ * Chat composer: subtle disabled state during in-flight submit so the
147
+ * user has a visible cue beyond the input element's own `disabled`
148
+ * styling (which varies by browser).
149
+ */
150
+ [data-scope='agent-chat'][data-submitting='true'] {
151
+ opacity: 0.7;
152
+ pointer-events: none;
153
+ }