eve 0.43.0 → 0.44.1

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 (151) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/src/channel/adapter.d.ts +2 -1
  3. package/dist/src/channel/instrumentation.js +1 -1
  4. package/dist/src/chunks/{use-eve-agent-C0SG1Ttk.js → use-eve-agent-BsLJiIbc.js} +224 -62
  5. package/dist/src/chunks/{use-eve-agent-D71oDil7.js → use-eve-agent-DpUEwS23.js} +222 -62
  6. package/dist/src/cli/commands/extension-init.js +1 -1
  7. package/dist/src/cli/commands/init.js +1 -1
  8. package/dist/src/cli/commands/registry-project.d.ts +3 -0
  9. package/dist/src/cli/commands/registry-project.js +2 -1
  10. package/dist/src/cli/commands/registry.d.ts +2 -0
  11. package/dist/src/cli/commands/registry.js +1 -1
  12. package/dist/src/cli/dev/tui/setup-commands.js +1 -1
  13. package/dist/src/cli/dev/tui/traces/trace-view.js +1 -1
  14. package/dist/src/client/eve-agent-store-helpers.d.ts +12 -0
  15. package/dist/src/client/eve-agent-store-helpers.js +2 -0
  16. package/dist/src/client/eve-agent-store.d.ts +8 -6
  17. package/dist/src/client/eve-agent-store.js +1 -2
  18. package/dist/src/client/ndjson.d.ts +3 -1
  19. package/dist/src/client/ndjson.js +3 -3
  20. package/dist/src/client/open-stream.d.ts +3 -0
  21. package/dist/src/client/open-stream.js +1 -1
  22. package/dist/src/compiled/.vendor-stamp.json +1 -1
  23. package/dist/src/compiled/jsonc-parser/index.d.ts +19 -0
  24. package/dist/src/compiler/extension-compatibility.d.ts +2 -2
  25. package/dist/src/compiler/extension-compatibility.js +1 -1
  26. package/dist/src/context/build-dynamic-tools.js +1 -1
  27. package/dist/src/context/dynamic-tool-lifecycle.d.ts +5 -1
  28. package/dist/src/context/dynamic-tool-lifecycle.js +1 -1
  29. package/dist/src/context/keys.d.ts +4 -2
  30. package/dist/src/context/keys.js +1 -1
  31. package/dist/src/execution/cancel-descendant-turns-step.js +1 -1
  32. package/dist/src/execution/dispatch-turn-step.js +1 -1
  33. package/dist/src/execution/eve-workflow-attributes.d.ts +3 -0
  34. package/dist/src/execution/eve-workflow-attributes.js +1 -1
  35. package/dist/src/execution/pending-turn-state.d.ts +9 -0
  36. package/dist/src/execution/pending-turn-state.js +1 -0
  37. package/dist/src/execution/settle-cancelled-turn-step.js +1 -1
  38. package/dist/src/execution/tasks/parent/tool-execution.js +1 -1
  39. package/dist/src/execution/turn-workflow.js +1 -1
  40. package/dist/src/execution/workflow-steps.js +1 -1
  41. package/dist/src/execution/workflow-trace-context.js +1 -1
  42. package/dist/src/harness/channel-delivery-instrumentation.js +1 -1
  43. package/dist/src/harness/instrumentation/content-policy.d.ts +6 -0
  44. package/dist/src/harness/instrumentation/content-policy.js +1 -0
  45. package/dist/src/harness/instrumentation/lifecycle.d.ts +6 -0
  46. package/dist/src/harness/instrumentation/native-events.d.ts +2 -0
  47. package/dist/src/harness/instrumentation/native-events.js +1 -1
  48. package/dist/src/harness/instrumentation/runtime-context.js +1 -1
  49. package/dist/src/harness/prepare-trace-context.d.ts +2 -0
  50. package/dist/src/harness/prepare-trace-context.js +1 -1
  51. package/dist/src/harness/runtime-actions.js +1 -1
  52. package/dist/src/harness/tool-loop.js +2 -2
  53. package/dist/src/internal/application/package.js +1 -1
  54. package/dist/src/internal/workflow-bundle/dynamic-remote-agent-transform.js +2 -2
  55. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.d.ts +4 -4
  56. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.js +3 -4
  57. package/dist/src/internal/workflow-bundle/stable-module-id.d.ts +6 -0
  58. package/dist/src/internal/workflow-bundle/stable-module-id.js +1 -0
  59. package/dist/src/packages/eve-catalog/src/index.js +1 -1
  60. package/dist/src/public/channels/chat-sdk/audience.d.ts +2 -0
  61. package/dist/src/public/channels/chat-sdk/audience.js +1 -0
  62. package/dist/src/public/channels/chat-sdk/chatSdkChannel.d.ts +2 -0
  63. package/dist/src/public/channels/chat-sdk/chatSdkChannel.js +2 -2
  64. package/dist/src/public/channels/discord/audience.d.ts +5 -0
  65. package/dist/src/public/channels/discord/audience.js +1 -0
  66. package/dist/src/public/channels/discord/discordChannel.d.ts +2 -0
  67. package/dist/src/public/channels/discord/discordChannel.js +1 -1
  68. package/dist/src/public/channels/discord/inbound.d.ts +1 -0
  69. package/dist/src/public/channels/discord/inbound.js +1 -1
  70. package/dist/src/public/channels/discord/index.d.ts +1 -0
  71. package/dist/src/public/channels/index.d.ts +3 -2
  72. package/dist/src/public/channels/slack/slackChannel.d.ts +4 -0
  73. package/dist/src/public/channels/slack/slackChannel.js +1 -1
  74. package/dist/src/public/channels/teams/index.d.ts +1 -0
  75. package/dist/src/public/channels/teams/teamsChannel.js +1 -1
  76. package/dist/src/public/channels/telegram/audience.d.ts +3 -0
  77. package/dist/src/public/channels/telegram/audience.js +1 -0
  78. package/dist/src/public/channels/telegram/index.d.ts +1 -0
  79. package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
  80. package/dist/src/public/channels/twilio/twilioChannel.d.ts +2 -0
  81. package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
  82. package/dist/src/public/definitions/channel.d.ts +1 -0
  83. package/dist/src/public/instrumentation/otel.d.ts +6 -10
  84. package/dist/src/public/instrumentation/otel.js +1 -1
  85. package/dist/src/public/tools/approval/approval-helpers.js +1 -1
  86. package/dist/src/react/use-eve-agent.d.ts +10 -1
  87. package/dist/src/react/use-eve-agent.js +1 -1
  88. package/dist/src/runtime/attributes/emit.d.ts +1 -1
  89. package/dist/src/runtime/attributes/normalize.d.ts +1 -1
  90. package/dist/src/runtime/attributes/normalize.js +1 -1
  91. package/dist/src/runtime/framework-tools/connection-search-dynamic.js +2 -2
  92. package/dist/src/runtime/framework-tools/subagent/local.js +1 -1
  93. package/dist/src/runtime/sessions/runtime-context-keys.js +1 -1
  94. package/dist/src/setup/integrations/linq/connect.js +1 -1
  95. package/dist/src/setup/primitives/pm/npm.js +1 -1
  96. package/dist/src/setup/primitives/pm/pnpm.js +5 -9
  97. package/dist/src/setup/primitives/pm/types.d.ts +0 -2
  98. package/dist/src/setup/scaffold/create/project.js +1 -1
  99. package/dist/src/setup/scaffold/create/web-template.d.ts +10 -4
  100. package/dist/src/setup/scaffold/create/web-template.js +669 -74
  101. package/dist/src/shared/channel-audience.d.ts +7 -0
  102. package/dist/src/shared/channel-audience.js +1 -0
  103. package/dist/src/shared/channel-definition.d.ts +2 -1
  104. package/dist/src/shared/durable-dynamic-tool-callbacks.d.ts +43 -19
  105. package/dist/src/shared/durable-dynamic-tool-callbacks.js +1 -1
  106. package/dist/src/svelte/index.js +1 -1
  107. package/dist/src/svelte/use-eve-agent.d.ts +4 -0
  108. package/dist/src/svelte/use-eve-agent.js +1 -1
  109. package/dist/src/tasks/delivery-context.d.ts +9 -0
  110. package/dist/src/tasks/delivery-context.js +5 -2
  111. package/dist/src/tracing/agent-action-instrumentation.js +1 -1
  112. package/dist/src/tracing/agent-approval-instrumentation.js +1 -1
  113. package/dist/src/tracing/agent-channel-delivery-instrumentation.js +1 -1
  114. package/dist/src/tracing/agent-otel-provider.d.ts +2 -0
  115. package/dist/src/tracing/agent-otel-provider.js +2 -2
  116. package/dist/src/tracing/agent-otel-session-context.d.ts +2 -0
  117. package/dist/src/tracing/agent-otel-session-context.js +1 -1
  118. package/dist/src/tracing/agent-trace-context-store.js +1 -1
  119. package/dist/src/tracing/agent-trace-state.d.ts +3 -0
  120. package/dist/src/tracing/channel-audience-context.d.ts +4 -0
  121. package/dist/src/tracing/channel-audience-context.js +1 -0
  122. package/dist/src/tracing/content-attributes.js +1 -1
  123. package/dist/src/tracing/content-span-processor.d.ts +7 -7
  124. package/dist/src/tracing/content-span-processor.js +1 -1
  125. package/dist/src/tracing/install-instrumentation-runtime.js +1 -1
  126. package/dist/src/tracing/local-instrumentation-runtime.d.ts +3 -0
  127. package/dist/src/tracing/local-instrumentation-runtime.js +1 -1
  128. package/dist/src/tracing/local-traces.d.ts +1 -10
  129. package/dist/src/tracing/local-traces.js +1 -1
  130. package/dist/src/tracing/otel-declaration.d.ts +34 -27
  131. package/dist/src/tracing/otel-declaration.js +1 -1
  132. package/dist/src/tracing/sampled-trace.d.ts +2 -0
  133. package/dist/src/tracing/sampled-trace.js +1 -0
  134. package/dist/src/tracing/span-export-policy.d.ts +41 -0
  135. package/dist/src/tracing/span-export-policy.js +1 -0
  136. package/dist/src/tracing/suppress-tracing.d.ts +2 -0
  137. package/dist/src/tracing/suppress-tracing.js +1 -0
  138. package/dist/src/vue/index.js +1 -1
  139. package/dist/src/vue/use-eve-agent.d.ts +4 -0
  140. package/dist/src/vue/use-eve-agent.js +1 -1
  141. package/docs/channels/custom.mdx +2 -0
  142. package/docs/concepts/sessions-runs-and-streaming.md +1 -1
  143. package/docs/guides/dynamic-capabilities.md +10 -2
  144. package/docs/guides/frontend/nextjs.mdx +23 -0
  145. package/docs/guides/frontend/overview.mdx +24 -9
  146. package/docs/guides/frontend/use-eve-agent-svelte.mdx +9 -9
  147. package/docs/guides/frontend/use-eve-agent-vue.mdx +9 -9
  148. package/docs/guides/instrumentation.md +2 -0
  149. package/docs/tools/overview.mdx +5 -4
  150. package/docs/tutorial/ship-it.mdx +1 -1
  151. package/package.json +1 -1
@@ -17,7 +17,7 @@ export default eveChannel({
17
17
 
18
18
  import type { UserContent } from "ai";
19
19
  import { useEveAgent } from "eve/react";
20
- import { AlertCircleIcon, BrainIcon } from "lucide-react";
20
+ import { AlertCircleIcon, BrainIcon, PlusIcon, SquareIcon } from "lucide-react";
21
21
  import { useState } from "react";
22
22
  import {
23
23
  Conversation,
@@ -27,20 +27,50 @@ import {
27
27
  import { Message, MessageContent } from "@/components/ai-elements/message";
28
28
  import {
29
29
  PromptInput,
30
+ PromptInputButton,
30
31
  type PromptInputMessage,
31
32
  PromptInputSubmit,
32
33
  PromptInputTextarea,
33
34
  } from "@/components/ai-elements/prompt-input";
34
35
  import { Shimmer } from "@/components/ai-elements/shimmer";
36
+ import { Button } from "@/components/ui/button";
35
37
  import { cn } from "@/lib/utils";
36
38
  import { AgentMessage } from "./agent-message";
37
39
 
38
40
  const AGENT_NAME = "__EVE_INIT_APP_NAME__";
39
41
 
40
- export function AgentChat() {
42
+ export function AgentChat({
43
+ sessionId,
44
+ sessionless = false,
45
+ }: {
46
+ readonly sessionId?: string;
47
+ readonly sessionless?: boolean;
48
+ }) {
41
49
  const [cancellationError, setCancellationError] = useState<string>();
42
- const agent = useEveAgent();
50
+ const agent = useEveAgent({
51
+ initialSession:
52
+ sessionId === undefined
53
+ ? undefined
54
+ : {
55
+ sessionId,
56
+ streamIndex: 0,
57
+ },
58
+ resume: sessionId !== undefined,
59
+ onSessionChange(session) {
60
+ if (sessionId === undefined && session !== undefined) {
61
+ // Next patches window.history to navigate, which would detach the active stream.
62
+ History.prototype.replaceState.call(
63
+ window.history,
64
+ window.history.state,
65
+ "",
66
+ \`/s/\${encodeURIComponent(session.sessionId)}\`,
67
+ );
68
+ }
69
+ },
70
+ });
71
+
43
72
  const isBusy = agent.status === "submitted" || agent.status === "streaming";
73
+ const isRestoring = sessionId !== undefined && agent.events.length === 0 && isBusy;
44
74
  const isEmpty = agent.data.messages.length === 0;
45
75
  const lastMessage = agent.data.messages.at(-1);
46
76
  const isPendingAssistantShell =
@@ -51,6 +81,9 @@ export function AgentChat() {
51
81
  (agent.status === "submitted" || lastMessage?.role !== "assistant" || isPendingAssistantShell);
52
82
  const turnFailure = isBusy ? undefined : getLatestTurnFailure(agent.events);
53
83
  const errorMessage = cancellationError ?? agent.error?.message ?? turnFailure;
84
+ const hasConversationContent = sessionless || !isEmpty || errorMessage !== undefined;
85
+ const showConversationLayout = isRestoring || hasConversationContent;
86
+ const activeSessionId = sessionId ?? agent.session?.sessionId;
54
87
 
55
88
  const requestCancellation = () => {
56
89
  setCancellationError(undefined);
@@ -61,12 +94,13 @@ export function AgentChat() {
61
94
 
62
95
  const handleSubmit = async (message: PromptInputMessage) => {
63
96
  const text = message.text.trim();
64
- if ((text.length === 0 && message.files.length === 0) || isBusy) return;
97
+ if ((text.length === 0 && message.files.length === 0) || isRestoring) return;
65
98
 
66
99
  setCancellationError(undefined);
100
+ const options = isBusy ? { turnPolicy: "steer" as const } : undefined;
67
101
 
68
102
  if (message.files.length === 0) {
69
- await agent.send(text);
103
+ await agent.send(text, options);
70
104
  return;
71
105
  }
72
106
 
@@ -83,42 +117,44 @@ export function AgentChat() {
83
117
  });
84
118
  }
85
119
 
86
- await agent.send(parts);
120
+ await agent.send(parts, options);
87
121
  };
88
122
 
89
123
  const composer = (
90
124
  <PromptInput onSubmit={handleSubmit}>
91
- <PromptInputTextarea disabled={isBusy} placeholder="Send a message…" />
92
- <PromptInputSubmit onStop={requestCancellation} status={agent.status} />
125
+ <PromptInputTextarea disabled={isRestoring} placeholder="Send a message…" />
126
+ {isBusy && !isRestoring ? (
127
+ <PromptInputButton
128
+ aria-label="Stop"
129
+ className="absolute right-12 bottom-2.5 rounded-full"
130
+ onClick={requestCancellation}
131
+ variant="default"
132
+ >
133
+ <SquareIcon className="size-3 fill-current" />
134
+ </PromptInputButton>
135
+ ) : null}
136
+ <PromptInputSubmit disabled={isRestoring} status={isBusy ? undefined : agent.status} />
93
137
  </PromptInput>
94
138
  );
95
139
 
96
140
  return (
97
141
  <main className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
98
- {isEmpty ? null : (
99
- <header className="flex h-14 shrink-0 items-center justify-center pl-4 pr-2">
100
- <span className="truncate text-muted-foreground text-sm">{AGENT_NAME}</span>
101
- </header>
102
- )}
103
-
104
- {errorMessage ? (
105
- <div className="mx-auto w-full max-w-3xl shrink-0 px-4 pt-2 sm:px-6">
106
- <div
107
- className="flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5 text-sm"
108
- role="alert"
109
- >
110
- <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />
111
- <div>
112
- <p className="font-medium">Request failed</p>
113
- <p className="mt-0.5 text-muted-foreground">{errorMessage}</p>
114
- </div>
115
- </div>
116
- </div>
142
+ {showConversationLayout ? (
143
+ <ChatHeader canStartNewChat={activeSessionId !== undefined} />
117
144
  ) : null}
118
145
 
119
- {isEmpty ? null : (
120
- <Conversation className="min-h-0 flex-1">
121
- <ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 py-6 sm:px-6">
146
+ {showConversationLayout ? (
147
+ <Conversation
148
+ className="min-h-0 flex-1"
149
+ initial={sessionId === undefined ? undefined : false}
150
+ resize={activeSessionId === undefined ? "smooth" : "instant"}
151
+ scrollRestorationKey={
152
+ isEmpty || activeSessionId === undefined
153
+ ? undefined
154
+ : \`eve:web-chat-scroll:\${activeSessionId}\`
155
+ }
156
+ >
157
+ <ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 pt-20 pb-36 sm:px-6">
122
158
  {agent.data.messages.map((message, index) =>
123
159
  showPendingThinking &&
124
160
  isPendingAssistantShell &&
@@ -138,30 +174,73 @@ export function AgentChat() {
138
174
  ),
139
175
  )}
140
176
  {showPendingThinking ? <PendingThinking /> : null}
177
+ {errorMessage ? <ErrorMessage message={errorMessage} /> : null}
141
178
  </ConversationContent>
142
179
  <ConversationScrollButton />
143
180
  </Conversation>
144
- )}
181
+ ) : null}
145
182
 
146
183
  <div
147
184
  className={cn(
148
185
  "mx-auto w-full px-4 sm:px-6",
149
- isEmpty
150
- ? "flex max-w-xl flex-1 flex-col items-center justify-center gap-8 pb-[10vh]"
151
- : "max-w-3xl shrink-0 pb-6",
186
+ showConversationLayout
187
+ ? "fixed bottom-0 left-1/2 z-20 max-w-3xl -translate-x-1/2 bg-gradient-to-t from-background via-background to-transparent pt-4 pb-6"
188
+ : "flex max-w-xl flex-1 flex-col items-center justify-center gap-8 pb-[10vh]",
152
189
  )}
153
190
  >
154
- {isEmpty ? (
191
+ {showConversationLayout ? null : (
155
192
  <div className="flex flex-col items-center gap-3 text-center">
156
193
  <h1 className="font-medium text-5xl tracking-tighter">{AGENT_NAME}</h1>
157
194
  </div>
158
- ) : null}
195
+ )}
159
196
  <div className="w-full">{composer}</div>
160
197
  </div>
161
198
  </main>
162
199
  );
163
200
  }
164
201
 
202
+ function ErrorMessage({ message }: { readonly message: string }) {
203
+ return (
204
+ <Message className="max-w-full" from="assistant">
205
+ <MessageContent>
206
+ <div
207
+ className="flex w-full items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5 text-sm"
208
+ role="alert"
209
+ >
210
+ <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />
211
+ <div>
212
+ <p className="font-medium">Request failed</p>
213
+ <p className="mt-0.5 text-muted-foreground">{message}</p>
214
+ </div>
215
+ </div>
216
+ </MessageContent>
217
+ </Message>
218
+ );
219
+ }
220
+
221
+ function ChatHeader({ canStartNewChat }: { readonly canStartNewChat: boolean }) {
222
+ return (
223
+ <header className="pointer-events-none fixed top-0 right-0 left-0 z-20 h-14">
224
+ <div className="relative mx-auto flex h-full w-full max-w-3xl items-center justify-center bg-background px-24">
225
+ <span className="truncate text-muted-foreground text-sm">{AGENT_NAME}</span>
226
+ {canStartNewChat ? (
227
+ <Button
228
+ aria-label="Start a new chat"
229
+ className="pointer-events-auto fixed top-2 right-6"
230
+ onClick={() => window.location.assign("/s")}
231
+ size="sm"
232
+ type="button"
233
+ variant="ghost"
234
+ >
235
+ <PlusIcon className="size-4" />
236
+ <span className="hidden sm:inline">New chat</span>
237
+ </Button>
238
+ ) : null}
239
+ </div>
240
+ </header>
241
+ );
242
+ }
243
+
165
244
  function PendingThinking() {
166
245
  return (
167
246
  <Message aria-live="polite" from="assistant">
@@ -208,6 +287,7 @@ import type {
208
287
  EveAuthorizationPart,
209
288
  EveDynamicToolPart,
210
289
  EveMessage,
290
+ EveMessageInputRequest,
211
291
  EveMessagePart,
212
292
  } from "eve/react";
213
293
  import {
@@ -219,6 +299,17 @@ import {
219
299
  XCircleIcon,
220
300
  } from "lucide-react";
221
301
  import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message";
302
+ import {
303
+ Question,
304
+ QuestionActions,
305
+ QuestionDescription,
306
+ QuestionInput,
307
+ QuestionOption,
308
+ QuestionOptions,
309
+ QuestionPrompt,
310
+ type QuestionResponse,
311
+ QuestionSubmit,
312
+ } from "@/components/ai-elements/question";
222
313
  import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning";
223
314
  import {
224
315
  Tool,
@@ -253,6 +344,9 @@ export function AgentMessage({
253
344
  (last, part, index) => (part.type === "text" ? index : last),
254
345
  -1,
255
346
  );
347
+ const hasAssistantText =
348
+ message.role === "assistant" &&
349
+ message.parts.some((part) => part.type === "text" && part.text.length > 0);
256
350
 
257
351
  return (
258
352
  <Message
@@ -260,15 +354,17 @@ export function AgentMessage({
260
354
  from={message.role}
261
355
  >
262
356
  <MessageContent>
263
- {message.parts.map((part, index) => (
264
- <AgentMessagePart
265
- canRespond={canRespond}
266
- key={partKey(part, index)}
267
- onInputResponses={onInputResponses}
268
- part={part}
269
- showCaret={isStreaming && message.role === "assistant" && index === lastTextIndex}
270
- />
271
- ))}
357
+ {message.parts.map((part, index) =>
358
+ hasAssistantText && part.type === "reasoning" ? null : (
359
+ <AgentMessagePart
360
+ canRespond={canRespond}
361
+ key={partKey(part, index)}
362
+ onInputResponses={onInputResponses}
363
+ part={part}
364
+ showCaret={isStreaming && message.role === "assistant" && index === lastTextIndex}
365
+ />
366
+ ),
367
+ )}
272
368
  </MessageContent>
273
369
  </Message>
274
370
  );
@@ -305,7 +401,19 @@ function AgentMessagePart({
305
401
  return <AttachmentPart part={part} />;
306
402
  case "authorization":
307
403
  return <AuthorizationPrompt part={part} />;
308
- case "dynamic-tool":
404
+ case "dynamic-tool": {
405
+ const inputRequest = part.toolMetadata?.eve?.inputRequest;
406
+ if (inputRequest?.kind === "question") {
407
+ return (
408
+ <QuestionRequest
409
+ canRespond={canRespond}
410
+ inputRequest={inputRequest}
411
+ inputResponse={part.toolMetadata?.eve?.inputResponse}
412
+ onInputResponses={onInputResponses}
413
+ />
414
+ );
415
+ }
416
+
309
417
  return (
310
418
  <Tool
311
419
  defaultOpen={part.state === "approval-requested" || part.state === "approval-responded"}
@@ -327,9 +435,76 @@ function AgentMessagePart({
327
435
  </ToolContent>
328
436
  </Tool>
329
437
  );
438
+ }
330
439
  }
331
440
  }
332
441
 
442
+ function QuestionRequest({
443
+ canRespond,
444
+ inputRequest,
445
+ inputResponse,
446
+ onInputResponses,
447
+ }: {
448
+ readonly canRespond: boolean;
449
+ readonly inputRequest: EveMessageInputRequest;
450
+ readonly inputResponse?: AgentInputResponse;
451
+ readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;
452
+ }) {
453
+ const selectedOption = inputRequest.options?.find(
454
+ (option) => option.id === inputResponse?.optionId,
455
+ );
456
+ const hasOptions = (inputRequest.options?.length ?? 0) > 0;
457
+ const acceptsFreeform = inputRequest.allowFreeform === true || !hasOptions;
458
+
459
+ const submitResponse = ({ selectedValues, text }: QuestionResponse) =>
460
+ onInputResponses([
461
+ {
462
+ optionId: selectedValues[0],
463
+ requestId: inputRequest.requestId,
464
+ text,
465
+ },
466
+ ]);
467
+
468
+ return (
469
+ <Question
470
+ defaultValue={{
471
+ selectedValues: inputResponse?.optionId ? [inputResponse.optionId] : [],
472
+ text: inputResponse?.text ?? "",
473
+ }}
474
+ disabled={!canRespond || inputResponse !== undefined}
475
+ onSubmit={submitResponse}
476
+ >
477
+ <QuestionPrompt>{inputRequest.prompt}</QuestionPrompt>
478
+ {hasOptions ? (
479
+ <QuestionOptions className="flex-col items-stretch" aria-label={inputRequest.prompt}>
480
+ {inputRequest.options?.map((option) => (
481
+ <QuestionOption className="justify-start text-left" key={option.id} value={option.id}>
482
+ <span>
483
+ <span className="block">{option.label}</span>
484
+ {option.description ? (
485
+ <span className="block font-normal text-xs opacity-70">{option.description}</span>
486
+ ) : null}
487
+ </span>
488
+ </QuestionOption>
489
+ ))}
490
+ </QuestionOptions>
491
+ ) : null}
492
+ {acceptsFreeform ? (
493
+ <QuestionInput aria-label="Answer" placeholder="Type your answer…" />
494
+ ) : null}
495
+ {inputResponse ? (
496
+ <QuestionDescription>
497
+ Responded: {selectedOption?.label ?? inputResponse.text ?? inputResponse.optionId}
498
+ </QuestionDescription>
499
+ ) : (
500
+ <QuestionActions>
501
+ <QuestionSubmit>Answer</QuestionSubmit>
502
+ </QuestionActions>
503
+ )}
504
+ </Question>
505
+ );
506
+ }
507
+
333
508
  function AttachmentPart({ part }: { readonly part: EveFilePart }) {
334
509
  const label = part.filename ?? "Attachment";
335
510
  const detail = [part.mediaType, formatBytes(part.size)].filter(Boolean).join(" - ");
@@ -668,6 +843,21 @@ export default function RootLayout({ children }: { readonly children: ReactNode
668
843
  export default function Page() {
669
844
  return <AgentChat />;
670
845
  }
846
+ `,"app/s/[sessionId]/page.tsx":`import { AgentChat } from "@/app/_components/agent-chat";
847
+
848
+ export default async function SessionPage({
849
+ params,
850
+ }: {
851
+ readonly params: Promise<{ readonly sessionId: string }>;
852
+ }) {
853
+ const { sessionId } = await params;
854
+ return <AgentChat sessionId={sessionId} />;
855
+ }
856
+ `,"app/s/page.tsx":`import { AgentChat } from "@/app/_components/agent-chat";
857
+
858
+ export default function NewSessionPage() {
859
+ return <AgentChat sessionless />;
860
+ }
671
861
  `,"components/ai-elements/chain-of-thought.tsx":`"use client";
672
862
 
673
863
  import { useControllableState } from "@radix-ui/react-use-controllable-state";
@@ -1394,21 +1584,116 @@ import { cn } from "@/lib/utils";
1394
1584
  import type { UIMessage } from "ai";
1395
1585
  import { ArrowDownIcon, DownloadIcon } from "lucide-react";
1396
1586
  import type { ComponentProps } from "react";
1397
- import { useCallback } from "react";
1587
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
1398
1588
  import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
1399
1589
 
1400
- export type ConversationProps = ComponentProps<typeof StickToBottom>;
1590
+ export type ConversationProps = ComponentProps<typeof StickToBottom> & {
1591
+ scrollRestorationKey?: string;
1592
+ };
1401
1593
 
1402
- export const Conversation = ({ className, ...props }: ConversationProps) => (
1594
+ export const Conversation = ({
1595
+ children,
1596
+ className,
1597
+ initial,
1598
+ scrollRestorationKey,
1599
+ ...props
1600
+ }: ConversationProps) => (
1403
1601
  <StickToBottom
1404
1602
  className={cn("relative flex-1 overflow-y-hidden", className)}
1405
- initial="smooth"
1603
+ initial={initial ?? (scrollRestorationKey === undefined ? "smooth" : false)}
1406
1604
  resize="smooth"
1407
1605
  role="log"
1408
1606
  {...props}
1409
- />
1607
+ >
1608
+ {typeof children === "function" ? (
1609
+ (context) => (
1610
+ <>
1611
+ {children(context)}
1612
+ {scrollRestorationKey === undefined ? null : (
1613
+ <ConversationScrollRestoration storageKey={scrollRestorationKey} />
1614
+ )}
1615
+ </>
1616
+ )
1617
+ ) : (
1618
+ <>
1619
+ {children}
1620
+ {scrollRestorationKey === undefined ? null : (
1621
+ <ConversationScrollRestoration storageKey={scrollRestorationKey} />
1622
+ )}
1623
+ </>
1624
+ )}
1625
+ </StickToBottom>
1410
1626
  );
1411
1627
 
1628
+ function ConversationScrollRestoration({ storageKey }: { readonly storageKey: string }) {
1629
+ const { scrollRef, scrollToBottom, state } = useStickToBottomContext();
1630
+ const restoredKeyRef = useRef<string | undefined>(undefined);
1631
+
1632
+ useLayoutEffect(() => {
1633
+ const scrollElement = scrollRef.current;
1634
+ if (scrollElement === null) return;
1635
+
1636
+ if (restoredKeyRef.current !== storageKey) {
1637
+ const saved = readScrollPosition(sessionStorage.getItem(storageKey));
1638
+ if (saved?.atBottom === false) {
1639
+ scrollElement.scrollTop = saved.scrollTop;
1640
+ requestAnimationFrame(() => {
1641
+ scrollElement.scrollTop = saved.scrollTop;
1642
+ });
1643
+ } else {
1644
+ scrollElement.scrollTop = scrollElement.scrollHeight;
1645
+ scrollToBottom({ animation: "instant", ignoreEscapes: true });
1646
+ }
1647
+ restoredKeyRef.current = storageKey;
1648
+ }
1649
+
1650
+ const saveNow = () => {
1651
+ sessionStorage.setItem(
1652
+ storageKey,
1653
+ JSON.stringify({
1654
+ atBottom: state.isAtBottom || state.isNearBottom,
1655
+ scrollTop: scrollElement.scrollTop,
1656
+ }),
1657
+ );
1658
+ };
1659
+ let frame: number | undefined;
1660
+ const scheduleSave = () => {
1661
+ if (frame !== undefined) return;
1662
+ frame = requestAnimationFrame(() => {
1663
+ frame = undefined;
1664
+ saveNow();
1665
+ });
1666
+ };
1667
+ scrollElement.addEventListener("scroll", scheduleSave, { passive: true });
1668
+ window.addEventListener("pagehide", saveNow);
1669
+ return () => {
1670
+ scrollElement.removeEventListener("scroll", scheduleSave);
1671
+ window.removeEventListener("pagehide", saveNow);
1672
+ if (frame !== undefined) cancelAnimationFrame(frame);
1673
+ saveNow();
1674
+ };
1675
+ }, [scrollRef, scrollToBottom, state, storageKey]);
1676
+
1677
+ return null;
1678
+ }
1679
+
1680
+ function readScrollPosition(value: string | null):
1681
+ | {
1682
+ readonly atBottom: boolean;
1683
+ readonly scrollTop: number;
1684
+ }
1685
+ | undefined {
1686
+ if (value === null) return undefined;
1687
+ try {
1688
+ const parsed = JSON.parse(value) as { atBottom?: unknown; scrollTop?: unknown };
1689
+ return typeof parsed.atBottom === "boolean" && typeof parsed.scrollTop === "number"
1690
+ ? { atBottom: parsed.atBottom, scrollTop: parsed.scrollTop }
1691
+ : undefined;
1692
+ } catch {
1693
+ return undefined;
1694
+ }
1695
+ }
1696
+
1412
1697
  export type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>;
1413
1698
 
1414
1699
  export const ConversationContent = ({ className, ...props }: ConversationContentProps) => (
@@ -1455,16 +1740,21 @@ export const ConversationScrollButton = ({
1455
1740
  ...props
1456
1741
  }: ConversationScrollButtonProps) => {
1457
1742
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
1743
+ const [isReady, setIsReady] = useState(false);
1744
+
1745
+ useEffect(() => setIsReady(true), []);
1458
1746
 
1459
1747
  const handleScrollToBottom = useCallback(() => {
1460
1748
  scrollToBottom();
1461
1749
  }, [scrollToBottom]);
1462
1750
 
1463
1751
  return (
1752
+ isReady &&
1464
1753
  !isAtBottom && (
1465
1754
  <Button
1755
+ aria-label="Scroll to bottom"
1466
1756
  className={cn(
1467
- "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
1757
+ "absolute bottom-32 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
1468
1758
  className,
1469
1759
  )}
1470
1760
  onClick={handleScrollToBottom}
@@ -3149,6 +3439,287 @@ export const PromptInputCommandSeparator = ({
3149
3439
  className,
3150
3440
  ...props
3151
3441
  }: PromptInputCommandSeparatorProps) => <CommandSeparator className={cn(className)} {...props} />;
3442
+ `,"components/ai-elements/question.tsx":`"use client";
3443
+
3444
+ import type {
3445
+ ChangeEvent,
3446
+ ComponentProps,
3447
+ FormEvent,
3448
+ HTMLAttributes,
3449
+ MouseEvent,
3450
+ ReactNode,
3451
+ } from "react";
3452
+
3453
+ import { Button } from "@/components/ui/button";
3454
+ import { Textarea } from "@/components/ui/textarea";
3455
+ import { cn } from "@/lib/utils";
3456
+ import { createContext, useCallback, useContext, useMemo, useState } from "react";
3457
+
3458
+ export interface QuestionValue {
3459
+ selectedValues: readonly string[];
3460
+ text: string;
3461
+ }
3462
+
3463
+ export interface QuestionResponse {
3464
+ selectedValues: readonly string[];
3465
+ text?: string;
3466
+ }
3467
+
3468
+ type SelectionMode = "multiple" | "single";
3469
+
3470
+ interface QuestionContextValue {
3471
+ disabled: boolean;
3472
+ selectedValues: readonly string[];
3473
+ selectionMode: SelectionMode;
3474
+ setText: (text: string) => void;
3475
+ text: string;
3476
+ toggleValue: (value: string) => void;
3477
+ }
3478
+
3479
+ const QuestionContext = createContext<QuestionContextValue | null>(null);
3480
+
3481
+ const useQuestion = () => {
3482
+ const context = useContext(QuestionContext);
3483
+
3484
+ if (!context) {
3485
+ throw new Error("Question components must be used within Question");
3486
+ }
3487
+
3488
+ return context;
3489
+ };
3490
+
3491
+ export type QuestionProps = Omit<ComponentProps<"form">, "defaultValue" | "onSubmit" | "value"> & {
3492
+ defaultValue?: QuestionValue;
3493
+ disabled?: boolean;
3494
+ onSubmit?: (
3495
+ response: QuestionResponse,
3496
+ event: FormEvent<HTMLFormElement>,
3497
+ ) => void | Promise<void>;
3498
+ onValueChange?: (value: QuestionValue) => void;
3499
+ selectionMode?: SelectionMode;
3500
+ value?: QuestionValue;
3501
+ };
3502
+
3503
+ const EMPTY_VALUE: QuestionValue = { selectedValues: [], text: "" };
3504
+
3505
+ const getSelectedValues = (
3506
+ currentValues: readonly string[],
3507
+ optionValue: string,
3508
+ selectionMode: SelectionMode,
3509
+ ): readonly string[] => {
3510
+ const isSelected = currentValues.includes(optionValue);
3511
+
3512
+ if (selectionMode === "single") {
3513
+ return isSelected ? [] : [optionValue];
3514
+ }
3515
+
3516
+ if (isSelected) {
3517
+ return currentValues.filter((item) => item !== optionValue);
3518
+ }
3519
+
3520
+ return [...currentValues, optionValue];
3521
+ };
3522
+
3523
+ export const Question = ({
3524
+ children,
3525
+ className,
3526
+ defaultValue = EMPTY_VALUE,
3527
+ disabled = false,
3528
+ onSubmit,
3529
+ onValueChange,
3530
+ selectionMode = "single",
3531
+ value: controlledValue,
3532
+ ...props
3533
+ }: QuestionProps) => {
3534
+ const [internalValue, setInternalValue] = useState(defaultValue);
3535
+ const value = controlledValue ?? internalValue;
3536
+
3537
+ const setValue = useCallback(
3538
+ (nextValue: QuestionValue) => {
3539
+ if (controlledValue === undefined) {
3540
+ setInternalValue(nextValue);
3541
+ }
3542
+ onValueChange?.(nextValue);
3543
+ },
3544
+ [controlledValue, onValueChange],
3545
+ );
3546
+
3547
+ const setText = useCallback(
3548
+ (text: string) => {
3549
+ setValue({ ...value, text });
3550
+ },
3551
+ [setValue, value],
3552
+ );
3553
+
3554
+ const toggleValue = useCallback(
3555
+ (optionValue: string) => {
3556
+ const selectedValues = getSelectedValues(value.selectedValues, optionValue, selectionMode);
3557
+ setValue({ ...value, selectedValues });
3558
+ },
3559
+ [selectionMode, setValue, value],
3560
+ );
3561
+
3562
+ const contextValue = useMemo(
3563
+ () => ({
3564
+ disabled,
3565
+ selectedValues: value.selectedValues,
3566
+ selectionMode,
3567
+ setText,
3568
+ text: value.text,
3569
+ toggleValue,
3570
+ }),
3571
+ [disabled, selectionMode, setText, toggleValue, value],
3572
+ );
3573
+
3574
+ const handleSubmit = useCallback(
3575
+ async (event: FormEvent<HTMLFormElement>) => {
3576
+ event.preventDefault();
3577
+ if (disabled) {
3578
+ return;
3579
+ }
3580
+
3581
+ const text = value.text.trim();
3582
+ if (value.selectedValues.length === 0 && text.length === 0) {
3583
+ return;
3584
+ }
3585
+
3586
+ await onSubmit?.(
3587
+ {
3588
+ selectedValues: value.selectedValues,
3589
+ text: text.length > 0 ? text : undefined,
3590
+ },
3591
+ event,
3592
+ );
3593
+ },
3594
+ [disabled, onSubmit, value],
3595
+ );
3596
+
3597
+ return (
3598
+ <QuestionContext.Provider value={contextValue}>
3599
+ <form
3600
+ className={cn("space-y-4 rounded-lg border bg-background p-4", className)}
3601
+ onSubmit={handleSubmit}
3602
+ {...props}
3603
+ >
3604
+ {children}
3605
+ </form>
3606
+ </QuestionContext.Provider>
3607
+ );
3608
+ };
3609
+
3610
+ export type QuestionPromptProps = HTMLAttributes<HTMLParagraphElement>;
3611
+
3612
+ export const QuestionPrompt = ({ className, ...props }: QuestionPromptProps) => (
3613
+ <p className={cn("font-medium text-sm", className)} {...props} />
3614
+ );
3615
+
3616
+ export type QuestionDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
3617
+
3618
+ export const QuestionDescription = ({ className, ...props }: QuestionDescriptionProps) => (
3619
+ <p className={cn("text-muted-foreground text-sm", className)} {...props} />
3620
+ );
3621
+
3622
+ export type QuestionOptionsProps = HTMLAttributes<HTMLDivElement>;
3623
+
3624
+ export const QuestionOptions = ({ className, ...props }: QuestionOptionsProps) => {
3625
+ const { selectionMode } = useQuestion();
3626
+
3627
+ return (
3628
+ <div
3629
+ className={cn("flex flex-wrap gap-2", className)}
3630
+ role={selectionMode === "single" ? "radiogroup" : "group"}
3631
+ {...props}
3632
+ />
3633
+ );
3634
+ };
3635
+
3636
+ export type QuestionOptionProps = Omit<ComponentProps<typeof Button>, "value"> & {
3637
+ value: string;
3638
+ };
3639
+
3640
+ export const QuestionOption = ({
3641
+ children,
3642
+ className,
3643
+ disabled,
3644
+ onClick,
3645
+ value,
3646
+ variant,
3647
+ ...props
3648
+ }: QuestionOptionProps) => {
3649
+ const question = useQuestion();
3650
+ const isSelected = question.selectedValues.includes(value);
3651
+ const role = question.selectionMode === "single" ? "radio" : "checkbox";
3652
+ const handleClick = useCallback(
3653
+ (event: MouseEvent<HTMLButtonElement>) => {
3654
+ question.toggleValue(value);
3655
+ onClick?.(event);
3656
+ },
3657
+ [onClick, question, value],
3658
+ );
3659
+
3660
+ return (
3661
+ <Button
3662
+ aria-checked={isSelected}
3663
+ className={cn("h-auto whitespace-normal", className)}
3664
+ disabled={question.disabled || disabled}
3665
+ onClick={handleClick}
3666
+ role={role}
3667
+ type="button"
3668
+ variant={variant ?? (isSelected ? "default" : "outline")}
3669
+ {...props}
3670
+ >
3671
+ {children ?? value}
3672
+ </Button>
3673
+ );
3674
+ };
3675
+
3676
+ export type QuestionInputProps = Omit<ComponentProps<typeof Textarea>, "defaultValue" | "value">;
3677
+
3678
+ export const QuestionInput = ({ className, disabled, onChange, ...props }: QuestionInputProps) => {
3679
+ const question = useQuestion();
3680
+ const handleChange = useCallback(
3681
+ (event: ChangeEvent<HTMLTextAreaElement>) => {
3682
+ question.setText(event.currentTarget.value);
3683
+ onChange?.(event);
3684
+ },
3685
+ [onChange, question],
3686
+ );
3687
+
3688
+ return (
3689
+ <Textarea
3690
+ className={cn("min-h-20", className)}
3691
+ disabled={question.disabled || disabled}
3692
+ onChange={handleChange}
3693
+ value={question.text}
3694
+ {...props}
3695
+ />
3696
+ );
3697
+ };
3698
+
3699
+ export type QuestionActionsProps = HTMLAttributes<HTMLDivElement>;
3700
+
3701
+ export const QuestionActions = ({ className, ...props }: QuestionActionsProps) => (
3702
+ <div className={cn("flex items-center justify-end gap-2", className)} {...props} />
3703
+ );
3704
+
3705
+ export type QuestionSubmitProps = ComponentProps<typeof Button> & {
3706
+ children?: ReactNode;
3707
+ };
3708
+
3709
+ export const QuestionSubmit = ({
3710
+ children = "Submit",
3711
+ disabled,
3712
+ ...props
3713
+ }: QuestionSubmitProps) => {
3714
+ const question = useQuestion();
3715
+ const hasResponse = question.selectedValues.length > 0 || question.text.trim().length > 0;
3716
+
3717
+ return (
3718
+ <Button disabled={question.disabled || disabled || !hasResponse} type="submit" {...props}>
3719
+ {children}
3720
+ </Button>
3721
+ );
3722
+ };
3152
3723
  `,"components/ai-elements/reasoning.tsx":`"use client";
3153
3724
 
3154
3725
  import { useControllableState } from "@radix-ui/react-use-controllable-state";
@@ -4942,6 +5513,36 @@ const betterAuthSession: AuthFn<Request> = async (request) => {
4942
5513
  export default eveChannel({
4943
5514
  auth: [betterAuthSession, vercelOidc(), localDev()],
4944
5515
  });
5516
+ `,"app/_components/authenticated-agent-chat.tsx":`import { headers } from "next/headers";
5517
+ import { auth } from "@/lib/auth";
5518
+ import { AgentChat } from "./agent-chat";
5519
+ import { AccountControl, SignIn } from "./web-chat-auth";
5520
+
5521
+ export async function AuthenticatedAgentChat({
5522
+ sessionId,
5523
+ sessionless,
5524
+ }: {
5525
+ readonly sessionId?: string;
5526
+ readonly sessionless?: boolean;
5527
+ }) {
5528
+ if (process.env.NODE_ENV === "development") {
5529
+ return <AgentChat sessionId={sessionId} sessionless={sessionless} />;
5530
+ }
5531
+
5532
+ const session = await auth.api.getSession({ headers: await headers() });
5533
+ if (!session) return <SignIn />;
5534
+
5535
+ return (
5536
+ <>
5537
+ <AgentChat sessionId={sessionId} sessionless={sessionless} />
5538
+ <AccountControl
5539
+ email={session.user.email}
5540
+ image={session.user.image}
5541
+ name={session.user.name}
5542
+ />
5543
+ </>
5544
+ );
5545
+ }
4945
5546
  `,"app/_components/web-chat-auth.tsx":`"use client";
4946
5547
 
4947
5548
  import { LogOutIcon } from "lucide-react";
@@ -5154,31 +5755,25 @@ export default function RootLayout({ children }: { readonly children: ReactNode
5154
5755
  </html>
5155
5756
  );
5156
5757
  }
5157
- `,"app/page.tsx":`import { headers } from "next/headers";
5158
- import { AgentChat } from "./_components/agent-chat";
5159
- import { AccountControl, SignIn } from "./_components/web-chat-auth";
5160
- import { auth } from "@/lib/auth";
5758
+ `,"app/page.tsx":`import { AuthenticatedAgentChat } from "./_components/authenticated-agent-chat";
5161
5759
 
5162
- export default async function Page() {
5163
- if (process.env.NODE_ENV === "development") {
5164
- return <AgentChat />;
5165
- }
5760
+ export default function Page() {
5761
+ return <AuthenticatedAgentChat />;
5762
+ }
5763
+ `,"app/s/[sessionId]/page.tsx":`import { AuthenticatedAgentChat } from "@/app/_components/authenticated-agent-chat";
5166
5764
 
5167
- const session = await auth.api.getSession({ headers: await headers() });
5168
- if (!session) {
5169
- return <SignIn />;
5170
- }
5765
+ export default async function SessionPage({
5766
+ params,
5767
+ }: {
5768
+ readonly params: Promise<{ readonly sessionId: string }>;
5769
+ }) {
5770
+ const { sessionId } = await params;
5771
+ return <AuthenticatedAgentChat sessionId={sessionId} />;
5772
+ }
5773
+ `,"app/s/page.tsx":`import { AuthenticatedAgentChat } from "@/app/_components/authenticated-agent-chat";
5171
5774
 
5172
- return (
5173
- <>
5174
- <AgentChat />
5175
- <AccountControl
5176
- email={session.user.email}
5177
- image={session.user.image}
5178
- name={session.user.name}
5179
- />
5180
- </>
5181
- );
5775
+ export default function NewSessionPage() {
5776
+ return <AuthenticatedAgentChat sessionless />;
5182
5777
  }
5183
5778
  `,"lib/auth-client.ts":`"use client";
5184
5779