@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.
- 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 +7 -5
- package/styles/agent-panel.css +153 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentLog.js","sourceRoot":"","sources":["../../src/client/agentLog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agentLog.js","sourceRoot":"","sources":["../../src/client/agentLog.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentEffect } from './effects.js'\nimport type { LogEntry, LogKind } from '../protocol.js'\nimport type { StateDiff } from '../state-diff.js'\n\nexport type AgentLogFilter = { kinds?: LogKind[]; since?: number }\n\nexport type AgentLogState = {\n entries: LogEntry[]\n filter: AgentLogFilter\n}\n\nexport type AgentLogInitOpts = { maxEntries?: number } // default 100\n\nexport type AgentLogMsg =\n /**\n * @humanOnly — internal: WS frame router dispatches this on every\n * `log-append` frame from the runtime. Agents observe the log via\n * the LAP read surface, not by emitting Append themselves.\n */\n | { type: 'Append'; entry: LogEntry }\n /** @intent(\"Clear the agent activity log\") */\n | { type: 'Clear' }\n /** @intent(\"Set the visibility filter for the agent log\") */\n | { type: 'SetFilter'; filter: AgentLogFilter }\n\nconst DEFAULT_MAX = 100\n\nexport function init(_opts: AgentLogInitOpts = {}): [AgentLogState, AgentEffect[]] {\n return [{ entries: [], filter: {} }, []]\n}\n\nexport function update(\n state: AgentLogState,\n msg: AgentLogMsg,\n opts: AgentLogInitOpts = {},\n): [AgentLogState, AgentEffect[]] {\n const max = opts.maxEntries ?? DEFAULT_MAX\n switch (msg.type) {\n case 'Append': {\n const next = [...state.entries, msg.entry]\n // Ring-buffer cap\n if (next.length > max) next.splice(0, next.length - max)\n return [{ ...state, entries: next }, []]\n }\n case 'Clear':\n return [{ ...state, entries: [] }, []]\n case 'SetFilter':\n return [{ ...state, filter: msg.filter }, []]\n }\n}\n\n// Connect bag:\nimport { tagSend, type Send } from '@llui/dom'\n\n// Sentinel for the memoization slot — distinguishable from any\n// possible parent state value (including null/undefined).\nconst UNSET: unique symbol = Symbol('agent-log-visible-unset')\n\n/**\n * Static prop bag with reactive accessors. See agentConnect.ts for\n * the rationale.\n *\n * `visibleEntries` is exposed as a reactive accessor returning the\n * filtered entry list — pass it to `each` directly:\n * each(bag.visibleEntries, (e) => …)\n */\nexport type ConnectBag<S> = {\n root: { 'data-scope': 'agent-log' }\n list: { 'data-part': 'list'; 'data-count': (s: S) => number }\n entryItem: (id: string) => {\n 'data-part': 'entry'\n 'data-id': string\n 'data-kind': (s: S) => LogKind | 'missing'\n }\n filterControls: {\n clearButton: { onClick: () => void; disabled: (s: S) => boolean }\n setFilter: (filter: AgentLogFilter) => void\n }\n /** Filtered view of entries — respects state.filter. */\n visibleEntries: (s: S) => LogEntry[]\n /**\n * Reactive accessor for an entry's structural diff (JSON-Patch).\n * Returns the entry's `stateDiff` when present, `null` otherwise —\n * `null` covers three distinct cases: the entry exists but its kind\n * (read / proposed / etc.) doesn't carry a diff; the entry was filtered\n * out; the entry was evicted by the ring-buffer or never appended.\n * Hosts that render a per-entry \"what changed\" sidecar wire this to\n * a structural primitive (`branch`, `each`) so the sidecar disposes\n * cleanly when the entry leaves.\n *\n * Lookup is over `state.entries` directly (NOT through the filter)\n * — a hidden-by-filter entry still has its diff available, which is\n * what consumers expect when reading from a sidecar that may outlive\n * the visibility filter.\n */\n entryDiff: (id: string) => (s: S) => StateDiff | null\n}\n\nexport function connect<S>(get: (s: S) => AgentLogState, send: Send<AgentLogMsg>): ConnectBag<S> {\n // Memoize the filter result by parent-state reference. Each render\n // pass typically calls `visibleEntries`, `list['data-count']`, and\n // every `entryItem(id)['data-kind']` — without this, an `each` loop\n // over visibleEntries triggers O(n) filter recomputes per item.\n // Parent state is immutable (TEA), so reference equality is enough.\n // Using a single-slot cache rather than a WeakMap because consumers\n // call from a hot path and a single recent state covers >99% of hits.\n let lastState: S | typeof UNSET = UNSET\n let lastResult: LogEntry[] = []\n const visible = (state: S): LogEntry[] => {\n if (state === lastState) return lastResult\n const s = get(state)\n lastResult = s.entries.filter((e) => {\n if (s.filter.kinds && !s.filter.kinds.includes(e.kind)) return false\n if (s.filter.since !== undefined && e.at < s.filter.since) return false\n return true\n })\n lastState = state\n return lastResult\n }\n const findVisible = (state: S, id: string): LogEntry | undefined =>\n visible(state).find((x) => x.id === id)\n\n // Per-id diff accessor cache. The `each(bag.visibleEntries)` pattern\n // calls `bag.entryDiff(entry.id)` once per row at view-construction —\n // memoizing keeps each row's accessor stable across re-renders, so\n // the underlying binding's `lastValue` short-circuits repeat reads\n // when state hasn't changed (parent state ref equality is sufficient\n // because TEA state is immutable). Without this, every view pass\n // would allocate a fresh closure and the binding would re-fire even\n // though the entry's diff is invariant for an entry's lifetime.\n const diffAccessorCache = new Map<string, (s: S) => StateDiff | null>()\n const findById = (state: S, id: string): LogEntry | undefined =>\n get(state).entries.find((x) => x.id === id)\n\n return {\n root: { 'data-scope': 'agent-log' },\n list: {\n 'data-part': 'list',\n 'data-count': (s) => visible(s).length,\n },\n entryItem: (id) => ({\n 'data-part': 'entry',\n 'data-id': id,\n 'data-kind': (s) => findVisible(s, id)?.kind ?? 'missing',\n }),\n filterControls: {\n clearButton: {\n onClick: tagSend(send, ['Clear'], () => send({ type: 'Clear' })),\n disabled: (s) => get(s).entries.length === 0,\n },\n setFilter: (filter) => send({ type: 'SetFilter', filter }),\n },\n visibleEntries: visible,\n entryDiff: (id) => {\n const cached = diffAccessorCache.get(id)\n if (cached) return cached\n const accessor = (s: S): StateDiff | null => findById(s, id)?.stateDiff ?? null\n diffAccessorCache.set(id, accessor)\n return accessor\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { JsonPatchOp, StateDiff } from '../state-diff.js';
|
|
2
|
+
/**
|
|
3
|
+
* Humanized renderers for `StateDiff` (JSON-Patch). The raw shape is
|
|
4
|
+
* accurate but technical (`{ op: 'add', path: '/items/3/name', value: 'X' }`);
|
|
5
|
+
* the agent panel reads better in plain prose.
|
|
6
|
+
*
|
|
7
|
+
* Two output forms:
|
|
8
|
+
* - `summarizeDiff` — one-line headline ("3 items changed") for a
|
|
9
|
+
* row in the activity feed.
|
|
10
|
+
* - `groupDiff` — structured per-top-level-path summary for an
|
|
11
|
+
* expanded sidecar that lists what changed in each region.
|
|
12
|
+
*
|
|
13
|
+
* Both are pure functions; both treat the input as immutable. Callers
|
|
14
|
+
* that need a different rendering (e.g. an emoji-driven layout, a
|
|
15
|
+
* deeper drill-down) should compose on top of `groupDiff` rather than
|
|
16
|
+
* forking — the grouping covers 90% of the structural work.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* One-line summary of the entire diff. Examples:
|
|
20
|
+
*
|
|
21
|
+
* - `[{ op: 'replace', path: '/cart/total', value: 9 }]`
|
|
22
|
+
* → "1 field changed"
|
|
23
|
+
* - `[{ op: 'add', path: '/items/-' }, { op: 'add', path: '/items/-' }]`
|
|
24
|
+
* → "2 items added"
|
|
25
|
+
* - mixed adds/removes/replaces across multiple regions
|
|
26
|
+
* → "5 changes across 3 regions"
|
|
27
|
+
*
|
|
28
|
+
* The summary collapses multiple ops on the same logical path
|
|
29
|
+
* (e.g. updating multiple fields on the same item) into a single
|
|
30
|
+
* "change" — counting raw op entries would surface implementation
|
|
31
|
+
* detail (which JSON-Patch ops the differ emitted), not user-relevant
|
|
32
|
+
* counts.
|
|
33
|
+
*/
|
|
34
|
+
export declare function summarizeDiff(diff: StateDiff | undefined | null): string;
|
|
35
|
+
/**
|
|
36
|
+
* Per-top-level-path breakdown. Returns an array (stable order) where
|
|
37
|
+
* each entry describes the changes affecting one top-level region.
|
|
38
|
+
* Useful for a sidecar that wants to render a row per region with the
|
|
39
|
+
* affected fields beneath it.
|
|
40
|
+
*
|
|
41
|
+
* The returned `paths` are the FULL JSON-Pointer paths of the ops, so
|
|
42
|
+
* a consumer can render "/items/3/name" verbatim or further humanize
|
|
43
|
+
* it. The renderer doesn't make policy choices about how deeply to
|
|
44
|
+
* label — that's the host's call.
|
|
45
|
+
*/
|
|
46
|
+
export type DiffGroup = {
|
|
47
|
+
/** Top-level state field, or `'*'` for whole-state replace. */
|
|
48
|
+
region: string;
|
|
49
|
+
adds: number;
|
|
50
|
+
removes: number;
|
|
51
|
+
replaces: number;
|
|
52
|
+
/** Full op paths in arrival order. */
|
|
53
|
+
paths: string[];
|
|
54
|
+
};
|
|
55
|
+
export declare function groupDiff(diff: StateDiff | undefined | null): DiffGroup[];
|
|
56
|
+
/**
|
|
57
|
+
* Per-op short verb + readable path. Useful for a flat detail view:
|
|
58
|
+
*
|
|
59
|
+
* - `{ op: 'replace', path: '/cart/total', value: 9 }` → `'changed cart.total'`
|
|
60
|
+
* - `{ op: 'add', path: '/items/3' }` → `'added items.3'`
|
|
61
|
+
* - `{ op: 'remove', path: '/items/3' }` → `'removed items.3'`
|
|
62
|
+
* - `{ op: 'replace', path: '/' }` → `'replaced state'`
|
|
63
|
+
*
|
|
64
|
+
* The path is converted from JSON-Pointer to dotted form (with
|
|
65
|
+
* `~0`/`~1` un-escaping) so it reads as a plain field accessor.
|
|
66
|
+
*/
|
|
67
|
+
export declare function describeOp(op: JsonPatchOp): string;
|
|
68
|
+
//# sourceMappingURL=diff-render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-render.d.ts","sourceRoot":"","sources":["../../src/client/diff-render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE9D;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAsCxE;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,GAAG,SAAS,EAAE,CAgBzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,WAAW,GAAG,MAAM,CAKlD"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Humanized renderers for `StateDiff` (JSON-Patch). The raw shape is
|
|
3
|
+
* accurate but technical (`{ op: 'add', path: '/items/3/name', value: 'X' }`);
|
|
4
|
+
* the agent panel reads better in plain prose.
|
|
5
|
+
*
|
|
6
|
+
* Two output forms:
|
|
7
|
+
* - `summarizeDiff` — one-line headline ("3 items changed") for a
|
|
8
|
+
* row in the activity feed.
|
|
9
|
+
* - `groupDiff` — structured per-top-level-path summary for an
|
|
10
|
+
* expanded sidecar that lists what changed in each region.
|
|
11
|
+
*
|
|
12
|
+
* Both are pure functions; both treat the input as immutable. Callers
|
|
13
|
+
* that need a different rendering (e.g. an emoji-driven layout, a
|
|
14
|
+
* deeper drill-down) should compose on top of `groupDiff` rather than
|
|
15
|
+
* forking — the grouping covers 90% of the structural work.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* One-line summary of the entire diff. Examples:
|
|
19
|
+
*
|
|
20
|
+
* - `[{ op: 'replace', path: '/cart/total', value: 9 }]`
|
|
21
|
+
* → "1 field changed"
|
|
22
|
+
* - `[{ op: 'add', path: '/items/-' }, { op: 'add', path: '/items/-' }]`
|
|
23
|
+
* → "2 items added"
|
|
24
|
+
* - mixed adds/removes/replaces across multiple regions
|
|
25
|
+
* → "5 changes across 3 regions"
|
|
26
|
+
*
|
|
27
|
+
* The summary collapses multiple ops on the same logical path
|
|
28
|
+
* (e.g. updating multiple fields on the same item) into a single
|
|
29
|
+
* "change" — counting raw op entries would surface implementation
|
|
30
|
+
* detail (which JSON-Patch ops the differ emitted), not user-relevant
|
|
31
|
+
* counts.
|
|
32
|
+
*/
|
|
33
|
+
export function summarizeDiff(diff) {
|
|
34
|
+
if (!diff || diff.length === 0)
|
|
35
|
+
return 'no changes';
|
|
36
|
+
let adds = 0;
|
|
37
|
+
let removes = 0;
|
|
38
|
+
let replaces = 0;
|
|
39
|
+
const topPaths = new Set();
|
|
40
|
+
for (const op of diff) {
|
|
41
|
+
topPaths.add(topLevelOf(op.path));
|
|
42
|
+
if (op.op === 'add')
|
|
43
|
+
adds++;
|
|
44
|
+
else if (op.op === 'remove')
|
|
45
|
+
removes++;
|
|
46
|
+
else
|
|
47
|
+
replaces++;
|
|
48
|
+
}
|
|
49
|
+
// Single-region special case: name the region so the summary doesn't
|
|
50
|
+
// hide WHERE the change happened. "3 changes in cart" beats "3 changes".
|
|
51
|
+
if (topPaths.size === 1) {
|
|
52
|
+
const region = Array.from(topPaths)[0];
|
|
53
|
+
if (region === '*') {
|
|
54
|
+
return 'state replaced';
|
|
55
|
+
}
|
|
56
|
+
const total = adds + removes + replaces;
|
|
57
|
+
return `${total} change${total === 1 ? '' : 's'} in ${region}`;
|
|
58
|
+
}
|
|
59
|
+
// Multi-region: prefer the dominant op verb when one dominates,
|
|
60
|
+
// else fall back to a generic count.
|
|
61
|
+
if (adds > 0 && removes === 0 && replaces === 0) {
|
|
62
|
+
return `${adds} item${adds === 1 ? '' : 's'} added across ${topPaths.size} regions`;
|
|
63
|
+
}
|
|
64
|
+
if (removes > 0 && adds === 0 && replaces === 0) {
|
|
65
|
+
return `${removes} item${removes === 1 ? '' : 's'} removed across ${topPaths.size} regions`;
|
|
66
|
+
}
|
|
67
|
+
if (replaces > 0 && adds === 0 && removes === 0) {
|
|
68
|
+
return `${replaces} field${replaces === 1 ? '' : 's'} changed across ${topPaths.size} regions`;
|
|
69
|
+
}
|
|
70
|
+
const total = adds + removes + replaces;
|
|
71
|
+
return `${total} changes across ${topPaths.size} regions`;
|
|
72
|
+
}
|
|
73
|
+
export function groupDiff(diff) {
|
|
74
|
+
if (!diff || diff.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
const byRegion = new Map();
|
|
77
|
+
for (const op of diff) {
|
|
78
|
+
const region = topLevelOf(op.path);
|
|
79
|
+
let g = byRegion.get(region);
|
|
80
|
+
if (!g) {
|
|
81
|
+
g = { region, adds: 0, removes: 0, replaces: 0, paths: [] };
|
|
82
|
+
byRegion.set(region, g);
|
|
83
|
+
}
|
|
84
|
+
if (op.op === 'add')
|
|
85
|
+
g.adds++;
|
|
86
|
+
else if (op.op === 'remove')
|
|
87
|
+
g.removes++;
|
|
88
|
+
else
|
|
89
|
+
g.replaces++;
|
|
90
|
+
g.paths.push(op.path);
|
|
91
|
+
}
|
|
92
|
+
return Array.from(byRegion.values());
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Per-op short verb + readable path. Useful for a flat detail view:
|
|
96
|
+
*
|
|
97
|
+
* - `{ op: 'replace', path: '/cart/total', value: 9 }` → `'changed cart.total'`
|
|
98
|
+
* - `{ op: 'add', path: '/items/3' }` → `'added items.3'`
|
|
99
|
+
* - `{ op: 'remove', path: '/items/3' }` → `'removed items.3'`
|
|
100
|
+
* - `{ op: 'replace', path: '/' }` → `'replaced state'`
|
|
101
|
+
*
|
|
102
|
+
* The path is converted from JSON-Pointer to dotted form (with
|
|
103
|
+
* `~0`/`~1` un-escaping) so it reads as a plain field accessor.
|
|
104
|
+
*/
|
|
105
|
+
export function describeOp(op) {
|
|
106
|
+
if (op.path === '' || op.path === '/') {
|
|
107
|
+
return op.op === 'replace' ? 'replaced state' : `${verbForOp(op.op)} state`;
|
|
108
|
+
}
|
|
109
|
+
return `${verbForOp(op.op)} ${dottedPath(op.path)}`;
|
|
110
|
+
}
|
|
111
|
+
// ── internals ────────────────────────────────────────────────────────
|
|
112
|
+
function topLevelOf(path) {
|
|
113
|
+
if (path === '' || path === '/')
|
|
114
|
+
return '*';
|
|
115
|
+
// JSON Pointer: leading '/', then segments.
|
|
116
|
+
const parts = path.split('/');
|
|
117
|
+
return parts.length >= 2 && parts[1] ? unescapePointerSegment(parts[1]) : '*';
|
|
118
|
+
}
|
|
119
|
+
function dottedPath(path) {
|
|
120
|
+
if (path === '' || path === '/')
|
|
121
|
+
return '';
|
|
122
|
+
// Drop leading '/', un-escape pointer segments, join with '.'.
|
|
123
|
+
const parts = path.split('/').slice(1).map(unescapePointerSegment);
|
|
124
|
+
return parts.join('.');
|
|
125
|
+
}
|
|
126
|
+
function unescapePointerSegment(seg) {
|
|
127
|
+
// RFC 6901: `~1` first (so a `~0` in the input doesn't introduce
|
|
128
|
+
// a `/` that interferes), then `~0`.
|
|
129
|
+
return seg.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
130
|
+
}
|
|
131
|
+
function verbForOp(op) {
|
|
132
|
+
switch (op) {
|
|
133
|
+
case 'add':
|
|
134
|
+
return 'added';
|
|
135
|
+
case 'remove':
|
|
136
|
+
return 'removed';
|
|
137
|
+
case 'replace':
|
|
138
|
+
return 'changed';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=diff-render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-render.js","sourceRoot":"","sources":["../../src/client/diff-render.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonPatchOp, StateDiff } from '../state-diff.js'\n\n/**\n * Humanized renderers for `StateDiff` (JSON-Patch). The raw shape is\n * accurate but technical (`{ op: 'add', path: '/items/3/name', value: 'X' }`);\n * the agent panel reads better in plain prose.\n *\n * Two output forms:\n * - `summarizeDiff` — one-line headline (\"3 items changed\") for a\n * row in the activity feed.\n * - `groupDiff` — structured per-top-level-path summary for an\n * expanded sidecar that lists what changed in each region.\n *\n * Both are pure functions; both treat the input as immutable. Callers\n * that need a different rendering (e.g. an emoji-driven layout, a\n * deeper drill-down) should compose on top of `groupDiff` rather than\n * forking — the grouping covers 90% of the structural work.\n */\n\n/**\n * One-line summary of the entire diff. Examples:\n *\n * - `[{ op: 'replace', path: '/cart/total', value: 9 }]`\n * → \"1 field changed\"\n * - `[{ op: 'add', path: '/items/-' }, { op: 'add', path: '/items/-' }]`\n * → \"2 items added\"\n * - mixed adds/removes/replaces across multiple regions\n * → \"5 changes across 3 regions\"\n *\n * The summary collapses multiple ops on the same logical path\n * (e.g. updating multiple fields on the same item) into a single\n * \"change\" — counting raw op entries would surface implementation\n * detail (which JSON-Patch ops the differ emitted), not user-relevant\n * counts.\n */\nexport function summarizeDiff(diff: StateDiff | undefined | null): string {\n if (!diff || diff.length === 0) return 'no changes'\n\n let adds = 0\n let removes = 0\n let replaces = 0\n const topPaths = new Set<string>()\n for (const op of diff) {\n topPaths.add(topLevelOf(op.path))\n if (op.op === 'add') adds++\n else if (op.op === 'remove') removes++\n else replaces++\n }\n\n // Single-region special case: name the region so the summary doesn't\n // hide WHERE the change happened. \"3 changes in cart\" beats \"3 changes\".\n if (topPaths.size === 1) {\n const region = Array.from(topPaths)[0]!\n if (region === '*') {\n return 'state replaced'\n }\n const total = adds + removes + replaces\n return `${total} change${total === 1 ? '' : 's'} in ${region}`\n }\n\n // Multi-region: prefer the dominant op verb when one dominates,\n // else fall back to a generic count.\n if (adds > 0 && removes === 0 && replaces === 0) {\n return `${adds} item${adds === 1 ? '' : 's'} added across ${topPaths.size} regions`\n }\n if (removes > 0 && adds === 0 && replaces === 0) {\n return `${removes} item${removes === 1 ? '' : 's'} removed across ${topPaths.size} regions`\n }\n if (replaces > 0 && adds === 0 && removes === 0) {\n return `${replaces} field${replaces === 1 ? '' : 's'} changed across ${topPaths.size} regions`\n }\n const total = adds + removes + replaces\n return `${total} changes across ${topPaths.size} regions`\n}\n\n/**\n * Per-top-level-path breakdown. Returns an array (stable order) where\n * each entry describes the changes affecting one top-level region.\n * Useful for a sidecar that wants to render a row per region with the\n * affected fields beneath it.\n *\n * The returned `paths` are the FULL JSON-Pointer paths of the ops, so\n * a consumer can render \"/items/3/name\" verbatim or further humanize\n * it. The renderer doesn't make policy choices about how deeply to\n * label — that's the host's call.\n */\nexport type DiffGroup = {\n /** Top-level state field, or `'*'` for whole-state replace. */\n region: string\n adds: number\n removes: number\n replaces: number\n /** Full op paths in arrival order. */\n paths: string[]\n}\n\nexport function groupDiff(diff: StateDiff | undefined | null): DiffGroup[] {\n if (!diff || diff.length === 0) return []\n const byRegion = new Map<string, DiffGroup>()\n for (const op of diff) {\n const region = topLevelOf(op.path)\n let g = byRegion.get(region)\n if (!g) {\n g = { region, adds: 0, removes: 0, replaces: 0, paths: [] }\n byRegion.set(region, g)\n }\n if (op.op === 'add') g.adds++\n else if (op.op === 'remove') g.removes++\n else g.replaces++\n g.paths.push(op.path)\n }\n return Array.from(byRegion.values())\n}\n\n/**\n * Per-op short verb + readable path. Useful for a flat detail view:\n *\n * - `{ op: 'replace', path: '/cart/total', value: 9 }` → `'changed cart.total'`\n * - `{ op: 'add', path: '/items/3' }` → `'added items.3'`\n * - `{ op: 'remove', path: '/items/3' }` → `'removed items.3'`\n * - `{ op: 'replace', path: '/' }` → `'replaced state'`\n *\n * The path is converted from JSON-Pointer to dotted form (with\n * `~0`/`~1` un-escaping) so it reads as a plain field accessor.\n */\nexport function describeOp(op: JsonPatchOp): string {\n if (op.path === '' || op.path === '/') {\n return op.op === 'replace' ? 'replaced state' : `${verbForOp(op.op)} state`\n }\n return `${verbForOp(op.op)} ${dottedPath(op.path)}`\n}\n\n// ── internals ────────────────────────────────────────────────────────\n\nfunction topLevelOf(path: string): string {\n if (path === '' || path === '/') return '*'\n // JSON Pointer: leading '/', then segments.\n const parts = path.split('/')\n return parts.length >= 2 && parts[1] ? unescapePointerSegment(parts[1]) : '*'\n}\n\nfunction dottedPath(path: string): string {\n if (path === '' || path === '/') return ''\n // Drop leading '/', un-escape pointer segments, join with '.'.\n const parts = path.split('/').slice(1).map(unescapePointerSegment)\n return parts.join('.')\n}\n\nfunction unescapePointerSegment(seg: string): string {\n // RFC 6901: `~1` first (so a `~0` in the input doesn't introduce\n // a `/` that interferes), then `~0`.\n return seg.replace(/~1/g, '/').replace(/~0/g, '~')\n}\n\nfunction verbForOp(op: JsonPatchOp['op']): string {\n switch (op) {\n case 'add':\n return 'added'\n case 'remove':\n return 'removed'\n case 'replace':\n return 'changed'\n }\n}\n"]}
|
|
@@ -4,6 +4,35 @@ export type EffectHandlerHost = {
|
|
|
4
4
|
send(msg: unknown): void;
|
|
5
5
|
/** Wraps an agentConnect msg into an app-Msg. */
|
|
6
6
|
wrapAgentConnect(m: unknown): unknown;
|
|
7
|
+
/**
|
|
8
|
+
* Wraps an agentAttention msg into an app-Msg. Optional; when
|
|
9
|
+
* undefined, the `AgentAttentionFlashTimeout` effect no-ops on
|
|
10
|
+
* fire and the attention spotlight stays set until the next
|
|
11
|
+
* dispatch replaces it. Hosts that wire the attention slice
|
|
12
|
+
* provide this; hosts that don't can leave it unset.
|
|
13
|
+
*/
|
|
14
|
+
wrapAgentAttention?(m: unknown): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Wraps an agentChat msg into an app-Msg. Required for hosts
|
|
17
|
+
* wiring the chat composer — the `AgentChatSendInput` handler
|
|
18
|
+
* dispatches `SubmitComplete` back into the slice via this wrapper
|
|
19
|
+
* after the frame send completes (success or failure), so the
|
|
20
|
+
* input field re-enables. Hosts that don't render chat leave it
|
|
21
|
+
* unset and the chat-send effect no-ops (the agentChat slice in
|
|
22
|
+
* that case has no live presence to begin with).
|
|
23
|
+
*/
|
|
24
|
+
wrapAgentChat?(m: unknown): unknown;
|
|
25
|
+
/**
|
|
26
|
+
* Active WS client (if any) — set lazily by the factory after
|
|
27
|
+
* `openWs` resolves a connection. The chat-send handler reads
|
|
28
|
+
* `submitUserInput` from here. Marked optional because the
|
|
29
|
+
* handler sits across the connection lifecycle: pre-WS / post-close
|
|
30
|
+
* `submitUserInput` is null and the handler degrades gracefully
|
|
31
|
+
* (still dispatches SubmitComplete so the UI re-enables).
|
|
32
|
+
*/
|
|
33
|
+
getWsClient?: () => {
|
|
34
|
+
submitUserInput(text: string, at?: number): void;
|
|
35
|
+
} | null;
|
|
7
36
|
/** Called for AgentForwardMsg — the payload is re-dispatched via send. */
|
|
8
37
|
forward(payload: unknown): void;
|
|
9
38
|
/** fetch for HTTP effects; override in tests. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effect-handler.d.ts","sourceRoot":"","sources":["../../src/client/effect-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAO/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEvD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;IACxB,iDAAiD;IACjD,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACrC,0EAA0E;IAC1E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAC/B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,yDAAyD;IACzD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1C,OAAO,IAAI,IAAI,CAAA;IACf;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAA;IAC3C;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,IAG5B,QAAQ,WAAW,KAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"effect-handler.d.ts","sourceRoot":"","sources":["../../src/client/effect-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAO/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEvD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;IACxB,iDAAiD;IACjD,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACrC;;;;;;OAMG;IACH,kBAAkB,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACxC;;;;;;;;OAQG;IACH,aAAa,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACnC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM;QAAE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/E,0EAA0E;IAC1E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAC/B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,yDAAyD;IACzD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1C,OAAO,IAAI,IAAI,CAAA;IACf;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAA;IAC3C;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,IAG5B,QAAQ,WAAW,KAAG,OAAO,CAAC,IAAI,CAAC,CA8CjE"}
|
|
@@ -49,6 +49,10 @@ export function createEffectHandler(host) {
|
|
|
49
49
|
return;
|
|
50
50
|
case 'AgentReconnectSchedule':
|
|
51
51
|
return handleReconnectSchedule(host, effect);
|
|
52
|
+
case 'AgentAttentionFlashTimeout':
|
|
53
|
+
return handleAttentionFlashTimeout(host, effect);
|
|
54
|
+
case 'AgentChatSendInput':
|
|
55
|
+
return handleChatSendInput(host, effect);
|
|
52
56
|
}
|
|
53
57
|
};
|
|
54
58
|
}
|
|
@@ -60,6 +64,41 @@ async function handleReconnectSchedule(host, effect) {
|
|
|
60
64
|
await new Promise((resolve) => setTimeout(resolve, effect.delayMs));
|
|
61
65
|
host.send(host.wrapAgentConnect({ type: 'ReconnectAttempt', elapsedMs: effect.delayMs }));
|
|
62
66
|
}
|
|
67
|
+
async function handleAttentionFlashTimeout(host, effect) {
|
|
68
|
+
// Mirror of handleReconnectSchedule's race-tolerant pattern: timer
|
|
69
|
+
// fires, the reducer's `Clear { entryId }` guard handles the case
|
|
70
|
+
// where a newer dispatch already replaced the spotlight. No cancel
|
|
71
|
+
// handle — keeping the timer-cancel logic out of the effect handler
|
|
72
|
+
// simplifies it and reduces the surface area for bugs.
|
|
73
|
+
if (!host.wrapAgentAttention)
|
|
74
|
+
return; // Host opted out — graceful degradation.
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, effect.delayMs));
|
|
76
|
+
host.send(host.wrapAgentAttention({ type: 'Clear', entryId: effect.entryId }));
|
|
77
|
+
}
|
|
78
|
+
async function handleChatSendInput(host, effect) {
|
|
79
|
+
const wrapper = host.wrapAgentChat;
|
|
80
|
+
// Frame send: best-effort. `submitUserInput` swallows its own
|
|
81
|
+
// throws (it's just `ws.send(JSON.stringify(...))` × 2), but a
|
|
82
|
+
// missing client (pre-open or post-close) silently no-ops the send
|
|
83
|
+
// and we just hit the SubmitComplete fall-through. The user's
|
|
84
|
+
// text was already cleared from `pendingInput` by the reducer; if
|
|
85
|
+
// the send fails, the host can render a session-status indicator
|
|
86
|
+
// separately (the `agentConnect.status` field is the canonical
|
|
87
|
+
// source of "are we online").
|
|
88
|
+
const client = host.getWsClient?.() ?? null;
|
|
89
|
+
if (client) {
|
|
90
|
+
try {
|
|
91
|
+
client.submitUserInput(effect.text, effect.at);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// best-effort — same contract as the frame-send path inside
|
|
95
|
+
// submitUserInput itself
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (wrapper) {
|
|
99
|
+
host.send(wrapper({ type: 'SubmitComplete' }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
63
102
|
// ── HTTP-bound handlers ─────────────────────────────────────────────
|
|
64
103
|
async function handleMintRequest(host, effect, doFetch) {
|
|
65
104
|
// Derive a default `mintUrl` from `agentBasePath` so consumers can
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effect-handler.js","sourceRoot":"","sources":["../../src/client/effect-handler.ts"],"names":[],"mappings":"AAiDA;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAuB;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAEpD,OAAO,KAAK,UAAU,MAAM,CAAC,MAAmB;QAC9C,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,kBAAkB;gBACrB,OAAO,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;YACjD,KAAK,aAAa;gBAChB,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YACnC,KAAK,cAAc;gBACjB,OAAO,aAAa,CAAC,IAAI,CAAC,CAAA;YAC5B,KAAK,kBAAkB;gBACrB,OAAO,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;YACjD,KAAK,kBAAkB;gBACrB,OAAO,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;YACjD,KAAK,aAAa;gBAChB,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;YAC5C,KAAK,mBAAmB;gBACtB,OAAO,kBAAkB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;YAC1C,KAAK,iBAAiB;gBACpB,OAAO,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YACvC,KAAK,qBAAqB;gBACxB,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAA;YACrC,KAAK,qBAAqB;gBACxB,wDAAwD;gBACxD,yDAAyD;gBACzD,mDAAmD;gBACnD,2BAA2B;gBAC3B,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;oBACxB,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;wBACxB,KAAK,EAAE,MAAM,CAAC,KAAK;wBACnB,GAAG,EAAE,MAAM,CAAC,GAAG;wBACf,MAAM,EAAE,MAAM,CAAC,MAAM;wBACrB,KAAK,EAAE,MAAM,CAAC,KAAK;wBACnB,SAAS,EAAE,MAAM,CAAC,SAAS;qBAC5B,CAAC,CAAA;gBACJ,CAAC;gBACD,OAAM;YACR,KAAK,mBAAmB;gBACtB,IAAI,IAAI,CAAC,cAAc;oBAAE,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAA;gBACpD,OAAM;YACR,KAAK,wBAAwB;gBAC3B,OAAO,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAChD,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,KAAK,UAAU,uBAAuB,CACpC,IAAuB,EACvB,MAAgE;IAEhE,iEAAiE;IACjE,kEAAkE;IAClE,iEAAiE;IACjE,6DAA6D;IAC7D,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;IACzE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;AAC3F,CAAC;AAED,uEAAuE;AAEvE,KAAK,UAAU,iBAAiB,CAC9B,IAAuB,EACvB,MAA0D,EAC1D,OAAc;IAEd,mEAAmE;IACnE,iEAAiE;IACjE,mEAAmE;IACnE,6CAA6C;IAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC5B,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,GAAG,IAAI,OAAO,CAAA;IAChD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,CAAA;QAC9E,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAA;YAClC,IAAI,CAAC,IAAI,CACP,IAAI,CAAC,gBAAgB,CAAC;gBACpB,IAAI,EAAE,YAAY;gBAClB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE;aAC9C,CAAC,CACH,CAAA;YACD,OAAM;QACR,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAiB,CAAA;QAC/C,IAAI,CAAC,IAAI,CACP,IAAI,CAAC,gBAAgB,CAAC;YACpB,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CACH,CAAA;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,IAAI,CACP,IAAI,CAAC,gBAAgB,CAAC;YACpB,IAAI,EAAE,YAAY;YAClB,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE;SAC9C,CAAC,CACH,CAAA;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,IAAuB,EACvB,MAA0D,EAC1D,OAAc;IAEd,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC5B,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,cAAc,EAAE;YAC/C,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;SAC5C,CAAC,CAAA;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAM;QACnB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAuB,CAAA;QACrD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;IACzF,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,IAAuB,EACvB,MAA0D,EAC1D,OAAc;IAEd,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC5B,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,eAAe,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;SAC1C,CAAC,CAAA;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAM;QACnB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwB,CAAA;QACtD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAA;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,WAAW;IACb,CAAC;AACH,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,IAAuB,EACvB,MAAqD,EACrD,OAAc;IAEd,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC5B,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,GAAG,IAAI,SAAS,EAAE;YAC9B,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;SAC1C,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,WAAW;IACb,CAAC;AACH,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,IAAuB,EAAE,OAAc;IACvE,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC5B,IAAI,CAAC,IAAI;QAAE,OAAM;IACjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,WAAW,EAAE;YAC5C,MAAM,EAAE,KAAK;YACb,WAAW,EAAE,SAAS;SACvB,CAAC,CAAA;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAM;QACnB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAqB,CAAA;QACnD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,WAAW;IACb,CAAC;AACH,CAAC;AAED,uEAAuE;AAEvE,SAAS,YAAY,CACnB,IAAuB,EACvB,MAAqD;IAErD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;AACzC,CAAC;AAED,SAAS,aAAa,CAAC,IAAuB;IAC5C,IAAI,CAAC,OAAO,EAAE,CAAA;AAChB,CAAC;AAED,uEAAuE;AAEvE,SAAS,gBAAgB,CACvB,IAAuB,EACvB,MAAyD;IAEzD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;AAC9B,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,MAA6D;IAE7D,kEAAkE;IAClE,oEAAoE;IACpE,4DAA4D;IAC5D,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,CAAC,CAAC,WAAW,IAAI,SAAS,CAAC;QAAE,OAAM;IAC3E,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;IACnE,CAAC;AACH,CAAC;AAED,uEAAuE;AAEvE,KAAK,UAAU,QAAQ,CAAC,GAAa;IACnC,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,SAAS,YAAY;IACnB,gEAAgE;IAChE,+DAA+D;IAC/D,wCAAwC;IACxC,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,QAAQ,CAAC,MAAM,CAAA;IAC3D,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,eAAe,GAAG,eAAe,CAAA;AAEvC;;;;;;GAMG;AACH,SAAS,SAAS,CAAC,IAAuB;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,IAAI,QAAQ,CAAA;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;IAC1D,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAA;IACjD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAA;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE,CAAA;AAC9B,CAAC","sourcesContent":["import type { AgentEffect } from './effects.js'\nimport type {\n MintResponse,\n ResumeListResponse,\n ResumeClaimResponse,\n SessionsResponse,\n} from '../protocol.js'\nimport type { AgentSessionStorage } from './factory.js'\n\nexport type EffectHandlerHost = {\n send(msg: unknown): void // root app send; wraps agent sub-msgs into the app Msg envelope\n /** Wraps an agentConnect msg into an app-Msg. */\n wrapAgentConnect(m: unknown): unknown\n /** Called for AgentForwardMsg — the payload is re-dispatched via send. */\n forward(payload: unknown): void\n /** fetch for HTTP effects; override in tests. */\n fetch?: typeof fetch\n /** Called before opening WS / on WS lifecycle events. */\n openWs(token: string, wsUrl: string): void\n closeWs(): void\n /**\n * Optional storage adapter. When set, `AgentSessionPersist` writes\n * to it and `AgentSessionClear` clears it; the host doesn't need\n * to handle these effects itself. When `null` or `undefined`, the\n * effects no-op here and host code (if any) handles them in the\n * outer effect router. The factory passes\n * `defaultSessionStorage()` by default, so the framework is\n * refresh-survival-ready out of the box.\n */\n sessionStorage?: AgentSessionStorage | null\n /**\n * Base path for agent HTTP endpoints. Default: `'/agent'` (matches\n * the canonical paths in `@llui/vite-plugin`'s dev middleware and\n * `@llui/agent/server/http/router.ts`).\n *\n * Override when the consumer ships `@cloudflare/vite-plugin` in\n * dev — that plugin routes every non-`/cdn-cgi/*` path to the\n * worker, shadowing canonical `/agent/*` URLs. The vite-plugin\n * registers a parallel handler at `/cdn-cgi/agent/*`; pass\n * `agentBasePath: '/cdn-cgi/agent'` here so the client hits that.\n *\n * Production deployments without cloudflare-vite leave this\n * unset; the agent server's router serves the canonical paths.\n */\n agentBasePath?: string\n}\n\ntype Fetch = typeof fetch\n\n/**\n * Top-level dispatcher. The switch is intentionally thin — each\n * `case` delegates to a per-effect function below. Splitting was\n * motivated by the previous 150-line monolith mixing HTTP, WS, and\n * browser-only side effects in one switch; per-effect handlers are\n * directly unit-testable and the dispatcher reads as a flat catalogue\n * of supported effect types.\n */\nexport function createEffectHandler(host: EffectHandlerHost) {\n const doFetch = host.fetch ?? fetch.bind(globalThis)\n\n return async function handle(effect: AgentEffect): Promise<void> {\n switch (effect.type) {\n case 'AgentMintRequest':\n return handleMintRequest(host, effect, doFetch)\n case 'AgentOpenWS':\n return handleOpenWs(host, effect)\n case 'AgentCloseWS':\n return handleCloseWs(host)\n case 'AgentResumeCheck':\n return handleResumeCheck(host, effect, doFetch)\n case 'AgentResumeClaim':\n return handleResumeClaim(host, effect, doFetch)\n case 'AgentRevoke':\n return handleRevoke(host, effect, doFetch)\n case 'AgentSessionsList':\n return handleSessionsList(host, doFetch)\n case 'AgentForwardMsg':\n return handleForwardMsg(host, effect)\n case 'AgentClipboardWrite':\n return handleClipboardWrite(effect)\n case 'AgentSessionPersist':\n // Framework-owned when a storage adapter is configured;\n // otherwise no-op and let the host's outer effect router\n // handle it (the legacy contract). See factory.ts'\n // `sessionStorage` option.\n if (host.sessionStorage) {\n host.sessionStorage.write({\n token: effect.token,\n tid: effect.tid,\n lapUrl: effect.lapUrl,\n wsUrl: effect.wsUrl,\n expiresAt: effect.expiresAt,\n })\n }\n return\n case 'AgentSessionClear':\n if (host.sessionStorage) host.sessionStorage.clear()\n return\n case 'AgentReconnectSchedule':\n return handleReconnectSchedule(host, effect)\n }\n }\n}\n\nasync function handleReconnectSchedule(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentReconnectSchedule' }>,\n): Promise<void> {\n // Single-shot timer. The reducer owns cancellation semantics via\n // the status guard in `ReconnectAttempt` — if the user dispatches\n // `Disconnect` while we're sleeping, the dispatched message hits\n // an `idle` reducer and is a no-op. No cancel handle needed.\n await new Promise<void>((resolve) => setTimeout(resolve, effect.delayMs))\n host.send(host.wrapAgentConnect({ type: 'ReconnectAttempt', elapsedMs: effect.delayMs }))\n}\n\n// ── HTTP-bound handlers ─────────────────────────────────────────────\n\nasync function handleMintRequest(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentMintRequest' }>,\n doFetch: Fetch,\n): Promise<void> {\n // Derive a default `mintUrl` from `agentBasePath` so consumers can\n // change the base path in one place (the effect handler) without\n // also having to keep the `agentConnect` opts in sync. `agentBase`\n // accepts both absolute paths and full URLs.\n const base = agentBase(host)\n if (!base) return\n const mintUrl = effect.mintUrl ?? `${base}/mint`\n try {\n const res = await doFetch(mintUrl, { method: 'POST', credentials: 'include' })\n if (!res.ok) {\n const detail = await safeText(res)\n host.send(\n host.wrapAgentConnect({\n type: 'MintFailed',\n error: { code: `http-${res.status}`, detail },\n }),\n )\n return\n }\n const body = (await res.json()) as MintResponse\n host.send(\n host.wrapAgentConnect({\n type: 'MintSucceeded',\n token: body.token,\n tid: body.tid,\n lapUrl: body.lapUrl,\n wsUrl: body.wsUrl,\n expiresAt: body.expiresAt,\n }),\n )\n } catch (e) {\n host.send(\n host.wrapAgentConnect({\n type: 'MintFailed',\n error: { code: 'network', detail: String(e) },\n }),\n )\n }\n}\n\nasync function handleResumeCheck(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentResumeCheck' }>,\n doFetch: Fetch,\n): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n const res = await doFetch(`${base}/resume/list`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ tids: effect.tids }),\n })\n if (!res.ok) return\n const body = (await res.json()) as ResumeListResponse\n host.send(host.wrapAgentConnect({ type: 'ResumeListLoaded', sessions: body.sessions }))\n } catch {\n /* quiet failure; user can retry */\n }\n}\n\nasync function handleResumeClaim(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentResumeClaim' }>,\n doFetch: Fetch,\n): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n const res = await doFetch(`${base}/resume/claim`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ tid: effect.tid }),\n })\n if (!res.ok) return\n const body = (await res.json()) as ResumeClaimResponse\n host.openWs(body.token, body.wsUrl)\n host.send(host.wrapAgentConnect({ type: 'WsOpened' }))\n } catch {\n /* quiet */\n }\n}\n\nasync function handleRevoke(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentRevoke' }>,\n doFetch: Fetch,\n): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n await doFetch(`${base}/revoke`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ tid: effect.tid }),\n })\n } catch {\n /* quiet */\n }\n}\n\nasync function handleSessionsList(host: EffectHandlerHost, doFetch: Fetch): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n const res = await doFetch(`${base}/sessions`, {\n method: 'GET',\n credentials: 'include',\n })\n if (!res.ok) return\n const body = (await res.json()) as SessionsResponse\n host.send(host.wrapAgentConnect({ type: 'SessionsLoaded', sessions: body.sessions }))\n } catch {\n /* quiet */\n }\n}\n\n// ── WS-bound handlers ───────────────────────────────────────────────\n\nfunction handleOpenWs(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentOpenWS' }>,\n): void {\n host.openWs(effect.token, effect.wsUrl)\n}\n\nfunction handleCloseWs(host: EffectHandlerHost): void {\n host.closeWs()\n}\n\n// ── Local handlers (no network) ─────────────────────────────────────\n\nfunction handleForwardMsg(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentForwardMsg' }>,\n): void {\n host.forward(effect.payload)\n}\n\nasync function handleClipboardWrite(\n effect: Extract<AgentEffect, { type: 'AgentClipboardWrite' }>,\n): Promise<void> {\n // Browser-only — `navigator.clipboard` is undefined in Node/jsdom\n // test environments. Silently no-op rather than throw, matching the\n // rest of the agent effect handlers' failure-quiet pattern.\n if (typeof navigator === 'undefined' || !('clipboard' in navigator)) return\n try {\n await navigator.clipboard.writeText(effect.text)\n } catch {\n /* quiet — clipboard permission denied or document not focused */\n }\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return await res.text()\n } catch {\n return ''\n }\n}\n\nfunction deriveOrigin(): string | null {\n // When running in the browser, `location.origin` is correct for\n // same-origin agent endpoints. Tests override `host.fetch` and\n // short-circuit before this is reached.\n if (typeof location !== 'undefined') return location.origin\n return null\n}\n\nconst ABSOLUTE_URL_RE = /^https?:\\/\\//i\n\n/**\n * Resolve the absolute base URL for agent HTTP endpoints. Accepts both\n * absolute paths (`/agent`) and full URLs (`https://api.example/agent`)\n * — the absolute URL form lets consumers point at a cross-origin agent\n * server without pre-composing every endpoint URL. Trailing slashes\n * are normalized so callers can always concatenate `${base}/mint`.\n */\nfunction agentBase(host: EffectHandlerHost): string | null {\n const raw = host.agentBasePath ?? '/agent'\n const trimmed = raw.endsWith('/') ? raw.slice(0, -1) : raw\n if (ABSOLUTE_URL_RE.test(trimmed)) return trimmed\n const origin = deriveOrigin()\n if (!origin) return null\n return `${origin}${trimmed}`\n}\n"]}
|
|
1
|
+
{"version":3,"file":"effect-handler.js","sourceRoot":"","sources":["../../src/client/effect-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentEffect } from './effects.js'\nimport type {\n MintResponse,\n ResumeListResponse,\n ResumeClaimResponse,\n SessionsResponse,\n} from '../protocol.js'\nimport type { AgentSessionStorage } from './factory.js'\n\nexport type EffectHandlerHost = {\n send(msg: unknown): void // root app send; wraps agent sub-msgs into the app Msg envelope\n /** Wraps an agentConnect msg into an app-Msg. */\n wrapAgentConnect(m: unknown): unknown\n /**\n * Wraps an agentAttention msg into an app-Msg. Optional; when\n * undefined, the `AgentAttentionFlashTimeout` effect no-ops on\n * fire and the attention spotlight stays set until the next\n * dispatch replaces it. Hosts that wire the attention slice\n * provide this; hosts that don't can leave it unset.\n */\n wrapAgentAttention?(m: unknown): unknown\n /**\n * Wraps an agentChat msg into an app-Msg. Required for hosts\n * wiring the chat composer — the `AgentChatSendInput` handler\n * dispatches `SubmitComplete` back into the slice via this wrapper\n * after the frame send completes (success or failure), so the\n * input field re-enables. Hosts that don't render chat leave it\n * unset and the chat-send effect no-ops (the agentChat slice in\n * that case has no live presence to begin with).\n */\n wrapAgentChat?(m: unknown): unknown\n /**\n * Active WS client (if any) — set lazily by the factory after\n * `openWs` resolves a connection. The chat-send handler reads\n * `submitUserInput` from here. Marked optional because the\n * handler sits across the connection lifecycle: pre-WS / post-close\n * `submitUserInput` is null and the handler degrades gracefully\n * (still dispatches SubmitComplete so the UI re-enables).\n */\n getWsClient?: () => { submitUserInput(text: string, at?: number): void } | null\n /** Called for AgentForwardMsg — the payload is re-dispatched via send. */\n forward(payload: unknown): void\n /** fetch for HTTP effects; override in tests. */\n fetch?: typeof fetch\n /** Called before opening WS / on WS lifecycle events. */\n openWs(token: string, wsUrl: string): void\n closeWs(): void\n /**\n * Optional storage adapter. When set, `AgentSessionPersist` writes\n * to it and `AgentSessionClear` clears it; the host doesn't need\n * to handle these effects itself. When `null` or `undefined`, the\n * effects no-op here and host code (if any) handles them in the\n * outer effect router. The factory passes\n * `defaultSessionStorage()` by default, so the framework is\n * refresh-survival-ready out of the box.\n */\n sessionStorage?: AgentSessionStorage | null\n /**\n * Base path for agent HTTP endpoints. Default: `'/agent'` (matches\n * the canonical paths in `@llui/vite-plugin`'s dev middleware and\n * `@llui/agent/server/http/router.ts`).\n *\n * Override when the consumer ships `@cloudflare/vite-plugin` in\n * dev — that plugin routes every non-`/cdn-cgi/*` path to the\n * worker, shadowing canonical `/agent/*` URLs. The vite-plugin\n * registers a parallel handler at `/cdn-cgi/agent/*`; pass\n * `agentBasePath: '/cdn-cgi/agent'` here so the client hits that.\n *\n * Production deployments without cloudflare-vite leave this\n * unset; the agent server's router serves the canonical paths.\n */\n agentBasePath?: string\n}\n\ntype Fetch = typeof fetch\n\n/**\n * Top-level dispatcher. The switch is intentionally thin — each\n * `case` delegates to a per-effect function below. Splitting was\n * motivated by the previous 150-line monolith mixing HTTP, WS, and\n * browser-only side effects in one switch; per-effect handlers are\n * directly unit-testable and the dispatcher reads as a flat catalogue\n * of supported effect types.\n */\nexport function createEffectHandler(host: EffectHandlerHost) {\n const doFetch = host.fetch ?? fetch.bind(globalThis)\n\n return async function handle(effect: AgentEffect): Promise<void> {\n switch (effect.type) {\n case 'AgentMintRequest':\n return handleMintRequest(host, effect, doFetch)\n case 'AgentOpenWS':\n return handleOpenWs(host, effect)\n case 'AgentCloseWS':\n return handleCloseWs(host)\n case 'AgentResumeCheck':\n return handleResumeCheck(host, effect, doFetch)\n case 'AgentResumeClaim':\n return handleResumeClaim(host, effect, doFetch)\n case 'AgentRevoke':\n return handleRevoke(host, effect, doFetch)\n case 'AgentSessionsList':\n return handleSessionsList(host, doFetch)\n case 'AgentForwardMsg':\n return handleForwardMsg(host, effect)\n case 'AgentClipboardWrite':\n return handleClipboardWrite(effect)\n case 'AgentSessionPersist':\n // Framework-owned when a storage adapter is configured;\n // otherwise no-op and let the host's outer effect router\n // handle it (the legacy contract). See factory.ts'\n // `sessionStorage` option.\n if (host.sessionStorage) {\n host.sessionStorage.write({\n token: effect.token,\n tid: effect.tid,\n lapUrl: effect.lapUrl,\n wsUrl: effect.wsUrl,\n expiresAt: effect.expiresAt,\n })\n }\n return\n case 'AgentSessionClear':\n if (host.sessionStorage) host.sessionStorage.clear()\n return\n case 'AgentReconnectSchedule':\n return handleReconnectSchedule(host, effect)\n case 'AgentAttentionFlashTimeout':\n return handleAttentionFlashTimeout(host, effect)\n case 'AgentChatSendInput':\n return handleChatSendInput(host, effect)\n }\n }\n}\n\nasync function handleReconnectSchedule(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentReconnectSchedule' }>,\n): Promise<void> {\n // Single-shot timer. The reducer owns cancellation semantics via\n // the status guard in `ReconnectAttempt` — if the user dispatches\n // `Disconnect` while we're sleeping, the dispatched message hits\n // an `idle` reducer and is a no-op. No cancel handle needed.\n await new Promise<void>((resolve) => setTimeout(resolve, effect.delayMs))\n host.send(host.wrapAgentConnect({ type: 'ReconnectAttempt', elapsedMs: effect.delayMs }))\n}\n\nasync function handleAttentionFlashTimeout(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentAttentionFlashTimeout' }>,\n): Promise<void> {\n // Mirror of handleReconnectSchedule's race-tolerant pattern: timer\n // fires, the reducer's `Clear { entryId }` guard handles the case\n // where a newer dispatch already replaced the spotlight. No cancel\n // handle — keeping the timer-cancel logic out of the effect handler\n // simplifies it and reduces the surface area for bugs.\n if (!host.wrapAgentAttention) return // Host opted out — graceful degradation.\n await new Promise<void>((resolve) => setTimeout(resolve, effect.delayMs))\n host.send(host.wrapAgentAttention({ type: 'Clear', entryId: effect.entryId }))\n}\n\nasync function handleChatSendInput(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentChatSendInput' }>,\n): Promise<void> {\n const wrapper = host.wrapAgentChat\n // Frame send: best-effort. `submitUserInput` swallows its own\n // throws (it's just `ws.send(JSON.stringify(...))` × 2), but a\n // missing client (pre-open or post-close) silently no-ops the send\n // and we just hit the SubmitComplete fall-through. The user's\n // text was already cleared from `pendingInput` by the reducer; if\n // the send fails, the host can render a session-status indicator\n // separately (the `agentConnect.status` field is the canonical\n // source of \"are we online\").\n const client = host.getWsClient?.() ?? null\n if (client) {\n try {\n client.submitUserInput(effect.text, effect.at)\n } catch {\n // best-effort — same contract as the frame-send path inside\n // submitUserInput itself\n }\n }\n if (wrapper) {\n host.send(wrapper({ type: 'SubmitComplete' }))\n }\n}\n\n// ── HTTP-bound handlers ─────────────────────────────────────────────\n\nasync function handleMintRequest(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentMintRequest' }>,\n doFetch: Fetch,\n): Promise<void> {\n // Derive a default `mintUrl` from `agentBasePath` so consumers can\n // change the base path in one place (the effect handler) without\n // also having to keep the `agentConnect` opts in sync. `agentBase`\n // accepts both absolute paths and full URLs.\n const base = agentBase(host)\n if (!base) return\n const mintUrl = effect.mintUrl ?? `${base}/mint`\n try {\n const res = await doFetch(mintUrl, { method: 'POST', credentials: 'include' })\n if (!res.ok) {\n const detail = await safeText(res)\n host.send(\n host.wrapAgentConnect({\n type: 'MintFailed',\n error: { code: `http-${res.status}`, detail },\n }),\n )\n return\n }\n const body = (await res.json()) as MintResponse\n host.send(\n host.wrapAgentConnect({\n type: 'MintSucceeded',\n token: body.token,\n tid: body.tid,\n lapUrl: body.lapUrl,\n wsUrl: body.wsUrl,\n expiresAt: body.expiresAt,\n }),\n )\n } catch (e) {\n host.send(\n host.wrapAgentConnect({\n type: 'MintFailed',\n error: { code: 'network', detail: String(e) },\n }),\n )\n }\n}\n\nasync function handleResumeCheck(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentResumeCheck' }>,\n doFetch: Fetch,\n): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n const res = await doFetch(`${base}/resume/list`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ tids: effect.tids }),\n })\n if (!res.ok) return\n const body = (await res.json()) as ResumeListResponse\n host.send(host.wrapAgentConnect({ type: 'ResumeListLoaded', sessions: body.sessions }))\n } catch {\n /* quiet failure; user can retry */\n }\n}\n\nasync function handleResumeClaim(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentResumeClaim' }>,\n doFetch: Fetch,\n): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n const res = await doFetch(`${base}/resume/claim`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ tid: effect.tid }),\n })\n if (!res.ok) return\n const body = (await res.json()) as ResumeClaimResponse\n host.openWs(body.token, body.wsUrl)\n host.send(host.wrapAgentConnect({ type: 'WsOpened' }))\n } catch {\n /* quiet */\n }\n}\n\nasync function handleRevoke(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentRevoke' }>,\n doFetch: Fetch,\n): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n await doFetch(`${base}/revoke`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ tid: effect.tid }),\n })\n } catch {\n /* quiet */\n }\n}\n\nasync function handleSessionsList(host: EffectHandlerHost, doFetch: Fetch): Promise<void> {\n const base = agentBase(host)\n if (!base) return\n try {\n const res = await doFetch(`${base}/sessions`, {\n method: 'GET',\n credentials: 'include',\n })\n if (!res.ok) return\n const body = (await res.json()) as SessionsResponse\n host.send(host.wrapAgentConnect({ type: 'SessionsLoaded', sessions: body.sessions }))\n } catch {\n /* quiet */\n }\n}\n\n// ── WS-bound handlers ───────────────────────────────────────────────\n\nfunction handleOpenWs(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentOpenWS' }>,\n): void {\n host.openWs(effect.token, effect.wsUrl)\n}\n\nfunction handleCloseWs(host: EffectHandlerHost): void {\n host.closeWs()\n}\n\n// ── Local handlers (no network) ─────────────────────────────────────\n\nfunction handleForwardMsg(\n host: EffectHandlerHost,\n effect: Extract<AgentEffect, { type: 'AgentForwardMsg' }>,\n): void {\n host.forward(effect.payload)\n}\n\nasync function handleClipboardWrite(\n effect: Extract<AgentEffect, { type: 'AgentClipboardWrite' }>,\n): Promise<void> {\n // Browser-only — `navigator.clipboard` is undefined in Node/jsdom\n // test environments. Silently no-op rather than throw, matching the\n // rest of the agent effect handlers' failure-quiet pattern.\n if (typeof navigator === 'undefined' || !('clipboard' in navigator)) return\n try {\n await navigator.clipboard.writeText(effect.text)\n } catch {\n /* quiet — clipboard permission denied or document not focused */\n }\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return await res.text()\n } catch {\n return ''\n }\n}\n\nfunction deriveOrigin(): string | null {\n // When running in the browser, `location.origin` is correct for\n // same-origin agent endpoints. Tests override `host.fetch` and\n // short-circuit before this is reached.\n if (typeof location !== 'undefined') return location.origin\n return null\n}\n\nconst ABSOLUTE_URL_RE = /^https?:\\/\\//i\n\n/**\n * Resolve the absolute base URL for agent HTTP endpoints. Accepts both\n * absolute paths (`/agent`) and full URLs (`https://api.example/agent`)\n * — the absolute URL form lets consumers point at a cross-origin agent\n * server without pre-composing every endpoint URL. Trailing slashes\n * are normalized so callers can always concatenate `${base}/mint`.\n */\nfunction agentBase(host: EffectHandlerHost): string | null {\n const raw = host.agentBasePath ?? '/agent'\n const trimmed = raw.endsWith('/') ? raw.slice(0, -1) : raw\n if (ABSOLUTE_URL_RE.test(trimmed)) return trimmed\n const origin = deriveOrigin()\n if (!origin) return null\n return `${origin}${trimmed}`\n}\n"]}
|
package/dist/client/effects.d.ts
CHANGED
|
@@ -72,6 +72,49 @@ export type AgentEffect =
|
|
|
72
72
|
| {
|
|
73
73
|
type: 'AgentReconnectSchedule';
|
|
74
74
|
delayMs: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Auto-clear the `agentAttention` spotlight after `delayMs`. The
|
|
78
|
+
* handler waits and dispatches `Clear { entryId }` back into the
|
|
79
|
+
* attention slice via `wrapAgentAttention`. The clear is conditional
|
|
80
|
+
* (matches `entryId` against `latestDispatch.entryId` in the reducer),
|
|
81
|
+
* so a fast follow-up dispatch isn't wiped by the previous dispatch's
|
|
82
|
+
* pending timer — same race-avoidance pattern as
|
|
83
|
+
* `AgentReconnectSchedule`'s status guard.
|
|
84
|
+
*
|
|
85
|
+
* No cancel handle: the handler is a thin `setTimeout` wrapper. If
|
|
86
|
+
* the host doesn't wire `wrapAttentionMsg` in the factory, the
|
|
87
|
+
* handler no-ops and the spotlight stays set until the next dispatch
|
|
88
|
+
* overwrites it (graceful degradation — the activity log still
|
|
89
|
+
* works, just without auto-clearing visual highlights).
|
|
90
|
+
*/
|
|
91
|
+
| {
|
|
92
|
+
type: 'AgentAttentionFlashTimeout';
|
|
93
|
+
entryId: string;
|
|
94
|
+
delayMs: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Send a user chat-composer submission upstream. Handler routes
|
|
98
|
+
* to `WsClient.submitUserInput(text, at)`, which:
|
|
99
|
+
*
|
|
100
|
+
* 1. emits a `user-input-submitted` WS frame so the server's
|
|
101
|
+
* parked `wait_for_user_input` waiters resolve;
|
|
102
|
+
* 2. synthesizes a `LogEntry { kind: 'user-input', detail: text }`
|
|
103
|
+
* and calls the factory's `onLogEntry` so `agentLog` (and
|
|
104
|
+
* mirror channels) render the user's reply inline with agent
|
|
105
|
+
* actions.
|
|
106
|
+
*
|
|
107
|
+
* After the frame sends, the handler dispatches
|
|
108
|
+
* `AgentChat.SubmitComplete` back into the chat slice via
|
|
109
|
+
* `wrapAgentChat` so the UI re-enables the input. The
|
|
110
|
+
* frame-send is best-effort: a closed/missing WS still resolves
|
|
111
|
+
* the SubmitComplete (so the input doesn't lock up); the user can
|
|
112
|
+
* retry once the connection is back.
|
|
113
|
+
*/
|
|
114
|
+
| {
|
|
115
|
+
type: 'AgentChatSendInput';
|
|
116
|
+
text: string;
|
|
117
|
+
at: number;
|
|
75
118
|
};
|
|
76
119
|
export type AgentEffectHandler = (effect: AgentEffect) => Promise<void>;
|
|
77
120
|
//# sourceMappingURL=effects.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effects.d.ts","sourceRoot":"","sources":["../../src/client/effects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD,MAAM,MAAM,WAAW;AACrB;;;;;;GAMG;AACD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAK7C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC/C;;;;;;;;;;;GAWG;GACD;IACE,IAAI,EAAE,qBAAqB,CAAA;IAC3B,KAAK,EAAE,UAAU,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB,GACD;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE;AAC/B;;;;;;;;;;;;GAYG;GACD;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"effects.d.ts","sourceRoot":"","sources":["../../src/client/effects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD,MAAM,MAAM,WAAW;AACrB;;;;;;GAMG;AACD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAK7C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC/C;;;;;;;;;;;GAWG;GACD;IACE,IAAI,EAAE,qBAAqB,CAAA;IAC3B,KAAK,EAAE,UAAU,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB,GACD;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE;AAC/B;;;;;;;;;;;;GAYG;GACD;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACrD;;;;;;;;;;;;;;GAcG;GACD;IAAE,IAAI,EAAE,4BAA4B,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC1E;;;;;;;;;;;;;;;;;GAiBG;GACD;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AAG5D,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"effects.js","sourceRoot":"","sources":["../../src/client/effects.ts"],"names":[],"mappings":"","sourcesContent":["import type { AgentToken } from '../protocol.js'\n\nexport type AgentEffect =\n /**\n * Mint a fresh agent token. `mintUrl` is optional — when omitted the\n * effect handler derives it from `EffectHandlerHost.agentBasePath`\n * (default `/agent`), producing `<agentBasePath>/mint`. Pass an\n * explicit value when the mint endpoint lives outside the configured\n * base path.\n */\n | { type: 'AgentMintRequest'; mintUrl?: string }\n | { type: 'AgentOpenWS'; token: AgentToken; wsUrl: string }\n | { type: 'AgentCloseWS' }\n | { type: 'AgentResumeCheck'; tids: string[] }\n | { type: 'AgentResumeClaim'; tid: string }\n | { type: 'AgentRevoke'; tid: string }\n | { type: 'AgentSessionsList' }\n | { type: 'AgentForwardMsg'; payload: unknown }\n // Handler reads `text` (no state lookup needed at handler time —\n // update() resolved it from the current state.pendingToken). Lets\n // the static-bag `connect()` shape avoid leaking state-reads into\n // event handlers.\n | { type: 'AgentClipboardWrite'; text: string }\n /**\n * Persist active session credentials so a page refresh can restore\n * the same WS without re-minting (and without invalidating the\n * agent's token via the rotate-on-resume path). Hosts typically\n * write to `sessionStorage` so the credentials are tab-scoped:\n * survive refresh, die on tab close. The framework emits this on\n * `MintSucceeded`; the matching `AgentSessionClear` is emitted on\n * `Revoke` of the active tid. Hosts that don't implement the\n * persist/restore loop can ignore both — the rest of the connect\n * lifecycle still works (the page just falls back to \"mint a new\n * session\" after refresh, same as before this effect existed).\n */\n | {\n type: 'AgentSessionPersist'\n token: AgentToken\n tid: string\n lapUrl: string\n wsUrl: string\n expiresAt: number\n }\n | { type: 'AgentSessionClear' }\n /**\n * Schedule the next WS-reconnect attempt. The handler waits\n * `delayMs` and dispatches `ReconnectAttempt { elapsedMs: delayMs }`\n * back into the reducer, which decides whether to re-open the WS\n * or transition to `failed` based on the cumulative wait. The\n * delay schedule itself is computed reducer-side from\n * `reconnectAttempt` — this effect is a thin setTimeout wrapper.\n *\n * The handler doesn't track cancellation: if the user dispatches\n * `Disconnect` while the timer is pending, the reducer transitions\n * to `idle` and the subsequent `ReconnectAttempt` becomes a no-op\n * via the status guard. Simpler than coordinating cancel handles.\n */\n | { type: 'AgentReconnectSchedule'; delayMs: number }\n\n// Handler implementation lands in Plan 7 alongside the WS client.\nexport type AgentEffectHandler = (effect: AgentEffect) => Promise<void>\n"]}
|
|
1
|
+
{"version":3,"file":"effects.js","sourceRoot":"","sources":["../../src/client/effects.ts"],"names":[],"mappings":"","sourcesContent":["import type { AgentToken } from '../protocol.js'\n\nexport type AgentEffect =\n /**\n * Mint a fresh agent token. `mintUrl` is optional — when omitted the\n * effect handler derives it from `EffectHandlerHost.agentBasePath`\n * (default `/agent`), producing `<agentBasePath>/mint`. Pass an\n * explicit value when the mint endpoint lives outside the configured\n * base path.\n */\n | { type: 'AgentMintRequest'; mintUrl?: string }\n | { type: 'AgentOpenWS'; token: AgentToken; wsUrl: string }\n | { type: 'AgentCloseWS' }\n | { type: 'AgentResumeCheck'; tids: string[] }\n | { type: 'AgentResumeClaim'; tid: string }\n | { type: 'AgentRevoke'; tid: string }\n | { type: 'AgentSessionsList' }\n | { type: 'AgentForwardMsg'; payload: unknown }\n // Handler reads `text` (no state lookup needed at handler time —\n // update() resolved it from the current state.pendingToken). Lets\n // the static-bag `connect()` shape avoid leaking state-reads into\n // event handlers.\n | { type: 'AgentClipboardWrite'; text: string }\n /**\n * Persist active session credentials so a page refresh can restore\n * the same WS without re-minting (and without invalidating the\n * agent's token via the rotate-on-resume path). Hosts typically\n * write to `sessionStorage` so the credentials are tab-scoped:\n * survive refresh, die on tab close. The framework emits this on\n * `MintSucceeded`; the matching `AgentSessionClear` is emitted on\n * `Revoke` of the active tid. Hosts that don't implement the\n * persist/restore loop can ignore both — the rest of the connect\n * lifecycle still works (the page just falls back to \"mint a new\n * session\" after refresh, same as before this effect existed).\n */\n | {\n type: 'AgentSessionPersist'\n token: AgentToken\n tid: string\n lapUrl: string\n wsUrl: string\n expiresAt: number\n }\n | { type: 'AgentSessionClear' }\n /**\n * Schedule the next WS-reconnect attempt. The handler waits\n * `delayMs` and dispatches `ReconnectAttempt { elapsedMs: delayMs }`\n * back into the reducer, which decides whether to re-open the WS\n * or transition to `failed` based on the cumulative wait. The\n * delay schedule itself is computed reducer-side from\n * `reconnectAttempt` — this effect is a thin setTimeout wrapper.\n *\n * The handler doesn't track cancellation: if the user dispatches\n * `Disconnect` while the timer is pending, the reducer transitions\n * to `idle` and the subsequent `ReconnectAttempt` becomes a no-op\n * via the status guard. Simpler than coordinating cancel handles.\n */\n | { type: 'AgentReconnectSchedule'; delayMs: number }\n /**\n * Auto-clear the `agentAttention` spotlight after `delayMs`. The\n * handler waits and dispatches `Clear { entryId }` back into the\n * attention slice via `wrapAgentAttention`. The clear is conditional\n * (matches `entryId` against `latestDispatch.entryId` in the reducer),\n * so a fast follow-up dispatch isn't wiped by the previous dispatch's\n * pending timer — same race-avoidance pattern as\n * `AgentReconnectSchedule`'s status guard.\n *\n * No cancel handle: the handler is a thin `setTimeout` wrapper. If\n * the host doesn't wire `wrapAttentionMsg` in the factory, the\n * handler no-ops and the spotlight stays set until the next dispatch\n * overwrites it (graceful degradation — the activity log still\n * works, just without auto-clearing visual highlights).\n */\n | { type: 'AgentAttentionFlashTimeout'; entryId: string; delayMs: number }\n /**\n * Send a user chat-composer submission upstream. Handler routes\n * to `WsClient.submitUserInput(text, at)`, which:\n *\n * 1. emits a `user-input-submitted` WS frame so the server's\n * parked `wait_for_user_input` waiters resolve;\n * 2. synthesizes a `LogEntry { kind: 'user-input', detail: text }`\n * and calls the factory's `onLogEntry` so `agentLog` (and\n * mirror channels) render the user's reply inline with agent\n * actions.\n *\n * After the frame sends, the handler dispatches\n * `AgentChat.SubmitComplete` back into the chat slice via\n * `wrapAgentChat` so the UI re-enables the input. The\n * frame-send is best-effort: a closed/missing WS still resolves\n * the SubmitComplete (so the input doesn't lock up); the user can\n * retry once the connection is back.\n */\n | { type: 'AgentChatSendInput'; text: string; at: number }\n\n// Handler implementation lands in Plan 7 alongside the WS client.\nexport type AgentEffectHandler = (effect: AgentEffect) => Promise<void>\n"]}
|
package/dist/client/factory.d.ts
CHANGED
|
@@ -83,6 +83,27 @@ export type CreateAgentClientOpts<State, Msg> = {
|
|
|
83
83
|
* stays empty (the UI won't show activity).
|
|
84
84
|
*/
|
|
85
85
|
wrapLogMsg?: (m: unknown) => Msg;
|
|
86
|
+
/**
|
|
87
|
+
* Optional: wrap an agentAttention msg so the visual-attention
|
|
88
|
+
* slice can clear its spotlight on the auto-clear timer. Hosts
|
|
89
|
+
* that wire `agentAttention` should set this; hosts that don't
|
|
90
|
+
* leave it unset and the spotlight (which they aren't rendering)
|
|
91
|
+
* never matters. The factory uses it for the reverse direction
|
|
92
|
+
* too: `onLogEntry` re-dispatches the same `Append { entry }`
|
|
93
|
+
* payload into the attention slice when wired, so a single
|
|
94
|
+
* incoming `log-append` frame fans out to both slices without
|
|
95
|
+
* the host needing to write the routing.
|
|
96
|
+
*/
|
|
97
|
+
wrapAttentionMsg?: (m: unknown) => Msg;
|
|
98
|
+
/**
|
|
99
|
+
* Optional: wrap an agentChat msg so the chat-composer's send
|
|
100
|
+
* effect can dispatch `SubmitComplete` back to re-enable the
|
|
101
|
+
* input. Hosts that wire the chat composer set this; hosts that
|
|
102
|
+
* don't can leave it unset and the chat-send effect no-ops
|
|
103
|
+
* (the chat slice was never wired anyway, so no UI is waiting
|
|
104
|
+
* for the SubmitComplete signal).
|
|
105
|
+
*/
|
|
106
|
+
wrapChatMsg?: (m: unknown) => Msg;
|
|
86
107
|
};
|
|
87
108
|
/**
|
|
88
109
|
* Codec registry for non-JSON-safe values (Date, Blob, Map, …)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EACZ,UAAU,EAEV,kBAAkB,EAEnB,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAoD,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAEnG;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN;IAAE,IAAI,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,GAC7C;IACE,IAAI,EAAE,qBAAqB,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAA;CACzD,CAAA;AAEL,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB;IACE,IAAI,EAAE,iBAAiB,CAAA;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAEL,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAA;CACzD,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;IACrD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAA;IACpF,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,YAAY,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,qBAAqB,CAAC,KAAK,EAAE,GAAG,IAAI;IAC9C,MAAM,EAAE,SAAS,CAAA;IACjB,GAAG,EAAE,iBAAiB,CAAA;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,OAAO,GAAG,IAAI,CAAA;IAC3B,MAAM,EAAE;QACN,UAAU,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,OAAO,CAAA;QACjC,UAAU,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,iBAAiB,CAAA;QAC3C,cAAc,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,GAAG,CAAA;QACnC,cAAc,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,GAAG,CAAA;QACnC;;;;;WAKG;QACH,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,GAAG,CAAA;
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/client/factory.ts"],"names":[],"mappings":"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"}
|