@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.
Files changed (45) hide show
  1. package/dist/client/agentConnect.d.ts +69 -1
  2. package/dist/client/agentConnect.d.ts.map +1 -1
  3. package/dist/client/agentConnect.js +201 -16
  4. package/dist/client/agentConnect.js.map +1 -1
  5. package/dist/client/effect-handler.d.ts +11 -0
  6. package/dist/client/effect-handler.d.ts.map +1 -1
  7. package/dist/client/effect-handler.js +29 -0
  8. package/dist/client/effect-handler.js.map +1 -1
  9. package/dist/client/effects.d.ts +39 -0
  10. package/dist/client/effects.d.ts.map +1 -1
  11. package/dist/client/effects.js.map +1 -1
  12. package/dist/client/factory.d.ts +53 -1
  13. package/dist/client/factory.d.ts.map +1 -1
  14. package/dist/client/factory.js +105 -0
  15. package/dist/client/factory.js.map +1 -1
  16. package/dist/server/core.d.ts +19 -0
  17. package/dist/server/core.d.ts.map +1 -1
  18. package/dist/server/core.js +35 -2
  19. package/dist/server/core.js.map +1 -1
  20. package/dist/server/lap/confirm-result.d.ts.map +1 -1
  21. package/dist/server/lap/confirm-result.js +2 -1
  22. package/dist/server/lap/confirm-result.js.map +1 -1
  23. package/dist/server/lap/describe.d.ts.map +1 -1
  24. package/dist/server/lap/describe.js +3 -2
  25. package/dist/server/lap/describe.js.map +1 -1
  26. package/dist/server/lap/forward.d.ts.map +1 -1
  27. package/dist/server/lap/forward.js +9 -6
  28. package/dist/server/lap/forward.js.map +1 -1
  29. package/dist/server/lap/message.d.ts.map +1 -1
  30. package/dist/server/lap/message.js +2 -1
  31. package/dist/server/lap/message.js.map +1 -1
  32. package/dist/server/lap/observe.d.ts.map +1 -1
  33. package/dist/server/lap/observe.js +6 -3
  34. package/dist/server/lap/observe.js.map +1 -1
  35. package/dist/server/lap/paused.d.ts +30 -0
  36. package/dist/server/lap/paused.d.ts.map +1 -0
  37. package/dist/server/lap/paused.js +38 -0
  38. package/dist/server/lap/paused.js.map +1 -0
  39. package/dist/server/lap/wait.d.ts.map +1 -1
  40. package/dist/server/lap/wait.js +2 -1
  41. package/dist/server/lap/wait.js.map +1 -1
  42. package/dist/server/token-store.d.ts.map +1 -1
  43. package/dist/server/token-store.js +7 -0
  44. package/dist/server/token-store.js.map +1 -1
  45. 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":"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"}
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 Claude surface.
28
- // Claude Desktop exposes MCP tools as bare names (`connect_session`),
29
- // but Claude Code namespaces them (`mcp__<server>__connect_session`)
30
- // and may defer-load them so an LLM that searches its tool list for
31
- // a literal `connect_session` won't find it. Naming the LLui MCP
32
- // server explicitly gives the model enough to resolve the right tool
33
- // on either platform.
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 Claude session to the LLui app. Call the LLui MCP server's ` +
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
- `(In Claude Code the tool may be namespaced as ` +
42
- `\`mcp__<server>__connect_session\` and deferredload it via tool search if needed.)`,
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
- { ...state, status: 'pending-claude', pendingToken: pending, error: null },
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
- case 'WsClosed':
56
- return [{ ...state, status: 'idle', pendingToken: null }, []];
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
- [{ type: 'AgentRevoke', tid: msg.tid }],
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;AAQ/C,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;;;;;;;;;;;;;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,CAsBjE"}
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