@llui/agent 0.0.49 → 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.
- package/dist/client/agentAttention.d.ts +129 -0
- package/dist/client/agentAttention.d.ts.map +1 -0
- package/dist/client/agentAttention.js +156 -0
- package/dist/client/agentAttention.js.map +1 -0
- package/dist/client/agentChat.d.ts +100 -0
- package/dist/client/agentChat.d.ts.map +1 -0
- package/dist/client/agentChat.js +84 -0
- package/dist/client/agentChat.js.map +1 -0
- package/dist/client/agentLog.d.ts +17 -0
- package/dist/client/agentLog.d.ts.map +1 -1
- package/dist/client/agentLog.js +18 -0
- package/dist/client/agentLog.js.map +1 -1
- package/dist/client/diff-render.d.ts +68 -0
- package/dist/client/diff-render.d.ts.map +1 -0
- package/dist/client/diff-render.js +141 -0
- package/dist/client/diff-render.js.map +1 -0
- package/dist/client/effect-handler.d.ts +29 -0
- package/dist/client/effect-handler.d.ts.map +1 -1
- package/dist/client/effect-handler.js +39 -0
- package/dist/client/effect-handler.js.map +1 -1
- package/dist/client/effects.d.ts +43 -0
- package/dist/client/effects.d.ts.map +1 -1
- package/dist/client/effects.js.map +1 -1
- package/dist/client/factory.d.ts +21 -0
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +15 -2
- package/dist/client/factory.js.map +1 -1
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/ws-client.d.ts +9 -0
- package/dist/client/ws-client.d.ts.map +1 -1
- package/dist/client/ws-client.js +120 -0
- package/dist/client/ws-client.js.map +1 -1
- package/dist/protocol.d.ts +103 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/server/cloudflare/durable-object.d.ts +41 -0
- package/dist/server/cloudflare/durable-object.d.ts.map +1 -1
- package/dist/server/cloudflare/durable-object.js +46 -0
- package/dist/server/cloudflare/durable-object.js.map +1 -1
- package/dist/server/cloudflare/index.d.ts +10 -3
- package/dist/server/cloudflare/index.d.ts.map +1 -1
- package/dist/server/cloudflare/index.js +10 -3
- package/dist/server/cloudflare/index.js.map +1 -1
- package/dist/server/core.d.ts +11 -1
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +1 -0
- package/dist/server/core.js.map +1 -1
- package/dist/server/lap/narrate.d.ts +31 -0
- package/dist/server/lap/narrate.d.ts.map +1 -0
- package/dist/server/lap/narrate.js +70 -0
- package/dist/server/lap/narrate.js.map +1 -0
- package/dist/server/lap/router.d.ts.map +1 -1
- package/dist/server/lap/router.js +6 -0
- package/dist/server/lap/router.js.map +1 -1
- package/dist/server/lap/wait-for-user-input.d.ts +13 -0
- package/dist/server/lap/wait-for-user-input.d.ts.map +1 -0
- package/dist/server/lap/wait-for-user-input.js +53 -0
- package/dist/server/lap/wait-for-user-input.js.map +1 -0
- package/dist/server/ws/pairing-registry.d.ts +101 -0
- package/dist/server/ws/pairing-registry.d.ts.map +1 -1
- package/dist/server/ws/pairing-registry.js +160 -0
- package/dist/server/ws/pairing-registry.js.map +1 -1
- package/package.json +5 -3
- 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":"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","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;
|
|
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"}
|
package/dist/client/agentLog.js
CHANGED
|
@@ -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
|