@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
@@ -0,0 +1,129 @@
1
+ import type { AgentEffect } from './effects.js';
2
+ import type { LogEntry } from '../protocol.js';
3
+ /**
4
+ * Visual attention layer. Companion to `agentLog` — same `Append { entry }`
5
+ * input shape, different output. While `agentLog` ring-buffers the full
6
+ * activity history, `agentAttention` tracks only the most recent
7
+ * dispatched entry's effective metadata so the host's view can flash
8
+ * highlight classes onto DOM regions whose state paths just changed.
9
+ *
10
+ * Why a separate slice: the activity log is a passive timeline (read-only,
11
+ * no time-bounded expiry, no per-region accessors). The attention layer
12
+ * is a transient projection — the "current dispatch's spotlight" — that
13
+ * has to clear itself after a configurable window, decay across renders,
14
+ * and drive per-path accessors (one per highlightable region in the
15
+ * host's layout). Mixing these into one slice would conflate "I want
16
+ * to read past actions" with "I want to point at the current one."
17
+ *
18
+ * Composition contract: the host appends the SAME `LogEntry` payload
19
+ * to both slices on every `log-append` from the ws-client (typically
20
+ * via a single `agent/log/Append` Msg routed by `sliceHandler`). The
21
+ * attention reducer ignores entries whose `kind` isn't `'dispatched'`
22
+ * — proposed, blocked, error, and read entries don't update the
23
+ * attention focus, since they don't represent a state mutation.
24
+ *
25
+ * Auto-clear: the reducer fires an `AgentAttentionFlashTimeout` effect
26
+ * keyed by `entryId`. After `flashDurationMs`, the effect handler
27
+ * dispatches a `Clear { entryId }` Msg back into the slice. The
28
+ * conditional clear (only-if-current) means a fast follow-up dispatch
29
+ * cleanly replaces the spotlight without the timer racing to wipe the
30
+ * new one. Hosts that don't wire `wrapAttentionMsg` in the factory
31
+ * still see the spotlight set; it just won't auto-clear (the next
32
+ * dispatch overwrites it instead).
33
+ */
34
+ export type AgentAttentionState = {
35
+ /**
36
+ * The current dispatch's spotlight, or null when no dispatch has
37
+ * landed yet (or the auto-clear timer fired and `latestDispatch.entryId`
38
+ * matched).
39
+ */
40
+ latestDispatch: {
41
+ entryId: string;
42
+ /**
43
+ * Top-level state paths the dispatch touched, derived from the
44
+ * entry's JSON-Patch `stateDiff`. A whole-state replace (path
45
+ * `/`) collapses to the wildcard `'*'` so callers can match every
46
+ * region without enumerating their own state keys.
47
+ */
48
+ paths: string[];
49
+ variant?: string;
50
+ intent?: string;
51
+ at: number;
52
+ } | null;
53
+ /** Configurable: how long the spotlight persists before auto-clear. */
54
+ flashDurationMs: number;
55
+ };
56
+ export type AgentAttentionInitOpts = {
57
+ /** Default 600ms — long enough to read, short enough not to obscure. */
58
+ flashDurationMs?: number;
59
+ };
60
+ export type AgentAttentionMsg = {
61
+ /**
62
+ * Same shape as `agentLog`'s `Append` so the host can route a
63
+ * single incoming Msg to both slices via `sliceHandler` without
64
+ * a translation layer.
65
+ */
66
+ type: 'Append';
67
+ entry: LogEntry;
68
+ } | {
69
+ /**
70
+ * Fired by the auto-clear timer effect. Guarded by `entryId` —
71
+ * the reducer only clears when `latestDispatch.entryId` matches,
72
+ * so a fast follow-up dispatch isn't wiped by the previous
73
+ * dispatch's pending timer.
74
+ */
75
+ type: 'Clear';
76
+ entryId: string;
77
+ } | {
78
+ /**
79
+ * Adjust the flash duration at runtime. Persists in state so
80
+ * subsequent timeouts use the new value. Existing in-flight
81
+ * timers are not cancelled — they'll fire at their original
82
+ * delay, and the conditional clear handles the race.
83
+ */
84
+ type: 'SetFlashDuration';
85
+ ms: number;
86
+ };
87
+ export declare function init(opts?: AgentAttentionInitOpts): [AgentAttentionState, AgentEffect[]];
88
+ export declare function update(state: AgentAttentionState, msg: AgentAttentionMsg): [AgentAttentionState, AgentEffect[]];
89
+ import type { Send } from '@llui/dom';
90
+ export type ConnectBag<S> = {
91
+ root: {
92
+ 'data-scope': 'agent-attention';
93
+ };
94
+ /**
95
+ * Reactive boolean accessor: true while the spotlight covers `path`.
96
+ * Use as the predicate for a conditional class binding in the host's
97
+ * own element bag. Memoized by `path` so each `flashing(path)` call
98
+ * returns the same accessor across renders, keeping the underlying
99
+ * binding's `lastValue` short-circuit valid.
100
+ */
101
+ flashing: (path: string) => (s: S) => boolean;
102
+ /**
103
+ * Convenience accessor: returns `className` (default `'agent-flash'`)
104
+ * while flashing, otherwise `undefined`. Spread into element bags
105
+ * via `class: bag.flashClass('items')`. Memoized per `(path, className)`
106
+ * pair.
107
+ */
108
+ flashClass: (path: string, className?: string) => (s: S) => string | undefined;
109
+ /**
110
+ * Metadata about the action that touched this path, or null when
111
+ * the spotlight isn't on this path. Useful for tooltips or aria-live
112
+ * narration: "agent → SelectAlternative just changed alternatives."
113
+ * Memoized per `path`.
114
+ */
115
+ regionAction: (path: string) => (s: S) => {
116
+ entryId: string;
117
+ variant?: string;
118
+ intent?: string;
119
+ at: number;
120
+ } | null;
121
+ /**
122
+ * Direct accessor on the latest dispatch envelope. Useful for a
123
+ * single panel-level "now flashing: X" indicator outside the
124
+ * per-region instrumentation.
125
+ */
126
+ latestDispatch: (s: S) => AgentAttentionState['latestDispatch'];
127
+ };
128
+ export declare function connect<S>(get: (s: S) => AgentAttentionState, _send: Send<AgentAttentionMsg>): ConnectBag<S>;
129
+ //# sourceMappingURL=agentAttention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentAttention.d.ts","sourceRoot":"","sources":["../../src/client/agentAttention.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAG9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;OAIG;IACH,cAAc,EAAE;QACd,OAAO,EAAE,MAAM,CAAA;QACf;;;;;WAKG;QACH,KAAK,EAAE,MAAM,EAAE,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,EAAE,EAAE,MAAM,CAAA;KACX,GAAG,IAAI,CAAA;IACR,uEAAuE;IACvE,eAAe,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,iBAAiB,GACzB;IACE;;;;OAIG;IACH,IAAI,EAAE,QAAQ,CAAA;IACd,KAAK,EAAE,QAAQ,CAAA;CAChB,GACD;IACE;;;;;OAKG;IACH,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;CAChB,GACD;IACE;;;;;OAKG;IACH,IAAI,EAAE,kBAAkB,CAAA;IACxB,EAAE,EAAE,MAAM,CAAA;CACX,CAAA;AAIL,wBAAgB,IAAI,CAAC,IAAI,GAAE,sBAA2B,GAAG,CAAC,mBAAmB,EAAE,WAAW,EAAE,CAAC,CAQ5F;AAED,wBAAgB,MAAM,CACpB,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE,iBAAiB,GACrB,CAAC,mBAAmB,EAAE,WAAW,EAAE,CAAC,CAuCtC;AAgCD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAIrC,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,IAAI,EAAE;QAAE,YAAY,EAAE,iBAAiB,CAAA;KAAE,CAAA;IACzC;;;;;;OAMG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAA;IAC9E;;;;;OAKG;IACH,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK;QACxC,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,EAAE,EAAE,MAAM,CAAA;KACX,GAAG,IAAI,CAAA;IACR;;;;OAIG;IACH,cAAc,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,mBAAmB,CAAC,gBAAgB,CAAC,CAAA;CAChE,CAAA;AAED,wBAAgB,OAAO,CAAC,CAAC,EACvB,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,mBAAmB,EAClC,KAAK,EAAE,IAAI,CAAC,iBAAiB,CAAC,GAC7B,UAAU,CAAC,CAAC,CAAC,CA2Ef"}
@@ -0,0 +1,156 @@
1
+ const DEFAULT_FLASH_MS = 600;
2
+ export function init(opts = {}) {
3
+ return [
4
+ {
5
+ latestDispatch: null,
6
+ flashDurationMs: opts.flashDurationMs ?? DEFAULT_FLASH_MS,
7
+ },
8
+ [],
9
+ ];
10
+ }
11
+ export function update(state, msg) {
12
+ switch (msg.type) {
13
+ case 'Append': {
14
+ const entry = msg.entry;
15
+ // Only dispatched entries update the spotlight. Read / proposed /
16
+ // blocked / error / user-input entries don't represent a state
17
+ // mutation, so the visual cue is meaningless for them.
18
+ if (entry.kind !== 'dispatched')
19
+ return [state, []];
20
+ const paths = topLevelPaths(entry.stateDiff);
21
+ // No diff (or empty diff) means the dispatch landed but nothing
22
+ // changed — silent success. Skip the spotlight rather than
23
+ // flashing nothing in particular.
24
+ if (paths.length === 0)
25
+ return [state, []];
26
+ const next = {
27
+ ...state,
28
+ latestDispatch: {
29
+ entryId: entry.id,
30
+ paths,
31
+ variant: entry.variant,
32
+ intent: entry.intent,
33
+ at: entry.at,
34
+ },
35
+ };
36
+ return [
37
+ next,
38
+ [{ type: 'AgentAttentionFlashTimeout', entryId: entry.id, delayMs: state.flashDurationMs }],
39
+ ];
40
+ }
41
+ case 'Clear': {
42
+ // Conditional clear: only wipe if the timer's entryId still
43
+ // matches the current spotlight. If a newer dispatch landed in
44
+ // the meantime, the older timer is a no-op.
45
+ if (state.latestDispatch?.entryId !== msg.entryId)
46
+ return [state, []];
47
+ return [{ ...state, latestDispatch: null }, []];
48
+ }
49
+ case 'SetFlashDuration': {
50
+ return [{ ...state, flashDurationMs: Math.max(0, msg.ms) }, []];
51
+ }
52
+ }
53
+ }
54
+ /**
55
+ * Extract top-level state paths from a JSON-Patch StateDiff.
56
+ *
57
+ * - `op.path === '/'` (root replace) → wildcard `'*'`. The host's
58
+ * accessors match every region against `'*'`, so a whole-state
59
+ * swap (rare in TEA but possible via dev hot-reload, time-travel
60
+ * restore) flashes everything.
61
+ * - `op.path === '/items/3/name'` → `'items'`. Multi-segment paths
62
+ * collapse to their top-level field; per-region matching is at
63
+ * field granularity, not deep-path granularity, because the host's
64
+ * layout typically maps regions to top-level state slices, not to
65
+ * deep cells.
66
+ * - Empty / undefined diff → empty array. No spotlight.
67
+ */
68
+ function topLevelPaths(diff) {
69
+ if (!diff || diff.length === 0)
70
+ return [];
71
+ const seen = new Set();
72
+ for (const op of diff) {
73
+ const path = op.path;
74
+ if (path === '' || path === '/') {
75
+ seen.add('*');
76
+ continue;
77
+ }
78
+ // JSON-Pointer: leading '/' splits into ['', '<top>', '<rest>...'].
79
+ const parts = path.split('/');
80
+ if (parts.length >= 2 && parts[1])
81
+ seen.add(parts[1]);
82
+ }
83
+ return Array.from(seen);
84
+ }
85
+ const UNSET = Symbol('agent-attention-unset');
86
+ export function connect(get, _send) {
87
+ // Per-call-shape accessor caches. Two reasons for memoizing:
88
+ // 1. Stable reference per `(path, className)` lets the underlying
89
+ // LLui binding short-circuit on `Object.is(lastValue, newValue)`
90
+ // — without it, `bag.flashing('items')` would allocate a fresh
91
+ // closure each call and the binding would re-fire even when the
92
+ // state hasn't changed.
93
+ // 2. Hosts iterate `each(visibleEntries, ...)` calling these in tight
94
+ // inner loops; per-render allocation costs would compound.
95
+ const flashingCache = new Map();
96
+ const flashClassCache = new Map();
97
+ const regionActionCache = new Map();
98
+ // Single-slot memo on the parent state ref for `flashing(path)`'s
99
+ // path → boolean lookup. Hot path: each frame's view evaluates
100
+ // every region's `flashing(path)` once; on a state where 30 regions
101
+ // are wired and one is highlighted, the inclusion check is trivial,
102
+ // but the parent-state-ref invariant still saves the `get(s)` and
103
+ // `Array.includes` work for the other 29.
104
+ let lastFlashState = UNSET;
105
+ let lastDispatch = null;
106
+ const refreshDispatch = (state) => {
107
+ if (state === lastFlashState)
108
+ return lastDispatch;
109
+ lastDispatch = get(state).latestDispatch;
110
+ lastFlashState = state;
111
+ return lastDispatch;
112
+ };
113
+ const matches = (state, path) => {
114
+ const d = refreshDispatch(state);
115
+ if (!d)
116
+ return false;
117
+ return d.paths.includes('*') || d.paths.includes(path);
118
+ };
119
+ return {
120
+ root: { 'data-scope': 'agent-attention' },
121
+ flashing: (path) => {
122
+ const cached = flashingCache.get(path);
123
+ if (cached)
124
+ return cached;
125
+ const accessor = (s) => matches(s, path);
126
+ flashingCache.set(path, accessor);
127
+ return accessor;
128
+ },
129
+ flashClass: (path, className = 'agent-flash') => {
130
+ const key = `${path}\0${className}`;
131
+ const cached = flashClassCache.get(key);
132
+ if (cached)
133
+ return cached;
134
+ const accessor = (s) => (matches(s, path) ? className : undefined);
135
+ flashClassCache.set(key, accessor);
136
+ return accessor;
137
+ },
138
+ regionAction: (path) => {
139
+ const cached = regionActionCache.get(path);
140
+ if (cached)
141
+ return cached;
142
+ const accessor = (s) => {
143
+ const d = refreshDispatch(s);
144
+ if (!d)
145
+ return null;
146
+ if (!d.paths.includes('*') && !d.paths.includes(path))
147
+ return null;
148
+ return { entryId: d.entryId, variant: d.variant, intent: d.intent, at: d.at };
149
+ };
150
+ regionActionCache.set(path, accessor);
151
+ return accessor;
152
+ },
153
+ latestDispatch: (s) => get(s).latestDispatch,
154
+ };
155
+ }
156
+ //# sourceMappingURL=agentAttention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentAttention.js","sourceRoot":"","sources":["../../src/client/agentAttention.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentEffect } from './effects.js'\nimport type { LogEntry } from '../protocol.js'\nimport type { StateDiff } from '../state-diff.js'\n\n/**\n * Visual attention layer. Companion to `agentLog` — same `Append { entry }`\n * input shape, different output. While `agentLog` ring-buffers the full\n * activity history, `agentAttention` tracks only the most recent\n * dispatched entry's effective metadata so the host's view can flash\n * highlight classes onto DOM regions whose state paths just changed.\n *\n * Why a separate slice: the activity log is a passive timeline (read-only,\n * no time-bounded expiry, no per-region accessors). The attention layer\n * is a transient projection — the \"current dispatch's spotlight\" — that\n * has to clear itself after a configurable window, decay across renders,\n * and drive per-path accessors (one per highlightable region in the\n * host's layout). Mixing these into one slice would conflate \"I want\n * to read past actions\" with \"I want to point at the current one.\"\n *\n * Composition contract: the host appends the SAME `LogEntry` payload\n * to both slices on every `log-append` from the ws-client (typically\n * via a single `agent/log/Append` Msg routed by `sliceHandler`). The\n * attention reducer ignores entries whose `kind` isn't `'dispatched'`\n * — proposed, blocked, error, and read entries don't update the\n * attention focus, since they don't represent a state mutation.\n *\n * Auto-clear: the reducer fires an `AgentAttentionFlashTimeout` effect\n * keyed by `entryId`. After `flashDurationMs`, the effect handler\n * dispatches a `Clear { entryId }` Msg back into the slice. The\n * conditional clear (only-if-current) means a fast follow-up dispatch\n * cleanly replaces the spotlight without the timer racing to wipe the\n * new one. Hosts that don't wire `wrapAttentionMsg` in the factory\n * still see the spotlight set; it just won't auto-clear (the next\n * dispatch overwrites it instead).\n */\nexport type AgentAttentionState = {\n /**\n * The current dispatch's spotlight, or null when no dispatch has\n * landed yet (or the auto-clear timer fired and `latestDispatch.entryId`\n * matched).\n */\n latestDispatch: {\n entryId: string\n /**\n * Top-level state paths the dispatch touched, derived from the\n * entry's JSON-Patch `stateDiff`. A whole-state replace (path\n * `/`) collapses to the wildcard `'*'` so callers can match every\n * region without enumerating their own state keys.\n */\n paths: string[]\n variant?: string\n intent?: string\n at: number\n } | null\n /** Configurable: how long the spotlight persists before auto-clear. */\n flashDurationMs: number\n}\n\nexport type AgentAttentionInitOpts = {\n /** Default 600ms — long enough to read, short enough not to obscure. */\n flashDurationMs?: number\n}\n\nexport type AgentAttentionMsg =\n | {\n /**\n * Same shape as `agentLog`'s `Append` so the host can route a\n * single incoming Msg to both slices via `sliceHandler` without\n * a translation layer.\n */\n type: 'Append'\n entry: LogEntry\n }\n | {\n /**\n * Fired by the auto-clear timer effect. Guarded by `entryId` —\n * the reducer only clears when `latestDispatch.entryId` matches,\n * so a fast follow-up dispatch isn't wiped by the previous\n * dispatch's pending timer.\n */\n type: 'Clear'\n entryId: string\n }\n | {\n /**\n * Adjust the flash duration at runtime. Persists in state so\n * subsequent timeouts use the new value. Existing in-flight\n * timers are not cancelled — they'll fire at their original\n * delay, and the conditional clear handles the race.\n */\n type: 'SetFlashDuration'\n ms: number\n }\n\nconst DEFAULT_FLASH_MS = 600\n\nexport function init(opts: AgentAttentionInitOpts = {}): [AgentAttentionState, AgentEffect[]] {\n return [\n {\n latestDispatch: null,\n flashDurationMs: opts.flashDurationMs ?? DEFAULT_FLASH_MS,\n },\n [],\n ]\n}\n\nexport function update(\n state: AgentAttentionState,\n msg: AgentAttentionMsg,\n): [AgentAttentionState, AgentEffect[]] {\n switch (msg.type) {\n case 'Append': {\n const entry = msg.entry\n // Only dispatched entries update the spotlight. Read / proposed /\n // blocked / error / user-input entries don't represent a state\n // mutation, so the visual cue is meaningless for them.\n if (entry.kind !== 'dispatched') return [state, []]\n const paths = topLevelPaths(entry.stateDiff)\n // No diff (or empty diff) means the dispatch landed but nothing\n // changed — silent success. Skip the spotlight rather than\n // flashing nothing in particular.\n if (paths.length === 0) return [state, []]\n const next: AgentAttentionState = {\n ...state,\n latestDispatch: {\n entryId: entry.id,\n paths,\n variant: entry.variant,\n intent: entry.intent,\n at: entry.at,\n },\n }\n return [\n next,\n [{ type: 'AgentAttentionFlashTimeout', entryId: entry.id, delayMs: state.flashDurationMs }],\n ]\n }\n case 'Clear': {\n // Conditional clear: only wipe if the timer's entryId still\n // matches the current spotlight. If a newer dispatch landed in\n // the meantime, the older timer is a no-op.\n if (state.latestDispatch?.entryId !== msg.entryId) return [state, []]\n return [{ ...state, latestDispatch: null }, []]\n }\n case 'SetFlashDuration': {\n return [{ ...state, flashDurationMs: Math.max(0, msg.ms) }, []]\n }\n }\n}\n\n/**\n * Extract top-level state paths from a JSON-Patch StateDiff.\n *\n * - `op.path === '/'` (root replace) → wildcard `'*'`. The host's\n * accessors match every region against `'*'`, so a whole-state\n * swap (rare in TEA but possible via dev hot-reload, time-travel\n * restore) flashes everything.\n * - `op.path === '/items/3/name'` → `'items'`. Multi-segment paths\n * collapse to their top-level field; per-region matching is at\n * field granularity, not deep-path granularity, because the host's\n * layout typically maps regions to top-level state slices, not to\n * deep cells.\n * - Empty / undefined diff → empty array. No spotlight.\n */\nfunction topLevelPaths(diff: StateDiff | undefined): string[] {\n if (!diff || diff.length === 0) return []\n const seen = new Set<string>()\n for (const op of diff) {\n const path = op.path\n if (path === '' || path === '/') {\n seen.add('*')\n continue\n }\n // JSON-Pointer: leading '/' splits into ['', '<top>', '<rest>...'].\n const parts = path.split('/')\n if (parts.length >= 2 && parts[1]) seen.add(parts[1])\n }\n return Array.from(seen)\n}\n\nimport type { Send } from '@llui/dom'\n\nconst UNSET: unique symbol = Symbol('agent-attention-unset')\n\nexport type ConnectBag<S> = {\n root: { 'data-scope': 'agent-attention' }\n /**\n * Reactive boolean accessor: true while the spotlight covers `path`.\n * Use as the predicate for a conditional class binding in the host's\n * own element bag. Memoized by `path` so each `flashing(path)` call\n * returns the same accessor across renders, keeping the underlying\n * binding's `lastValue` short-circuit valid.\n */\n flashing: (path: string) => (s: S) => boolean\n /**\n * Convenience accessor: returns `className` (default `'agent-flash'`)\n * while flashing, otherwise `undefined`. Spread into element bags\n * via `class: bag.flashClass('items')`. Memoized per `(path, className)`\n * pair.\n */\n flashClass: (path: string, className?: string) => (s: S) => string | undefined\n /**\n * Metadata about the action that touched this path, or null when\n * the spotlight isn't on this path. Useful for tooltips or aria-live\n * narration: \"agent → SelectAlternative just changed alternatives.\"\n * Memoized per `path`.\n */\n regionAction: (path: string) => (s: S) => {\n entryId: string\n variant?: string\n intent?: string\n at: number\n } | null\n /**\n * Direct accessor on the latest dispatch envelope. Useful for a\n * single panel-level \"now flashing: X\" indicator outside the\n * per-region instrumentation.\n */\n latestDispatch: (s: S) => AgentAttentionState['latestDispatch']\n}\n\nexport function connect<S>(\n get: (s: S) => AgentAttentionState,\n _send: Send<AgentAttentionMsg>,\n): ConnectBag<S> {\n // Per-call-shape accessor caches. Two reasons for memoizing:\n // 1. Stable reference per `(path, className)` lets the underlying\n // LLui binding short-circuit on `Object.is(lastValue, newValue)`\n // — without it, `bag.flashing('items')` would allocate a fresh\n // closure each call and the binding would re-fire even when the\n // state hasn't changed.\n // 2. Hosts iterate `each(visibleEntries, ...)` calling these in tight\n // inner loops; per-render allocation costs would compound.\n const flashingCache = new Map<string, (s: S) => boolean>()\n const flashClassCache = new Map<string, (s: S) => string | undefined>()\n const regionActionCache = new Map<\n string,\n (s: S) => {\n entryId: string\n variant?: string\n intent?: string\n at: number\n } | null\n >()\n\n // Single-slot memo on the parent state ref for `flashing(path)`'s\n // path → boolean lookup. Hot path: each frame's view evaluates\n // every region's `flashing(path)` once; on a state where 30 regions\n // are wired and one is highlighted, the inclusion check is trivial,\n // but the parent-state-ref invariant still saves the `get(s)` and\n // `Array.includes` work for the other 29.\n let lastFlashState: S | typeof UNSET = UNSET\n let lastDispatch: AgentAttentionState['latestDispatch'] = null\n const refreshDispatch = (state: S): AgentAttentionState['latestDispatch'] => {\n if (state === lastFlashState) return lastDispatch\n lastDispatch = get(state).latestDispatch\n lastFlashState = state\n return lastDispatch\n }\n\n const matches = (state: S, path: string): boolean => {\n const d = refreshDispatch(state)\n if (!d) return false\n return d.paths.includes('*') || d.paths.includes(path)\n }\n\n return {\n root: { 'data-scope': 'agent-attention' },\n flashing: (path) => {\n const cached = flashingCache.get(path)\n if (cached) return cached\n const accessor = (s: S): boolean => matches(s, path)\n flashingCache.set(path, accessor)\n return accessor\n },\n flashClass: (path, className = 'agent-flash') => {\n const key = `${path}\\0${className}`\n const cached = flashClassCache.get(key)\n if (cached) return cached\n const accessor = (s: S): string | undefined => (matches(s, path) ? className : undefined)\n flashClassCache.set(key, accessor)\n return accessor\n },\n regionAction: (path) => {\n const cached = regionActionCache.get(path)\n if (cached) return cached\n const accessor = (\n s: S,\n ): { entryId: string; variant?: string; intent?: string; at: number } | null => {\n const d = refreshDispatch(s)\n if (!d) return null\n if (!d.paths.includes('*') && !d.paths.includes(path)) return null\n return { entryId: d.entryId, variant: d.variant, intent: d.intent, at: d.at }\n }\n regionActionCache.set(path, accessor)\n return accessor\n },\n latestDispatch: (s) => get(s).latestDispatch,\n }\n}\n"]}
@@ -0,0 +1,100 @@
1
+ import type { AgentEffect } from './effects.js';
2
+ /**
3
+ * In-app chat composer slice. Owns the editor-state half of the
4
+ * conversational surface (`pendingInput`, `submitting`); the
5
+ * timeline half lives in `agentLog` (which renders the user's
6
+ * submission as a `LogEntry { kind: 'user-input' }` alongside agent
7
+ * actions, so the conversation reads chronologically).
8
+ *
9
+ * Pattern: host wires the slice into its app state via
10
+ * `sliceHandler`, spreads `connect()`'s prop bag into the host's
11
+ * own input/button layout, and the `Submit` Msg fires an effect
12
+ * that the framework's effect handler turns into a WS
13
+ * `user-input-submitted` frame + a synthesized log entry. The
14
+ * agent's `wait_for_user_input` LAP tool picks up the frame at the
15
+ * server.
16
+ *
17
+ * The composer is NOT an LLM. It's a relay surface: the user's text
18
+ * is delivered to the user's own LLM (Claude desktop / IDE / wherever
19
+ * is mounted on the MCP bridge), which already has cross-app context.
20
+ * The framework just provides the in-app rendezvous so the user
21
+ * doesn't have to alt-tab to a different window to talk to their
22
+ * agent about the app they're looking at.
23
+ */
24
+ export type AgentChatState = {
25
+ /** Current contents of the input field. Bound to the input's `value`. */
26
+ pendingInput: string;
27
+ /**
28
+ * True between `Submit` and the effect handler completing the
29
+ * frame send. Disables the submit button and (typically) the
30
+ * input itself; reducer-driven so the UI never disagrees.
31
+ */
32
+ submitting: boolean;
33
+ };
34
+ export type AgentChatInitOpts = {
35
+ /** Pre-fill the input on mount — e.g. session-restore. */
36
+ initialInput?: string;
37
+ };
38
+ export type AgentChatMsg = {
39
+ /** Bound to the input's `oninput` event. Round-trip stays in the slice. */
40
+ type: 'SetInput';
41
+ value: string;
42
+ } | {
43
+ /**
44
+ * Bound to the submit button's `onClick` and the input's
45
+ * `onKeyDown` (Enter, no shift). Reducer:
46
+ *
47
+ * - Empty/whitespace → no-op (no effect, no state change).
48
+ * - Has content → clear `pendingInput`, set `submitting: true`,
49
+ * emit `AgentChatSendInput { text, at }` effect.
50
+ */
51
+ type: 'Submit';
52
+ } | {
53
+ /**
54
+ * Fired by the effect handler after the frame sends so the
55
+ * UI re-enables the input. Always paired 1:1 with each
56
+ * dispatched `AgentChatSendInput` effect.
57
+ */
58
+ type: 'SubmitComplete';
59
+ };
60
+ export declare function init(opts?: AgentChatInitOpts): [AgentChatState, AgentEffect[]];
61
+ export declare function update(state: AgentChatState, msg: AgentChatMsg): [AgentChatState, AgentEffect[]];
62
+ import { type Send } from '@llui/dom';
63
+ /**
64
+ * Static-prop-bag-with-reactive-accessors. Spread directly into
65
+ * element helpers; matches the convention of the other agent
66
+ * namespaces.
67
+ *
68
+ * The `input` bag carries both `oninput` and `onkeydown` because
69
+ * the canonical chat-composer affordance is "type, press Enter"
70
+ * (Shift+Enter for newline if the host wraps it themselves). Hosts
71
+ * that want a multiline textarea can spread `input.oninput` and
72
+ * skip `onkeydown`, wiring submit to the button only.
73
+ */
74
+ export type ConnectBag<S> = {
75
+ root: {
76
+ 'data-scope': 'agent-chat';
77
+ 'data-submitting': (s: S) => boolean;
78
+ };
79
+ input: {
80
+ 'data-part': 'input';
81
+ value: (s: S) => string;
82
+ disabled: (s: S) => boolean;
83
+ oninput: (e: Event) => void;
84
+ onkeydown: (e: KeyboardEvent) => void;
85
+ };
86
+ submitButton: {
87
+ 'data-part': 'submit';
88
+ onClick: () => void;
89
+ disabled: (s: S) => boolean;
90
+ };
91
+ /**
92
+ * True iff the input has non-whitespace content AND we're not
93
+ * mid-submit. Useful as the predicate for a "send" affordance
94
+ * separate from the button's own `disabled` field (e.g. a
95
+ * keyboard-shortcut hint).
96
+ */
97
+ canSubmit: (s: S) => boolean;
98
+ };
99
+ export declare function connect<S>(get: (s: S) => AgentChatState, send: Send<AgentChatMsg>): ConnectBag<S>;
100
+ //# sourceMappingURL=agentChat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentChat.d.ts","sourceRoot":"","sources":["../../src/client/agentChat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,YAAY,GACpB;IACE,2EAA2E;IAC3E,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd,GACD;IACE;;;;;;;OAOG;IACH,IAAI,EAAE,QAAQ,CAAA;CACf,GACD;IACE;;;;OAIG;IACH,IAAI,EAAE,gBAAgB,CAAA;CACvB,CAAA;AAEL,wBAAgB,IAAI,CAAC,IAAI,GAAE,iBAAsB,GAAG,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,CAQlF;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,YAAY,GAAG,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,CAyBhG;AAED,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAE9C;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,IAAI,EAAE;QAAE,YAAY,EAAE,YAAY,CAAC;QAAC,iBAAiB,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;KAAE,CAAA;IAC1E,KAAK,EAAE;QACL,WAAW,EAAE,OAAO,CAAA;QACpB,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;QACvB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;QAC3B,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;QAC3B,SAAS,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;KACtC,CAAA;IACD,YAAY,EAAE;QACZ,WAAW,EAAE,QAAQ,CAAA;QACrB,OAAO,EAAE,MAAM,IAAI,CAAA;QACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;KAC5B,CAAA;IACD;;;;;OAKG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;CAC7B,CAAA;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAyCjG"}
@@ -0,0 +1,84 @@
1
+ export function init(opts = {}) {
2
+ return [
3
+ {
4
+ pendingInput: opts.initialInput ?? '',
5
+ submitting: false,
6
+ },
7
+ [],
8
+ ];
9
+ }
10
+ export function update(state, msg) {
11
+ switch (msg.type) {
12
+ case 'SetInput':
13
+ // No-op when the value is identical — keeps state ref stable
14
+ // for memoization above this slice.
15
+ if (msg.value === state.pendingInput)
16
+ return [state, []];
17
+ return [{ ...state, pendingInput: msg.value }, []];
18
+ case 'Submit': {
19
+ // Don't double-submit while a previous send is in flight, and
20
+ // don't send empty/whitespace messages — neither is a useful
21
+ // signal for the agent and both are common bounce events
22
+ // (keyboard auto-repeat on Enter, click-then-click-again).
23
+ if (state.submitting)
24
+ return [state, []];
25
+ const trimmed = state.pendingInput.trim();
26
+ if (trimmed.length === 0)
27
+ return [state, []];
28
+ const at = Date.now();
29
+ return [
30
+ { ...state, pendingInput: '', submitting: true },
31
+ [{ type: 'AgentChatSendInput', text: trimmed, at }],
32
+ ];
33
+ }
34
+ case 'SubmitComplete':
35
+ if (!state.submitting)
36
+ return [state, []];
37
+ return [{ ...state, submitting: false }, []];
38
+ }
39
+ }
40
+ import { tagSend } from '@llui/dom';
41
+ export function connect(get, send) {
42
+ const submit = tagSend(send, ['Submit'], () => send({ type: 'Submit' }));
43
+ const setInput = (value) => send({ type: 'SetInput', value });
44
+ return {
45
+ root: {
46
+ 'data-scope': 'agent-chat',
47
+ 'data-submitting': (s) => get(s).submitting,
48
+ },
49
+ input: {
50
+ 'data-part': 'input',
51
+ value: (s) => get(s).pendingInput,
52
+ disabled: (s) => get(s).submitting,
53
+ oninput: (e) => {
54
+ // The cast is the standard "input event target is an
55
+ // HTMLInputElement / HTMLTextAreaElement" assumption; we
56
+ // coerce via `.value`. Hosts using non-DOM inputs (custom
57
+ // contenteditable etc.) bypass this prop bag and dispatch
58
+ // SetInput directly.
59
+ const target = e.target;
60
+ if (target && typeof target.value === 'string')
61
+ setInput(target.value);
62
+ },
63
+ onkeydown: (e) => {
64
+ if (e.key === 'Enter' && !e.shiftKey) {
65
+ e.preventDefault();
66
+ submit();
67
+ }
68
+ },
69
+ },
70
+ submitButton: {
71
+ 'data-part': 'submit',
72
+ onClick: submit,
73
+ disabled: (s) => {
74
+ const cs = get(s);
75
+ return cs.submitting || cs.pendingInput.trim().length === 0;
76
+ },
77
+ },
78
+ canSubmit: (s) => {
79
+ const cs = get(s);
80
+ return !cs.submitting && cs.pendingInput.trim().length > 0;
81
+ },
82
+ };
83
+ }
84
+ //# sourceMappingURL=agentChat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentChat.js","sourceRoot":"","sources":["../../src/client/agentChat.ts"],"names":[],"mappings":"AAkEA,MAAM,UAAU,IAAI,CAAC,OAA0B,EAAE;IAC/C,OAAO;QACL;YACE,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,EAAE;YACrC,UAAU,EAAE,KAAK;SAClB;QACD,EAAE;KACH,CAAA;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAqB,EAAE,GAAiB;IAC7D,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,UAAU;YACb,6DAA6D;YAC7D,oCAAoC;YACpC,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,YAAY;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACxD,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;QACpD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,8DAA8D;YAC9D,6DAA6D;YAC7D,yDAAyD;YACzD,2DAA2D;YAC3D,IAAI,KAAK,CAAC,UAAU;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACxC,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAA;YACzC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YACrB,OAAO;gBACL,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;gBAChD,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;aACpD,CAAA;QACH,CAAC;QACD,KAAK,gBAAgB;YACnB,IAAI,CAAC,KAAK,CAAC,UAAU;gBAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YACzC,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;IAChD,CAAC;AACH,CAAC;AAED,OAAO,EAAE,OAAO,EAAa,MAAM,WAAW,CAAA;AAoC9C,MAAM,UAAU,OAAO,CAAI,GAA6B,EAAE,IAAwB;IAChF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;IACxE,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;IACrE,OAAO;QACL,IAAI,EAAE;YACJ,YAAY,EAAE,YAAY;YAC1B,iBAAiB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU;SAC5C;QACD,KAAK,EAAE;YACL,WAAW,EAAE,OAAO;YACpB,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY;YACjC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU;YAClC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;gBACb,qDAAqD;gBACrD,yDAAyD;gBACzD,0DAA0D;gBAC1D,0DAA0D;gBAC1D,qBAAqB;gBACrB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAmC,CAAA;gBACpD,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;oBAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACxE,CAAC;YACD,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;gBACf,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;oBACrC,CAAC,CAAC,cAAc,EAAE,CAAA;oBAClB,MAAM,EAAE,CAAA;gBACV,CAAC;YACH,CAAC;SACF;QACD,YAAY,EAAE;YACZ,WAAW,EAAE,QAAQ;YACrB,OAAO,EAAE,MAAM;YACf,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;gBACd,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;gBACjB,OAAO,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,CAAA;YAC7D,CAAC;SACF;QACD,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;YACf,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;YACjB,OAAO,CAAC,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA;QAC5D,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { AgentEffect } from './effects.js'\n\n/**\n * In-app chat composer slice. Owns the editor-state half of the\n * conversational surface (`pendingInput`, `submitting`); the\n * timeline half lives in `agentLog` (which renders the user's\n * submission as a `LogEntry { kind: 'user-input' }` alongside agent\n * actions, so the conversation reads chronologically).\n *\n * Pattern: host wires the slice into its app state via\n * `sliceHandler`, spreads `connect()`'s prop bag into the host's\n * own input/button layout, and the `Submit` Msg fires an effect\n * that the framework's effect handler turns into a WS\n * `user-input-submitted` frame + a synthesized log entry. The\n * agent's `wait_for_user_input` LAP tool picks up the frame at the\n * server.\n *\n * The composer is NOT an LLM. It's a relay surface: the user's text\n * is delivered to the user's own LLM (Claude desktop / IDE / wherever\n * is mounted on the MCP bridge), which already has cross-app context.\n * The framework just provides the in-app rendezvous so the user\n * doesn't have to alt-tab to a different window to talk to their\n * agent about the app they're looking at.\n */\nexport type AgentChatState = {\n /** Current contents of the input field. Bound to the input's `value`. */\n pendingInput: string\n /**\n * True between `Submit` and the effect handler completing the\n * frame send. Disables the submit button and (typically) the\n * input itself; reducer-driven so the UI never disagrees.\n */\n submitting: boolean\n}\n\nexport type AgentChatInitOpts = {\n /** Pre-fill the input on mount — e.g. session-restore. */\n initialInput?: string\n}\n\nexport type AgentChatMsg =\n | {\n /** Bound to the input's `oninput` event. Round-trip stays in the slice. */\n type: 'SetInput'\n value: string\n }\n | {\n /**\n * Bound to the submit button's `onClick` and the input's\n * `onKeyDown` (Enter, no shift). Reducer:\n *\n * - Empty/whitespace → no-op (no effect, no state change).\n * - Has content → clear `pendingInput`, set `submitting: true`,\n * emit `AgentChatSendInput { text, at }` effect.\n */\n type: 'Submit'\n }\n | {\n /**\n * Fired by the effect handler after the frame sends so the\n * UI re-enables the input. Always paired 1:1 with each\n * dispatched `AgentChatSendInput` effect.\n */\n type: 'SubmitComplete'\n }\n\nexport function init(opts: AgentChatInitOpts = {}): [AgentChatState, AgentEffect[]] {\n return [\n {\n pendingInput: opts.initialInput ?? '',\n submitting: false,\n },\n [],\n ]\n}\n\nexport function update(state: AgentChatState, msg: AgentChatMsg): [AgentChatState, AgentEffect[]] {\n switch (msg.type) {\n case 'SetInput':\n // No-op when the value is identical — keeps state ref stable\n // for memoization above this slice.\n if (msg.value === state.pendingInput) return [state, []]\n return [{ ...state, pendingInput: msg.value }, []]\n case 'Submit': {\n // Don't double-submit while a previous send is in flight, and\n // don't send empty/whitespace messages — neither is a useful\n // signal for the agent and both are common bounce events\n // (keyboard auto-repeat on Enter, click-then-click-again).\n if (state.submitting) return [state, []]\n const trimmed = state.pendingInput.trim()\n if (trimmed.length === 0) return [state, []]\n const at = Date.now()\n return [\n { ...state, pendingInput: '', submitting: true },\n [{ type: 'AgentChatSendInput', text: trimmed, at }],\n ]\n }\n case 'SubmitComplete':\n if (!state.submitting) return [state, []]\n return [{ ...state, submitting: false }, []]\n }\n}\n\nimport { tagSend, type Send } from '@llui/dom'\n\n/**\n * Static-prop-bag-with-reactive-accessors. Spread directly into\n * element helpers; matches the convention of the other agent\n * namespaces.\n *\n * The `input` bag carries both `oninput` and `onkeydown` because\n * the canonical chat-composer affordance is \"type, press Enter\"\n * (Shift+Enter for newline if the host wraps it themselves). Hosts\n * that want a multiline textarea can spread `input.oninput` and\n * skip `onkeydown`, wiring submit to the button only.\n */\nexport type ConnectBag<S> = {\n root: { 'data-scope': 'agent-chat'; 'data-submitting': (s: S) => boolean }\n input: {\n 'data-part': 'input'\n value: (s: S) => string\n disabled: (s: S) => boolean\n oninput: (e: Event) => void\n onkeydown: (e: KeyboardEvent) => void\n }\n submitButton: {\n 'data-part': 'submit'\n onClick: () => void\n disabled: (s: S) => boolean\n }\n /**\n * True iff the input has non-whitespace content AND we're not\n * mid-submit. Useful as the predicate for a \"send\" affordance\n * separate from the button's own `disabled` field (e.g. a\n * keyboard-shortcut hint).\n */\n canSubmit: (s: S) => boolean\n}\n\nexport function connect<S>(get: (s: S) => AgentChatState, send: Send<AgentChatMsg>): ConnectBag<S> {\n const submit = tagSend(send, ['Submit'], () => send({ type: 'Submit' }))\n const setInput = (value: string) => send({ type: 'SetInput', value })\n return {\n root: {\n 'data-scope': 'agent-chat',\n 'data-submitting': (s) => get(s).submitting,\n },\n input: {\n 'data-part': 'input',\n value: (s) => get(s).pendingInput,\n disabled: (s) => get(s).submitting,\n oninput: (e) => {\n // The cast is the standard \"input event target is an\n // HTMLInputElement / HTMLTextAreaElement\" assumption; we\n // coerce via `.value`. Hosts using non-DOM inputs (custom\n // contenteditable etc.) bypass this prop bag and dispatch\n // SetInput directly.\n const target = e.target as { value?: string } | null\n if (target && typeof target.value === 'string') setInput(target.value)\n },\n onkeydown: (e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n submit()\n }\n },\n },\n submitButton: {\n 'data-part': 'submit',\n onClick: submit,\n disabled: (s) => {\n const cs = get(s)\n return cs.submitting || cs.pendingInput.trim().length === 0\n },\n },\n canSubmit: (s) => {\n const cs = get(s)\n return !cs.submitting && cs.pendingInput.trim().length > 0\n },\n }\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import type { AgentEffect } from './effects.js';
2
2
  import type { LogEntry, LogKind } from '../protocol.js';
3
+ import type { StateDiff } from '../state-diff.js';
3
4
  export type AgentLogFilter = {
4
5
  kinds?: LogKind[];
5
6
  since?: number;
@@ -63,6 +64,22 @@ export type ConnectBag<S> = {
63
64
  };
64
65
  /** Filtered view of entries — respects state.filter. */
65
66
  visibleEntries: (s: S) => LogEntry[];
67
+ /**
68
+ * Reactive accessor for an entry's structural diff (JSON-Patch).
69
+ * Returns the entry's `stateDiff` when present, `null` otherwise —
70
+ * `null` covers three distinct cases: the entry exists but its kind
71
+ * (read / proposed / etc.) doesn't carry a diff; the entry was filtered
72
+ * out; the entry was evicted by the ring-buffer or never appended.
73
+ * Hosts that render a per-entry "what changed" sidecar wire this to
74
+ * a structural primitive (`branch`, `each`) so the sidecar disposes
75
+ * cleanly when the entry leaves.
76
+ *
77
+ * Lookup is over `state.entries` directly (NOT through the filter)
78
+ * — a hidden-by-filter entry still has its diff available, which is
79
+ * what consumers expect when reading from a sidecar that may outlive
80
+ * the visibility filter.
81
+ */
82
+ entryDiff: (id: string) => (s: S) => StateDiff | null;
66
83
  };
67
84
  export declare function connect<S>(get: (s: S) => AgentLogState, send: Send<AgentLogMsg>): ConnectBag<S>;
68
85
  //# sourceMappingURL=agentLog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agentLog.d.ts","sourceRoot":"","sources":["../../src/client/agentLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAEvD,MAAM,MAAM,cAAc,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAElE,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,QAAQ,EAAE,CAAA;IACnB,MAAM,EAAE,cAAc,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtD,MAAM,MAAM,WAAW;AACrB;;;;GAIG;AACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE;AACrC,8CAA8C;GAC5C;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE;AACnB,6DAA6D;GAC3D;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,CAAA;AAIjD,wBAAgB,IAAI,CAAC,KAAK,GAAE,gBAAqB,GAAG,CAAC,aAAa,EAAE,WAAW,EAAE,CAAC,CAEjF;AAED,wBAAgB,MAAM,CACpB,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,WAAW,EAChB,IAAI,GAAE,gBAAqB,GAC1B,CAAC,aAAa,EAAE,WAAW,EAAE,CAAC,CAchC;AAGD,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAM9C;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,IAAI,EAAE;QAAE,YAAY,EAAE,WAAW,CAAA;KAAE,CAAA;IACnC,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;KAAE,CAAA;IAC7D,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK;QACzB,WAAW,EAAE,OAAO,CAAA;QACpB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,GAAG,SAAS,CAAA;KAC3C,CAAA;IACD,cAAc,EAAE;QACd,WAAW,EAAE;YAAE,OAAO,EAAE,MAAM,IAAI,CAAC;YAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;SAAE,CAAA;QACjE,SAAS,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAA;KAC5C,CAAA;IACD,wDAAwD;IACxD,cAAc,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAA;CACrC,CAAA;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,aAAa,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA4C/F"}
1
+ {"version":3,"file":"agentLog.d.ts","sourceRoot":"","sources":["../../src/client/agentLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,MAAM,MAAM,cAAc,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAElE,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,QAAQ,EAAE,CAAA;IACnB,MAAM,EAAE,cAAc,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtD,MAAM,MAAM,WAAW;AACrB;;;;GAIG;AACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE;AACrC,8CAA8C;GAC5C;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE;AACnB,6DAA6D;GAC3D;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE,CAAA;AAIjD,wBAAgB,IAAI,CAAC,KAAK,GAAE,gBAAqB,GAAG,CAAC,aAAa,EAAE,WAAW,EAAE,CAAC,CAEjF;AAED,wBAAgB,MAAM,CACpB,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,WAAW,EAChB,IAAI,GAAE,gBAAqB,GAC1B,CAAC,aAAa,EAAE,WAAW,EAAE,CAAC,CAchC;AAGD,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAM9C;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,IAAI,EAAE;QAAE,YAAY,EAAE,WAAW,CAAA;KAAE,CAAA;IACnC,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;KAAE,CAAA;IAC7D,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK;QACzB,WAAW,EAAE,OAAO,CAAA;QACpB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,GAAG,SAAS,CAAA;KAC3C,CAAA;IACD,cAAc,EAAE;QACd,WAAW,EAAE;YAAE,OAAO,EAAE,MAAM,IAAI,CAAC;YAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;SAAE,CAAA;QACjE,SAAS,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAA;KAC5C,CAAA;IACD,wDAAwD;IACxD,cAAc,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAA;IACpC;;;;;;;;;;;;;;OAcG;IACH,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,SAAS,GAAG,IAAI,CAAA;CACtD,CAAA;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,aAAa,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA+D/F"}
@@ -48,6 +48,16 @@ export function connect(get, send) {
48
48
  return lastResult;
49
49
  };
50
50
  const findVisible = (state, id) => visible(state).find((x) => x.id === id);
51
+ // Per-id diff accessor cache. The `each(bag.visibleEntries)` pattern
52
+ // calls `bag.entryDiff(entry.id)` once per row at view-construction —
53
+ // memoizing keeps each row's accessor stable across re-renders, so
54
+ // the underlying binding's `lastValue` short-circuits repeat reads
55
+ // when state hasn't changed (parent state ref equality is sufficient
56
+ // because TEA state is immutable). Without this, every view pass
57
+ // would allocate a fresh closure and the binding would re-fire even
58
+ // though the entry's diff is invariant for an entry's lifetime.
59
+ const diffAccessorCache = new Map();
60
+ const findById = (state, id) => get(state).entries.find((x) => x.id === id);
51
61
  return {
52
62
  root: { 'data-scope': 'agent-log' },
53
63
  list: {
@@ -67,6 +77,14 @@ export function connect(get, send) {
67
77
  setFilter: (filter) => send({ type: 'SetFilter', filter }),
68
78
  },
69
79
  visibleEntries: visible,
80
+ entryDiff: (id) => {
81
+ const cached = diffAccessorCache.get(id);
82
+ if (cached)
83
+ return cached;
84
+ const accessor = (s) => findById(s, id)?.stateDiff ?? null;
85
+ diffAccessorCache.set(id, accessor);
86
+ return accessor;
87
+ },
70
88
  };
71
89
  }
72
90
  //# sourceMappingURL=agentLog.js.map