@llui/agent 0.0.43 → 0.0.45
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/agentConnect.d.ts +69 -1
- package/dist/client/agentConnect.d.ts.map +1 -1
- package/dist/client/agentConnect.js +201 -16
- package/dist/client/agentConnect.js.map +1 -1
- package/dist/client/effect-handler.d.ts +11 -0
- package/dist/client/effect-handler.d.ts.map +1 -1
- package/dist/client/effect-handler.js +29 -0
- package/dist/client/effect-handler.js.map +1 -1
- package/dist/client/effects.d.ts +39 -0
- package/dist/client/effects.d.ts.map +1 -1
- package/dist/client/effects.js.map +1 -1
- package/dist/client/factory.d.ts +53 -1
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +105 -0
- package/dist/client/factory.js.map +1 -1
- package/dist/server/core.d.ts +19 -0
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +35 -2
- package/dist/server/core.js.map +1 -1
- package/dist/server/lap/confirm-result.d.ts.map +1 -1
- package/dist/server/lap/confirm-result.js +2 -1
- package/dist/server/lap/confirm-result.js.map +1 -1
- package/dist/server/lap/describe.d.ts.map +1 -1
- package/dist/server/lap/describe.js +3 -2
- package/dist/server/lap/describe.js.map +1 -1
- package/dist/server/lap/forward.d.ts.map +1 -1
- package/dist/server/lap/forward.js +9 -6
- package/dist/server/lap/forward.js.map +1 -1
- package/dist/server/lap/message.d.ts.map +1 -1
- package/dist/server/lap/message.js +2 -1
- package/dist/server/lap/message.js.map +1 -1
- package/dist/server/lap/observe.d.ts.map +1 -1
- package/dist/server/lap/observe.js +6 -3
- package/dist/server/lap/observe.js.map +1 -1
- package/dist/server/lap/paused.d.ts +30 -0
- package/dist/server/lap/paused.d.ts.map +1 -0
- package/dist/server/lap/paused.js +38 -0
- package/dist/server/lap/paused.js.map +1 -0
- package/dist/server/lap/wait.d.ts.map +1 -1
- package/dist/server/lap/wait.js +2 -1
- package/dist/server/lap/wait.js.map +1 -1
- package/dist/server/token-store.d.ts.map +1 -1
- package/dist/server/token-store.js +7 -0
- package/dist/server/token-store.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Send } from '@llui/dom';
|
|
2
2
|
import type { AgentSession, AgentToken } from '../protocol.js';
|
|
3
3
|
import type { AgentEffect } from './effects.js';
|
|
4
|
-
export type AgentConnectStatus = 'idle' | 'minting' | 'pending-claude' | 'active' | 'error';
|
|
4
|
+
export type AgentConnectStatus = 'idle' | 'minting' | 'pending-claude' | 'active' | 'reconnecting' | 'failed' | 'error';
|
|
5
5
|
export type AgentConnectPendingToken = {
|
|
6
6
|
token: AgentToken;
|
|
7
7
|
tid: string;
|
|
@@ -15,6 +15,12 @@ export type AgentConnectPendingToken = {
|
|
|
15
15
|
*/
|
|
16
16
|
connectSnippet: string;
|
|
17
17
|
expiresAt: number;
|
|
18
|
+
/**
|
|
19
|
+
* Cached so the auto-reconnect path can re-open the WS without
|
|
20
|
+
* re-minting. The MintSucceeded → AgentOpenWS path stores it; the
|
|
21
|
+
* RestoreSession path also fills it in. Cleared by `Disconnect`.
|
|
22
|
+
*/
|
|
23
|
+
wsUrl: string;
|
|
18
24
|
};
|
|
19
25
|
export type AgentConnectState = {
|
|
20
26
|
status: AgentConnectStatus;
|
|
@@ -25,6 +31,21 @@ export type AgentConnectState = {
|
|
|
25
31
|
code: string;
|
|
26
32
|
detail: string;
|
|
27
33
|
} | null;
|
|
34
|
+
/**
|
|
35
|
+
* Reconnect attempt counter. Incremented on each WS-close that
|
|
36
|
+
* triggers an auto-reconnect; reset on `WsOpened` and on user
|
|
37
|
+
* actions (`Disconnect`, fresh `Mint`). Drives the backoff schedule
|
|
38
|
+
* (1s, 2s, 4s, 8s, 16s, 30s, 30s, …) and surfaces to UI as
|
|
39
|
+
* "reconnecting (attempt 3 / next in 4s)".
|
|
40
|
+
*/
|
|
41
|
+
reconnectAttempt: number;
|
|
42
|
+
/**
|
|
43
|
+
* Total cumulative ms spent in `reconnecting` for the current
|
|
44
|
+
* outage. Compared against `reconnectGiveUpMs` (effect-side option,
|
|
45
|
+
* default 5 min) to decide when to surface `failed` to the user.
|
|
46
|
+
* Reset whenever a WS opens successfully.
|
|
47
|
+
*/
|
|
48
|
+
reconnectElapsedMs: number;
|
|
28
49
|
};
|
|
29
50
|
export type AgentConnectMsg =
|
|
30
51
|
/** @intent("Mint a new agent token and open the pairing WebSocket") */
|
|
@@ -104,6 +125,53 @@ export type AgentConnectMsg =
|
|
|
104
125
|
*/
|
|
105
126
|
| {
|
|
106
127
|
type: 'CopyConnectSnippet';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @humanOnly — internal: app boot dispatches this with credentials
|
|
131
|
+
* read from sessionStorage to skip the mint round-trip after page
|
|
132
|
+
* refresh. The agent's token (still alive on the server) keeps
|
|
133
|
+
* working since we don't go through the rotate-on-resume path. The
|
|
134
|
+
* reducer is idempotent against an in-flight Mint — only fires from
|
|
135
|
+
* `idle`.
|
|
136
|
+
*/
|
|
137
|
+
| {
|
|
138
|
+
type: 'RestoreSession';
|
|
139
|
+
token: AgentToken;
|
|
140
|
+
tid: string;
|
|
141
|
+
lapUrl: string;
|
|
142
|
+
wsUrl: string;
|
|
143
|
+
expiresAt: number;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* @intent("Disconnect the active agent session and clear all
|
|
147
|
+
* persisted credentials. Stops any in-flight reconnect attempt;
|
|
148
|
+
* subsequent WS closures stay in `idle` instead of triggering
|
|
149
|
+
* auto-reconnect. Use when the user explicitly clicks Disconnect
|
|
150
|
+
* in the panel — for transient drops, do nothing and let the
|
|
151
|
+
* reconnect loop run.")
|
|
152
|
+
*/
|
|
153
|
+
| {
|
|
154
|
+
type: 'Disconnect';
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @humanOnly — internal: scheduler effect dispatched this when the
|
|
158
|
+
* backoff timer fired. The reducer increments the attempt counter,
|
|
159
|
+
* adds the just-elapsed delay to `reconnectElapsedMs`, and emits
|
|
160
|
+
* `AgentOpenWS` with the cached pendingToken/wsUrl so the WS can
|
|
161
|
+
* reattach without minting.
|
|
162
|
+
*/
|
|
163
|
+
| {
|
|
164
|
+
type: 'ReconnectAttempt';
|
|
165
|
+
elapsedMs: number;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* @humanOnly — internal: scheduler effect dispatched this when the
|
|
169
|
+
* give-up ceiling was reached without a successful WS open.
|
|
170
|
+
* Reducer flips status to `failed` so the UI can surface a clear
|
|
171
|
+
* error and offer a manual reconnect.
|
|
172
|
+
*/
|
|
173
|
+
| {
|
|
174
|
+
type: 'ReconnectGaveUp';
|
|
107
175
|
};
|
|
108
176
|
/**
|
|
109
177
|
* Options threaded through `init()` and `update()`. `mintUrl` is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentConnect.d.ts","sourceRoot":"","sources":["../../src/client/agentConnect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C,MAAM,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"agentConnect.d.ts","sourceRoot":"","sources":["../../src/client/agentConnect.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { tagSend } from '@llui/dom';
|
|
2
|
+
/**
|
|
3
|
+
* Backoff schedule for the auto-reconnect loop. Doubles starting at
|
|
4
|
+
* 1s, caps at 30s. Translates `state.reconnectAttempt` into the next
|
|
5
|
+
* delay; the effect handler schedules a `setTimeout` for that long
|
|
6
|
+
* and dispatches `ReconnectAttempt` when it fires.
|
|
7
|
+
*
|
|
8
|
+
* Lives in the reducer so tests can pin the timings without poking
|
|
9
|
+
* effect-handler internals; the constants are not exported because
|
|
10
|
+
* tweaking them changes UX more than tweaks to the give-up ceiling.
|
|
11
|
+
*/
|
|
12
|
+
const RECONNECT_BASE_MS = 1000;
|
|
13
|
+
const RECONNECT_CAP_MS = 30_000;
|
|
14
|
+
function reconnectDelayMs(attempt) {
|
|
15
|
+
const factor = Math.min(Math.pow(2, attempt), RECONNECT_CAP_MS / RECONNECT_BASE_MS);
|
|
16
|
+
return Math.min(RECONNECT_BASE_MS * factor, RECONNECT_CAP_MS);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Total cumulative wait, across all reconnect attempts, before the
|
|
20
|
+
* loop gives up and transitions to `'failed'`. 5 minutes is long
|
|
21
|
+
* enough to weather a brief server outage but short enough that a
|
|
22
|
+
* permanently-down endpoint surfaces clearly to the user instead of
|
|
23
|
+
* silently spinning.
|
|
24
|
+
*/
|
|
25
|
+
const RECONNECT_GIVE_UP_MS = 5 * 60 * 1000;
|
|
2
26
|
/** Component shape is [State, Effect[]] — consistent with @llui/components. */
|
|
3
27
|
export function init(_opts) {
|
|
4
28
|
return [
|
|
@@ -8,6 +32,8 @@ export function init(_opts) {
|
|
|
8
32
|
sessions: [],
|
|
9
33
|
resumable: [],
|
|
10
34
|
error: null,
|
|
35
|
+
reconnectAttempt: 0,
|
|
36
|
+
reconnectElapsedMs: 0,
|
|
11
37
|
},
|
|
12
38
|
[],
|
|
13
39
|
];
|
|
@@ -24,36 +50,187 @@ export function update(state, msg, opts = {}) {
|
|
|
24
50
|
return [{ ...state, status: 'minting' }, [mintEffect]];
|
|
25
51
|
}
|
|
26
52
|
case 'MintSucceeded': {
|
|
27
|
-
// The connect snippet has to work across every
|
|
28
|
-
// Claude Desktop
|
|
29
|
-
// but Claude Code
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
53
|
+
// The connect snippet has to work across every MCP surface.
|
|
54
|
+
// Claude Desktop and similar clients expose MCP tools as bare
|
|
55
|
+
// names (`connect_session`), but Claude Code (and other tool-list-
|
|
56
|
+
// namespacing clients) emit them as `mcp__llui__connect_session`
|
|
57
|
+
// and may defer-load them — so an LLM that searches its tool list
|
|
58
|
+
// for a literal `connect_session` won't find it. Naming the LLui
|
|
59
|
+
// MCP server explicitly (with its canonical `llui` install name,
|
|
60
|
+
// matching the install docs) gives the model enough to resolve
|
|
61
|
+
// the right tool on either platform; the parenthetical names the
|
|
62
|
+
// edge case so a deferred-tool client doesn't bail out.
|
|
63
|
+
//
|
|
64
|
+
// Phrased generically (`AI assistant`, `Some MCP clients`) since
|
|
65
|
+
// MCP support is rapidly expanding past Claude — the snippet
|
|
66
|
+
// shouldn't telegraph "this is Claude-only" when it works against
|
|
67
|
+
// any compliant client. The literal `mcp__llui__` prefix matches
|
|
68
|
+
// the install command in `site/content/agents.md`; users who
|
|
69
|
+
// renamed the server in their config can substitute their name.
|
|
34
70
|
const pending = {
|
|
35
71
|
token: msg.token,
|
|
36
72
|
tid: msg.tid,
|
|
37
73
|
lapUrl: msg.lapUrl,
|
|
38
|
-
connectSnippet: `Connect this
|
|
74
|
+
connectSnippet: `Connect this AI assistant to the LLui app. Call the LLui MCP server's ` +
|
|
75
|
+
`\`connect_session\` tool with url=${JSON.stringify(msg.lapUrl)} and ` +
|
|
76
|
+
`token=${JSON.stringify(msg.token)}. ` +
|
|
77
|
+
`(Some MCP clients namespace tools as ` +
|
|
78
|
+
`\`mcp__llui__connect_session\` and load them lazily — search the tool list if \`connect_session\` isn't immediately available.)`,
|
|
79
|
+
expiresAt: msg.expiresAt,
|
|
80
|
+
wsUrl: msg.wsUrl,
|
|
81
|
+
};
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
...state,
|
|
85
|
+
status: 'pending-claude',
|
|
86
|
+
pendingToken: pending,
|
|
87
|
+
error: null,
|
|
88
|
+
reconnectAttempt: 0,
|
|
89
|
+
reconnectElapsedMs: 0,
|
|
90
|
+
},
|
|
91
|
+
[
|
|
92
|
+
{ type: 'AgentOpenWS', token: msg.token, wsUrl: msg.wsUrl },
|
|
93
|
+
// Persist alongside opening the WS so the host can store the
|
|
94
|
+
// credentials in sessionStorage; on page refresh, app boot
|
|
95
|
+
// dispatches `RestoreSession` with the same shape and we
|
|
96
|
+
// re-enter the same state without re-minting.
|
|
97
|
+
{
|
|
98
|
+
type: 'AgentSessionPersist',
|
|
99
|
+
token: msg.token,
|
|
100
|
+
tid: msg.tid,
|
|
101
|
+
lapUrl: msg.lapUrl,
|
|
102
|
+
wsUrl: msg.wsUrl,
|
|
103
|
+
expiresAt: msg.expiresAt,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
case 'RestoreSession': {
|
|
109
|
+
// Idempotent guard: only fires from idle. A racing Mint click
|
|
110
|
+
// would already have moved us to `minting` — restoring on top
|
|
111
|
+
// would clobber the in-flight pending state with stale
|
|
112
|
+
// credentials read from sessionStorage. Easier to no-op here
|
|
113
|
+
// than to coordinate the race in the host.
|
|
114
|
+
if (state.status !== 'idle')
|
|
115
|
+
return [state, []];
|
|
116
|
+
// Regenerate the connectSnippet so the user can re-paste if
|
|
117
|
+
// their AI lost the original tool call (same shape as
|
|
118
|
+
// MintSucceeded — the framework owns this string and updates
|
|
119
|
+
// to it ride along the agent package version).
|
|
120
|
+
const restored = {
|
|
121
|
+
token: msg.token,
|
|
122
|
+
tid: msg.tid,
|
|
123
|
+
lapUrl: msg.lapUrl,
|
|
124
|
+
connectSnippet: `Connect this AI assistant to the LLui app. Call the LLui MCP server's ` +
|
|
39
125
|
`\`connect_session\` tool with url=${JSON.stringify(msg.lapUrl)} and ` +
|
|
40
126
|
`token=${JSON.stringify(msg.token)}. ` +
|
|
41
|
-
`(
|
|
42
|
-
`\`
|
|
127
|
+
`(Some MCP clients namespace tools as ` +
|
|
128
|
+
`\`mcp__llui__connect_session\` and load them lazily — search the tool list if \`connect_session\` isn't immediately available.)`,
|
|
43
129
|
expiresAt: msg.expiresAt,
|
|
130
|
+
wsUrl: msg.wsUrl,
|
|
44
131
|
};
|
|
45
132
|
return [
|
|
46
|
-
{
|
|
133
|
+
{
|
|
134
|
+
...state,
|
|
135
|
+
status: 'pending-claude',
|
|
136
|
+
pendingToken: restored,
|
|
137
|
+
error: null,
|
|
138
|
+
reconnectAttempt: 0,
|
|
139
|
+
reconnectElapsedMs: 0,
|
|
140
|
+
},
|
|
47
141
|
[{ type: 'AgentOpenWS', token: msg.token, wsUrl: msg.wsUrl }],
|
|
48
142
|
];
|
|
49
143
|
}
|
|
50
144
|
case 'MintFailed':
|
|
51
145
|
return [{ ...state, status: 'error', error: msg.error }, []];
|
|
52
|
-
case 'WsOpened':
|
|
146
|
+
case 'WsOpened': {
|
|
53
147
|
// WS is open but Claude hasn't bound yet; stay at pending-claude.
|
|
148
|
+
// If we were `reconnecting`, this is a successful reattach —
|
|
149
|
+
// back to pending-claude and reset the attempt counters.
|
|
150
|
+
if (state.status === 'reconnecting') {
|
|
151
|
+
return [
|
|
152
|
+
{ ...state, status: 'pending-claude', reconnectAttempt: 0, reconnectElapsedMs: 0 },
|
|
153
|
+
[],
|
|
154
|
+
];
|
|
155
|
+
}
|
|
54
156
|
return [state, []];
|
|
55
|
-
|
|
56
|
-
|
|
157
|
+
}
|
|
158
|
+
case 'WsClosed': {
|
|
159
|
+
// Three cases:
|
|
160
|
+
// 1. We had no pendingToken (already idle / pre-mint) → no-op.
|
|
161
|
+
// 2. Status is `idle` or `failed` (Disconnect already cleared,
|
|
162
|
+
// or we previously gave up) → no-op so a delayed close
|
|
163
|
+
// event after Disconnect doesn't accidentally restart the
|
|
164
|
+
// loop.
|
|
165
|
+
// 3. We're connected/connecting and the close was unsolicited
|
|
166
|
+
// → schedule a reconnect with backoff.
|
|
167
|
+
if (state.pendingToken === null)
|
|
168
|
+
return [{ ...state, status: 'idle' }, []];
|
|
169
|
+
if (state.status === 'idle' || state.status === 'failed')
|
|
170
|
+
return [state, []];
|
|
171
|
+
const delayMs = reconnectDelayMs(state.reconnectAttempt);
|
|
172
|
+
return [
|
|
173
|
+
{ ...state, status: 'reconnecting', error: null },
|
|
174
|
+
[{ type: 'AgentReconnectSchedule', delayMs }],
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
case 'ReconnectAttempt': {
|
|
178
|
+
// Backoff timer fired. If the user disconnected in the gap, we
|
|
179
|
+
// moved to idle and ignore. Otherwise, increment attempt + add
|
|
180
|
+
// the elapsed delay to the cumulative window. Past the give-up
|
|
181
|
+
// ceiling, transition to `failed` so the UI can offer a manual
|
|
182
|
+
// reconnect; otherwise re-open the WS with the cached
|
|
183
|
+
// credentials (no mint, same token — the server's grace window
|
|
184
|
+
// is what makes this transparent to the agent).
|
|
185
|
+
if (state.status !== 'reconnecting' || state.pendingToken === null) {
|
|
186
|
+
return [state, []];
|
|
187
|
+
}
|
|
188
|
+
const newElapsed = state.reconnectElapsedMs + msg.elapsedMs;
|
|
189
|
+
if (newElapsed >= RECONNECT_GIVE_UP_MS) {
|
|
190
|
+
return [{ ...state, status: 'failed', reconnectElapsedMs: newElapsed }, []];
|
|
191
|
+
}
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
...state,
|
|
195
|
+
reconnectAttempt: state.reconnectAttempt + 1,
|
|
196
|
+
reconnectElapsedMs: newElapsed,
|
|
197
|
+
},
|
|
198
|
+
[
|
|
199
|
+
{
|
|
200
|
+
type: 'AgentOpenWS',
|
|
201
|
+
token: state.pendingToken.token,
|
|
202
|
+
wsUrl: state.pendingToken.wsUrl,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
case 'ReconnectGaveUp':
|
|
208
|
+
return [{ ...state, status: 'failed' }, []];
|
|
209
|
+
case 'Disconnect': {
|
|
210
|
+
// User-initiated. Revoke the active tid (server kills the
|
|
211
|
+
// pairing), wipe the persisted credentials so a refresh can't
|
|
212
|
+
// restore them, and zero the reconnect counters so any in-
|
|
213
|
+
// flight backoff timer that fires post-disconnect becomes a
|
|
214
|
+
// no-op (the status guard in `ReconnectAttempt` keeps it from
|
|
215
|
+
// re-opening the WS).
|
|
216
|
+
const tid = state.pendingToken?.tid;
|
|
217
|
+
const effects = [];
|
|
218
|
+
if (tid !== undefined)
|
|
219
|
+
effects.push({ type: 'AgentRevoke', tid });
|
|
220
|
+
effects.push({ type: 'AgentSessionClear' });
|
|
221
|
+
effects.push({ type: 'AgentCloseWS' });
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
...state,
|
|
225
|
+
status: 'idle',
|
|
226
|
+
pendingToken: null,
|
|
227
|
+
error: null,
|
|
228
|
+
reconnectAttempt: 0,
|
|
229
|
+
reconnectElapsedMs: 0,
|
|
230
|
+
},
|
|
231
|
+
effects,
|
|
232
|
+
];
|
|
233
|
+
}
|
|
57
234
|
case 'ActivatedByClaude':
|
|
58
235
|
return [{ ...state, status: 'active' }, []];
|
|
59
236
|
case 'ResumeList':
|
|
@@ -63,14 +240,22 @@ export function update(state, msg, opts = {}) {
|
|
|
63
240
|
case 'Resume':
|
|
64
241
|
return [state, [{ type: 'AgentResumeClaim', tid: msg.tid }]];
|
|
65
242
|
case 'Revoke': {
|
|
66
|
-
// Optimistically remove from sessions + resumable.
|
|
243
|
+
// Optimistically remove from sessions + resumable. If the
|
|
244
|
+
// revoked tid matches the currently-pending session, also fire
|
|
245
|
+
// AgentSessionClear so the host wipes its persisted credentials
|
|
246
|
+
// — otherwise a refresh would try to RestoreSession with a
|
|
247
|
+
// server-side-revoked token and end up at an auth-failed WS.
|
|
248
|
+
const isActiveTid = state.pendingToken !== null && state.pendingToken.tid === msg.tid;
|
|
249
|
+
const effects = [{ type: 'AgentRevoke', tid: msg.tid }];
|
|
250
|
+
if (isActiveTid)
|
|
251
|
+
effects.push({ type: 'AgentSessionClear' });
|
|
67
252
|
return [
|
|
68
253
|
{
|
|
69
254
|
...state,
|
|
70
255
|
sessions: state.sessions.filter((s) => s.tid !== msg.tid),
|
|
71
256
|
resumable: state.resumable.filter((s) => s.tid !== msg.tid),
|
|
72
257
|
},
|
|
73
|
-
|
|
258
|
+
effects,
|
|
74
259
|
];
|
|
75
260
|
}
|
|
76
261
|
case 'ClearError':
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentConnect.js","sourceRoot":"","sources":["../../src/client/agentConnect.ts"],"names":[],"mappings":"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","sourcesContent":["import { tagSend, type Send } from '@llui/dom'\nimport type { AgentSession, AgentToken } from '../protocol.js'\nimport type { AgentEffect } from './effects.js'\n\nexport type AgentConnectStatus = 'idle' | 'minting' | 'pending-claude' | 'active' | 'error'\n\nexport type AgentConnectPendingToken = {\n token: AgentToken\n tid: string\n lapUrl: string\n /**\n * Natural-language connect instruction the user copies into Claude.\n * Includes URL, token, and the explicit `connect_session` tool\n * call. Works in any Claude client (Desktop, CC CLI, etc.) — the\n * Desktop-specific `/llui-connect` slash command is sugar over the\n * same tool call.\n */\n connectSnippet: string\n expiresAt: number\n}\n\nexport type AgentConnectState = {\n status: AgentConnectStatus\n pendingToken: AgentConnectPendingToken | null\n sessions: AgentSession[]\n resumable: AgentSession[]\n error: { code: string; detail: string } | null\n}\n\nexport type AgentConnectMsg =\n /** @intent(\"Mint a new agent token and open the pairing WebSocket\") */\n | { type: 'Mint' }\n /**\n * @humanOnly — internal: dispatched by the AgentMintRequest effect\n * handler when the mint endpoint replies success. Carries the token\n * and connection URLs into state.\n */\n | {\n type: 'MintSucceeded'\n token: AgentToken\n tid: string\n lapUrl: string\n wsUrl: string\n expiresAt: number\n }\n /** @humanOnly — internal: dispatched by the AgentMintRequest handler on failure. */\n | { type: 'MintFailed'; error: { code: string; detail: string } }\n /** @humanOnly — internal: WS adapter signalled the pairing socket is open. */\n | { type: 'WsOpened' }\n /** @humanOnly — internal: WS adapter signalled the pairing socket is closed. */\n | { type: 'WsClosed' }\n /** @humanOnly — internal: Claude bound the session via /agent/claim. */\n | { type: 'ActivatedByClaude' }\n /** @intent(\"Check which previously-issued agent sessions can be resumed\") */\n | { type: 'ResumeList'; tids: string[] }\n /** @humanOnly — internal: AgentResumeCheck effect handler returned the list. */\n | { type: 'ResumeListLoaded'; sessions: AgentSession[] }\n /** @intent(\"Resume an existing agent session by tid\") */\n | { type: 'Resume'; tid: string }\n /** @intent(\"Revoke an agent session by tid\") */\n | { type: 'Revoke'; tid: string }\n /** @intent(\"Dismiss the current agent connect error\") */\n | { type: 'ClearError' }\n /** @humanOnly — internal: AgentSessionsList effect handler returned the list. */\n | { type: 'SessionsLoaded'; sessions: AgentSession[] }\n /** @intent(\"Refresh the list of active agent sessions\") */\n | { type: 'RefreshSessions' }\n /**\n * @intent(\"Copy the agent connect snippet to the clipboard\")\n * Resolves the pendingToken's snippet in update() (state-reading is\n * what update() is for) and dispatches a clipboard-write effect.\n */\n | { type: 'CopyConnectSnippet' }\n\n/**\n * Options threaded through `init()` and `update()`. `mintUrl` is\n * optional — when omitted the agent effect handler derives it from\n * `EffectHandlerHost.agentBasePath` (default `/agent` → `/agent/mint`).\n * Set explicitly only when the mint endpoint lives outside the\n * configured base path.\n */\nexport type AgentConnectInitOpts = { mintUrl?: string }\n\n/** Component shape is [State, Effect[]] — consistent with @llui/components. */\nexport function init(_opts: AgentConnectInitOpts): [AgentConnectState, AgentEffect[]] {\n return [\n {\n status: 'idle',\n pendingToken: null,\n sessions: [],\n resumable: [],\n error: null,\n },\n [],\n ]\n}\n\nexport function update(\n state: AgentConnectState,\n msg: AgentConnectMsg,\n opts: AgentConnectInitOpts = {},\n): [AgentConnectState, AgentEffect[]] {\n switch (msg.type) {\n case 'Mint': {\n // mintUrl: undefined means \"let the effect handler derive it\n // from agentBasePath\". Only include the property when explicitly\n // set, so the effect's discriminated shape stays clean.\n const mintEffect: AgentEffect =\n opts.mintUrl !== undefined\n ? { type: 'AgentMintRequest', mintUrl: opts.mintUrl }\n : { type: 'AgentMintRequest' }\n return [{ ...state, status: 'minting' }, [mintEffect]]\n }\n case 'MintSucceeded': {\n // The connect snippet has to work across every Claude surface.\n // Claude Desktop exposes MCP tools as bare names (`connect_session`),\n // but Claude Code namespaces them (`mcp__<server>__connect_session`)\n // and may defer-load them — so an LLM that searches its tool list for\n // a literal `connect_session` won't find it. Naming the LLui MCP\n // server explicitly gives the model enough to resolve the right tool\n // on either platform.\n const pending: AgentConnectPendingToken = {\n token: msg.token,\n tid: msg.tid,\n lapUrl: msg.lapUrl,\n connectSnippet:\n `Connect this Claude session to the LLui app. Call the LLui MCP server's ` +\n `\\`connect_session\\` tool with url=${JSON.stringify(msg.lapUrl)} and ` +\n `token=${JSON.stringify(msg.token)}. ` +\n `(In Claude Code the tool may be namespaced as ` +\n `\\`mcp__<server>__connect_session\\` and deferred — load it via tool search if needed.)`,\n expiresAt: msg.expiresAt,\n }\n return [\n { ...state, status: 'pending-claude', pendingToken: pending, error: null },\n [{ type: 'AgentOpenWS', token: msg.token, wsUrl: msg.wsUrl }],\n ]\n }\n case 'MintFailed':\n return [{ ...state, status: 'error', error: msg.error }, []]\n case 'WsOpened':\n // WS is open but Claude hasn't bound yet; stay at pending-claude.\n return [state, []]\n case 'WsClosed':\n return [{ ...state, status: 'idle', pendingToken: null }, []]\n case 'ActivatedByClaude':\n return [{ ...state, status: 'active' }, []]\n case 'ResumeList':\n return [state, [{ type: 'AgentResumeCheck', tids: msg.tids }]]\n case 'ResumeListLoaded':\n return [{ ...state, resumable: msg.sessions }, []]\n case 'Resume':\n return [state, [{ type: 'AgentResumeClaim', tid: msg.tid }]]\n case 'Revoke': {\n // Optimistically remove from sessions + resumable.\n return [\n {\n ...state,\n sessions: state.sessions.filter((s) => s.tid !== msg.tid),\n resumable: state.resumable.filter((s) => s.tid !== msg.tid),\n },\n [{ type: 'AgentRevoke', tid: msg.tid }],\n ]\n }\n case 'ClearError':\n return [{ ...state, error: null }, []]\n case 'SessionsLoaded':\n return [{ ...state, sessions: msg.sessions }, []]\n case 'RefreshSessions':\n return [state, [{ type: 'AgentSessionsList' }]]\n case 'CopyConnectSnippet': {\n // No-op when there's no pending token — the button's\n // `disabled` accessor already gates the click, but we accept\n // the message for runtime safety.\n if (!state.pendingToken) return [state, []]\n return [state, [{ type: 'AgentClipboardWrite', text: state.pendingToken.connectSnippet }]]\n }\n }\n}\n\n// ── Connect helper ────────────────────────────────────────────────────────────\n\nexport type AgentConnectConnectOptions = {\n id?: string // optional DOM id prefix\n}\n\n/**\n * Static prop bag with reactive accessors. Mirrors the @llui/components\n * pattern (e.g. `dialog.connect`): callers spread bag keys directly\n * into element helpers, and function-valued props re-evaluate per\n * binding-mask hit. The previous shape — `(state) => bag` — required\n * callers to wrap every prop access in their own arrow, which the\n * documented usage didn't do (and silently produced `undefined` props\n * when spread).\n */\nexport type ConnectBag<S> = {\n root: { 'data-scope': 'agent-connect'; 'data-state': (s: S) => AgentConnectStatus }\n mintTrigger: { onClick: () => void; disabled: (s: S) => boolean }\n pendingTokenBox: { 'data-part': 'pending-token'; 'data-visible': (s: S) => boolean }\n copyConnectSnippetButton: { onClick: () => void; disabled: (s: S) => boolean }\n sessionsList: { 'data-part': 'sessions-list' }\n sessionItem: (tid: string) => { 'data-part': 'session-item'; 'data-tid': string }\n revokeButton: (tid: string) => { onClick: () => void }\n resumeBanner: { 'data-part': 'resume-banner'; 'data-visible': (s: S) => boolean }\n resumeItem: (tid: string) => { 'data-part': 'resume-item'; 'data-tid': string }\n resumeButton: (tid: string) => { onClick: () => void }\n dismissButton: (tid: string) => { onClick: () => void }\n error: {\n 'data-part': 'error'\n 'data-visible': (s: S) => boolean\n onClick: () => void\n }\n}\n\n/**\n * Builds prop bags for the view. Static-bag-with-reactive-accessors\n * shape (matches the @llui/components convention); spread directly\n * into element helpers.\n */\nexport function connect<S>(\n get: (s: S) => AgentConnectState,\n send: Send<AgentConnectMsg>,\n _opts: AgentConnectConnectOptions = {},\n): ConnectBag<S> {\n return {\n root: {\n 'data-scope': 'agent-connect',\n 'data-state': (s) => get(s).status,\n },\n mintTrigger: {\n onClick: tagSend(send, ['Mint'], () => send({ type: 'Mint' })),\n disabled: (s) => {\n const cs = get(s)\n return cs.status === 'minting' || cs.status === 'pending-claude' || cs.status === 'active'\n },\n },\n pendingTokenBox: {\n 'data-part': 'pending-token',\n 'data-visible': (s) => get(s).pendingToken !== null,\n },\n copyConnectSnippetButton: {\n // The handler reads state at click time via the Msg/effect path:\n // CopyConnectSnippet → update() reads pendingToken.connectSnippet\n // → effect AgentClipboardWrite writes to navigator.clipboard.\n // Routing through update() keeps state reads out of event\n // handlers, which is what makes the static-bag-with-reactive-\n // accessors shape work cleanly.\n onClick: tagSend(send, ['CopyConnectSnippet'], () => send({ type: 'CopyConnectSnippet' })),\n disabled: (s) => get(s).pendingToken === null,\n },\n sessionsList: { 'data-part': 'sessions-list' },\n sessionItem: (tid) => ({ 'data-part': 'session-item', 'data-tid': tid }),\n revokeButton: (tid) => ({\n onClick: tagSend(send, ['Revoke'], () => send({ type: 'Revoke', tid })),\n }),\n resumeBanner: {\n 'data-part': 'resume-banner',\n 'data-visible': (s) => get(s).resumable.length > 0,\n },\n resumeItem: (tid) => ({ 'data-part': 'resume-item', 'data-tid': tid }),\n resumeButton: (tid) => ({\n onClick: tagSend(send, ['Resume'], () => send({ type: 'Resume', tid })),\n }),\n dismissButton: (tid) => ({\n // For dismiss, we currently just remove the resumable record\n // locally. A \"dismiss forever\" flag could land in a follow-up;\n // for v1, dismiss is a client-side-only state prune by reusing\n // the Revoke Msg path with intent-split; for now emit Revoke\n // which both revokes server-side AND removes locally.\n onClick: tagSend(send, ['Revoke'], () => send({ type: 'Revoke', tid })),\n }),\n error: {\n 'data-part': 'error',\n 'data-visible': (s) => get(s).error !== null,\n onClick: tagSend(send, ['ClearError'], () => send({ type: 'ClearError' })),\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agentConnect.js","sourceRoot":"","sources":["../../src/client/agentConnect.ts"],"names":[],"mappings":"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","sourcesContent":["import { tagSend, type Send } from '@llui/dom'\nimport type { AgentSession, AgentToken } from '../protocol.js'\nimport type { AgentEffect } from './effects.js'\n\nexport type AgentConnectStatus =\n | 'idle'\n | 'minting'\n | 'pending-claude'\n | 'active'\n | 'reconnecting'\n | 'failed'\n | 'error'\n\nexport type AgentConnectPendingToken = {\n token: AgentToken\n tid: string\n lapUrl: string\n /**\n * Natural-language connect instruction the user copies into Claude.\n * Includes URL, token, and the explicit `connect_session` tool\n * call. Works in any Claude client (Desktop, CC CLI, etc.) — the\n * Desktop-specific `/llui-connect` slash command is sugar over the\n * same tool call.\n */\n connectSnippet: string\n expiresAt: number\n /**\n * Cached so the auto-reconnect path can re-open the WS without\n * re-minting. The MintSucceeded → AgentOpenWS path stores it; the\n * RestoreSession path also fills it in. Cleared by `Disconnect`.\n */\n wsUrl: string\n}\n\nexport type AgentConnectState = {\n status: AgentConnectStatus\n pendingToken: AgentConnectPendingToken | null\n sessions: AgentSession[]\n resumable: AgentSession[]\n error: { code: string; detail: string } | null\n /**\n * Reconnect attempt counter. Incremented on each WS-close that\n * triggers an auto-reconnect; reset on `WsOpened` and on user\n * actions (`Disconnect`, fresh `Mint`). Drives the backoff schedule\n * (1s, 2s, 4s, 8s, 16s, 30s, 30s, …) and surfaces to UI as\n * \"reconnecting (attempt 3 / next in 4s)\".\n */\n reconnectAttempt: number\n /**\n * Total cumulative ms spent in `reconnecting` for the current\n * outage. Compared against `reconnectGiveUpMs` (effect-side option,\n * default 5 min) to decide when to surface `failed` to the user.\n * Reset whenever a WS opens successfully.\n */\n reconnectElapsedMs: number\n}\n\nexport type AgentConnectMsg =\n /** @intent(\"Mint a new agent token and open the pairing WebSocket\") */\n | { type: 'Mint' }\n /**\n * @humanOnly — internal: dispatched by the AgentMintRequest effect\n * handler when the mint endpoint replies success. Carries the token\n * and connection URLs into state.\n */\n | {\n type: 'MintSucceeded'\n token: AgentToken\n tid: string\n lapUrl: string\n wsUrl: string\n expiresAt: number\n }\n /** @humanOnly — internal: dispatched by the AgentMintRequest handler on failure. */\n | { type: 'MintFailed'; error: { code: string; detail: string } }\n /** @humanOnly — internal: WS adapter signalled the pairing socket is open. */\n | { type: 'WsOpened' }\n /** @humanOnly — internal: WS adapter signalled the pairing socket is closed. */\n | { type: 'WsClosed' }\n /** @humanOnly — internal: Claude bound the session via /agent/claim. */\n | { type: 'ActivatedByClaude' }\n /** @intent(\"Check which previously-issued agent sessions can be resumed\") */\n | { type: 'ResumeList'; tids: string[] }\n /** @humanOnly — internal: AgentResumeCheck effect handler returned the list. */\n | { type: 'ResumeListLoaded'; sessions: AgentSession[] }\n /** @intent(\"Resume an existing agent session by tid\") */\n | { type: 'Resume'; tid: string }\n /** @intent(\"Revoke an agent session by tid\") */\n | { type: 'Revoke'; tid: string }\n /** @intent(\"Dismiss the current agent connect error\") */\n | { type: 'ClearError' }\n /** @humanOnly — internal: AgentSessionsList effect handler returned the list. */\n | { type: 'SessionsLoaded'; sessions: AgentSession[] }\n /** @intent(\"Refresh the list of active agent sessions\") */\n | { type: 'RefreshSessions' }\n /**\n * @intent(\"Copy the agent connect snippet to the clipboard\")\n * Resolves the pendingToken's snippet in update() (state-reading is\n * what update() is for) and dispatches a clipboard-write effect.\n */\n | { type: 'CopyConnectSnippet' }\n /**\n * @humanOnly — internal: app boot dispatches this with credentials\n * read from sessionStorage to skip the mint round-trip after page\n * refresh. The agent's token (still alive on the server) keeps\n * working since we don't go through the rotate-on-resume path. The\n * reducer is idempotent against an in-flight Mint — only fires from\n * `idle`.\n */\n | {\n type: 'RestoreSession'\n token: AgentToken\n tid: string\n lapUrl: string\n wsUrl: string\n expiresAt: number\n }\n /**\n * @intent(\"Disconnect the active agent session and clear all\n * persisted credentials. Stops any in-flight reconnect attempt;\n * subsequent WS closures stay in `idle` instead of triggering\n * auto-reconnect. Use when the user explicitly clicks Disconnect\n * in the panel — for transient drops, do nothing and let the\n * reconnect loop run.\")\n */\n | { type: 'Disconnect' }\n /**\n * @humanOnly — internal: scheduler effect dispatched this when the\n * backoff timer fired. The reducer increments the attempt counter,\n * adds the just-elapsed delay to `reconnectElapsedMs`, and emits\n * `AgentOpenWS` with the cached pendingToken/wsUrl so the WS can\n * reattach without minting.\n */\n | { type: 'ReconnectAttempt'; elapsedMs: number }\n /**\n * @humanOnly — internal: scheduler effect dispatched this when the\n * give-up ceiling was reached without a successful WS open.\n * Reducer flips status to `failed` so the UI can surface a clear\n * error and offer a manual reconnect.\n */\n | { type: 'ReconnectGaveUp' }\n\n/**\n * Options threaded through `init()` and `update()`. `mintUrl` is\n * optional — when omitted the agent effect handler derives it from\n * `EffectHandlerHost.agentBasePath` (default `/agent` → `/agent/mint`).\n * Set explicitly only when the mint endpoint lives outside the\n * configured base path.\n */\nexport type AgentConnectInitOpts = { mintUrl?: string }\n\n/**\n * Backoff schedule for the auto-reconnect loop. Doubles starting at\n * 1s, caps at 30s. Translates `state.reconnectAttempt` into the next\n * delay; the effect handler schedules a `setTimeout` for that long\n * and dispatches `ReconnectAttempt` when it fires.\n *\n * Lives in the reducer so tests can pin the timings without poking\n * effect-handler internals; the constants are not exported because\n * tweaking them changes UX more than tweaks to the give-up ceiling.\n */\nconst RECONNECT_BASE_MS = 1000\nconst RECONNECT_CAP_MS = 30_000\nfunction reconnectDelayMs(attempt: number): number {\n const factor = Math.min(Math.pow(2, attempt), RECONNECT_CAP_MS / RECONNECT_BASE_MS)\n return Math.min(RECONNECT_BASE_MS * factor, RECONNECT_CAP_MS)\n}\n\n/**\n * Total cumulative wait, across all reconnect attempts, before the\n * loop gives up and transitions to `'failed'`. 5 minutes is long\n * enough to weather a brief server outage but short enough that a\n * permanently-down endpoint surfaces clearly to the user instead of\n * silently spinning.\n */\nconst RECONNECT_GIVE_UP_MS = 5 * 60 * 1000\n\n/** Component shape is [State, Effect[]] — consistent with @llui/components. */\nexport function init(_opts: AgentConnectInitOpts): [AgentConnectState, AgentEffect[]] {\n return [\n {\n status: 'idle',\n pendingToken: null,\n sessions: [],\n resumable: [],\n error: null,\n reconnectAttempt: 0,\n reconnectElapsedMs: 0,\n },\n [],\n ]\n}\n\nexport function update(\n state: AgentConnectState,\n msg: AgentConnectMsg,\n opts: AgentConnectInitOpts = {},\n): [AgentConnectState, AgentEffect[]] {\n switch (msg.type) {\n case 'Mint': {\n // mintUrl: undefined means \"let the effect handler derive it\n // from agentBasePath\". Only include the property when explicitly\n // set, so the effect's discriminated shape stays clean.\n const mintEffect: AgentEffect =\n opts.mintUrl !== undefined\n ? { type: 'AgentMintRequest', mintUrl: opts.mintUrl }\n : { type: 'AgentMintRequest' }\n return [{ ...state, status: 'minting' }, [mintEffect]]\n }\n case 'MintSucceeded': {\n // The connect snippet has to work across every MCP surface.\n // Claude Desktop and similar clients expose MCP tools as bare\n // names (`connect_session`), but Claude Code (and other tool-list-\n // namespacing clients) emit them as `mcp__llui__connect_session`\n // and may defer-load them — so an LLM that searches its tool list\n // for a literal `connect_session` won't find it. Naming the LLui\n // MCP server explicitly (with its canonical `llui` install name,\n // matching the install docs) gives the model enough to resolve\n // the right tool on either platform; the parenthetical names the\n // edge case so a deferred-tool client doesn't bail out.\n //\n // Phrased generically (`AI assistant`, `Some MCP clients`) since\n // MCP support is rapidly expanding past Claude — the snippet\n // shouldn't telegraph \"this is Claude-only\" when it works against\n // any compliant client. The literal `mcp__llui__` prefix matches\n // the install command in `site/content/agents.md`; users who\n // renamed the server in their config can substitute their name.\n const pending: AgentConnectPendingToken = {\n token: msg.token,\n tid: msg.tid,\n lapUrl: msg.lapUrl,\n connectSnippet:\n `Connect this AI assistant to the LLui app. Call the LLui MCP server's ` +\n `\\`connect_session\\` tool with url=${JSON.stringify(msg.lapUrl)} and ` +\n `token=${JSON.stringify(msg.token)}. ` +\n `(Some MCP clients namespace tools as ` +\n `\\`mcp__llui__connect_session\\` and load them lazily — search the tool list if \\`connect_session\\` isn't immediately available.)`,\n expiresAt: msg.expiresAt,\n wsUrl: msg.wsUrl,\n }\n return [\n {\n ...state,\n status: 'pending-claude',\n pendingToken: pending,\n error: null,\n reconnectAttempt: 0,\n reconnectElapsedMs: 0,\n },\n [\n { type: 'AgentOpenWS', token: msg.token, wsUrl: msg.wsUrl },\n // Persist alongside opening the WS so the host can store the\n // credentials in sessionStorage; on page refresh, app boot\n // dispatches `RestoreSession` with the same shape and we\n // re-enter the same state without re-minting.\n {\n type: 'AgentSessionPersist',\n token: msg.token,\n tid: msg.tid,\n lapUrl: msg.lapUrl,\n wsUrl: msg.wsUrl,\n expiresAt: msg.expiresAt,\n },\n ],\n ]\n }\n case 'RestoreSession': {\n // Idempotent guard: only fires from idle. A racing Mint click\n // would already have moved us to `minting` — restoring on top\n // would clobber the in-flight pending state with stale\n // credentials read from sessionStorage. Easier to no-op here\n // than to coordinate the race in the host.\n if (state.status !== 'idle') return [state, []]\n // Regenerate the connectSnippet so the user can re-paste if\n // their AI lost the original tool call (same shape as\n // MintSucceeded — the framework owns this string and updates\n // to it ride along the agent package version).\n const restored: AgentConnectPendingToken = {\n token: msg.token,\n tid: msg.tid,\n lapUrl: msg.lapUrl,\n connectSnippet:\n `Connect this AI assistant to the LLui app. Call the LLui MCP server's ` +\n `\\`connect_session\\` tool with url=${JSON.stringify(msg.lapUrl)} and ` +\n `token=${JSON.stringify(msg.token)}. ` +\n `(Some MCP clients namespace tools as ` +\n `\\`mcp__llui__connect_session\\` and load them lazily — search the tool list if \\`connect_session\\` isn't immediately available.)`,\n expiresAt: msg.expiresAt,\n wsUrl: msg.wsUrl,\n }\n return [\n {\n ...state,\n status: 'pending-claude',\n pendingToken: restored,\n error: null,\n reconnectAttempt: 0,\n reconnectElapsedMs: 0,\n },\n [{ type: 'AgentOpenWS', token: msg.token, wsUrl: msg.wsUrl }],\n ]\n }\n case 'MintFailed':\n return [{ ...state, status: 'error', error: msg.error }, []]\n case 'WsOpened': {\n // WS is open but Claude hasn't bound yet; stay at pending-claude.\n // If we were `reconnecting`, this is a successful reattach —\n // back to pending-claude and reset the attempt counters.\n if (state.status === 'reconnecting') {\n return [\n { ...state, status: 'pending-claude', reconnectAttempt: 0, reconnectElapsedMs: 0 },\n [],\n ]\n }\n return [state, []]\n }\n case 'WsClosed': {\n // Three cases:\n // 1. We had no pendingToken (already idle / pre-mint) → no-op.\n // 2. Status is `idle` or `failed` (Disconnect already cleared,\n // or we previously gave up) → no-op so a delayed close\n // event after Disconnect doesn't accidentally restart the\n // loop.\n // 3. We're connected/connecting and the close was unsolicited\n // → schedule a reconnect with backoff.\n if (state.pendingToken === null) return [{ ...state, status: 'idle' }, []]\n if (state.status === 'idle' || state.status === 'failed') return [state, []]\n const delayMs = reconnectDelayMs(state.reconnectAttempt)\n return [\n { ...state, status: 'reconnecting', error: null },\n [{ type: 'AgentReconnectSchedule', delayMs }],\n ]\n }\n case 'ReconnectAttempt': {\n // Backoff timer fired. If the user disconnected in the gap, we\n // moved to idle and ignore. Otherwise, increment attempt + add\n // the elapsed delay to the cumulative window. Past the give-up\n // ceiling, transition to `failed` so the UI can offer a manual\n // reconnect; otherwise re-open the WS with the cached\n // credentials (no mint, same token — the server's grace window\n // is what makes this transparent to the agent).\n if (state.status !== 'reconnecting' || state.pendingToken === null) {\n return [state, []]\n }\n const newElapsed = state.reconnectElapsedMs + msg.elapsedMs\n if (newElapsed >= RECONNECT_GIVE_UP_MS) {\n return [{ ...state, status: 'failed', reconnectElapsedMs: newElapsed }, []]\n }\n return [\n {\n ...state,\n reconnectAttempt: state.reconnectAttempt + 1,\n reconnectElapsedMs: newElapsed,\n },\n [\n {\n type: 'AgentOpenWS',\n token: state.pendingToken.token,\n wsUrl: state.pendingToken.wsUrl,\n },\n ],\n ]\n }\n case 'ReconnectGaveUp':\n return [{ ...state, status: 'failed' }, []]\n case 'Disconnect': {\n // User-initiated. Revoke the active tid (server kills the\n // pairing), wipe the persisted credentials so a refresh can't\n // restore them, and zero the reconnect counters so any in-\n // flight backoff timer that fires post-disconnect becomes a\n // no-op (the status guard in `ReconnectAttempt` keeps it from\n // re-opening the WS).\n const tid = state.pendingToken?.tid\n const effects: AgentEffect[] = []\n if (tid !== undefined) effects.push({ type: 'AgentRevoke', tid })\n effects.push({ type: 'AgentSessionClear' })\n effects.push({ type: 'AgentCloseWS' })\n return [\n {\n ...state,\n status: 'idle',\n pendingToken: null,\n error: null,\n reconnectAttempt: 0,\n reconnectElapsedMs: 0,\n },\n effects,\n ]\n }\n case 'ActivatedByClaude':\n return [{ ...state, status: 'active' }, []]\n case 'ResumeList':\n return [state, [{ type: 'AgentResumeCheck', tids: msg.tids }]]\n case 'ResumeListLoaded':\n return [{ ...state, resumable: msg.sessions }, []]\n case 'Resume':\n return [state, [{ type: 'AgentResumeClaim', tid: msg.tid }]]\n case 'Revoke': {\n // Optimistically remove from sessions + resumable. If the\n // revoked tid matches the currently-pending session, also fire\n // AgentSessionClear so the host wipes its persisted credentials\n // — otherwise a refresh would try to RestoreSession with a\n // server-side-revoked token and end up at an auth-failed WS.\n const isActiveTid = state.pendingToken !== null && state.pendingToken.tid === msg.tid\n const effects: AgentEffect[] = [{ type: 'AgentRevoke', tid: msg.tid }]\n if (isActiveTid) effects.push({ type: 'AgentSessionClear' })\n return [\n {\n ...state,\n sessions: state.sessions.filter((s) => s.tid !== msg.tid),\n resumable: state.resumable.filter((s) => s.tid !== msg.tid),\n },\n effects,\n ]\n }\n case 'ClearError':\n return [{ ...state, error: null }, []]\n case 'SessionsLoaded':\n return [{ ...state, sessions: msg.sessions }, []]\n case 'RefreshSessions':\n return [state, [{ type: 'AgentSessionsList' }]]\n case 'CopyConnectSnippet': {\n // No-op when there's no pending token — the button's\n // `disabled` accessor already gates the click, but we accept\n // the message for runtime safety.\n if (!state.pendingToken) return [state, []]\n return [state, [{ type: 'AgentClipboardWrite', text: state.pendingToken.connectSnippet }]]\n }\n }\n}\n\n// ── Connect helper ────────────────────────────────────────────────────────────\n\nexport type AgentConnectConnectOptions = {\n id?: string // optional DOM id prefix\n}\n\n/**\n * Static prop bag with reactive accessors. Mirrors the @llui/components\n * pattern (e.g. `dialog.connect`): callers spread bag keys directly\n * into element helpers, and function-valued props re-evaluate per\n * binding-mask hit. The previous shape — `(state) => bag` — required\n * callers to wrap every prop access in their own arrow, which the\n * documented usage didn't do (and silently produced `undefined` props\n * when spread).\n */\nexport type ConnectBag<S> = {\n root: { 'data-scope': 'agent-connect'; 'data-state': (s: S) => AgentConnectStatus }\n mintTrigger: { onClick: () => void; disabled: (s: S) => boolean }\n pendingTokenBox: { 'data-part': 'pending-token'; 'data-visible': (s: S) => boolean }\n copyConnectSnippetButton: { onClick: () => void; disabled: (s: S) => boolean }\n sessionsList: { 'data-part': 'sessions-list' }\n sessionItem: (tid: string) => { 'data-part': 'session-item'; 'data-tid': string }\n revokeButton: (tid: string) => { onClick: () => void }\n resumeBanner: { 'data-part': 'resume-banner'; 'data-visible': (s: S) => boolean }\n resumeItem: (tid: string) => { 'data-part': 'resume-item'; 'data-tid': string }\n resumeButton: (tid: string) => { onClick: () => void }\n dismissButton: (tid: string) => { onClick: () => void }\n error: {\n 'data-part': 'error'\n 'data-visible': (s: S) => boolean\n onClick: () => void\n }\n}\n\n/**\n * Builds prop bags for the view. Static-bag-with-reactive-accessors\n * shape (matches the @llui/components convention); spread directly\n * into element helpers.\n */\nexport function connect<S>(\n get: (s: S) => AgentConnectState,\n send: Send<AgentConnectMsg>,\n _opts: AgentConnectConnectOptions = {},\n): ConnectBag<S> {\n return {\n root: {\n 'data-scope': 'agent-connect',\n 'data-state': (s) => get(s).status,\n },\n mintTrigger: {\n onClick: tagSend(send, ['Mint'], () => send({ type: 'Mint' })),\n disabled: (s) => {\n const cs = get(s)\n return cs.status === 'minting' || cs.status === 'pending-claude' || cs.status === 'active'\n },\n },\n pendingTokenBox: {\n 'data-part': 'pending-token',\n 'data-visible': (s) => get(s).pendingToken !== null,\n },\n copyConnectSnippetButton: {\n // The handler reads state at click time via the Msg/effect path:\n // CopyConnectSnippet → update() reads pendingToken.connectSnippet\n // → effect AgentClipboardWrite writes to navigator.clipboard.\n // Routing through update() keeps state reads out of event\n // handlers, which is what makes the static-bag-with-reactive-\n // accessors shape work cleanly.\n onClick: tagSend(send, ['CopyConnectSnippet'], () => send({ type: 'CopyConnectSnippet' })),\n disabled: (s) => get(s).pendingToken === null,\n },\n sessionsList: { 'data-part': 'sessions-list' },\n sessionItem: (tid) => ({ 'data-part': 'session-item', 'data-tid': tid }),\n revokeButton: (tid) => ({\n onClick: tagSend(send, ['Revoke'], () => send({ type: 'Revoke', tid })),\n }),\n resumeBanner: {\n 'data-part': 'resume-banner',\n 'data-visible': (s) => get(s).resumable.length > 0,\n },\n resumeItem: (tid) => ({ 'data-part': 'resume-item', 'data-tid': tid }),\n resumeButton: (tid) => ({\n onClick: tagSend(send, ['Resume'], () => send({ type: 'Resume', tid })),\n }),\n dismissButton: (tid) => ({\n // For dismiss, we currently just remove the resumable record\n // locally. A \"dismiss forever\" flag could land in a follow-up;\n // for v1, dismiss is a client-side-only state prune by reusing\n // the Revoke Msg path with intent-split; for now emit Revoke\n // which both revokes server-side AND removes locally.\n onClick: tagSend(send, ['Revoke'], () => send({ type: 'Revoke', tid })),\n }),\n error: {\n 'data-part': 'error',\n 'data-visible': (s) => get(s).error !== null,\n onClick: tagSend(send, ['ClearError'], () => send({ type: 'ClearError' })),\n },\n }\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentEffect } from './effects.js';
|
|
2
|
+
import type { AgentSessionStorage } from './factory.js';
|
|
2
3
|
export type EffectHandlerHost = {
|
|
3
4
|
send(msg: unknown): void;
|
|
4
5
|
/** Wraps an agentConnect msg into an app-Msg. */
|
|
@@ -10,6 +11,16 @@ export type EffectHandlerHost = {
|
|
|
10
11
|
/** Called before opening WS / on WS lifecycle events. */
|
|
11
12
|
openWs(token: string, wsUrl: string): void;
|
|
12
13
|
closeWs(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Optional storage adapter. When set, `AgentSessionPersist` writes
|
|
16
|
+
* to it and `AgentSessionClear` clears it; the host doesn't need
|
|
17
|
+
* to handle these effects itself. When `null` or `undefined`, the
|
|
18
|
+
* effects no-op here and host code (if any) handles them in the
|
|
19
|
+
* outer effect router. The factory passes
|
|
20
|
+
* `defaultSessionStorage()` by default, so the framework is
|
|
21
|
+
* refresh-survival-ready out of the box.
|
|
22
|
+
*/
|
|
23
|
+
sessionStorage?: AgentSessionStorage | null;
|
|
13
24
|
/**
|
|
14
25
|
* Base path for agent HTTP endpoints. Default: `'/agent'` (matches
|
|
15
26
|
* the canonical paths in `@llui/vite-plugin`'s dev middleware and
|
|
@@ -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;
|
|
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,CA0CjE"}
|
|
@@ -28,9 +28,38 @@ export function createEffectHandler(host) {
|
|
|
28
28
|
return handleForwardMsg(host, effect);
|
|
29
29
|
case 'AgentClipboardWrite':
|
|
30
30
|
return handleClipboardWrite(effect);
|
|
31
|
+
case 'AgentSessionPersist':
|
|
32
|
+
// Framework-owned when a storage adapter is configured;
|
|
33
|
+
// otherwise no-op and let the host's outer effect router
|
|
34
|
+
// handle it (the legacy contract). See factory.ts'
|
|
35
|
+
// `sessionStorage` option.
|
|
36
|
+
if (host.sessionStorage) {
|
|
37
|
+
host.sessionStorage.write({
|
|
38
|
+
token: effect.token,
|
|
39
|
+
tid: effect.tid,
|
|
40
|
+
lapUrl: effect.lapUrl,
|
|
41
|
+
wsUrl: effect.wsUrl,
|
|
42
|
+
expiresAt: effect.expiresAt,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
case 'AgentSessionClear':
|
|
47
|
+
if (host.sessionStorage)
|
|
48
|
+
host.sessionStorage.clear();
|
|
49
|
+
return;
|
|
50
|
+
case 'AgentReconnectSchedule':
|
|
51
|
+
return handleReconnectSchedule(host, effect);
|
|
31
52
|
}
|
|
32
53
|
};
|
|
33
54
|
}
|
|
55
|
+
async function handleReconnectSchedule(host, effect) {
|
|
56
|
+
// Single-shot timer. The reducer owns cancellation semantics via
|
|
57
|
+
// the status guard in `ReconnectAttempt` — if the user dispatches
|
|
58
|
+
// `Disconnect` while we're sleeping, the dispatched message hits
|
|
59
|
+
// an `idle` reducer and is a no-op. No cancel handle needed.
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, effect.delayMs));
|
|
61
|
+
host.send(host.wrapAgentConnect({ type: 'ReconnectAttempt', elapsedMs: effect.delayMs }));
|
|
62
|
+
}
|
|
34
63
|
// ── HTTP-bound handlers ─────────────────────────────────────────────
|
|
35
64
|
async function handleMintRequest(host, effect, doFetch) {
|
|
36
65
|
// Derive a default `mintUrl` from `agentBasePath` so consumers can
|