@agent-native/agentkit 0.3.2-nightly-20260927082735 → 0.4.0

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.
@@ -1,13 +1,15 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ActionButton, AgentSuggestionBar, IconButton, MessageQueueDrawer, PromptComposer, Surface, TextField, agentSuggestionPrompt, splitMarkdownBlocks, writeClipboardText, } from "@agent-native/toolkit/agentkit";
3
- import { IconActivity, IconAlertCircle, IconArrowFork, IconBook2, IconBrain, IconChevronRight, IconCircleCheck, IconCode, IconCopy, IconDotsVertical, IconFile, IconGitBranch, IconId, IconMessage, IconPlugConnected, IconPlayerPause, IconPlayerPlay, IconSearch, IconTerminal2, IconChecklist, IconThumbDown, IconThumbUp, IconTool, IconX, } from "@tabler/icons-react";
4
- import { Component, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
3
+ import { Dialog, Popover, TextArea } from "@agent-native/toolkit/design-system";
4
+ import { IconActivity, IconAlertCircle, IconArrowDown, IconArrowFork, IconArrowUp, IconBook2, IconBrain, IconChevronLeft, IconChevronRight, IconCircleCheck, IconCode, IconCopy, IconDotsVertical, IconFile, IconFolder, IconFileText, IconCheckbox, IconMail, IconUser, IconPresentation, IconStack2, IconMessageChatbot, IconGitBranch, IconId, IconMessage, IconPlugConnected, IconPlayerPause, IconPlayerPlay, IconPencil, IconRefresh, IconSearch, IconTerminal2, IconChecklist, IconThumbDown, IconThumbUp, IconTool, IconX, } from "@tabler/icons-react";
5
+ import { createContext, Component, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
5
6
  import ReactMarkdown from "react-markdown";
6
7
  import remarkGfm from "remark-gfm";
7
8
  import { createAgentKitComposerSubmission, snapshotComposerValue, } from "./composer-submission.js";
8
9
  import { inferAgentActivityKind, } from "../protocol/index.js";
9
10
  import { useAgentCapability, useAgentConnection, useAgentKit, useAgentKitControl, useAgentKitMutation, useAgentThread, } from "./context.js";
10
11
  import { AgentStreamingText } from "./streaming-text.js";
12
+ /** Isolates custom renderers so one extension cannot take down the chat. */
11
13
  export class AgentKitErrorBoundary extends Component {
12
14
  state = {
13
15
  resetKey: this.props.resetKey,
@@ -55,6 +57,7 @@ function AgentKitRegionSlot({ slot: Slot, threadId, children, }) {
55
57
  }
56
58
  const allowedAgentProtocols = new Set(["http:", "https:", "mailto:", "blob:"]);
57
59
  const allowedAgentImageProtocols = new Set(["http:", "https:", "blob:"]);
60
+ /** Keeps an unfinished bold delimiter literal while a streamed message grows. */
58
61
  function escapeIncompleteStrongMarkdown(text) {
59
62
  const delimiterPositions = new Map();
60
63
  let inFence = false;
@@ -113,6 +116,7 @@ function escapeIncompleteStrongMarkdown(text) {
113
116
  .sort((left, right) => right.position - left.position)
114
117
  .reduce((value, { delimiter, position }) => `${value.slice(0, position)}\\${delimiter[0]}\\${delimiter[1]}${value.slice(position + 2)}`, text);
115
118
  }
119
+ /** Rejects executable and embedded-data URLs before they reach a default anchor. */
116
120
  export function safeAgentHref(href) {
117
121
  if (!href)
118
122
  return undefined;
@@ -140,6 +144,7 @@ function AgentMarkdown({ text }) {
140
144
  const blocks = useMemo(() => (tail ? [...completedBlocks, tail] : completedBlocks), [completedBlocks, tail]);
141
145
  return (_jsx("div", { className: "agentkit-markdown", "data-format": "markdown", children: blocks.map((block, index) => (_jsx(AgentMarkdownBlock, { text: block }, index))) }));
142
146
  }
147
+ /** Keeps participant avatars on image-capable, non-executable URL schemes. */
143
148
  export function safeAgentImageSrc(src) {
144
149
  if (!src)
145
150
  return undefined;
@@ -458,6 +463,17 @@ export function AgentActivityGroup({ runId, afterSequence, throughSequence, excl
458
463
  const tool = toolMap.get(id);
459
464
  return tool ? [toolToActivity(tool)] : [];
460
465
  });
466
+ const durableToolResults = items.flatMap((activity) => {
467
+ const tool = toolMap.get(activity.id);
468
+ const Renderer = tool
469
+ ? (registry.tools?.[tool.name] ?? slots.tool)
470
+ : undefined;
471
+ return tool && Renderer && tool.status !== "running"
472
+ ? [{ tool, Renderer }]
473
+ : [];
474
+ });
475
+ const durableToolResultIds = new Set(durableToolResults.map(({ tool }) => tool.id));
476
+ const activityItems = items.filter((activity) => !durableToolResultIds.has(activity.id));
461
477
  const running = items.some((item) => item.status === "running");
462
478
  const run = runId ? thread.runs[runId] : undefined;
463
479
  const segmentStartedEvent = firstWorkEvents(runEvents).find((event) => sequenceInRange(event.sequence, { afterSequence, throughSequence }));
@@ -507,7 +523,7 @@ export function AgentActivityGroup({ runId, afterSequence, throughSequence, excl
507
523
  if (items.length === 0)
508
524
  return null;
509
525
  const displayGroups = [];
510
- for (const activity of items) {
526
+ for (const activity of activityItems) {
511
527
  const sourceTool = toolMap.get(activity.id);
512
528
  const hasCustomToolRenderer = Boolean(sourceTool &&
513
529
  !activityMap.has(activity.id) &&
@@ -525,7 +541,7 @@ export function AgentActivityGroup({ runId, afterSequence, throughSequence, excl
525
541
  displayGroups.push([activity]);
526
542
  }
527
543
  }
528
- const labelsSummary = Array.from(new Set(items.map((item) => item.label.trim()).filter(Boolean)));
544
+ const labelsSummary = Array.from(new Set(activityItems.map((item) => item.label.trim()).filter(Boolean)));
529
545
  const remaining = Math.max(0, labelsSummary.length - 2);
530
546
  const summary = `${labelsSummary.slice(0, 2).join(", ")}${remaining ? ` +${remaining}` : ""}`;
531
547
  const formatDuration = (ms) => formatAgentKitDuration(ms, {
@@ -542,26 +558,26 @@ export function AgentActivityGroup({ runId, afterSequence, throughSequence, excl
542
558
  ? labels.workedFor.replace("{{duration}}", formatDuration(durationMs))
543
559
  : labels.worked
544
560
  : summary || labels.activities;
545
- return (_jsxs("details", { className: "agentkit-activities", "data-running": visiblyRunning ? "true" : undefined, open: open, children: [_jsxs("summary", { className: "agentkit-activities-summary", onClick: (event) => {
546
- event.preventDefault();
547
- setOpen((current) => !current);
548
- }, children: [_jsx(IconActivity, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { className: "agentkit-activities-label", children: summaryLabel }), !completedRunSummary ? (_jsx("span", { className: "agentkit-activities-count", children: items.length })) : null, _jsx(IconChevronRight, { "aria-hidden": "true", className: "agentkit-icon agentkit-summary-chevron" })] }), _jsx("div", { className: "agentkit-activities-list", children: displayGroups.map((activities) => {
549
- const activity = activities[0];
550
- if (activities.length > 1) {
551
- return (_jsx(RepeatedActivityCluster, { activities: activities, threadId: threadId }, `cluster:${activity.id}`));
552
- }
553
- const sourceTool = toolMap.get(activity.id);
554
- const ToolRenderer = sourceTool
555
- ? (registry.tools?.[sourceTool.name] ?? slots.tool)
556
- : undefined;
557
- if (sourceTool && ToolRenderer && !activityMap.has(activity.id)) {
558
- return (_jsx(ToolRenderer, { value: sourceTool, threadId: threadId }, sourceTool.id));
559
- }
560
- const Renderer = registry.activities?.[activity.kind] ??
561
- slots.activity ??
562
- AgentActivityItem;
563
- return (_jsx(Renderer, { value: activity, threadId: threadId }, activity.id));
564
- }) })] }));
561
+ return (_jsxs(_Fragment, { children: [durableToolResults.length ? (_jsx("div", { "data-agentkit-tool-results": "true", children: durableToolResults.map(({ tool, Renderer }) => (_jsx(Renderer, { value: tool, threadId: threadId }, tool.id))) })) : null, displayGroups.length ? (_jsxs("details", { className: "agentkit-activities", "data-running": visiblyRunning ? "true" : undefined, open: open, children: [_jsxs("summary", { className: "agentkit-activities-summary", onClick: (event) => {
562
+ event.preventDefault();
563
+ setOpen((current) => !current);
564
+ }, children: [_jsx(IconActivity, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { className: "agentkit-activities-label", children: summaryLabel }), !completedRunSummary ? (_jsx("span", { className: "agentkit-activities-count", children: activityItems.length })) : null, _jsx(IconChevronRight, { "aria-hidden": "true", className: "agentkit-icon agentkit-summary-chevron" })] }), _jsx("div", { className: "agentkit-activities-list", children: displayGroups.map((activities) => {
565
+ const activity = activities[0];
566
+ if (activities.length > 1) {
567
+ return (_jsx(RepeatedActivityCluster, { activities: activities, threadId: threadId }, `cluster:${activity.id}`));
568
+ }
569
+ const sourceTool = toolMap.get(activity.id);
570
+ const ToolRenderer = sourceTool
571
+ ? (registry.tools?.[sourceTool.name] ?? slots.tool)
572
+ : undefined;
573
+ if (sourceTool && ToolRenderer && !activityMap.has(activity.id)) {
574
+ return (_jsx(ToolRenderer, { value: sourceTool, threadId: threadId }, sourceTool.id));
575
+ }
576
+ const Renderer = registry.activities?.[activity.kind] ??
577
+ slots.activity ??
578
+ AgentActivityItem;
579
+ return (_jsx(Renderer, { value: activity, threadId: threadId }, activity.id));
580
+ }) })] })) : null] }));
565
581
  }
566
582
  export function AgentCollaborationFeed({ runId, afterSequence, throughSequence, }) {
567
583
  const { threadId, slots, registry, labels } = useAgentKit();
@@ -770,7 +786,91 @@ export function AgentWidgetView({ value: widget, threadId, }) {
770
786
  const titleId = `${widget.id}-title`;
771
787
  return (_jsx("section", { className: "agentkit-widget-shell", "data-widget-kind": widget.kind, "aria-labelledby": widget.title ? titleId : undefined, children: _jsxs(Surface, { className: "agentkit-widget", elevation: "low", padding: "none", children: [widget.title ? _jsx("h3", { id: titleId, children: widget.title }) : null, typeof widget.data === "string" ? _jsx("p", { children: widget.data }) : null, widget.actions?.length ? (_jsx("div", { className: "agentkit-widget-actions", children: widget.actions.map((action) => (_jsx(AgentWidgetActionButton, { widget: widget, action: action, threadId: threadId }, action.id))) })) : null] }) }));
772
788
  }
773
- export function AgentMessagePartView({ value: part, threadId, active = false, resetKey = `${threadId}:${part.type}`, }) {
789
+ function AgentMentionIcon({ icon }) {
790
+ const props = { size: 14, className: "agentkit-mention-icon" };
791
+ switch (icon) {
792
+ case "folder":
793
+ return _jsx(IconFolder, { ...props });
794
+ case "document":
795
+ return _jsx(IconFileText, { ...props });
796
+ case "form":
797
+ return _jsx(IconCheckbox, { ...props });
798
+ case "email":
799
+ return _jsx(IconMail, { ...props });
800
+ case "user":
801
+ return _jsx(IconUser, { ...props });
802
+ case "deck":
803
+ return _jsx(IconPresentation, { ...props });
804
+ case "agent":
805
+ return _jsx(IconMessageChatbot, { ...props });
806
+ case "file":
807
+ return _jsx(IconFile, { ...props });
808
+ default:
809
+ return _jsx(IconStack2, { ...props });
810
+ }
811
+ }
812
+ function renderUserMessageText(text) {
813
+ const richMatches = Array.from(text.matchAll(/@\[([^\]|]+)\|([^\]]+)\]/g));
814
+ if (richMatches.length) {
815
+ const parts = [];
816
+ let lastIndex = 0;
817
+ richMatches.forEach((match, index) => {
818
+ const start = match.index ?? 0;
819
+ if (start > lastIndex)
820
+ parts.push(text.slice(lastIndex, start));
821
+ const label = match[1] ?? "";
822
+ parts.push(_jsxs("span", { className: "agentkit-mention", "data-mention-label": label, children: [_jsx(AgentMentionIcon, { icon: match[2] ?? "" }), _jsx("span", { className: "agentkit-mention-label", children: label })] }, `rich-mention:${start}:${index}`));
823
+ lastIndex = start + match[0].length;
824
+ });
825
+ if (lastIndex < text.length)
826
+ parts.push(text.slice(lastIndex));
827
+ return parts;
828
+ }
829
+ const plainMatches = Array.from(text.matchAll(/(^|\s)@(\w+)/g));
830
+ if (!plainMatches.length)
831
+ return [text];
832
+ const parts = [];
833
+ let lastIndex = 0;
834
+ plainMatches.forEach((match, index) => {
835
+ const matchIndex = match.index ?? 0;
836
+ const start = matchIndex + (match[1]?.length ?? 0);
837
+ const end = matchIndex + match[0].length;
838
+ if (start > lastIndex)
839
+ parts.push(text.slice(lastIndex, start));
840
+ const label = match[2] ?? "";
841
+ parts.push(_jsxs("span", { className: "agentkit-mention agentkit-mention--plain", "data-mention-label": label, children: ["@", label] }, `plain-mention:${start}:${index}`));
842
+ lastIndex = end;
843
+ });
844
+ if (lastIndex < text.length)
845
+ parts.push(text.slice(lastIndex));
846
+ return parts;
847
+ }
848
+ function AgentUserMessageText({ text }) {
849
+ const { labels } = useAgentKit();
850
+ const [expanded, setExpanded] = useState(false);
851
+ const [expandable, setExpandable] = useState(false);
852
+ const contentRef = useRef(null);
853
+ const content = useMemo(() => renderUserMessageText(text), [text]);
854
+ useLayoutEffect(() => {
855
+ const element = contentRef.current;
856
+ if (!element)
857
+ return;
858
+ const measure = () => setExpandable(element.scrollHeight > 200);
859
+ measure();
860
+ if (typeof ResizeObserver === "undefined")
861
+ return;
862
+ const observer = new ResizeObserver(measure);
863
+ observer.observe(element);
864
+ return () => observer.disconnect();
865
+ }, [content]);
866
+ return (_jsxs("div", { className: "agentkit-user-message-text", children: [_jsx("div", { className: "agentkit-user-message-text-clip", "data-clamped": !expanded && expandable ? "true" : undefined, children: _jsx("div", { ref: contentRef, className: "agentkit-user-message-text-content", children: content }) }), expandable ? (_jsx("button", { type: "button", className: "agentkit-message-expand", "aria-expanded": expanded, onClick: () => setExpanded((value) => !value), children: expanded ? labels.collapseMessage : labels.expandMessage })) : null] }));
867
+ }
868
+ function AgentImageAttachmentPreview({ src, alt, }) {
869
+ const { labels } = useAgentKit();
870
+ const [open, setOpen] = useState(false);
871
+ return (_jsx(Dialog, { open: open, onOpenChange: setOpen, title: alt || labels.imagePreview, closeLabel: labels.closePreview, size: "fullscreen", trigger: _jsx("button", { type: "button", className: "agentkit-image-attachment-trigger", "aria-label": labels.previewAttachment.replace("{{name}}", alt), title: alt, children: _jsx("img", { src: src, alt: alt }) }), children: _jsx("div", { className: "agentkit-image-preview", children: _jsx("img", { src: src, alt: alt, draggable: false }) }) }));
872
+ }
873
+ export function AgentMessagePartView({ value: part, threadId, active = false, resetKey = `${threadId}:${part.type}`, userMessage = false, }) {
774
874
  const { labels, slots, registry } = useAgentKit();
775
875
  const RegisteredRenderer = registry.messageParts?.[part.type];
776
876
  if (RegisteredRenderer) {
@@ -778,8 +878,8 @@ export function AgentMessagePartView({ value: part, threadId, active = false, re
778
878
  }
779
879
  switch (part.type) {
780
880
  case "text": {
781
- const Renderer = slots.text;
782
- return Renderer ? (_jsx(Renderer, { value: part, threadId: threadId, active: active, resetKey: resetKey })) : part.format === "markdown" ? (_jsx(AgentStreamingText, { text: part.text, active: active, resetKey: resetKey, children: (visibleText) => _jsx(AgentMarkdown, { text: visibleText }) })) : (_jsx("p", { "data-format": "plain", children: _jsx(AgentStreamingText, { text: part.text, active: active, resetKey: resetKey }) }));
881
+ const Renderer = userMessage ? undefined : slots.text;
882
+ return Renderer ? (_jsx(Renderer, { value: part, threadId: threadId, active: active, resetKey: resetKey })) : userMessage ? (_jsx(AgentUserMessageText, { text: part.text })) : part.format === "markdown" ? (_jsx(AgentStreamingText, { text: part.text, active: active, resetKey: resetKey, children: (visibleText) => _jsx(AgentMarkdown, { text: visibleText }) })) : (_jsx("p", { "data-format": "plain", children: _jsx(AgentStreamingText, { text: part.text, active: active, resetKey: resetKey }) }));
783
883
  }
784
884
  case "reasoning": {
785
885
  const Renderer = slots.reasoning;
@@ -804,6 +904,12 @@ export function AgentMessagePartView({ value: part, threadId, active = false, re
804
904
  if (Renderer)
805
905
  return _jsx(Renderer, { value: part, threadId: threadId });
806
906
  const href = safeAgentHref(part.url);
907
+ if (userMessage && part.mediaType?.startsWith("image/") && href) {
908
+ return _jsx(AgentImageAttachmentPreview, { src: href, alt: part.name });
909
+ }
910
+ if (userMessage && part.name.startsWith("pasted-text-")) {
911
+ return href ? (_jsxs("a", { className: "agentkit-file agentkit-file--pasted-text", href: href, download: part.name, title: part.name, children: [_jsx(IconFileText, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { children: labels.pastedText })] })) : (_jsxs("span", { className: "agentkit-file agentkit-file--pasted-text", children: [_jsx(IconFileText, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { children: labels.pastedText })] }));
912
+ }
807
913
  return href ? (_jsxs("a", { className: "agentkit-file", href: href, download: part.name, children: [_jsx(IconFile, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { children: part.name })] })) : (_jsxs("span", { className: "agentkit-file", children: [_jsx(IconFile, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { children: part.name })] }));
808
914
  }
809
915
  case "widget": {
@@ -818,6 +924,7 @@ export function AgentMessagePartView({ value: part, threadId, active = false, re
818
924
  return null;
819
925
  }
820
926
  }
927
+ /** Resolves the stable server request identity without exposing local UI ids. */
821
928
  export function resolveAgentMessageRequestId(message, events) {
822
929
  const metadata = message.metadata;
823
930
  for (const source of [metadata?.custom, metadata]) {
@@ -840,24 +947,111 @@ export function resolveAgentMessageRequestId(message, events) {
840
947
  }
841
948
  return undefined;
842
949
  }
950
+ function resolveAgentMessageRunId(message, events) {
951
+ const metadata = message.metadata;
952
+ for (const source of [metadata?.custom, metadata]) {
953
+ const value = source?.runId;
954
+ if (typeof value === "string" && value)
955
+ return value;
956
+ }
957
+ for (let index = events.length - 1; index >= 0; index -= 1) {
958
+ const event = events[index];
959
+ if (((event.type === "message.created" ||
960
+ event.type === "message.completed") &&
961
+ event.message.id === message.id) ||
962
+ ((event.type === "message.delta" || event.type === "reasoning.delta") &&
963
+ event.messageId === message.id)) {
964
+ return event.runId;
965
+ }
966
+ }
967
+ return undefined;
968
+ }
969
+ const AgentMessageEditContext = createContext(null);
970
+ function messageText(message) {
971
+ return message.parts
972
+ .filter((part) => part.type === "text")
973
+ .map((part) => part.text)
974
+ .join("\n");
975
+ }
976
+ function messageRunOptions(message) {
977
+ const metadata = message.metadata ?? {};
978
+ const effort = metadata.reasoningEffort ?? metadata.effort;
979
+ const reasoningEffort = [
980
+ "none",
981
+ "minimal",
982
+ "low",
983
+ "medium",
984
+ "high",
985
+ "xhigh",
986
+ ].includes(String(effort))
987
+ ? String(effort)
988
+ : undefined;
989
+ return {
990
+ ...(typeof metadata.agentId === "string"
991
+ ? { agentId: metadata.agentId }
992
+ : {}),
993
+ ...(typeof metadata.model === "string" ? { model: metadata.model } : {}),
994
+ ...(typeof (metadata.requestMode ?? metadata.mode) === "string"
995
+ ? { mode: String(metadata.requestMode ?? metadata.mode) }
996
+ : {}),
997
+ ...(reasoningEffort ? { reasoningEffort } : {}),
998
+ metadata,
999
+ };
1000
+ }
1001
+ function findPreviousMessage(messages, messageId) {
1002
+ const index = messages.findIndex((message) => message.id === messageId);
1003
+ return index < 0 ? null : messages[index - 1];
1004
+ }
1005
+ function findPreviousUserMessage(messages, messageId) {
1006
+ const index = messages.findIndex((message) => message.id === messageId);
1007
+ if (index < 0)
1008
+ return undefined;
1009
+ for (let previous = index - 1; previous >= 0; previous -= 1) {
1010
+ const message = messages[previous];
1011
+ if (message?.role === "user")
1012
+ return message;
1013
+ }
1014
+ return undefined;
1015
+ }
1016
+ async function forkAndResubmitMessage({ controller, threadId, messages, sourceMessage, text, attachments, options, metadata, onThreadForked, messageUnavailable, }) {
1017
+ const previousMessage = findPreviousMessage(messages, sourceMessage.id);
1018
+ if (previousMessage === null) {
1019
+ throw new Error(messageUnavailable);
1020
+ }
1021
+ const forkedThread = await controller.forkThread(threadId, previousMessage?.id);
1022
+ onThreadForked?.(forkedThread);
1023
+ await controller.sendMessage({
1024
+ threadId: forkedThread.id,
1025
+ text,
1026
+ attachments,
1027
+ options,
1028
+ metadata,
1029
+ });
1030
+ }
843
1031
  export function AgentMessageActions({ value: message, threadId, }) {
844
- const { labels, onThreadForked } = useAgentKit();
1032
+ const { controller, labels, slots, onThreadForked, onCopyMessage, branchNavigation, } = useAgentKit();
845
1033
  const thread = useAgentThread(threadId);
846
1034
  const feedbackCapability = useAgentCapability("feedback");
847
1035
  const forkingCapability = useAgentCapability("threadForking");
848
1036
  const control = useAgentKitControl(threadId);
849
- const text = message.parts
850
- .filter((part) => part.type === "text")
851
- .map((part) => part.text)
852
- .join("\n");
1037
+ const editContext = useContext(AgentMessageEditContext);
1038
+ const text = messageText(message);
1039
+ const previousUserMessage = message.role === "assistant"
1040
+ ? findPreviousUserMessage(thread.messages, message.id)
1041
+ : undefined;
1042
+ const isLastAssistantMessage = message.role === "assistant" && thread.messages.at(-1)?.id === message.id;
853
1043
  const [copied, setCopied] = useState(false);
854
1044
  const [feedback, setFeedback] = useState(null);
1045
+ const [feedbackReason, setFeedbackReason] = useState("");
1046
+ const [feedbackReasonOpen, setFeedbackReasonOpen] = useState(false);
1047
+ const [feedbackReasonSubmitted, setFeedbackReasonSubmitted] = useState(false);
855
1048
  const copiedTimer = useRef(null);
856
1049
  const [requestIdCopied, setRequestIdCopied] = useState(false);
857
- const [actionModeOpen, setActionModeOpen] = useState(false);
858
- const actionPanelId = useId();
1050
+ const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
859
1051
  const requestIdCopiedTimer = useRef(null);
860
1052
  const requestId = useMemo(() => resolveAgentMessageRequestId(message, thread.events), [message, thread.events]);
1053
+ const runId = useMemo(() => resolveAgentMessageRunId(message, thread.events), [message, thread.events]);
1054
+ const messageSeq = thread.messages.findIndex((item) => item.id === message.id);
861
1055
  useEffect(() => () => {
862
1056
  if (copiedTimer.current)
863
1057
  clearTimeout(copiedTimer.current);
@@ -865,7 +1059,8 @@ export function AgentMessageActions({ value: message, threadId, }) {
865
1059
  clearTimeout(requestIdCopiedTimer.current);
866
1060
  }, [message.id]);
867
1061
  const copyAction = useAgentKitMutation(async () => {
868
- if (!(await writeClipboardText(text))) {
1062
+ const handled = await onCopyMessage?.({ message, text });
1063
+ if (!handled && !(await writeClipboardText(text))) {
869
1064
  throw new Error(labels.copyUnavailable);
870
1065
  }
871
1066
  setCopied(true);
@@ -873,7 +1068,22 @@ export function AgentMessageActions({ value: message, threadId, }) {
873
1068
  clearTimeout(copiedTimer.current);
874
1069
  copiedTimer.current = setTimeout(() => setCopied(false), 1_400);
875
1070
  }, `${threadId}:${message.id}:copy`);
876
- const feedbackAction = useAgentKitMutation((value) => control.submitFeedback(message.id, value), `${threadId}:${message.id}:feedback`);
1071
+ const feedbackAction = useAgentKitMutation(({ value, reason, }) => {
1072
+ const options = {
1073
+ ...(runId ? { runId } : {}),
1074
+ ...(messageSeq >= 0 ? { messageSeq } : {}),
1075
+ ...(reason ? { reason } : {}),
1076
+ };
1077
+ return Object.keys(options).length
1078
+ ? control.submitFeedback(message.id, value, options)
1079
+ : control.submitFeedback(message.id, value);
1080
+ }, `${threadId}:${message.id}:feedback`);
1081
+ const branchNavigationAction = useAgentKitMutation(async (direction) => {
1082
+ if (direction === "previous")
1083
+ await branchNavigation?.onPrevious();
1084
+ else
1085
+ await branchNavigation?.onNext();
1086
+ }, `${threadId}:${message.id}:branch-navigation`);
877
1087
  const requestIdAction = useAgentKitMutation(async () => {
878
1088
  if (!requestId)
879
1089
  throw new Error(labels.requestIdUnavailable);
@@ -891,41 +1101,106 @@ export function AgentMessageActions({ value: message, threadId, }) {
891
1101
  const thread = await control.fork(message.id);
892
1102
  onThreadForked?.(thread);
893
1103
  }, `${threadId}:${message.id}:fork`);
1104
+ const regenerateAction = useAgentKitMutation(async () => {
1105
+ if (!previousUserMessage)
1106
+ return;
1107
+ await forkAndResubmitMessage({
1108
+ controller,
1109
+ threadId,
1110
+ messages: thread.messages,
1111
+ sourceMessage: previousUserMessage,
1112
+ text: messageText(previousUserMessage),
1113
+ attachments: previousUserMessage.parts.filter((part) => part.type === "file"),
1114
+ options: messageRunOptions(previousUserMessage),
1115
+ metadata: previousUserMessage.metadata,
1116
+ onThreadForked,
1117
+ messageUnavailable: labels.messageUnavailable,
1118
+ });
1119
+ }, `${threadId}:${message.id}:regenerate`);
894
1120
  const updateFeedback = async (value) => {
895
1121
  const previous = feedback;
896
1122
  if (previous === value)
897
1123
  return;
898
1124
  setFeedback(value);
899
1125
  try {
900
- await feedbackAction.execute(value);
1126
+ await feedbackAction.execute({ value });
901
1127
  }
902
1128
  catch {
903
1129
  setFeedback(previous);
904
1130
  }
905
1131
  };
1132
+ const submitFeedbackReason = async (event) => {
1133
+ event.preventDefault();
1134
+ const reason = feedbackReason.trim();
1135
+ if (!reason)
1136
+ return;
1137
+ try {
1138
+ await feedbackAction.execute({ value: "negative", reason });
1139
+ setFeedbackReason("");
1140
+ setFeedbackReasonSubmitted(true);
1141
+ setFeedbackReasonOpen(false);
1142
+ }
1143
+ catch {
1144
+ // coercion-ok: feedbackAction.error is rendered below and the draft stays available for retry.
1145
+ }
1146
+ };
906
1147
  const actionError = copyAction.error ??
907
1148
  feedbackAction.error ??
1149
+ branchNavigationAction.error ??
908
1150
  requestIdAction.error ??
909
- forkAction.error;
910
- return (_jsxs("div", { className: "agentkit-message-actions", "data-action-mode": actionModeOpen ? "expanded" : "default", children: [message.role === "assistant" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "agentkit-message-action-swap", id: actionPanelId, "aria-live": "polite", children: [_jsxs("div", { className: "agentkit-message-action-group agentkit-message-action-group--default", "aria-hidden": actionModeOpen, children: [_jsx(IconButton, { label: copied ? labels.copied : labels.copy, icon: copied ? (_jsx(IconCircleCheck, { "aria-hidden": "true" })) : (_jsx(IconCopy, { "aria-hidden": "true" })), size: "compact", pending: copyAction.pending, disabled: actionModeOpen, onPress: () => void copyAction.execute().catch(() => undefined) }), feedbackCapability.visible ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: labels.positiveFeedback, icon: _jsx(IconThumbUp, { "aria-hidden": "true" }), size: "compact", "aria-pressed": feedback === "positive", pending: feedbackAction.pending && feedback === "positive", disabled: actionModeOpen ||
911
- feedbackAction.pending ||
912
- !feedbackCapability.enabled, title: feedbackCapability.reason, onPress: () => void updateFeedback("positive") }), _jsx(IconButton, { label: labels.negativeFeedback, icon: _jsx(IconThumbDown, { "aria-hidden": "true" }), size: "compact", "aria-pressed": feedback === "negative", pending: feedbackAction.pending && feedback === "negative", disabled: actionModeOpen ||
913
- feedbackAction.pending ||
914
- !feedbackCapability.enabled, title: feedbackCapability.reason, onPress: () => void updateFeedback("negative") })] })) : null] }), _jsxs("div", { className: "agentkit-message-action-group agentkit-message-action-group--request-id", "aria-hidden": !actionModeOpen, children: [_jsx(IconButton, { label: requestIdCopied ? labels.copied : labels.copyRequestId, icon: requestIdCopied ? (_jsx(IconCircleCheck, { "aria-hidden": "true" })) : (_jsx(IconId, { "aria-hidden": "true" })), size: "compact", pending: requestIdAction.pending, disabled: !actionModeOpen || !requestId || requestIdAction.pending, title: requestId
915
- ? requestIdCopied
916
- ? labels.copied
917
- : labels.copyRequestId
918
- : labels.requestIdUnavailable, onPress: () => void requestIdAction.execute().catch(() => undefined) }), forkingCapability.visible && onThreadForked ? (_jsx(IconButton, { label: labels.fork, icon: _jsx(IconGitBranch, { "aria-hidden": "true" }), size: "compact", pending: forkAction.pending, disabled: !actionModeOpen || !forkingCapability.enabled, title: forkingCapability.reason, onPress: () => void forkAction.execute().catch(() => undefined) })) : null] })] }), _jsx(IconButton, { label: labels.messageActions, icon: _jsx(IconDotsVertical, { "aria-hidden": "true" }), size: "compact", "aria-expanded": actionModeOpen, "aria-controls": actionPanelId, title: labels.messageActions, onPress: () => {
919
- setActionModeOpen((open) => {
920
- const next = !open;
921
- if (!next)
922
- setRequestIdCopied(false);
923
- return next;
924
- });
925
- } })] })) : (_jsx(IconButton, { label: copied ? labels.copied : labels.copy, icon: copied ? (_jsx(IconCircleCheck, { "aria-hidden": "true" })) : (_jsx(IconCopy, { "aria-hidden": "true" })), size: "compact", pending: copyAction.pending, onPress: () => void copyAction.execute().catch(() => undefined) })), message.createdAt ? (_jsx("time", { dateTime: message.createdAt, children: new Date(message.createdAt).toLocaleTimeString([], {
926
- hour: "numeric",
927
- minute: "2-digit",
928
- }) })) : null, actionError ? (_jsx("span", { className: "agentkit-command-error", role: "alert", children: actionError.message })) : null] }));
1151
+ forkAction.error ??
1152
+ regenerateAction.error;
1153
+ return (_jsxs("div", { className: "agentkit-message-actions", children: [message.role === "assistant" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "agentkit-message-actions-leading", children: [_jsx(IconButton, { label: copied ? labels.copied : labels.copy, icon: copied ? (_jsx(IconCircleCheck, { "aria-hidden": "true" })) : (_jsx(IconCopy, { "aria-hidden": "true" })), size: "compact", pending: copyAction.pending, onPress: () => void copyAction.execute().catch(() => undefined) }), feedbackCapability.visible ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: labels.positiveFeedback, icon: _jsx(IconThumbUp, { "aria-hidden": "true" }), size: "compact", "aria-pressed": feedback === "positive", pending: feedbackAction.pending && feedback === "positive", disabled: feedbackAction.pending || !feedbackCapability.enabled, title: feedbackCapability.reason, onPress: () => void updateFeedback("positive") }), _jsx(Popover, { open: feedbackReasonOpen, onOpenChange: (open) => {
1154
+ setFeedbackReasonOpen(open);
1155
+ if (open)
1156
+ setFeedbackReasonSubmitted(false);
1157
+ }, placement: "bottom", align: "start", className: "agentkit-feedback-popover", trigger: _jsx(IconButton, { label: labels.negativeFeedback, icon: _jsx(IconThumbDown, { "aria-hidden": "true" }), size: "compact", "aria-pressed": feedback === "negative", pending: feedbackAction.pending && feedback === "negative", disabled: feedbackAction.pending || !feedbackCapability.enabled, title: feedbackCapability.reason, onPress: () => void updateFeedback("negative") }), children: _jsxs("form", { className: "agentkit-feedback-form", onSubmit: submitFeedbackReason, children: [_jsx(TextArea, { label: labels.feedbackWhatWentWrong, value: feedbackReason, onChange: setFeedbackReason, placeholder: labels.feedbackPlaceholder, rows: 3, maxLength: 2_000, disabled: feedbackAction.pending, className: "agentkit-feedback-textarea" }), _jsxs("div", { className: "agentkit-feedback-footer", children: [_jsx("span", { children: labels.feedbackKeyboardHint.replace("{{shortcut}}", typeof navigator !== "undefined" &&
1158
+ /Mac|iPhone|iPad/.test(navigator.userAgent)
1159
+ ? "⌘"
1160
+ : "Ctrl") }), _jsx(ActionButton, { type: "submit", intent: "primary", size: "compact", pending: feedbackAction.pending, disabled: feedbackAction.pending || !feedbackReason.trim(), children: labels.feedbackSubmit })] })] }) }), feedbackReasonSubmitted ? (_jsx("span", { className: "agentkit-visually-hidden", "aria-live": "polite", children: labels.feedbackSubmitted })) : null] })) : null] }), _jsxs("div", { className: "agentkit-message-actions-trailing", children: [isLastAssistantMessage &&
1161
+ previousUserMessage &&
1162
+ forkingCapability.visible &&
1163
+ onThreadForked ? (_jsx(IconButton, { label: labels.regenerateResponse, icon: _jsx(IconRefresh, { "aria-hidden": "true" }), size: "compact", pending: regenerateAction.pending, disabled: regenerateAction.pending ||
1164
+ thread.activeRunIds.length > 0 ||
1165
+ !forkingCapability.enabled, title: forkingCapability.reason, onPress: () => void regenerateAction.execute().catch(() => undefined) })) : null, branchNavigation && branchNavigation.count > 1 ? (_jsxs("div", { className: "agentkit-branch-navigation", "aria-label": labels.branchPosition
1166
+ .replace("{{index}}", String(branchNavigation.index))
1167
+ .replace("{{count}}", String(branchNavigation.count)), children: [_jsx(IconButton, { label: labels.previousBranch, icon: _jsx(IconChevronLeft, { "aria-hidden": "true" }), size: "compact", disabled: branchNavigation.index <= 1 ||
1168
+ branchNavigationAction.pending, pending: branchNavigationAction.pending, onPress: () => void branchNavigationAction
1169
+ .execute("previous")
1170
+ .catch(() => undefined) }), _jsx("span", { "aria-live": "polite", children: labels.branchPosition
1171
+ .replace("{{index}}", String(branchNavigation.index))
1172
+ .replace("{{count}}", String(branchNavigation.count)) }), _jsx(IconButton, { label: labels.nextBranch, icon: _jsx(IconChevronRight, { "aria-hidden": "true" }), size: "compact", disabled: branchNavigation.index >= branchNavigation.count ||
1173
+ branchNavigationAction.pending, pending: branchNavigationAction.pending, onPress: () => void branchNavigationAction
1174
+ .execute("next")
1175
+ .catch(() => undefined) })] })) : null, message.createdAt ? (_jsx("time", { dateTime: message.createdAt, children: labels.formatTimestamp?.(message.createdAt) ??
1176
+ new Date(message.createdAt).toLocaleTimeString([], {
1177
+ hour: "numeric",
1178
+ minute: "2-digit",
1179
+ }) })) : null, slots.messageActionsTrailing ? (_jsx(slots.messageActionsTrailing, { value: message, threadId: threadId })) : null, _jsx(Popover, { open: actionsMenuOpen, onOpenChange: (open) => {
1180
+ setActionsMenuOpen(open);
1181
+ if (!open)
1182
+ setRequestIdCopied(false);
1183
+ }, placement: "bottom", align: "end", className: "agentkit-message-menu", trigger: _jsx(IconButton, { label: labels.messageActions, icon: _jsx(IconDotsVertical, { "aria-hidden": "true" }), size: "compact", "aria-expanded": actionsMenuOpen, title: labels.messageActions }), children: _jsxs("div", { className: "agentkit-message-menu-items", role: "menu", children: [_jsxs("button", { type: "button", role: "menuitem", className: "agentkit-message-menu-item", disabled: !requestId || requestIdAction.pending, title: requestId
1184
+ ? requestIdCopied
1185
+ ? labels.copied
1186
+ : labels.copyRequestId
1187
+ : labels.requestIdUnavailable, onClick: () => {
1188
+ setActionsMenuOpen(false);
1189
+ void requestIdAction.execute().catch(() => undefined);
1190
+ }, children: [requestIdCopied ? (_jsx(IconCircleCheck, { "aria-hidden": "true" })) : (_jsx(IconId, { "aria-hidden": "true" })), _jsx("span", { children: requestIdCopied
1191
+ ? labels.copied
1192
+ : requestId
1193
+ ? labels.copyRequestId
1194
+ : labels.requestIdUnavailable })] }), forkingCapability.visible && onThreadForked ? (_jsxs("button", { type: "button", role: "menuitem", className: "agentkit-message-menu-item", disabled: !forkingCapability.enabled || forkAction.pending, title: forkingCapability.reason, onClick: () => {
1195
+ setActionsMenuOpen(false);
1196
+ void forkAction.execute().catch(() => undefined);
1197
+ }, children: [_jsx(IconGitBranch, { "aria-hidden": "true" }), _jsx("span", { children: labels.fork })] })) : null] }) })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "agentkit-message-actions-leading", children: [_jsx(IconButton, { label: copied ? labels.copied : labels.copy, icon: copied ? (_jsx(IconCircleCheck, { "aria-hidden": "true" })) : (_jsx(IconCopy, { "aria-hidden": "true" })), size: "compact", pending: copyAction.pending, onPress: () => void copyAction.execute().catch(() => undefined) }), editContext?.enabled &&
1198
+ forkingCapability.visible &&
1199
+ text.trim() ? (_jsx(IconButton, { label: labels.editMessage, icon: _jsx(IconPencil, { "aria-hidden": "true" }), size: "compact", disabled: thread.activeRunIds.length > 0 || !forkingCapability.enabled, title: forkingCapability.reason, "aria-pressed": editContext.message?.id === message.id, onPress: () => editContext.setMessage(message) })) : null] }), _jsx("div", { className: "agentkit-message-actions-trailing", children: message.createdAt ? (_jsx("time", { dateTime: message.createdAt, children: labels.formatTimestamp?.(message.createdAt) ??
1200
+ new Date(message.createdAt).toLocaleTimeString([], {
1201
+ hour: "numeric",
1202
+ minute: "2-digit",
1203
+ }) })) : null })] })), actionError ? (_jsx("span", { className: "agentkit-command-error", role: "alert", children: actionError.message })) : null] }));
929
1204
  }
930
1205
  export function AgentMessageView({ value: message, threadId, }) {
931
1206
  const { labels, slots } = useAgentKit();
@@ -938,7 +1213,7 @@ export function AgentMessageView({ value: message, threadId, }) {
938
1213
  !embeddedWidgetIds.has(widget.id));
939
1214
  const attachedAnnotations = Object.values(thread.annotations).filter((annotation) => thread.annotationMessageIds[annotation.id] === message.id &&
940
1215
  !embeddedAnnotationIds.has(annotation.id));
941
- return (_jsxs("article", { className: "agentkit-message", "data-role": message.role, "data-message-id": message.id, "aria-label": message.role === "user" ? labels.you : labels.assistant, "aria-busy": message.status === "streaming", children: [_jsxs("div", { className: "agentkit-message-content", children: [message.parts.map((part, index) => (_jsx(AgentMessagePartView, { value: part, threadId: threadId, active: message.status === "streaming", resetKey: `${threadId}:${message.id}:${index}` }, `${message.id}-${index}`))), attachedWidgets.map((widget) => (_jsx(AgentMessagePartView, { value: { type: "widget", widget }, threadId: threadId }, widget.id))), attachedAnnotations.length ? (_jsx("div", { className: "agentkit-annotations", children: attachedAnnotations.map((annotation) => (_jsx(AgentMessagePartView, { value: { type: "annotation", annotation }, threadId: threadId }, annotation.id))) })) : null, Supplement ? _jsx(Supplement, { value: message, threadId: threadId }) : null] }), _jsx(Actions, { value: message, threadId: threadId })] }));
1216
+ return (_jsxs("article", { className: "agentkit-message", "data-role": message.role, "data-message-id": message.id, "aria-label": message.role === "user" ? labels.you : labels.assistant, "aria-busy": message.status === "streaming", children: [_jsxs("div", { className: "agentkit-message-content", children: [message.parts.map((part, index) => (_jsx(AgentMessagePartView, { value: part, threadId: threadId, active: message.status === "streaming", resetKey: `${threadId}:${message.id}:${index}`, userMessage: message.role === "user" }, `${message.id}-${index}`))), attachedWidgets.map((widget) => (_jsx(AgentMessagePartView, { value: { type: "widget", widget }, threadId: threadId }, widget.id))), attachedAnnotations.length ? (_jsx("div", { className: "agentkit-annotations", children: attachedAnnotations.map((annotation) => (_jsx(AgentMessagePartView, { value: { type: "annotation", annotation }, threadId: threadId }, annotation.id))) })) : null, Supplement ? _jsx(Supplement, { value: message, threadId: threadId }) : null] }), _jsx(Actions, { value: message, threadId: threadId })] }));
942
1217
  }
943
1218
  export function AgentRunFailure({ error, runId, threadId, }) {
944
1219
  const { labels } = useAgentKit();
@@ -948,15 +1223,20 @@ export function AgentConnectionErrorView({ error, threadId, recover, recovering,
948
1223
  const { labels } = useAgentKit();
949
1224
  return (_jsxs("div", { className: "agentkit-error", "data-error-code": error.code, "data-thread-id": threadId, role: "alert", children: [_jsx("strong", { children: labels.error }), _jsx("span", { children: error.message }), error.retryable ? (_jsx("div", { className: "agentkit-error-actions", children: _jsx(ActionButton, { emphasis: "outline", size: "compact", pending: recovering, onPress: () => void recover().catch(() => undefined), children: labels.reconnect }) })) : null, recoveryError ? (_jsx("span", { className: "agentkit-command-error", children: recoveryError.message })) : null] }));
950
1225
  }
951
- export function AgentKitComposer({ threadId: requestedThreadId, className, queueWhileRunning = true, showModelSelector = true, slashCommands, slashSkills, includeDefaultSlashCommands, includeDefaultSlashSkills, onSlashCommand, plusMenuMode, voiceEnabled = false, autoFocus = true, mode, defaultMode = "act", onModeChange, contextItems, onRemoveContextItem, onInspectContextItem, onRetryContextItem, contextMenuItems, attachmentAdapter, inlineTextAttachments, beforeSend, }) {
952
- const { threadId: contextThreadId, labels, registerComposerFocus, slots, } = useAgentKit();
1226
+ export function AgentKitComposer({ threadId: requestedThreadId, className, queueWhileRunning = true, showModelSelector = true, slashCommands, slashSkills, includeDefaultSlashCommands, includeDefaultSlashSkills, onSlashCommand, plusMenuMode, voiceEnabled = false, autoFocus = true, mode, defaultMode = "act", onModeChange, toolbarSlot, disabled, onDisabledClick, placeholder, initialText, initialTextKey, layoutVariant, availableModels, modelListLoading, selectedModel, selectedEngine, selectedEffort, onConnectProvider, onConnectLocalRuntime, imageModelMenu, onModelChange, onEffortChange, availableAgents, selectedAgent, agentOnly, onAgentChange, onModelSelectorOpenChange, modelStatusChecksEnabled, attachmentsEnabled, onAttachmentRequest, contextButtonTooltipDisabled, onTextChange, contextItems, onRemoveContextItem, extraActionButton, onSubmit: onSubmitOverride, onBeforeSubmit, onAttachmentError, interceptBuildRequestsForBuilder, planModeDisabled, planModeDisabledReason, stopButton, onInspectContextItem, onRetryContextItem, contextMenuItems, attachmentAdapter, inlineTextAttachments, beforeSend, }) {
1227
+ const { threadId: contextThreadId, controller, labels, onThreadForked, registerComposerFocus, slots, } = useAgentKit();
1228
+ const editContext = useContext(AgentMessageEditContext);
1229
+ const editingMessage = editContext?.message ?? null;
953
1230
  const threadId = requestedThreadId ?? contextThreadId;
954
1231
  const queueCapability = useAgentCapability("messageQueue");
955
1232
  const suggestionsCapability = useAgentCapability("suggestions");
956
1233
  const uploadsCapability = useAgentCapability("uploads");
957
1234
  const modelSelectionCapability = useAgentCapability("modelSelection");
958
1235
  const canQueue = queueCapability.enabled;
959
- const canUpload = uploadsCapability.enabled;
1236
+ const canUpload = uploadsCapability.enabled && attachmentsEnabled !== false;
1237
+ // The host opts in through showModelSelector, so a capability the backend
1238
+ // never reported keeps the selector instead of silently removing a control
1239
+ // the host asked for. Only an explicit denial or outage takes it away.
960
1240
  const canSelectModel = modelSelectionCapability.state === "unknown" ||
961
1241
  modelSelectionCapability.enabled;
962
1242
  const control = useAgentKitControl(threadId);
@@ -964,8 +1244,15 @@ export function AgentKitComposer({ threadId: requestedThreadId, className, queue
964
1244
  const command = useAgentKitMutation((execute) => execute(), threadId);
965
1245
  const composerRef = useRef(null);
966
1246
  const [uncontrolledMode, setUncontrolledMode] = useState(defaultMode);
1247
+ const [attachmentError, setAttachmentError] = useState(null);
967
1248
  const executionMode = mode ?? uncontrolledMode;
968
1249
  const active = thread.activeRunIds.length > 0;
1250
+ const composerInitialText = editingMessage
1251
+ ? messageText(editingMessage)
1252
+ : initialText;
1253
+ const composerInitialTextKey = editingMessage
1254
+ ? `edit:${editingMessage.id}`
1255
+ : initialTextKey;
969
1256
  const focusComposer = useCallback(() => {
970
1257
  if (!autoFocus)
971
1258
  return;
@@ -974,84 +1261,224 @@ export function AgentKitComposer({ threadId: requestedThreadId, className, queue
974
1261
  globalThis.requestAnimationFrame(() => composerRef.current?.focus());
975
1262
  }
976
1263
  }, [autoFocus]);
1264
+ const reportAttachmentError = useCallback((message) => {
1265
+ if (onAttachmentError)
1266
+ onAttachmentError(message);
1267
+ else
1268
+ setAttachmentError(message);
1269
+ }, [onAttachmentError]);
1270
+ const addDroppedAttachment = useCallback(async (file) => {
1271
+ try {
1272
+ const composer = composerRef.current;
1273
+ if (!composer)
1274
+ throw new Error(labels.error);
1275
+ await composer.addAttachment(file);
1276
+ setAttachmentError(null);
1277
+ }
1278
+ catch (error) {
1279
+ const message = error instanceof Error ? error.message : labels.error;
1280
+ reportAttachmentError(message);
1281
+ throw error;
1282
+ }
1283
+ }, [labels.error, reportAttachmentError]);
1284
+ useEffect(() => {
1285
+ const receiveAttachments = (event) => {
1286
+ const detail = event.detail;
1287
+ if (detail?.threadId !== threadId || !canUpload)
1288
+ return;
1289
+ void Promise.all(detail.files.map((file) => addDroppedAttachment(file))).catch(() => undefined);
1290
+ };
1291
+ window.addEventListener("agentkit:attach-files", receiveAttachments);
1292
+ return () => window.removeEventListener("agentkit:attach-files", receiveAttachments);
1293
+ }, [addDroppedAttachment, canUpload, threadId]);
1294
+ const composerToolbarSlot = editingMessage ? (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: labels.cancelEditing, icon: _jsx(IconX, { "aria-hidden": "true" }), size: "compact", onPress: () => {
1295
+ composerRef.current?.setText(initialText ?? "");
1296
+ editContext?.setMessage(null);
1297
+ focusComposer();
1298
+ } }), toolbarSlot] })) : (toolbarSlot);
977
1299
  useEffect(() => registerComposerFocus(threadId, focusComposer), [focusComposer, registerComposerFocus, threadId]);
978
- const submitPrompt = async (text, files, references, options) => {
979
- const submission = command.execute(async () => {
980
- const effort = options.effort;
1300
+ // i18n-ignore: These type-level callback property names are not visible copy.
1301
+ const submitMessage = async (text, files, references, // i18n-ignore: Type-only callback tuple, not rendered copy.
1302
+ options) => {
1303
+ const effort = options.effort;
1304
+ const contextItemsForSubmission = options.contextItems ?? contextItems;
1305
+ const metadata = {
1306
+ ...(editingMessage?.metadata ?? {}),
1307
+ ...(contextItemsForSubmission === undefined
1308
+ ? {}
1309
+ : { contextItems: contextItemsForSubmission }),
1310
+ ...(references.length ? { references } : {}),
1311
+ ...(options.engine ? { engine: options.engine } : {}),
1312
+ ...(options.model ? { model: options.model } : {}),
1313
+ ...(effort ? { effort } : {}),
1314
+ ...(selectedAgent ? { agentId: selectedAgent } : {}),
1315
+ mode: executionMode,
1316
+ requestMode: executionMode,
1317
+ };
1318
+ const runOptions = {
1319
+ ...(selectedAgent ? { agentId: selectedAgent } : {}),
1320
+ model: options.model,
1321
+ mode: executionMode,
1322
+ reasoningEffort: effort && !["auto", "max"].includes(effort)
1323
+ ? effort
1324
+ : undefined,
1325
+ metadata,
1326
+ };
1327
+ const uploadFiles = files.map((file) => ({
1328
+ name: file.name,
1329
+ mediaType: file.type || "application/octet-stream",
1330
+ size: file.size,
1331
+ body: file,
1332
+ }));
1333
+ if (editingMessage) {
1334
+ if (!onThreadForked) {
1335
+ throw new Error(labels.navigationUnavailable);
1336
+ }
1337
+ const previousMessage = findPreviousMessage(thread.messages, editingMessage.id);
1338
+ if (previousMessage === null) {
1339
+ throw new Error(labels.messageUnavailable);
1340
+ }
1341
+ const forkedThread = await controller.forkThread(threadId, previousMessage?.id);
1342
+ onThreadForked(forkedThread);
1343
+ const uploadedAttachments = uploadFiles.length && canUpload
1344
+ ? await controller.uploadFiles(forkedThread.id, uploadFiles)
1345
+ : [];
1346
+ const previousAttachments = editingMessage.parts.filter((part) => part.type === "file");
981
1347
  const draft = createAgentKitComposerSubmission({
982
- threadId,
983
- intent: canQueue &&
984
- (options.intent === "queued" || (active && queueWhileRunning))
985
- ? "queued"
986
- : "immediate",
1348
+ threadId: forkedThread.id,
1349
+ intent: "immediate",
987
1350
  text,
988
- contextItems: options.contextItems,
1351
+ contextItems: contextItemsForSubmission,
989
1352
  references,
990
- options: {
991
- model: options.model,
992
- mode: executionMode,
993
- reasoningEffort: effort && !["auto", "max"].includes(effort)
994
- ? effort
995
- : undefined,
996
- },
1353
+ options: runOptions,
997
1354
  });
998
- const attachments = files.length && canUpload
999
- ? await control.uploadFiles(files.map((file) => ({
1000
- name: file.name,
1001
- mediaType: file.type || "application/octet-stream",
1002
- size: file.size,
1003
- body: file,
1004
- })))
1005
- : [];
1006
1355
  const payload = Object.freeze({
1007
1356
  ...draft,
1008
- attachments: snapshotComposerValue(attachments),
1357
+ attachments: snapshotComposerValue([
1358
+ ...previousAttachments,
1359
+ ...uploadedAttachments,
1360
+ ]),
1009
1361
  });
1010
1362
  await beforeSend?.(payload);
1011
- const metadata = payload.references.length || payload.contextItems !== undefined
1012
- ? {
1013
- ...(payload.references.length
1014
- ? { references: payload.references }
1015
- : {}),
1016
- ...(payload.contextItems === undefined
1017
- ? {}
1018
- : { contextItems: payload.contextItems }),
1019
- }
1020
- : undefined;
1021
- const message = {
1363
+ const sendMetadata = {
1364
+ ...payload.options.metadata,
1365
+ ...(payload.references.length
1366
+ ? { references: payload.references }
1367
+ : {}),
1368
+ ...(payload.contextItems === undefined
1369
+ ? {}
1370
+ : { contextItems: payload.contextItems }),
1371
+ };
1372
+ await controller.sendMessage({
1373
+ threadId: forkedThread.id,
1022
1374
  text: payload.text,
1023
1375
  attachments: [...payload.attachments],
1024
1376
  options: payload.options,
1025
- metadata,
1026
- };
1027
- if (payload.intent === "queued") {
1028
- await control.queueMessage(message);
1029
- }
1030
- else {
1031
- await control.sendMessage(message);
1032
- }
1377
+ metadata: sendMetadata,
1378
+ });
1379
+ editContext?.setMessage(null);
1380
+ return;
1381
+ }
1382
+ if (onSubmitOverride) {
1383
+ await onSubmitOverride(text, files, references, options);
1384
+ return;
1385
+ }
1386
+ const draft = createAgentKitComposerSubmission({
1387
+ threadId,
1388
+ intent: canQueue &&
1389
+ (options.intent === "queued" || (active && queueWhileRunning))
1390
+ ? "queued"
1391
+ : "immediate",
1392
+ text,
1393
+ contextItems: contextItemsForSubmission,
1394
+ references,
1395
+ options: runOptions,
1396
+ });
1397
+ const attachments = uploadFiles.length && canUpload
1398
+ ? await control.uploadFiles(uploadFiles)
1399
+ : [];
1400
+ const payload = Object.freeze({
1401
+ ...draft,
1402
+ attachments: snapshotComposerValue(attachments),
1033
1403
  });
1034
- focusComposer();
1035
- await submission.finally(focusComposer);
1404
+ await beforeSend?.(payload);
1405
+ const sendMetadata = {
1406
+ ...payload.options.metadata,
1407
+ ...(payload.references.length ? { references: payload.references } : {}),
1408
+ ...(payload.contextItems === undefined
1409
+ ? {}
1410
+ : { contextItems: payload.contextItems }),
1411
+ };
1412
+ const message = {
1413
+ text: payload.text,
1414
+ attachments: [...payload.attachments],
1415
+ options: payload.options,
1416
+ metadata: sendMetadata,
1417
+ };
1418
+ if (payload.intent === "queued") {
1419
+ await control.queueMessage(message);
1420
+ }
1421
+ else {
1422
+ await control.sendMessage(message);
1423
+ }
1424
+ };
1425
+ const prepareHostSubmit = async () => {
1426
+ if (disabled) {
1427
+ onDisabledClick?.();
1428
+ return false;
1429
+ }
1430
+ if (editingMessage)
1431
+ return true;
1432
+ return !onBeforeSubmit || (await onBeforeSubmit());
1036
1433
  };
1037
- const steerQueued = !active
1434
+ const steerQueued = !disabled
1038
1435
  ? (item) => void command
1039
- .execute(() => control.steerQueued(item.id))
1436
+ .execute(async () => {
1437
+ if (!(await prepareHostSubmit()))
1438
+ return;
1439
+ if (onSubmitOverride) {
1440
+ await submitMessage(item.text, [], [], {
1441
+ intent: "immediate",
1442
+ attachments: item.attachments,
1443
+ });
1444
+ await control.removeQueued(item.id);
1445
+ }
1446
+ else {
1447
+ await control.steerQueued(item.id);
1448
+ }
1449
+ })
1040
1450
  .catch(() => undefined)
1041
1451
  .finally(focusComposer)
1042
1452
  : undefined;
1453
+ const supportsQueueReordering = controller.supportsQueuedMessageReordering?.() ?? false;
1454
+ const moveQueuedToTop = useAgentKitMutation((messageId) => control.moveQueuedMessageToTop(messageId), `${threadId}:move-queued-to-top`);
1043
1455
  const removeQueued = (item) => void command
1044
1456
  .execute(() => control.removeQueued(item.id))
1045
1457
  .catch(() => undefined)
1046
1458
  .finally(focusComposer);
1047
- const selectSuggestion = (suggestion) => void submitPrompt(agentSuggestionPrompt(suggestion), [], [], {
1048
- contextItems,
1459
+ const selectSuggestion = (suggestion) => void command
1460
+ .execute(async () => {
1461
+ if (!(await prepareHostSubmit()))
1462
+ return;
1463
+ await submitMessage(agentSuggestionPrompt(suggestion), [], [], {
1464
+ intent: active && queueWhileRunning && canQueue ? "queued" : "immediate",
1465
+ contextItems,
1466
+ });
1049
1467
  })
1050
1468
  .catch(() => undefined)
1051
1469
  .finally(focusComposer);
1052
1470
  const Queue = slots.queue;
1053
1471
  const Suggestions = slots.suggestions;
1054
- return (_jsxs("div", { className: `agentkit-composer-stack ${className ?? ""}`, children: [queueCapability.visible ? (Queue ? (_jsx(Queue, { items: thread.queuedMessages, threadId: threadId, active: active, pending: command.pending, onSteer: steerQueued, onRemove: removeQueued })) : (_jsx(MessageQueueDrawer, { variant: "recessed", items: thread.queuedMessages, disabled: command.pending, onSteer: steerQueued
1472
+ return (_jsxs("div", { className: `agentkit-composer-stack ${className ?? ""}`, children: [queueCapability.visible ? (Queue ? (_jsx(Queue, { items: thread.queuedMessages, threadId: threadId, active: active, pending: command.pending || Boolean(disabled), onSteer: steerQueued, onRemove: removeQueued })) : (_jsx(MessageQueueDrawer, { variant: "recessed", items: thread.queuedMessages.map((message) => ({
1473
+ id: message.id,
1474
+ text: message.text,
1475
+ images: (message.attachments ?? []).flatMap((attachment) => {
1476
+ if (!attachment.mediaType?.startsWith("image/"))
1477
+ return [];
1478
+ const src = safeAgentImageSrc(attachment.url);
1479
+ return src ? [src] : [];
1480
+ }),
1481
+ })), disabled: command.pending || Boolean(disabled), onSteer: steerQueued
1055
1482
  ? (item) => {
1056
1483
  const message = thread.queuedMessages.find((candidate) => candidate.id === item.id);
1057
1484
  if (message)
@@ -1067,15 +1494,42 @@ export function AgentKitComposer({ threadId: requestedThreadId, className, queue
1067
1494
  steerHint: labels.queueSteerHint,
1068
1495
  remove: labels.queueRemove,
1069
1496
  moreActions: labels.queueMore,
1070
- } }))) : null, suggestionsCapability.visible && thread.suggestions.length ? (Suggestions ? (_jsx(Suggestions, { suggestions: thread.suggestions, threadId: threadId, pending: command.pending, onSelect: selectSuggestion })) : (_jsx(AgentSuggestionBar, { suggestions: thread.suggestions.map((suggestion) => ({
1497
+ }, getItemActions: supportsQueueReordering
1498
+ ? (item) => [
1499
+ {
1500
+ id: "move-to-top",
1501
+ label: labels.queueMoveToTop,
1502
+ icon: _jsx(IconArrowUp, { "aria-hidden": "true", size: 14 }),
1503
+ disabled: item.id === thread.queuedMessages[0]?.id ||
1504
+ moveQueuedToTop.pending,
1505
+ onSelect: (selected) => void command
1506
+ .execute(() => moveQueuedToTop.execute(selected.id))
1507
+ .catch(() => undefined),
1508
+ },
1509
+ ]
1510
+ : undefined }))) : null, suggestionsCapability.visible && thread.suggestions.length ? (Suggestions ? (_jsx(Suggestions, { suggestions: thread.suggestions, threadId: threadId, pending: command.pending, onSelect: selectSuggestion })) : (_jsx(AgentSuggestionBar, { suggestions: thread.suggestions.map((suggestion) => ({
1071
1511
  ...suggestion,
1072
- disabled: command.pending,
1073
- })), ariaLabel: labels.suggestions, onSelect: selectSuggestion, className: "agentkit-suggestions" }))) : null, _jsx(PromptComposer, { contextItems: contextItems, onRemoveContextItem: onRemoveContextItem, onInspectContextItem: onInspectContextItem, onRetryContextItem: onRetryContextItem, contextMenuItems: contextMenuItems, attachmentAdapter: attachmentAdapter, inlineTextAttachments: inlineTextAttachments, rootClassName: "agentkit-composer", layoutVariant: "default", draftScope: `agentkit:${threadId}`, ariaLabel: labels.composerLabel, placeholder: labels.composerPlaceholder, autoFocus: autoFocus, composerRef: composerRef, submitting: command.pending, willQueue: active && queueWhileRunning && canQueue, showModelSelector: showModelSelector && canSelectModel, attachmentsEnabled: canUpload, slashCommands: slashCommands, slashSkills: slashSkills, includeDefaultSlashCommands: includeDefaultSlashCommands ?? false, includeDefaultSlashSkills: includeDefaultSlashSkills ?? false, onSlashCommand: onSlashCommand, plusMenuMode: plusMenuMode ?? (canUpload ? "upload-only" : "hidden"), voiceEnabled: voiceEnabled, execMode: executionMode === "plan" ? "plan" : "build", onExecModeChange: (nextMode) => {
1512
+ disabled: command.pending || Boolean(disabled),
1513
+ })), ariaLabel: labels.suggestions, onSelect: selectSuggestion, className: "agentkit-suggestions" }))) : null, _jsx(PromptComposer, { contextItems: contextItems, onRemoveContextItem: onRemoveContextItem, onInspectContextItem: onInspectContextItem, onRetryContextItem: onRetryContextItem, contextMenuItems: contextMenuItems, attachmentAdapter: attachmentAdapter, inlineTextAttachments: inlineTextAttachments, rootClassName: "agentkit-composer", draftScope: `agentkit:${threadId}${editingMessage ? `:edit:${editingMessage.id}` : ""}`, ariaLabel: labels.composerLabel, placeholder: placeholder ?? labels.composerPlaceholder, disabled: disabled, onDisabledClick: onDisabledClick, onConnectProvider: onConnectProvider, onConnectLocalRuntime: onConnectLocalRuntime, imageModelMenu: imageModelMenu, autoFocus: autoFocus, composerRef: composerRef, submitting: command.pending, willQueue: active && queueWhileRunning && canQueue, showModelSelector: showModelSelector && canSelectModel, availableModels: availableModels, modelListLoading: modelListLoading, selectedModel: selectedModel, selectedEngine: selectedEngine, selectedEffort: selectedEffort, onModelChange: onModelChange, onEffortChange: onEffortChange, availableAgents: availableAgents, selectedAgent: selectedAgent, agentOnly: agentOnly, onAgentChange: onAgentChange, onModelSelectorOpenChange: onModelSelectorOpenChange, modelStatusChecksEnabled: modelStatusChecksEnabled, layoutVariant: layoutVariant, toolbarSlot: composerToolbarSlot, initialText: composerInitialText, initialTextKey: composerInitialTextKey, onTextChange: onTextChange, extraActionButton: extraActionButton, onBeforeSubmit: onBeforeSubmit, onAttachmentError: reportAttachmentError, interceptBuildRequestsForBuilder: interceptBuildRequestsForBuilder, planModeDisabled: planModeDisabled, planModeDisabledReason: planModeDisabledReason, stopButton: stopButton, attachmentsEnabled: canUpload, onAttachmentRequest: onAttachmentRequest, contextButtonTooltipDisabled: contextButtonTooltipDisabled, slashCommands: slashCommands, slashSkills: slashSkills, includeDefaultSlashCommands: includeDefaultSlashCommands ?? false, includeDefaultSlashSkills: includeDefaultSlashSkills ?? false, onSlashCommand: onSlashCommand, plusMenuMode: plusMenuMode ?? "full", voiceEnabled: voiceEnabled, execMode: executionMode === "plan" ? "plan" : "build", onExecModeChange: (nextMode) => {
1074
1514
  const next = nextMode === "plan" ? "plan" : "act";
1075
1515
  if (mode === undefined)
1076
1516
  setUncontrolledMode(next);
1077
1517
  onModeChange?.(next);
1078
- }, onSubmit: submitPrompt }), command.error ? (_jsxs("div", { className: "agentkit-composer-error", role: "alert", children: [_jsx(IconAlertCircle, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { children: command.error.message })] })) : null] }));
1518
+ }, onSubmit: async (text, files, references, options) => {
1519
+ if (disabled) {
1520
+ onDisabledClick?.();
1521
+ return;
1522
+ }
1523
+ focusComposer();
1524
+ try {
1525
+ await command.execute(async () => {
1526
+ await submitMessage(text, files, references, options);
1527
+ });
1528
+ }
1529
+ finally {
1530
+ focusComposer();
1531
+ }
1532
+ } }), command.error ? (_jsxs("div", { className: "agentkit-composer-error", role: "alert", children: [_jsx(IconAlertCircle, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { children: command.error.message })] })) : null, attachmentError ? (_jsxs("div", { className: "agentkit-composer-error", role: "alert", children: [_jsx(IconAlertCircle, { "aria-hidden": "true", className: "agentkit-icon" }), _jsx("span", { children: attachmentError })] })) : null] }));
1079
1533
  }
1080
1534
  const AGENTKIT_TRANSCRIPT_BOTTOM_THRESHOLD = 24;
1081
1535
  const AGENTKIT_TRANSCRIPT_SCROLLBAR_IDLE_MS = 900;
@@ -1092,11 +1546,23 @@ export function isAgentKitTranscriptNearBottom(element, threshold = AGENTKIT_TRA
1092
1546
  return (element.scrollHeight - element.clientHeight - element.scrollTop <= threshold);
1093
1547
  }
1094
1548
  export function AgentKitChat({ title, toolbar, composer = true, composerProps, emptyComposerPlacement = "center", autoScroll = true, className, }) {
1095
- const { threadId, slots, labels } = useAgentKit();
1549
+ const { threadId, slots, labels, onThreadForked } = useAgentKit();
1096
1550
  const connection = useAgentConnection();
1551
+ const uploadsCapability = useAgentCapability("uploads");
1097
1552
  const control = useAgentKitControl(threadId);
1098
1553
  const recovery = useAgentKitMutation(() => control.load(), threadId);
1099
1554
  const thread = useAgentThread(threadId);
1555
+ const [pendingEdit, setPendingEdit] = useState(null);
1556
+ const editingMessage = pendingEdit?.threadId === threadId ? pendingEdit.message : null;
1557
+ const setEditingMessage = useCallback((message) => {
1558
+ setPendingEdit(message ? { threadId, message } : null);
1559
+ }, [threadId]);
1560
+ const messageEditContext = {
1561
+ enabled: composer && Boolean(onThreadForked),
1562
+ message: editingMessage,
1563
+ setMessage: setEditingMessage,
1564
+ };
1565
+ useEffect(() => setPendingEdit(null), [threadId]);
1100
1566
  const hasConversation = thread.messages.length > 0;
1101
1567
  const Message = slots.message ?? AgentMessageView;
1102
1568
  const EmptyState = slots.emptyState;
@@ -1113,6 +1579,7 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1113
1579
  const Footer = slots.footer;
1114
1580
  const transcriptRef = useRef(null);
1115
1581
  const transcriptContentRef = useRef(null);
1582
+ const fileDragDepthRef = useRef(0);
1116
1583
  const followsTranscriptRef = useRef(true);
1117
1584
  const transcriptThreadIdRef = useRef(threadId);
1118
1585
  const previousLastMessageIdRef = useRef(undefined);
@@ -1120,9 +1587,59 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1120
1587
  const transcriptScrollbarInteractionRef = useRef(false);
1121
1588
  const transcriptScrollbarTimeoutRef = useRef(undefined);
1122
1589
  const [transcriptScrollbarVisible, setTranscriptScrollbarVisible] = useState(false);
1590
+ const [transcriptAwayFromBottom, setTranscriptAwayFromBottom] = useState(false);
1591
+ const [fileDragActive, setFileDragActive] = useState(false);
1123
1592
  const lastMessage = thread.messages.at(-1);
1124
1593
  const lastEventId = thread.events.at(-1)?.id;
1125
1594
  const connectionError = connection.status === "error" ? connection.error : undefined;
1595
+ const canDropFiles = composer &&
1596
+ uploadsCapability.enabled &&
1597
+ composerProps?.attachmentsEnabled !== false;
1598
+ const hasFiles = (event) => Array.from(event.dataTransfer.types).includes("Files");
1599
+ const handleFileDragEnter = (event) => {
1600
+ if (!canDropFiles || event.defaultPrevented || !hasFiles(event))
1601
+ return;
1602
+ if (event.relatedTarget instanceof Node &&
1603
+ event.currentTarget.contains(event.relatedTarget)) {
1604
+ return;
1605
+ }
1606
+ event.preventDefault();
1607
+ fileDragDepthRef.current += 1;
1608
+ setFileDragActive(true);
1609
+ };
1610
+ const handleFileDragOver = (event) => {
1611
+ if (!canDropFiles || event.defaultPrevented || !hasFiles(event))
1612
+ return;
1613
+ event.preventDefault();
1614
+ event.dataTransfer.dropEffect = "copy";
1615
+ setFileDragActive(true);
1616
+ };
1617
+ const handleFileDragLeave = (event) => {
1618
+ if (!canDropFiles || !hasFiles(event))
1619
+ return;
1620
+ if (event.relatedTarget instanceof Node &&
1621
+ event.currentTarget.contains(event.relatedTarget)) {
1622
+ return;
1623
+ }
1624
+ fileDragDepthRef.current = Math.max(0, fileDragDepthRef.current - 1);
1625
+ setFileDragActive(fileDragDepthRef.current > 0);
1626
+ };
1627
+ const handleFileDrop = (event) => {
1628
+ if (!canDropFiles || !hasFiles(event))
1629
+ return;
1630
+ fileDragDepthRef.current = 0;
1631
+ setFileDragActive(false);
1632
+ if (event.defaultPrevented)
1633
+ return;
1634
+ event.preventDefault();
1635
+ event.stopPropagation();
1636
+ const files = Array.from(event.dataTransfer.files);
1637
+ if (!files.length)
1638
+ return;
1639
+ window.dispatchEvent(new CustomEvent("agentkit:attach-files", {
1640
+ detail: { threadId, files },
1641
+ }));
1642
+ };
1126
1643
  const clearTranscriptScrollbarTimeout = useCallback(() => {
1127
1644
  if (transcriptScrollbarTimeoutRef.current === undefined)
1128
1645
  return;
@@ -1159,6 +1676,15 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1159
1676
  return;
1160
1677
  transcript.scrollTop = nextScrollTop;
1161
1678
  }, [autoScroll]);
1679
+ const returnTranscriptToBottom = useCallback(() => {
1680
+ const transcript = transcriptRef.current;
1681
+ if (!transcript)
1682
+ return;
1683
+ followsTranscriptRef.current = true;
1684
+ const nextScrollTop = Math.max(0, transcript.scrollHeight - transcript.clientHeight);
1685
+ transcript.scrollTop = nextScrollTop;
1686
+ setTranscriptAwayFromBottom(false);
1687
+ }, []);
1162
1688
  const cancelScheduledTranscriptFollow = useCallback(() => {
1163
1689
  if (transcriptFollowFrameRef.current === undefined)
1164
1690
  return;
@@ -1188,6 +1714,7 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1188
1714
  transcriptThreadIdRef.current = threadId;
1189
1715
  previousLastMessageIdRef.current = undefined;
1190
1716
  followsTranscriptRef.current = true;
1717
+ setTranscriptAwayFromBottom(false);
1191
1718
  hideTranscriptScrollbar();
1192
1719
  }
1193
1720
  const previousLastMessageId = previousLastMessageIdRef.current;
@@ -1235,13 +1762,15 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1235
1762
  const transcript = transcriptRef.current;
1236
1763
  if (!transcript)
1237
1764
  return;
1238
- if (autoScroll && transcriptScrollbarInteractionRef.current) {
1239
- followsTranscriptRef.current = isAgentKitTranscriptNearBottom(transcript);
1765
+ const nearBottom = isAgentKitTranscriptNearBottom(transcript);
1766
+ if (nearBottom || transcriptScrollbarInteractionRef.current) {
1767
+ followsTranscriptRef.current = nearBottom;
1240
1768
  }
1769
+ setTranscriptAwayFromBottom(!nearBottom);
1241
1770
  if (transcriptScrollbarInteractionRef.current) {
1242
1771
  scheduleTranscriptScrollbarHide();
1243
1772
  }
1244
- }, [autoScroll, scheduleTranscriptScrollbarHide]);
1773
+ }, [scheduleTranscriptScrollbarHide]);
1245
1774
  const handleTranscriptPointerDown = useCallback((event) => {
1246
1775
  const bounds = event.currentTarget.getBoundingClientRect();
1247
1776
  const edgeDistance = Math.min(Math.abs(event.clientX - bounds.left), Math.abs(bounds.right - event.clientX));
@@ -1267,7 +1796,8 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1267
1796
  return result;
1268
1797
  }, [thread.events]);
1269
1798
  const messageBoundarySequences = useMemo(() => {
1270
- const result = new Map();
1799
+ const firstVisibleSequence = new Map();
1800
+ const lastTextDeltaSequence = new Map();
1271
1801
  const assistantMessageIds = new Set(thread.messages
1272
1802
  .filter((message) => message.role === "assistant")
1273
1803
  .map((message) => message.id));
@@ -1282,7 +1812,16 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1282
1812
  : undefined;
1283
1813
  if (!messageId)
1284
1814
  continue;
1285
- result.set(messageId, Math.min(result.get(messageId) ?? Infinity, event.sequence));
1815
+ if (!firstVisibleSequence.has(messageId)) {
1816
+ firstVisibleSequence.set(messageId, event.sequence);
1817
+ }
1818
+ if (event.type === "message.delta") {
1819
+ lastTextDeltaSequence.set(messageId, event.sequence);
1820
+ }
1821
+ }
1822
+ const result = new Map();
1823
+ for (const [messageId, sequence] of firstVisibleSequence) {
1824
+ result.set(messageId, lastTextDeltaSequence.get(messageId) ?? sequence);
1286
1825
  }
1287
1826
  return result;
1288
1827
  }, [thread.events, thread.messages]);
@@ -1370,6 +1909,6 @@ export function AgentKitChat({ title, toolbar, composer = true, composerProps, e
1370
1909
  }
1371
1910
  transcriptItems.push(_jsx(AgentKitSurfaceBoundary, { surface: "activity", resetKey: `${runId}:${thread.events.length}`, children: renderRunFailure(runId) }, `run-failure:${runId}`));
1372
1911
  }
1373
- return (_jsxs("section", { "aria-label": typeof title === "string" ? title : labels.conversation, className: `agentkit-chat ${className ?? ""}`, "data-empty": !hasConversation, "data-empty-composer-placement": emptyComposerPlacement, children: [hasConversation && (title || toolbar) ? (_jsx(AgentKitSurfaceBoundary, { surface: "header", resetKey: threadId, children: _jsx(AgentKitRegionSlot, { slot: Header, threadId: threadId, children: _jsxs("header", { className: "agentkit-chat-header", children: [_jsx("div", { className: "agentkit-chat-title", children: title }), _jsx(AgentKitRegionSlot, { slot: Toolbar, threadId: threadId, children: _jsx("div", { className: "agentkit-chat-toolbar", children: toolbar }) })] }) }) })) : null, _jsx("div", { ref: transcriptRef, className: "agentkit-transcript", "aria-live": "off", "data-scrollbar-visible": transcriptScrollbarVisible, onKeyDown: handleTranscriptKeyDown, onPointerDown: handleTranscriptPointerDown, onScroll: handleTranscriptScroll, onTouchMove: revealTranscriptScrollbar, onWheel: revealTranscriptScrollbar, children: _jsx("div", { ref: transcriptContentRef, className: "agentkit-transcript-content", children: _jsxs(AgentKitRegionSlot, { slot: Transcript, threadId: threadId, children: [!hasConversation && EmptyState ? (_jsx(EmptyState, { threadId: threadId })) : null, transcriptItems, approvalEntries.map(([approvalId, request]) => (_jsx(AgentKitSurfaceBoundary, { surface: "approval", resetKey: `${approvalId}:${thread.events.length}`, children: Approval ? (_jsx(Approval, { value: request, threadId: threadId, runId: thread.approvalRunIds[approvalId] ?? "" })) : (_jsx(AgentApprovalPrompt, { request: request, runId: thread.approvalRunIds[approvalId] ?? "" })) }, approvalId))), connectionRequestEntries.map(([requestId, request]) => (_jsx(AgentKitSurfaceBoundary, { surface: "connection-request", resetKey: `${requestId}:${request.status}`, children: ConnectionRequest ? (_jsx(ConnectionRequest, { value: request, threadId: threadId, runId: thread.connectionRequestRunIds[requestId] ?? "" })) : (_jsx(AgentConnectionRequestCard, { request: request, runId: thread.connectionRequestRunIds[requestId] ?? "" })) }, requestId))), connectionError ? (_jsx(AgentKitSurfaceBoundary, { surface: "connection-error", resetKey: `${threadId}:${connectionError.code}`, children: _jsx(ErrorState, { error: connectionError, threadId: threadId, recover: recovery.execute, recovering: recovery.pending, recoveryError: recovery.error }) })) : null] }) }) }), composer ? (_jsx("footer", { className: "agentkit-chat-footer", children: _jsx(AgentKitSurfaceBoundary, { surface: "composer", resetKey: threadId, children: _jsx(AgentKitRegionSlot, { slot: Footer, threadId: threadId, children: slots.composer ? (_jsx(Composer, { threadId: threadId })) : (_jsx(AgentKitComposer, { ...composerProps, threadId: threadId })) }) }) })) : null] }));
1912
+ return (_jsx(AgentMessageEditContext.Provider, { value: messageEditContext, children: _jsxs("section", { "aria-label": typeof title === "string" ? title : labels.conversation, className: `agentkit-chat ${className ?? ""}`, "data-empty": !hasConversation, "data-empty-composer-placement": emptyComposerPlacement, onDragEnter: handleFileDragEnter, onDragOver: handleFileDragOver, onDragLeave: handleFileDragLeave, onDrop: handleFileDrop, children: [fileDragActive ? (_jsx("div", { className: "agentkit-file-drop-overlay", role: "status", children: labels.dropFilesToAttach })) : null, hasConversation && (title || toolbar) ? (_jsx(AgentKitSurfaceBoundary, { surface: "header", resetKey: threadId, children: _jsx(AgentKitRegionSlot, { slot: Header, threadId: threadId, children: _jsxs("header", { className: "agentkit-chat-header", children: [_jsx("div", { className: "agentkit-chat-title", children: title }), _jsx(AgentKitRegionSlot, { slot: Toolbar, threadId: threadId, children: _jsx("div", { className: "agentkit-chat-toolbar", children: toolbar }) })] }) }) })) : null, _jsxs("div", { ref: transcriptRef, className: "agentkit-transcript", "aria-live": "off", "data-scrollbar-visible": transcriptScrollbarVisible, onKeyDown: handleTranscriptKeyDown, onPointerDown: handleTranscriptPointerDown, onScroll: handleTranscriptScroll, onTouchMove: revealTranscriptScrollbar, onWheel: revealTranscriptScrollbar, children: [_jsx("div", { ref: transcriptContentRef, className: "agentkit-transcript-content", children: _jsxs(AgentKitRegionSlot, { slot: Transcript, threadId: threadId, children: [!hasConversation && EmptyState ? (_jsx(EmptyState, { threadId: threadId })) : null, transcriptItems, approvalEntries.map(([approvalId, request]) => (_jsx(AgentKitSurfaceBoundary, { surface: "approval", resetKey: `${approvalId}:${thread.events.length}`, children: Approval ? (_jsx(Approval, { value: request, threadId: threadId, runId: thread.approvalRunIds[approvalId] ?? "" })) : (_jsx(AgentApprovalPrompt, { request: request, runId: thread.approvalRunIds[approvalId] ?? "" })) }, approvalId))), connectionRequestEntries.map(([requestId, request]) => (_jsx(AgentKitSurfaceBoundary, { surface: "connection-request", resetKey: `${requestId}:${request.status}`, children: ConnectionRequest ? (_jsx(ConnectionRequest, { value: request, threadId: threadId, runId: thread.connectionRequestRunIds[requestId] ?? "" })) : (_jsx(AgentConnectionRequestCard, { request: request, runId: thread.connectionRequestRunIds[requestId] ?? "" })) }, requestId))), connectionError ? (_jsx(AgentKitSurfaceBoundary, { surface: "connection-error", resetKey: `${threadId}:${connectionError.code}`, children: _jsx(ErrorState, { error: connectionError, threadId: threadId, recover: recovery.execute, recovering: recovery.pending, recoveryError: recovery.error }) })) : null] }) }), transcriptAwayFromBottom ? (_jsx("div", { className: "agentkit-scroll-to-bottom-control", children: _jsx(IconButton, { label: labels.scrollToBottom, icon: _jsx(IconArrowDown, { "aria-hidden": "true" }), size: "compact", onPress: returnTranscriptToBottom }) })) : null] }), composer ? (_jsx("footer", { className: "agentkit-chat-footer", children: _jsx(AgentKitSurfaceBoundary, { surface: "composer", resetKey: threadId, children: _jsx(AgentKitRegionSlot, { slot: Footer, threadId: threadId, children: slots.composer ? (_jsx(Composer, { threadId: threadId })) : (_jsx(AgentKitComposer, { ...composerProps, threadId: threadId })) }) }) })) : null] }) }));
1374
1913
  }
1375
1914
  //# sourceMappingURL=components.js.map